Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 45 additions & 12 deletions handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,44 +429,52 @@ func TestServerResourcePatchHandlerValidRemoveOp(t *testing.T) {
assertEqualStatusCode(t, http.StatusNoContent, rr.Code)
}

func TestServerResourcePostHandlerMissingSchemas(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/Users", strings.NewReader(`{"userName": "test1"}`))
rr := httptest.NewRecorder()
newTestServer(t).ServeHTTP(rr, req)

assertEqualStatusCode(t, http.StatusBadRequest, rr.Code)
}

func TestServerResourcePostHandlerValid(t *testing.T) {
tests := []struct {
name string
target string
body io.Reader
body string
expectedUserName string
expectedExternalID interface{}
}{
{
name: "Users post request without version",
target: "/Users",
body: strings.NewReader(`{"id": "other", "userName": "test1", "externalId": "external_test1"}`),
body: `{"id": "other", "userName": "test1", "externalId": "external_test1","schemas":["urn:ietf:params:scim:schemas:core:2.0:User"]}`,
expectedUserName: "test1",
expectedExternalID: "external_test1",
}, {
name: "Users post request with version",
target: "/v2/Users",
body: strings.NewReader(`{"id": "other", "userName": "test2", "externalId": "external_test2"}`),
body: `{"id": "other", "userName": "test2", "externalId": "external_test2","schemas":["urn:ietf:params:scim:schemas:core:2.0:User"]}`,
expectedUserName: "test2",
expectedExternalID: "external_test2",
}, {
name: "Users post request without externalId",
target: "/v2/Users",
body: strings.NewReader(`{"id": "other", "userName": "test3"}`),
body: `{"id": "other", "userName": "test3","schemas":["urn:ietf:params:scim:schemas:core:2.0:User"]}`,
expectedUserName: "test3",
expectedExternalID: nil,
}, {
name: "Users post request with immutable attribute",
target: "/v2/Users",
body: strings.NewReader(`{"id": "other", "userName": "test3", "immutableThing": "test"}`),
body: `{"id": "other", "userName": "test3", "immutableThing": "test","schemas":["urn:ietf:params:scim:schemas:core:2.0:User"]}`,
expectedUserName: "test3",
expectedExternalID: nil,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, test.target, test.body)
req := httptest.NewRequest(http.MethodPost, test.target, strings.NewReader(test.body))
rr := httptest.NewRecorder()
newTestServer(t).ServeHTTP(rr, req)

Expand Down Expand Up @@ -495,8 +503,33 @@ func TestServerResourcePostHandlerValid(t *testing.T) {
}
}

func TestServerResourcePostHandlerWithExtension(t *testing.T) {
body := `{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User", "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"],
"userName": "test1",
"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User": {
"employeeNumber": "1234"
}
}`
req := httptest.NewRequest(http.MethodPost, "/EnterpriseUsers", strings.NewReader(body))
rr := httptest.NewRecorder()
newTestServer(t).ServeHTTP(rr, req)

assertEqualStatusCode(t, http.StatusCreated, rr.Code)
}

func TestServerResourcePostHandlerWrongSchema(t *testing.T) {
body := `{"userName": "test1", "schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"]}`
req := httptest.NewRequest(http.MethodPost, "/Users", strings.NewReader(body))
rr := httptest.NewRecorder()
newTestServer(t).ServeHTTP(rr, req)

assertEqualStatusCode(t, http.StatusBadRequest, rr.Code)
}

func TestServerResourcePutHandlerNotFound(t *testing.T) {
req := httptest.NewRequest(http.MethodPut, "/Users/9999", strings.NewReader(`{"userName": "other"}`))
reqBody := `{"userName": "other","schemas":["urn:ietf:params:scim:schemas:core:2.0:User"]}`
req := httptest.NewRequest(http.MethodPut, "/Users/9999", strings.NewReader(reqBody))
rr := httptest.NewRecorder()
newTestServer(t).ServeHTTP(rr, req)

Expand All @@ -520,34 +553,34 @@ func TestServerResourcePutHandlerValid(t *testing.T) {
tests := []struct {
name string
target string
body io.Reader
body string
expectedUserName string
expectedExternalID interface{}
}{
{
name: "Users put request",
target: "/v2/Users/0002",
body: strings.NewReader(`{"id": "other", "userName": "test2", "externalId": "external_test2"}`),
body: `{"id": "other", "userName": "test2", "externalId": "external_test2","schemas":["urn:ietf:params:scim:schemas:core:2.0:User"]}`,
expectedUserName: "test2",
expectedExternalID: "external_test2",
}, {
name: "Users put request without externalId",
target: "/Users/0003",
body: strings.NewReader(`{"id": "other", "userName": "test3"}`),
body: `{"id": "other", "userName": "test3","schemas":["urn:ietf:params:scim:schemas:core:2.0:User"]}`,
expectedUserName: "test3",
expectedExternalID: nil,
}, {
name: "Users put request with immutable attribute",
target: "/Users/0003",
body: strings.NewReader(`{"id": "other", "userName": "test3", "immutableThing": "test"}`),
body: `{"id": "other", "userName": "test3", "immutableThing": "test","schemas":["urn:ietf:params:scim:schemas:core:2.0:User"]}`,
expectedUserName: "test3",
expectedExternalID: nil,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodPut, test.target, test.body)
req := httptest.NewRequest(http.MethodPut, test.target, strings.NewReader(test.body))
rr := httptest.NewRecorder()
newTestServer(t).ServeHTTP(rr, req)

Expand Down
2 changes: 1 addition & 1 deletion resource_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func (t ResourceType) validate(raw []byte) (ResourceAttributes, *errors.ScimErro
continue
}

extensionAttributes, scimErr := extension.Schema.Validate(extensionField)
extensionAttributes, scimErr := extension.Schema.ValidateExtension(extensionField)
if scimErr != nil {
return ResourceAttributes{}, scimErr
}
Expand Down
47 changes: 43 additions & 4 deletions schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,23 @@ func (s Schema) ToMap() map[string]interface{} {
}
}

// Validate validates given resource based on the schema. Does NOT validate mutability.
// Validate validates given resource based on the schema, including the
// "schemas" attribute. Does NOT validate mutability.
// NOTE: only used in POST and PUT requests where attributes MAY be (re)defined.
func (s Schema) Validate(resource interface{}) (map[string]interface{}, *errors.ScimError) {
return s.validate(resource, false)
return s.validate(resource, false, true)
}

// ValidateExtension validates an extension resource without checking the
// "schemas" attribute, since extensions are nested under their schema ID
// and do not carry their own "schemas" array.
func (s Schema) ValidateExtension(resource interface{}) (map[string]interface{}, *errors.ScimError) {
return s.validate(resource, false, false)
}

// ValidateMutability validates given resource based on the schema, including strict immutability checks.
func (s Schema) ValidateMutability(resource interface{}) (map[string]interface{}, *errors.ScimError) {
return s.validate(resource, true)
return s.validate(resource, true, false)
}

// ValidatePatchOperation validates an individual operation and its related value.
Expand Down Expand Up @@ -127,12 +135,18 @@ func (s Schema) getRawAttributes() []map[string]interface{} {
return attributes
}

func (s Schema) validate(resource interface{}, checkMutability bool) (map[string]interface{}, *errors.ScimError) {
func (s Schema) validate(resource interface{}, checkMutability, checkSchemaID bool) (map[string]interface{}, *errors.ScimError) {
core, ok := resource.(map[string]interface{})
if !ok {
return nil, &errors.ScimErrorInvalidSyntax
}

if checkSchemaID {
if err := s.validateSchemaID(core); err != nil {
return nil, err
}
}

attributes := make(map[string]interface{})
for _, attribute := range s.Attributes {
var hit interface{}
Expand Down Expand Up @@ -164,3 +178,28 @@ func (s Schema) validate(resource interface{}, checkMutability bool) (map[string
}
return attributes, nil
}

func (s Schema) validateSchemaID(resource map[string]interface{}) *errors.ScimError {
resourceSchemas, present := resource["schemas"]
if !present {
return &errors.ScimErrorInvalidSyntax
}

resourceSchemasSlice, ok := resourceSchemas.([]interface{})
if !ok {
return &errors.ScimErrorInvalidSyntax
}

var schemaFound bool
for _, v := range resourceSchemasSlice {
if v == s.ID {
schemaFound = true
break
}
}
if !schemaFound {
return &errors.ScimErrorInvalidSyntax
}

return nil
}
Loading