From 61a7b70b5df90ef89487d0344cabb76147b50f6d Mon Sep 17 00:00:00 2001 From: Victor Lyuboslavsky Date: Fri, 4 Apr 2025 11:00:46 -0500 Subject: [PATCH] SCIM integration tests (#27750) For #27287 This PR adds integration tests for SCIM API endpoints as well as some bug fixes found by these tests. # Checklist for submitter - [x] Added/updated automated tests - [x] Manual QA for all new/changed functionality --- cmd/fleetctl/users_test.go | 2 +- .../MDM-end-user-authentication.md | 2 +- ...CIM-integration.md => SCIM-integration.md} | 4 + .../assets/SCIM-Okta-provisioning.png | Bin 0 -> 142268 bytes ee/server/integrationtest/scim/scim_test.go | 1607 +++++++++++++++++ ee/server/integrationtest/scim/suite.go | 44 + ee/server/scim/groups.go | 12 +- ee/server/scim/users.go | 39 +- server/datastore/mysql/scim.go | 25 +- server/datastore/mysql/scim_test.go | 44 +- server/fleet/scim.go | 4 +- .../integrationtest/android/android_test.go | 9 +- .../service/integrationtest/android/suite.go | 59 + server/service/integrationtest/http.go | 8 +- server/service/integrationtest/suite.go | 99 +- server/service/testing_utils.go | 25 +- 16 files changed, 1824 insertions(+), 159 deletions(-) rename docs/Contributing/{MDM-SCIM-integration.md => SCIM-integration.md} (94%) create mode 100644 docs/Contributing/assets/SCIM-Okta-provisioning.png create mode 100644 ee/server/integrationtest/scim/scim_test.go create mode 100644 ee/server/integrationtest/scim/suite.go create mode 100644 server/service/integrationtest/android/suite.go diff --git a/cmd/fleetctl/users_test.go b/cmd/fleetctl/users_test.go index a2a582cb99..b3d2ac86e8 100644 --- a/cmd/fleetctl/users_test.go +++ b/cmd/fleetctl/users_test.go @@ -191,7 +191,7 @@ func TestCreateBulkUsers(t *testing.T) { user15,user15@example.com,false,false,,1:admin user16,user16@example.com,false,false,,1:admin 2:maintainer`) - expectedText := `{"kind":"user_roles","apiVersion":"v1","spec":{"roles":{"admin1@example.com":{"global_role":"admin","teams":null},"user11@example.com":{"global_role":"maintainer","teams":null},"user12@example.com":{"global_role":"observer","teams":null},"user13@example.com":{"global_role":"admin","teams":null},"user14@example.com":{"global_role":null,"teams":[{"team":"","role":"maintainer"}]},"user15@example.com":{"global_role":null,"teams":[{"team":"","role":"admin"}]},"user16@example.com":{"global_role":null,"teams":[{"team":"","role":"admin"},{"team":"","role":"maintainer"}]},"user1@example.com":{"global_role":"observer","teams":null},"user2@example.com":{"global_role":"observer","teams":null}}}} + expectedText := `{"kind":"user_roles","apiVersion":"v1","spec":{"roles":{"admin1@example.com":{"global_role":"admin","teams":null},"user11@example.com":{"global_role":"maintainer","teams":null},"user12@example.com":{"global_role":"observer","teams":null},"user13@example.com":{"global_role":"admin","teams":null},"user14@example.com":{"global_role":null,"teams":[{"team":"","role":"maintainer"}]},"user15@example.com":{"global_role":null,"teams":[{"team":"","role":"admin"}]},"user16@example.com":{"global_role":null,"teams":[{"team":"","role":"admin"},{"team":"","role":"maintainer"}]},"user1@example.com":{"global_role":"maintainer","teams":null},"user2@example.com":{"global_role":"observer","teams":null}}}} ` assert.Equal(t, "", runAppForTest(t, []string{"user", "create-users", "--csv", csvFile})) diff --git a/docs/Contributing/MDM-end-user-authentication.md b/docs/Contributing/MDM-end-user-authentication.md index d3212dd525..f431e111cf 100644 --- a/docs/Contributing/MDM-end-user-authentication.md +++ b/docs/Contributing/MDM-end-user-authentication.md @@ -18,7 +18,7 @@ Create a SAML app in an IdP. ## Description -If the IT admin configured end user authentication, we change the `configuration_web_url` value in the [enrollment JSON profile](https://developer.apple.com/documentation/devicemanagement/profile) to be `{server_url}/api/v1/fleet/mdm/sso`. This page initiates the SSO flow in the setup assistant webview. +If the IT admin configured end user authentication, we change the `configuration_web_url` value in the [enrollment JSON profile](https://developer.apple.com/documentation/devicemanagement/profile) to be `{server_url}/mdm/sso`. This page gets the SAML Request from `{server_url}/api/v1/fleet/mdm/sso` and initiates the SSO flow in the setup assistant web view. `end_user_authentication` setting is global, but `enable_end_user_authentication` is a team setting. diff --git a/docs/Contributing/MDM-SCIM-integration.md b/docs/Contributing/SCIM-integration.md similarity index 94% rename from docs/Contributing/MDM-SCIM-integration.md rename to docs/Contributing/SCIM-integration.md index 3baff570ec..8462aaa7fc 100644 --- a/docs/Contributing/MDM-SCIM-integration.md +++ b/docs/Contributing/SCIM-integration.md @@ -11,6 +11,10 @@ - https://developer.okta.com/docs/guides/scim-provisioning-integration-prepare/main/ +Sample provisioning settings that work. Capabilities can be disabled and attributes can be removed as needed. + +![Okta to Fleet provisioning](./assets/SCIM-Okta-provisioning.png) + ### Testing Okta integration First, create at least one SCIM user: diff --git a/docs/Contributing/assets/SCIM-Okta-provisioning.png b/docs/Contributing/assets/SCIM-Okta-provisioning.png new file mode 100644 index 0000000000000000000000000000000000000000..fbac152cfe9095fd9c0100feb8229c09d1b45f6a GIT binary patch literal 142268 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 0 { + user, ok := caseInsensitiveResources[0].(map[string]interface{}) + assert.True(t, ok, "User should be an object") + assert.Equal(t, userID, user["id"], "Should be the same user despite case differences in userName filter") + assert.Equal(t, "testuser@example.com", user["userName"], "Original userName should be preserved") + } + + // Test filtering users by non-existent userName + filterResp = nil + s.DoJSON(t, "GET", scimPath("/Users"), nil, http.StatusOK, &filterResp, "filter", `userName eq "bozo@example.com"`) + assert.EqualValues(t, filterResp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:ListResponse"}) + filterResources, ok = filterResp["Resources"].([]interface{}) + assert.True(t, ok, "Resources should be an array") + assert.Empty(t, filterResources, "Should have no users matching the filter") + + // Test updating a user + updateUserPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, + "userName": "testuser@example.com", + "name": map[string]interface{}{ + "givenName": "Updated", + "familyName": "User", + }, + "emails": []map[string]interface{}{ + { + "value": "testuser@example.com", + "type": "work", + "primary": true, + }, + }, + "active": true, + } + + var updateResp map[string]interface{} + s.DoJSON(t, "PUT", scimPath("/Users/"+userID), updateUserPayload, http.StatusOK, &updateResp) + assert.Equal(t, "testuser@example.com", updateResp["userName"]) + + // Verify the name was updated + name, ok := updateResp["name"].(map[string]interface{}) + assert.True(t, ok, "Name should be an object") + assert.Equal(t, "Updated", name["givenName"]) + + // Test patching a user (updating just the active status) + patchUserPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:api:messages:2.0:PatchOp"}, + "Operations": []map[string]interface{}{ + { + "op": "replace", + "path": "active", + "value": false, + }, + }, + } + + var patchResp map[string]interface{} + s.DoJSON(t, "PATCH", scimPath("/Users/"+userID), patchUserPayload, http.StatusOK, &patchResp) + assert.Equal(t, false, patchResp["active"]) + + // Test patching a user without path attribute (updating just the active status) + patchUserPayload = map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:api:messages:2.0:PatchOp"}, + "Operations": []map[string]interface{}{ + { + "op": "replace", + "value": map[string]interface{}{ + "active": true, + }, + }, + }, + } + + patchResp = nil + s.DoJSON(t, "PATCH", scimPath("/Users/"+userID), patchUserPayload, http.StatusOK, &patchResp) + assert.Equal(t, true, patchResp["active"]) + + // Test deleting a user + s.Do(t, "DELETE", scimPath("/Users/"+userID), nil, http.StatusNoContent) + + // Verify the user was deleted by trying to get it (should return 404) + var errorResp map[string]interface{} + s.DoJSON(t, "GET", scimPath("/Users/"+userID), nil, http.StatusNotFound, &errorResp) + assert.EqualValues(t, errorResp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:Error"}) + assert.Contains(t, errorResp["detail"], "not found") + + // Test replacing a user that doesn't exist + nonExistentUserID := "99999" + updateNonExistentUserPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, + "userName": "nonexistent@example.com", + "name": map[string]interface{}{ + "givenName": "Non", + "familyName": "Existent", + }, + "emails": []map[string]interface{}{ + { + "value": "nonexistent@example.com", + "type": "work", + "primary": true, + }, + }, + "active": true, + } + + var updateNonExistentResp map[string]interface{} + s.DoJSON(t, "PUT", scimPath("/Users/"+nonExistentUserID), updateNonExistentUserPayload, http.StatusNotFound, &updateNonExistentResp) + assert.EqualValues(t, updateNonExistentResp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:Error"}) + assert.Contains(t, updateNonExistentResp["detail"], "not found") + + // Test deleting a user that was already deleted + var deleteAgainResp map[string]interface{} + s.DoJSON(t, "DELETE", scimPath("/Users/"+userID), nil, http.StatusNotFound, &deleteAgainResp) + assert.EqualValues(t, deleteAgainResp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:Error"}) + assert.Contains(t, deleteAgainResp["detail"], "not found") +} + +func testGroupsBasicCRUD(t *testing.T, s *Suite) { + // First, create a user to add as a member of the group + createUserPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, + "userName": "groupmember@example.com", + "name": map[string]interface{}{ + "givenName": "Group", + "familyName": "Member", + }, + "emails": []map[string]interface{}{ + { + "value": "groupmember@example.com", + "type": "work", + "primary": true, + }, + }, + "active": true, + } + + var createUserResp map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Users"), createUserPayload, http.StatusCreated, &createUserResp) + userID := createUserResp["id"].(string) + assert.NotEmpty(t, userID) + + // Test creating a group + createGroupPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:Group"}, + "displayName": "Test Group", + "members": []map[string]interface{}{ + { + "value": userID, + }, + }, + } + + var createResp map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Groups"), createGroupPayload, http.StatusCreated, &createResp) + + // Verify the created group + assert.Equal(t, "Test Group", createResp["displayName"]) + + // Verify members + members, ok := createResp["members"].([]interface{}) + assert.True(t, ok, "Members should be an array") + require.Equal(t, 1, len(members), "Should have 1 member") + member := members[0].(map[string]interface{}) + assert.Equal(t, userID, member["value"]) + assert.Equal(t, "User", member["type"]) + assert.Equal(t, "Users/"+userID, member["$ref"]) + + // Extract the group ID for subsequent operations + groupID := createResp["id"].(string) + assert.NotEmpty(t, groupID) + + // Test getting a group by ID + var getResp map[string]interface{} + s.DoJSON(t, "GET", scimPath("/Groups/"+groupID), nil, http.StatusOK, &getResp) + assert.Equal(t, groupID, getResp["id"]) + assert.Equal(t, "Test Group", getResp["displayName"]) + + // Verify members in the GET response + getMembers, ok := getResp["members"].([]interface{}) + assert.True(t, ok, "Members should be an array") + assert.Equal(t, 1, len(getMembers), "Should have 1 member") + getMember := getMembers[0].(map[string]interface{}) + assert.Equal(t, userID, getMember["value"]) + + // Test getting a group with a bad ID + var errResp map[string]interface{} + s.DoJSON(t, "GET", scimPath("/Groups/99999"), nil, http.StatusNotFound, &errResp) + assert.Contains(t, errResp["detail"], "Resource 99999 not found") + assert.EqualValues(t, errResp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:Error"}) + + // Test listing groups + var listResp map[string]interface{} + s.DoJSON(t, "GET", scimPath("/Groups"), nil, http.StatusOK, &listResp) + assert.EqualValues(t, listResp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:ListResponse"}) + resources, ok := listResp["Resources"].([]interface{}) + assert.True(t, ok, "Resources should be an array") + assert.GreaterOrEqual(t, len(resources), 1, "Should have at least 1 group") + + // Test updating a group + updateGroupPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:Group"}, + "displayName": "Updated Test Group", + "members": []map[string]interface{}{}, // Remove all members + } + + var updateResp map[string]interface{} + s.DoJSON(t, "PUT", scimPath("/Groups/"+groupID), updateGroupPayload, http.StatusOK, &updateResp) + assert.Equal(t, "Updated Test Group", updateResp["displayName"]) + + // Verify members were removed + _, membersExist := updateResp["members"] + assert.False(t, membersExist, "Members should not be present or should be empty") + + // Test patching a group (updating just the displayName) + patchGroupPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:api:messages:2.0:PatchOp"}, + "Operations": []map[string]interface{}{ + { + "op": "replace", + "path": "displayName", + "value": "Patched Test Group", + }, + }, + } + + var patchResp map[string]interface{} + s.DoJSON(t, "PATCH", scimPath("/Groups/"+groupID), patchGroupPayload, http.StatusOK, &patchResp) + assert.Equal(t, "Patched Test Group", patchResp["displayName"]) + + // Test patching a group without path attribute (updating just the displayName) + patchGroupPayload = map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:api:messages:2.0:PatchOp"}, + "Operations": []map[string]interface{}{ + { + "op": "replace", + "value": map[string]interface{}{ + "displayName": "Patched Again Test Group", + }, + }, + }, + } + + patchResp = nil + s.DoJSON(t, "PATCH", scimPath("/Groups/"+groupID), patchGroupPayload, http.StatusOK, &patchResp) + assert.Equal(t, "Patched Again Test Group", patchResp["displayName"]) + + // Test deleting a group + s.Do(t, "DELETE", scimPath("/Groups/"+groupID), nil, http.StatusNoContent) + + // Verify the group was deleted by trying to get it (should return 404) + var errorResp map[string]interface{} + s.DoJSON(t, "GET", scimPath("/Groups/"+groupID), nil, http.StatusNotFound, &errorResp) + assert.EqualValues(t, errorResp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:Error"}) + assert.Contains(t, errorResp["detail"], "not found") + + // Test replacing a group that doesn't exist + nonExistentGroupID := "99999" + updateNonExistentGroupPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:Group"}, + "displayName": "Non-Existent Group", + "members": []map[string]interface{}{}, + } + + var updateNonExistentResp map[string]interface{} + s.DoJSON(t, "PUT", scimPath("/Groups/"+nonExistentGroupID), updateNonExistentGroupPayload, http.StatusNotFound, &updateNonExistentResp) + assert.EqualValues(t, updateNonExistentResp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:Error"}) + assert.Contains(t, updateNonExistentResp["detail"], "not found") + + // Test deleting a group that was already deleted + var deleteAgainResp map[string]interface{} + s.DoJSON(t, "DELETE", scimPath("/Groups/"+groupID), nil, http.StatusNotFound, &deleteAgainResp) + assert.EqualValues(t, deleteAgainResp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:Error"}) + assert.Contains(t, deleteAgainResp["detail"], "not found") + + // Clean up the user we created + s.Do(t, "DELETE", scimPath("/Users/"+userID), nil, http.StatusNoContent) +} + +func testCreateGroup(t *testing.T, s *Suite) { + // Create multiple test users to be added as members + userIDs := make([]string, 0, 5) + + for i := 1; i <= 5; i++ { + userName := fmt.Sprintf("group-test-user-%d@example.com", i) + createUserPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, + "userName": userName, + "name": map[string]interface{}{ + "givenName": fmt.Sprintf("User%d", i), + "familyName": "GroupTest", + }, + "emails": []map[string]interface{}{ + { + "value": userName, + "type": "work", + "primary": true, + }, + }, + "active": true, + } + + var createResp map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Users"), createUserPayload, http.StatusCreated, &createResp) + userID := createResp["id"].(string) + userIDs = append(userIDs, userID) + } + + // Test 1: Create a group with 0 members + emptyGroupPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:Group"}, + "displayName": "Empty Group", + } + + var emptyGroupResp map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Groups"), emptyGroupPayload, http.StatusCreated, &emptyGroupResp) + + // Verify the created group + assert.Equal(t, "Empty Group", emptyGroupResp["displayName"]) + + // Verify no members + _, membersExist := emptyGroupResp["members"] + assert.False(t, membersExist, "Members should not be present for an empty group") + + emptyGroupID := emptyGroupResp["id"].(string) + assert.NotEmpty(t, emptyGroupID) + + // Test 2: Create a group with many members + members := make([]map[string]interface{}, 0, len(userIDs)) + for _, userID := range userIDs { + members = append(members, map[string]interface{}{ + "value": userID, + }) + } + + manyMembersGroupPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:Group"}, + "displayName": "Many Members Group", + "members": members, + } + + var manyMembersGroupResp map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Groups"), manyMembersGroupPayload, http.StatusCreated, &manyMembersGroupResp) + + // Verify the created group + assert.Equal(t, "Many Members Group", manyMembersGroupResp["displayName"]) + + // Verify members + respMembers, ok := manyMembersGroupResp["members"].([]interface{}) + assert.True(t, ok, "Members should be an array") + assert.Equal(t, len(userIDs), len(respMembers), "Should have the same number of members as we added") + + // Verify each member is in the response + memberValues := make([]string, 0, len(respMembers)) + for _, member := range respMembers { + memberMap, ok := member.(map[string]interface{}) + assert.True(t, ok, "Member should be an object") + memberValues = append(memberValues, memberMap["value"].(string)) + assert.Equal(t, "User", memberMap["type"]) + assert.Contains(t, memberMap["$ref"], "Users/") + } + + for _, userID := range userIDs { + assert.Contains(t, memberValues, userID, "User ID should be in the members list") + } + + manyMembersGroupID := manyMembersGroupResp["id"].(string) + assert.NotEmpty(t, manyMembersGroupID) + + // Test 3: Create a group with externalId + externalIDGroupPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:Group"}, + "displayName": "External ID Group", + "externalId": "external-system-group-789", + "members": []map[string]interface{}{ + { + "value": userIDs[0], // Just add the first user as a member + }, + }, + } + + var externalIDGroupResp map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Groups"), externalIDGroupPayload, http.StatusCreated, &externalIDGroupResp) + + // Verify the created group + assert.Equal(t, "External ID Group", externalIDGroupResp["displayName"]) + assert.Equal(t, "external-system-group-789", externalIDGroupResp["externalId"]) + + // Verify members + externalIDGroupMembers, ok := externalIDGroupResp["members"].([]interface{}) + assert.True(t, ok, "Members should be an array") + assert.Equal(t, 1, len(externalIDGroupMembers), "Should have 1 member") + + externalIDGroupID := externalIDGroupResp["id"].(string) + assert.NotEmpty(t, externalIDGroupID) + + // Test 4: Try to create a group with the same display name (should fail) + duplicateGroupPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:Group"}, + "displayName": "Empty Group", // Same as the first group + } + + var errorResp map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Groups"), duplicateGroupPayload, http.StatusConflict, &errorResp) + + // Verify error response + assert.EqualValues(t, errorResp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:Error"}) + assert.Contains(t, errorResp["detail"], "One or more of the attribute values are already in use or are reserved") + + // Test 4: Try to create a group without displayName (should fail) + noDisplayNamePayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:Group"}, + // No displayName + } + + errorResp = nil + s.DoJSON(t, "POST", scimPath("/Groups"), noDisplayNamePayload, http.StatusBadRequest, &errorResp) + + // Verify error response + assert.EqualValues(t, errorResp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:Error"}) + assert.Contains(t, errorResp["detail"], "A required value was missing") + + // Test 5: Try to create a group with invalid member ID (should fail) + invalidMemberPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:Group"}, + "displayName": "Invalid Member Group", + "members": []map[string]interface{}{ + { + "value": "invalid-user-id", + }, + }, + } + + errorResp = nil + s.DoJSON(t, "POST", scimPath("/Groups"), invalidMemberPayload, http.StatusBadRequest, &errorResp) + + // Verify error response + assert.EqualValues(t, errorResp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:Error"}) + + // Clean up + // Delete the groups + s.Do(t, "DELETE", scimPath("/Groups/"+emptyGroupID), nil, http.StatusNoContent) + s.Do(t, "DELETE", scimPath("/Groups/"+manyMembersGroupID), nil, http.StatusNoContent) + s.Do(t, "DELETE", scimPath("/Groups/"+externalIDGroupID), nil, http.StatusNoContent) + + // Delete the users + for _, userID := range userIDs { + s.Do(t, "DELETE", scimPath("/Users/"+userID), nil, http.StatusNoContent) + } +} + +func testCreateUser(t *testing.T, s *Suite) { + // Test creating a user without givenName + userWithoutGivenName := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, + "userName": "no-given-name@example.com", + "name": map[string]interface{}{ + "familyName": "NoGivenName", + }, + "emails": []map[string]interface{}{ + { + "value": "no-given-name@example.com", + "type": "work", + "primary": true, + }, + }, + "active": true, + } + + var createResp1 map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Users"), userWithoutGivenName, http.StatusCreated, &createResp1) + assert.Equal(t, "no-given-name@example.com", createResp1["userName"]) + userID1 := createResp1["id"].(string) + + // Verify name only has familyName + name1, ok := createResp1["name"].(map[string]interface{}) + assert.True(t, ok, "Name should be an object") + assert.Equal(t, "NoGivenName", name1["familyName"]) + assert.Nil(t, name1["givenName"], "givenName should be nil") + + // Test creating a user without familyName + userWithoutFamilyName := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, + "userName": "no-family-name@example.com", + "name": map[string]interface{}{ + "givenName": "NoFamilyName", + }, + "emails": []map[string]interface{}{ + { + "value": "no-family-name@example.com", + "type": "work", + "primary": true, + }, + }, + "active": true, + } + + var createResp2 map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Users"), userWithoutFamilyName, http.StatusCreated, &createResp2) + assert.Equal(t, "no-family-name@example.com", createResp2["userName"]) + userID2 := createResp2["id"].(string) + + // Verify name only has givenName + name2, ok := createResp2["name"].(map[string]interface{}) + assert.True(t, ok, "Name should be an object") + assert.Equal(t, "NoFamilyName", name2["givenName"]) + assert.Nil(t, name2["familyName"], "familyName should be nil") + + // Test creating a user without emails + userWithoutEmails := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, + "userName": "no-emails@example.com", + "name": map[string]interface{}{ + "givenName": "No", + "familyName": "Emails", + }, + "active": true, + } + + var createResp3 map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Users"), userWithoutEmails, http.StatusCreated, &createResp3) + assert.Equal(t, "no-emails@example.com", createResp3["userName"]) + userID3 := createResp3["id"].(string) + + // Verify emails is not present or empty + _, hasEmails := createResp3["emails"] + assert.False(t, hasEmails, "emails should not be present") + + // Test creating a user without active status + userWithoutActive := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, + "userName": "no-active@example.com", + "name": map[string]interface{}{ + "givenName": "No", + "familyName": "Active", + }, + "emails": []map[string]interface{}{ + { + "value": "no-active@example.com", + "type": "work", + "primary": true, + }, + }, + } + + var createResp4 map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Users"), userWithoutActive, http.StatusCreated, &createResp4) + assert.Equal(t, "no-active@example.com", createResp4["userName"]) + userID4 := createResp4["id"].(string) + + // Verify active is not present or nil + _, hasActive := createResp4["active"] + assert.False(t, hasActive, "active should not be present") + + // Test creating a user with multiple emails + userWithMultipleEmails := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, + "userName": "multiple-emails@example.com", + "name": map[string]interface{}{ + "givenName": "Multiple", + "familyName": "Emails", + }, + "emails": []map[string]interface{}{ + { + "value": "multiple-emails@example.com", + "type": "work", + "primary": true, + }, + { + "value": "multiple-emails-home@example.com", + "type": "home", + "primary": false, + }, + { + "value": "multiple-emails-other@example.com", + "type": "other", + "primary": false, + }, + }, + "active": true, + } + + var createResp5 map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Users"), userWithMultipleEmails, http.StatusCreated, &createResp5) + assert.Equal(t, "multiple-emails@example.com", createResp5["userName"]) + userID5 := createResp5["id"].(string) + + // Verify multiple emails are present + emails, ok := createResp5["emails"].([]interface{}) + assert.True(t, ok, "Emails should be an array") + assert.Equal(t, 3, len(emails), "Should have 3 emails") + + // Test creating a user with empty userName + userWithEmptyUserName := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, + "userName": "", // Empty userName + "name": map[string]interface{}{ + "givenName": "Empty", + "familyName": "UserName", + }, + "emails": []map[string]interface{}{ + { + "value": "empty-username@example.com", + "type": "work", + "primary": true, + }, + }, + "active": true, + } + + var errorResp1 map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Users"), userWithEmptyUserName, http.StatusBadRequest, &errorResp1) + + // Verify error response + assert.EqualValues(t, errorResp1["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:Error"}) + assert.Contains(t, errorResp1["detail"], "Bad Request") + + // Test creating a user with duplicate userName + duplicateUserNamePayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, + "userName": "multiple-emails@example.com", // Same as userWithMultipleEmails + "name": map[string]interface{}{ + "givenName": "Duplicate", + "familyName": "UserName", + }, + "emails": []map[string]interface{}{ + { + "value": "duplicate@example.com", + "type": "work", + "primary": true, + }, + }, + "active": true, + } + + var errorResp2 map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Users"), duplicateUserNamePayload, http.StatusConflict, &errorResp2) + + // Verify error response + assert.EqualValues(t, errorResp2["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:Error"}) + assert.Contains(t, errorResp2["detail"], "One or more of the attribute values are already in use or are reserved") + + // Test creating a user with duplicate userName using different case. + // userName must be case insensitive + duplicateUserNamePayload = map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, + "userName": "Multiple-Emails@example.com", // Same as userWithMultipleEmails + "name": map[string]interface{}{ + "givenName": "Duplicate", + "familyName": "UserName", + }, + "emails": []map[string]interface{}{ + { + "value": "duplicate@example.com", + "type": "work", + "primary": true, + }, + }, + "active": true, + } + + var errorResp3 map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Users"), duplicateUserNamePayload, http.StatusConflict, &errorResp3) + + // Verify error response + assert.EqualValues(t, errorResp3["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:Error"}) + assert.Contains(t, errorResp3["detail"], "One or more of the attribute values are already in use or are reserved") + + // Test creating a user with externalId + userWithExternalID := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, + "userName": "external-id-user@example.com", + "externalId": "external-system-123456", + "name": map[string]interface{}{ + "givenName": "External", + "familyName": "IDUser", + }, + "emails": []map[string]interface{}{ + { + "value": "external-id-user@example.com", + "type": "work", + "primary": true, + }, + }, + "active": true, + } + + var createResp6 map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Users"), userWithExternalID, http.StatusCreated, &createResp6) + assert.Equal(t, "external-id-user@example.com", createResp6["userName"]) + userID6 := createResp6["id"].(string) + + // Verify externalId is present and correct + assert.Equal(t, "external-system-123456", createResp6["externalId"]) + + // Make sure these users can be deleted. + s.Do(t, "DELETE", scimPath("/Users/"+userID1), nil, http.StatusNoContent) + s.Do(t, "DELETE", scimPath("/Users/"+userID2), nil, http.StatusNoContent) + s.Do(t, "DELETE", scimPath("/Users/"+userID3), nil, http.StatusNoContent) + s.Do(t, "DELETE", scimPath("/Users/"+userID4), nil, http.StatusNoContent) + s.Do(t, "DELETE", scimPath("/Users/"+userID5), nil, http.StatusNoContent) + s.Do(t, "DELETE", scimPath("/Users/"+userID6), nil, http.StatusNoContent) +} + +func testPatchUserFailure(t *testing.T, s *Suite) { + // First create a user to patch + createUserPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, + "userName": "patch-test@example.com", + "name": map[string]interface{}{ + "givenName": "Patch", + "familyName": "Test", + }, + "emails": []map[string]interface{}{ + { + "value": "patch-test@example.com", + "type": "work", + "primary": true, + }, + }, + "active": true, + } + + var createResp map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Users"), createUserPayload, http.StatusCreated, &createResp) + userID := createResp["id"].(string) + + // Test 1: Patch with unsupported operation (add instead of replace) + unsupportedOpPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:api:messages:2.0:PatchOp"}, + "Operations": []map[string]interface{}{ + { + "op": "add", // Only "replace" is supported + "path": "active", + "value": false, + }, + }, + } + + var errorResp1 map[string]interface{} + s.DoJSON(t, "PATCH", scimPath("/Users/"+userID), unsupportedOpPayload, http.StatusBadRequest, &errorResp1) + assert.EqualValues(t, errorResp1["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:Error"}) + assert.Contains(t, errorResp1["detail"], "Bad Request.") + + // Test 2: Patch with unsupported field (userName instead of active) + unsupportedFieldPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:api:messages:2.0:PatchOp"}, + "Operations": []map[string]interface{}{ + { + "op": "replace", + "path": "userName", // Only "active" is supported + "value": "new-username@example.com", + }, + }, + } + + var errorResp2 map[string]interface{} + s.DoJSON(t, "PATCH", scimPath("/Users/"+userID), unsupportedFieldPayload, http.StatusBadRequest, &errorResp2) + assert.EqualValues(t, errorResp2["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:Error"}) + assert.Contains(t, errorResp2["detail"], "Bad Request.") + + // Test 3: Patch with no path and invalid value format + invalidValuePayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:api:messages:2.0:PatchOp"}, + "Operations": []map[string]interface{}{ + { + "op": "replace", + // No path specified + "value": "not-a-map", // Should be a map with "active" key + }, + }, + } + + var errorResp3 map[string]interface{} + s.DoJSON(t, "PATCH", scimPath("/Users/"+userID), invalidValuePayload, http.StatusBadRequest, &errorResp3) + assert.EqualValues(t, errorResp3["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:Error"}) + assert.Contains(t, errorResp3["detail"], "A required value was missing") + + // Test 4 A: Patch with multiple operations (only one is supported) + multipleOpsPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:api:messages:2.0:PatchOp"}, + "Operations": []map[string]interface{}{ + { + "op": "replace", + "path": "active", + "value": false, + }, + { + "op": "replace", + "path": "active", + "value": true, + }, + }, + } + + var errorResp4 map[string]interface{} + s.DoJSON(t, "PATCH", scimPath("/Users/"+userID), multipleOpsPayload, http.StatusBadRequest, &errorResp4) + assert.EqualValues(t, errorResp4["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:Error"}) + assert.Contains(t, errorResp4["detail"], "Bad Request. Invalid parameter provided in request: Operations") + + // Test 4 B: Patch with multiple values, only one is supported + multipleValuesPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:api:messages:2.0:PatchOp"}, + "Operations": []map[string]interface{}{ + { + "op": "replace", + "value": map[string]interface{}{ + "active": false, + "name": map[string]interface{}{ + "givenName": "Updated", + }, + }, + }, + }, + } + + var errorResp4B map[string]interface{} + s.DoJSON(t, "PATCH", scimPath("/Users/"+userID), multipleValuesPayload, http.StatusBadRequest, &errorResp4B) + assert.EqualValues(t, errorResp4B["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:Error"}) + assert.Contains(t, errorResp4B["detail"], "Bad Request") + + // Test 5: Patch with wrong value type for active + wrongTypePayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:api:messages:2.0:PatchOp"}, + "Operations": []map[string]interface{}{ + { + "op": "replace", + "path": "active", + "value": "not-a-boolean", // Should be a boolean + }, + }, + } + + var errorResp5 map[string]interface{} + s.DoJSON(t, "PATCH", scimPath("/Users/"+userID), wrongTypePayload, http.StatusBadRequest, &errorResp5) + assert.EqualValues(t, errorResp5["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:Error"}) + assert.Contains(t, errorResp5["detail"], "A required value was missing") + + // Clean up the created user + s.Do(t, "DELETE", scimPath("/Users/"+userID), nil, http.StatusNoContent) +} + +func testUsersPagination(t *testing.T, s *Suite) { + // Create multiple users for pagination testing + userIDs := make([]string, 0, 10) + + for i := 1; i <= 10; i++ { + userName := fmt.Sprintf("pagination-user-%d@example.com", i) + createUserPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, + "userName": userName, + "name": map[string]interface{}{ + "givenName": fmt.Sprintf("User%d", i), + "familyName": "Pagination", + }, + "emails": []map[string]interface{}{ + { + "value": userName, + "type": "work", + "primary": true, + }, + }, + "active": true, + } + + var createResp map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Users"), createUserPayload, http.StatusCreated, &createResp) + userID := createResp["id"].(string) + userIDs = append(userIDs, userID) + } + + // Test 1: Get first page with 3 users per page + var page1Resp map[string]interface{} + s.DoJSON(t, "GET", scimPath("/Users"), nil, http.StatusOK, &page1Resp, "startIndex", "1", "count", "3") + + // Verify response structure + assert.EqualValues(t, page1Resp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:ListResponse"}) + assert.Equal(t, float64(10), page1Resp["totalResults"], "Total results should be 10") + + // Verify resources + resources1, ok := page1Resp["Resources"].([]interface{}) + assert.True(t, ok, "Resources should be an array") + assert.Equal(t, 3, len(resources1), "First page should have 3 users") + + // Verify the users on the first page + userNames1 := make([]string, 0, 3) + for _, resource := range resources1 { + user, ok := resource.(map[string]interface{}) + assert.True(t, ok, "User should be an object") + userName, ok := user["userName"].(string) + assert.True(t, ok, "userName should be a string") + userNames1 = append(userNames1, userName) + } + assert.Contains(t, userNames1, "pagination-user-1@example.com", "First page should contain user 1") + assert.Contains(t, userNames1, "pagination-user-2@example.com", "First page should contain user 2") + assert.Contains(t, userNames1, "pagination-user-3@example.com", "First page should contain user 3") + + // Test 2: Get second page with 3 users per page + var page2Resp map[string]interface{} + s.DoJSON(t, "GET", scimPath("/Users"), nil, http.StatusOK, &page2Resp, "startIndex", "4", "count", "3") + + // Verify response structure + assert.EqualValues(t, page2Resp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:ListResponse"}) + assert.Equal(t, float64(10), page2Resp["totalResults"], "Total results should be 10") + + // Verify resources + resources2, ok := page2Resp["Resources"].([]interface{}) + assert.True(t, ok, "Resources should be an array") + assert.Equal(t, 3, len(resources2), "Second page should have 3 users") + + // Verify the users on the second page + userNames2 := make([]string, 0, 3) + for _, resource := range resources2 { + user, ok := resource.(map[string]interface{}) + assert.True(t, ok, "User should be an object") + userName, ok := user["userName"].(string) + assert.True(t, ok, "userName should be a string") + userNames2 = append(userNames2, userName) + } + assert.Contains(t, userNames2, "pagination-user-4@example.com", "Second page should contain user 4") + assert.Contains(t, userNames2, "pagination-user-5@example.com", "Second page should contain user 5") + assert.Contains(t, userNames2, "pagination-user-6@example.com", "Second page should contain user 6") + + // Test 3: Get third page with 3 users per page + var page3Resp map[string]interface{} + s.DoJSON(t, "GET", scimPath("/Users"), nil, http.StatusOK, &page3Resp, "startIndex", "7", "count", "3") + + // Verify response structure + assert.EqualValues(t, page3Resp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:ListResponse"}) + assert.Equal(t, float64(10), page3Resp["totalResults"], "Total results should be 10") + + // Verify resources + resources3, ok := page3Resp["Resources"].([]interface{}) + assert.True(t, ok, "Resources should be an array") + assert.Equal(t, 3, len(resources3), "Third page should have 3 users") + + // Verify the users on the third page + userNames3 := make([]string, 0, 3) + for _, resource := range resources3 { + user, ok := resource.(map[string]interface{}) + assert.True(t, ok, "User should be an object") + userName, ok := user["userName"].(string) + assert.True(t, ok, "userName should be a string") + userNames3 = append(userNames3, userName) + } + assert.Contains(t, userNames3, "pagination-user-7@example.com", "Third page should contain user 7") + assert.Contains(t, userNames3, "pagination-user-8@example.com", "Third page should contain user 8") + assert.Contains(t, userNames3, "pagination-user-9@example.com", "Third page should contain user 9") + + // Test 4: Get fourth page with 3 users per page (should contain only 1 user) + var page4Resp map[string]interface{} + s.DoJSON(t, "GET", scimPath("/Users"), nil, http.StatusOK, &page4Resp, "startIndex", "10", "count", "3") + + // Verify response structure + assert.EqualValues(t, page4Resp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:ListResponse"}) + assert.Equal(t, float64(10), page4Resp["totalResults"], "Total results should be 10") + + // Verify resources + resources4, ok := page4Resp["Resources"].([]interface{}) + assert.True(t, ok, "Resources should be an array") + require.Len(t, resources4, 1, "Fourth page should have 1 user") + + // Verify the user on the fourth page + user4, ok := resources4[0].(map[string]interface{}) + assert.True(t, ok, "User should be an object") + userName4, ok := user4["userName"].(string) + assert.True(t, ok, "userName should be a string") + assert.Equal(t, "pagination-user-10@example.com", userName4, "Fourth page should contain user 10") + + // Test 5: Get page with startIndex beyond the total results (should return empty resources) + var emptyPageResp map[string]interface{} + s.DoJSON(t, "GET", scimPath("/Users"), nil, http.StatusOK, &emptyPageResp, "startIndex", "11", "count", "3") + + // Verify response structure + assert.EqualValues(t, emptyPageResp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:ListResponse"}) + assert.Equal(t, float64(10), emptyPageResp["totalResults"], "Total results should be 10") + + // Verify resources + emptyResources, ok := emptyPageResp["Resources"].([]interface{}) + assert.True(t, ok, "Resources should be an array") + assert.Empty(t, emptyResources, "Page beyond total results should have 0 users") + + // Test 6: Get all users in a single page + var allUsersResp map[string]interface{} + s.DoJSON(t, "GET", scimPath("/Users"), nil, http.StatusOK, &allUsersResp, "count", "20") + + // Verify response structure + assert.EqualValues(t, allUsersResp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:ListResponse"}) + assert.Equal(t, float64(10), allUsersResp["totalResults"], "Total results should be 10") + + // Verify resources + allResources, ok := allUsersResp["Resources"].([]interface{}) + assert.True(t, ok, "Resources should be an array") + assert.Equal(t, 10, len(allResources), "All users page should have 10 users") + + // Clean up all created users + for _, userID := range userIDs { + s.Do(t, "DELETE", scimPath("/Users/"+userID), nil, http.StatusNoContent) + } +} + +func testGroupsPagination(t *testing.T, s *Suite) { + // First, create a user to be added as a member of some groups + createUserPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, + "userName": "group-pagination-member@example.com", + "name": map[string]interface{}{ + "givenName": "Group", + "familyName": "PaginationMember", + }, + "emails": []map[string]interface{}{ + { + "value": "group-pagination-member@example.com", + "type": "work", + "primary": true, + }, + }, + "active": true, + } + + var createUserResp map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Users"), createUserPayload, http.StatusCreated, &createUserResp) + userID := createUserResp["id"].(string) + assert.NotEmpty(t, userID) + + // Create multiple groups for pagination testing + groupIDs := make([]string, 0, 10) + + for i := 1; i <= 10; i++ { + // Add the user as a member to even-numbered groups + var members []map[string]interface{} + if i%2 == 0 { + members = []map[string]interface{}{ + { + "value": userID, + }, + } + } + + createGroupPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:Group"}, + "displayName": fmt.Sprintf("Pagination Group %d", i), + } + + if len(members) > 0 { + createGroupPayload["members"] = members + } + + var createResp map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Groups"), createGroupPayload, http.StatusCreated, &createResp) + groupID := createResp["id"].(string) + groupIDs = append(groupIDs, groupID) + } + + // Test 1: Get first page with 3 groups per page + var page1Resp map[string]interface{} + s.DoJSON(t, "GET", scimPath("/Groups"), nil, http.StatusOK, &page1Resp, "startIndex", "1", "count", "3") + + // Verify response structure + assert.EqualValues(t, page1Resp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:ListResponse"}) + assert.Equal(t, float64(10), page1Resp["totalResults"], "Total results should be 10") + + // Verify resources + resources1, ok := page1Resp["Resources"].([]interface{}) + assert.True(t, ok, "Resources should be an array") + assert.Equal(t, 3, len(resources1), "First page should have 3 groups") + + // Verify the groups on the first page + displayNames1 := make([]string, 0, 3) + for _, resource := range resources1 { + group, ok := resource.(map[string]interface{}) + assert.True(t, ok, "Group should be an object") + displayName, ok := group["displayName"].(string) + assert.True(t, ok, "displayName should be a string") + displayNames1 = append(displayNames1, displayName) + } + assert.Contains(t, displayNames1, "Pagination Group 1", "First page should contain group 1") + assert.Contains(t, displayNames1, "Pagination Group 2", "First page should contain group 2") + assert.Contains(t, displayNames1, "Pagination Group 3", "First page should contain group 3") + + // Test 2: Get second page with 3 groups per page + var page2Resp map[string]interface{} + s.DoJSON(t, "GET", scimPath("/Groups"), nil, http.StatusOK, &page2Resp, "startIndex", "4", "count", "3") + + // Verify response structure + assert.EqualValues(t, page2Resp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:ListResponse"}) + assert.Equal(t, float64(10), page2Resp["totalResults"], "Total results should be 10") + + // Verify resources + resources2, ok := page2Resp["Resources"].([]interface{}) + assert.True(t, ok, "Resources should be an array") + assert.Equal(t, 3, len(resources2), "Second page should have 3 groups") + + // Verify the groups on the second page + displayNames2 := make([]string, 0, 3) + for _, resource := range resources2 { + group, ok := resource.(map[string]interface{}) + assert.True(t, ok, "Group should be an object") + displayName, ok := group["displayName"].(string) + assert.True(t, ok, "displayName should be a string") + displayNames2 = append(displayNames2, displayName) + } + assert.Contains(t, displayNames2, "Pagination Group 4", "Second page should contain group 4") + assert.Contains(t, displayNames2, "Pagination Group 5", "Second page should contain group 5") + assert.Contains(t, displayNames2, "Pagination Group 6", "Second page should contain group 6") + + // Test 3: Get third page with 3 groups per page + var page3Resp map[string]interface{} + s.DoJSON(t, "GET", scimPath("/Groups"), nil, http.StatusOK, &page3Resp, "startIndex", "7", "count", "3") + + // Verify response structure + assert.EqualValues(t, page3Resp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:ListResponse"}) + assert.Equal(t, float64(10), page3Resp["totalResults"], "Total results should be 10") + + // Verify resources + resources3, ok := page3Resp["Resources"].([]interface{}) + assert.True(t, ok, "Resources should be an array") + assert.Equal(t, 3, len(resources3), "Third page should have 3 groups") + + // Verify the groups on the third page + displayNames3 := make([]string, 0, 3) + for _, resource := range resources3 { + group, ok := resource.(map[string]interface{}) + assert.True(t, ok, "Group should be an object") + displayName, ok := group["displayName"].(string) + assert.True(t, ok, "displayName should be a string") + displayNames3 = append(displayNames3, displayName) + } + assert.Contains(t, displayNames3, "Pagination Group 7", "Third page should contain group 7") + assert.Contains(t, displayNames3, "Pagination Group 8", "Third page should contain group 8") + assert.Contains(t, displayNames3, "Pagination Group 9", "Third page should contain group 9") + + // Test 4: Get fourth page with 3 groups per page (should contain only 1 group) + var page4Resp map[string]interface{} + s.DoJSON(t, "GET", scimPath("/Groups"), nil, http.StatusOK, &page4Resp, "startIndex", "10", "count", "3") + + // Verify response structure + assert.EqualValues(t, page4Resp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:ListResponse"}) + assert.Equal(t, float64(10), page4Resp["totalResults"], "Total results should be 10") + + // Verify resources + resources4, ok := page4Resp["Resources"].([]interface{}) + assert.True(t, ok, "Resources should be an array") + require.Len(t, resources4, 1, "Fourth page should have 1 group") + + // Verify the group on the fourth page + group4, ok := resources4[0].(map[string]interface{}) + assert.True(t, ok, "Group should be an object") + displayName4, ok := group4["displayName"].(string) + assert.True(t, ok, "displayName should be a string") + assert.Equal(t, "Pagination Group 10", displayName4, "Fourth page should contain group 10") + + // Test 5: Get page with startIndex beyond the total results (should return empty resources) + var emptyPageResp map[string]interface{} + s.DoJSON(t, "GET", scimPath("/Groups"), nil, http.StatusOK, &emptyPageResp, "startIndex", "11", "count", "3") + + // Verify response structure + assert.EqualValues(t, emptyPageResp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:ListResponse"}) + assert.Equal(t, float64(10), emptyPageResp["totalResults"], "Total results should be 10") + + // Verify resources + emptyResources, ok := emptyPageResp["Resources"].([]interface{}) + assert.True(t, ok, "Resources should be an array") + assert.Empty(t, emptyResources, "Page beyond total results should have 0 groups") + + // Test 6: Get all groups in a single page + var allGroupsResp map[string]interface{} + s.DoJSON(t, "GET", scimPath("/Groups"), nil, http.StatusOK, &allGroupsResp, "count", "20") + + // Verify response structure + assert.EqualValues(t, allGroupsResp["schemas"], []interface{}{"urn:ietf:params:scim:api:messages:2.0:ListResponse"}) + assert.Equal(t, float64(10), allGroupsResp["totalResults"], "Total results should be 10") + + // Verify resources + allResources, ok := allGroupsResp["Resources"].([]interface{}) + assert.True(t, ok, "Resources should be an array") + assert.Equal(t, 10, len(allResources), "All groups page should have 10 groups") + + // Test 7: Verify that even-numbered groups have the user as a member + for _, resource := range allResources { + group, ok := resource.(map[string]interface{}) + assert.True(t, ok, "Group should be an object") + displayName, ok := group["displayName"].(string) + assert.True(t, ok, "displayName should be a string") + + // Extract the group number from the display name + var groupNum int + _, err := fmt.Sscanf(displayName, "Pagination Group %d", &groupNum) + assert.NoError(t, err, "Should be able to extract group number from display name") + + // Check if the group has members based on its number + if groupNum%2 == 0 { + // Even-numbered groups should have the user as a member + members, ok := group["members"].([]interface{}) + assert.True(t, ok, "members should be an array") + assert.Equal(t, 1, len(members), "Even-numbered group should have 1 member") + + if len(members) > 0 { + member, ok := members[0].(map[string]interface{}) + assert.True(t, ok, "Member should be an object") + assert.Equal(t, userID, member["value"], "Member should be the test user") + } + } else { + // Odd-numbered groups should not have members + _, hasMembersField := group["members"] + assert.False(t, hasMembersField, "Odd-numbered group should not have members field") + } + } + + // Clean up all created groups + for _, groupID := range groupIDs { + s.Do(t, "DELETE", scimPath("/Groups/"+groupID), nil, http.StatusNoContent) + } + + // Clean up the user + s.Do(t, "DELETE", scimPath("/Users/"+userID), nil, http.StatusNoContent) +} + +func testUsersAndGroups(t *testing.T, s *Suite) { + // Create multiple test users + userIDs := make([]string, 0, 3) + for i := 1; i <= 3; i++ { + userName := fmt.Sprintf("user-group-test-%d@example.com", i) + createUserPayload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, + "userName": userName, + "name": map[string]interface{}{ + "givenName": fmt.Sprintf("User%d", i), + "familyName": "GroupTest", + }, + "emails": []map[string]interface{}{ + { + "value": userName, + "type": "work", + "primary": true, + }, + }, + "active": true, + } + + var createResp map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Users"), createUserPayload, http.StatusCreated, &createResp) + userID := createResp["id"].(string) + userIDs = append(userIDs, userID) + } + + // Create two groups with different membership patterns + // Group 1: Contains users 1 and 2 + group1Members := []map[string]interface{}{ + { + "value": userIDs[0], + }, + { + "value": userIDs[1], + }, + } + + createGroup1Payload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:Group"}, + "displayName": "Test Group 1", + "members": group1Members, + } + + var createGroup1Resp map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Groups"), createGroup1Payload, http.StatusCreated, &createGroup1Resp) + group1ID := createGroup1Resp["id"].(string) + + // Group 2: Contains users 2 and 3 + group2Members := []map[string]interface{}{ + { + "value": userIDs[1], + }, + { + "value": userIDs[2], + }, + } + + createGroup2Payload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:Group"}, + "displayName": "Test Group 2", + "members": group2Members, + } + + var createGroup2Resp map[string]interface{} + s.DoJSON(t, "POST", scimPath("/Groups"), createGroup2Payload, http.StatusCreated, &createGroup2Resp) + group2ID := createGroup2Resp["id"].(string) + + // Test 1: Verify that User 1 is in Group 1 only + var user1Resp map[string]interface{} + s.DoJSON(t, "GET", scimPath("/Users/"+userIDs[0]), nil, http.StatusOK, &user1Resp) + + // Check groups field in user response + user1Groups, ok := user1Resp["groups"].([]interface{}) + assert.True(t, ok, "User should have groups field") + assert.Equal(t, 1, len(user1Groups), "User 1 should be in 1 group") + + // Verify the group is Group 1 + if len(user1Groups) > 0 { + group, ok := user1Groups[0].(map[string]interface{}) + assert.True(t, ok, "Group should be an object") + assert.Equal(t, group1ID, group["value"], "User 1 should be in Group 1") + assert.Equal(t, "Groups/"+group1ID, group["$ref"], "Group $ref should be correct") + } + + // Test 2: Verify that User 2 is in both Group 1 and Group 2 + var user2Resp map[string]interface{} + s.DoJSON(t, "GET", scimPath("/Users/"+userIDs[1]), nil, http.StatusOK, &user2Resp) + + // Check groups field in user response + user2Groups, ok := user2Resp["groups"].([]interface{}) + assert.True(t, ok, "User should have groups field") + assert.Equal(t, 2, len(user2Groups), "User 2 should be in 2 groups") + + // Verify the groups include both Group 1 and Group 2 + groupValues := make([]string, 0, 2) + for _, g := range user2Groups { + group, ok := g.(map[string]interface{}) + assert.True(t, ok, "Group should be an object") + groupValues = append(groupValues, group["value"].(string)) + } + assert.Contains(t, groupValues, group1ID, "User 2 should be in Group 1") + assert.Contains(t, groupValues, group2ID, "User 2 should be in Group 2") + + // Test 3: Verify that User 3 is in Group 2 only + var user3Resp map[string]interface{} + s.DoJSON(t, "GET", scimPath("/Users/"+userIDs[2]), nil, http.StatusOK, &user3Resp) + + // Check groups field in user response + user3Groups, ok := user3Resp["groups"].([]interface{}) + assert.True(t, ok, "User should have groups field") + assert.Equal(t, 1, len(user3Groups), "User 3 should be in 1 group") + + // Verify the group is Group 2 + if len(user3Groups) > 0 { + group, ok := user3Groups[0].(map[string]interface{}) + assert.True(t, ok, "Group should be an object") + assert.Equal(t, group2ID, group["value"], "User 3 should be in Group 2") + assert.Equal(t, "Groups/"+group2ID, group["$ref"], "Group $ref should be correct") + } + + // Test 4: Update Group 1 to remove User 1 and add User 3 + updateGroup1Payload := map[string]interface{}{ + "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:Group"}, + "displayName": "Test Group 1", + "members": []map[string]interface{}{ + { + "value": userIDs[1], // Keep User 2 + }, + { + "value": userIDs[2], // Add User 3 + }, + }, + } + + var updateGroup1Resp map[string]interface{} + s.DoJSON(t, "PUT", scimPath("/Groups/"+group1ID), updateGroup1Payload, http.StatusOK, &updateGroup1Resp) + + // Test 5: Verify that User 1 is no longer in any group + user1Resp = nil + s.DoJSON(t, "GET", scimPath("/Users/"+userIDs[0]), nil, http.StatusOK, &user1Resp) + + // Check groups field in user response + _, hasGroups := user1Resp["groups"] + assert.False(t, hasGroups, "User 1 should not have groups field or it should be empty") + + // Test 6: Verify that User 3 is now in both groups + user3Resp = nil + s.DoJSON(t, "GET", scimPath("/Users/"+userIDs[2]), nil, http.StatusOK, &user3Resp) + + // Check groups field in user response + user3Groups, ok = user3Resp["groups"].([]interface{}) + assert.True(t, ok, "User should have groups field") + assert.Equal(t, 2, len(user3Groups), "User 3 should be in 2 groups") + + // Verify the groups include both Group 1 and Group 2 + groupValues = make([]string, 0, 2) + for _, g := range user3Groups { + group, ok := g.(map[string]interface{}) + assert.True(t, ok, "Group should be an object") + groupValues = append(groupValues, group["value"].(string)) + } + assert.Contains(t, groupValues, group1ID, "User 3 should be in Group 1") + assert.Contains(t, groupValues, group2ID, "User 3 should be in Group 2") + + // Clean up + // Delete the groups + s.Do(t, "DELETE", scimPath("/Groups/"+group1ID), nil, http.StatusNoContent) + s.Do(t, "DELETE", scimPath("/Groups/"+group2ID), nil, http.StatusNoContent) + + // Delete the users + for _, userID := range userIDs { + s.Do(t, "DELETE", scimPath("/Users/"+userID), nil, http.StatusNoContent) + } +} + +func scimPath(suffix string) string { + paths := []string{"/api/v1/fleet/scim", "/api/latest/fleet/scim"} + prefix := paths[time.Now().UnixNano()%int64(len(paths))] + return prefix + suffix +} diff --git a/ee/server/integrationtest/scim/suite.go b/ee/server/integrationtest/scim/suite.go new file mode 100644 index 0000000000..ef471cbc71 --- /dev/null +++ b/ee/server/integrationtest/scim/suite.go @@ -0,0 +1,44 @@ +package scim + +import ( + "os" + "testing" + + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/service" + "github.com/fleetdm/fleet/v4/server/service/integrationtest" + "github.com/go-kit/log" +) + +type Suite struct { + integrationtest.BaseSuite +} + +func SetUpSuite(t *testing.T, uniqueTestName string) *Suite { + ds, redisPool, fleetCfg, fleetSvc, ctx := integrationtest.SetUpDSRedisService(t, uniqueTestName) + logger := log.NewLogfmtLogger(os.Stdout) + users, server := service.RunServerForTestsWithServiceWithDS(t, ctx, ds, fleetSvc, &service.TestServerOpts{ + License: &fleet.LicenseInfo{ + Tier: fleet.TierFree, + }, + FleetConfig: &fleetCfg, + Pool: redisPool, + Logger: logger, + EnableSCIM: true, + }) + + s := &Suite{ + BaseSuite: integrationtest.BaseSuite{ + Logger: logger, + DS: ds, + FleetCfg: fleetCfg, + Users: users, + Server: server, + }, + } + + integrationtest.SetUpServerURL(t, ds, server) + + s.Token = s.GetTestAdminToken(t) + return s +} diff --git a/ee/server/scim/groups.go b/ee/server/scim/groups.go index 7c115f39da..7c3bfefd1c 100644 --- a/ee/server/scim/groups.go +++ b/ee/server/scim/groups.go @@ -155,10 +155,12 @@ func createGroupResource(group *fleet.ScimGroup) scim.Resource { return groupResource } +// GetAll +// Pagination is 1-indexed. func (g *GroupHandler) GetAll(r *http.Request, params scim.ListRequestParams) (scim.Page, error) { - page := params.StartIndex - if page < 1 { - page = 1 + startIndex := params.StartIndex + if startIndex < 1 { + startIndex = 1 } count := params.Count if count > maxResults { @@ -169,8 +171,8 @@ func (g *GroupHandler) GetAll(r *http.Request, params scim.ListRequestParams) (s } opts := fleet.ScimListOptions{ - Page: uint(page), // nolint:gosec // ignore G115 - PerPage: uint(count), // nolint:gosec // ignore G115 + StartIndex: uint(startIndex), // nolint:gosec // ignore G115 + PerPage: uint(count), // nolint:gosec // ignore G115 } resourceFilter := r.URL.Query().Get("filter") diff --git a/ee/server/scim/users.go b/ee/server/scim/users.go index beef4545a4..9bcd8dd755 100644 --- a/ee/server/scim/users.go +++ b/ee/server/scim/users.go @@ -50,6 +50,11 @@ func (u *UserHandler) Create(r *http.Request, attributes scim.ResourceAttributes level.Error(u.logger).Log("msg", "failed to get userName", "err", err) return scim.Resource{}, err } + // In IETF documents, “non-empty” is generally used in the literal sense of “having at least one character.” That means if a value contains one or more spaces (and nothing else), it is still considered non-empty. + if len(userName) == 0 { + level.Info(u.logger).Log("msg", "userName is empty") + return scim.Resource{}, errors.ScimErrorBadParams([]string{userNameAttr}) + } _, err = u.ds.ScimUserByUserName(r.Context(), userName) switch { case err != nil && !fleet.IsNotFound(err): @@ -260,6 +265,8 @@ func createUserResource(user *fleet.ScimUser) scim.Resource { } // GetAll +// Pagination is 1-indexed. +// // Per RFC7644 3.4.2, SHOULD ignore any query parameters they do not recognize instead of rejecting the query for versioning compatibility reasons // https://datatracker.ietf.org/doc/html/rfc7644#section-3.4.2 // @@ -274,9 +281,9 @@ func createUserResource(user *fleet.ScimUser) scim.Resource { // totalResults: The total number of results returned by the list or query operation. The value may be larger than the number of // resources returned, such as when returning a single page (see Section 3.4.2.4) of results where multiple pages are available. func (u *UserHandler) GetAll(r *http.Request, params scim.ListRequestParams) (scim.Page, error) { - page := params.StartIndex - if page < 1 { - page = 1 + startIndex := params.StartIndex + if startIndex < 1 { + startIndex = 1 } count := params.Count if count > maxResults { @@ -288,8 +295,8 @@ func (u *UserHandler) GetAll(r *http.Request, params scim.ListRequestParams) (sc opts := fleet.ScimUsersListOptions{ ScimListOptions: fleet.ScimListOptions{ - Page: uint(page), // nolint:gosec // ignore G115 - PerPage: uint(count), // nolint:gosec // ignore G115 + StartIndex: uint(startIndex), // nolint:gosec // ignore G115 + PerPage: uint(count), // nolint:gosec // ignore G115 }, } resourceFilter := r.URL.Query().Get("filter") @@ -400,6 +407,10 @@ func (u *UserHandler) Patch(r *http.Request, id string, operations []scim.PatchO return scim.Resource{}, err } + if len(operations) > 1 { + level.Info(u.logger).Log("msg", "too many patch operations") + return scim.Resource{}, errors.ScimErrorBadParams([]string{"Operations"}) + } for _, op := range operations { if op.Op != "replace" { level.Info(u.logger).Log("msg", "unsupported patch operation", "op", op.Op) @@ -435,14 +446,16 @@ func (u *UserHandler) Patch(r *http.Request, id string, operations []scim.PatchO } } - err = u.ds.ReplaceScimUser(r.Context(), user) - switch { - case fleet.IsNotFound(err): - level.Info(u.logger).Log("msg", "failed to find user to patch", "id", id) - return scim.Resource{}, errors.ScimErrorResourceNotFound(id) - case err != nil: - level.Error(u.logger).Log("msg", "failed to patch user", "id", id, "err", err) - return scim.Resource{}, err + if len(operations) != 0 { + err = u.ds.ReplaceScimUser(r.Context(), user) + switch { + case fleet.IsNotFound(err): + level.Info(u.logger).Log("msg", "failed to find user to patch", "id", id) + return scim.Resource{}, errors.ScimErrorResourceNotFound(id) + case err != nil: + level.Error(u.logger).Log("msg", "failed to patch user", "id", id, "err", err) + return scim.Resource{}, err + } } return createUserResource(user), nil diff --git a/server/datastore/mysql/scim.go b/server/datastore/mysql/scim.go index b521a61180..7242176119 100644 --- a/server/datastore/mysql/scim.go +++ b/server/datastore/mysql/scim.go @@ -227,13 +227,6 @@ func insertEmails(ctx context.Context, tx sqlx.ExtContext, user *fleet.ScimUser) // DeleteScimUser deletes a SCIM user from the database func (ds *Datastore) DeleteScimUser(ctx context.Context, id uint) error { return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { - // Delete all email entries for the user - const deleteEmailsQuery = `DELETE FROM scim_user_emails WHERE scim_user_id = ?` - _, err := tx.ExecContext(ctx, deleteEmailsQuery, id) - if err != nil { - return ctxerr.Wrap(ctx, err, "delete scim user emails") - } - // Delete the user const deleteUserQuery = `DELETE FROM scim_users WHERE id = ?` result, err := tx.ExecContext(ctx, deleteUserQuery, id) @@ -257,16 +250,13 @@ func (ds *Datastore) DeleteScimUser(ctx context.Context, id uint) error { // ListScimUsers retrieves a list of SCIM users with optional filtering func (ds *Datastore) ListScimUsers(ctx context.Context, opts fleet.ScimUsersListOptions) (users []fleet.ScimUser, totalResults uint, err error) { // Default pagination values if not provided - if opts.Page == 0 { - opts.Page = 1 + if opts.StartIndex == 0 { + opts.StartIndex = 1 } if opts.PerPage == 0 { opts.PerPage = SCIMDefaultResourcesPerPage } - // Calculate offset for pagination - offset := (opts.Page - 1) * opts.PerPage - // Build the base query baseQuery := ` SELECT DISTINCT @@ -298,7 +288,7 @@ func (ds *Datastore) ListScimUsers(ctx context.Context, opts fleet.ScimUsersList // Add pagination to the main query query := baseQuery + whereClause + " ORDER BY scim_users.id LIMIT ? OFFSET ?" - params = append(params, opts.PerPage, offset) + params = append(params, opts.PerPage, opts.StartIndex-1) // Execute the query err = sqlx.SelectContext(ctx, ds.reader(ctx), &users, query, params...) @@ -719,16 +709,13 @@ func (ds *Datastore) DeleteScimGroup(ctx context.Context, id uint) error { // ListScimGroups retrieves a list of SCIM groups with pagination func (ds *Datastore) ListScimGroups(ctx context.Context, opts fleet.ScimListOptions) (groups []fleet.ScimGroup, totalResults uint, err error) { // Default pagination values if not provided - if opts.Page == 0 { - opts.Page = 1 + if opts.StartIndex == 0 { + opts.StartIndex = 1 } if opts.PerPage == 0 { opts.PerPage = SCIMDefaultResourcesPerPage } - // Calculate offset for pagination - offset := (opts.Page - 1) * opts.PerPage - // Build the query baseQuery := ` SELECT DISTINCT @@ -745,7 +732,7 @@ func (ds *Datastore) ListScimGroups(ctx context.Context, opts fleet.ScimListOpti // Add pagination to the main query query := baseQuery + " ORDER BY scim_groups.id LIMIT ? OFFSET ?" - params := []interface{}{opts.PerPage, offset} + params := []interface{}{opts.PerPage, opts.StartIndex - 1} // Execute the query err = sqlx.SelectContext(ctx, ds.reader(ctx), &groups, query, params...) diff --git a/server/datastore/mysql/scim_test.go b/server/datastore/mysql/scim_test.go index 914b657230..102e5696a4 100644 --- a/server/datastore/mysql/scim_test.go +++ b/server/datastore/mysql/scim_test.go @@ -521,8 +521,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) { // Test 1: List all users without filters allUsers, totalResults, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{ ScimListOptions: fleet.ScimListOptions{ - Page: 1, - PerPage: 10, + StartIndex: 1, + PerPage: 10, }, }) require.Nil(t, err) @@ -553,8 +553,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) { // Test 2: Pagination - first page with 2 items page1Users, totalPage1, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{ ScimListOptions: fleet.ScimListOptions{ - Page: 1, - PerPage: 2, + StartIndex: 1, + PerPage: 2, }, }) require.Nil(t, err) @@ -564,8 +564,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) { // Test 3: Pagination - second page with 2 items page2Users, totalPage2, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{ ScimListOptions: fleet.ScimListOptions{ - Page: 2, - PerPage: 2, + StartIndex: 3, // StartIndex is 1-based, so for the second page with 2 items per page, we start at index 3 + PerPage: 2, }, }) require.Nil(t, err) @@ -582,8 +582,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) { // Test 4: Filter by username listUsers, totalListUsers, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{ ScimListOptions: fleet.ScimListOptions{ - Page: 1, - PerPage: 10, + StartIndex: 1, + PerPage: 10, }, UserNameFilter: ptr.String("list-test-user2"), }) @@ -596,8 +596,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) { // Test 5: Filter by email type and value homeEmailUsers, totalHomeEmailUsers, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{ ScimListOptions: fleet.ScimListOptions{ - Page: 1, - PerPage: 10, + StartIndex: 1, + PerPage: 10, }, EmailTypeFilter: ptr.String("home"), EmailValueFilter: ptr.String("personal.user2@example.com"), @@ -611,8 +611,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) { // Test 6: Filter by email type and value - work emails workEmailUsers, totalWorkEmailUsers, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{ ScimListOptions: fleet.ScimListOptions{ - Page: 1, - PerPage: 10, + StartIndex: 1, + PerPage: 10, }, EmailTypeFilter: ptr.String("work"), EmailValueFilter: ptr.String("different.user3@example.com"), @@ -624,8 +624,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) { // Test 7: No results for non-matching filters noUsers, totalNoUsers1, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{ ScimListOptions: fleet.ScimListOptions{ - Page: 1, - PerPage: 10, + StartIndex: 1, + PerPage: 10, }, UserNameFilter: ptr.String("nonexistent"), }) @@ -635,8 +635,8 @@ func testListScimUsers(t *testing.T, ds *Datastore) { noUsers, totalNoUsers2, err := ds.ListScimUsers(context.Background(), fleet.ScimUsersListOptions{ ScimListOptions: fleet.ScimListOptions{ - Page: 1, - PerPage: 10, + StartIndex: 1, + PerPage: 10, }, EmailTypeFilter: ptr.String("nonexistent"), EmailValueFilter: ptr.String("nonexistent"), @@ -1030,8 +1030,8 @@ func testListScimGroups(t *testing.T, ds *Datastore) { // Test 1: List all groups allGroups, totalResults, err := ds.ListScimGroups(context.Background(), fleet.ScimListOptions{ - Page: 1, - PerPage: 10, + StartIndex: 1, + PerPage: 10, }) require.Nil(t, err) assert.GreaterOrEqual(t, len(allGroups), 3) // There might be other groups from previous tests @@ -1051,8 +1051,8 @@ func testListScimGroups(t *testing.T, ds *Datastore) { // Test 2: Pagination - first page with 2 items page1Groups, totalPage1, err := ds.ListScimGroups(context.Background(), fleet.ScimListOptions{ - Page: 1, - PerPage: 2, + StartIndex: 1, + PerPage: 2, }) require.Nil(t, err) assert.Equal(t, 2, len(page1Groups)) @@ -1060,8 +1060,8 @@ func testListScimGroups(t *testing.T, ds *Datastore) { // Test 3: Pagination - second page with 2 items page2Groups, totalPage2, err := ds.ListScimGroups(context.Background(), fleet.ScimListOptions{ - Page: 2, - PerPage: 2, + StartIndex: 3, // StartIndex is 1-based, so for the second page with 2 items per page, we start at index 3 + PerPage: 2, }) require.Nil(t, err) assert.GreaterOrEqual(t, len(page2Groups), 1) // At least 1 item on the second page diff --git a/server/fleet/scim.go b/server/fleet/scim.go index f7e710f8bd..1d5792ed94 100644 --- a/server/fleet/scim.go +++ b/server/fleet/scim.go @@ -25,8 +25,8 @@ type ScimUserEmail struct { } type ScimListOptions struct { - // Which page to return (must be positive integer) - Page uint + // 1-based index of the first result to return (must be positive integer) + StartIndex uint // How many results per page (must be positive integer) PerPage uint } diff --git a/server/service/integrationtest/android/android_test.go b/server/service/integrationtest/android/android_test.go index fc9adcf46f..aa23db8388 100644 --- a/server/service/integrationtest/android/android_test.go +++ b/server/service/integrationtest/android/android_test.go @@ -6,16 +6,15 @@ import ( "github.com/fleetdm/fleet/v4/server/datastore/mysql" "github.com/fleetdm/fleet/v4/server/mdm/android" - "github.com/fleetdm/fleet/v4/server/service/integrationtest" "github.com/stretchr/testify/assert" ) func TestAndroid(t *testing.T) { - s := integrationtest.SetUpSuite(t, "integrationtest.Android") + s := SetUpSuite(t, "integrationtest.Android") cases := []struct { name string - fn func(t *testing.T, s *integrationtest.Suite) + fn func(t *testing.T, s *Suite) }{ {"HappyPath", testHappyPath}, } @@ -27,14 +26,14 @@ func TestAndroid(t *testing.T) { } } -func testHappyPath(t *testing.T, s *integrationtest.Suite) { +func testHappyPath(t *testing.T, s *Suite) { signupDetails := expectSignupDetails(t, s) var signupURL android.EnterpriseSignupResponse s.DoJSON(t, "GET", "/api/v1/fleet/android_enterprise/signup_url", nil, http.StatusOK, &signupURL) assert.Equal(t, signupURL.Url, signupDetails.Url) } -func expectSignupDetails(t *testing.T, s *integrationtest.Suite) *android.SignupDetails { +func expectSignupDetails(t *testing.T, s *Suite) *android.SignupDetails { signupDetails := &android.SignupDetails{ Url: "URL", Name: "Name", diff --git a/server/service/integrationtest/android/suite.go b/server/service/integrationtest/android/suite.go new file mode 100644 index 0000000000..87b3c5253b --- /dev/null +++ b/server/service/integrationtest/android/suite.go @@ -0,0 +1,59 @@ +package android + +import ( + "os" + "testing" + + "github.com/fleetdm/fleet/v4/server/fleet" + android_mock "github.com/fleetdm/fleet/v4/server/mdm/android/mock" + android_service "github.com/fleetdm/fleet/v4/server/mdm/android/service" + "github.com/fleetdm/fleet/v4/server/service" + "github.com/fleetdm/fleet/v4/server/service/integrationtest" + "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" + "github.com/go-kit/log" + "github.com/stretchr/testify/require" +) + +type Suite struct { + integrationtest.BaseSuite + AndroidProxy *android_mock.Proxy +} + +func SetUpSuite(t *testing.T, uniqueTestName string) *Suite { + ds, redisPool, fleetCfg, fleetSvc, ctx := integrationtest.SetUpDSRedisService(t, uniqueTestName) + logger := log.NewLogfmtLogger(os.Stdout) + proxy := android_mock.Proxy{} + proxy.InitCommonMocks() + androidSvc, err := android_service.NewServiceWithProxy( + logger, + ds, + &proxy, + fleetSvc, + ) + require.NoError(t, err) + users, server := service.RunServerForTestsWithServiceWithDS(t, ctx, ds, fleetSvc, &service.TestServerOpts{ + License: &fleet.LicenseInfo{ + Tier: fleet.TierFree, + }, + FleetConfig: &fleetCfg, + Pool: redisPool, + Logger: logger, + FeatureRoutes: []endpoint_utils.HandlerRoutesFunc{android_service.GetRoutes(fleetSvc, androidSvc)}, + }) + + s := &Suite{ + BaseSuite: integrationtest.BaseSuite{ + Logger: logger, + DS: ds, + FleetCfg: fleetCfg, + Users: users, + Server: server, + }, + AndroidProxy: &proxy, + } + + integrationtest.SetUpServerURL(t, ds, server) + + s.Token = s.GetTestAdminToken(t) + return s +} diff --git a/server/service/integrationtest/http.go b/server/service/integrationtest/http.go index cdbd1e9575..ddcd3d51eb 100644 --- a/server/service/integrationtest/http.go +++ b/server/service/integrationtest/http.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/require" ) -func (s *Suite) DoJSON(t *testing.T, verb, path string, params interface{}, expectedStatusCode int, v interface{}, queryParams ...string) { +func (s *BaseSuite) DoJSON(t *testing.T, verb, path string, params interface{}, expectedStatusCode int, v interface{}, queryParams ...string) { resp := s.Do(t, verb, path, params, expectedStatusCode, queryParams...) err := json.UnmarshalRead(resp.Body, v) require.NoError(t, err) @@ -21,7 +21,7 @@ func (s *Suite) DoJSON(t *testing.T, verb, path string, params interface{}, expe } } -func (s *Suite) Do(t *testing.T, verb, path string, params interface{}, expectedStatusCode int, queryParams ...string) *http.Response { +func (s *BaseSuite) Do(t *testing.T, verb, path string, params interface{}, expectedStatusCode int, queryParams ...string) *http.Response { j, err := json.Marshal(params) require.NoError(t, err) @@ -33,13 +33,13 @@ func (s *Suite) Do(t *testing.T, verb, path string, params interface{}, expected return resp } -func (s *Suite) DoRaw(t *testing.T, verb string, path string, rawBytes []byte, expectedStatusCode int, queryParams ...string) *http.Response { +func (s *BaseSuite) DoRaw(t *testing.T, verb string, path string, rawBytes []byte, expectedStatusCode int, queryParams ...string) *http.Response { return s.DoRawWithHeaders(t, verb, path, rawBytes, expectedStatusCode, map[string]string{ "Authorization": fmt.Sprintf("Bearer %s", s.Token), }, queryParams...) } -func (s *Suite) DoRawWithHeaders( +func (s *BaseSuite) DoRawWithHeaders( t *testing.T, verb string, path string, rawBytes []byte, expectedStatusCode int, headers map[string]string, queryParams ...string, ) *http.Response { return httptest.DoHTTPReq(t, decodeJSON, verb, rawBytes, s.Server.URL+path, headers, expectedStatusCode, queryParams...) diff --git a/server/service/integrationtest/suite.go b/server/service/integrationtest/suite.go index 545c7b4078..74bc883015 100644 --- a/server/service/integrationtest/suite.go +++ b/server/service/integrationtest/suite.go @@ -3,74 +3,53 @@ package integrationtest import ( "context" "net/http/httptest" - "os" "testing" "github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/datastore/mysql" "github.com/fleetdm/fleet/v4/server/datastore/redis/redistest" "github.com/fleetdm/fleet/v4/server/fleet" - android_mock "github.com/fleetdm/fleet/v4/server/mdm/android/mock" - android_service "github.com/fleetdm/fleet/v4/server/mdm/android/service" - "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/service" - "github.com/fleetdm/fleet/v4/server/service/middleware/endpoint_utils" "github.com/fleetdm/fleet/v4/server/test" "github.com/go-kit/log" "github.com/stretchr/testify/require" ) -type Suite struct { - Logger log.Logger - FleetCfg config.FleetConfig - Server *httptest.Server - DS *mysql.Datastore - Users map[string]fleet.User - Token string - AndroidProxy *android_mock.Proxy +type BaseSuite struct { + Logger log.Logger + FleetCfg config.FleetConfig + Server *httptest.Server + DS *mysql.Datastore + Users map[string]fleet.User + Token string cachedAdminToken string } -var testUsers = map[string]struct { - Email string - PlaintextPassword string - GlobalRole *string -}{ - "admin1": { - PlaintextPassword: test.GoodPassword, - Email: "admin1@example.com", - GlobalRole: ptr.String(fleet.RoleAdmin), - }, - "user1": { - PlaintextPassword: test.GoodPassword, - Email: "user1@example.com", - GlobalRole: ptr.String(fleet.RoleMaintainer), - }, - "user2": { - PlaintextPassword: test.GoodPassword, - Email: "user2@example.com", - GlobalRole: ptr.String(fleet.RoleObserver), - }, -} - -func (s *Suite) GetTestAdminToken(t *testing.T) string { - testUser := testUsers["admin1"] - +func (s *BaseSuite) GetTestAdminToken(t *testing.T) string { // because the login endpoint is rate-limited, use the cached admin token // if available (if for some reason a test needs to logout the admin user, // then set cachedAdminToken = "" so that a new token is retrieved). if s.cachedAdminToken == "" { - s.cachedAdminToken = s.GetTestToken(t, testUser.Email, testUser.PlaintextPassword) + s.cachedAdminToken = s.GetTestToken(t, service.TestAdminUserEmail, test.GoodPassword) } return s.cachedAdminToken } -func (s *Suite) GetTestToken(t *testing.T, email string, password string) string { +func (s *BaseSuite) GetTestToken(t *testing.T, email string, password string) string { return service.GetToken(t, email, password, s.Server.URL) } -func SetUpSuite(t *testing.T, uniqueTestName string) *Suite { +func SetUpServerURL(t *testing.T, ds *mysql.Datastore, server *httptest.Server) { + appConf, err := ds.AppConfig(t.Context()) + require.NoError(t, err) + appConf.ServerSettings.ServerURL = server.URL + err = ds.SaveAppConfig(t.Context(), appConf) + require.NoError(t, err) +} + +func SetUpDSRedisService(t *testing.T, uniqueTestName string) (*mysql.Datastore, fleet.RedisPool, config.FleetConfig, + fleet.Service, context.Context) { ds := mysql.CreateMySQLDS(t) test.AddAllHostsLabel(t, ds) @@ -85,44 +64,8 @@ func SetUpSuite(t *testing.T, uniqueTestName string) *Suite { redisPool := redistest.SetupRedis(t, uniqueTestName, false, false, false) fleetCfg := config.TestConfig() - logger := log.NewLogfmtLogger(os.Stdout) fleetSvc, ctx := service.NewTestService(t, ds, fleetCfg) - proxy := android_mock.Proxy{} - proxy.InitCommonMocks() - androidSvc, err := android_service.NewServiceWithProxy( - logger, - ds, - &proxy, - fleetSvc, - ) - require.NoError(t, err) - users, server := service.RunServerForTestsWithServiceWithDS(t, ctx, ds, fleetSvc, &service.TestServerOpts{ - License: &fleet.LicenseInfo{ - Tier: fleet.TierFree, - }, - FleetConfig: &fleetCfg, - Pool: redisPool, - Logger: logger, - FeatureRoutes: []endpoint_utils.HandlerRoutesFunc{android_service.GetRoutes(fleetSvc, androidSvc)}, - }) - - s := &Suite{ - Logger: logger, - DS: ds, - FleetCfg: fleetCfg, - Users: users, - Server: server, - AndroidProxy: &proxy, - } - - appConf, err = ds.AppConfig(ctx) - require.NoError(t, err) - appConf.ServerSettings.ServerURL = server.URL - err = ds.SaveAppConfig(ctx, appConf) - require.NoError(t, err) - - s.Token = s.GetTestAdminToken(t) - return s + return ds, redisPool, fleetCfg, fleetSvc, ctx } func testContext() context.Context { diff --git a/server/service/testing_utils.go b/server/service/testing_utils.go index 64dbc2b4c2..bef11af6a4 100644 --- a/server/service/testing_utils.go +++ b/server/service/testing_utils.go @@ -10,12 +10,12 @@ import ( "net/http/httptest" "os" "sort" - "strings" "sync" "testing" "time" "github.com/WatchBeam/clock" + "github.com/fleetdm/fleet/v4/ee/server/scim" eeservice "github.com/fleetdm/fleet/v4/ee/server/service" "github.com/fleetdm/fleet/v4/ee/server/service/digicert" "github.com/fleetdm/fleet/v4/server/config" @@ -255,15 +255,11 @@ func createTestUsers(t *testing.T, ds fleet.Datastore) map[string]fleet.User { userID := uint(1) for _, key := range keys { u := testUsers[key] - role := fleet.RoleObserver - if strings.Contains(u.Email, "admin") { - role = fleet.RoleAdmin - } user := &fleet.User{ ID: userID, // We need to set this in case ds is a mocked Datastore. Name: "Test Name " + u.Email, Email: u.Email, - GlobalRole: &role, + GlobalRole: u.GlobalRole, } err := user.SetPassword(u.PlaintextPassword, 10, 10) require.Nil(t, err) @@ -275,6 +271,12 @@ func createTestUsers(t *testing.T, ds fleet.Datastore) map[string]fleet.User { return users } +const ( + TestAdminUserEmail = "admin1@example.com" + TestMaintainerUserEmail = "user1@example.com" + TestObserverUserEmail = "user2@example.com" +) + var testUsers = map[string]struct { Email string PlaintextPassword string @@ -282,17 +284,17 @@ var testUsers = map[string]struct { }{ "admin1": { PlaintextPassword: test.GoodPassword, - Email: "admin1@example.com", + Email: TestAdminUserEmail, GlobalRole: ptr.String(fleet.RoleAdmin), }, "user1": { PlaintextPassword: test.GoodPassword, - Email: "user1@example.com", + Email: TestMaintainerUserEmail, GlobalRole: ptr.String(fleet.RoleMaintainer), }, "user2": { PlaintextPassword: test.GoodPassword, - Email: "user2@example.com", + Email: TestObserverUserEmail, GlobalRole: ptr.String(fleet.RoleObserver), }, } @@ -353,6 +355,7 @@ type TestServerOpts struct { FeatureRoutes []endpoint_utils.HandlerRoutesFunc SCEPConfigService fleet.SCEPConfigService DigiCertService fleet.DigiCertService + EnableSCIM bool } func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServerOpts) (map[string]fleet.User, *httptest.Server) { @@ -451,6 +454,10 @@ func RunServerForTestsWithServiceWithDS(t *testing.T, ctx context.Context, ds fl debugHandler := MakeDebugHandler(svc, cfg, logger, errHandler, ds) rootMux.Handle("/debug/", debugHandler) + if len(opts) > 0 && opts[0].EnableSCIM { + require.NoError(t, scim.RegisterSCIM(rootMux, ds, svc, logger)) + } + server := httptest.NewUnstartedServer(rootMux) server.Config = cfg.Server.DefaultHTTPServer(ctx, rootMux) // WriteTimeout is set for security purposes.