mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 21:47:20 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #35508 # Checklist for submitter If some of the following don't apply, delete the relevant line. ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually
2725 lines
106 KiB
Go
2725 lines
106 KiB
Go
package mysql
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
"github.com/fleetdm/fleet/v4/server/test"
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestScim(t *testing.T) {
|
|
ds := CreateMySQLDS(t)
|
|
|
|
cases := []struct {
|
|
name string
|
|
fn func(t *testing.T, ds *Datastore)
|
|
}{
|
|
{"ScimUserCreate", testScimUserCreate},
|
|
{"ScimUserCreateValidation", testScimUserCreateValidation},
|
|
{"ScimUserByID", testScimUserByID},
|
|
{"ScimUserByUserName", testScimUserByUserName},
|
|
{"ScimUserByUserNameOrEmail", testScimUserByUserNameOrEmail},
|
|
{"ScimUserByHostID", testScimUserByHostID},
|
|
{"ReplaceScimUser", testReplaceScimUser},
|
|
{"ReplaceScimUserEmails", testReplaceScimUserEmails},
|
|
{"ReplaceScimUserValidation", testScimUserReplaceValidation},
|
|
{"DeleteScimUser", testDeleteScimUser},
|
|
{"ListScimUsers", testListScimUsers},
|
|
{"ScimGroupCreate", testScimGroupCreate},
|
|
{"ScimGroupCreateValidation", testScimGroupCreateValidation},
|
|
{"ScimGroupByID", testScimGroupByID},
|
|
{"ScimGroupByDisplayName", testScimGroupByDisplayName},
|
|
{"ReplaceScimGroup", testReplaceScimGroup},
|
|
{"ReplaceScimGroupValidation", testScimGroupReplaceValidation},
|
|
{"DeleteScimGroup", testDeleteScimGroup},
|
|
{"ListScimGroups", testListScimGroups},
|
|
{"ScimLastRequest", testScimLastRequest},
|
|
{"ScimUsersExist", testScimUsersExist},
|
|
{"TriggerResendIdPProfiles", testTriggerResendIdPProfiles},
|
|
{"TriggerResendIdPProfilesOnTeam", testTriggerResendIdPProfilesOnTeam},
|
|
{"SetOrUpdateHostSCIMUserMapping", testSetOrUpdateHostSCIMUserMapping},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
defer TruncateTables(t, ds)
|
|
c.fn(t, ds)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testScimUserCreate(t *testing.T, ds *Datastore) {
|
|
usersToCreate := []fleet.ScimUser{
|
|
{
|
|
UserName: "user1",
|
|
ExternalID: nil,
|
|
GivenName: nil,
|
|
FamilyName: nil,
|
|
Active: nil,
|
|
Emails: []fleet.ScimUserEmail{},
|
|
Department: nil,
|
|
},
|
|
{
|
|
UserName: "user2",
|
|
ExternalID: ptr.String("ext-123"),
|
|
GivenName: ptr.String("John"),
|
|
FamilyName: ptr.String("Doe"),
|
|
Active: ptr.Bool(true),
|
|
Emails: []fleet.ScimUserEmail{
|
|
{
|
|
Email: "john.doe@example.com",
|
|
Primary: ptr.Bool(true),
|
|
Type: ptr.String("work"),
|
|
},
|
|
},
|
|
Department: ptr.String(""),
|
|
},
|
|
{
|
|
UserName: "user3",
|
|
ExternalID: ptr.String("ext-456"),
|
|
GivenName: ptr.String("Jane"),
|
|
FamilyName: ptr.String("Smith"),
|
|
Active: ptr.Bool(true),
|
|
Emails: []fleet.ScimUserEmail{
|
|
{
|
|
Email: "jane.personal@example.com",
|
|
Primary: ptr.Bool(false),
|
|
Type: ptr.String("home"),
|
|
},
|
|
{
|
|
Email: "jane.smith@example.com",
|
|
Primary: ptr.Bool(true),
|
|
Type: ptr.String("work"),
|
|
},
|
|
},
|
|
Department: ptr.String("Development"),
|
|
},
|
|
}
|
|
|
|
for _, u := range usersToCreate {
|
|
var err error
|
|
userCopy := u
|
|
userCopy.ID, err = ds.CreateScimUser(t.Context(), &u)
|
|
assert.Nil(t, err)
|
|
|
|
verify, err := ds.ScimUserByUserName(t.Context(), u.UserName)
|
|
assert.Nil(t, err)
|
|
|
|
assert.Equal(t, userCopy.ID, verify.ID)
|
|
assert.Equal(t, userCopy.UserName, verify.UserName)
|
|
assert.Equal(t, userCopy.ExternalID, verify.ExternalID)
|
|
assert.Equal(t, userCopy.GivenName, verify.GivenName)
|
|
assert.Equal(t, userCopy.FamilyName, verify.FamilyName)
|
|
assert.Equal(t, userCopy.Active, verify.Active)
|
|
assert.Equal(t, userCopy.Department, verify.Department)
|
|
assert.False(t, verify.UpdatedAt.IsZero(), "UpdatedAt should not be zero")
|
|
|
|
// Verify emails
|
|
assert.Equal(t, len(userCopy.Emails), len(verify.Emails))
|
|
for i, email := range userCopy.Emails {
|
|
assert.Equal(t, email.Email, verify.Emails[i].Email)
|
|
assert.Equal(t, email.Primary, verify.Emails[i].Primary)
|
|
assert.Equal(t, email.Type, verify.Emails[i].Type)
|
|
assert.Equal(t, u.ID, verify.Emails[i].ScimUserID)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testScimUserByID(t *testing.T, ds *Datastore) {
|
|
users := createTestScimUsers(t, ds)
|
|
|
|
// Create test groups and associate them with users
|
|
groups := createTestScimGroups(t, ds, []uint{users[0].ID, users[1].ID})
|
|
|
|
for _, tt := range users {
|
|
returned, err := ds.ScimUserByID(t.Context(), tt.ID)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, tt.ID, returned.ID)
|
|
assert.Equal(t, tt.UserName, returned.UserName)
|
|
assert.Equal(t, tt.ExternalID, returned.ExternalID)
|
|
assert.Equal(t, tt.GivenName, returned.GivenName)
|
|
assert.Equal(t, tt.FamilyName, returned.FamilyName)
|
|
assert.Equal(t, tt.Active, returned.Active)
|
|
assert.Equal(t, tt.Department, returned.Department)
|
|
|
|
// Verify emails
|
|
assert.Equal(t, len(tt.Emails), len(returned.Emails))
|
|
for i, email := range tt.Emails {
|
|
assert.Equal(t, email.Email, returned.Emails[i].Email)
|
|
assert.Equal(t, email.Primary, returned.Emails[i].Primary)
|
|
assert.Equal(t, email.Type, returned.Emails[i].Type)
|
|
assert.Equal(t, tt.ID, returned.Emails[i].ScimUserID)
|
|
}
|
|
|
|
// Verify groups
|
|
// User 0 and 1 should be in groups, User 2 should not be in any group
|
|
if tt.ID == users[0].ID || tt.ID == users[1].ID {
|
|
assert.NotEmpty(t, returned.Groups, "User should have groups")
|
|
|
|
// Check if the user is in the expected groups
|
|
var foundInGroups bool
|
|
for _, group := range groups {
|
|
for _, userID := range group.ScimUsers {
|
|
if userID == tt.ID {
|
|
foundInGroups = true
|
|
|
|
// Verify the group is in the user's Groups field
|
|
var foundGroup bool
|
|
for _, userGroup := range returned.Groups {
|
|
if userGroup.ID == group.ID {
|
|
foundGroup = true
|
|
assert.Equal(t, group.DisplayName, userGroup.DisplayName, "Group display name should match")
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, foundGroup, "User's Groups field should contain the group")
|
|
break
|
|
}
|
|
}
|
|
if foundInGroups {
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, foundInGroups, "User should be found in at least one group")
|
|
} else {
|
|
assert.Empty(t, returned.Groups, "User should not have any groups")
|
|
}
|
|
}
|
|
|
|
// test missing user
|
|
_, err := ds.ScimUserByID(t.Context(), 10000000000)
|
|
assert.True(t, fleet.IsNotFound(err))
|
|
}
|
|
|
|
func testScimUserByUserName(t *testing.T, ds *Datastore) {
|
|
users := createTestScimUsers(t, ds)
|
|
|
|
// Create test groups and associate them with users
|
|
groups := createTestScimGroups(t, ds, []uint{users[0].ID, users[1].ID})
|
|
|
|
for _, tt := range users {
|
|
returned, err := ds.ScimUserByUserName(t.Context(), tt.UserName)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, tt.ID, returned.ID)
|
|
assert.Equal(t, tt.UserName, returned.UserName)
|
|
assert.Equal(t, tt.ExternalID, returned.ExternalID)
|
|
assert.Equal(t, tt.GivenName, returned.GivenName)
|
|
assert.Equal(t, tt.FamilyName, returned.FamilyName)
|
|
assert.Equal(t, tt.Active, returned.Active)
|
|
assert.Equal(t, tt.Department, returned.Department)
|
|
assert.False(t, returned.UpdatedAt.IsZero(), "UpdatedAt should not be zero")
|
|
|
|
// Verify emails
|
|
assert.Equal(t, len(tt.Emails), len(returned.Emails))
|
|
for i, email := range tt.Emails {
|
|
assert.Equal(t, email.Email, returned.Emails[i].Email)
|
|
assert.Equal(t, email.Primary, returned.Emails[i].Primary)
|
|
assert.Equal(t, email.Type, returned.Emails[i].Type)
|
|
assert.Equal(t, tt.ID, returned.Emails[i].ScimUserID)
|
|
}
|
|
|
|
// Verify groups
|
|
// User 0 and 1 should be in groups, User 2 should not be in any group
|
|
if tt.ID == users[0].ID || tt.ID == users[1].ID {
|
|
assert.NotEmpty(t, returned.Groups, "User should have groups")
|
|
|
|
// Check if the user is in the expected groups
|
|
var foundInGroups bool
|
|
for _, group := range groups {
|
|
for _, userID := range group.ScimUsers {
|
|
if userID == tt.ID {
|
|
foundInGroups = true
|
|
|
|
// Verify the group is in the user's Groups field
|
|
var foundGroup bool
|
|
for _, userGroup := range returned.Groups {
|
|
if userGroup.ID == group.ID {
|
|
foundGroup = true
|
|
assert.Equal(t, group.DisplayName, userGroup.DisplayName, "Group display name should match")
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, foundGroup, "User's Groups field should contain the group")
|
|
break
|
|
}
|
|
}
|
|
if foundInGroups {
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, foundInGroups, "User should be found in at least one group")
|
|
} else {
|
|
assert.Empty(t, returned.Groups, "User should not have any groups")
|
|
}
|
|
}
|
|
|
|
// test missing user
|
|
_, err := ds.ScimUserByUserName(t.Context(), "nonexistent-user")
|
|
assert.NotNil(t, err)
|
|
}
|
|
|
|
func createTestScimUsers(t *testing.T, ds *Datastore) []*fleet.ScimUser {
|
|
createUsers := []fleet.ScimUser{
|
|
{
|
|
UserName: "test-user1",
|
|
ExternalID: ptr.String("ext-test-123"),
|
|
GivenName: ptr.String("Test"),
|
|
FamilyName: ptr.String("User"),
|
|
Active: ptr.Bool(true),
|
|
Emails: []fleet.ScimUserEmail{
|
|
{
|
|
Email: "test.user@example.com",
|
|
Primary: ptr.Bool(true),
|
|
Type: ptr.String("work"),
|
|
},
|
|
},
|
|
Department: nil,
|
|
},
|
|
{
|
|
UserName: "test-user2",
|
|
ExternalID: ptr.String("ext-test-456"),
|
|
GivenName: ptr.String("Another"),
|
|
FamilyName: ptr.String("User"),
|
|
Active: ptr.Bool(true),
|
|
Emails: []fleet.ScimUserEmail{
|
|
{
|
|
Email: "another.personal@example.com",
|
|
Primary: ptr.Bool(false),
|
|
Type: ptr.String("home"),
|
|
},
|
|
{
|
|
Email: "another.user@example.com",
|
|
Primary: ptr.Bool(true),
|
|
Type: ptr.String("work"),
|
|
},
|
|
},
|
|
Department: ptr.String("QA"),
|
|
},
|
|
}
|
|
|
|
var users []*fleet.ScimUser
|
|
for _, u := range createUsers {
|
|
var err error
|
|
u.ID, err = ds.CreateScimUser(t.Context(), &u)
|
|
require.Nil(t, err)
|
|
users = append(users, &u)
|
|
}
|
|
return users
|
|
}
|
|
|
|
func testReplaceScimUser(t *testing.T, ds *Datastore) {
|
|
// Create a test user
|
|
user := fleet.ScimUser{
|
|
UserName: "replace-test-user",
|
|
ExternalID: ptr.String("ext-replace-123"),
|
|
GivenName: ptr.String("Original"),
|
|
FamilyName: ptr.String("User"),
|
|
Active: ptr.Bool(true),
|
|
Emails: []fleet.ScimUserEmail{
|
|
{
|
|
Email: "original.user@example.com",
|
|
Primary: ptr.Bool(true),
|
|
Type: ptr.String("work"),
|
|
},
|
|
},
|
|
}
|
|
|
|
var err error
|
|
user.ID, err = ds.CreateScimUser(t.Context(), &user)
|
|
require.Nil(t, err)
|
|
|
|
// Create a test group and associate it with the user
|
|
group := fleet.ScimGroup{
|
|
DisplayName: "Test Group for User",
|
|
ExternalID: ptr.String("ext-group-for-user"),
|
|
ScimUsers: []uint{user.ID},
|
|
}
|
|
group.ID, err = ds.CreateScimGroup(t.Context(), &group)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the user was created correctly and has the group
|
|
createdUser, err := ds.ScimUserByID(t.Context(), user.ID)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, user.UserName, createdUser.UserName)
|
|
assert.Equal(t, user.ExternalID, createdUser.ExternalID)
|
|
assert.Equal(t, user.GivenName, createdUser.GivenName)
|
|
assert.Equal(t, user.FamilyName, createdUser.FamilyName)
|
|
assert.Equal(t, user.Active, createdUser.Active)
|
|
assert.Equal(t, 1, len(createdUser.Emails))
|
|
assert.Equal(t, "original.user@example.com", createdUser.Emails[0].Email)
|
|
|
|
// Verify the user has the group
|
|
require.Len(t, createdUser.Groups, 1)
|
|
assert.Equal(t, fleet.ScimUserGroup{ID: group.ID, DisplayName: group.DisplayName}, createdUser.Groups[0])
|
|
|
|
// Modify the user and attempt to modify the Groups field
|
|
updatedUser := fleet.ScimUser{
|
|
ID: user.ID,
|
|
UserName: "replace-test-user", // Same username
|
|
ExternalID: ptr.String("ext-replace-456"), // Changed external ID
|
|
GivenName: ptr.String("Updated"), // Changed given name
|
|
FamilyName: ptr.String("User"), // Same family name
|
|
Active: ptr.Bool(false), // Changed active status
|
|
Emails: []fleet.ScimUserEmail{ // Changed emails
|
|
{
|
|
Email: "updated.user@example.com",
|
|
Primary: ptr.Bool(true),
|
|
Type: ptr.String("work"),
|
|
},
|
|
{
|
|
Email: "personal.user@example.com",
|
|
Primary: ptr.Bool(false),
|
|
Type: ptr.String("home"),
|
|
},
|
|
},
|
|
Groups: []fleet.ScimUserGroup{{ID: 999, DisplayName: "Ignored Group"}}, // Attempt to modify Groups (should be ignored)
|
|
}
|
|
|
|
// Replace the user
|
|
err = ds.ReplaceScimUser(t.Context(), &updatedUser)
|
|
require.Nil(t, err)
|
|
|
|
// Verify the user was updated correctly
|
|
replacedUser, err := ds.ScimUserByID(t.Context(), user.ID)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, updatedUser.UserName, replacedUser.UserName)
|
|
assert.Equal(t, updatedUser.ExternalID, replacedUser.ExternalID)
|
|
assert.Equal(t, updatedUser.GivenName, replacedUser.GivenName)
|
|
assert.Equal(t, updatedUser.FamilyName, replacedUser.FamilyName)
|
|
assert.Equal(t, updatedUser.Active, replacedUser.Active)
|
|
|
|
// Verify emails were replaced
|
|
assert.Equal(t, 2, len(replacedUser.Emails))
|
|
assert.Equal(t, "personal.user@example.com", replacedUser.Emails[0].Email) // Alphabetical order
|
|
assert.Equal(t, "updated.user@example.com", replacedUser.Emails[1].Email)
|
|
|
|
// Verify that the Groups field was NOT modified (it should still contain the original group)
|
|
require.Len(t, replacedUser.Groups, 1, "Groups field should not be modified by ReplaceScimUser")
|
|
assert.Equal(t, group.ID, replacedUser.Groups[0].ID, "Groups field should still contain the original group ID")
|
|
assert.Equal(t, group.DisplayName, replacedUser.Groups[0].DisplayName, "Groups field should still contain the original group display name")
|
|
|
|
// Now remove the user from the group using the group methods
|
|
updatedGroup := fleet.ScimGroup{
|
|
ID: group.ID,
|
|
DisplayName: group.DisplayName,
|
|
ExternalID: group.ExternalID,
|
|
ScimUsers: []uint{}, // Remove the user
|
|
}
|
|
err = ds.ReplaceScimGroup(t.Context(), &updatedGroup)
|
|
require.Nil(t, err)
|
|
|
|
// Verify that the user no longer has the group
|
|
userAfterGroupUpdate, err := ds.ScimUserByID(t.Context(), user.ID)
|
|
require.Nil(t, err)
|
|
assert.Empty(t, userAfterGroupUpdate.Groups, "User should no longer have any groups")
|
|
|
|
// Test replacing a non-existent user
|
|
nonExistentUser := fleet.ScimUser{
|
|
ID: 99999, // Non-existent ID
|
|
UserName: "non-existent",
|
|
ExternalID: ptr.String("ext-non-existent"),
|
|
GivenName: ptr.String("Non"),
|
|
FamilyName: ptr.String("Existent"),
|
|
Active: ptr.Bool(true),
|
|
}
|
|
|
|
err = ds.ReplaceScimUser(t.Context(), &nonExistentUser)
|
|
assert.True(t, fleet.IsNotFound(err))
|
|
}
|
|
|
|
func testReplaceScimUserEmails(t *testing.T, ds *Datastore) {
|
|
// Create a test user
|
|
user := fleet.ScimUser{
|
|
UserName: "email-test-user",
|
|
ExternalID: ptr.String("ext-email-123"),
|
|
GivenName: ptr.String("Email"),
|
|
FamilyName: ptr.String("Test"),
|
|
Active: ptr.Bool(true),
|
|
Emails: []fleet.ScimUserEmail{
|
|
{
|
|
Email: "original.email@example.com",
|
|
Primary: ptr.Bool(true),
|
|
Type: ptr.String("work"),
|
|
},
|
|
},
|
|
}
|
|
|
|
var err error
|
|
user.ID, err = ds.CreateScimUser(t.Context(), &user)
|
|
require.Nil(t, err)
|
|
|
|
// Smoke test email optimization - replacing with the same emails should not update emails
|
|
// First, get the current user to have a reference point
|
|
currentUser, err := ds.ScimUserByID(t.Context(), user.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Create a copy of the user with the same emails
|
|
sameEmailsUser := fleet.ScimUser{
|
|
ID: user.ID,
|
|
UserName: "multi-update@example.com",
|
|
ExternalID: ptr.String("ext-replace-456"),
|
|
GivenName: ptr.String("Multiple"),
|
|
FamilyName: ptr.String("Updates"),
|
|
Active: ptr.Bool(true),
|
|
Emails: currentUser.Emails, // Same emails as current user
|
|
}
|
|
|
|
// Replace the user
|
|
err = ds.ReplaceScimUser(t.Context(), &sameEmailsUser)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the user was updated correctly but emails remain the same
|
|
sameEmailsResult, err := ds.ScimUserByID(t.Context(), user.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, sameEmailsUser.UserName, sameEmailsResult.UserName)
|
|
assert.Equal(t, sameEmailsUser.ExternalID, sameEmailsResult.ExternalID)
|
|
assert.Equal(t, sameEmailsUser.GivenName, sameEmailsResult.GivenName)
|
|
assert.Equal(t, sameEmailsUser.FamilyName, sameEmailsResult.FamilyName)
|
|
assert.Equal(t, sameEmailsUser.Active, sameEmailsResult.Active)
|
|
|
|
// Verify emails are the same as before
|
|
assert.Equal(t, len(currentUser.Emails), len(sameEmailsResult.Emails))
|
|
for i := range currentUser.Emails {
|
|
assert.Equal(t, currentUser.Emails[i].Email, sameEmailsResult.Emails[i].Email)
|
|
assert.Equal(t, currentUser.Emails[i].Type, sameEmailsResult.Emails[i].Type)
|
|
assert.Equal(t, currentUser.Emails[i].Primary, sameEmailsResult.Emails[i].Primary)
|
|
}
|
|
|
|
// Test validation for multiple primary emails
|
|
multiPrimaryUser := fleet.ScimUser{
|
|
ID: user.ID,
|
|
UserName: "multi-primary@example.com",
|
|
ExternalID: ptr.String("ext-multi-primary"),
|
|
GivenName: ptr.String("Multi"),
|
|
FamilyName: ptr.String("Primary"),
|
|
Active: ptr.Bool(true),
|
|
Emails: []fleet.ScimUserEmail{
|
|
{
|
|
Email: "primary1@example.com",
|
|
Primary: ptr.Bool(true), // First primary
|
|
Type: ptr.String("work"),
|
|
},
|
|
{
|
|
Email: "primary2@example.com",
|
|
Primary: ptr.Bool(true), // Second primary - should cause validation error
|
|
Type: ptr.String("home"),
|
|
},
|
|
},
|
|
}
|
|
|
|
// This should fail with a validation error
|
|
err = ds.ReplaceScimUser(t.Context(), &multiPrimaryUser)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "only one email can be marked as primary")
|
|
|
|
// Test email comparison behavior with different combinations of nil/non-nil fields
|
|
// First, create a user with an email that has all fields set
|
|
userWithAllFields := fleet.ScimUser{
|
|
ID: user.ID,
|
|
UserName: "all-fields@example.com",
|
|
ExternalID: ptr.String("ext-all-fields"),
|
|
GivenName: ptr.String("All"),
|
|
FamilyName: ptr.String("Fields"),
|
|
Active: ptr.Bool(true),
|
|
Emails: []fleet.ScimUserEmail{
|
|
{
|
|
Email: "all-fields@example.com",
|
|
Primary: ptr.Bool(true),
|
|
Type: ptr.String("work"),
|
|
},
|
|
},
|
|
}
|
|
|
|
err = ds.ReplaceScimUser(t.Context(), &userWithAllFields)
|
|
require.NoError(t, err)
|
|
|
|
// Now create a user with the same email but with nil Primary field
|
|
userWithNilPrimary := fleet.ScimUser{
|
|
ID: user.ID,
|
|
UserName: "all-fields@example.com",
|
|
ExternalID: ptr.String("ext-all-fields"),
|
|
GivenName: ptr.String("All"),
|
|
FamilyName: ptr.String("Fields"),
|
|
Active: ptr.Bool(true),
|
|
Emails: []fleet.ScimUserEmail{
|
|
{
|
|
Email: "all-fields@example.com",
|
|
Primary: nil, // Changed from true to nil
|
|
Type: ptr.String("work"),
|
|
},
|
|
},
|
|
}
|
|
|
|
// This should update the emails since the Primary field changed
|
|
err = ds.ReplaceScimUser(t.Context(), &userWithNilPrimary)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the email was updated
|
|
var nilPrimaryUser *fleet.ScimUser
|
|
nilPrimaryUser, err = ds.ScimUserByID(t.Context(), user.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, nilPrimaryUser.Emails, 1)
|
|
assert.Equal(t, "all-fields@example.com", nilPrimaryUser.Emails[0].Email)
|
|
assert.Nil(t, nilPrimaryUser.Emails[0].Primary, "Primary field should be nil")
|
|
|
|
// Now create a user with the same email but with nil Type field
|
|
userWithNilType := fleet.ScimUser{
|
|
ID: user.ID,
|
|
UserName: "all-fields@example.com",
|
|
ExternalID: ptr.String("ext-all-fields"),
|
|
GivenName: ptr.String("All"),
|
|
FamilyName: ptr.String("Fields"),
|
|
Active: ptr.Bool(true),
|
|
Emails: []fleet.ScimUserEmail{
|
|
{
|
|
Email: "all-fields@example.com",
|
|
Primary: nil,
|
|
Type: nil, // Changed from "work" to nil
|
|
},
|
|
},
|
|
}
|
|
|
|
// This should update the emails since the Type field changed
|
|
err = ds.ReplaceScimUser(t.Context(), &userWithNilType)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the email was updated
|
|
var nilTypeUser *fleet.ScimUser
|
|
nilTypeUser, err = ds.ScimUserByID(t.Context(), user.ID)
|
|
require.NoError(t, err)
|
|
require.Len(t, nilTypeUser.Emails, 1)
|
|
assert.Equal(t, "all-fields@example.com", nilTypeUser.Emails[0].Email)
|
|
assert.Nil(t, nilTypeUser.Emails[0].Type, "Type field should be nil")
|
|
}
|
|
|
|
func testDeleteScimUser(t *testing.T, ds *Datastore) {
|
|
// Create a test user
|
|
user := fleet.ScimUser{
|
|
UserName: "delete-test-user",
|
|
ExternalID: ptr.String("ext-delete-123"),
|
|
GivenName: ptr.String("Delete"),
|
|
FamilyName: ptr.String("User"),
|
|
Active: ptr.Bool(true),
|
|
Emails: []fleet.ScimUserEmail{
|
|
{
|
|
Email: "delete.user@example.com",
|
|
Primary: ptr.Bool(true),
|
|
Type: ptr.String("work"),
|
|
},
|
|
},
|
|
}
|
|
|
|
var err error
|
|
user.ID, err = ds.CreateScimUser(t.Context(), &user)
|
|
require.Nil(t, err)
|
|
|
|
// Verify the user was created correctly
|
|
createdUser, err := ds.ScimUserByID(t.Context(), user.ID)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, user.UserName, createdUser.UserName)
|
|
|
|
// Delete the user
|
|
err = ds.DeleteScimUser(t.Context(), user.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the user was deleted
|
|
_, err = ds.ScimUserByID(t.Context(), user.ID)
|
|
assert.True(t, fleet.IsNotFound(err))
|
|
|
|
// Test deleting a non-existent user
|
|
err = ds.DeleteScimUser(t.Context(), 99999) // Non-existent ID
|
|
assert.True(t, fleet.IsNotFound(err))
|
|
}
|
|
|
|
func testListScimUsers(t *testing.T, ds *Datastore) {
|
|
// Create test users with different attributes and emails
|
|
users := []fleet.ScimUser{
|
|
{
|
|
UserName: "list-test-user1",
|
|
ExternalID: ptr.String("ext-list-123"),
|
|
GivenName: ptr.String("List"),
|
|
FamilyName: ptr.String("User1"),
|
|
Active: ptr.Bool(true),
|
|
Emails: []fleet.ScimUserEmail{
|
|
{
|
|
Email: "list.user1@example.com",
|
|
Primary: ptr.Bool(true),
|
|
Type: ptr.String("work"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
UserName: "list-test-user2",
|
|
ExternalID: ptr.String("ext-list-456"),
|
|
GivenName: ptr.String("List"),
|
|
FamilyName: ptr.String("User2"),
|
|
Active: ptr.Bool(true),
|
|
Emails: []fleet.ScimUserEmail{
|
|
{
|
|
Email: "list.user2@example.com",
|
|
Primary: ptr.Bool(true),
|
|
Type: ptr.String("work"),
|
|
},
|
|
{
|
|
Email: "personal.user2@example.com",
|
|
Primary: ptr.Bool(false),
|
|
Type: ptr.String("home"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
UserName: "different-user3",
|
|
ExternalID: ptr.String("ext-list-789"),
|
|
GivenName: ptr.String("Different"),
|
|
FamilyName: ptr.String("User3"),
|
|
Active: ptr.Bool(false),
|
|
Emails: []fleet.ScimUserEmail{
|
|
{
|
|
Email: "different.user3@example.com",
|
|
Primary: ptr.Bool(true),
|
|
Type: ptr.String("work"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Create the users
|
|
for i := range users {
|
|
var err error
|
|
users[i].ID, err = ds.CreateScimUser(t.Context(), &users[i])
|
|
require.Nil(t, err)
|
|
}
|
|
|
|
// Create a group and associate it with the first user
|
|
group := fleet.ScimGroup{
|
|
DisplayName: "Test Group for ListUsers",
|
|
ExternalID: ptr.String("ext-group-for-list"),
|
|
ScimUsers: []uint{users[0].ID},
|
|
}
|
|
var err error
|
|
group.ID, err = ds.CreateScimGroup(t.Context(), &group)
|
|
require.NoError(t, err)
|
|
|
|
// Test 1: List all users without filters
|
|
allUsers, totalResults, err := ds.ListScimUsers(t.Context(), fleet.ScimUsersListOptions{
|
|
ScimListOptions: fleet.ScimListOptions{
|
|
StartIndex: 1,
|
|
PerPage: 10,
|
|
},
|
|
})
|
|
require.Nil(t, err)
|
|
assert.Equal(t, 3, len(allUsers))
|
|
assert.Equal(t, uint(3), totalResults)
|
|
|
|
// Verify that our test users are in the results
|
|
foundUsers := 0
|
|
for _, u := range allUsers {
|
|
for _, testUser := range users {
|
|
if u.ID == testUser.ID {
|
|
foundUsers++
|
|
assert.False(t, u.UpdatedAt.IsZero(), "UpdatedAt should not be zero")
|
|
|
|
// Verify Groups field for the first user
|
|
if testUser.ID == users[0].ID {
|
|
require.Len(t, u.Groups, 1, "First user should have exactly one group")
|
|
assert.Equal(t, group.ID, u.Groups[0].ID, "First user should be in the test group")
|
|
assert.Equal(t, group.DisplayName, u.Groups[0].DisplayName, "Group display name should match")
|
|
} else {
|
|
assert.Empty(t, u.Groups, "Other users should not have groups")
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
}
|
|
assert.Equal(t, 3, foundUsers)
|
|
|
|
// Test 2: Pagination - first page with 2 items
|
|
page1Users, totalPage1, err := ds.ListScimUsers(t.Context(), fleet.ScimUsersListOptions{
|
|
ScimListOptions: fleet.ScimListOptions{
|
|
StartIndex: 1,
|
|
PerPage: 2,
|
|
},
|
|
})
|
|
require.Nil(t, err)
|
|
assert.Equal(t, 2, len(page1Users))
|
|
assert.Equal(t, uint(3), totalPage1) // Total should still be 3
|
|
|
|
// Test 3: Pagination - second page with 2 items
|
|
page2Users, totalPage2, err := ds.ListScimUsers(t.Context(), fleet.ScimUsersListOptions{
|
|
ScimListOptions: fleet.ScimListOptions{
|
|
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.Equal(t, 1, len(page2Users))
|
|
assert.Equal(t, uint(3), totalPage2) // Total should still be 3
|
|
|
|
// Verify that page1 and page2 contain different users
|
|
for _, p1User := range page1Users {
|
|
for _, p2User := range page2Users {
|
|
assert.NotEqual(t, p1User.ID, p2User.ID, "Users should not appear on multiple pages")
|
|
}
|
|
}
|
|
|
|
// Test 4: Filter by username
|
|
listUsers, totalListUsers, err := ds.ListScimUsers(t.Context(), fleet.ScimUsersListOptions{
|
|
ScimListOptions: fleet.ScimListOptions{
|
|
StartIndex: 1,
|
|
PerPage: 10,
|
|
},
|
|
UserNameFilter: ptr.String("list-test-user2"),
|
|
})
|
|
|
|
require.Nil(t, err)
|
|
require.Len(t, listUsers, 1)
|
|
assert.Equal(t, uint(1), totalListUsers)
|
|
assert.Equal(t, "list-test-user2", listUsers[0].UserName)
|
|
assert.False(t, listUsers[0].UpdatedAt.IsZero(), "UpdatedAt should not be zero")
|
|
|
|
// Test 5: Filter by email type and value
|
|
homeEmailUsers, totalHomeEmailUsers, err := ds.ListScimUsers(t.Context(), fleet.ScimUsersListOptions{
|
|
ScimListOptions: fleet.ScimListOptions{
|
|
StartIndex: 1,
|
|
PerPage: 10,
|
|
},
|
|
EmailTypeFilter: ptr.String("home"),
|
|
EmailValueFilter: ptr.String("personal.user2@example.com"),
|
|
})
|
|
require.Nil(t, err)
|
|
require.Len(t, homeEmailUsers, 1)
|
|
assert.Equal(t, uint(1), totalHomeEmailUsers)
|
|
assert.Equal(t, users[1].ID, homeEmailUsers[0].ID)
|
|
assert.Equal(t, 2, len(homeEmailUsers[0].Emails))
|
|
assert.False(t, homeEmailUsers[0].UpdatedAt.IsZero(), "UpdatedAt should not be zero")
|
|
|
|
// Test 6: Filter by email type and value - work emails
|
|
workEmailUsers, totalWorkEmailUsers, err := ds.ListScimUsers(t.Context(), fleet.ScimUsersListOptions{
|
|
ScimListOptions: fleet.ScimListOptions{
|
|
StartIndex: 1,
|
|
PerPage: 10,
|
|
},
|
|
EmailTypeFilter: ptr.String("work"),
|
|
EmailValueFilter: ptr.String("different.user3@example.com"),
|
|
})
|
|
require.Nil(t, err)
|
|
assert.Len(t, workEmailUsers, 1)
|
|
assert.Equal(t, uint(1), totalWorkEmailUsers)
|
|
assert.False(t, workEmailUsers[0].UpdatedAt.IsZero(), "UpdatedAt should not be zero")
|
|
|
|
// Test 7: No results for non-matching filters
|
|
noUsers, totalNoUsers1, err := ds.ListScimUsers(t.Context(), fleet.ScimUsersListOptions{
|
|
ScimListOptions: fleet.ScimListOptions{
|
|
StartIndex: 1,
|
|
PerPage: 10,
|
|
},
|
|
UserNameFilter: ptr.String("nonexistent"),
|
|
})
|
|
require.Nil(t, err)
|
|
assert.Empty(t, noUsers)
|
|
assert.Equal(t, uint(0), totalNoUsers1)
|
|
|
|
noUsers, totalNoUsers2, err := ds.ListScimUsers(t.Context(), fleet.ScimUsersListOptions{
|
|
ScimListOptions: fleet.ScimListOptions{
|
|
StartIndex: 1,
|
|
PerPage: 10,
|
|
},
|
|
EmailTypeFilter: ptr.String("nonexistent"),
|
|
EmailValueFilter: ptr.String("nonexistent"),
|
|
})
|
|
require.Nil(t, err)
|
|
assert.Empty(t, noUsers)
|
|
assert.Equal(t, uint(0), totalNoUsers2)
|
|
}
|
|
|
|
func testScimGroupCreate(t *testing.T, ds *Datastore) {
|
|
// Create test users first
|
|
users := createTestScimUsers(t, ds)
|
|
userIDs := make([]uint, len(users))
|
|
for i, user := range users {
|
|
userIDs[i] = user.ID
|
|
}
|
|
|
|
groupsToCreate := []fleet.ScimGroup{
|
|
{
|
|
DisplayName: "Group1",
|
|
ExternalID: nil,
|
|
ScimUsers: []uint{},
|
|
},
|
|
{
|
|
DisplayName: "Group2",
|
|
ExternalID: ptr.String("ext-group-123"),
|
|
ScimUsers: []uint{userIDs[0]},
|
|
},
|
|
{
|
|
DisplayName: "Group3",
|
|
ExternalID: ptr.String("ext-group-456"),
|
|
ScimUsers: userIDs,
|
|
},
|
|
}
|
|
|
|
for _, g := range groupsToCreate {
|
|
var err error
|
|
groupCopy := g
|
|
groupCopy.ID, err = ds.CreateScimGroup(t.Context(), &g)
|
|
require.NoError(t, err)
|
|
|
|
verify, err := ds.ScimGroupByID(t.Context(), g.ID, false)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, groupCopy.ID, verify.ID)
|
|
assert.Equal(t, groupCopy.DisplayName, verify.DisplayName)
|
|
assert.Equal(t, groupCopy.ExternalID, verify.ExternalID)
|
|
|
|
// Verify users
|
|
assert.Equal(t, len(groupCopy.ScimUsers), len(verify.ScimUsers))
|
|
if len(groupCopy.ScimUsers) > 0 {
|
|
// Sort the user IDs for comparison
|
|
sort.Slice(groupCopy.ScimUsers, func(i, j int) bool {
|
|
return groupCopy.ScimUsers[i] < groupCopy.ScimUsers[j]
|
|
})
|
|
sort.Slice(verify.ScimUsers, func(i, j int) bool {
|
|
return verify.ScimUsers[i] < verify.ScimUsers[j]
|
|
})
|
|
assert.Equal(t, groupCopy.ScimUsers, verify.ScimUsers)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testScimGroupCreateValidation(t *testing.T, ds *Datastore) {
|
|
// Test validation for ExternalID
|
|
longString := strings.Repeat("a", fleet.SCIMMaxFieldLength+1) // String longer than allowed
|
|
|
|
// Test ExternalID validation
|
|
groupWithLongExternalID := fleet.ScimGroup{
|
|
DisplayName: "Valid Name",
|
|
ExternalID: ptr.String(longString),
|
|
ScimUsers: []uint{},
|
|
}
|
|
_, err := ds.CreateScimGroup(t.Context(), &groupWithLongExternalID)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "external_id exceeds maximum length")
|
|
|
|
// Test DisplayName validation
|
|
groupWithLongDisplayName := fleet.ScimGroup{
|
|
DisplayName: longString,
|
|
ExternalID: ptr.String("valid-external-id"),
|
|
ScimUsers: []uint{},
|
|
}
|
|
_, err = ds.CreateScimGroup(t.Context(), &groupWithLongDisplayName)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "display_name exceeds maximum length")
|
|
|
|
// Test with valid values
|
|
validGroup := fleet.ScimGroup{
|
|
DisplayName: "Valid Name",
|
|
ExternalID: ptr.String("valid-external-id"),
|
|
ScimUsers: []uint{},
|
|
}
|
|
_, err = ds.CreateScimGroup(t.Context(), &validGroup)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func testScimGroupByID(t *testing.T, ds *Datastore) {
|
|
// Create test users first
|
|
users := createTestScimUsers(t, ds)
|
|
userIDs := make([]uint, len(users))
|
|
for i, user := range users {
|
|
userIDs[i] = user.ID
|
|
}
|
|
|
|
// Create test groups
|
|
groups := createTestScimGroups(t, ds, userIDs)
|
|
|
|
// Test retrieving each group
|
|
for _, tt := range groups {
|
|
returned, err := ds.ScimGroupByID(t.Context(), tt.ID, false)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, tt.ID, returned.ID)
|
|
assert.Equal(t, tt.DisplayName, returned.DisplayName)
|
|
assert.Equal(t, tt.ExternalID, returned.ExternalID)
|
|
|
|
// Verify users
|
|
assert.Equal(t, len(tt.ScimUsers), len(returned.ScimUsers))
|
|
if len(tt.ScimUsers) > 0 {
|
|
// Sort the user IDs for comparison
|
|
sort.Slice(tt.ScimUsers, func(i, j int) bool {
|
|
return tt.ScimUsers[i] < tt.ScimUsers[j]
|
|
})
|
|
sort.Slice(returned.ScimUsers, func(i, j int) bool {
|
|
return returned.ScimUsers[i] < returned.ScimUsers[j]
|
|
})
|
|
assert.Equal(t, tt.ScimUsers, returned.ScimUsers)
|
|
}
|
|
}
|
|
|
|
// Test missing group
|
|
_, err := ds.ScimGroupByID(t.Context(), 10000000000, false)
|
|
assert.True(t, fleet.IsNotFound(err))
|
|
|
|
// Test with excludeUsers=true
|
|
for _, tt := range groups {
|
|
returnedWithoutUsers, err := ds.ScimGroupByID(t.Context(), tt.ID, true)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, tt.ID, returnedWithoutUsers.ID)
|
|
assert.Equal(t, tt.DisplayName, returnedWithoutUsers.DisplayName)
|
|
assert.Equal(t, tt.ExternalID, returnedWithoutUsers.ExternalID)
|
|
// Verify that users were not fetched
|
|
assert.Empty(t, returnedWithoutUsers.ScimUsers, "ScimUsers should be empty when excludeUsers=true")
|
|
}
|
|
}
|
|
|
|
func testScimGroupByDisplayName(t *testing.T, ds *Datastore) {
|
|
// Create test users first
|
|
users := createTestScimUsers(t, ds)
|
|
userIDs := make([]uint, len(users))
|
|
for i, user := range users {
|
|
userIDs[i] = user.ID
|
|
}
|
|
|
|
// Create test groups
|
|
groups := createTestScimGroups(t, ds, userIDs)
|
|
|
|
// Test retrieving each group by display name
|
|
for _, tt := range groups {
|
|
returned, err := ds.ScimGroupByDisplayName(t.Context(), tt.DisplayName)
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, tt.ID, returned.ID)
|
|
assert.Equal(t, tt.DisplayName, returned.DisplayName)
|
|
assert.Equal(t, tt.ExternalID, returned.ExternalID)
|
|
|
|
// Verify users
|
|
assert.Equal(t, len(tt.ScimUsers), len(returned.ScimUsers))
|
|
if len(tt.ScimUsers) > 0 {
|
|
// Sort the user IDs for comparison
|
|
sort.Slice(tt.ScimUsers, func(i, j int) bool {
|
|
return tt.ScimUsers[i] < tt.ScimUsers[j]
|
|
})
|
|
sort.Slice(returned.ScimUsers, func(i, j int) bool {
|
|
return returned.ScimUsers[i] < returned.ScimUsers[j]
|
|
})
|
|
assert.Equal(t, tt.ScimUsers, returned.ScimUsers)
|
|
}
|
|
}
|
|
|
|
// Test missing group
|
|
_, err := ds.ScimGroupByDisplayName(t.Context(), "Nonexistent Group")
|
|
assert.True(t, fleet.IsNotFound(err))
|
|
}
|
|
|
|
// createTestScimGroups creates test SCIM groups for testing
|
|
func createTestScimGroups(t *testing.T, ds *Datastore, userIDs []uint) []*fleet.ScimGroup {
|
|
createGroups := []fleet.ScimGroup{
|
|
{
|
|
DisplayName: "Test Group 1",
|
|
ExternalID: ptr.String("ext-test-group-123"),
|
|
ScimUsers: []uint{},
|
|
},
|
|
{
|
|
DisplayName: "Test Group 2",
|
|
ExternalID: ptr.String("ext-test-group-456"),
|
|
ScimUsers: []uint{userIDs[0]},
|
|
},
|
|
{
|
|
DisplayName: "Test Group 3",
|
|
ExternalID: ptr.String("ext-test-group-789"),
|
|
ScimUsers: userIDs,
|
|
},
|
|
}
|
|
|
|
var groups []*fleet.ScimGroup
|
|
for _, g := range createGroups {
|
|
var err error
|
|
g.ID, err = ds.CreateScimGroup(t.Context(), &g)
|
|
require.NoError(t, err)
|
|
groups = append(groups, &g)
|
|
}
|
|
return groups
|
|
}
|
|
|
|
func testReplaceScimGroup(t *testing.T, ds *Datastore) {
|
|
// Create test users first
|
|
users := createTestScimUsers(t, ds)
|
|
userIDs := make([]uint, len(users))
|
|
for i, user := range users {
|
|
userIDs[i] = user.ID
|
|
}
|
|
|
|
// Create a test group
|
|
group := fleet.ScimGroup{
|
|
DisplayName: "Replace Test Group",
|
|
ExternalID: ptr.String("ext-replace-group-123"),
|
|
ScimUsers: []uint{userIDs[0]},
|
|
}
|
|
|
|
var err error
|
|
group.ID, err = ds.CreateScimGroup(t.Context(), &group)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the group was created correctly
|
|
createdGroup, err := ds.ScimGroupByID(t.Context(), group.ID, false)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, group.DisplayName, createdGroup.DisplayName)
|
|
assert.Equal(t, group.ExternalID, createdGroup.ExternalID)
|
|
assert.Equal(t, 1, len(createdGroup.ScimUsers))
|
|
assert.Equal(t, userIDs[0], createdGroup.ScimUsers[0])
|
|
|
|
// Modify the group
|
|
updatedGroup := fleet.ScimGroup{
|
|
ID: group.ID,
|
|
DisplayName: "Updated Group",
|
|
ExternalID: ptr.String("ext-replace-group-456"),
|
|
ScimUsers: userIDs, // Add all users
|
|
}
|
|
|
|
// Replace the group
|
|
err = ds.ReplaceScimGroup(t.Context(), &updatedGroup)
|
|
require.Nil(t, err)
|
|
|
|
// Verify the group was updated correctly
|
|
replacedGroup, err := ds.ScimGroupByID(t.Context(), group.ID, false)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, updatedGroup.DisplayName, replacedGroup.DisplayName)
|
|
assert.Equal(t, updatedGroup.ExternalID, replacedGroup.ExternalID)
|
|
|
|
// Verify users were updated
|
|
assert.Equal(t, len(userIDs), len(replacedGroup.ScimUsers))
|
|
|
|
// Sort the user IDs for comparison
|
|
sort.Slice(userIDs, func(i, j int) bool {
|
|
return userIDs[i] < userIDs[j]
|
|
})
|
|
sort.Slice(replacedGroup.ScimUsers, func(i, j int) bool {
|
|
return replacedGroup.ScimUsers[i] < replacedGroup.ScimUsers[j]
|
|
})
|
|
assert.Equal(t, userIDs, replacedGroup.ScimUsers)
|
|
|
|
// Test replacing a non-existent group
|
|
nonExistentGroup := fleet.ScimGroup{
|
|
ID: 99999, // Non-existent ID
|
|
DisplayName: "Non-existent",
|
|
ExternalID: ptr.String("ext-non-existent"),
|
|
ScimUsers: []uint{},
|
|
}
|
|
|
|
err = ds.ReplaceScimGroup(t.Context(), &nonExistentGroup)
|
|
assert.True(t, fleet.IsNotFound(err))
|
|
}
|
|
|
|
func testScimGroupReplaceValidation(t *testing.T, ds *Datastore) {
|
|
// Create a valid group first
|
|
group := fleet.ScimGroup{
|
|
DisplayName: "Validation Test Group",
|
|
ExternalID: ptr.String("ext-validation-group"),
|
|
ScimUsers: []uint{},
|
|
}
|
|
|
|
var err error
|
|
group.ID, err = ds.CreateScimGroup(t.Context(), &group)
|
|
require.NoError(t, err)
|
|
|
|
// Test validation for ExternalID
|
|
longString := strings.Repeat("a", fleet.SCIMMaxFieldLength+1) // String longer than allowed
|
|
|
|
// Test ExternalID validation
|
|
groupWithLongExternalID := fleet.ScimGroup{
|
|
ID: group.ID,
|
|
DisplayName: "Valid Name",
|
|
ExternalID: ptr.String(longString),
|
|
ScimUsers: []uint{},
|
|
}
|
|
err = ds.ReplaceScimGroup(t.Context(), &groupWithLongExternalID)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "external_id exceeds maximum length")
|
|
|
|
// Test DisplayName validation
|
|
groupWithLongDisplayName := fleet.ScimGroup{
|
|
ID: group.ID,
|
|
DisplayName: longString,
|
|
ExternalID: ptr.String("valid-external-id"),
|
|
ScimUsers: []uint{},
|
|
}
|
|
err = ds.ReplaceScimGroup(t.Context(), &groupWithLongDisplayName)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "display_name exceeds maximum length")
|
|
|
|
// Test with valid values
|
|
validGroup := fleet.ScimGroup{
|
|
ID: group.ID,
|
|
DisplayName: "Updated Valid Name",
|
|
ExternalID: ptr.String("updated-valid-external-id"),
|
|
ScimUsers: []uint{},
|
|
}
|
|
err = ds.ReplaceScimGroup(t.Context(), &validGroup)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func testDeleteScimGroup(t *testing.T, ds *Datastore) {
|
|
// Create test users first
|
|
users := createTestScimUsers(t, ds)
|
|
userIDs := make([]uint, len(users))
|
|
for i, user := range users {
|
|
userIDs[i] = user.ID
|
|
}
|
|
|
|
// Create a test group
|
|
group := fleet.ScimGroup{
|
|
DisplayName: "Delete Test Group",
|
|
ExternalID: ptr.String("ext-delete-group"),
|
|
ScimUsers: userIDs,
|
|
}
|
|
|
|
var err error
|
|
group.ID, err = ds.CreateScimGroup(t.Context(), &group)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the group was created correctly
|
|
createdGroup, err := ds.ScimGroupByID(t.Context(), group.ID, false)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, group.DisplayName, createdGroup.DisplayName)
|
|
|
|
// Delete the group
|
|
err = ds.DeleteScimGroup(t.Context(), group.ID)
|
|
require.Nil(t, err)
|
|
|
|
// Verify the group was deleted
|
|
_, err = ds.ScimGroupByID(t.Context(), group.ID, false)
|
|
assert.True(t, fleet.IsNotFound(err))
|
|
|
|
// Test deleting a non-existent group
|
|
err = ds.DeleteScimGroup(t.Context(), 99999) // Non-existent ID
|
|
assert.True(t, fleet.IsNotFound(err))
|
|
}
|
|
|
|
func testListScimGroups(t *testing.T, ds *Datastore) {
|
|
// Create test users first
|
|
users := createTestScimUsers(t, ds)
|
|
userIDs := make([]uint, len(users))
|
|
for i, user := range users {
|
|
userIDs[i] = user.ID
|
|
}
|
|
|
|
// Create test groups
|
|
groups := []fleet.ScimGroup{
|
|
{
|
|
DisplayName: "List Test Group 1",
|
|
ExternalID: ptr.String("ext-list-group-123"),
|
|
ScimUsers: []uint{},
|
|
},
|
|
{
|
|
DisplayName: "List Test Group 2",
|
|
ExternalID: ptr.String("ext-list-group-456"),
|
|
ScimUsers: []uint{userIDs[0]},
|
|
},
|
|
{
|
|
DisplayName: "List Test Group 3",
|
|
ExternalID: ptr.String("ext-list-group-789"),
|
|
ScimUsers: userIDs,
|
|
},
|
|
}
|
|
|
|
// Create the groups
|
|
for i := range groups {
|
|
var err error
|
|
groups[i].ID, err = ds.CreateScimGroup(t.Context(), &groups[i])
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Test 1: List all groups
|
|
allGroups, totalResults, err := ds.ListScimGroups(t.Context(), fleet.ScimGroupsListOptions{
|
|
ScimListOptions: fleet.ScimListOptions{
|
|
StartIndex: 1,
|
|
PerPage: 10,
|
|
},
|
|
})
|
|
require.Nil(t, err)
|
|
assert.GreaterOrEqual(t, len(allGroups), 3) // There might be other groups from previous tests
|
|
assert.GreaterOrEqual(t, totalResults, uint(3))
|
|
|
|
// Verify that our test groups are in the results
|
|
foundGroups := 0
|
|
for _, g := range allGroups {
|
|
for _, testGroup := range groups {
|
|
if g.ID == testGroup.ID {
|
|
foundGroups++
|
|
break
|
|
}
|
|
}
|
|
}
|
|
assert.Equal(t, 3, foundGroups)
|
|
|
|
// Test 2: Pagination - first page with 2 items
|
|
page1Groups, totalPage1, err := ds.ListScimGroups(t.Context(), fleet.ScimGroupsListOptions{
|
|
ScimListOptions: fleet.ScimListOptions{
|
|
StartIndex: 1,
|
|
PerPage: 2,
|
|
},
|
|
})
|
|
require.Nil(t, err)
|
|
assert.Equal(t, 2, len(page1Groups))
|
|
assert.GreaterOrEqual(t, totalPage1, uint(3)) // Total should be at least 3
|
|
|
|
// Test 3: Pagination - second page with 2 items
|
|
page2Groups, totalPage2, err := ds.ListScimGroups(t.Context(), fleet.ScimGroupsListOptions{
|
|
ScimListOptions: fleet.ScimListOptions{
|
|
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
|
|
assert.GreaterOrEqual(t, totalPage2, uint(3)) // Total should be at least 3
|
|
|
|
// Verify that page1 and page2 contain different groups
|
|
for _, p1Group := range page1Groups {
|
|
for _, p2Group := range page2Groups {
|
|
assert.NotEqual(t, p1Group.ID, p2Group.ID, "Groups should not appear on multiple pages")
|
|
}
|
|
}
|
|
|
|
// Test 4: Filter by display name
|
|
displayName := "List Test Group 2"
|
|
filteredGroups, totalFilteredResults, err := ds.ListScimGroups(t.Context(), fleet.ScimGroupsListOptions{
|
|
ScimListOptions: fleet.ScimListOptions{
|
|
StartIndex: 1,
|
|
PerPage: 10,
|
|
},
|
|
DisplayNameFilter: &displayName,
|
|
})
|
|
require.Nil(t, err)
|
|
assert.Equal(t, 1, len(filteredGroups), "Should find exactly one group with the specified display name")
|
|
assert.Equal(t, uint(1), totalFilteredResults)
|
|
assert.Equal(t, displayName, filteredGroups[0].DisplayName)
|
|
|
|
// Test 5: Filter by non-existent display name
|
|
nonExistentName := "Non-Existent Group"
|
|
emptyResults, totalEmptyResults, err := ds.ListScimGroups(t.Context(), fleet.ScimGroupsListOptions{
|
|
ScimListOptions: fleet.ScimListOptions{
|
|
StartIndex: 1,
|
|
PerPage: 10,
|
|
},
|
|
DisplayNameFilter: &nonExistentName,
|
|
})
|
|
require.Nil(t, err)
|
|
assert.Empty(t, emptyResults, "Should find no groups with a non-existent display name")
|
|
assert.Equal(t, uint(0), totalEmptyResults)
|
|
|
|
// Test 6: List groups with ExcludeUsers=true
|
|
groupsWithoutUsers, totalWithoutUsers, err := ds.ListScimGroups(t.Context(), fleet.ScimGroupsListOptions{
|
|
ScimListOptions: fleet.ScimListOptions{
|
|
StartIndex: 1,
|
|
PerPage: 10,
|
|
},
|
|
ExcludeUsers: true,
|
|
})
|
|
require.Nil(t, err)
|
|
assert.GreaterOrEqual(t, len(groupsWithoutUsers), 3, "Should find at least 3 groups")
|
|
assert.Equal(t, totalResults, totalWithoutUsers, "Total count should be the same with or without users")
|
|
|
|
// Verify that users were not fetched
|
|
for _, group := range groupsWithoutUsers {
|
|
assert.Empty(t, group.ScimUsers, "ScimUsers should be empty when ExcludeUsers=true")
|
|
}
|
|
}
|
|
|
|
func testScimUserCreateValidation(t *testing.T, ds *Datastore) {
|
|
// Test validation for ExternalID
|
|
longString := strings.Repeat("a", fleet.SCIMMaxFieldLength+1) // String longer than SCIMMaxFieldLength
|
|
|
|
// Test ExternalID validation
|
|
userWithLongExternalID := fleet.ScimUser{
|
|
UserName: "valid-username",
|
|
ExternalID: ptr.String(longString),
|
|
GivenName: ptr.String("Valid"),
|
|
FamilyName: ptr.String("Name"),
|
|
Active: ptr.Bool(true),
|
|
}
|
|
_, err := ds.CreateScimUser(t.Context(), &userWithLongExternalID)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "external_id exceeds maximum length")
|
|
|
|
// Test UserName validation
|
|
userWithLongUserName := fleet.ScimUser{
|
|
UserName: longString,
|
|
ExternalID: ptr.String("valid-external-id"),
|
|
GivenName: ptr.String("Valid"),
|
|
FamilyName: ptr.String("Name"),
|
|
Active: ptr.Bool(true),
|
|
}
|
|
_, err = ds.CreateScimUser(t.Context(), &userWithLongUserName)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "user_name exceeds maximum length")
|
|
|
|
// Test GivenName validation
|
|
userWithLongGivenName := fleet.ScimUser{
|
|
UserName: "valid-username",
|
|
ExternalID: ptr.String("valid-external-id"),
|
|
GivenName: ptr.String(longString),
|
|
FamilyName: ptr.String("Name"),
|
|
Active: ptr.Bool(true),
|
|
}
|
|
_, err = ds.CreateScimUser(t.Context(), &userWithLongGivenName)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "given_name exceeds maximum length")
|
|
|
|
// Test FamilyName validation
|
|
userWithLongFamilyName := fleet.ScimUser{
|
|
UserName: "valid-username",
|
|
ExternalID: ptr.String("valid-external-id"),
|
|
GivenName: ptr.String("Valid"),
|
|
FamilyName: ptr.String(longString),
|
|
Active: ptr.Bool(true),
|
|
}
|
|
_, err = ds.CreateScimUser(t.Context(), &userWithLongFamilyName)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "family_name exceeds maximum length")
|
|
|
|
// Test with valid values
|
|
validUser := fleet.ScimUser{
|
|
UserName: "valid-username",
|
|
ExternalID: ptr.String("valid-external-id"),
|
|
GivenName: ptr.String("Valid"),
|
|
FamilyName: ptr.String("Name"),
|
|
Active: ptr.Bool(true),
|
|
}
|
|
_, err = ds.CreateScimUser(t.Context(), &validUser)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func testScimUserByHostID(t *testing.T, ds *Datastore) {
|
|
// Create a test SCIM user with emails
|
|
user1 := fleet.ScimUser{
|
|
UserName: "host-test-user1",
|
|
ExternalID: ptr.String("ext-host-123"),
|
|
GivenName: ptr.String("Host"),
|
|
FamilyName: ptr.String("User"),
|
|
Active: ptr.Bool(true),
|
|
Emails: []fleet.ScimUserEmail{
|
|
{
|
|
Email: "host.user@example.com",
|
|
Primary: ptr.Bool(true),
|
|
Type: ptr.String("work"),
|
|
},
|
|
},
|
|
Department: ptr.String("Engineering"),
|
|
}
|
|
|
|
var err error
|
|
user1.ID, err = ds.CreateScimUser(t.Context(), &user1)
|
|
require.Nil(t, err)
|
|
|
|
// Create a group and associate it with the user
|
|
group := fleet.ScimGroup{
|
|
DisplayName: "Host Test Group",
|
|
ExternalID: ptr.String("ext-host-group"),
|
|
ScimUsers: []uint{user1.ID},
|
|
}
|
|
group.ID, err = ds.CreateScimGroup(t.Context(), &group)
|
|
require.NoError(t, err)
|
|
|
|
// Create a second test SCIM user without emails, without groups nor department
|
|
user2 := fleet.ScimUser{
|
|
UserName: "host-test-user2",
|
|
ExternalID: ptr.String("ext-host-456"),
|
|
GivenName: ptr.String("No"),
|
|
FamilyName: ptr.String("Emails"),
|
|
Active: ptr.Bool(true),
|
|
Emails: []fleet.ScimUserEmail{},
|
|
Department: nil,
|
|
}
|
|
user2.ID, err = ds.CreateScimUser(t.Context(), &user2)
|
|
require.Nil(t, err)
|
|
|
|
// Create test hosts
|
|
hostID1 := uint(1000001) // Use a dummy host ID for testing
|
|
hostID2 := uint(1000002) // Use a different dummy host ID for testing
|
|
|
|
// Associate the hosts with the SCIM users
|
|
_, err = ds.writer(t.Context()).ExecContext(
|
|
t.Context(),
|
|
"INSERT INTO host_scim_user (host_id, scim_user_id) VALUES (?, ?), (?, ?)",
|
|
hostID1, user1.ID,
|
|
hostID2, user2.ID,
|
|
)
|
|
require.Nil(t, err)
|
|
|
|
// Test 1: Get SCIM user with emails and groups by host ID
|
|
result1, err := ds.ScimUserByHostID(t.Context(), hostID1)
|
|
assert.Nil(t, err)
|
|
assert.NotNil(t, result1)
|
|
assert.Equal(t, user1.ID, result1.ID)
|
|
assert.Equal(t, user1.UserName, result1.UserName)
|
|
assert.Equal(t, user1.ExternalID, result1.ExternalID)
|
|
assert.Equal(t, user1.GivenName, result1.GivenName)
|
|
assert.Equal(t, user1.FamilyName, result1.FamilyName)
|
|
assert.Equal(t, user1.Active, result1.Active)
|
|
assert.Equal(t, user1.Department, result1.Department)
|
|
assert.False(t, result1.UpdatedAt.IsZero(), "UpdatedAt should not be zero")
|
|
|
|
// Verify emails
|
|
require.Equal(t, 1, len(result1.Emails))
|
|
assert.Equal(t, "host.user@example.com", result1.Emails[0].Email)
|
|
assert.Equal(t, true, *result1.Emails[0].Primary)
|
|
assert.Equal(t, "work", *result1.Emails[0].Type)
|
|
|
|
// Verify groups
|
|
require.Equal(t, 1, len(result1.Groups))
|
|
assert.Equal(t, group.ID, result1.Groups[0].ID)
|
|
assert.Equal(t, group.DisplayName, result1.Groups[0].DisplayName)
|
|
|
|
// Test 2: Get SCIM user without emails and without groups by host ID
|
|
result2, err := ds.ScimUserByHostID(t.Context(), hostID2)
|
|
assert.Nil(t, err)
|
|
assert.NotNil(t, result2)
|
|
assert.Equal(t, user2.ID, result2.ID)
|
|
assert.Equal(t, user2.UserName, result2.UserName)
|
|
assert.Equal(t, user2.ExternalID, result2.ExternalID)
|
|
assert.Equal(t, user2.GivenName, result2.GivenName)
|
|
assert.Equal(t, user2.FamilyName, result2.FamilyName)
|
|
assert.Equal(t, user2.Active, result2.Active)
|
|
assert.Equal(t, user2.Department, result2.Department)
|
|
assert.False(t, result2.UpdatedAt.IsZero(), "UpdatedAt should not be zero")
|
|
|
|
// Verify no emails
|
|
assert.Empty(t, result2.Emails)
|
|
|
|
// Verify no groups
|
|
assert.Empty(t, result2.Groups)
|
|
|
|
// Test 3: Get SCIM user for a host that doesn't have an associated user
|
|
nonExistentHostID := uint(9999999)
|
|
_, err = ds.ScimUserByHostID(t.Context(), nonExistentHostID)
|
|
assert.NotNil(t, err)
|
|
assert.True(t, fleet.IsNotFound(err))
|
|
}
|
|
|
|
func testScimUserByUserNameOrEmail(t *testing.T, ds *Datastore) {
|
|
// Create test users with different attributes and emails
|
|
users := []fleet.ScimUser{
|
|
{
|
|
UserName: "email-test-user1",
|
|
ExternalID: ptr.String("ext-email-123"),
|
|
GivenName: ptr.String("Email"),
|
|
FamilyName: ptr.String("User1"),
|
|
Active: ptr.Bool(true),
|
|
Emails: []fleet.ScimUserEmail{
|
|
{
|
|
Email: "email.user1@example.com",
|
|
Primary: ptr.Bool(true),
|
|
Type: ptr.String("work"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
UserName: "email-test-user2",
|
|
ExternalID: ptr.String("ext-email-456"),
|
|
GivenName: ptr.String("Email"),
|
|
FamilyName: ptr.String("User2"),
|
|
Active: ptr.Bool(true),
|
|
Emails: []fleet.ScimUserEmail{
|
|
{
|
|
Email: "email.user2@example.com",
|
|
Primary: ptr.Bool(true),
|
|
Type: ptr.String("work"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
UserName: "duplicate-email-user1",
|
|
ExternalID: ptr.String("ext-dup-123"),
|
|
GivenName: ptr.String("Duplicate"),
|
|
FamilyName: ptr.String("Email1"),
|
|
Active: ptr.Bool(true),
|
|
Emails: []fleet.ScimUserEmail{
|
|
{
|
|
Email: "duplicate@example.com", // Duplicate email
|
|
Primary: ptr.Bool(true),
|
|
Type: ptr.String("work"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
UserName: "duplicate-email-user2",
|
|
ExternalID: ptr.String("ext-dup-456"),
|
|
GivenName: ptr.String("Duplicate"),
|
|
FamilyName: ptr.String("Email2"),
|
|
Active: ptr.Bool(true),
|
|
Emails: []fleet.ScimUserEmail{
|
|
{
|
|
Email: "duplicate@example.com", // Duplicate email
|
|
Primary: ptr.Bool(true),
|
|
Type: ptr.String("work"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// Create the users
|
|
for i := range users {
|
|
var err error
|
|
users[i].ID, err = ds.CreateScimUser(t.Context(), &users[i])
|
|
require.Nil(t, err)
|
|
}
|
|
|
|
// Test 1: Find user by userName
|
|
email := "email.user1@example.com"
|
|
user, err := ds.ScimUserByUserNameOrEmail(t.Context(), "email-test-user1", email)
|
|
assert.Nil(t, err)
|
|
require.NotNil(t, user)
|
|
assert.Equal(t, "email-test-user1", user.UserName)
|
|
assert.Equal(t, users[0].ID, user.ID)
|
|
assert.False(t, user.UpdatedAt.IsZero(), "UpdatedAt should not be zero")
|
|
|
|
// Test 2: Find user by email when userName is empty
|
|
user, err = ds.ScimUserByUserNameOrEmail(t.Context(), "", email)
|
|
assert.Nil(t, err)
|
|
require.NotNil(t, user)
|
|
assert.Equal(t, "email-test-user1", user.UserName)
|
|
assert.Equal(t, users[0].ID, user.ID)
|
|
|
|
// Test 3: Find user by email when userName doesn't exist
|
|
email = "email.user2@example.com"
|
|
user, err = ds.ScimUserByUserNameOrEmail(t.Context(), "nonexistent-user", email)
|
|
assert.Nil(t, err)
|
|
require.NotNil(t, user)
|
|
assert.Equal(t, "email-test-user2", user.UserName)
|
|
assert.Equal(t, users[1].ID, user.ID)
|
|
assert.False(t, user.UpdatedAt.IsZero(), "UpdatedAt should not be zero")
|
|
|
|
// Test 4: Handle case where multiple users have the same email
|
|
email = "duplicate@example.com"
|
|
user, err = ds.ScimUserByUserNameOrEmail(t.Context(), "nonexistent-user", email)
|
|
assert.Nil(t, err)
|
|
assert.Nil(t, user, "Should return nil when multiple users have the same email")
|
|
|
|
// Test 5: Handle case where neither userName nor email match any user
|
|
email = "nonexistent@example.com"
|
|
user, err = ds.ScimUserByUserNameOrEmail(t.Context(), "nonexistent-user", email)
|
|
assert.NotNil(t, err)
|
|
assert.True(t, fleet.IsNotFound(err))
|
|
assert.Nil(t, user)
|
|
|
|
// Test 6: Handle case where email is empty
|
|
user, err = ds.ScimUserByUserNameOrEmail(t.Context(), "nonexistent-user", "")
|
|
assert.NotNil(t, err)
|
|
assert.True(t, fleet.IsNotFound(err))
|
|
assert.Nil(t, user)
|
|
|
|
// Test 7: Find user when email is used as userName
|
|
// This tests the case where the userName field contains an email address
|
|
user, err = ds.ScimUserByUserNameOrEmail(t.Context(), "nonexistent-username", "email-test-user1")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "email-test-user1", user.UserName)
|
|
assert.Equal(t, users[0].ID, user.ID)
|
|
}
|
|
|
|
func testScimUserReplaceValidation(t *testing.T, ds *Datastore) {
|
|
// Create a valid user first
|
|
user := fleet.ScimUser{
|
|
UserName: "replace-validation-user",
|
|
ExternalID: ptr.String("ext-replace-validation"),
|
|
GivenName: ptr.String("Original"),
|
|
FamilyName: ptr.String("User"),
|
|
Active: ptr.Bool(true),
|
|
Department: ptr.String("Customer support"),
|
|
}
|
|
|
|
var err error
|
|
user.ID, err = ds.CreateScimUser(t.Context(), &user)
|
|
require.NoError(t, err)
|
|
|
|
// Test validation for ExternalID
|
|
longString := strings.Repeat("a", fleet.SCIMMaxFieldLength+1) // String longer than SCIMMaxFieldLength
|
|
|
|
// Test ExternalID validation
|
|
userWithLongExternalID := fleet.ScimUser{
|
|
ID: user.ID,
|
|
UserName: "valid-username",
|
|
ExternalID: ptr.String(longString),
|
|
GivenName: ptr.String("Valid"),
|
|
FamilyName: ptr.String("Name"),
|
|
Active: ptr.Bool(true),
|
|
Department: ptr.String("Customer support"),
|
|
}
|
|
err = ds.ReplaceScimUser(t.Context(), &userWithLongExternalID)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "external_id exceeds maximum length")
|
|
|
|
// Test UserName validation
|
|
userWithLongUserName := fleet.ScimUser{
|
|
ID: user.ID,
|
|
UserName: longString,
|
|
ExternalID: ptr.String("valid-external-id"),
|
|
GivenName: ptr.String("Valid"),
|
|
FamilyName: ptr.String("Name"),
|
|
Active: ptr.Bool(true),
|
|
Department: ptr.String("Customer support"),
|
|
}
|
|
err = ds.ReplaceScimUser(t.Context(), &userWithLongUserName)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "user_name exceeds maximum length")
|
|
|
|
// Test GivenName validation
|
|
userWithLongGivenName := fleet.ScimUser{
|
|
ID: user.ID,
|
|
UserName: "valid-username",
|
|
ExternalID: ptr.String("valid-external-id"),
|
|
GivenName: ptr.String(longString),
|
|
FamilyName: ptr.String("Name"),
|
|
Active: ptr.Bool(true),
|
|
Department: ptr.String("Customer support"),
|
|
}
|
|
err = ds.ReplaceScimUser(t.Context(), &userWithLongGivenName)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "given_name exceeds maximum length")
|
|
|
|
// Test FamilyName validation
|
|
userWithLongFamilyName := fleet.ScimUser{
|
|
ID: user.ID,
|
|
UserName: "valid-username",
|
|
ExternalID: ptr.String("valid-external-id"),
|
|
GivenName: ptr.String("Valid"),
|
|
FamilyName: ptr.String(longString),
|
|
Active: ptr.Bool(true),
|
|
Department: ptr.String("Customer support"),
|
|
}
|
|
err = ds.ReplaceScimUser(t.Context(), &userWithLongFamilyName)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "family_name exceeds maximum length")
|
|
|
|
// Test Department validation
|
|
userWithLongDepartment := fleet.ScimUser{
|
|
ID: user.ID,
|
|
UserName: "valid-username",
|
|
ExternalID: ptr.String("valid-external-id"),
|
|
GivenName: ptr.String("Valid"),
|
|
FamilyName: ptr.String("Valid"),
|
|
Active: ptr.Bool(true),
|
|
Department: ptr.String(longString),
|
|
}
|
|
err = ds.ReplaceScimUser(t.Context(), &userWithLongDepartment)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "department exceeds maximum length")
|
|
|
|
// Test with valid values
|
|
validUser := fleet.ScimUser{
|
|
ID: user.ID,
|
|
UserName: "updated-username",
|
|
ExternalID: ptr.String("updated-external-id"),
|
|
GivenName: ptr.String("Updated"),
|
|
FamilyName: ptr.String("Name"),
|
|
Active: ptr.Bool(true),
|
|
Department: ptr.String("Customer support updated"),
|
|
}
|
|
err = ds.ReplaceScimUser(t.Context(), &validUser)
|
|
assert.NoError(t, err)
|
|
|
|
updated, err := ds.ScimUserByID(t.Context(), user.ID)
|
|
assert.Nil(t, err)
|
|
assert.NotNil(t, updated)
|
|
assert.Equal(t, updated.ID, user.ID)
|
|
assert.Equal(t, updated.UserName, validUser.UserName)
|
|
assert.Equal(t, updated.ExternalID, validUser.ExternalID)
|
|
assert.Equal(t, updated.GivenName, validUser.GivenName)
|
|
assert.Equal(t, updated.FamilyName, validUser.FamilyName)
|
|
assert.Equal(t, updated.Active, validUser.Active)
|
|
assert.Equal(t, updated.Department, validUser.Department)
|
|
assert.Greater(t, updated.UpdatedAt, user.UpdatedAt)
|
|
}
|
|
|
|
func testScimLastRequest(t *testing.T, ds *Datastore) {
|
|
// Initially, there should be no last request
|
|
initialRequest, err := ds.ScimLastRequest(t.Context())
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, initialRequest)
|
|
|
|
// Validation tests for UpdateScimLastRequest
|
|
// Nil request should return nil
|
|
err = ds.UpdateScimLastRequest(t.Context(), nil)
|
|
assert.NoError(t, err)
|
|
|
|
// Status exceeding max length should return error
|
|
longStatus := strings.Repeat("a", SCIMMaxStatusLength+1)
|
|
invalidStatusRequest := &fleet.ScimLastRequest{
|
|
Status: longStatus,
|
|
Details: "Valid details",
|
|
}
|
|
err = ds.UpdateScimLastRequest(t.Context(), invalidStatusRequest)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "status exceeds maximum length")
|
|
|
|
// Details exceeding max length should return error
|
|
longDetails := strings.Repeat("b", fleet.SCIMMaxFieldLength+1) // 256 characters
|
|
invalidDetailsRequest := &fleet.ScimLastRequest{
|
|
Status: "valid",
|
|
Details: longDetails,
|
|
}
|
|
err = ds.UpdateScimLastRequest(t.Context(), invalidDetailsRequest)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "details exceeds maximum length")
|
|
|
|
// Create a new last request with valid values
|
|
newRequest := &fleet.ScimLastRequest{
|
|
Status: "success",
|
|
Details: "Initial SCIM request",
|
|
}
|
|
err = ds.UpdateScimLastRequest(t.Context(), newRequest)
|
|
assert.NoError(t, err)
|
|
|
|
// Retrieve the last request and verify it matches
|
|
retrievedRequest, err := ds.ScimLastRequest(t.Context())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, retrievedRequest)
|
|
assert.Equal(t, "success", retrievedRequest.Status)
|
|
assert.Equal(t, "Initial SCIM request", retrievedRequest.Details)
|
|
assert.False(t, retrievedRequest.RequestedAt.IsZero(), "RequestedAt should not be zero")
|
|
|
|
// Do and check the same request again -- timestamp should update
|
|
err = ds.UpdateScimLastRequest(t.Context(), newRequest)
|
|
assert.NoError(t, err)
|
|
retrievedSameRequest, err := ds.ScimLastRequest(t.Context())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, retrievedSameRequest)
|
|
assert.Equal(t, retrievedRequest.Status, retrievedSameRequest.Status)
|
|
assert.Equal(t, retrievedRequest.Details, retrievedSameRequest.Details)
|
|
// Verify that the timestamp is newer
|
|
assert.True(t, retrievedSameRequest.RequestedAt.After(retrievedRequest.RequestedAt),
|
|
"Same request timestamp should be after the original timestamp")
|
|
|
|
// Update the last request with new valid values
|
|
updatedRequest := &fleet.ScimLastRequest{
|
|
Status: "error",
|
|
Details: "Updated SCIM request with error",
|
|
}
|
|
err = ds.UpdateScimLastRequest(t.Context(), updatedRequest)
|
|
assert.NoError(t, err)
|
|
|
|
// Retrieve the updated last request and verify it matches
|
|
retrievedUpdatedRequest, err := ds.ScimLastRequest(t.Context())
|
|
require.NoError(t, err)
|
|
require.NotNil(t, retrievedUpdatedRequest)
|
|
assert.Equal(t, "error", retrievedUpdatedRequest.Status)
|
|
assert.Equal(t, "Updated SCIM request with error", retrievedUpdatedRequest.Details)
|
|
assert.False(t, retrievedUpdatedRequest.RequestedAt.IsZero(), "RequestedAt should not be zero")
|
|
|
|
// Verify that the updated timestamp is newer
|
|
assert.True(t, retrievedUpdatedRequest.RequestedAt.After(retrievedSameRequest.RequestedAt),
|
|
"Updated request timestamp should be after the original timestamp")
|
|
}
|
|
|
|
func testTriggerResendIdPProfiles(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
// create some hosts to deploy profiles to
|
|
host1 := test.NewHost(t, ds, "host1", "1", "host1key", "host1uuid", time.Now())
|
|
host2 := test.NewHost(t, ds, "host2", "2", "host2key", "host2uuid", time.Now())
|
|
host3 := test.NewHost(t, ds, "host3", "3", "host3key", "host3uuid", time.Now())
|
|
hostW1 := test.NewHost(t, ds, "hostW1", "4", "hostW1key", "hostW1uuid", time.Now(), test.WithPlatform("windows"))
|
|
hostW2 := test.NewHost(t, ds, "hostW2", "5", "hostW2key", "hostW2uuid", time.Now(), test.WithPlatform("windows"))
|
|
hostW3 := test.NewHost(t, ds, "hostW3", "6", "hostW3key", "hostW3uuid", time.Now(), test.WithPlatform("windows"))
|
|
|
|
// create profiles that use the IdP variables, and one that doesn't
|
|
profUsername, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("a", "a", 0), nil)
|
|
require.NoError(t, err)
|
|
profGroup, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("b", "b", 0), nil)
|
|
require.NoError(t, err)
|
|
profAll, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("c", "c", 0), nil)
|
|
require.NoError(t, err)
|
|
profNone, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("d", "d", 0), nil)
|
|
require.NoError(t, err)
|
|
|
|
profWUsername, err := ds.NewMDMWindowsConfigProfile(ctx, *generateWindowsCP("w", "w", 0), nil)
|
|
require.NoError(t, err)
|
|
profWGroup, err := ds.NewMDMWindowsConfigProfile(ctx, *generateWindowsCP("x", "x", 0), nil)
|
|
require.NoError(t, err)
|
|
profWAll, err := ds.NewMDMWindowsConfigProfile(ctx, *generateWindowsCP("y", "y", 0), nil)
|
|
require.NoError(t, err)
|
|
profWNone, err := ds.NewMDMWindowsConfigProfile(ctx, *generateWindowsCP("z", "z", 0), nil)
|
|
require.NoError(t, err)
|
|
|
|
t.Logf("profUsername=%s, profGroup=%s, profAll=%s, profNone=%s", profUsername.ProfileUUID, profGroup.ProfileUUID, profAll.ProfileUUID, profNone.ProfileUUID)
|
|
|
|
// insert the relationship between profile and variables
|
|
varsPerProfile := map[string][]string{
|
|
profUsername.ProfileUUID: {string(fleet.FleetVarHostEndUserIDPUsername), string(fleet.FleetVarHostEndUserIDPUsernameLocalPart)},
|
|
profWUsername.ProfileUUID: {string(fleet.FleetVarHostEndUserIDPUsername), string(fleet.FleetVarHostEndUserIDPUsernameLocalPart)},
|
|
profGroup.ProfileUUID: {string(fleet.FleetVarHostEndUserIDPGroups)},
|
|
profWGroup.ProfileUUID: {string(fleet.FleetVarHostEndUserIDPGroups)},
|
|
profAll.ProfileUUID: {string(fleet.FleetVarHostEndUserIDPUsername), string(fleet.FleetVarHostEndUserIDPUsernameLocalPart), string(fleet.FleetVarHostEndUserIDPGroups)},
|
|
profWAll.ProfileUUID: {string(fleet.FleetVarHostEndUserIDPUsername), string(fleet.FleetVarHostEndUserIDPUsernameLocalPart), string(fleet.FleetVarHostEndUserIDPGroups)},
|
|
}
|
|
for profUUID, vars := range varsPerProfile {
|
|
column := "apple_profile_uuid"
|
|
if strings.HasPrefix(profUUID, "w") {
|
|
column = "windows_profile_uuid"
|
|
}
|
|
for _, v := range vars {
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, fmt.Sprintf(`INSERT INTO mdm_configuration_profile_variables (%s, fleet_variable_id)
|
|
SELECT ?, id FROM fleet_variables WHERE name = ?`, column), profUUID, "FLEET_VAR_"+v)
|
|
return err
|
|
})
|
|
}
|
|
}
|
|
|
|
// create some scim users and assign one to hosts 1/4 and 2/5
|
|
scimUser1, err := ds.CreateScimUser(ctx, &fleet.ScimUser{UserName: "a@example.com"})
|
|
require.NoError(t, err)
|
|
scimUser2, err := ds.CreateScimUser(ctx, &fleet.ScimUser{UserName: "b@example.com"})
|
|
require.NoError(t, err)
|
|
scimUser3, err := ds.CreateScimUser(ctx, &fleet.ScimUser{UserName: "c@example.com"})
|
|
require.NoError(t, err)
|
|
err = ds.associateHostWithScimUser(ctx, host1.ID, scimUser1)
|
|
require.NoError(t, err)
|
|
err = ds.associateHostWithScimUser(ctx, hostW1.ID, scimUser1)
|
|
require.NoError(t, err)
|
|
err = ds.associateHostWithScimUser(ctx, host2.ID, scimUser2)
|
|
require.NoError(t, err)
|
|
err = ds.associateHostWithScimUser(ctx, hostW2.ID, scimUser2)
|
|
require.NoError(t, err)
|
|
|
|
// no profiles exist yet for any host, so this setup hasn't triggered anything
|
|
assertHostProfileStatus(t, ds, host1.UUID)
|
|
assertHostProfileStatus(t, ds, host2.UUID)
|
|
assertHostProfileStatus(t, ds, host3.UUID)
|
|
assertHostProfileStatus(t, ds, hostW1.UUID)
|
|
assertHostProfileStatus(t, ds, hostW2.UUID)
|
|
assertHostProfileStatus(t, ds, hostW3.UUID)
|
|
|
|
// mark all profiles as installed on all hosts
|
|
forceSetAppleHostProfileStatus(t, ds, host1.UUID, profNone, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host2.UUID, profNone, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host3.UUID, profNone, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host1.UUID, profUsername, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host2.UUID, profUsername, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host3.UUID, profUsername, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host1.UUID, profGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host2.UUID, profGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host3.UUID, profGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host1.UUID, profAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host2.UUID, profAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host3.UUID, profAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW1.UUID, profWNone, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW2.UUID, profWNone, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW3.UUID, profWNone, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW1.UUID, profWUsername, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW2.UUID, profWUsername, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW3.UUID, profWUsername, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW1.UUID, profWGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW2.UUID, profWGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW3.UUID, profWGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW1.UUID, profWAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW2.UUID, profWAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW3.UUID, profWAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
|
|
// change username of scim user 1
|
|
err = ds.ReplaceScimUser(ctx, &fleet.ScimUser{ID: scimUser1, UserName: "A@example.com"})
|
|
require.NoError(t, err)
|
|
|
|
// this triggered a resend of profUsername and profAll on host1 and hostW1
|
|
assertHostProfileStatus(t, ds, host1.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, host2.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, host3.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, hostW1.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, hostW2.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, hostW3.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
|
|
// reset the status for host1
|
|
forceSetAppleHostProfileStatus(t, ds, host1.UUID, profUsername, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host1.UUID, profAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW1.UUID, profWUsername, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW1.UUID, profWAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
|
|
// create a scim group for user1 and user2
|
|
group1, err := ds.CreateScimGroup(ctx, &fleet.ScimGroup{DisplayName: "g1", ScimUsers: []uint{scimUser1, scimUser2}})
|
|
require.NoError(t, err)
|
|
|
|
// this triggered a resend of profGroup and profAll on host1 and host2
|
|
assertHostProfileStatus(t, ds, host1.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, host2.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, host3.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, hostW1.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, hostW2.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, hostW3.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
|
|
// reset the statuses
|
|
forceSetAppleHostProfileStatus(t, ds, host1.UUID, profGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host1.UUID, profAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host2.UUID, profGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host2.UUID, profAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW1.UUID, profWGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW1.UUID, profWAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW2.UUID, profWGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW2.UUID, profWAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
|
|
// create another scim group with no user and update other properties of
|
|
// user1, does not trigger anything
|
|
group2, err := ds.CreateScimGroup(ctx, &fleet.ScimGroup{DisplayName: "g2"})
|
|
require.NoError(t, err)
|
|
err = ds.ReplaceScimUser(ctx, &fleet.ScimUser{ID: scimUser1, UserName: "A@example.com", ExternalID: ptr.String("A")})
|
|
require.NoError(t, err)
|
|
|
|
assertHostProfileStatus(t, ds, host1.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, host2.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, host3.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, hostW1.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, hostW2.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, hostW3.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
|
|
// change group1's name, affects host1 and host2 (via user1 and user2)
|
|
err = ds.ReplaceScimGroup(ctx, &fleet.ScimGroup{ID: group1, DisplayName: "G1", ScimUsers: []uint{scimUser1, scimUser2}})
|
|
require.NoError(t, err)
|
|
|
|
assertHostProfileStatus(t, ds, host1.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, host2.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, host3.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, hostW1.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, hostW2.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, hostW3.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
|
|
// reset the statuses
|
|
forceSetAppleHostProfileStatus(t, ds, host1.UUID, profGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host1.UUID, profAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host2.UUID, profGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host2.UUID, profAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW1.UUID, profWGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW1.UUID, profWAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW2.UUID, profWGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW2.UUID, profWAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
|
|
// assign user3 as IdP user of host3
|
|
err = ds.associateHostWithScimUser(ctx, host3.ID, scimUser3)
|
|
require.NoError(t, err)
|
|
err = ds.associateHostWithScimUser(ctx, hostW3.ID, scimUser3)
|
|
require.NoError(t, err)
|
|
|
|
// affects host3
|
|
assertHostProfileStatus(t, ds, host1.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, host2.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, host3.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, hostW1.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, hostW2.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, hostW3.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
|
|
// reset the statuses
|
|
forceSetAppleHostProfileStatus(t, ds, host3.UUID, profUsername, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host3.UUID, profGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host3.UUID, profAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW3.UUID, profWUsername, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW3.UUID, profWGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW3.UUID, profWAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
|
|
// add user3 and remove user2 from the group
|
|
err = ds.ReplaceScimGroup(ctx, &fleet.ScimGroup{ID: group1, DisplayName: "G1", ScimUsers: []uint{scimUser1, scimUser3}})
|
|
require.NoError(t, err)
|
|
|
|
// affects host2 and host3, not host1 because user2 is not its IdP user (it
|
|
// is an extra one)
|
|
assertHostProfileStatus(t, ds, host1.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, host2.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, host3.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, hostW1.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, hostW2.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, hostW3.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
|
|
// reset the statuses
|
|
forceSetAppleHostProfileStatus(t, ds, host2.UUID, profGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host2.UUID, profAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host3.UUID, profGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host3.UUID, profAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW2.UUID, profWGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW2.UUID, profWAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW3.UUID, profWGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW3.UUID, profWAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
|
|
// delete group2, has no user so no effect
|
|
err = ds.DeleteScimGroup(ctx, group2)
|
|
require.NoError(t, err)
|
|
|
|
assertHostProfileStatus(t, ds, host1.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, host2.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, host3.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, hostW1.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, hostW2.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, hostW3.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
|
|
// delete user3, affects only host3 (not the official IdP user for host1)
|
|
err = ds.DeleteScimUser(ctx, scimUser3)
|
|
require.NoError(t, err)
|
|
|
|
assertHostProfileStatus(t, ds, host1.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, host2.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, host3.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, hostW1.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, hostW2.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, hostW3.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
|
|
// reset the statuses
|
|
forceSetAppleHostProfileStatus(t, ds, host3.UUID, profUsername, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host3.UUID, profGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host3.UUID, profAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW3.UUID, profWUsername, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW3.UUID, profWGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW3.UUID, profWAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
|
|
// delete user1
|
|
err = ds.DeleteScimUser(ctx, scimUser1)
|
|
require.NoError(t, err)
|
|
// add user2 as new user for host1
|
|
err = ds.associateHostWithScimUser(ctx, host1.ID, scimUser2)
|
|
require.NoError(t, err)
|
|
err = ds.associateHostWithScimUser(ctx, hostW1.ID, scimUser2)
|
|
require.NoError(t, err)
|
|
|
|
assertHostProfileStatus(t, ds, host1.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, host2.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, host3.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, hostW1.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, hostW2.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, hostW3.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
|
|
// reset the statuses, but set username operation to remove
|
|
forceSetAppleHostProfileStatus(t, ds, host1.UUID, profUsername, fleet.MDMOperationTypeRemove, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host1.UUID, profGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host1.UUID, profAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW1.UUID, profWUsername, fleet.MDMOperationTypeRemove, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW1.UUID, profWGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW1.UUID, profWAll, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
|
|
// update name of user2, will affect host1 and host2, but NOT the
|
|
// profUsername of host1 because it is not installed (it is removed)
|
|
err = ds.ReplaceScimUser(ctx, &fleet.ScimUser{ID: scimUser2, UserName: "B@example.com", GivenName: ptr.String("B")})
|
|
require.NoError(t, err)
|
|
|
|
assertHostProfileStatus(t, ds, host1.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, host2.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, host3.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, hostW1.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, hostW2.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryPending},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, hostW3.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWUsername.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWAll.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
}
|
|
|
|
// for https://github.com/fleetdm/fleet/issues/28820
|
|
func testTriggerResendIdPProfilesOnTeam(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
// create a couple hosts to deploy profiles to
|
|
host1 := test.NewHost(t, ds, "host1", "1", "h1key", "host1uuid", time.Now())
|
|
host2 := test.NewHost(t, ds, "host2", "2", "h2key", "host2uuid", time.Now())
|
|
hostW1 := test.NewHost(t, ds, "hostW1", "3", "hw1key", "hostW1uuid", time.Now(), test.WithPlatform("windows"))
|
|
hostW2 := test.NewHost(t, ds, "hostW2", "4", "hw2key", "hostW2uuid", time.Now(), test.WithPlatform("windows"))
|
|
|
|
// create a team and make host2 part of that team
|
|
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
|
|
require.NoError(t, err)
|
|
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host2.ID, hostW2.ID}))
|
|
require.NoError(t, err)
|
|
|
|
// create some profiles with/without vars on the team
|
|
profGroup, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("a", "a", team.ID), []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups})
|
|
require.NoError(t, err)
|
|
profNone, err := ds.NewMDMAppleConfigProfile(ctx, *generateAppleCP("b", "b", team.ID), nil)
|
|
require.NoError(t, err)
|
|
profWGroup, err := ds.NewMDMWindowsConfigProfile(ctx, *generateWindowsCP("wa", "wa", team.ID), []fleet.FleetVarName{fleet.FleetVarHostEndUserIDPGroups})
|
|
require.NoError(t, err)
|
|
profWNone, err := ds.NewMDMWindowsConfigProfile(ctx, *generateWindowsCP("wb", "wb", team.ID), nil)
|
|
require.NoError(t, err)
|
|
|
|
t.Logf("profGroup=%s, profNone=%s", profGroup.ProfileUUID, profNone.ProfileUUID)
|
|
t.Logf("profWGroup=%s, profWNone=%s", profWGroup.ProfileUUID, profWNone.ProfileUUID)
|
|
|
|
// create some scim data
|
|
scimUser1, err := ds.CreateScimUser(ctx, &fleet.ScimUser{UserName: "a@example.com"})
|
|
require.NoError(t, err)
|
|
scimUser2, err := ds.CreateScimUser(ctx, &fleet.ScimUser{UserName: "b@example.com"})
|
|
require.NoError(t, err)
|
|
err = ds.associateHostWithScimUser(ctx, host2.ID, scimUser1)
|
|
require.NoError(t, err)
|
|
err = ds.associateHostWithScimUser(ctx, hostW2.ID, scimUser1)
|
|
require.NoError(t, err)
|
|
group1, err := ds.CreateScimGroup(ctx, &fleet.ScimGroup{DisplayName: "g1", ScimUsers: []uint{scimUser1, scimUser2}})
|
|
require.NoError(t, err)
|
|
|
|
forceSetAppleHostProfileStatus(t, ds, host2.UUID, profGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetAppleHostProfileStatus(t, ds, host2.UUID, profNone, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW2.UUID, profWGroup, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
forceSetWindowsHostProfileStatus(t, ds, hostW2.UUID, profWNone, fleet.MDMOperationTypeInstall, fleet.MDMDeliveryVerifying)
|
|
|
|
assertHostProfileStatus(t, ds, host1.UUID)
|
|
assertHostProfileStatus(t, ds, hostW1.UUID)
|
|
assertHostProfileStatus(t, ds, host2.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
assertHostProfileStatus(t, ds, hostW2.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryVerifying})
|
|
|
|
// remove user 1 from group, affects the host2 profile
|
|
err = ds.ReplaceScimGroup(ctx, &fleet.ScimGroup{ID: group1, DisplayName: "g1", ScimUsers: []uint{scimUser2}})
|
|
require.NoError(t, err)
|
|
|
|
assertHostProfileStatus(t, ds, host1.UUID)
|
|
assertHostProfileStatus(t, ds, hostW1.UUID)
|
|
// the bug was failing this check, profNone was set to Pending although only
|
|
// profGroup should have changed.
|
|
assertHostProfileStatus(t, ds, host2.UUID,
|
|
hostProfileStatus{profNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profGroup.ProfileUUID, fleet.MDMDeliveryPending})
|
|
assertHostProfileStatus(t, ds, hostW2.UUID,
|
|
hostProfileStatus{profWNone.ProfileUUID, fleet.MDMDeliveryVerifying},
|
|
hostProfileStatus{profWGroup.ProfileUUID, fleet.MDMDeliveryPending})
|
|
}
|
|
|
|
type hostProfileStatus struct {
|
|
ProfileUUID string
|
|
Status fleet.MDMDeliveryStatus
|
|
}
|
|
|
|
func assertHostProfileStatus(t *testing.T, ds *Datastore, hostUUID string, wantProfiles ...hostProfileStatus) {
|
|
withOpProfiles := make([]hostProfileOpStatus, 0, len(wantProfiles))
|
|
for _, p := range wantProfiles {
|
|
withOpProfiles = append(withOpProfiles, hostProfileOpStatus{
|
|
ProfileUUID: p.ProfileUUID,
|
|
Status: p.Status,
|
|
OpType: fleet.MDMOperationTypeInstall,
|
|
})
|
|
}
|
|
assertHostProfileOpStatus(t, ds, hostUUID, withOpProfiles...)
|
|
}
|
|
|
|
type hostProfileOpStatus struct {
|
|
ProfileUUID string
|
|
Status fleet.MDMDeliveryStatus
|
|
OpType fleet.MDMOperationType
|
|
}
|
|
|
|
func assertHostProfileOpStatus(t *testing.T, ds *Datastore, hostUUID string, wantProfiles ...hostProfileOpStatus) {
|
|
ctx := t.Context()
|
|
winProfs, err := ds.GetHostMDMWindowsProfiles(ctx, hostUUID)
|
|
require.NoError(t, err)
|
|
appleProfs, err := ds.GetHostMDMAppleProfiles(ctx, hostUUID)
|
|
require.NoError(t, err)
|
|
androidProfs, err := ds.GetHostMDMAndroidProfiles(ctx, hostUUID)
|
|
require.NoError(t, err)
|
|
|
|
type commonHostProf struct {
|
|
Status fleet.MDMDeliveryStatus
|
|
Type fleet.MDMOperationType
|
|
ProfileUUID string
|
|
}
|
|
profs := make([]commonHostProf, 0, len(appleProfs)+len(winProfs)+len(androidProfs))
|
|
for _, wp := range winProfs {
|
|
var status fleet.MDMDeliveryStatus
|
|
if wp.Status == nil {
|
|
status = fleet.MDMDeliveryPending
|
|
} else {
|
|
status = *wp.Status
|
|
}
|
|
profs = append(profs, commonHostProf{
|
|
ProfileUUID: wp.ProfileUUID,
|
|
Status: status,
|
|
Type: wp.OperationType,
|
|
})
|
|
}
|
|
for _, ap := range appleProfs {
|
|
var status fleet.MDMDeliveryStatus
|
|
if ap.Status == nil {
|
|
status = fleet.MDMDeliveryPending
|
|
} else {
|
|
status = *ap.Status
|
|
}
|
|
profs = append(profs, commonHostProf{
|
|
ProfileUUID: ap.ProfileUUID,
|
|
Status: status,
|
|
Type: ap.OperationType,
|
|
})
|
|
}
|
|
for _, anp := range androidProfs {
|
|
var status fleet.MDMDeliveryStatus
|
|
if anp.Status == nil {
|
|
status = fleet.MDMDeliveryPending
|
|
} else {
|
|
status = *anp.Status
|
|
}
|
|
profs = append(profs, commonHostProf{
|
|
ProfileUUID: anp.ProfileUUID,
|
|
Status: status,
|
|
Type: anp.OperationType,
|
|
})
|
|
}
|
|
|
|
require.Len(t, profs, len(wantProfiles))
|
|
|
|
// index the status of the actual profiles for quick lookup
|
|
profStatus := make(map[string]fleet.MDMDeliveryStatus, len(profs))
|
|
profOpType := make(map[string]fleet.MDMOperationType, len(profs))
|
|
for _, prof := range profs {
|
|
profStatus[prof.ProfileUUID] = prof.Status
|
|
profOpType[prof.ProfileUUID] = prof.Type
|
|
}
|
|
for _, want := range wantProfiles {
|
|
status, ok := profStatus[want.ProfileUUID]
|
|
require.True(t, ok, "profile %s not found in host %s", want.ProfileUUID, hostUUID)
|
|
assert.Equal(t, want.Status, status, "profile %s", want.ProfileUUID)
|
|
assert.Equal(t, want.OpType, profOpType[want.ProfileUUID], "profile %s", want.ProfileUUID)
|
|
}
|
|
}
|
|
|
|
// helper function to force-set a host profile status
|
|
func forceSetAppleHostProfileStatus(t *testing.T, ds *Datastore, hostUUID string, profile *fleet.MDMAppleConfigProfile, operation fleet.MDMOperationType, status fleet.MDMDeliveryStatus) {
|
|
ctx := t.Context()
|
|
|
|
// empty status string means set to NULL
|
|
var actualStatus *fleet.MDMDeliveryStatus
|
|
if status != "" {
|
|
actualStatus = &status
|
|
}
|
|
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
_, err := q.ExecContext(ctx, `INSERT INTO host_mdm_apple_profiles
|
|
(profile_identifier, host_uuid, status, operation_type, command_uuid, profile_name, checksum, profile_uuid)
|
|
VALUES
|
|
(?, ?, ?, ?, ?, ?, UNHEX(MD5(?)), ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
status = VALUES(status),
|
|
operation_type = VALUES(operation_type)
|
|
`,
|
|
profile.Identifier, hostUUID, actualStatus, operation, uuid.NewString(), profile.Name, profile.Mobileconfig, profile.ProfileUUID)
|
|
return err
|
|
})
|
|
}
|
|
|
|
func testScimUsersExist(t *testing.T, ds *Datastore) {
|
|
// Create test users
|
|
users := createTestScimUsers(t, ds)
|
|
userIDs := make([]uint, len(users))
|
|
for i, user := range users {
|
|
userIDs[i] = user.ID
|
|
}
|
|
|
|
// Test 1: Empty slice should return true
|
|
exist, err := ds.ScimUsersExist(t.Context(), []uint{})
|
|
require.NoError(t, err)
|
|
assert.True(t, exist, "Empty slice should return true")
|
|
|
|
// Test 2: All existing users should return true
|
|
exist, err = ds.ScimUsersExist(t.Context(), userIDs)
|
|
require.NoError(t, err)
|
|
assert.True(t, exist, "All existing users should return true")
|
|
|
|
// Test 3: Mix of existing and non-existing users should return false
|
|
nonExistentIDs := userIDs
|
|
nonExistentIDs = append(nonExistentIDs, 99999)
|
|
exist, err = ds.ScimUsersExist(t.Context(), nonExistentIDs)
|
|
require.NoError(t, err)
|
|
assert.False(t, exist, "Mix of existing and non-existing users should return false")
|
|
|
|
// Test 4: Only non-existing users should return false
|
|
exist, err = ds.ScimUsersExist(t.Context(), []uint{99999, 100000})
|
|
require.NoError(t, err)
|
|
assert.False(t, exist, "Only non-existing users should return false")
|
|
|
|
// Test 5: Test with a large number of IDs to verify batching works
|
|
// First, create a large number of test users
|
|
largeUserIDs := make([]uint, 0, 25000)
|
|
largeUserIDs = append(largeUserIDs, userIDs...) // Add existing users
|
|
|
|
// Add some non-existent IDs to test batching with mixed results
|
|
for i := 0; i < 24990; i++ {
|
|
largeUserIDs = append(largeUserIDs, uint(1000000)+uint(i)) // nolint:gosec // dismiss G115 integer overflow
|
|
}
|
|
|
|
exist, err = ds.ScimUsersExist(t.Context(), largeUserIDs)
|
|
require.NoError(t, err)
|
|
assert.False(t, exist, "Large batch with non-existing users should return false")
|
|
|
|
// Test 6: Test with a large number of existing IDs
|
|
// This is a bit tricky to test thoroughly without creating thousands of users,
|
|
// so we'll just verify the function handles a large slice without errors
|
|
largeExistingIDs := make([]uint, 0, 25000)
|
|
for i := 0; i < 25000; i++ {
|
|
largeExistingIDs = append(largeExistingIDs, userIDs[i%len(userIDs)])
|
|
}
|
|
|
|
exist, err = ds.ScimUsersExist(t.Context(), largeExistingIDs)
|
|
require.NoError(t, err)
|
|
assert.True(t, exist, "Large batch with only existing users should return true")
|
|
}
|
|
|
|
func testSetOrUpdateHostSCIMUserMapping(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
|
|
// Create test SCIM users
|
|
user1 := fleet.ScimUser{
|
|
UserName: "mapping-test-user1",
|
|
ExternalID: ptr.String("ext-mapping-123"),
|
|
GivenName: ptr.String("Test"),
|
|
FamilyName: ptr.String("User1"),
|
|
Active: ptr.Bool(true),
|
|
Emails: []fleet.ScimUserEmail{
|
|
{
|
|
Email: "user1@example.com",
|
|
Primary: ptr.Bool(true),
|
|
Type: ptr.String("work"),
|
|
},
|
|
},
|
|
Department: ptr.String("Engineering"),
|
|
}
|
|
|
|
user2 := fleet.ScimUser{
|
|
UserName: "mapping-test-user2",
|
|
ExternalID: ptr.String("ext-mapping-456"),
|
|
GivenName: ptr.String("Test"),
|
|
FamilyName: ptr.String("User2"),
|
|
Active: ptr.Bool(true),
|
|
Emails: []fleet.ScimUserEmail{
|
|
{
|
|
Email: "user2@example.com",
|
|
Primary: ptr.Bool(true),
|
|
Type: ptr.String("work"),
|
|
},
|
|
},
|
|
Department: ptr.String("Sales"),
|
|
}
|
|
|
|
var err error
|
|
user1.ID, err = ds.CreateScimUser(ctx, &user1)
|
|
require.NoError(t, err)
|
|
|
|
user2.ID, err = ds.CreateScimUser(ctx, &user2)
|
|
require.NoError(t, err)
|
|
|
|
hostID1 := uint(1)
|
|
hostID2 := uint(2)
|
|
|
|
// Create new host-SCIM user mapping
|
|
err = ds.SetOrUpdateHostSCIMUserMapping(ctx, hostID1, user1.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the mapping was created
|
|
var scimUserID uint
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
return sqlx.GetContext(ctx, q, &scimUserID,
|
|
"SELECT scim_user_id FROM host_scim_user WHERE host_id = ?", hostID1)
|
|
})
|
|
assert.Equal(t, user1.ID, scimUserID)
|
|
|
|
// Test 2: Update existing host-SCIM user mapping
|
|
err = ds.SetOrUpdateHostSCIMUserMapping(ctx, hostID1, user2.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the mapping was updated (should now point to user2)
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
return sqlx.GetContext(ctx, q, &scimUserID,
|
|
"SELECT scim_user_id FROM host_scim_user WHERE host_id = ?", hostID1)
|
|
})
|
|
assert.Equal(t, user2.ID, scimUserID)
|
|
|
|
// Verify there's only one mapping for this host
|
|
var count int
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
return sqlx.GetContext(ctx, q, &count,
|
|
"SELECT COUNT(*) FROM host_scim_user WHERE host_id = ?", hostID1)
|
|
})
|
|
assert.Equal(t, 1, count)
|
|
|
|
// Test 3: Create mapping for a different host
|
|
err = ds.SetOrUpdateHostSCIMUserMapping(ctx, hostID2, user1.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify both hosts have mappings
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
return sqlx.GetContext(ctx, q, &scimUserID,
|
|
"SELECT scim_user_id FROM host_scim_user WHERE host_id = ?", hostID2)
|
|
})
|
|
assert.Equal(t, user1.ID, scimUserID)
|
|
|
|
// Verify total mappings
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
return sqlx.GetContext(ctx, q, &count,
|
|
"SELECT COUNT(*) FROM host_scim_user")
|
|
})
|
|
assert.Equal(t, 2, count)
|
|
|
|
// Update mapping back to original user for hostID1
|
|
err = ds.SetOrUpdateHostSCIMUserMapping(ctx, hostID1, user1.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Verify hostID1 now maps to user1
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
return sqlx.GetContext(ctx, q, &scimUserID,
|
|
"SELECT scim_user_id FROM host_scim_user WHERE host_id = ?", hostID1)
|
|
})
|
|
assert.Equal(t, user1.ID, scimUserID)
|
|
|
|
// Error case - non-existent SCIM user
|
|
nonExistentUserID := uint(999999)
|
|
err = ds.SetOrUpdateHostSCIMUserMapping(ctx, hostID1, nonExistentUserID)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "foreign key constraint")
|
|
|
|
// Verify that failing update didn't change existing mapping
|
|
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
|
return sqlx.GetContext(ctx, q, &scimUserID,
|
|
"SELECT scim_user_id FROM host_scim_user WHERE host_id = ?", hostID1)
|
|
})
|
|
assert.Equal(t, user1.ID, scimUserID) // Should still be user1
|
|
|
|
// Verify the mapping can be queried via ScimUserByHostID
|
|
result, err := ds.ScimUserByHostID(ctx, hostID1)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, user1.ID, result.ID)
|
|
assert.Equal(t, "mapping-test-user1", result.UserName)
|
|
|
|
result, err = ds.ScimUserByHostID(ctx, hostID2)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, user1.ID, result.ID)
|
|
assert.Equal(t, "mapping-test-user1", result.UserName)
|
|
}
|