Skip to content

Support multivalued user attributes in SCIM - #50865

Open
cgeorgilakis wants to merge 1 commit into
keycloak:mainfrom
eosc-kc:50856_scim_multivalues
Open

Support multivalued user attributes in SCIM#50865
cgeorgilakis wants to merge 1 commit into
keycloak:mainfrom
eosc-kc:50856_scim_multivalues

Conversation

@cgeorgilakis

Copy link
Copy Markdown
Contributor

Closes #50856

Copilot AI balanced review requested due to automatic review settings July 14, 2026 12:36
@cgeorgilakis
cgeorgilakis requested a review from a team as a code owner July 14, 2026 12:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds SCIM exposure of all values from multivalued user-profile attributes.

Changes:

  • Returns collections for multivalued attributes.
  • Serializes .value attributes as SCIM complex arrays.
  • Adds integration coverage for simple and complex custom attributes.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
AbstractUserModelSchema.java Reads all values for multivalued attributes.
UserExtensionModelSchema.java Maps multivalued extension values to SCIM arrays.
UserTest.java Tests both supported multivalued representations.

Copilot AI review requested due to automatic review settings July 15, 2026 08:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

Copilot AI review requested due to automatic review settings July 15, 2026 13:21
@cgeorgilakis
cgeorgilakis force-pushed the 50856_scim_multivalues branch from 48f10e5 to 160abdf Compare July 15, 2026 13:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

private Attribute<UserModel, User> createCustomAttribute(Object scimName, boolean isMultivalued) {
Attribute.Builder<UserModel, User> builder = Attribute.<UserModel, User>simple(scimName.toString());
if (isMultivalued) {
builder = builder.multivalued()
Copilot AI review requested due to automatic review settings July 15, 2026 13:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Copilot AI review requested due to automatic review settings July 15, 2026 13:35
@cgeorgilakis
cgeorgilakis force-pushed the 50856_scim_multivalues branch from 72e3b60 to 8d864bc Compare July 15, 2026 13:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Copilot AI review requested due to automatic review settings July 21, 2026 12:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (2)

scim/model/src/main/java/org/keycloak/scim/model/user/UserExtensionModelSchema.java:225

  • A filtered PATCH remove on these new multivalued mappings deletes the entire attribute. For example, remove assurance[value eq "x"] resolves the mapper through its .value alias, but ScimFilterToJsonNodeConverter returns null when complexType == null; this branch then interprets that as remove-all instead of removing only x. The filter conversion needs to support these simple-backed multivalued mappings (and this case should be covered by a PATCH test).
                        if (values == null || values.isEmpty()) {
                            model.removeAttribute(name);
                            return;

scim/model/src/main/java/org/keycloak/scim/model/user/UserExtensionModelSchema.java:322

  • Multivalued nested mappings are only shaped correctly when the sub-attribute is named value. A mapping such as roles.type falls through and emits "roles":{"type":[...]}, while the schema change declares roles itself multivalued (which requires an array of complex objects). Either serialize every multivalued nested mapping as an array of objects or reject non-value mappings during validation.
                        // Handle multivalued attributes annotated as "<parent>.value"
                        // so that SCIM gets: "<parent>": [ { "value": "..." }, ... ]
                        if ("value".equals(attributeName) && value instanceof Collection<?> values) {

@sguilhen

Copy link
Copy Markdown
Contributor

@cgeorgilakis All commits need to be signed-off

@sguilhen
sguilhen requested a review from pedroigor July 29, 2026 13:08
Copilot AI review requested due to automatic review settings August 4, 2026 10:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Not ready to approve

Nested custom attributes are not correctly published in schema discovery, and unsupported nested mappings produce inconsistent representations.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (2)

scim/model/src/main/java/org/keycloak/scim/model/user/UserExtensionModelSchema.java:322

  • Only multivalued .value mappings enter this array-of-objects branch. A lone multivalued mapping such as <parent>.type passes the new validation but is emitted as { "parent": { "type": [...] } }, while the schema now declares parent multivalued; writes from the declared array shape also collapse to an empty string. Either reject multivalued nested mappings other than .value, or implement the same array-of-objects handling for them.
                        // Handle multivalued attributes annotated as "<parent>.value"
                        // so that SCIM gets: "<parent>": [ { "value": "..." }, ... ]
                        if ("value".equals(attributeName) && value instanceof Collection<?> values) {

scim/model/src/main/java/org/keycloak/scim/model/schema/SchemaResourceTypeProvider.java:94

  • This parent is only added to the local topLevelAttributes map. For the internal UserExtensionModelSchema, that representation is discarded at the end of buildSchema; unlike the simple-extension branch at line 148, it never adds the nested attribute to schemas under parentName. Consequently the new assurance.value mapping is absent from /Schemas (and this multiValued flag is never exposed), so nested custom parents must also be registered on their actual extension schema.
                        p.setMultiValued(attribute.isMultivalued());
  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Copilot AI review requested due to automatic review settings August 4, 2026 12:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Not ready to approve

Nested multivalued sub-attributes other than .value produce a representation that contradicts the advertised SCIM schema.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (2)

scim/model/src/main/java/org/keycloak/scim/model/user/UserExtensionModelSchema.java:322

  • A multivalued mapping to a nested sub-attribute other than .value (for example, roles.type) still falls through to a map containing a list, producing "roles":{"type":[...]} even though the schema now advertises roles as a multivalued complex attribute. Encode every nested multivalued mapping as an array of objects and decode the corresponding sub-attribute on input, or reject these unsupported mappings explicitly.
                        // Handle multivalued attributes annotated as "<parent>.value"
                        // so that SCIM gets: "<parent>": [ { "value": "..." }, ... ]
                        if ("value".equals(attributeName) && value instanceof Collection<?> values) {

scim/model/src/main/java/org/keycloak/scim/model/schema/SchemaResourceTypeProvider.java:94

  • The changed /Schemas contract is not covered by the new tests: SchemaTest extensively asserts multiValued metadata, but no test verifies that a multivalued custom complex parent is reported as multiValued: true. Add a schema-discovery assertion for the new assurance mapping so regressions in this branch are detected.
                        p.setMultiValued(attribute.isMultivalued());
  • Files reviewed: 5/5 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

@cgeorgilakis

Copy link
Copy Markdown
Contributor Author

@cgeorgilakis All commits need to be signed-off

I know it. It was a temporary commit based on your comment.
Now, it is one signed-off commit.

Copilot AI review requested due to automatic review settings August 5, 2026 07:12
@cgeorgilakis
cgeorgilakis force-pushed the 50856_scim_multivalues branch from 1530ada to ddf9ec2 Compare August 5, 2026 07:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Nested mapping shapes and cross-schema parent collisions can produce incorrect SCIM responses and discovery schemas.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (2)

scim/model/src/main/java/org/keycloak/scim/model/user/UserExtensionModelSchema.java:164

  • This validation still accepts a multivalued nested mapping such as phones.type. The mapper marks phones as multivalued in discovery, but the representation setter handles collections specially only for .value, producing phones: {"type": [...]} instead of the advertised array of complex values; either reject multivalued non-.value mappings or serialize them consistently.
            boolean hasMultivaluedValue = mappings.stream()
                    .anyMatch(mapping -> "value".equals(mapping.subAttributeName()) && mapping.multivalued());
            List<ExtensionSubAttributeMapping> siblings = mappings.stream()
                    .filter(mapping -> !("value".equals(mapping.subAttributeName()) && mapping.multivalued()))
                    .toList();

scim/model/src/main/java/org/keycloak/scim/model/schema/SchemaResourceTypeProvider.java:102

  • Nested parents are cached only by topName, so if two custom schema URNs both define (for example) contact.value, this callback runs only for the first schema. The second schema is then omitted from /Schemas, while its sub-attribute is appended to the first schema's parent; include the schema URN in the cache key for extension attributes.
                            addToExtensionSchema(parentName, p);
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Copilot AI review requested due to automatic review settings August 5, 2026 07:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Filtered removals can erase all values, and schema discovery can produce missing or inaccurate extension definitions.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (3)

scim/model/src/main/java/org/keycloak/scim/model/user/UserExtensionModelSchema.java:225

  • A filtered PATCH removal such as assurance[value eq "x"] clears the entire attribute instead of only x. This mapper is still a simple attribute (complexType == null), so ScimFilterToJsonNodeConverter returns null for the filter; this branch then calls removeAttribute, making the selective-removal logic below unreachable for the complex .value form.
                        if (values == null || values.isEmpty()) {
                            model.removeAttribute(name);
                            return;

scim/model/src/main/java/org/keycloak/scim/model/schema/SchemaResourceTypeProvider.java:102

  • Nested parents are cached only by topName, so if two extension schemas both define (for example) roles.value, this registration runs only for the first schema and the second /Schemas/{urn} omits roles. Key extension parents by schema URN plus name while keeping the exposed attribute name unchanged.
                            addToExtensionSchema(parentName, p);

scim/model/src/main/java/org/keycloak/scim/model/schema/SchemaResourceTypeProvider.java:90

  • This applies the leaf mapper's cardinality to every nested parent. For a multivalued mapping such as parent.type, the representation setter emits one parent object containing a list in type, while discovery now declares parent as an array (and the sub-attribute as scalar), so the advertised schema contradicts actual responses. Either reserve parent multivaluedness for the special .value shape or emit arrays of complex objects for every multivalued nested mapping.
                        p.setMultiValued(attribute.isMultivalued());
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Copilot AI review requested due to automatic review settings August 5, 2026 08:48
@cgeorgilakis
cgeorgilakis force-pushed the 50856_scim_multivalues branch from 6630bd6 to 9bff647 Compare August 5, 2026 08:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Filtered removal and non-value nested mappings currently produce incorrect behavior.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (2)

scim/model/src/main/java/org/keycloak/scim/model/user/UserExtensionModelSchema.java:221

  • A multivalued <parent>.value mapper is still built as a simple attribute (complexType == null). Consequently, a valid filtered PATCH such as remove <schema>:assurance[value eq "x"] makes ScimFilterToJsonNodeConverter return null, and this remover deletes the entire Keycloak attribute instead of only x; model this as a filterable complex value attribute (or otherwise preserve the filter value) and cover filtered removal.
    private Attribute<UserModel, User> createCustomAttribute(Object scimName, boolean isMultivalued) {
        Attribute.Builder<UserModel, User> builder = Attribute.<UserModel, User>simple(scimName.toString());
        if (isMultivalued) {
            builder = builder.multivalued()

scim/model/src/main/java/org/keycloak/scim/model/user/UserExtensionModelSchema.java:322

  • This converts collections to a multivalued complex shape only when the leaf is named value. A multivalued profile attribute mapped to another supported nested path (for example <schema>:roles.type) is emitted as {roles: {type: [...]}}, while SchemaResourceTypeProvider now declares roles as a multivalued complex attribute with a single-valued sub-attribute; either generalize the array-of-objects conversion to the mapped leaf or reject these mappings during validation.
                        // Handle multivalued attributes annotated as "<parent>.value"
                        // so that SCIM gets: "<parent>": [ { "value": "..." }, ... ]
                        if ("value".equals(attributeName) && value instanceof Collection<?> values) {
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Copilot AI review requested due to automatic review settings August 5, 2026 08:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Filtered PATCH removal or replacement can discard unmatched multivalued attribute values.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (1)

scim/model/src/main/java/org/keycloak/scim/model/user/UserExtensionModelSchema.java:225

  • Filtered PATCH operations on the new complex .value shape delete all values. These mappers still have complexType == null, so ScimFilterToJsonNodeConverter returns null for a path such as assurance[value eq "x"]; this branch then removes the entire model attribute, and filtered remove/replace loses every unmatched value. Make this mapping filterable as a complex value (or extend filter conversion) before using the selective remover.
                    .withModelRemover((model, name, values) -> {
                        if (values == null || values.isEmpty()) {
                            model.removeAttribute(name);
                            return;
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

@cgeorgilakis
cgeorgilakis requested a review from sguilhen August 5, 2026 08:58
Closes keycloak#50856

Signed-off-by: cgeorgilakis-grnet <cgeorgilakis@admin.grnet.gr>
@cgeorgilakis
cgeorgilakis force-pushed the 50856_scim_multivalues branch from 9bff647 to 87fff81 Compare August 5, 2026 13:01

@keycloak-github-bot keycloak-github-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unreported flaky test detected, please review

@keycloak-github-bot

Copy link
Copy Markdown

Unreported flaky test detected

If the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR.

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#employeeSigPostNoIdpKeyTestNoKeyNameInKeyInfo

Keycloak CI - Adapter IT Strict Cookies

org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
...

Report flaky test

@pedroigor pedroigor left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @cgeorgilakis.

Created #51525 as a follow-up.

@pedroigor

Copy link
Copy Markdown
Contributor

@cgeorgilakis Can you please rebase and force-push?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support multivalued user attributes in SCIM

4 participants