diff --git a/pkg/iam/scim/bridge/bridge.go b/pkg/iam/scim/bridge/bridge.go index 990a48948..5bc5de253 100644 --- a/pkg/iam/scim/bridge/bridge.go +++ b/pkg/iam/scim/bridge/bridge.go @@ -104,17 +104,20 @@ func (s *Bridge) Run(ctx context.Context) (created, updated, deleted, deactivate } created++ } else { - needsUpdate := s.forceUpdate - - if existingSCIM.Active != pu.Active { - needsUpdate = true - } - if existingSCIM.DisplayName != pu.DisplayName { - needsUpdate = true - } - if existingSCIM.Title != pu.Title { - needsUpdate = true - } + needsUpdate := s.forceUpdate || + existingSCIM.Active != pu.Active || + existingSCIM.DisplayName != pu.DisplayName || + existingSCIM.Title != pu.Title || + existingSCIM.GivenName != pu.GivenName || + existingSCIM.FamilyName != pu.FamilyName || + existingSCIM.ExternalID != pu.ExternalID || + existingSCIM.Department != pu.Department || + existingSCIM.CostCenter != pu.CostCenter || + existingSCIM.EnterpriseOrganization != pu.EnterpriseOrganization || + existingSCIM.Division != pu.Division || + existingSCIM.EmployeeNumber != pu.EmployeeNumber || + existingSCIM.ManagerValue != pu.ManagerValue || + existingSCIM.PreferredLanguage != pu.PreferredLanguage if needsUpdate { if !s.dryRun { diff --git a/pkg/iam/scim/bridge/client/client.go b/pkg/iam/scim/bridge/client/client.go index 8ce22ff4d..33b828b41 100644 --- a/pkg/iam/scim/bridge/client/client.go +++ b/pkg/iam/scim/bridge/client/client.go @@ -33,13 +33,21 @@ type ( } User struct { - ID string `json:"id,omitempty"` - UserName string `json:"userName"` - DisplayName string `json:"displayName"` - GivenName string `json:"-"` - FamilyName string `json:"-"` - Active bool `json:"active"` - Title string `json:"title"` + ID string `json:"id,omitempty"` + UserName string `json:"userName"` + DisplayName string `json:"displayName"` + GivenName string `json:"-"` + FamilyName string `json:"-"` + Active bool `json:"active"` + Title string `json:"title"` + ExternalID string `json:"-"` + Department string `json:"-"` + CostCenter string `json:"-"` + EnterpriseOrganization string `json:"-"` + Division string `json:"-"` + EmployeeNumber string `json:"-"` + ManagerValue string `json:"-"` + PreferredLanguage string `json:"-"` } Users []User @@ -114,25 +122,7 @@ func (c *Client) listUsersPage(ctx context.Context, startIndex, count int) (User } func (c *Client) CreateUser(ctx context.Context, user *User) error { - payload := map[string]any{ - "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, - "userName": user.UserName, - "name": map[string]string{ - "givenName": user.GivenName, - "familyName": user.FamilyName, - "formatted": user.DisplayName, - }, - "displayName": user.DisplayName, - "active": user.Active, - "emails": []map[string]any{ - { - "value": user.UserName, - "type": "work", - "primary": true, - }, - }, - "title": user.Title, - } + payload := buildUserPayload(user) body, err := json.Marshal(payload) if err != nil { @@ -163,25 +153,7 @@ func (c *Client) CreateUser(ctx context.Context, user *User) error { } func (c *Client) UpdateUser(ctx context.Context, userID string, user *User) error { - payload := map[string]any{ - "schemas": []string{"urn:ietf:params:scim:schemas:core:2.0:User"}, - "userName": user.UserName, - "name": map[string]string{ - "givenName": user.GivenName, - "familyName": user.FamilyName, - "formatted": user.DisplayName, - }, - "displayName": user.DisplayName, - "active": user.Active, - "emails": []map[string]any{ - { - "value": user.UserName, - "type": "work", - "primary": true, - }, - }, - "title": user.Title, - } + payload := buildUserPayload(user) body, err := json.Marshal(payload) if err != nil { @@ -211,6 +183,65 @@ func (c *Client) UpdateUser(ctx context.Context, userID string, user *User) erro return nil } +func buildUserPayload(user *User) map[string]any { + schemas := []string{"urn:ietf:params:scim:schemas:core:2.0:User"} + + enterprise := map[string]any{} + if user.EmployeeNumber != "" { + enterprise["employeeNumber"] = user.EmployeeNumber + } + if user.Department != "" { + enterprise["department"] = user.Department + } + if user.CostCenter != "" { + enterprise["costCenter"] = user.CostCenter + } + if user.EnterpriseOrganization != "" { + enterprise["organization"] = user.EnterpriseOrganization + } + if user.Division != "" { + enterprise["division"] = user.Division + } + if user.ManagerValue != "" { + enterprise["manager"] = map[string]string{"value": user.ManagerValue} + } + + payload := map[string]any{ + "schemas": schemas, + "userName": user.UserName, + "name": map[string]string{ + "givenName": user.GivenName, + "familyName": user.FamilyName, + "formatted": user.DisplayName, + }, + "displayName": user.DisplayName, + "active": user.Active, + "emails": []map[string]any{ + { + "value": user.UserName, + "type": "work", + "primary": true, + }, + }, + "title": user.Title, + } + + if user.ExternalID != "" { + payload["externalId"] = user.ExternalID + } + if user.PreferredLanguage != "" { + payload["preferredLanguage"] = user.PreferredLanguage + } + + if len(enterprise) > 0 { + schemas = append(schemas, "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User") + payload["schemas"] = schemas + payload["urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"] = enterprise + } + + return payload +} + func (c *Client) DeactivateUser(ctx context.Context, userID string) error { payload := map[string]any{ "schemas": []string{"urn:ietf:params:scim:api:messages:2.0:PatchOp"}, diff --git a/pkg/iam/scim/bridge/provider/googleworkspace/provider.go b/pkg/iam/scim/bridge/provider/googleworkspace/provider.go index 580bfb2c5..ff0926a06 100644 --- a/pkg/iam/scim/bridge/provider/googleworkspace/provider.go +++ b/pkg/iam/scim/bridge/provider/googleworkspace/provider.go @@ -83,47 +83,20 @@ func (p *Provider) ListUsers(ctx context.Context) (scimclient.Users, error) { continue } - var title string - - if u.Organizations == nil { - title = "" + user := scimclient.User{ + UserName: u.PrimaryEmail, + DisplayName: u.Name.FullName, + GivenName: u.Name.GivenName, + FamilyName: u.Name.FamilyName, + Active: !u.Suspended && !u.Archived, } - data, err := json.Marshal(u.Organizations) - if err != nil { - title = "" - } + p.extractOrganizationFields(u.Organizations, &user) + p.extractExternalID(u.ExternalIds, &user) + p.extractRelations(u.Relations, &user) + p.extractPreferredLanguage(u.Languages, &user) - var orgs []admin.UserOrganization - if err := json.Unmarshal(data, &orgs); err != nil { - title = "" - } - - // Prefer the primary organization's title - for _, org := range orgs { - if org.Primary && org.Title != "" { - title = org.Title - } - } - - // Fall back to the first organization with a title - for _, org := range orgs { - if org.Title != "" { - title = org.Title - } - } - - allUsers = append( - allUsers, - scimclient.User{ - UserName: u.PrimaryEmail, - DisplayName: u.Name.FullName, - GivenName: u.Name.GivenName, - FamilyName: u.Name.FamilyName, - Active: !u.Suspended && !u.Archived, - Title: title, - }, - ) + allUsers = append(allUsers, user) } pageToken = resp.NextPageToken @@ -134,3 +107,122 @@ func (p *Provider) ListUsers(ctx context.Context) (scimclient.Users, error) { return allUsers, nil } + +func (p *Provider) extractOrganizationFields(raw interface{}, user *scimclient.User) { + if raw == nil { + return + } + + data, err := json.Marshal(raw) + if err != nil { + return + } + + var orgs []admin.UserOrganization + if err := json.Unmarshal(data, &orgs); err != nil { + return + } + + var primary *admin.UserOrganization + var first *admin.UserOrganization + for i := range orgs { + if first == nil { + first = &orgs[i] + } + if orgs[i].Primary { + primary = &orgs[i] + break + } + } + + org := primary + if org == nil { + org = first + } + if org == nil { + return + } + + user.Title = org.Title + user.Department = org.Department + user.CostCenter = org.CostCenter + user.EnterpriseOrganization = org.Name + user.Division = org.Description +} + +func (p *Provider) extractExternalID(raw interface{}, user *scimclient.User) { + if raw == nil { + return + } + + data, err := json.Marshal(raw) + if err != nil { + return + } + + var ids []admin.UserExternalId + if err := json.Unmarshal(data, &ids); err != nil { + return + } + + for _, id := range ids { + if id.Type == "organization" && id.Value != "" { + user.ExternalID = id.Value + return + } + } + + if len(ids) > 0 && ids[0].Value != "" { + user.ExternalID = ids[0].Value + } +} + +func (p *Provider) extractRelations(raw interface{}, user *scimclient.User) { + if raw == nil { + return + } + + data, err := json.Marshal(raw) + if err != nil { + return + } + + var relations []admin.UserRelation + if err := json.Unmarshal(data, &relations); err != nil { + return + } + + for _, rel := range relations { + if rel.Type == "manager" && rel.Value != "" { + user.ManagerValue = rel.Value + return + } + } +} + +func (p *Provider) extractPreferredLanguage(raw interface{}, user *scimclient.User) { + if raw == nil { + return + } + + data, err := json.Marshal(raw) + if err != nil { + return + } + + var languages []admin.UserLanguage + if err := json.Unmarshal(data, &languages); err != nil { + return + } + + for _, lang := range languages { + if lang.Preference == "preferred" && lang.LanguageCode != "" { + user.PreferredLanguage = lang.LanguageCode + return + } + } + + if len(languages) > 0 && languages[0].LanguageCode != "" { + user.PreferredLanguage = languages[0].LanguageCode + } +}