Skip to content

Support multivalued user attributes in SCIM - #50865

Open
cgeorgilakis wants to merge 2 commits into
keycloak:mainfrom
eosc-kc:50856_scim_multivalues
Open

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

Conversation

@cgeorgilakis

Copy link
Copy Markdown
Contributor

Closes #50856

Copilot AI 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.

Comment on lines +84 to +87
AttributeMetadata metadata = attributes.getMetadata(name);
//support multivalued user attribute to scim as array
if (metadata != null && metadata.isMultivalued()) {
return attributes.get(name);
Comment on lines +210 to +214
if ("value".equals(attributeName) && value instanceof Collection<?> values) {
subAttributes.put(parentAttributeName, values.stream()
.filter(Objects::nonNull)
.map(v -> Map.<String, Object>of("value", v))
.toList());
Comment on lines +208 to +214
// Handle multivalued attributes annotated as "<parent>.value"
// so that SCIM gets: "<parent>": [ { "value": "..." }, ... ]
if ("value".equals(attributeName) && value instanceof Collection<?> values) {
subAttributes.put(parentAttributeName, values.stream()
.filter(Objects::nonNull)
.map(v -> Map.<String, Object>of("value", v))
.toList());
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.

Comment on lines +139 to +140
if (isMultivalued) {
builder = builder.multivalued();
Comment on lines 90 to +94
Attribute parent = topLevelAttributes.computeIfAbsent(topName, k -> {
Attribute p = new Attribute();
p.setName(k);
p.setType("complex");
p.setMultiValued(false);
p.setMultiValued(attribute.isMultivalued());
Comment on lines +219 to +220
if (existingParent instanceof Map<?, ?> existingMap && !existingMap.isEmpty()) {
return;
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.

Comment on lines 32 to 34
import org.keycloak.scim.resource.Scim;
import org.keycloak.scim.resource.schema.Schema;
import org.keycloak.scim.resource.user.Email;
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()
Comment on lines +223 to +225
if (values == null || values.isEmpty()) {
model.removeAttribute(name);
return;
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.

Comment on lines +223 to +225
if (values == null || values.isEmpty()) {
model.removeAttribute(name);
return;
Comment on lines +320 to +322
// Handle multivalued attributes annotated as "<parent>.value"
// so that SCIM gets: "<parent>": [ { "value": "..." }, ... ]
if ("value".equals(attributeName) && value instanceof Collection<?> values) {
Copilot AI review requested due to automatic review settings July 15, 2026 13:35
Closes keycloak#50856

Signed-off-by: cgeorgilakis-grnet <cgeorgilakis@admin.grnet.gr>

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.

Comment on lines 87 to 89
if (value.isArray()) {
values = value.valueStream().map(JsonNode::asText).collect(Collectors.toSet());
values = value.valueStream().map(AttributeMapper::asMultivaluedString).collect(Collectors.toSet());
} else {
* return a single value.
*/
protected boolean shouldReturnMultivaluedValues(AttributeMetadata metadata, Attribute<UserModel, User> scimAttribute) {
return metadata != null && scimAttribute != null && metadata.isMultivalued() && (scimAttribute.isMultivalued() || ( isInternal() && scimAttribute.isExtension()));

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.

The isInternal() && scimAttribute.isExtension() fallback here appears unreachable today: for the extension schema (where isInternal() is true), createCustomAttribute always mirrors metadata.isMultivalued() onto the SCIM attribute, so scimAttribute.isMultivalued() already covers it. For core/enterprise schemas, isInternal() is false so the right side never fires. So the entire right side of the or seems to be dead code, no?

More importantly, if we later publish custom schemas via /Schemas (flipping isInternal() to false), this fallback would stop working as it guards on isInternal() == true, which is the opposite of what a published schema would have.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I removed it.

}

@Test
public void testGetMultivaluedCustomAttributesWithAndWithoutValueSubAttribute() {

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.

Wouldn't it be good to have tests covering PUT, PATCH, and maybe scenarios exercising the validateExtensionAttributeMappings path?

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

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

3 participants