Update Google SCIM bridge

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2026-03-11 09:53:57 +01:00
parent dd3885ac54
commit f674426dfa
3 changed files with 220 additions and 94 deletions

View File

@@ -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 {

View File

@@ -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"},

View File

@@ -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
}
}