8000 Support for managing members via group resource type by pedroigor · Pull Request #47636 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ public R get(String id) {
return get(id, null, null);
}

public R get(String id, List<String> attributes) {
return get(id, attributes, null);
}

public R get(String id, List<String> attributes, List<String> excludedAttributes) {
requireNonNull(id, "SCIM resource ID must not be null");

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.keycloak.scim.resource.group;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

Expand Down Expand Up @@ -37,4 +38,17 @@ public List<Member> getMembers() {
public void setMembers(List<Member> members) {
this.members = members;
}

public void addMember(String userId) {
Member member = new Member();
member.setValue(userId);
addMember(member);
}

public void addMember(Member member) {
if (members == null) {
members = new ArrayList<>();
}
members.add(member);
}
}
Original file line number Diff line number Diff line change
@@ -1,52 +1,9 @@
package org.keycloak.scim.resource.group;

import org.keycloak.scim.resource.common.MultiValuedAttribute;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class Member {

@JsonProperty("value")
private String value;

@JsonProperty("$ref")
private String ref;

@JsonProperty("type")
private String type;

@JsonProperty("display")
private String display;

public String getValue() {
return value;
}

public void setValue(String value) {
this.value = value;
}

public String getRef() {
return ref;
}

public void setRef(String ref) {
this.ref = ref;
}

public String getType() {
return type;
}

public void setType(String type) {
this.type = type;
}

public String getDisplay() {
return display;
}

public void setDisplay(String display) {
this.display = display;
}
public class Member extends MultiValuedAttribute {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.keycloak.scim.resource.schema;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
Expand Down Expand Up @@ -104,19 +103,9 @@ public void remove(R resource, M model, String rawPath) {
}

Path path = new Path(this, rawPath);
JsonNode value = NullNode.getInstance();

if (path.hasFilter()) {
try {
value = JsonSerialization.createObjectNode(resource);
} catch (IOException e 8000 ) {
throw new RuntimeException(e);
}
}

for (Entry<Attribute<M, R>, JsonNode> entry : resolveAttributes(path.getPath(), value).entrySet()) {
JsonNode attrValue = path.getValue(entry.getValue());
setValue(model, entry.getKey(), attrValue, REMOVE);
for (Entry<Attribute<M, R>, JsonNode> entry : resolveAttributes(path.getPath(), NullNode.getInstance()).entrySet()) {
setValue(model, entry.getKey(), path.getValue(entry.getKey()), REMOVE);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ public boolean isExcluded(ModelSchema<M, R> schema, List<String> requestedAttrib

if (Attribute.RETURNED_REQUEST.equals(returned)) {
// No attributes parameter specified - returned: request attributes are not returned by default
return true;
return !isPresent(schema, requestedAttributes);
}

return isPresent(schema, excludedAttributes);
8BD0 Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
package org.keycloak.scim.resource.schema.path;

import java.util.function.Predicate;

import org.keycloak.scim.filter.FilterUtils;
import org.keycloak.scim.filter.ScimFilterParser;
import org.keycloak.scim.resource.ResourceTypeRepresentation;
import org.keycloak.scim.resource.schema.ModelSchema;
import org.keycloak.scim.resource.schema.attribute.Attribute;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.NullNode;

public final class Path {
Expand Down Expand Up @@ -52,31 +49,12 @@ public String getPath() {
return path;
}

public JsonNode getValue(JsonNode rawValue) {
public JsonNode getValue(Attribute<?, ?> attribute) {
if (filter == null) {
return rawValue;
return NullNode.getInstance();
}

ScimFilterParser.FilterContext filterContext = FilterUtils.parseFilter(filter);
Predicate<JsonNode> predicate = new ScimJsonNodeFilterEvaluator().visit(filterContext);

if (rawValue.isArray()) {
ArrayNode matches = JsonNodeFactory.instance.arrayNode();
for (JsonNode node : rawValue) {
if (node.isObject() && predicate.test(node)) {
matches.add(node);
}
}
if (!matches.isEmpty()) {
return matches.size() == 1 ? matches.get(0) : matches;
}
}

return NullNode.getInstance();
return new ScimFilterToJsonNodeConverter(attribute).visit(filterContext);
}

public boolean hasFilter() {
return filter != null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package org.keycloak.scim.resource.schema.path;

import org.keycloak.models.ModelValidationException;
import org.keycloak.scim.filter.ScimFilterParser;
import org.keycloak.scim.filter.ScimFilterParserBaseVisitor;
import org.keycloak.scim.resource.schema.attribute.Attribute;
import org.keycloak.util.JsonSerialization;

import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;

class ScimFilterToJsonNodeConverter extends ScimFilterParserBaseVisitor<JsonNode> {

private final Attribute<?, ?> attribute;

public ScimFilterToJsonNodeConverter(Attribute<?, ?> attribute) {
this.attribute = attribute;
}

@Override
public JsonNode visitFilter(ScimFilterParser.FilterContext ctx) {
return visit(ctx.expression());
}

@Override
public JsonNode visitExpression(ScimFilterParser.ExpressionContext ctx) {
if (ctx.OR() != null) {
JsonNode left = visit(ctx.expression());
JsonNode right = visit(ctx.andExpression());
ArrayNode array = JsonNodeFactory.instance.arrayNode();
flattenIntoArray(array, left);
flattenIntoArray(array, right);
return array;
}
return visit(ctx.andExpression());
}

@Override
public JsonNode visitAndExpression(ScimFilterParser.AndExpressionContext ctx) {
if (ctx.AND() != null) {
JsonNode left = visit(ctx.andExpression());
JsonNode right = visit(ctx.notExpression());
ObjectNode merged = JsonNodeFactory.instance.objectNode();
mergeFields(merged, left);
mergeFields(merged, right);
return merged;
}
return visit(ctx.notExpression());
}

@Override
public JsonNode visitNotExpression(ScimFilterParser.NotExpressionContext ctx) {
if (ctx.NOT() != null) {
throw new IllegalArgumentException("NOT operator is not supported when converting a SCIM filter to a JSON value");
}
return visit(ctx.atom());
}

@Override
public JsonNode visitAtom(ScimFilterParser.AtomContext ctx) {
if (ctx.valuePath() != null) {
return visit(ctx.valuePath());
}
if (ctx.attributeExpression() != null) {
return visit(ctx.attributeExpression());
}
return visit(ctx.expression());
}

@Override
public JsonNode visitValuePath(ScimFilterParser.ValuePathContext ctx) {
return visit(ctx.expression());
}

@Override
public JsonNode visitPresentExpression(ScimFilterParser.PresentExpressionContext ctx) {
throw new IllegalArgumentException("Present (pr) operator is not supported when converting a SCIM filter to a JSON value");
}

@Override
public JsonNode visitComparisonExpression(ScimFilterParser.ComparisonExpressionContext ctx) {
String operator = ctx.compareOp().getText().toLowerCase();

if (!"eq".equals(operator)) {
throw new IllegalArgumentException("Only 'eq' operator is supported when converting a SCIM filter to a JSON value, got: " + operator);
}

Class<?> complexType = attribute.getComplexType();

if (complexType == null) {
return null;
}

String attrName = ctx.ATTRPATH().getText();

if (!isComplexTypeAttribute(complexType, attrName)) {
throw new ModelValidationException("Unknown attribute " + attrName);
}

String value = extractValue(ctx.compValue());
ObjectNode node = JsonNodeFactory.instance.objectNode();

if (value == null) {
node.putNull(attrName);
} else {
node.put(attrName, value);
}

return node;
}

private static boolean isComplexTypeAttribute(Class<?> complexType, String attrName) {
JavaType javaType = JsonSerialization.mapper.getTypeFactory().constructType(complexType);
SerializationConfig serializationConfig = JsonSerialization.mapper.getSerializationConfig();
return serializationConfig.introspect(javaType).findProperties().stream().anyMatch(p -> p.getName().equals(attrName));
}

private String extractValue(ScimFilterParser.CompValueContext ctx) {
if (ctx.STRING() != null) {
String raw = ctx.STRING().getText();
return unescapeJsonString(raw.substring(1, raw.length() - 1));
}
if (ctx.TRUE() != null) return "true";
if (ctx.FALSE() != null) return "false";
if (ctx.NULL() != null) return null;
if (ctx.NUMBER() != null) return ctx.NUMBER().getText();
return null;
}

private String unescapeJsonString(String s) {
return s.replace("\\\"", "\"")
.replace("\\\\", "\\")
.replace("\\/", "/")
.replace("\\b", "\b")
.replace("\\f", "\f")
.replace("\\n", "\n")
.replace("\\r", "\r")
.replace("\\t", "\t");
}

private void flattenIntoArray(ArrayNode array, JsonNode node) {
if (node.isArray()) {
node.forEach(array::add);
} else {
array.add(node);
}
}

private void mergeFields(ObjectNode target, JsonNode source) {
if (source.isObject()) {
source.properties().forEach(e -> target.set(e.getKey(), e.getValue()));
}
}
}
Loading
Loading
0