Support JSON objects when evaluating claims in regex policy
Closes #11514
This commit is contained in:
parent
c95ecd9e88
commit
3d2c3fbc6a
11 changed files with 301 additions and 66 deletions
|
@ -17,6 +17,13 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.authorization.policy.provider.regex;
|
package org.keycloak.authorization.policy.provider.regex;
|
||||||
|
|
||||||
|
import static org.keycloak.utils.JsonUtils.getJsonValue;
|
||||||
|
import static org.keycloak.utils.JsonUtils.hasPath;
|
||||||
|
import static org.keycloak.utils.JsonUtils.splitClaimPath;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
@ -27,6 +34,8 @@ import org.keycloak.authorization.model.Policy;
|
||||||
import org.keycloak.authorization.policy.evaluation.Evaluation;
|
import org.keycloak.authorization.policy.evaluation.Evaluation;
|
||||||
import org.keycloak.authorization.policy.provider.PolicyProvider;
|
import org.keycloak.authorization.policy.provider.PolicyProvider;
|
||||||
import org.keycloak.representations.idm.authorization.RegexPolicyRepresentation;
|
import org.keycloak.representations.idm.authorization.RegexPolicyRepresentation;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:yoshiyuki.tabata.jy@hitachi.com">Yoshiyuki Tabata</a>
|
* @author <a href="mailto:yoshiyuki.tabata.jy@hitachi.com">Yoshiyuki Tabata</a>
|
||||||
|
@ -47,17 +56,60 @@ public class RegexPolicyProvider implements PolicyProvider {
|
||||||
public void evaluate(Evaluation evaluation) {
|
public void evaluate(Evaluation evaluation) {
|
||||||
AuthorizationProvider authorizationProvider = evaluation.getAuthorizationProvider();
|
AuthorizationProvider authorizationProvider = evaluation.getAuthorizationProvider();
|
||||||
RegexPolicyRepresentation policy = representationFunction.apply(evaluation.getPolicy(), authorizationProvider);
|
RegexPolicyRepresentation policy = representationFunction.apply(evaluation.getPolicy(), authorizationProvider);
|
||||||
Attributes.Entry targetClaim = evaluation.getContext().getIdentity().getAttributes().getValue(policy.getTargetClaim());
|
String value = getClaimValue(evaluation, policy);
|
||||||
|
|
||||||
if (targetClaim == null) {
|
if (value == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Pattern pattern = Pattern.compile(policy.getPattern());
|
Pattern pattern = Pattern.compile(policy.getPattern());
|
||||||
Matcher matcher = pattern.matcher(targetClaim.asString(0));
|
Matcher matcher = pattern.matcher(value);
|
||||||
if (matcher.matches()) {
|
if (matcher.matches()) {
|
||||||
evaluation.grant();
|
evaluation.grant();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getClaimValue(Evaluation evaluation, RegexPolicyRepresentation policy) {
|
||||||
|
Attributes attributes = evaluation.getContext().getIdentity().getAttributes();
|
||||||
|
String targetClaim = policy.getTargetClaim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (hasPath(targetClaim)) {
|
||||||
|
return resolveJsonValue(attributes, targetClaim);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveSimpleValue(attributes, targetClaim);
|
||||||
|
} catch (IOException cause) {
|
||||||
|
throw new RuntimeException("Failed to resolve value from claim: " + targetClaim, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveSimpleValue(Attributes attributes, String targetClaim) {
|
||||||
|
Attributes.Entry value = attributes.getValue(targetClaim);
|
||||||
|
|
||||||
|
if (value == null || value.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.asString(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveJsonValue(Attributes attributes, String targetClaim) throws IOException {
|
||||||
|
List<String> paths = splitClaimPath(targetClaim);
|
||||||
|
|
||||||
|
if (paths.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Attributes.Entry attribute = attributes.getValue(paths.get(0));
|
||||||
|
|
||||||
|
if (attribute == null || attribute.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode node = JsonSerialization.readValue(attribute.asString(0), JsonNode.class);
|
||||||
|
String path = String.join(".", paths.subList(1, paths.size()));
|
||||||
|
|
||||||
|
return Optional.ofNullable(getJsonValue(node, path)).map(Object::toString).orElse(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,8 @@
|
||||||
<module name="org.keycloak.keycloak-server-spi"/>
|
<module name="org.keycloak.keycloak-server-spi"/>
|
||||||
<module name="org.keycloak.keycloak-server-spi-private"/>
|
<module name="org.keycloak.keycloak-server-spi-private"/>
|
||||||
<module name="org.keycloak.keycloak-services"/>
|
<module name="org.keycloak.keycloak-services"/>
|
||||||
|
<module name="com.fasterxml.jackson.core.jackson-core"/>
|
||||||
|
<module name="com.fasterxml.jackson.core.jackson-databind"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</module>
|
</module>
|
||||||
|
|
|
@ -31,6 +31,8 @@
|
||||||
<module name="org.keycloak.keycloak-server-spi"/>
|
<module name="org.keycloak.keycloak-server-spi"/>
|
||||||
<module name="org.keycloak.keycloak-server-spi-private"/>
|
<module name="org.keycloak.keycloak-server-spi-private"/>
|
||||||
<module name="org.keycloak.keycloak-services"/>
|
<module name="org.keycloak.keycloak-services"/>
|
||||||
|
<module name="com.fasterxml.jackson.core.jackson-core"/>
|
||||||
|
<module name="com.fasterxml.jackson.core.jackson-databind"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</module>
|
</module>
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2022 Red Hat, Inc. and/or its affiliates
|
||||||
|
* and other contributors as indicated by the @author tags.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.keycloak.utils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility methods for manipulating JSON objects.
|
||||||
|
*/
|
||||||
|
public class JsonUtils {
|
||||||
|
|
||||||
|
// A character in a claim component is either a literal character escaped by a backslash (\., \\, \_, \q, etc.)
|
||||||
|
// or any character other than backslash (escaping) and dot (claim component separator)
|
||||||
|
private static final Pattern CLAIM_COMPONENT = Pattern.compile("^((\\\\.|[^\\\\.])+?)\\.");
|
||||||
|
private static final Pattern BACKSLASHED_CHARACTER = Pattern.compile("\\\\(.)");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits the given {@code claim} into separate paths if the value contains separators as per {@link #CLAIM_COMPONENT}.
|
||||||
|
*
|
||||||
|
* @param claim the claim
|
||||||
|
* @return a list with the paths
|
||||||
|
*/
|
||||||
|
public static List<String> splitClaimPath(String claim) {
|
||||||
|
final LinkedList<String> claimComponents = new LinkedList<>();
|
||||||
|
Matcher m = CLAIM_COMPONENT.matcher(claim);
|
||||||
|
int start = 0;
|
||||||
|
while (m.find()) {
|
||||||
|
claimComponents.add(BACKSLASHED_CHARACTER.matcher(m.group(1)).replaceAll("$1"));
|
||||||
|
start = m.end();
|
||||||
|
// This is necessary to match the start of region as the start of string as determined by ^
|
||||||
|
m.region(start, claim.length());
|
||||||
|
}
|
||||||
|
if (claim.length() > start) {
|
||||||
|
claimComponents.add(BACKSLASHED_CHARACTER.matcher(claim.substring(start)).replaceAll("$1"));
|
||||||
|
}
|
||||||
|
return claimComponents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the given {@code claim} contains paths.
|
||||||
|
*
|
||||||
|
* @param claim the claim
|
||||||
|
* @return {@code true} if the {@code claim} contains paths. Otherwise, false.
|
||||||
|
*/
|
||||||
|
public static boolean hasPath(String claim) {
|
||||||
|
return CLAIM_COMPONENT.matcher(claim).find();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Returns the value corresponding to the given {@code claim}.
|
||||||
|
*
|
||||||
|
* @param node the JSON node
|
||||||
|
* @param claim the claim
|
||||||
|
* @return the value
|
||||||
|
*/
|
||||||
|
public static Object getJsonValue(JsonNode node, String claim) {
|
||||||
|
if (node != null) {
|
||||||
|
List<String> fields = splitClaimPath(claim);
|
||||||
|
if (fields.isEmpty() || claim.endsWith(".")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode currentNode = node;
|
||||||
|
for (String currentFieldName : fields) {
|
||||||
|
|
||||||
|
// if array path, retrieve field name and index
|
||||||
|
String currentNodeName = currentFieldName;
|
||||||
|
int arrayIndex = -1;
|
||||||
|
if (currentFieldName.endsWith("]")) {
|
||||||
|
int bi = currentFieldName.indexOf("[");
|
||||||
|
if (bi == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String is = currentFieldName.substring(bi + 1, currentFieldName.length() - 1).trim();
|
||||||
|
arrayIndex = Integer.parseInt(is);
|
||||||
|
if( arrayIndex < 0) throw new ArrayIndexOutOfBoundsException();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
currentNodeName = currentFieldName.substring(0, bi).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
currentNode = currentNode.get(currentNodeName);
|
||||||
|
if (arrayIndex > -1 && currentNode.isArray()) {
|
||||||
|
currentNode = currentNode.get(arrayIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentNode == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentNode.isArray()) {
|
||||||
|
List<String> values = new ArrayList<>();
|
||||||
|
for (JsonNode childNode : currentNode) {
|
||||||
|
if (childNode.isTextual()) {
|
||||||
|
values.add(childNode.textValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (values.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return values ;
|
||||||
|
} else if (currentNode.isNull()) {
|
||||||
|
return null;
|
||||||
|
} else if (currentNode.isValueNode()) {
|
||||||
|
String ret = currentNode.asText();
|
||||||
|
if (ret != null && !ret.trim().isEmpty())
|
||||||
|
return ret.trim();
|
||||||
|
else
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return currentNode;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -195,6 +195,8 @@ public class KeycloakIdentity implements Identity {
|
||||||
while (valueIterator.hasNext()) {
|
while (valueIterator.hasNext()) {
|
||||||
values.add(valueIterator.next().asText());
|
values.add(valueIterator.next().asText());
|
||||||
}
|
}
|
||||||
|
} else if (fieldValue.isObject()) {
|
||||||
|
values.add(fieldValue.toString());
|
||||||
} else {
|
} else {
|
||||||
String value = fieldValue.asText();
|
String value = fieldValue.asText();
|
||||||
|
|
||||||
|
|
|
@ -17,13 +17,14 @@
|
||||||
|
|
||||||
package org.keycloak.broker.oidc.mappers;
|
package org.keycloak.broker.oidc.mappers;
|
||||||
|
|
||||||
|
import static org.keycloak.utils.JsonUtils.splitClaimPath;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
|
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
|
||||||
import org.keycloak.broker.oidc.OIDCIdentityProvider;
|
import org.keycloak.broker.oidc.OIDCIdentityProvider;
|
||||||
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
|
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
|
||||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||||
import org.keycloak.models.IdentityProviderMapperModel;
|
import org.keycloak.models.IdentityProviderMapperModel;
|
||||||
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
|
|
||||||
import org.keycloak.representations.JsonWebToken;
|
import org.keycloak.representations.JsonWebToken;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
@ -47,7 +48,7 @@ public abstract class AbstractClaimMapper extends AbstractIdentityProviderMapper
|
||||||
// found no match, try other claims
|
// found no match, try other claims
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> split = OIDCAttributeMapperHelper.splitClaimPath(claim);
|
List<String> split = splitClaimPath(claim);
|
||||||
Map<String, Object> jsonObject = token.getOtherClaims();
|
Map<String, Object> jsonObject = token.getOtherClaims();
|
||||||
final int length = split.size();
|
final int length = split.size();
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
|
|
||||||
package org.keycloak.broker.oidc.mappers;
|
package org.keycloak.broker.oidc.mappers;
|
||||||
|
|
||||||
|
import static org.keycloak.utils.JsonUtils.splitClaimPath;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.broker.oidc.OIDCIdentityProvider;
|
import org.keycloak.broker.oidc.OIDCIdentityProvider;
|
||||||
|
@ -27,7 +29,6 @@ import org.keycloak.models.IdentityProviderSyncMode;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
|
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -206,7 +207,7 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr
|
||||||
logger.debug("Going to process JsonNode path " + fieldPath + " on data " + baseNode);
|
logger.debug("Going to process JsonNode path " + fieldPath + " on data " + baseNode);
|
||||||
if (baseNode != null) {
|
if (baseNode != null) {
|
||||||
|
|
||||||
List<String> fields = OIDCAttributeMapperHelper.splitClaimPath(fieldPath);
|
List<String> fields = splitClaimPath(fieldPath);
|
||||||
if (fields.isEmpty() || fieldPath.endsWith(".")) {
|
if (fields.isEmpty() || fieldPath.endsWith(".")) {
|
||||||
logger.debug("JSON path is invalid " + fieldPath);
|
logger.debug("JSON path is invalid " + fieldPath);
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
|
|
||||||
package org.keycloak.protocol.oidc.mappers;
|
package org.keycloak.protocol.oidc.mappers;
|
||||||
|
|
||||||
|
import static org.keycloak.utils.JsonUtils.splitClaimPath;
|
||||||
|
|
||||||
import org.keycloak.models.ProtocolMapperModel;
|
import org.keycloak.models.ProtocolMapperModel;
|
||||||
import org.keycloak.protocol.ProtocolMapperUtils;
|
import org.keycloak.protocol.ProtocolMapperUtils;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
|
@ -100,7 +102,7 @@ abstract class AbstractUserRoleMappingMapper extends AbstractOIDCProtocolMapper
|
||||||
protocolClaim = CLIENT_ID_PATTERN.matcher(protocolClaim).replaceAll(clientId);
|
protocolClaim = CLIENT_ID_PATTERN.matcher(protocolClaim).replaceAll(clientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> split = OIDCAttributeMapperHelper.splitClaimPath(protocolClaim);
|
List<String> split = splitClaimPath(protocolClaim);
|
||||||
|
|
||||||
// Special case
|
// Special case
|
||||||
if (checkAccessToken(token, split, attributeValue)) {
|
if (checkAccessToken(token, split, attributeValue)) {
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
|
|
||||||
package org.keycloak.protocol.oidc.mappers;
|
package org.keycloak.protocol.oidc.mappers;
|
||||||
|
|
||||||
|
import static org.keycloak.utils.JsonUtils.splitClaimPath;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.keycloak.models.ProtocolMapperModel;
|
import org.keycloak.models.ProtocolMapperModel;
|
||||||
|
@ -31,8 +33,6 @@ import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -250,28 +250,6 @@ public class OIDCAttributeMapperHelper {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A character in a claim component is either a literal character escaped by a backslash (\., \\, \_, \q, etc.)
|
|
||||||
// or any character other than backslash (escaping) and dot (claim component separator)
|
|
||||||
private static final Pattern CLAIM_COMPONENT = Pattern.compile("^((\\\\.|[^\\\\.])+?)\\.");
|
|
||||||
|
|
||||||
private static final Pattern BACKSLASHED_CHARACTER = Pattern.compile("\\\\(.)");
|
|
||||||
|
|
||||||
public static List<String> splitClaimPath(String claimPath) {
|
|
||||||
final LinkedList<String> claimComponents = new LinkedList<>();
|
|
||||||
Matcher m = CLAIM_COMPONENT.matcher(claimPath);
|
|
||||||
int start = 0;
|
|
||||||
while (m.find()) {
|
|
||||||
claimComponents.add(BACKSLASHED_CHARACTER.matcher(m.group(1)).replaceAll("$1"));
|
|
||||||
start = m.end();
|
|
||||||
// This is necessary to match the start of region as the start of string as determined by ^
|
|
||||||
m.region(start, claimPath.length());
|
|
||||||
}
|
|
||||||
if (claimPath.length() > start) {
|
|
||||||
claimComponents.add(BACKSLASHED_CHARACTER.matcher(claimPath.substring(start)).replaceAll("$1"));
|
|
||||||
}
|
|
||||||
return claimComponents;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void mapClaim(IDToken token, ProtocolMapperModel mappingModel, Object attributeValue) {
|
public static void mapClaim(IDToken token, ProtocolMapperModel mappingModel, Object attributeValue) {
|
||||||
mapClaim(token, mappingModel, attributeValue, tokenPropertySetters, token.getOtherClaims());
|
mapClaim(token, mappingModel, attributeValue, tokenPropertySetters, token.getOtherClaims());
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,8 @@ package org.keycloak.protocol.oidc.mappers;
|
||||||
|
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.keycloak.utils.JsonUtils;
|
||||||
|
|
||||||
import static org.junit.Assert.assertThat;
|
import static org.junit.Assert.assertThat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,17 +30,17 @@ public class OIDCAttributeMapperHelperTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSplitClaimPath() {
|
public void testSplitClaimPath() {
|
||||||
assertThat(OIDCAttributeMapperHelper.splitClaimPath(""), Matchers.empty());
|
assertThat(JsonUtils.splitClaimPath(""), Matchers.empty());
|
||||||
assertThat(OIDCAttributeMapperHelper.splitClaimPath("a"), Matchers.contains("a"));
|
assertThat(JsonUtils.splitClaimPath("a"), Matchers.contains("a"));
|
||||||
|
|
||||||
assertThat(OIDCAttributeMapperHelper.splitClaimPath("a.b"), Matchers.contains("a", "b"));
|
assertThat(JsonUtils.splitClaimPath("a.b"), Matchers.contains("a", "b"));
|
||||||
assertThat(OIDCAttributeMapperHelper.splitClaimPath("a\\.b"), Matchers.contains("a.b"));
|
assertThat(JsonUtils.splitClaimPath("a\\.b"), Matchers.contains("a.b"));
|
||||||
assertThat(OIDCAttributeMapperHelper.splitClaimPath("a\\\\.b"), Matchers.contains("a\\", "b"));
|
assertThat(JsonUtils.splitClaimPath("a\\\\.b"), Matchers.contains("a\\", "b"));
|
||||||
assertThat(OIDCAttributeMapperHelper.splitClaimPath("a\\\\\\.b"), Matchers.contains("a\\.b"));
|
assertThat(JsonUtils.splitClaimPath("a\\\\\\.b"), Matchers.contains("a\\.b"));
|
||||||
|
|
||||||
assertThat(OIDCAttributeMapperHelper.splitClaimPath("c.a\\\\.b"), Matchers.contains("c", "a\\", "b"));
|
assertThat(JsonUtils.splitClaimPath("c.a\\\\.b"), Matchers.contains("c", "a\\", "b"));
|
||||||
assertThat(OIDCAttributeMapperHelper.splitClaimPath("c.a\\\\\\.b"), Matchers.contains("c", "a\\.b"));
|
assertThat(JsonUtils.splitClaimPath("c.a\\\\\\.b"), Matchers.contains("c", "a\\.b"));
|
||||||
assertThat(OIDCAttributeMapperHelper.splitClaimPath("c\\\\\\.b.a\\\\\\.b"), Matchers.contains("c\\.b", "a\\.b"));
|
assertThat(JsonUtils.splitClaimPath("c\\\\\\.b.a\\\\\\.b"), Matchers.contains("c\\.b", "a\\.b"));
|
||||||
assertThat(OIDCAttributeMapperHelper.splitClaimPath("c\\h\\.b.a\\\\\\.b"), Matchers.contains("ch.b", "a\\.b"));
|
assertThat(JsonUtils.splitClaimPath("c\\h\\.b.a\\\\\\.b"), Matchers.contains("ch.b", "a\\.b"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,38 +56,35 @@ public class RegexPolicyTest extends AbstractAuthzTest {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||||
ProtocolMapperRepresentation userAttrFooProtocolMapper = new ProtocolMapperRepresentation();
|
Map<String, String> claims = new HashMap<>();
|
||||||
userAttrFooProtocolMapper.setName("userAttrFoo");
|
|
||||||
userAttrFooProtocolMapper.setProtocolMapper(UserAttributeMapper.PROVIDER_ID);
|
|
||||||
userAttrFooProtocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
|
||||||
Map<String, String> configFoo = new HashMap<>();
|
|
||||||
configFoo.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
|
|
||||||
configFoo.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
|
|
||||||
configFoo.put(OIDCAttributeMapperHelper.JSON_TYPE, "String");
|
|
||||||
configFoo.put("user.attribute", "foo");
|
|
||||||
configFoo.put("claim.name", "foo");
|
|
||||||
userAttrFooProtocolMapper.setConfig(configFoo);
|
|
||||||
|
|
||||||
ProtocolMapperRepresentation userAttrBarProtocolMapper = new ProtocolMapperRepresentation();
|
claims.put("user.attribute", "foo");
|
||||||
userAttrBarProtocolMapper.setName("userAttrBar");
|
claims.put("claim.name", "foo");
|
||||||
userAttrBarProtocolMapper.setProtocolMapper(UserAttributeMapper.PROVIDER_ID);
|
ProtocolMapperRepresentation userAttrFooProtocolMapper = addClaimMapper("userAttrFoo", claims);
|
||||||
userAttrBarProtocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
|
||||||
Map<String, String> configBar = new HashMap<>();
|
claims.put("user.attribute", "bar");
|
||||||
configBar.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
|
claims.put("claim.name", "bar");
|
||||||
configBar.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
|
ProtocolMapperRepresentation userAttrBarProtocolMapper = addClaimMapper("userAttrBar", claims);
|
||||||
configBar.put(OIDCAttributeMapperHelper.JSON_TYPE, "String");
|
|
||||||
configBar.put("user.attribute", "bar");
|
claims.put("user.attribute", "json-simple");
|
||||||
configBar.put("claim.name", "bar");
|
claims.put("claim.name", "userinfo");
|
||||||
userAttrBarProtocolMapper.setConfig(configBar);
|
ProtocolMapperRepresentation userAttrJsonProtocolMapper = addClaimMapper("userAttrJsonSimple", claims);
|
||||||
|
|
||||||
|
claims.put("user.attribute", "json-complex");
|
||||||
|
claims.put("claim.name", "json-complex");
|
||||||
|
ProtocolMapperRepresentation userAttrJsonComplexProtocolMapper = addClaimMapper("userAttrJsonComplex", claims);
|
||||||
|
|
||||||
|
// For JSON-based claims, you can use dot notation for nesting and square brackets to access array fields by index. For example, contact.address[0].country.
|
||||||
|
|
||||||
testRealms.add(RealmBuilder.create().name("authz-test")
|
testRealms.add(RealmBuilder.create().name("authz-test")
|
||||||
.user(UserBuilder.create().username("marta").password("password").addAttribute("foo", "foo").addAttribute("bar",
|
.user(UserBuilder.create().username("marta").password("password").addAttribute("foo", "foo").addAttribute("bar",
|
||||||
"barbar"))
|
"barbar").addAttribute("json-simple", "{\"tenant\": \"abc\"}")
|
||||||
|
.addAttribute("json-complex", "{\"userinfo\": {\"tenant\": \"abc\"}, \"some-array\": [\"foo\",\"bar\"]}"))
|
||||||
.user(UserBuilder.create().username("taro").password("password").addAttribute("foo", "faa").addAttribute("bar",
|
.user(UserBuilder.create().username("taro").password("password").addAttribute("foo", "faa").addAttribute("bar",
|
||||||
"bbarbar"))
|
"bbarbar"))
|
||||||
.client(ClientBuilder.create().clientId("resource-server-test").secret("secret").authorizationServicesEnabled(true)
|
.client(ClientBuilder.create().clientId("resource-server-test").secret("secret").authorizationServicesEnabled(true)
|
||||||
.redirectUris("http://localhost/resource-server-test").directAccessGrants()
|
.redirectUris("http://localhost/resource-server-test").directAccessGrants()
|
||||||
.protocolMapper(userAttrFooProtocolMapper, userAttrBarProtocolMapper))
|
.protocolMapper(userAttrFooProtocolMapper, userAttrBarProtocolMapper, userAttrJsonProtocolMapper, userAttrJsonComplexProtocolMapper))
|
||||||
.build());
|
.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,12 +92,21 @@ public class RegexPolicyTest extends AbstractAuthzTest {
|
||||||
public void configureAuthorization() throws Exception {
|
public void configureAuthorization() throws Exception {
|
||||||
createResource("Resource A");
|
createResource("Resource A");
|
||||||
createResource("Resource B");
|
createResource("Resource B");
|
||||||
|
createResource("Resource C");
|
||||||
|
createResource("Resource D");
|
||||||
|
createResource("Resource E");
|
||||||
|
|
||||||
createRegexPolicy("Regex foo Policy", "foo", "foo");
|
createRegexPolicy("Regex foo Policy", "foo", "foo");
|
||||||
createRegexPolicy("Regex bar Policy", "bar", "^bar.+$");
|
createRegexPolicy("Regex bar Policy", "bar", "^bar.+$");
|
||||||
|
createRegexPolicy("Regex json-simple Policy", "userinfo.tenant", "^abc$");
|
||||||
|
createRegexPolicy("Regex json-complex Policy", "json-complex.userinfo.tenant", "^abc$");
|
||||||
|
createRegexPolicy("Regex json-array Policy", "json-complex.some-array[1]", "bar");
|
||||||
|
|
||||||
createResourcePermission("Resource A Permission", "Resource A", "Regex foo Policy");
|
createResourcePermission("Resource A Permission", "Resource A", "Regex foo Policy");
|
||||||
createResourcePermission("Resource B Permission", "Resource B", "Regex bar Policy");
|
createResourcePermission("Resource B Permission", "Resource B", "Regex bar Policy");
|
||||||
|
createResourcePermission("Resource C Permission", "Resource C", "Regex json-simple Policy");
|
||||||
|
createResourcePermission("Resource D Permission", "Resource D", "Regex json-complex Policy");
|
||||||
|
createResourcePermission("Resource E Permission", "Resource E", "Regex json-array Policy");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createResource(String name) {
|
private void createResource(String name) {
|
||||||
|
@ -164,6 +170,21 @@ public class RegexPolicyTest extends AbstractAuthzTest {
|
||||||
ticket = authzClient.protection().permission().create(request).getTicket();
|
ticket = authzClient.protection().permission().create(request).getTicket();
|
||||||
response = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
|
response = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
|
||||||
assertNotNull(response.getToken());
|
assertNotNull(response.getToken());
|
||||||
|
|
||||||
|
request = new PermissionRequest("Resource C");
|
||||||
|
ticket = authzClient.protection().permission().create(request).getTicket();
|
||||||
|
response = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
|
||||||
|
assertNotNull(response.getToken());
|
||||||
|
|
||||||
|
request = new PermissionRequest("Resource D");
|
||||||
|
ticket = authzClient.protection().permission().create(request).getTicket();
|
||||||
|
response = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
|
||||||
|
assertNotNull(response.getToken());
|
||||||
|
|
||||||
|
request = new PermissionRequest("Resource E");
|
||||||
|
ticket = authzClient.protection().permission().create(request).getTicket();
|
||||||
|
response = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
|
||||||
|
assertNotNull(response.getToken());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -188,9 +209,42 @@ public class RegexPolicyTest extends AbstractAuthzTest {
|
||||||
} catch (AuthorizationDeniedException ignore) {
|
} catch (AuthorizationDeniedException ignore) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Access Resource C with taro.
|
||||||
|
request = new PermissionRequest("Resource C");
|
||||||
|
ticket = authzClient.protection().permission().create(request).getTicket();
|
||||||
|
try {
|
||||||
|
authzClient.authorization("taro", "password").authorize(new AuthorizationRequest(ticket));
|
||||||
|
fail("Should fail.");
|
||||||
|
} catch (AuthorizationDeniedException ignore) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access Resource D with taro.
|
||||||
|
request = new PermissionRequest("Resource D");
|
||||||
|
ticket = authzClient.protection().permission().create(request).getTicket();
|
||||||
|
try {
|
||||||
|
authzClient.authorization("taro", "password").authorize(new AuthorizationRequest(ticket));
|
||||||
|
fail("Should fail.");
|
||||||
|
} catch (AuthorizationDeniedException ignore) {
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private AuthzClient getAuthzClient() {
|
private AuthzClient getAuthzClient() {
|
||||||
return AuthzClient.create(getClass().getResourceAsStream("/authorization-test/default-keycloak.json"));
|
return AuthzClient.create(getClass().getResourceAsStream("/authorization-test/default-keycloak.json"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ProtocolMapperRepresentation addClaimMapper(String name, Map<String, String> claims) {
|
||||||
|
ProtocolMapperRepresentation userAttrBarProtocolMapper = new ProtocolMapperRepresentation();
|
||||||
|
userAttrBarProtocolMapper.setName(name);
|
||||||
|
userAttrBarProtocolMapper.setProtocolMapper(UserAttributeMapper.PROVIDER_ID);
|
||||||
|
userAttrBarProtocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||||
|
Map<String, String> configBar = new HashMap<>(claims);
|
||||||
|
configBar.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
|
||||||
|
configBar.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
|
||||||
|
configBar.put(OIDCAttributeMapperHelper.JSON_TYPE, "String");
|
||||||
|
userAttrBarProtocolMapper.setConfig(configBar);
|
||||||
|
return userAttrBarProtocolMapper;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue