diff --git a/pkg/iam/scim/bridge/bridge.go b/pkg/iam/scim/bridge/bridge.go index eeecf3091..990a48948 100644 --- a/pkg/iam/scim/bridge/bridge.go +++ b/pkg/iam/scim/bridge/bridge.go @@ -112,6 +112,9 @@ func (s *Bridge) Run(ctx context.Context) (created, updated, deleted, deactivate if existingSCIM.DisplayName != pu.DisplayName { needsUpdate = true } + if existingSCIM.Title != pu.Title { + needsUpdate = true + } if needsUpdate { if !s.dryRun { diff --git a/pkg/iam/scim/bridge/client/client.go b/pkg/iam/scim/bridge/client/client.go index 0558a9418..8ce22ff4d 100644 --- a/pkg/iam/scim/bridge/client/client.go +++ b/pkg/iam/scim/bridge/client/client.go @@ -39,6 +39,7 @@ type ( GivenName string `json:"-"` FamilyName string `json:"-"` Active bool `json:"active"` + Title string `json:"title"` } Users []User @@ -130,6 +131,7 @@ func (c *Client) CreateUser(ctx context.Context, user *User) error { "primary": true, }, }, + "title": user.Title, } body, err := json.Marshal(payload) @@ -178,6 +180,7 @@ func (c *Client) UpdateUser(ctx context.Context, userID string, user *User) erro "primary": true, }, }, + "title": user.Title, } body, err := json.Marshal(payload) diff --git a/pkg/iam/scim/bridge/provider/googleworkspace/provider.go b/pkg/iam/scim/bridge/provider/googleworkspace/provider.go index bc0cab562..580bfb2c5 100644 --- a/pkg/iam/scim/bridge/provider/googleworkspace/provider.go +++ b/pkg/iam/scim/bridge/provider/googleworkspace/provider.go @@ -18,6 +18,7 @@ package googleworkspace import ( "context" + "encoding/json" "fmt" "net/http" "strings" @@ -82,6 +83,36 @@ func (p *Provider) ListUsers(ctx context.Context) (scimclient.Users, error) { continue } + var title string + + if u.Organizations == nil { + title = "" + } + + data, err := json.Marshal(u.Organizations) + if err != nil { + title = "" + } + + 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{ @@ -90,6 +121,7 @@ func (p *Provider) ListUsers(ctx context.Context) (scimclient.Users, error) { GivenName: u.Name.GivenName, FamilyName: u.Name.FamilyName, Active: !u.Suspended && !u.Archived, + Title: title, }, ) } diff --git a/pkg/iam/scim/schema.go b/pkg/iam/scim/schema.go index 07d3d302c..f3719040e 100644 --- a/pkg/iam/scim/schema.go +++ b/pkg/iam/scim/schema.go @@ -93,6 +93,13 @@ func UserSchema() schema.Schema { }, }, ), + schema.SimpleCoreAttribute( + schema.SimpleStringParams( + schema.StringParams{ + Name: "title", + }, + ), + ), }, } } diff --git a/pkg/iam/scim/service.go b/pkg/iam/scim/service.go index d2d4b4504..614772423 100644 --- a/pkg/iam/scim/service.go +++ b/pkg/iam/scim/service.go @@ -128,7 +128,7 @@ func (s *Service) CreateUser( config *coredata.SCIMConfiguration, attributes scim.ResourceAttributes, ) (scim.Resource, error) { - email, fullName, active := ParseUserFromAttributes(attributes) + email, fullName, active, title := ParseUserFromAttributes(attributes) if email == "" { return scim.Resource{}, scimerrors.ScimErrorBadRequest("userName or email is required") } @@ -190,6 +190,7 @@ func (s *Service) CreateUser( Source: coredata.ProfileSourceSCIM, State: profileState, FullName: fullName, + Position: &title, CreatedAt: now, UpdatedAt: now, } @@ -204,6 +205,8 @@ func (s *Service) CreateUser( } else { profile.Source = coredata.ProfileSourceSCIM profile.State = profileState + profile.FullName = fullName + profile.Position = &title profile.UpdatedAt = now if err := profile.Update(ctx, tx, scope); err != nil { return fmt.Errorf("cannot update profile: %w", err) @@ -379,8 +382,8 @@ func (s *Service) ReplaceUser( profileID gid.GID, attributes scim.ResourceAttributes, ) (scim.Resource, error) { - fullName, active := ParseUserFromReplaceAttributes(attributes) - profile, err := s.updateUser(ctx, config, profileID, fullName, active) + fullName, active, title := ParseUserFromReplaceAttributes(attributes) + profile, err := s.updateUser(ctx, config, profileID, fullName, active, title) if err != nil { return scim.Resource{}, err } @@ -394,8 +397,8 @@ func (s *Service) PatchUser( profileID gid.GID, operations []scim.PatchOperation, ) (scim.Resource, error) { - fullName, active := ParseUserFromPatchOperations(operations) - profile, err := s.updateUser(ctx, config, profileID, fullName, active) + fullName, active, title := ParseUserFromPatchOperations(operations) + profile, err := s.updateUser(ctx, config, profileID, fullName, active, title) if err != nil { return scim.Resource{}, err } @@ -409,6 +412,7 @@ func (s *Service) updateUser( profileID gid.GID, fullName string, active *bool, + title string, ) (*coredata.MembershipProfile, error) { scope := coredata.NewScopeFromObjectID(config.OrganizationID) now := time.Now() @@ -447,6 +451,12 @@ func (s *Service) updateUser( profile.UpdatedAt = now } + if title == "" { + profile.Position = nil + } else { + profile.Position = &title + } + if shouldReactivate { profile.State = coredata.ProfileStateActive profile.UpdatedAt = now @@ -598,7 +608,7 @@ func (s *Service) createEvent( return event } -func ParseUserFromAttributes(attributes scim.ResourceAttributes) (email string, fullName string, active bool) { +func ParseUserFromAttributes(attributes scim.ResourceAttributes) (email string, fullName string, active bool, title string) { userName, _ := attributes["userName"].(string) displayName, _ := attributes["displayName"].(string) @@ -646,10 +656,14 @@ func ParseUserFromAttributes(attributes scim.ResourceAttributes) (email string, fullName = userName } - return email, fullName, active + if t, ok := attributes["title"].(string); ok { + title = t + } + + return email, fullName, active, title } -func ParseUserFromReplaceAttributes(attributes scim.ResourceAttributes) (fullName string, active *bool) { +func ParseUserFromReplaceAttributes(attributes scim.ResourceAttributes) (fullName string, active *bool, title string) { displayName, _ := attributes["displayName"].(string) var givenName, familyName string @@ -668,10 +682,14 @@ func ParseUserFromReplaceAttributes(attributes scim.ResourceAttributes) (fullNam activeVal = a } - return fullName, &activeVal + if t, ok := attributes["title"].(string); ok { + title = t + } + + return fullName, &activeVal, title } -func ParseUserFromPatchOperations(operations []scim.PatchOperation) (fullName string, active *bool) { +func ParseUserFromPatchOperations(operations []scim.PatchOperation) (fullName string, active *bool, title string) { var givenName, familyName string for _, op := range operations { @@ -720,6 +738,10 @@ func ParseUserFromPatchOperations(operations []scim.PatchOperation) (fullName st if name, ok := op.Value.(string); ok { familyName = name } + case "title": + if t, ok := op.Value.(string); ok { + title = t + } } } } @@ -729,7 +751,7 @@ func ParseUserFromPatchOperations(operations []scim.PatchOperation) (fullName st fullName = strings.TrimSpace(givenName + " " + familyName) } - return fullName, active + return fullName, active, title } func userToResource(p *coredata.MembershipProfile) scim.Resource { @@ -750,6 +772,7 @@ func userToResource(p *coredata.MembershipProfile) scim.Resource { "primary": true, }, }, + "title": p.Position, }, Meta: scim.Meta{ Created: &p.CreatedAt,