Skip to content

Commit ca4304c

Browse files
committed
feat(extensions): validate schema options
Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
1 parent 159b9bf commit ca4304c

5 files changed

Lines changed: 156 additions & 43 deletions

File tree

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,25 @@
11
{
22
"$schema": "https://json-schema.org/draft/2020-12/schema",
33
"$id": "https://github.com/chainloop-dev/chainloop/app/controlplane/extensions/core/dependencytrack/v1/registration-request",
4-
"$ref": "#/$defs/registrationRequest",
5-
"$defs": {
6-
"registrationRequest": {
7-
"properties": {
8-
"instanceURI": {
9-
"type": "string",
10-
"format": "uri",
11-
"description": "The URL of the Dependency-Track instance"
12-
},
13-
"apiKey": {
14-
"type": "string",
15-
"description": "The API key to use for authentication"
16-
},
17-
"port": {
18-
"type": "number"
19-
},
20-
"allowAutoCreate": {
21-
"type": "boolean",
22-
"description": "Support of creating projects on demand"
23-
}
24-
},
25-
"additionalProperties": false,
26-
"type": "object",
27-
"required": ["instanceURI", "apiKey"]
4+
"properties": {
5+
"instanceURI": {
6+
"type": "string",
7+
"format": "uri",
8+
"description": "The URL of the Dependency-Track instance"
9+
},
10+
"apiKey": {
11+
"type": "string",
12+
"description": "The API key to use for authentication"
13+
},
14+
"allowAutoCreate": {
15+
"type": "boolean",
16+
"description": "Support of creating projects on demand"
17+
},
18+
"port": {
19+
"type": "number"
2820
}
29-
}
21+
},
22+
"additionalProperties": false,
23+
"type": "object",
24+
"required": ["instanceURI", "apiKey"]
3025
}

app/controlplane/extensions/sdk/v1/fanout.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -375,8 +375,37 @@ func FromConfig(data Configuration, v any) error {
375375
return json.Unmarshal(data, v)
376376
}
377377

378-
// generate a JSON schema from a struct, see
378+
// generate a flat JSON schema from a struct using https://github.com/invopop/jsonschema
379+
// We've put some limitations on the kind of input structs we support, for example:
380+
// - Nested schemas are not supported
381+
// - Array based properties are not supported
382+
379383
func generateJSONSchema(schema any) ([]byte, error) {
380-
s := jsonschema.Reflect(schema)
384+
if schema == nil {
385+
return nil, fmt.Errorf("schema is nil")
386+
}
387+
388+
r := &jsonschema.Reflector{}
389+
// Set top-level properties flattened
390+
// https://github.com/invopop/jsonschema#expandedstruct
391+
r.ExpandedStruct = true
392+
393+
s := r.Reflect(schema)
394+
395+
// Double check that the schema is valid
396+
// Nested schemas are not supported
397+
if len(s.Definitions) > 0 {
398+
return nil, fmt.Errorf("nested schemas are not supported")
399+
}
400+
401+
// Array based properties are not supported
402+
for _, k := range s.Properties.Keys() {
403+
p, _ := s.Properties.Get(k)
404+
s := p.(*jsonschema.Schema)
405+
if s.Items != nil {
406+
return nil, fmt.Errorf("array based properties are not supported")
407+
}
408+
}
409+
381410
return json.Marshal(s)
382411
}

app/controlplane/extensions/sdk/v1/fanout_test.go

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,13 @@ import (
2626
"github.com/stretchr/testify/require"
2727
)
2828

29+
type schema struct {
30+
TestProperty string `json:"testProperty"`
31+
}
32+
2933
var inputSchema = &sdk.InputSchema{
30-
Registration: struct{ TestProperty string }{TestProperty: "test"},
31-
Attachment: struct{ TestProperty string }{TestProperty: "test"},
34+
Registration: schema{},
35+
Attachment: schema{},
3236
}
3337

3438
func TestNewBaseIntegration(t *testing.T) {
@@ -192,13 +196,13 @@ func TestString(t *testing.T) {
192196
}
193197
}
194198

195-
func TestValidateRegistrationRequest(t *testing.T) {
196-
var schema struct {
197-
Username string `json:"username"`
198-
Email string `json:"email" jsonschema:"format=email"`
199-
Optional int `json:"optional,omitempty"`
200-
}
199+
type registrationSchema struct {
200+
Username string `json:"username"`
201+
Email string `json:"email" jsonschema:"format=email"`
202+
Optional int `json:"optional,omitempty"`
203+
}
201204

205+
func TestValidateRegistrationRequest(t *testing.T) {
202206
testCases := []struct {
203207
name string
204208
input map[string]interface{}
@@ -250,7 +254,7 @@ func TestValidateRegistrationRequest(t *testing.T) {
250254
got, err := sdk.NewFanOut(
251255
&sdk.NewParams{
252256
ID: "ID", Version: "123",
253-
InputSchema: &sdk.InputSchema{Registration: &schema, Attachment: struct{}{}},
257+
InputSchema: &sdk.InputSchema{Registration: &registrationSchema{}, Attachment: &attachmentSchema{}},
254258
}, sdk.WithEnvelope())
255259

256260
require.NoError(t, err)
@@ -267,12 +271,12 @@ func TestValidateRegistrationRequest(t *testing.T) {
267271
}
268272
}
269273

270-
func TestValidateAttachmentRequest(t *testing.T) {
271-
var schema struct {
272-
ProjectID int `json:"projectID,omitempty" jsonschema:"oneof_required=projectID,minLength=1"`
273-
ProjectName string `json:"projectName,omitempty" jsonschema:"oneof_required=projectName,minLength=1"`
274-
}
274+
type attachmentSchema struct {
275+
ProjectID int `json:"projectID,omitempty" jsonschema:"oneof_required=projectID,minLength=1"`
276+
ProjectName string `json:"projectName,omitempty" jsonschema:"oneof_required=projectName,minLength=1"`
277+
}
275278

279+
func TestValidateAttachmentRequest(t *testing.T) {
276280
testCases := []struct {
277281
name string
278282
input map[string]interface{}
@@ -317,7 +321,7 @@ func TestValidateAttachmentRequest(t *testing.T) {
317321
got, err := sdk.NewFanOut(
318322
&sdk.NewParams{
319323
ID: "ID", Version: "123",
320-
InputSchema: &sdk.InputSchema{Registration: struct{}{}, Attachment: &schema},
324+
InputSchema: &sdk.InputSchema{Registration: &registrationSchema{}, Attachment: &attachmentSchema{}},
321325
}, sdk.WithEnvelope())
322326

323327
require.NoError(t, err)
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//
2+
// Copyright 2023 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package sdk
17+
18+
import (
19+
"testing"
20+
21+
"github.com/stretchr/testify/assert"
22+
)
23+
24+
type emptySchema struct {
25+
}
26+
27+
type basicSchema struct {
28+
Foo string `json:"foo"`
29+
}
30+
31+
type arrayBased struct {
32+
FooA []string
33+
}
34+
35+
type nested struct {
36+
Bar string `json:"bar"`
37+
Foo basicSchema
38+
}
39+
40+
func TestGenerateJSONSchema(t *testing.T) {
41+
testCases := []struct {
42+
name string
43+
input interface{}
44+
wantErrMsg string
45+
}{
46+
{
47+
name: "invalid, missing schema",
48+
wantErrMsg: "schema is nil",
49+
},
50+
{
51+
name: "valid empty schema",
52+
input: emptySchema{},
53+
},
54+
{
55+
name: "valid input",
56+
input: basicSchema{},
57+
},
58+
{
59+
name: "invalid, nested",
60+
input: nested{},
61+
wantErrMsg: "nested schemas are not supported",
62+
},
63+
{
64+
name: "invalid, array property",
65+
input: arrayBased{},
66+
wantErrMsg: "array based properties are not supported",
67+
},
68+
}
69+
70+
for _, tc := range testCases {
71+
t.Run(tc.name, func(t *testing.T) {
72+
_, err := generateJSONSchema(tc.input)
73+
if tc.wantErrMsg != "" {
74+
assert.ErrorContains(t, err, tc.wantErrMsg)
75+
} else {
76+
assert.NoError(t, err)
77+
}
78+
})
79+
}
80+
81+
}

app/controlplane/internal/dispatcher/dispatcher_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,11 @@ func (s *dispatcherTestSuite) SetupTest() {
150150
customImplementation := mockedSDK.NewFanOutExtension(s.T())
151151
customImplementation.On("Register", ctx, mock.Anything).Return(&sdk.RegistrationResponse{Configuration: []byte("deadbeef")}, nil)
152152
customImplementation.On("Attach", ctx, mock.Anything).Return(&sdk.AttachmentResponse{Configuration: []byte("deadbeef")}, nil)
153-
fanOutSchemas := &sdk.InputSchema{Registration: struct{ TestProperty string }{}, Attachment: struct{ TestProperty string }{}}
153+
type schema struct {
154+
TestProperty string
155+
}
156+
157+
fanOutSchemas := &sdk.InputSchema{Registration: schema{}, Attachment: schema{}}
154158

155159
b, err := sdk.NewFanOut(
156160
&sdk.NewParams{

0 commit comments

Comments
 (0)