fix(scim): add type attribute to ScimEmail (#9690)

# Which Problems Are Solved

- SCIM PATCH operations for users from Entra ID for the `emails`
attribute fails due to missing `type` subattribute

# How the Problems Are Solved

- Adds the `type` attribute to the `ScimUser` struct and sets the
default value to `"work"` in the `mapWriteModelToScimUser()` method.

# Additional Changes

# Additional Context

The SCIM handlers for POST and PUT ignore multiple emails and only uses
the primary email for a given user, or falls back to the first email if
none are marked as primary. PATCH operations however, will attempt to
resolve the provided filter in `operations[].path`.

Some services, such as Entra ID, only support patching emails by
filtering for `emails[type eq "(work|home|other)"].value`, which fails
with Zitadel as the ScimUser struct (and thus the generated schema)
doesn't include the `type` field.

This commit adds the `type` field to work around this issue, while still
preserving compatibility with filters such as `emails[primary eq
true].value`.

-
https://discord.com/channels/927474939156643850/927866013545025566/1356556668527448191

---------

Co-authored-by: Christer Edvartsen <christer.edvartsen@nav.no>
Co-authored-by: Thomas Siegfried Krampl <thomas.siegfried.krampl@nav.no>
This commit is contained in:
Trong Huu Nguyen
2025-06-19 11:42:44 +02:00
committed by GitHub
parent 28f7218ea1
commit 3a4298c179
12 changed files with 135 additions and 2 deletions

View File

@@ -90,6 +90,7 @@ type ScimIms struct {
type ScimEmail struct {
Value string `json:"value" scim:"required"`
Primary bool `json:"primary"`
Type string `json:"type,omitempty"`
}
type ScimPhoneNumber struct {

View File

@@ -382,6 +382,10 @@ func (h *UsersHandler) mapAndValidateMetadata(ctx context.Context, user *ScimUse
if err := extractJsonMetadata(ctx, md, metadata.KeyRoles, &user.Roles); err != nil {
logging.OnError(err).Warn("Could not deserialize scim roles metadata")
}
if err := extractJsonMetadata(ctx, md, metadata.KeyEmails, &user.Emails); err != nil {
logging.OnError(err).Warn("Could not deserialize scim emails metadata")
}
}
func (h *UsersHandler) buildResourceForQuery(ctx context.Context, user *query.User) *schemas.Resource {

View File

@@ -129,7 +129,8 @@ func getValueForMetadataKey(user *ScimUser, key metadata.Key) ([]byte, error) {
metadata.KeyAddresses,
metadata.KeyEntitlements,
metadata.KeyIms,
metadata.KeyPhotos:
metadata.KeyPhotos,
metadata.KeyEmails:
val, err := json.Marshal(value)
if err != nil {
return nil, err
@@ -223,6 +224,8 @@ func getRawValueForMetadataKey(user *ScimUser, key metadata.Key) interface{} {
return user.Locale
case metadata.KeyTimezone:
return user.Timezone
case metadata.KeyEmails:
return user.Emails
case metadata.KeyProvisioningDomain:
break
}

View File

@@ -685,6 +685,39 @@ func TestOperationCollection_Apply(t *testing.T) {
},
wantErr: true,
},
{
name: "replace filter complex subattribute multiple emails primary value",
op: &patch.Operation{
Operation: patch.OperationTypeReplace,
Path: test.Must(filter.ParsePath(`emails[primary eq true].value`)),
Value: json.RawMessage(`"jeanie.rebecca.pendleton@example.com"`),
},
want: &ScimUser{
Emails: []*ScimEmail{
{
Value: "jeanie.rebecca.pendleton@example.com",
Primary: true,
},
},
},
},
{
name: "replace filter complex subattribute multiple emails type value",
op: &patch.Operation{
Operation: patch.OperationTypeReplace,
Path: test.Must(filter.ParsePath(`emails[type eq "work"].value`)),
Value: json.RawMessage(`"jeanie.rebecca.pendleton@example.com"`),
},
want: &ScimUser{
Emails: []*ScimEmail{
{
Value: "jeanie.rebecca.pendleton@example.com",
Primary: true,
Type: "work",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -711,6 +744,7 @@ func TestOperationCollection_Apply(t *testing.T) {
{
Value: "jeanie.pendleton@example.com",
Primary: true,
Type: "work",
},
},
PhoneNumbers: []*ScimPhoneNumber{