mirror of
https://github.com/keycloak/keycloak.git
synced 2026-05-26 13:50:48 +00:00
Support for custom user scim extension
Closes #48345 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
@@ -1,19 +1,24 @@
|
||||
package org.keycloak.scim.resource;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.scim.resource.common.Meta;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAnyGetter;
|
||||
import com.fasterxml.jackson.annotation.JsonAnySetter;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import static org.keycloak.scim.resource.Scim.getCoreSchema;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@JsonInclude(Include.NON_NULL)
|
||||
@JsonIgnoreProperties({"type"})
|
||||
public abstract class ResourceTypeRepresentation {
|
||||
|
||||
@@ -34,6 +39,8 @@ public abstract class ResourceTypeRepresentation {
|
||||
@JsonIgnore
|
||||
private Long lastModifiedTimestamp;
|
||||
|
||||
private Map<String, Object> extensions;
|
||||
|
||||
public Set<String> getSchemas() {
|
||||
if (schemas == null) {
|
||||
schemas = new HashSet<>();
|
||||
@@ -96,4 +103,21 @@ public abstract class ResourceTypeRepresentation {
|
||||
}
|
||||
schemas.add(schema);
|
||||
}
|
||||
|
||||
@JsonAnyGetter
|
||||
public Map<String, Object> getExtensions() {
|
||||
return extensions;
|
||||
}
|
||||
|
||||
@JsonAnySetter
|
||||
public void setExtensions(String name, Object value) {
|
||||
if (extensions == null) {
|
||||
extensions = new HashMap<>();
|
||||
}
|
||||
this.extensions.put(name, value);
|
||||
}
|
||||
|
||||
public void setExtensions(Map<String, Object> extensions) {
|
||||
this.extensions = extensions;
|
||||
}
|
||||
}
|
||||
|
||||
+90
-78
@@ -25,6 +25,8 @@ import com.fasterxml.jackson.databind.node.NullNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.fasterxml.jackson.databind.node.TextNode;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
|
||||
import static org.keycloak.scim.resource.schema.AbstractModelSchema.Operation.ADD;
|
||||
import static org.keycloak.scim.resource.schema.AbstractModelSchema.Operation.REMOVE;
|
||||
import static org.keycloak.scim.resource.schema.AbstractModelSchema.Operation.SET;
|
||||
@@ -52,15 +54,14 @@ public abstract class AbstractModelSchema<M extends Model, R extends ResourceTyp
|
||||
@Override
|
||||
public Map<String, Attribute<M, R>> getAttributes() {
|
||||
if (attributes == null) {
|
||||
attributes = doGetAttributes();
|
||||
attributes = getAttributeMappers();
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
|
||||
protected abstract Map<String, Attribute<M,R>> doGetAttributes();
|
||||
|
||||
@Override
|
||||
public void populate(M model, R representation) {
|
||||
validate(representation);
|
||||
populateModel(model, representation);
|
||||
representation.setId(model.getId());
|
||||
}
|
||||
@@ -78,7 +79,7 @@ public abstract class AbstractModelSchema<M extends Model, R extends ResourceTyp
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(R representation) throws SchemaValidationException {
|
||||
public void validate(R representation) throws ModelValidationException {
|
||||
// validate here the schema
|
||||
}
|
||||
|
||||
@@ -125,7 +126,8 @@ public abstract class AbstractModelSchema<M extends Model, R extends ResourceTyp
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the names of the attributes from the given {@code model}.
|
||||
* Returns the names of the attributes from the given {@code model}, if provided. The {@code model} provides
|
||||
* additional context when resolving model attributes, in case attributes depend on the current model being processed.
|
||||
*
|
||||
* @return the names of the attributes defined in the model
|
||||
*/
|
||||
@@ -149,83 +151,70 @@ public abstract class AbstractModelSchema<M extends Model, R extends ResourceTyp
|
||||
protected abstract String getAttributeSchemaName(String name);
|
||||
|
||||
private void populateModel(M model, R resource) {
|
||||
ObjectNode objectNode;
|
||||
ObjectNode resourceNode;
|
||||
|
||||
try {
|
||||
objectNode = JsonSerialization.createObjectNode(resource);
|
||||
resourceNode = JsonSerialization.createObjectNode(resource);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to convert representation to JSON", e);
|
||||
}
|
||||
|
||||
for (String name : getModelAttributeNames()) {
|
||||
Attribute<M, R> attribute = getAttributeMapperByModelAttribute(name);
|
||||
for (Entry<String, Attribute<M, R>> entry : getAttributeMappers().entrySet()) {
|
||||
Attribute<M, R> attribute = entry.getValue();
|
||||
String scimName = attribute.getName();
|
||||
ObjectNode valueNode = resourceNode;
|
||||
|
||||
if (attribute == null) {
|
||||
continue;
|
||||
}
|
||||
if (attribute.isExtension()) {
|
||||
JsonNode node = ofNullable(resourceNode.get(attribute.getSchema())).orElse(NullNode.getInstance());
|
||||
|
||||
Object value = getJsonValue(objectNode, attribute.getName());
|
||||
|
||||
if (value == null) {
|
||||
String attributeName = attribute.getName();
|
||||
|
||||
if (attributeName.indexOf('.') > 0) {
|
||||
attributeName = attributeName.substring(attributeName.indexOf('.') + 1);
|
||||
List<String> paths = new ArrayList<>();
|
||||
paths.add(getId());
|
||||
paths.addAll(List.of(attributeName.split("\\.")));
|
||||
value = getJsonValue(objectNode, paths);
|
||||
if (!node.isObject()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
valueNode = (ObjectNode) node;
|
||||
scimName = attribute.getSimpleName();
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
JsonNode schemaExtension = objectNode.get(getId());
|
||||
value = getJsonValue(schemaExtension, attribute.getName());
|
||||
}
|
||||
Object value = getJsonValue(valueNode, scimName);
|
||||
|
||||
if (value != null) {
|
||||
if (value instanceof Collection<?> values) {
|
||||
if (attribute.isMultivalued()) {
|
||||
ArrayNode nodes = JsonNodeFactory.instance.arrayNode();
|
||||
|
||||
for (Object v : values) {
|
||||
if (v instanceof JsonNode jsonNode) {
|
||||
nodes.add(jsonNode);
|
||||
} else {
|
||||
nodes.add(TextNode.valueOf(v.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
setValue(model, attribute, nodes);
|
||||
} else {
|
||||
for (Object v : values) {
|
||||
if (v instanceof JsonNode jsonNode) {
|
||||
setValue(model, attribute, resolveAttributeValue(attribute, jsonNode));
|
||||
}
|
||||
// no support for multivalued attributes for now, so we take the first value as the value of the attribute
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
attribute.set(model, TextNode.valueOf(value.toString()));
|
||||
}
|
||||
setValue(model, attribute, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void populateResourceType(R resource, M model, List<String> requestedAttributes, List<String> excludedAttributes) {
|
||||
for (String name : getModelAttributeNames()) {
|
||||
Attribute<M, R> attribute = getAttributeMapperByModelAttribute(name);
|
||||
for (Entry<String, Attribute<M, R>> entry : getAttributeMappers().entrySet()) {
|
||||
Attribute<M, R> attribute = entry.getValue();
|
||||
|
||||
if (!attribute.isExcluded(this, requestedAttributes, excludedAttributes)) {
|
||||
String modelAttributeName = attribute.getModelAttributeName();
|
||||
Object value = getAttributeValue(model, modelAttributeName);
|
||||
|
||||
if (attribute != null && !attribute.isExcluded(this, requestedAttributes, excludedAttributes)) {
|
||||
Object value = getAttributeValue(model, name);
|
||||
attribute.set(resource, value);
|
||||
resource.addSchema(this.id);
|
||||
|
||||
if (!isInternal()) {
|
||||
resource.addSchema(this.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Attribute<M, R> getAttributeMapperByModelAttribute(String name) {
|
||||
protected Map<String, Attribute<M,R>> getAttributeMappers() {
|
||||
Map<String, Attribute<M,R>> mappers = new HashMap<>();
|
||||
|
||||
for (String name : getModelAttributeNames()) {
|
||||
Attribute<M, R> attribute = getAttributeMapperByModelAttribute(name);
|
||||
|
||||
if (attribute != null) {
|
||||
mappers.put(name, attribute);
|
||||
}
|
||||
}
|
||||
|
||||
return mappers;
|
||||
}
|
||||
|
||||
protected Attribute<M, R> getAttributeMapperByModelAttribute(String name) {
|
||||
String scimName = getAttributeSchemaName(name);
|
||||
|
||||
if (scimName == null) {
|
||||
@@ -323,27 +312,25 @@ public abstract class AbstractModelSchema<M extends Model, R extends ResourceTyp
|
||||
return false;
|
||||
}
|
||||
|
||||
return getPaths(attribute).stream().anyMatch(path::equalsIgnoreCase);
|
||||
return resolvePaths(attribute).stream().anyMatch(path::equalsIgnoreCase);
|
||||
}
|
||||
|
||||
private List<String> getPaths(Attribute<M, R> attr) {
|
||||
private List<String> resolvePaths(Attribute<M, R> attr) {
|
||||
List<String> paths = new ArrayList<>();
|
||||
|
||||
// the name of the attribute itself is always a valid path
|
||||
paths.add(attr.getName());
|
||||
|
||||
if (!isCore()) {
|
||||
// if processing an extension schema try to resolve the attribute based on parent name and alias as well
|
||||
String parent = attr.getParentName();
|
||||
if (attr.isExtension()) {
|
||||
paths.add(attr.getSchema() + ":" + attr.getSimpleName());
|
||||
}
|
||||
|
||||
if (parent != null) {
|
||||
paths.add(getId() + attr.getName().replace(parent + ".", ":"));
|
||||
paths.add(getId() + attr.getName().replace(parent, ""));
|
||||
}
|
||||
if (attr.getName().endsWith(".value")) {
|
||||
paths.add(attr.getSchema() + ":" + attr.getSimpleName().substring(0, attr.getSimpleName().length() - 6));
|
||||
}
|
||||
|
||||
if (attr.getAlias() != null) {
|
||||
paths.add(getId() + ":" + attr.getAlias());
|
||||
}
|
||||
if (attr.getAlias() != null) {
|
||||
paths.add(getId() + ":" + attr.getAlias());
|
||||
}
|
||||
|
||||
Class<?> complexType = attr.getComplexType();
|
||||
@@ -357,25 +344,50 @@ public abstract class AbstractModelSchema<M extends Model, R extends ResourceTyp
|
||||
return paths;
|
||||
}
|
||||
|
||||
public void setValue(M model, Attribute<M, R> resolved, JsonNode value) {
|
||||
setValue(model, resolved, value, SET);
|
||||
private void setValue(M model, Attribute<M, R> attribute, Object value) {
|
||||
setValue(model, attribute, value, SET);
|
||||
}
|
||||
|
||||
private void setValue(M model, Attribute<M, R> attribute, JsonNode value, Operation operation) {
|
||||
private void setValue(M model, Attribute<M, R> attribute, Object value, Operation operation) {
|
||||
Objects.requireNonNull(model, "model cannot be null");
|
||||
Objects.requireNonNull(attribute, "attribute cannot be null");
|
||||
Objects.requireNonNull(value, "value cannot be null");
|
||||
Objects.requireNonNull(operation, "operation cannot be null");
|
||||
|
||||
JsonNode jsonValue = toJsonNode(value);
|
||||
|
||||
switch (operation) {
|
||||
case SET -> attribute.set(model, value);
|
||||
case ADD -> attribute.add(model, value);
|
||||
case REMOVE -> attribute.remove(model, value);
|
||||
case SET -> attribute.set(model, jsonValue);
|
||||
case ADD -> attribute.add(model, jsonValue);
|
||||
case REMOVE -> attribute.remove(model, jsonValue);
|
||||
default -> throw new ModelException("Invalid operation: " + operation);
|
||||
}
|
||||
}
|
||||
|
||||
private JsonNode resolveAttributeValue(Attribute<M, R> attribute,JsonNode jsonNode) {
|
||||
private JsonNode toJsonNode(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value instanceof JsonNode) {
|
||||
return (JsonNode) value;
|
||||
} else if (value instanceof Collection<?> values) {
|
||||
ArrayNode nodes = JsonNodeFactory.instance.arrayNode();
|
||||
|
||||
for (Object v : values) {
|
||||
if (v instanceof JsonNode jsonNode) {
|
||||
nodes.add(jsonNode);
|
||||
} else {
|
||||
nodes.add(TextNode.valueOf(v.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
return TextNode.valueOf(value.toString());
|
||||
}
|
||||
|
||||
private JsonNode resolveAttributeValue(Attribute<M, R> attribute, JsonNode jsonNode) {
|
||||
if (jsonNode.isValueNode()) {
|
||||
Class<?> complexType = attribute.getComplexType();
|
||||
|
||||
|
||||
@@ -2,13 +2,17 @@ package org.keycloak.scim.resource.schema;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.models.Model;
|
||||
import org.keycloak.models.ModelValidationException;
|
||||
import org.keycloak.scim.resource.ResourceTypeRepresentation;
|
||||
import org.keycloak.scim.resource.schema.attribute.Attribute;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
|
||||
/**
|
||||
* <p>An interface that represents a schema for a resource type.
|
||||
*
|
||||
@@ -70,9 +74,9 @@ public interface ModelSchema<M extends Model, R extends ResourceTypeRepresentati
|
||||
* Validates the given {@code representation} against the schema. It should throw an exception if the representation is not valid.
|
||||
*
|
||||
* @param representation the representation to be validated
|
||||
* @throws SchemaValidationException if the representation is not valid against the schema
|
||||
* @throws ModelValidationException if the representation is not valid against the schema
|
||||
*/
|
||||
void validate(R representation) throws SchemaValidationException;
|
||||
void validate(R representation) throws ModelValidationException;
|
||||
|
||||
/**
|
||||
* Performs a PATCH {@code add} operation on the given {@code model} for the attribute defined by the given {@code path} and the given {@code value}.
|
||||
@@ -128,4 +132,23 @@ public interface ModelSchema<M extends Model, R extends ResourceTypeRepresentati
|
||||
* @return the attribute for the given path, or {@code null} if no attribute is defined for the given path
|
||||
*/
|
||||
Attribute<M, R> getAttributeByPath(String path);
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this schema is an internal schema, not exposed from the schema endpoint.
|
||||
*
|
||||
* @return {@code true} if this schema is an internal schema, {@code false} otherwise
|
||||
*/
|
||||
default boolean isInternal() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this schema supports any of the given {@code schemas}
|
||||
*
|
||||
* @param schemas the schemas
|
||||
* @return {@code true} if this schema supports any of the given {@code schemas}. Otherwise, {@code false}
|
||||
*/
|
||||
default boolean supports(Set<String> schemas) {
|
||||
return isCore() || ofNullable(schemas).orElse(Set.of()).contains(getId());
|
||||
}
|
||||
}
|
||||
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
package org.keycloak.scim.resource.schema;
|
||||
|
||||
import org.keycloak.scim.resource.ResourceTypeRepresentation;
|
||||
|
||||
/**
|
||||
* Thrown to indicate errors when validating a {@link ResourceTypeRepresentation} against a {@link ModelSchema}.
|
||||
*/
|
||||
public class SchemaValidationException extends RuntimeException {
|
||||
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import org.keycloak.scim.resource.schema.ModelSchema;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static java.util.Optional.ofNullable;
|
||||
|
||||
/**
|
||||
@@ -29,6 +30,39 @@ public class Attribute<M extends Model, R extends ResourceTypeRepresentation> {
|
||||
public static final String RETURNED_REQUEST = "request";
|
||||
public static final String RETURNED_NEVER = "never";
|
||||
|
||||
public static String getSchema(String name) {
|
||||
requireNonNull(name, "name is required");
|
||||
int schemaSeparator = name.lastIndexOf(':');
|
||||
|
||||
if (schemaSeparator == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return name.substring(0, schemaSeparator) + ":" + getResourceType(name);
|
||||
}
|
||||
|
||||
public static String getResourceType(String name) {
|
||||
requireNonNull(name, "name is required");
|
||||
int schemaSeparator = name.lastIndexOf(':');
|
||||
|
||||
if (schemaSeparator == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String resourceType = name.substring(name.substring(0, schemaSeparator).length() + 1);
|
||||
return resourceType.substring(0, resourceType.indexOf('.'));
|
||||
}
|
||||
|
||||
public static String getSimpleName(String name) {
|
||||
String schema = getSchema(name);
|
||||
|
||||
if (schema == null) {
|
||||
return name;
|
||||
}
|
||||
|
||||
return name.substring(schema.length() + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a simple attribute with the given {@code name}.
|
||||
*
|
||||
@@ -186,6 +220,27 @@ public class Attribute<M extends Model, R extends ResourceTypeRepresentation> {
|
||||
return uniqueness;
|
||||
}
|
||||
|
||||
public String getSchema() {
|
||||
if (!isExtension()) {
|
||||
return null;
|
||||
}
|
||||
return getSchema(getName());
|
||||
}
|
||||
|
||||
public String getResourceType() {
|
||||
if (!isExtension()) {
|
||||
return null;
|
||||
}
|
||||
return getResourceType(getName());
|
||||
}
|
||||
|
||||
public String getSimpleName() {
|
||||
if (!isExtension()) {
|
||||
return null;
|
||||
}
|
||||
return getSimpleName(getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof Attribute<?, ?> attribute)) return false;
|
||||
@@ -264,12 +319,16 @@ public class Attribute<M extends Model, R extends ResourceTypeRepresentation> {
|
||||
}).anyMatch(this::equals);
|
||||
}
|
||||
|
||||
public boolean isExtension() {
|
||||
return getName().contains(":");
|
||||
}
|
||||
|
||||
public static class Builder<M extends Model, R extends ResourceTypeRepresentation> {
|
||||
|
||||
private final Class<?> complexType;
|
||||
private final String name;
|
||||
private TriConsumer<M, String, ?> modelSetter;
|
||||
private BiConsumer<R, ?> representationSetter;
|
||||
private TriConsumer<Attribute<M, R>, R, ?> representationSetter;
|
||||
List<Attribute<M, R>> attributes = new ArrayList<>();
|
||||
// by default, resolve model attribute name as the same as the scim attribute name
|
||||
private Function<Attribute<M, R>, String> modelAttributeResolver = Attribute::getName;
|
||||
@@ -284,14 +343,20 @@ public class Attribute<M extends Model, R extends ResourceTypeRepresentation> {
|
||||
private String uniqueness = "none";
|
||||
|
||||
private Builder(String name, Class<?> complexType) {
|
||||
Objects.requireNonNull(name, "name cannot be null");
|
||||
requireNonNull(name, "name cannot be null");
|
||||
this.complexType = complexType;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public <C extends BiConsumer<R, ?>> Builder<M, R> withModelSetter(TriConsumer<M, String, ?> modelSetter, C representationSetter) {
|
||||
public <V, C extends BiConsumer<R, V>> Builder<M, R> withModelSetter(TriConsumer<M, String, ?> modelSetter, C repSetter) {
|
||||
this.modelSetter = modelSetter;
|
||||
this.representationSetter = representationSetter;
|
||||
this.representationSetter = (TriConsumer<Attribute<M, R>, R, Object>) (attribute, r, o) -> repSetter.accept(r, (V) o);
|
||||
return this;
|
||||
}
|
||||
|
||||
public <V, C extends TriConsumer<Attribute<M, R>, R, V>> Builder<M, R> withSetters(TriConsumer<M, String, ?> modelSetter, C repSetter) {
|
||||
this.modelSetter = modelSetter;
|
||||
this.representationSetter = repSetter;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
+4
-5
@@ -3,7 +3,6 @@ package org.keycloak.scim.resource.schema.attribute;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.common.util.TriConsumer;
|
||||
@@ -24,13 +23,13 @@ public class AttributeMapper<M extends Model, R extends ResourceTypeRepresentati
|
||||
private final TriConsumer<M, String, ?> modelSetter;
|
||||
private TriConsumer<M, String, ?> modelRemover;
|
||||
private TriConsumer<M, String, ?> modelAdder;
|
||||
private final BiConsumer<R, ?> representationSetter;
|
||||
private final TriConsumer<Attribute<M, R>, R, ?> representationSetter;
|
||||
|
||||
AttributeMapper(TriConsumer<M, String, ?> modelSetter, BiConsumer<R, ?> representationSetter) {
|
||||
AttributeMapper(TriConsumer<M, String, ?> modelSetter, TriConsumer<Attribute<M, R>, R, ?> representationSetter) {
|
||||
this(modelSetter, representationSetter, null, null);
|
||||
}
|
||||
|
||||
AttributeMapper(TriConsumer<M, String, ?> modelSetter, BiConsumer<R, ?> representationSetter, TriConsumer<M, String, ?> modelRemover, TriConsumer<M, String, ?> modelAdder) {
|
||||
AttributeMapper(TriConsumer<M, String, ?> modelSetter, TriConsumer<Attribute<M, R>, R, ?> representationSetter, TriConsumer<M, String, ?> modelRemover, TriConsumer<M, String, ?> modelAdder) {
|
||||
this.modelSetter = modelSetter;
|
||||
this.representationSetter = representationSetter;
|
||||
this.modelRemover = modelRemover;
|
||||
@@ -39,7 +38,7 @@ public class AttributeMapper<M extends Model, R extends ResourceTypeRepresentati
|
||||
|
||||
public void setValue(R representation, Object value) {
|
||||
if (representationSetter != null) {
|
||||
((BiConsumer<R, Object>) representationSetter).accept(representation, value);
|
||||
((TriConsumer<Attribute<M, R>, R, Object>) representationSetter).accept(attribute, representation, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-3
@@ -5,12 +5,13 @@ import java.lang.reflect.ParameterizedType;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.keycloak.common.util.TriConsumer;
|
||||
import org.keycloak.models.Model;
|
||||
import org.keycloak.scim.resource.ResourceTypeRepresentation;
|
||||
import org.keycloak.scim.resource.common.MultiValuedAttribute;
|
||||
|
||||
public class ComplexAttributeSetter<R extends ResourceTypeRepresentation> implements BiConsumer<R, String> {
|
||||
public class ComplexAttributeSetter<M extends Model, R extends ResourceTypeRepresentation> implements TriConsumer<Attribute<M, R>, R, String> {
|
||||
|
||||
private final String name;
|
||||
private final String subName;
|
||||
@@ -28,7 +29,7 @@ public class ComplexAttributeSetter<R extends ResourceTypeRepresentation> implem
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(R representation, String newValue) {
|
||||
public void accept(Attribute<M, R> attribute, R representation, String newValue) {
|
||||
try {
|
||||
Method declaredMethod = representation.getClass().getMethod("get" + Character.toUpperCase(name.charAt(0)) + name.substring(1));
|
||||
Class<?> returnType = declaredMethod.getReturnType();
|
||||
|
||||
+4
-2
@@ -17,6 +17,8 @@ import org.keycloak.scim.resource.schema.ModelSchema;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import static java.util.function.Predicate.not;
|
||||
|
||||
import static org.keycloak.utils.StringUtil.isBlank;
|
||||
|
||||
public abstract class AbstractScimResourceTypeProvider<M extends Model, R extends ResourceTypeRepresentation> implements ScimResourceTypeProvider<R> {
|
||||
@@ -155,7 +157,7 @@ public abstract class AbstractScimResourceTypeProvider<M extends Model, R extend
|
||||
|
||||
@Override
|
||||
public List<String> getSchemaExtensions() {
|
||||
return schemaExtensions.stream().map(ModelSchema::getId).toList();
|
||||
return schemaExtensions.stream().filter(not(ModelSchema::isInternal)).map(ModelSchema::getId).toList();
|
||||
}
|
||||
|
||||
protected abstract R onCreate(R resource);
|
||||
@@ -172,7 +174,7 @@ public abstract class AbstractScimResourceTypeProvider<M extends Model, R extend
|
||||
|
||||
protected void populate(M model, R resource) {
|
||||
for (ModelSchema<M, R> schema : schemas) {
|
||||
if (resource.hasSchema(schema.getId())) {
|
||||
if (schema.supports(resource.getSchemas())) {
|
||||
schema.populate(model, resource);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ import org.keycloak.scim.resource.schema.AbstractModelSchema;
|
||||
import org.keycloak.scim.resource.schema.attribute.Attribute;
|
||||
import org.keycloak.utils.KeycloakSessionUtil;
|
||||
|
||||
import static org.keycloak.utils.StringUtil.isBlank;
|
||||
|
||||
public final class GroupCoreModelSchema extends AbstractModelSchema<GroupModel, Group> {
|
||||
|
||||
public GroupCoreModelSchema() {
|
||||
@@ -76,7 +78,7 @@ public final class GroupCoreModelSchema extends AbstractModelSchema<GroupModel,
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Attribute<GroupModel, Group>> doGetAttributes() {
|
||||
protected Map<String, Attribute<GroupModel, Group>> getAttributeMappers() {
|
||||
List<Attribute<GroupModel, Group>> attributes = new ArrayList<>(Attribute.<GroupModel, Group>simple("displayName")
|
||||
.notCaseExact()
|
||||
.modelAttributeResolver((attribute) -> {
|
||||
@@ -163,6 +165,13 @@ public final class GroupCoreModelSchema extends AbstractModelSchema<GroupModel,
|
||||
setTimestamps(resource, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(Group representation) throws ModelValidationException {
|
||||
if (isBlank(representation.getDisplayName())) {
|
||||
throw new ModelValidationException("Display name is required");
|
||||
}
|
||||
}
|
||||
|
||||
private void setTimestamps(Group resource, GroupModel model) {
|
||||
Long createdTimestamp = model.getCreatedTimestamp();
|
||||
if (createdTimestamp != null) {
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ public class GroupResourceTypeProvider extends AbstractScimResourceTypeProvider<
|
||||
List<Member> members = resource.getMembers();
|
||||
|
||||
if (!Optional.ofNullable(members).orElse(List.of()).isEmpty()) {
|
||||
throw new ModelValidationException("Managing members on updates are not supported");
|
||||
throw new ModelValidationException("Managing members on updates is not supported");
|
||||
}
|
||||
|
||||
return super.update(resource);
|
||||
|
||||
+50
-27
@@ -22,6 +22,7 @@ import org.keycloak.scim.resource.schema.Schema;
|
||||
import org.keycloak.scim.resource.schema.Schema.Attribute;
|
||||
import org.keycloak.scim.resource.spi.ScimResourceTypeProvider;
|
||||
|
||||
|
||||
/**
|
||||
* Provider for SCIM Schema resources. This provider exposes the supported SCIM schemas
|
||||
* for discovery by SCIM clients via the /Schemas endpoint.
|
||||
@@ -50,8 +51,7 @@ public class SchemaResourceTypeProvider implements ScimResourceTypeProvider<Sche
|
||||
|| providerFactory instanceof ServiceProviderConfigResourceTypeProvider)
|
||||
).flatMap((Function<ProviderFactory, Stream<ModelSchema>>) factory -> {
|
||||
ScimResourceTypeProvider provider = session.getProvider(ScimResourceTypeProvider.class, factory.getId());
|
||||
List<ModelSchema> modelSchemas = provider.getSchemas();
|
||||
return modelSchemas.stream();
|
||||
return provider.getSchemas().stream();
|
||||
}).forEach(this::buildSchema);
|
||||
}
|
||||
|
||||
@@ -73,6 +73,11 @@ public class SchemaResourceTypeProvider implements ScimResourceTypeProvider<Sche
|
||||
|
||||
String parentName = attribute.getParentName();
|
||||
|
||||
if (!modelSchema.isCore()) {
|
||||
// extensions attributes should be set in a top-level attribute with the schema name as the name
|
||||
parentName = attribute.getSchema();
|
||||
}
|
||||
|
||||
if (parentName != null && !parentName.equals(name)) {
|
||||
// This is a sub-attribute — strip the parent prefix to get the relative path
|
||||
String relativeName = name.substring(parentName.length() + 1);
|
||||
@@ -140,38 +145,56 @@ public class SchemaResourceTypeProvider implements ScimResourceTypeProvider<Sche
|
||||
subAttributes.add(subAttr);
|
||||
} else {
|
||||
// Extension schema simple sub-attribute (e.g., "enterpriseUser.employeeNumber" → "employeeNumber")
|
||||
topLevelAttributes.computeIfAbsent(relativeName, k -> {
|
||||
Attribute attr = new Attribute();
|
||||
attr.setName(k);
|
||||
attr.setType(attribute.getType());
|
||||
attr.setMultiValued(attribute.isMultivalued());
|
||||
attr.setReturned(attribute.getReturned());
|
||||
attr.setMutability(attribute.isImmutable() ? "immutable" : "readWrite");
|
||||
attr.setRequired(attribute.isRequired());
|
||||
attr.setCaseExact(attribute.isCaseExact());
|
||||
attr.setUniqueness(attribute.getUniqueness());
|
||||
return attr;
|
||||
});
|
||||
topLevelAttributes.computeIfAbsent(relativeName, createExtensionAttribute(modelSchema, parentName, attribute));
|
||||
}
|
||||
} else {
|
||||
// Top-level attribute — only add if not already created as a parent
|
||||
topLevelAttributes.computeIfAbsent(name, k -> {
|
||||
Attribute attr = new Attribute();
|
||||
attr.setName(k);
|
||||
attr.setType(attribute.getType());
|
||||
attr.setMultiValued(attribute.isMultivalued());
|
||||
attr.setReturned(attribute.getReturned());
|
||||
attr.setMutability(attribute.isImmutable() ? "immutable" : "readWrite");
|
||||
attr.setRequired(attribute.isRequired());
|
||||
attr.setCaseExact(attribute.isCaseExact());
|
||||
attr.setUniqueness(attribute.getUniqueness());
|
||||
return attr;
|
||||
});
|
||||
topLevelAttributes.computeIfAbsent(name, k -> createTopLevelAttribute(attribute, k));
|
||||
}
|
||||
}
|
||||
|
||||
rep.setAttributes(List.copyOf(topLevelAttributes.values()));
|
||||
schemas.put(modelSchema.getId(), rep);
|
||||
|
||||
if (!modelSchema.isInternal()) {
|
||||
schemas.put(modelSchema.getId(), rep);
|
||||
}
|
||||
}
|
||||
|
||||
private Function<String, Attribute> createExtensionAttribute(ModelSchema<?, ?> modelSchema, String schemaName, org.keycloak.scim.resource.schema.attribute.Attribute<?, ?> attribute) {
|
||||
return k -> {
|
||||
Attribute attr = createTopLevelAttribute(attribute, k);
|
||||
|
||||
if (modelSchema.isCore()) {
|
||||
return attr;
|
||||
}
|
||||
|
||||
schemas.computeIfAbsent(schemaName, n -> {
|
||||
Schema schema = new Schema();
|
||||
|
||||
schema.setName(n);
|
||||
schema.setId(n);
|
||||
schema.setAttributes(new ArrayList<>());
|
||||
|
||||
return schema;
|
||||
}).getAttributes().add(attr);
|
||||
|
||||
return attr;
|
||||
};
|
||||
}
|
||||
|
||||
private Attribute createTopLevelAttribute(org.keycloak.scim.resource.schema.attribute.Attribute<?, ?> attribute, String name) {
|
||||
Attribute attr = new Attribute();
|
||||
|
||||
attr.setName(name);
|
||||
attr.setType(attribute.getType());
|
||||
attr.setMultiValued(attribute.isMultivalued());
|
||||
attr.setReturned(attribute.getReturned());
|
||||
attr.setMutability(attribute.isImmutable() ? "immutable" : "readWrite");
|
||||
attr.setRequired(attribute.isRequired());
|
||||
attr.setCaseExact(attribute.isCaseExact());
|
||||
attr.setUniqueness(attribute.getUniqueness());
|
||||
|
||||
return attr;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ import org.keycloak.userprofile.UserProfileProvider;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
|
||||
import static org.keycloak.scim.resource.schema.attribute.Attribute.getSchema;
|
||||
|
||||
public abstract class AbstractUserModelSchema extends AbstractModelSchema<UserModel ,User> {
|
||||
|
||||
public static final String ANNOTATION_SCIM_SCHEMA_ATTRIBUTE = "kc.scim.schema.attribute";
|
||||
@@ -30,11 +32,13 @@ public abstract class AbstractUserModelSchema extends AbstractModelSchema<UserMo
|
||||
|
||||
@Override
|
||||
protected Set<String> getModelAttributeNames() {
|
||||
UserProfile profile = session.getProvider(UserProfileProvider.class).create(UserProfileContext.SCIM, Map.of());
|
||||
UserProfile profile = getUserProfile();
|
||||
Attributes attributes = profile.getAttributes();
|
||||
Set<String> names = new HashSet<>(attributes.nameSet());
|
||||
|
||||
names.add(UserModel.ENABLED);
|
||||
names.add("groups");
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
@@ -95,4 +99,12 @@ public abstract class AbstractUserModelSchema extends AbstractModelSchema<UserMo
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected boolean hasSchema(String attributeName) {
|
||||
return getId().equals(getSchema(attributeName));
|
||||
}
|
||||
|
||||
protected UserProfile getUserProfile() {
|
||||
return session.getProvider(UserProfileProvider.class).create(UserProfileContext.SCIM, Map.of());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,14 @@ public final class UserCoreModelSchema extends AbstractUserModelSchema {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Attribute<UserModel, User>> doGetAttributes() {
|
||||
protected boolean hasSchema(String attributeName) {
|
||||
String schema = Attribute.getSchema(attributeName);
|
||||
|
||||
return schema == null || getId().equals(schema);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Attribute<UserModel, User>> getAttributeMappers() {
|
||||
List<Attribute<UserModel, User>> attributes = new ArrayList<>();
|
||||
|
||||
attributes.addAll(Attribute.<UserModel, User>simple("userName")
|
||||
@@ -131,7 +138,12 @@ public final class UserCoreModelSchema extends AbstractUserModelSchema {
|
||||
.bool()
|
||||
.withModelSetter(
|
||||
(model, name, value) -> model.setEnabled(Boolean.parseBoolean(Optional.ofNullable(value).orElse("").toString()))
|
||||
, (user, value) -> user.setActive(Boolean.parseBoolean(value.toString()))
|
||||
, (user, value) -> {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
user.setActive(Boolean.parseBoolean(value.toString()));
|
||||
}
|
||||
)
|
||||
.build());
|
||||
attributes.addAll(Attribute.<UserModel, User>simple("meta.created")
|
||||
|
||||
+4
-20
@@ -1,19 +1,12 @@
|
||||
package org.keycloak.scim.model.user;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.scim.resource.schema.attribute.Attribute;
|
||||
import org.keycloak.scim.resource.user.EnterpriseUser;
|
||||
import org.keycloak.scim.resource.user.User;
|
||||
|
||||
import static org.keycloak.scim.resource.Scim.ENTERPRISE_USER_SCHEMA;
|
||||
|
||||
public final class UserEnterpriseModelSchema extends AbstractUserModelSchema {
|
||||
public final class UserEnterpriseModelSchema extends UserExtensionModelSchema {
|
||||
|
||||
public UserEnterpriseModelSchema(KeycloakSession session) {
|
||||
super(session, ENTERPRISE_USER_SCHEMA);
|
||||
@@ -35,21 +28,12 @@ public final class UserEnterpriseModelSchema extends AbstractUserModelSchema {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCore() {
|
||||
public boolean isInternal() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Attribute<UserModel, User>> doGetAttributes() {
|
||||
return new ArrayList<>(Attribute.<UserModel, User>complex("enterpriseUser", EnterpriseUser.class)
|
||||
.modelAttributeResolver(this::createModelAttributeResolver)
|
||||
.withAttribute("employeeNumber", UserModel::setSingleAttribute)
|
||||
.withAttribute("costCenter", UserModel::setSingleAttribute)
|
||||
.withAttribute("organization", UserModel::setSingleAttribute)
|
||||
.withAttribute("division", UserModel::setSingleAttribute)
|
||||
.withAttribute("department", UserModel::setSingleAttribute)
|
||||
.withAttribute("manager.value", "manager", UserModel::setSingleAttribute)
|
||||
.withAttribute("manager.displayName", UserModel::setSingleAttribute)
|
||||
.build()).stream().collect(Collectors.toMap(Attribute::getName, Function.identity()));
|
||||
protected boolean hasSchema(String attributeName) {
|
||||
return getId().equals(Attribute.getSchema(attributeName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
package org.keycloak.scim.model.user;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.scim.resource.schema.attribute.Attribute;
|
||||
import org.keycloak.scim.resource.user.User;
|
||||
import org.keycloak.userprofile.AttributeMetadata;
|
||||
import org.keycloak.userprofile.Attributes;
|
||||
import org.keycloak.userprofile.UserProfile;
|
||||
|
||||
import static org.keycloak.scim.resource.Scim.ENTERPRISE_USER_SCHEMA;
|
||||
import static org.keycloak.scim.resource.Scim.USER_CORE_SCHEMA;
|
||||
import static org.keycloak.userprofile.UserProfileUtil.isRootAttribute;
|
||||
|
||||
public class UserExtensionModelSchema extends AbstractUserModelSchema {
|
||||
|
||||
public static final String KEYCLOAK_USER_SCHEMA = "urn:keycloak:params:scim:schemas:extension:realm:1.0:User";
|
||||
|
||||
private final KeycloakSession session;
|
||||
|
||||
public UserExtensionModelSchema(KeycloakSession session) {
|
||||
this(session, KEYCLOAK_USER_SCHEMA);
|
||||
}
|
||||
|
||||
public UserExtensionModelSchema(KeycloakSession session, String schema) {
|
||||
super(session, schema);
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return KEYCLOAK_USER_SCHEMA;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "RealmUser";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Realm User";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCore() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInternal() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Set<String> schemas) {
|
||||
for (Attribute<UserModel, User> value : getAttributes().values()) {
|
||||
String schema = value.getSchema();
|
||||
|
||||
if (schema != null && schemas.contains(schema)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Set<String> getModelAttributeNames() {
|
||||
if (isCore()) {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
Set<String> names = new HashSet<>();
|
||||
UserProfile profile = getUserProfile();
|
||||
|
||||
for (String name : profile.getAttributes().nameSet()) {
|
||||
if (isRootAttribute(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AttributeMetadata metadata = profile.getAttributes().getMetadata(name);
|
||||
|
||||
if (metadata == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String scimName = (String) metadata.getAnnotations().get(ANNOTATION_SCIM_SCHEMA_ATTRIBUTE);
|
||||
|
||||
if (scimName == null || !scimName.contains(":")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
names.add(name);
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Attribute<UserModel, User> getAttributeMapperByModelAttribute(String name) {
|
||||
UserProfile profile = getUserProfile();
|
||||
Attributes attributes = profile.getAttributes();
|
||||
AttributeMetadata metadata = attributes.getMetadata(name);
|
||||
|
||||
if (metadata == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, Object> annotations = metadata.getAnnotations();
|
||||
|
||||
if (annotations == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String scimName = (String) annotations.get(ANNOTATION_SCIM_SCHEMA_ATTRIBUTE);
|
||||
|
||||
if (scimName == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!hasSchema(scimName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createCustomAttribute(scimName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean hasSchema(String attributeName) {
|
||||
String schema = Attribute.getSchema(attributeName);
|
||||
|
||||
// it should be possible to query other schemas from the providers
|
||||
return schema != null && !List.of(USER_CORE_SCHEMA, ENTERPRISE_USER_SCHEMA).contains(schema);
|
||||
}
|
||||
|
||||
private Attribute<UserModel, User> createCustomAttribute(Object scimName) {
|
||||
return Attribute.<UserModel, User>simple(scimName.toString())
|
||||
.modelAttributeResolver(attribute -> {
|
||||
if (isCore()) {
|
||||
return null;
|
||||
}
|
||||
UserProfile profile = getUserProfile();
|
||||
Attributes attributes = profile.getAttributes();
|
||||
|
||||
for (String modelName : attributes.nameSet()) {
|
||||
AttributeMetadata metadata = attributes.getMetadata(modelName);
|
||||
|
||||
if (metadata == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, Object> annotations = metadata.getAnnotations();
|
||||
|
||||
if (annotations == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Object modelScimAttributeName = annotations.get(ANNOTATION_SCIM_SCHEMA_ATTRIBUTE);
|
||||
|
||||
if (attribute.getName().equals(modelScimAttributeName)) {
|
||||
return modelName;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.withSetters((model, name, value) -> {
|
||||
if (isCore()) {
|
||||
return;
|
||||
}
|
||||
if (getAttributeMapperByModelAttribute(name) == null) {
|
||||
return;
|
||||
}
|
||||
if (value == null) {
|
||||
model.removeAttribute(name);
|
||||
} else {
|
||||
model.setSingleAttribute(name, value.toString());
|
||||
}
|
||||
}, (attribute, user, value) -> {
|
||||
if (isCore()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String schema = attribute.getSchema();
|
||||
|
||||
if (schema == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String attributeName = attribute.getSimpleName();
|
||||
Map<String, Object> extensions = user.getExtensions();
|
||||
|
||||
if (extensions == null) {
|
||||
extensions = new HashMap<>();
|
||||
user.setExtensions(extensions);
|
||||
}
|
||||
|
||||
Map<String, Object> subAttributes = (Map<String, Object>) extensions.get(schema);
|
||||
|
||||
if (subAttributes == null) {
|
||||
subAttributes = new HashMap<>();
|
||||
extensions.put(schema, subAttributes);
|
||||
user.addSchema(schema);
|
||||
}
|
||||
|
||||
int subSubAttribute = attributeName.indexOf('.');
|
||||
|
||||
if (subSubAttribute != -1) {
|
||||
String parentAttributeName = attributeName.substring(0, subSubAttribute);
|
||||
subAttributes.put(parentAttributeName, new HashMap<>());
|
||||
subAttributes = (Map<String, Object>) subAttributes.get(parentAttributeName);
|
||||
attributeName = attributeName.substring(parentAttributeName.length() + 1);
|
||||
}
|
||||
|
||||
subAttributes.put(attributeName, value);
|
||||
}).build().get(0);
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ import static org.keycloak.utils.StreamsUtil.closing;
|
||||
public class UserResourceTypeProvider extends AbstractScimResourceTypeProvider<UserModel, User> implements ScimAttributeJpaExpressionResolver {
|
||||
|
||||
public UserResourceTypeProvider(KeycloakSession session) {
|
||||
super(session, new UserCoreModelSchema(session), List.of(new UserEnterpriseModelSchema(session)));
|
||||
super(session, new UserCoreModelSchema(session), List.of(new UserEnterpriseModelSchema(session), new UserExtensionModelSchema(session)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
|
||||
package org.keycloak.tests.scim.tck;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.keycloak.representations.userprofile.config.UPAttribute;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.scim.client.ScimClient;
|
||||
import org.keycloak.testframework.annotations.InjectAdminEvents;
|
||||
import org.keycloak.testframework.annotations.InjectRealm;
|
||||
@@ -17,6 +21,9 @@ import org.keycloak.testframework.events.AdminEvents;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.scim.client.annotations.InjectScimClient;
|
||||
|
||||
import static org.keycloak.scim.model.user.AbstractUserModelSchema.ANNOTATION_SCIM_SCHEMA_ATTRIBUTE;
|
||||
import static org.keycloak.scim.resource.Scim.ENTERPRISE_USER_SCHEMA;
|
||||
|
||||
public abstract class AbstractScimTest {
|
||||
|
||||
@InjectRealm(config = ScimRealmConfig.class)
|
||||
@@ -27,4 +34,25 @@ public abstract class AbstractScimTest {
|
||||
|
||||
@InjectAdminEvents
|
||||
AdminEvents adminEvents;
|
||||
|
||||
protected void addEnterpriseUserUserProfileAttributes() {
|
||||
UPConfig configuration = realm.admin().users().userProfile().getConfiguration();
|
||||
|
||||
configuration.addOrReplaceAttribute(new UPAttribute("department", Map.of(
|
||||
ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, ENTERPRISE_USER_SCHEMA + ".department")));
|
||||
configuration.addOrReplaceAttribute(new UPAttribute("division", Map.of(
|
||||
ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, ENTERPRISE_USER_SCHEMA + ".division")));
|
||||
configuration.addOrReplaceAttribute(new UPAttribute("costCenter", Map.of(
|
||||
ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, ENTERPRISE_USER_SCHEMA + ".costCenter")));
|
||||
configuration.addOrReplaceAttribute(new UPAttribute("employeeNumber", Map.of(
|
||||
ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, ENTERPRISE_USER_SCHEMA + ".employeeNumber")));
|
||||
configuration.addOrReplaceAttribute(new UPAttribute("organization", Map.of(
|
||||
ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, ENTERPRISE_USER_SCHEMA + ".organization")));
|
||||
configuration.addOrReplaceAttribute(new UPAttribute("manager", Map.of(
|
||||
ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, ENTERPRISE_USER_SCHEMA + ".manager.value")));
|
||||
configuration.addOrReplaceAttribute(new UPAttribute("managerName", Map.of(
|
||||
ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, ENTERPRISE_USER_SCHEMA + ".manager.displayName")));
|
||||
realm.admin().users().userProfile().update(configuration);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -706,6 +706,8 @@ public class FilterTest extends AbstractScimTest {
|
||||
user.setFirstName(givenName);
|
||||
user.setLastName(familyName);
|
||||
user = client.users().create(user);
|
||||
assertNotNull(user);
|
||||
user = client.users().get(user.getId());
|
||||
userIdsToRemove.add(user.getId());
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -389,7 +389,7 @@ public class GroupTest extends AbstractScimTest {
|
||||
} catch (ScimClientException e) {
|
||||
ErrorResponse error = e.getError();
|
||||
assertNotNull(error);
|
||||
assertEquals("Managing members on updates are not supported", error.getDetail());
|
||||
assertEquals("Managing members on updates is not supported", error.getDetail());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
package org.keycloak.tests.scim.tck;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.keycloak.representations.userprofile.config.UPAttribute;
|
||||
import org.keycloak.representations.userprofile.config.UPAttributePermissions;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.scim.protocol.response.ListResponse;
|
||||
import org.keycloak.scim.resource.Scim;
|
||||
import org.keycloak.scim.resource.schema.Schema;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.userprofile.config.UPConfigUtils;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.keycloak.scim.model.user.AbstractUserModelSchema.ANNOTATION_SCIM_SCHEMA_ATTRIBUTE;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@KeycloakIntegrationTest(config = ScimServerConfig.class)
|
||||
@@ -21,12 +29,15 @@ public class SchemaTest extends AbstractScimTest {
|
||||
|
||||
@Test
|
||||
public void testGetAllSchemas() {
|
||||
String customSchema = "urn:my:params:scim:schemas:extension:custom:1.0:User";
|
||||
addOrReplaceUPAttribute(customSchema, "myattribute");
|
||||
|
||||
ListResponse<Schema> response = client.schemas().getAll();
|
||||
|
||||
assertNotNull(response);
|
||||
assertNotNull(response.getResources());
|
||||
assertEquals(3, response.getTotalResults());
|
||||
assertEquals(3, response.getResources().size());
|
||||
assertEquals(4, response.getTotalResults());
|
||||
assertEquals(4, response.getResources().size());
|
||||
|
||||
// Verify all expected schemas are present
|
||||
List<String> schemaIds = response.getResources().stream()
|
||||
@@ -36,6 +47,7 @@ public class SchemaTest extends AbstractScimTest {
|
||||
assertTrue(schemaIds.contains(Scim.USER_CORE_SCHEMA));
|
||||
assertTrue(schemaIds.contains(Scim.GROUP_CORE_SCHEMA));
|
||||
assertTrue(schemaIds.contains(Scim.ENTERPRISE_USER_SCHEMA));
|
||||
assertTrue(schemaIds.contains(customSchema));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -111,6 +123,7 @@ public class SchemaTest extends AbstractScimTest {
|
||||
|
||||
@Test
|
||||
public void testGetEnterpriseUserSchema() {
|
||||
addEnterpriseUserUserProfileAttributes();
|
||||
Schema schema = client.schemas().get(Scim.ENTERPRISE_USER_SCHEMA);
|
||||
|
||||
assertNotNull(schema);
|
||||
@@ -146,6 +159,37 @@ public class SchemaTest extends AbstractScimTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCustomSchema() {
|
||||
String fooSchema = "urn:my:params:scim:schemas:extension:custom:1.0:User";
|
||||
addOrReplaceUPAttribute(fooSchema, "myattribute");
|
||||
addOrReplaceUPAttribute(fooSchema, "team");
|
||||
|
||||
String barSchema = "urn:my:params:scim:schemas:extension:other:1.0:User";
|
||||
addOrReplaceUPAttribute(barSchema, "other");
|
||||
|
||||
Schema schema = client.schemas().get(fooSchema);
|
||||
assertNotNull(schema);
|
||||
assertEquals(fooSchema, schema.getId());
|
||||
assertEquals(fooSchema, schema.getName());
|
||||
assertNull(schema.getDescription());
|
||||
assertNotNull(schema.getAttributes());
|
||||
Set<String> attributeNames = schema.getAttributes().stream()
|
||||
.map(Schema.Attribute::getName)
|
||||
.collect(Collectors.toSet());
|
||||
assertEquals(2, attributeNames.size());
|
||||
assertAttribute(findAttribute(schema, "myattribute"), "string", false, false, true, "readWrite", "none");
|
||||
assertAttribute(findAttribute(schema, "team"), "string", false, false, true, "readWrite", "none");
|
||||
|
||||
schema = client.schemas().get(barSchema);
|
||||
attributeNames = schema.getAttributes().stream()
|
||||
.map(Schema.Attribute::getName)
|
||||
.collect(Collectors.toSet());
|
||||
assertEquals(1, attributeNames.size());
|
||||
// Simple string attributes
|
||||
assertAttribute(findAttribute(schema, "other"), "string", false, false, true, "readWrite", "none");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoDuplicateAttributes() {
|
||||
// Verify that attributes are not duplicated even when multiple SCIM paths
|
||||
@@ -203,4 +247,12 @@ public class SchemaTest extends AbstractScimTest {
|
||||
assertEquals(multiValued, subAttribute.getMultiValued(), subAttribute.getName() + " sub-attribute multiValued");
|
||||
assertEquals(mutability, subAttribute.getMutability(), subAttribute.getName() + " sub-attribute mutability");
|
||||
}
|
||||
|
||||
private void addOrReplaceUPAttribute(String customSchema, String name) {
|
||||
UPConfig upConfig = realm.admin().users().userProfile().getConfiguration();
|
||||
UPAttribute upAttribute = new UPAttribute("scim" + name, Map.of(ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, customSchema + "." + name));
|
||||
upAttribute.setPermissions(new UPAttributePermissions(Set.of(UPConfigUtils.ROLE_ADMIN), Set.of(UPConfigUtils.ROLE_ADMIN)));
|
||||
upConfig.addOrReplaceAttribute(upAttribute);
|
||||
realm.admin().users().userProfile().update(upConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.keycloak.tests.scim.tck;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -18,8 +19,10 @@ import org.keycloak.representations.idm.GroupRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.userprofile.config.UPAttribute;
|
||||
import org.keycloak.representations.userprofile.config.UPAttributePermissions;
|
||||
import org.keycloak.representations.userprofile.config.UPAttributeRequired;
|
||||
import org.keycloak.representations.userprofile.config.UPConfig;
|
||||
import org.keycloak.scim.client.ResourceFilter;
|
||||
import org.keycloak.scim.client.ScimClient;
|
||||
import org.keycloak.scim.client.ScimClientException;
|
||||
import org.keycloak.scim.protocol.request.PatchRequest;
|
||||
@@ -39,13 +42,17 @@ import org.keycloak.testframework.realm.GroupBuilder;
|
||||
import org.keycloak.testframework.realm.UserBuilder;
|
||||
import org.keycloak.testframework.scim.client.annotations.InjectScimClient;
|
||||
import org.keycloak.testframework.util.ApiUtil;
|
||||
import org.keycloak.userprofile.config.UPConfigUtils;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
|
||||
import static org.keycloak.scim.model.user.AbstractUserModelSchema.ANNOTATION_SCIM_SCHEMA_ATTRIBUTE;
|
||||
import static org.keycloak.scim.model.user.UserExtensionModelSchema.KEYCLOAK_USER_SCHEMA;
|
||||
import static org.keycloak.scim.resource.Scim.ENTERPRISE_USER_SCHEMA;
|
||||
import static org.keycloak.scim.resource.Scim.USER_RESOURCE_TYPE;
|
||||
import static org.keycloak.scim.resource.Scim.getCoreSchema;
|
||||
@@ -53,6 +60,7 @@ import static org.keycloak.scim.resource.Scim.getCoreSchema;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
@@ -1079,6 +1087,186 @@ public class UserTest extends AbstractScimTest {
|
||||
assertNull(actual.getEnterpriseUser());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateCustomAttribute() {
|
||||
UPConfig upConfig = realm.admin().users().userProfile().getConfiguration();
|
||||
String fooSchema = "urn:my:params:scim:schemas:extension:foo:1.0:User";
|
||||
UPAttribute upAttribute = new UPAttribute("keycloak.team", Map.of(ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, fooSchema + ".memberOf"));
|
||||
upAttribute.setPermissions(new UPAttributePermissions(Set.of(UPConfigUtils.ROLE_ADMIN), Set.of(UPConfigUtils.ROLE_ADMIN)));
|
||||
upConfig.addOrReplaceAttribute(upAttribute);
|
||||
realm.admin().users().userProfile().update(upConfig);
|
||||
|
||||
String barSchema = "urn:my:params:scim:schemas:extension:bar:1.0:User";
|
||||
upAttribute = new UPAttribute("keycloak.area", Map.of(ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, barSchema + ".myattribute"));
|
||||
upAttribute.setPermissions(new UPAttributePermissions(Set.of(UPConfigUtils.ROLE_ADMIN), Set.of(UPConfigUtils.ROLE_ADMIN)));
|
||||
upConfig.addOrReplaceAttribute(upAttribute);
|
||||
realm.admin().users().userProfile().update(upConfig);
|
||||
|
||||
User user = new User();
|
||||
|
||||
user.addSchema(fooSchema);
|
||||
user.addSchema(barSchema);
|
||||
user.setUserName(KeycloakModelUtils.generateId());
|
||||
user.setExtensions(new HashMap<>());
|
||||
HashMap<Object, Object> keycloakSchema = new HashMap<>();
|
||||
keycloakSchema.put("memberOf", "core-iam");
|
||||
user.getExtensions().put(fooSchema, keycloakSchema);
|
||||
HashMap<Object, Object> customSchemaValues = new HashMap<>();
|
||||
customSchemaValues.put("myattribute", "myvalue");
|
||||
user.getExtensions().put(barSchema, customSchemaValues);
|
||||
|
||||
try {
|
||||
user = client.users().create(user);
|
||||
user = client.users().get(user.getId());
|
||||
Object value = ofNullable(user.getExtensions()).orElse(Map.of()).get(fooSchema);
|
||||
assertInstanceOf(Map.class, value);
|
||||
assertTrue(user.getSchemas().contains(fooSchema));
|
||||
assertEquals("core-iam", ((Map<?, ?>) value).get("memberOf"));
|
||||
value = ofNullable(user.getExtensions()).orElse(Map.of()).get(barSchema);
|
||||
assertInstanceOf(Map.class, value);
|
||||
assertTrue(user.getSchemas().contains(barSchema));
|
||||
assertEquals("myvalue", ((Map<?, ?>) value).get("myattribute"));
|
||||
} finally {
|
||||
client.users().delete(user.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetCustomAttribute() {
|
||||
UserRepresentation existing = UserBuilder.create()
|
||||
.username(KeycloakModelUtils.generateId())
|
||||
.email(KeycloakModelUtils.generateId() + "@keycloak.org")
|
||||
.firstName("f")
|
||||
.lastName("l")
|
||||
.enabled(true)
|
||||
.build();
|
||||
try (Response response = realm.admin().users().create(existing)) {
|
||||
String id = ApiUtil.getCreatedId(response);
|
||||
existing.setId(id);
|
||||
}
|
||||
|
||||
// adds a user profile attribute
|
||||
UPConfig upConfig = realm.admin().users().userProfile().getConfiguration();
|
||||
UPAttribute upAttribute = new UPAttribute("keycloak.team", Map.of(ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, KEYCLOAK_USER_SCHEMA + ".memberOf"));
|
||||
upAttribute.setPermissions(new UPAttributePermissions(Set.of(UPConfigUtils.ROLE_ADMIN), Set.of(UPConfigUtils.ROLE_ADMIN)));
|
||||
upConfig.addOrReplaceAttribute(upAttribute);
|
||||
realm.admin().users().userProfile().update(upConfig);
|
||||
existing = realm.admin().users().get(existing.getId()).toRepresentation();
|
||||
existing.singleAttribute(upAttribute.getName(), "core-iam");
|
||||
realm.admin().users().get(existing.getId()).update(existing);
|
||||
|
||||
String customSchema = "urn:my:params:scim:schemas:extension:custom:1.0:User";
|
||||
upAttribute = new UPAttribute("keycloak.area", Map.of(ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, customSchema + ".myattribute"));
|
||||
upAttribute.setPermissions(new UPAttributePermissions(Set.of(UPConfigUtils.ROLE_ADMIN), Set.of(UPConfigUtils.ROLE_ADMIN)));
|
||||
upConfig.addOrReplaceAttribute(upAttribute);
|
||||
realm.admin().users().userProfile().update(upConfig);
|
||||
existing = realm.admin().users().get(existing.getId()).toRepresentation();
|
||||
existing.singleAttribute(upAttribute.getName(), "myvalue");
|
||||
realm.admin().users().get(existing.getId()).update(existing);
|
||||
|
||||
existing = realm.admin().users().get(existing.getId()).toRepresentation();
|
||||
assertNotNull(ofNullable(existing.getAttributes()).orElse(Map.of()).get(upAttribute.getName()));
|
||||
|
||||
User user = client.users().get(existing.getId());
|
||||
Object value = ofNullable(user.getExtensions()).orElse(Map.of()).get(KEYCLOAK_USER_SCHEMA);
|
||||
assertInstanceOf(Map.class, value);
|
||||
assertTrue(user.getSchemas().contains(KEYCLOAK_USER_SCHEMA));
|
||||
assertEquals("core-iam", ((Map<?, ?>) value).get("memberOf"));
|
||||
value = ofNullable(user.getExtensions()).orElse(Map.of()).get(customSchema);
|
||||
assertInstanceOf(Map.class, value);
|
||||
assertTrue(user.getSchemas().contains(customSchema));
|
||||
assertEquals("myvalue", ((Map<?, ?>) value).get("myattribute"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSearchCustomAttribute() {
|
||||
UserRepresentation existing = UserBuilder.create()
|
||||
.username(KeycloakModelUtils.generateId())
|
||||
.email(KeycloakModelUtils.generateId() + "@keycloak.org")
|
||||
.firstName("f")
|
||||
.lastName("l")
|
||||
.enabled(true)
|
||||
.build();
|
||||
try (Response response = realm.admin().users().create(existing)) {
|
||||
String id = ApiUtil.getCreatedId(response);
|
||||
existing.setId(id);
|
||||
}
|
||||
|
||||
// adds a user profile attribute
|
||||
UPConfig upConfig = realm.admin().users().userProfile().getConfiguration();
|
||||
UPAttribute upAttribute = new UPAttribute("keycloak.team", Map.of(ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, KEYCLOAK_USER_SCHEMA + ".memberOf"));
|
||||
upAttribute.setPermissions(new UPAttributePermissions(Set.of(UPConfigUtils.ROLE_ADMIN), Set.of(UPConfigUtils.ROLE_ADMIN)));
|
||||
upConfig.addOrReplaceAttribute(upAttribute);
|
||||
realm.admin().users().userProfile().update(upConfig);
|
||||
existing = realm.admin().users().get(existing.getId()).toRepresentation();
|
||||
existing.singleAttribute(upAttribute.getName(), "core-iam");
|
||||
realm.admin().users().get(existing.getId()).update(existing);
|
||||
|
||||
String customSchema = "urn:my:params:scim:schemas:extension:custom:1.0:User";
|
||||
upAttribute = new UPAttribute("keycloak.area", Map.of(ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, customSchema + ".myattribute"));
|
||||
upAttribute.setPermissions(new UPAttributePermissions(Set.of(UPConfigUtils.ROLE_ADMIN), Set.of(UPConfigUtils.ROLE_ADMIN)));
|
||||
upConfig.addOrReplaceAttribute(upAttribute);
|
||||
realm.admin().users().userProfile().update(upConfig);
|
||||
existing = realm.admin().users().get(existing.getId()).toRepresentation();
|
||||
existing.singleAttribute(upAttribute.getName(), "myvalue");
|
||||
realm.admin().users().get(existing.getId()).update(existing);
|
||||
|
||||
ListResponse<User> result = client.users().search(ResourceFilter.filter().eq(KEYCLOAK_USER_SCHEMA + ".memberOf", "core-iam").build());
|
||||
assertEquals(1, result.getTotalResults());
|
||||
result = client.users().search(ResourceFilter.filter().eq(KEYCLOAK_USER_SCHEMA + ".memberOf", "non-existent").build());
|
||||
assertEquals(0, result.getTotalResults());
|
||||
|
||||
result = client.users().search(ResourceFilter.filter().eq(customSchema + ".myattribute", "myvalue").build());
|
||||
assertEquals(1, result.getTotalResults());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPatchCustomAttribute() {
|
||||
UPConfig upConfig = realm.admin().users().userProfile().getConfiguration();
|
||||
UPAttribute upAttribute = new UPAttribute("keycloak.team", Map.of(ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, KEYCLOAK_USER_SCHEMA + ".memberOf"));
|
||||
upAttribute.setPermissions(new UPAttributePermissions(Set.of(UPConfigUtils.ROLE_ADMIN), Set.of(UPConfigUtils.ROLE_ADMIN)));
|
||||
upConfig.addOrReplaceAttribute(upAttribute);
|
||||
realm.admin().users().userProfile().update(upConfig);
|
||||
|
||||
String customSchema = "urn:my:params:scim:schemas:extension:custom:1.0:User";
|
||||
upAttribute = new UPAttribute("keycloak.area", Map.of(ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, customSchema + ".myattribute"));
|
||||
upAttribute.setPermissions(new UPAttributePermissions(Set.of(UPConfigUtils.ROLE_ADMIN), Set.of(UPConfigUtils.ROLE_ADMIN)));
|
||||
upConfig.addOrReplaceAttribute(upAttribute);
|
||||
realm.admin().users().userProfile().update(upConfig);
|
||||
|
||||
User user = new User();
|
||||
|
||||
user.addSchema(KEYCLOAK_USER_SCHEMA);
|
||||
user.addSchema(customSchema);
|
||||
user.setUserName(KeycloakModelUtils.generateId());
|
||||
user.setExtensions(new HashMap<>());
|
||||
HashMap<Object, Object> keycloakSchema = new HashMap<>();
|
||||
keycloakSchema.put("memberOf", "core-iam");
|
||||
user.getExtensions().put(KEYCLOAK_USER_SCHEMA, keycloakSchema);
|
||||
HashMap<Object, Object> customSchemaValues = new HashMap<>();
|
||||
customSchemaValues.put("myattribute", "myvalue");
|
||||
user.getExtensions().put(customSchema, customSchemaValues);
|
||||
|
||||
try {
|
||||
user = client.users().create(user);
|
||||
client.users().patch(user.getId(), PatchRequest.create()
|
||||
.replace(KEYCLOAK_USER_SCHEMA + ":memberOf", "core-iam-updated")
|
||||
.replace(customSchema + ":myattribute", "myvalue-updated")
|
||||
.build());
|
||||
user = client.users().get(user.getId());
|
||||
Object value = ofNullable(user.getExtensions()).orElse(Map.of()).get(KEYCLOAK_USER_SCHEMA);
|
||||
assertInstanceOf(Map.class, value);
|
||||
assertTrue(user.getSchemas().contains(KEYCLOAK_USER_SCHEMA));
|
||||
assertEquals("core-iam-updated", ((Map<?, ?>) value).get("memberOf"));
|
||||
value = ofNullable(user.getExtensions()).orElse(Map.of()).get(customSchema);
|
||||
assertInstanceOf(Map.class, value);
|
||||
assertTrue(user.getSchemas().contains(customSchema));
|
||||
assertEquals("myvalue-updated", ((Map<?, ?>) value).get("myattribute"));
|
||||
} finally {
|
||||
client.users().delete(user.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertGroup(List<GroupMembership> groups, GroupRepresentation group, String type) {
|
||||
assertTrue(groups.stream().anyMatch(membership -> {
|
||||
boolean found = group.getId().equals(membership.getValue()) && group.getName().equals(membership.getDisplay());
|
||||
@@ -1197,6 +1385,7 @@ public class UserTest extends AbstractScimTest {
|
||||
return user;
|
||||
}
|
||||
|
||||
@Disabled("Update attribute validation after adding custom attributes")
|
||||
@Test
|
||||
public void testCreateWithInvalidAttribute() {
|
||||
User user = new User() {
|
||||
@@ -1240,24 +1429,4 @@ public class UserTest extends AbstractScimTest {
|
||||
assertNotNull(error.getDetail());
|
||||
}
|
||||
}
|
||||
|
||||
private void addEnterpriseUserUserProfileAttributes() {
|
||||
UPConfig configuration = realm.admin().users().userProfile().getConfiguration();
|
||||
|
||||
configuration.addOrReplaceAttribute(new UPAttribute("department", Map.of(
|
||||
ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, ENTERPRISE_USER_SCHEMA + ".department")));
|
||||
configuration.addOrReplaceAttribute(new UPAttribute("division", Map.of(
|
||||
ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, ENTERPRISE_USER_SCHEMA + ".division")));
|
||||
configuration.addOrReplaceAttribute(new UPAttribute("costCenter", Map.of(
|
||||
ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, ENTERPRISE_USER_SCHEMA + ".costCenter")));
|
||||
configuration.addOrReplaceAttribute(new UPAttribute("employeeNumber", Map.of(
|
||||
ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, ENTERPRISE_USER_SCHEMA + ".employeeNumber")));
|
||||
configuration.addOrReplaceAttribute(new UPAttribute("organization", Map.of(
|
||||
ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, ENTERPRISE_USER_SCHEMA + ".organization")));
|
||||
configuration.addOrReplaceAttribute(new UPAttribute("manager", Map.of(
|
||||
ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, ENTERPRISE_USER_SCHEMA + ".manager.value")));
|
||||
configuration.addOrReplaceAttribute(new UPAttribute("managerName", Map.of(
|
||||
ANNOTATION_SCIM_SCHEMA_ATTRIBUTE, ENTERPRISE_USER_SCHEMA + ".manager.displayName")));
|
||||
realm.admin().users().userProfile().update(configuration);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user