diff --git a/changes/29609-scim-add-department b/changes/29609-scim-add-department
new file mode 100644
index 0000000000..a9ea64b3da
--- /dev/null
+++ b/changes/29609-scim-add-department
@@ -0,0 +1 @@
+* Added support for ingesting user's "Department" via SCIM and added support to set the `FLEET_VAR_HOST_END_USER_IDP_DEPARTMENT` variable on configuration profiles.
diff --git a/ee/server/scim/scim.go b/ee/server/scim/scim.go
index 57e67848c5..5cb3198b52 100644
--- a/ee/server/scim/scim.go
+++ b/ee/server/scim/scim.go
@@ -159,7 +159,23 @@ func RegisterSCIM(
Endpoint: "/Users",
Description: optional.NewString("User Account"),
Schema: userSchema,
- Handler: NewUserHandler(ds, scimLogger),
+ SchemaExtensions: []scim.SchemaExtension{
+ {
+ Schema: schema.Schema{
+ Attributes: []schema.CoreAttribute{
+ schema.SimpleCoreAttribute(schema.SimpleStringParams(schema.StringParams{
+ Name: "department",
+ Required: false,
+ })),
+ },
+ Description: optional.NewString("Enterprise User"),
+ ID: "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User",
+ Name: optional.NewString("Enterprise User"),
+ },
+ Required: false,
+ },
+ },
+ Handler: NewUserHandler(ds, scimLogger),
},
{
ID: optional.NewString("Group"),
diff --git a/ee/server/scim/users.go b/ee/server/scim/users.go
index a74ab513ad..99b36e562f 100644
--- a/ee/server/scim/users.go
+++ b/ee/server/scim/users.go
@@ -34,6 +34,9 @@ const (
valueAttr = "value"
typeAttr = "type"
primaryAttr = "primary"
+
+ extensionEnterpriseUserAttributes = "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"
+ departmentAttr = "department"
)
type UserHandler struct {
@@ -70,7 +73,7 @@ func (u *UserHandler) Create(r *http.Request, attributes scim.ResourceAttributes
return scim.Resource{}, errors.ScimErrorUniqueness
}
- user, err := createUserFromAttributes(attributes)
+ user, err := u.createUserFromAttributes(attributes)
if err != nil {
level.Error(u.logger).Log("msg", "failed to create user from attributes", userNameAttr, userName, "err", err)
return scim.Resource{}, err
@@ -83,7 +86,7 @@ func (u *UserHandler) Create(r *http.Request, attributes scim.ResourceAttributes
return createUserResource(user), nil
}
-func createUserFromAttributes(attributes scim.ResourceAttributes) (*fleet.ScimUser, error) {
+func (u *UserHandler) createUserFromAttributes(attributes scim.ResourceAttributes) (*fleet.ScimUser, error) {
user := fleet.ScimUser{}
var err error
user.UserName, err = getRequiredResource[string](attributes, userNameAttr)
@@ -138,9 +141,48 @@ func createUserFromAttributes(attributes scim.ResourceAttributes) (*fleet.ScimUs
userEmails = append(userEmails, userEmail)
}
user.Emails = userEmails
+
+ // Attempt to get extension enterprise user attributes.
+ extendedAttributes := u.getExtensionEnterpriseUserAttributes(user.UserName, attributes)
+ user.Department = extendedAttributes.department
+
return &user, nil
}
+type extendedAttributes struct {
+ department *string
+}
+
+func (u *UserHandler) getExtensionEnterpriseUserAttributes(userName string, attributes scim.ResourceAttributes) extendedAttributes {
+ var attrs extendedAttributes
+ m_, ok := attributes[extensionEnterpriseUserAttributes]
+ if !ok {
+ return attrs
+ }
+ m, ok := m_.(map[string]any)
+ if !ok {
+ level.Error(u.logger).Log(
+ "msg", fmt.Sprintf("unexpected type for %s: %T", extensionEnterpriseUserAttributes, m_),
+ userNameAttr, userName,
+ )
+ return attrs
+ }
+
+ // Attempt to get department attribute.
+ if department_, ok := m[departmentAttr]; ok {
+ if department, ok := department_.(string); ok {
+ attrs.department = &department
+ } else {
+ level.Error(u.logger).Log(
+ "msg", fmt.Sprintf("unexpected type for %s.department: %T", extensionEnterpriseUserAttributes, department_),
+ userNameAttr, userName,
+ )
+ }
+ }
+
+ return attrs
+}
+
func getRequiredResource[T string | bool](attributes scim.ResourceAttributes, key string) (T, error) {
var val T
valIntf, ok := attributes[key]
@@ -267,6 +309,11 @@ func createUserResource(user *fleet.ScimUser) scim.Resource {
}
userResource.Attributes[groupsAttr] = groups
}
+ if user.Department != nil {
+ extensionEnterpriseUserAttributesMap := make(scim.ResourceAttributes)
+ extensionEnterpriseUserAttributesMap[departmentAttr] = *user.Department
+ userResource.Attributes[extensionEnterpriseUserAttributes] = extensionEnterpriseUserAttributesMap
+ }
return userResource
}
@@ -354,7 +401,7 @@ func (u *UserHandler) Replace(r *http.Request, id string, attributes scim.Resour
return scim.Resource{}, errors.ScimErrorResourceNotFound(id)
}
- user, err := createUserFromAttributes(attributes)
+ user, err := u.createUserFromAttributes(attributes)
if err != nil {
level.Error(u.logger).Log("msg", "failed to create user from attributes", "id", id, "err", err)
return scim.Resource{}, err
@@ -477,6 +524,11 @@ func (u *UserHandler) Patch(r *http.Request, id string, operations []scim.PatchO
if err != nil {
return scim.Resource{}, err
}
+ case extensionEnterpriseUserAttributes + ":" + departmentAttr:
+ err = u.patchDepartment(op.Op, v, user)
+ if err != nil {
+ return scim.Resource{}, err
+ }
default:
level.Info(u.logger).Log("msg", "unsupported patch value field", "field", k)
return scim.Resource{}, errors.ScimErrorBadParams([]string{fmt.Sprintf("%v", op)})
@@ -781,6 +833,19 @@ func (u *UserHandler) patchUserName(op string, v interface{}, user *fleet.ScimUs
return nil
}
+func (u *UserHandler) patchDepartment(op string, v interface{}, user *fleet.ScimUser) error {
+ if op == scim.PatchOperationRemove || v == nil {
+ user.Department = nil
+ return nil
+ }
+ department, err := getConcreteType[string](u, v, departmentAttr)
+ if err != nil {
+ return err
+ }
+ user.Department = &department
+ return nil
+}
+
func clearPrimaryFlagFromEmails(user *fleet.ScimUser) {
for i, email := range user.Emails {
if email.Primary != nil && *email.Primary {
diff --git a/frontend/interfaces/host.ts b/frontend/interfaces/host.ts
index 3d0a560c9e..52ef9c900c 100644
--- a/frontend/interfaces/host.ts
+++ b/frontend/interfaces/host.ts
@@ -219,6 +219,7 @@ export interface IHostEndUser {
idp_full_name?: string;
idp_info_updated_at: string | null;
idp_groups?: string[];
+ idp_department?: string;
other_emails?: Array<{
email: string;
source: string;
diff --git a/frontend/pages/hosts/details/cards/User/User.tsx b/frontend/pages/hosts/details/cards/User/User.tsx
index ffea08778d..d1c2e7597e 100644
--- a/frontend/pages/hosts/details/cards/User/User.tsx
+++ b/frontend/pages/hosts/details/cards/User/User.tsx
@@ -55,6 +55,10 @@ const User = ({
const showGroups = showUsername && userNameDisplayValues.length > 0;
const showChromeProfiles = chromeProfilesDisplayValues.length > 0;
const showOtherEmails = otherEmailsDisplayValues.length > 0;
+ const userDepartment = [];
+ if (endUser?.idp_department) {
+ userDepartment.push(endUser.idp_department);
+ }
return (
}
/>
)}
+ {showUsername && (
+
+ Department (IdP)
+
+ }
+ value={}
+ />
+ )}
{showChromeProfiles && (
fleet.SCIMMaxFieldLength {
return fmt.Errorf("family_name exceeds maximum length of %d characters", fleet.SCIMMaxFieldLength)
}
+ if user.Department != nil && len(*user.Department) > fleet.SCIMMaxFieldLength {
+ return fmt.Errorf("department exceeds maximum length of %d characters", fleet.SCIMMaxFieldLength)
+ }
return nil
}
@@ -1157,7 +1173,11 @@ func triggerResendProfilesForIDPUserChange(ctx context.Context, tx sqlx.ExtConte
return err
}
return triggerResendProfilesUsingVariables(ctx, tx, hostIDs,
- []string{fleet.FleetVarHostEndUserIDPUsername, fleet.FleetVarHostEndUserIDPUsernameLocalPart})
+ []string{
+ fleet.FleetVarHostEndUserIDPUsername,
+ fleet.FleetVarHostEndUserIDPUsernameLocalPart,
+ fleet.FleetVarHostEndUserIDPDepartment,
+ })
}
func triggerResendProfilesForIDPUserDeleted(ctx context.Context, tx sqlx.ExtContext, deletedScimUserID uint) error {
@@ -1166,7 +1186,12 @@ func triggerResendProfilesForIDPUserDeleted(ctx context.Context, tx sqlx.ExtCont
return err
}
return triggerResendProfilesUsingVariables(ctx, tx, hostIDs,
- []string{fleet.FleetVarHostEndUserIDPUsername, fleet.FleetVarHostEndUserIDPUsernameLocalPart, fleet.FleetVarHostEndUserIDPGroups})
+ []string{
+ fleet.FleetVarHostEndUserIDPUsername,
+ fleet.FleetVarHostEndUserIDPUsernameLocalPart,
+ fleet.FleetVarHostEndUserIDPGroups,
+ fleet.FleetVarHostEndUserIDPDepartment,
+ })
}
func triggerResendProfilesForIDPGroupChange(ctx context.Context, tx sqlx.ExtContext, updatedScimGroupID uint) error {
@@ -1213,7 +1238,12 @@ func triggerResendProfilesForIDPUserAddedToHost(ctx context.Context, tx sqlx.Ext
return nil
}
return triggerResendProfilesUsingVariables(ctx, tx, []uint{hostID},
- []string{fleet.FleetVarHostEndUserIDPUsername, fleet.FleetVarHostEndUserIDPUsernameLocalPart, fleet.FleetVarHostEndUserIDPGroups})
+ []string{
+ fleet.FleetVarHostEndUserIDPUsername,
+ fleet.FleetVarHostEndUserIDPUsernameLocalPart,
+ fleet.FleetVarHostEndUserIDPDepartment,
+ fleet.FleetVarHostEndUserIDPGroups,
+ })
}
func triggerResendProfilesUsingVariables(ctx context.Context, tx sqlx.ExtContext, hostIDs []uint, affectedVars []string) error {
diff --git a/server/datastore/mysql/scim_test.go b/server/datastore/mysql/scim_test.go
index 7a537e9d62..a887bd8234 100644
--- a/server/datastore/mysql/scim_test.go
+++ b/server/datastore/mysql/scim_test.go
@@ -63,6 +63,7 @@ func testScimUserCreate(t *testing.T, ds *Datastore) {
FamilyName: nil,
Active: nil,
Emails: []fleet.ScimUserEmail{},
+ Department: nil,
},
{
UserName: "user2",
@@ -77,6 +78,7 @@ func testScimUserCreate(t *testing.T, ds *Datastore) {
Type: ptr.String("work"),
},
},
+ Department: ptr.String(""),
},
{
UserName: "user3",
@@ -96,6 +98,7 @@ func testScimUserCreate(t *testing.T, ds *Datastore) {
Type: ptr.String("work"),
},
},
+ Department: ptr.String("Development"),
},
}
@@ -114,6 +117,7 @@ func testScimUserCreate(t *testing.T, ds *Datastore) {
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
@@ -142,6 +146,7 @@ func testScimUserByID(t *testing.T, ds *Datastore) {
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))
@@ -207,6 +212,7 @@ func testScimUserByUserName(t *testing.T, ds *Datastore) {
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
@@ -273,6 +279,7 @@ func createTestScimUsers(t *testing.T, ds *Datastore) []*fleet.ScimUser {
Type: ptr.String("work"),
},
},
+ Department: nil,
},
{
UserName: "test-user2",
@@ -292,6 +299,7 @@ func createTestScimUsers(t *testing.T, ds *Datastore) []*fleet.ScimUser {
Type: ptr.String("work"),
},
},
+ Department: ptr.String("QA"),
},
}
@@ -1400,6 +1408,7 @@ func testScimUserByHostID(t *testing.T, ds *Datastore) {
Type: ptr.String("work"),
},
},
+ Department: ptr.String("Engineering"),
}
var err error
@@ -1415,7 +1424,7 @@ func testScimUserByHostID(t *testing.T, ds *Datastore) {
group.ID, err = ds.CreateScimGroup(t.Context(), &group)
require.NoError(t, err)
- // Create a second test SCIM user without emails and without groups
+ // 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"),
@@ -1423,6 +1432,7 @@ func testScimUserByHostID(t *testing.T, ds *Datastore) {
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)
@@ -1450,6 +1460,7 @@ func testScimUserByHostID(t *testing.T, ds *Datastore) {
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
@@ -1473,6 +1484,7 @@ func testScimUserByHostID(t *testing.T, ds *Datastore) {
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
@@ -1616,6 +1628,7 @@ func testScimUserReplaceValidation(t *testing.T, ds *Datastore) {
GivenName: ptr.String("Original"),
FamilyName: ptr.String("User"),
Active: ptr.Bool(true),
+ Department: ptr.String("Customer support"),
}
var err error
@@ -1633,6 +1646,7 @@ func testScimUserReplaceValidation(t *testing.T, ds *Datastore) {
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)
@@ -1646,6 +1660,7 @@ func testScimUserReplaceValidation(t *testing.T, ds *Datastore) {
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)
@@ -1659,6 +1674,7 @@ func testScimUserReplaceValidation(t *testing.T, ds *Datastore) {
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)
@@ -1672,11 +1688,26 @@ func testScimUserReplaceValidation(t *testing.T, ds *Datastore) {
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,
@@ -1685,9 +1716,22 @@ func testScimUserReplaceValidation(t *testing.T, ds *Datastore) {
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) {
diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go
index 97db978ecf..25b66cb777 100644
--- a/server/fleet/hosts.go
+++ b/server/fleet/hosts.go
@@ -817,6 +817,7 @@ type HostEndUser struct {
IdpUserName string `json:"idp_username,omitempty"`
IdpFullName string `json:"idp_full_name,omitempty"`
IdpGroups []string `json:"idp_groups,omitempty"`
+ Department string `json:"idp_department,omitempty"`
IdpInfoUpdatedAt *time.Time `json:"idp_info_updated_at"`
OtherEmails []HostDeviceMapping `json:"other_emails,omitempty"`
}
diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go
index 998498e049..b03008c069 100644
--- a/server/fleet/mdm.go
+++ b/server/fleet/mdm.go
@@ -39,6 +39,7 @@ const (
FleetVarHostEndUserIDPUsername = "HOST_END_USER_IDP_USERNAME"
FleetVarHostEndUserIDPUsernameLocalPart = "HOST_END_USER_IDP_USERNAME_LOCAL_PART"
FleetVarHostEndUserIDPGroups = "HOST_END_USER_IDP_GROUPS"
+ FleetVarHostEndUserIDPDepartment = "HOST_END_USER_IDP_DEPARTMENT"
FleetVarSCEPRenewalID = "SCEP_RENEWAL_ID"
FleetVarDigiCertDataPrefix = "DIGICERT_DATA_"
diff --git a/server/fleet/scim.go b/server/fleet/scim.go
index f64ff3291e..dab97f38d4 100644
--- a/server/fleet/scim.go
+++ b/server/fleet/scim.go
@@ -12,6 +12,7 @@ type ScimUser struct {
UserName string `db:"user_name"`
GivenName *string `db:"given_name"`
FamilyName *string `db:"family_name"`
+ Department *string `db:"department"`
Active *bool `db:"active"`
UpdatedAt time.Time `db:"updated_at"`
Emails []ScimUserEmail
diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go
index b45dfe1f7f..88cfc3435a 100644
--- a/server/service/apple_mdm.go
+++ b/server/service/apple_mdm.go
@@ -73,6 +73,7 @@ var (
fleetVarHostEndUserEmailIDPRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarHostEndUserEmailIDP))
fleetVarHostHardwareSerialRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarHostHardwareSerial))
fleetVarHostEndUserIDPUsernameRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarHostEndUserIDPUsername))
+ fleetVarHostEndUserIDPDepartmentRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarHostEndUserIDPDepartment))
fleetVarHostEndUserIDPUsernameLocalPartRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarHostEndUserIDPUsernameLocalPart))
fleetVarHostEndUserIDPGroupsRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarHostEndUserIDPGroups))
fleetVarSCEPRenewalIDRegexp = regexp.MustCompile(fmt.Sprintf(`(\$FLEET_VAR_%s)|(\${FLEET_VAR_%[1]s})`, fleet.FleetVarSCEPRenewalID))
@@ -80,7 +81,7 @@ var (
fleetVarsSupportedInConfigProfiles = []string{
fleet.FleetVarNDESSCEPChallenge, fleet.FleetVarNDESSCEPProxyURL, fleet.FleetVarHostEndUserEmailIDP,
fleet.FleetVarHostHardwareSerial, fleet.FleetVarHostEndUserIDPUsername, fleet.FleetVarHostEndUserIDPUsernameLocalPart,
- fleet.FleetVarHostEndUserIDPGroups, fleet.FleetVarSCEPRenewalID,
+ fleet.FleetVarHostEndUserIDPGroups, fleet.FleetVarHostEndUserIDPDepartment, fleet.FleetVarSCEPRenewalID,
}
)
@@ -4575,7 +4576,7 @@ func preprocessProfileContents(
case fleetVar == fleet.FleetVarHostEndUserEmailIDP || fleetVar == fleet.FleetVarHostHardwareSerial ||
fleetVar == fleet.FleetVarHostEndUserIDPUsername || fleetVar == fleet.FleetVarHostEndUserIDPUsernameLocalPart ||
- fleetVar == fleet.FleetVarHostEndUserIDPGroups || fleetVar == fleet.FleetVarSCEPRenewalID:
+ fleetVar == fleet.FleetVarHostEndUserIDPGroups || fleetVar == fleet.FleetVarHostEndUserIDPDepartment || fleetVar == fleet.FleetVarSCEPRenewalID:
// No extra validation needed for these variables
case strings.HasPrefix(fleetVar, fleet.FleetVarDigiCertPasswordPrefix) || strings.HasPrefix(fleetVar, fleet.FleetVarDigiCertDataPrefix):
@@ -4799,7 +4800,7 @@ func preprocessProfileContents(
hostContents = replaceFleetVariableInXML(fleetVarHostHardwareSerialRegexp, hostContents, hardwareSerial)
case fleetVar == fleet.FleetVarHostEndUserIDPUsername || fleetVar == fleet.FleetVarHostEndUserIDPUsernameLocalPart ||
- fleetVar == fleet.FleetVarHostEndUserIDPGroups:
+ fleetVar == fleet.FleetVarHostEndUserIDPGroups || fleetVar == fleet.FleetVarHostEndUserIDPDepartment:
user, ok, err := getHostEndUserIDPUser(ctx, ds, target, hostUUID, fleetVar, hostIDForUUIDCache)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting host end user IDP username")
@@ -4821,6 +4822,9 @@ func preprocessProfileContents(
case fleet.FleetVarHostEndUserIDPGroups:
rx = fleetVarHostEndUserIDPGroupsRegexp
value = strings.Join(user.IdpGroups, ",")
+ case fleet.FleetVarHostEndUserIDPDepartment:
+ rx = fleetVarHostEndUserIDPDepartmentRegexp
+ value = user.Department
}
hostContents = replaceFleetVariableInXML(rx, hostContents, value)
diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go
index de4e883ea6..8c0fa03f8b 100644
--- a/server/service/apple_mdm_test.go
+++ b/server/service/apple_mdm_test.go
@@ -4796,6 +4796,54 @@ func TestPreprocessProfileContentsEndUserIDP(t *testing.T) {
assert.Contains(t, updatedProfile.Detail, "There is no IdP groups for this host. Fleet couldn’t populate $FLEET_VAR_HOST_END_USER_IDP_GROUPS.")
},
},
+ {
+ desc: "profile with scim department",
+ profileContent: "$FLEET_VAR_" + fleet.FleetVarHostEndUserIDPDepartment,
+ expectedStatus: fleet.MDMDeliveryPending,
+ setup: func() {
+ ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
+ require.EqualValues(t, 1, hostID)
+ return &fleet.ScimUser{
+ UserName: "user1@example.com",
+ Groups: []fleet.ScimUserGroup{},
+ Department: ptr.String("Engineering"),
+ }, nil
+ }
+ ds.ListHostDeviceMappingFunc = func(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
+ return nil, nil
+ }
+ },
+ assert: func(output string) {
+ assert.Empty(t, updatedPayload.Detail) // no error detail
+ assert.Len(t, targets, 1) // target is still present
+ require.Equal(t, "Engineering", output)
+ },
+ },
+ {
+ // Given department is not required, we don't make a profile fail if the user has no department,
+ // we replace by empty string instead.
+ desc: "profile with scim department, user has no department, replaces with empty string",
+ profileContent: "$FLEET_VAR_" + fleet.FleetVarHostEndUserIDPDepartment,
+ expectedStatus: fleet.MDMDeliveryPending,
+ setup: func() {
+ ds.ScimUserByHostIDFunc = func(ctx context.Context, hostID uint) (*fleet.ScimUser, error) {
+ require.EqualValues(t, 1, hostID)
+ return &fleet.ScimUser{
+ UserName: "user1@example.com",
+ Groups: []fleet.ScimUserGroup{},
+ Department: nil,
+ }, nil
+ }
+ ds.ListHostDeviceMappingFunc = func(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
+ return nil, nil
+ }
+ },
+ assert: func(output string) {
+ assert.Empty(t, updatedPayload.Detail) // no error detail
+ assert.Len(t, targets, 1) // target is still present
+ require.Empty(t, output) // replaces to empty string
+ },
+ },
}
for _, c := range cases {
@@ -5027,7 +5075,12 @@ func TestValidateConfigProfileFleetVariables(t *testing.T) {
name: "Custom profile with IdP variables happy path",
profile: customProfileForValidation("value"),
errMsg: "",
- vars: []string{"HOST_END_USER_IDP_USERNAME", "HOST_END_USER_IDP_USERNAME_LOCAL_PART", "HOST_END_USER_IDP_GROUPS"},
+ vars: []string{
+ "HOST_END_USER_IDP_USERNAME",
+ "HOST_END_USER_IDP_USERNAME_LOCAL_PART",
+ "HOST_END_USER_IDP_GROUPS",
+ "HOST_END_USER_IDP_DEPARTMENT",
+ },
},
{
name: "Custom SCEP and NDES 2 valid profiles should error",
diff --git a/server/service/hosts.go b/server/service/hosts.go
index ecb3c88fe0..cdf1f71aa5 100644
--- a/server/service/hosts.go
+++ b/server/service/hosts.go
@@ -1378,12 +1378,16 @@ func getEndUsers(ctx context.Context, ds fleet.Datastore, hostID uint) ([]fleet.
IdpFullName: scimUser.DisplayName(),
IdpInfoUpdatedAt: ptr.Time(scimUser.UpdatedAt),
}
+
if scimUser.ExternalID != nil {
endUser.IdpID = *scimUser.ExternalID
}
for _, group := range scimUser.Groups {
endUser.IdpGroups = append(endUser.IdpGroups, group.DisplayName)
}
+ if scimUser.Department != nil {
+ endUser.Department = *scimUser.Department
+ }
endUsers = append(endUsers, endUser)
}
diff --git a/server/service/testdata/profiles/custom-profile-validation.mobileconfig b/server/service/testdata/profiles/custom-profile-validation.mobileconfig
index 05c278456e..ed9f7fd54c 100644
--- a/server/service/testdata/profiles/custom-profile-validation.mobileconfig
+++ b/server/service/testdata/profiles/custom-profile-validation.mobileconfig
@@ -9,6 +9,8 @@
$FLEET_VAR_HOST_END_USER_IDP_USERNAME
LocalPart
${FLEET_VAR_HOST_END_USER_IDP_USERNAME_LOCAL_PART}
+ Department
+ ${FLEET_VAR_HOST_END_USER_IDP_DEPARTMENT}
Groups
$FLEET_VAR_HOST_END_USER_IDP_GROUPS
Other