From c419fe5b306a0b462045b792e6c08ae3c06e46ff Mon Sep 17 00:00:00 2001 From: avinash Date: Sat, 7 Dec 2024 17:46:56 +0530 Subject: [PATCH 1/6] add custom attributes support --- resource_type.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/resource_type.go b/resource_type.go index 8761d7d..704601e 100644 --- a/resource_type.go +++ b/resource_type.go @@ -18,6 +18,9 @@ func unmarshal(data []byte, v interface{}) error { return d.Decode(v) } +// AllowNonScimKeys ResourceAttributes will have non scim complaint attributes to support custom attributes from idp +var AllowNonScimKeys = false + // ResourceType specifies the metadata about a resource type. type ResourceType struct { // ID is the resource type's server unique id. This is often the same value as the "name" attribute. @@ -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 AllowNonScimKeys { + for k, v := range m { + if _, ok := attributes[k]; !ok { + attributes[k] = v + } + } + } return attributes, nil } From c1c3848eabeb36ccc93f0a3e97bb8fec3da2e287 Mon Sep 17 00:00:00 2001 From: avinash Date: Sat, 7 Dec 2024 17:47:04 +0530 Subject: [PATCH 2/6] add custom attributes support --- .idea/.gitignore | 8 ++++++++ .idea/vcs.xml | 4 ++++ 2 files changed, 12 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..d843f34 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file From 0f271f873977c10301f391015f0eb0c69a62b64c Mon Sep 17 00:00:00 2001 From: avinash Date: Sun, 8 Dec 2024 15:52:41 +0530 Subject: [PATCH 3/6] Add resource type flag --- resource_type.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resource_type.go b/resource_type.go index 704601e..a8e60e4 100644 --- a/resource_type.go +++ b/resource_type.go @@ -18,9 +18,6 @@ func unmarshal(data []byte, v interface{}) error { return d.Decode(v) } -// AllowNonScimKeys ResourceAttributes will have non scim complaint attributes to support custom attributes from idp -var AllowNonScimKeys = false - // ResourceType specifies the metadata about a resource type. type ResourceType struct { // ID is the resource type's server unique id. This is often the same value as the "name" attribute. @@ -39,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{} { @@ -117,7 +117,7 @@ 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 AllowNonScimKeys { + if t.AllowNonScimKeys { for k, v := range m { if _, ok := attributes[k]; !ok { attributes[k] = v From 91ed2bee0dc24795be0dcd6d9c2cdef91ac08d17 Mon Sep 17 00:00:00 2001 From: avinash Date: Mon, 9 Dec 2024 10:53:38 +0530 Subject: [PATCH 4/6] Add Unit tests --- handlers_test.go | 101 ++++++++++++++++++++++++++++++++++++++++++++--- utils_test.go | 17 ++++++++ 2 files changed, 112 insertions(+), 6 deletions(-) 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/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) + } + } +} From ca20fe6ddde3d11a58514354a274a1c7d003dc7f Mon Sep 17 00:00:00 2001 From: Avinash Date: Mon, 9 Dec 2024 10:56:59 +0530 Subject: [PATCH 5/6] Delete .idea/.gitignore --- .idea/.gitignore | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .idea/.gitignore diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml From ea81811d8909f31195fb343cb7a50c687ec560ec Mon Sep 17 00:00:00 2001 From: Avinash Date: Mon, 9 Dec 2024 10:57:16 +0530 Subject: [PATCH 6/6] Delete .idea/vcs.xml --- .idea/vcs.xml | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .idea/vcs.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index d843f34..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file