diff --git a/handlers_test.go b/handlers_test.go index 9d7e6c2..ffc588a 100644 --- a/handlers_test.go +++ b/handlers_test.go @@ -964,12 +964,13 @@ func newTestServer(t *testing.T) Server { ServiceProviderConfig: &ServiceProviderConfig{}, ResourceTypes: []ResourceType{ { - ID: optional.NewString("User"), - Name: "User", - Endpoint: "/Users", - Description: optional.NewString("User Account"), - Schema: userSchema, - Handler: newTestResourceHandler(), + ID: optional.NewString("User"), + Name: "User", + Endpoint: "/Users", + Description: optional.NewString("User Account"), + Schema: userSchema, + Handler: newTestResourceHandler(), + AllowNonScimKeys: true, }, { ID: optional.NewString("EnterpriseUser"), @@ -998,3 +999,91 @@ func newTestServer(t *testing.T) Server { } return s } + +func TestServerResourceHandlerWithCustomAttributes(t *testing.T) { + tests := []struct { + name string + target string + body io.Reader + expectedUserName string + expectedExternalID interface{} + ExpectedCustomAttribute interface{} + }{ + { + name: "Users post With String Custom attribute", + target: "/Users", + body: strings.NewReader(`{"id": "other", "userName": "test1", "externalId": "external_test1","custom_attribute":"test"}`), + expectedUserName: "test1", + expectedExternalID: "external_test1", + ExpectedCustomAttribute: "test", + }, + { + name: "Users post With boolean Custom attribute", + target: "/Users", + body: strings.NewReader(`{"id": "other", "userName": "test1", "externalId": "external_test1","custom_attribute": true}`), + expectedUserName: "test1", + expectedExternalID: "external_test1", + ExpectedCustomAttribute: true, + }, + { + name: "Users post With Object Custom attribute", + target: "/Users", + body: strings.NewReader(`{"id": "other", "userName": "test1", "externalId": "external_test1","custom_attribute":{"test_key":"test_value"}}`), + expectedUserName: "test1", + expectedExternalID: "external_test1", + ExpectedCustomAttribute: map[string]interface{}{"test_key": "test_value"}, + }, + { + name: "Users post With number Custom attribute", + target: "/Users", + body: strings.NewReader(`{"id": "other", "userName": "test1", "externalId": "external_test1","custom_attribute": 1}`), + expectedUserName: "test1", + expectedExternalID: "external_test1", + ExpectedCustomAttribute: float64(1), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodPost, test.target, test.body) + rr := httptest.NewRecorder() + newTestServer(t).ServeHTTP(rr, req) + + assertEqualStatusCode(t, http.StatusCreated, rr.Code) + + assertEqual(t, "application/scim+json", rr.Header().Get("Content-Type")) + + var resource map[string]interface{} + assertUnmarshalNoError(t, json.Unmarshal(rr.Body.Bytes(), &resource)) + + assertEqual(t, test.expectedUserName, resource["userName"]) + assertEqual(t, test.expectedExternalID, resource["externalId"]) + + meta, ok := resource["meta"].(map[string]interface{}) + assertTypeOk(t, ok, "object") + + switch v := test.ExpectedCustomAttribute.(type) { + case string: + assertEqual(t, v, resource["custom_attribute"]) + case map[string]interface{}: + obj, ok := resource["custom_attribute"].(map[string]interface{}) + assertTypeOk(t, ok, "object") + assertEqualMaps(t, v, obj) + case bool: + assertEqual(t, v, resource["custom_attribute"]) + case float64: + assertEqual(t, v, resource["custom_attribute"]) + default: + t.Errorf("Unexpected type %T", v) + } + + assertEqual(t, "User", meta["resourceType"]) + assertNotNil(t, meta["created"], "created") + assertNotNil(t, meta["lastModified"], "last modified") + assertEqual(t, fmt.Sprintf("Users/%s", resource["id"]), meta["location"]) + assertEqual(t, fmt.Sprintf("v%s", resource["id"]), meta["version"]) + // ETag and version needs to be the same. + assertEqual(t, rr.Header().Get("Etag"), meta["version"]) + }) + } +} diff --git a/resource_type.go b/resource_type.go index 8761d7d..a8e60e4 100644 --- a/resource_type.go +++ b/resource_type.go @@ -36,6 +36,9 @@ type ResourceType struct { // Handler is the set of callback method that connect the SCIM server with a provider of the resource type. Handler ResourceHandler + + // AllowNonScimKeys is a flag to allow non scim complaint attributes to be part of the resource type + AllowNonScimKeys bool } func (t ResourceType) getRaw() map[string]interface{} { @@ -113,6 +116,14 @@ func (t ResourceType) validate(raw []byte) (ResourceAttributes, *errors.ScimErro attributes[extension.Schema.ID] = extensionAttributes } + // add all the keys from the original map that are not in the schema + if t.AllowNonScimKeys { + for k, v := range m { + if _, ok := attributes[k]; !ok { + attributes[k] = v + } + } + } return attributes, nil } diff --git a/utils_test.go b/utils_test.go index 24ddd1d..95c7c23 100644 --- a/utils_test.go +++ b/utils_test.go @@ -96,3 +96,20 @@ func getLen(x interface{}) (ok bool, length int) { }() return true, v.Len() } + +func assertEqualMaps(t *testing.T, map1, map2 map[string]interface{}) { + if len(map1) != len(map2) { + t.Errorf("Maps have different lengths: %d != %d", len(map1), len(map2)) + } + + for key, value1 := range map1 { + value2, ok := map2[key] + if !ok { + t.Errorf("Key %s not found in map2", key) + } + + if !reflect.DeepEqual(value1, value2) { + t.Errorf("Values for key %s are different: %v != %v", key, value1, value2) + } + } +}