AbstractUserRoleMappingMapper.java
/*
* Copyright 2016 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.protocol.oidc.mappers;
import static org.keycloak.utils.JsonUtils.splitClaimPath;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Base class for mapping of user role mappings to an ID and Access Token claim.
*
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
*/
abstract class AbstractUserRoleMappingMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper, UserInfoTokenMapper {
@Override
public int getPriority() {
return ProtocolMapperUtils.PRIORITY_ROLE_MAPPER;
}
/**
* Retrieves all roles of the current user based on direct roles set to the user, its groups and their parent groups.
* Then it recursively expands all composite roles, and restricts according to the given predicate {@code restriction}.
* If the current client sessions is restricted (i.e. no client found in active user session has full scope allowed),
* the final list of roles is also restricted by the client scope. Finally, the list is mapped to the token into
* a claim.
*
* @param token
* @param mappingModel
* @param rolesToAdd
* @param clientId
* @param prefix
*/
protected static void setClaim(IDToken token, ProtocolMapperModel mappingModel, Set<String> rolesToAdd,
String clientId, String prefix) {
Set<String> realmRoleNames;
if (prefix != null && !prefix.isEmpty()) {
realmRoleNames = rolesToAdd.stream()
.map(roleName -> prefix + roleName)
.collect(Collectors.toSet());
} else {
realmRoleNames = rolesToAdd;
}
mapClaim(token, mappingModel, realmRoleNames, clientId);
}
private static final Pattern CLIENT_ID_PATTERN = Pattern.compile("\\$\\{client_id\\}");
private static final Pattern DOT_PATTERN = Pattern.compile("\\.");
private static final String DOT_REPLACEMENT = "\\\\\\\\.";
private static void mapClaim(IDToken token, ProtocolMapperModel mappingModel, Object attributeValue, String clientId) {
attributeValue = OIDCAttributeMapperHelper.mapAttributeValue(mappingModel, attributeValue);
if (attributeValue == null) return;
String protocolClaim = mappingModel.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME);
if (protocolClaim == null) {
return;
}
if (clientId != null) {
// case when clientId contains dots
clientId = DOT_PATTERN.matcher(clientId).replaceAll(DOT_REPLACEMENT);
Matcher matcher = CLIENT_ID_PATTERN.matcher(protocolClaim);
if (matcher.find()) {
protocolClaim = matcher.replaceAll(clientId);
}
if (!(protocolClaim.endsWith("roles") || protocolClaim.startsWith(clientId) || protocolClaim.endsWith(clientId))) {
// the claim name does not reference the current client, do not map roles
// or if the claim does not end with roles suffix, do not map roles.
// the role suffix is used to move roles to a single location other than the default location (e.g.: realm_access and resource_access claims)
return;
}
}
List<String> split = splitClaimPath(protocolClaim);
// Special case
if (checkAccessToken(token, split, attributeValue)) {
return;
}
final int length = split.size();
int i = 0;
Map<String, Object> jsonObject = token.getOtherClaims();
for (String component : split) {
i++;
if (i == length) {
// Case when we want to add to existing set of roles
Object last = jsonObject.get(component);
if (last instanceof Collection && attributeValue instanceof Collection) {
((Collection) last).addAll((Collection) attributeValue);
} else {
jsonObject.put(component, attributeValue);
}
} else {
Map<String, Object> nested = (Map<String, Object>)jsonObject.get(component);
if (nested == null) {
nested = new HashMap<>();
jsonObject.put(component, nested);
}
jsonObject = nested;
}
}
}
// Special case when roles are put to the access token via "realmAcces, resourceAccess" properties
private static boolean checkAccessToken(IDToken idToken, List<String> path, Object attributeValue) {
if (!(idToken instanceof AccessToken)) {
return false;
}
if (!(attributeValue instanceof Collection)) {
return false;
}
Collection<String> roles = (Collection<String>) attributeValue;
AccessToken token = (AccessToken) idToken;
AccessToken.Access access = null;
if (path.size() == 2 && "realm_access".equals(path.get(0)) && "roles".equals(path.get(1))) {
access = token.getRealmAccess();
if (access == null) {
access = new AccessToken.Access();
token.setRealmAccess(access);
}
} else if (path.size() == 3 && "resource_access".equals(path.get(0)) && "roles".equals(path.get(2))) {
String clientId = path.get(1);
access = token.addAccess(clientId);
} else {
return false;
}
for (String role : roles) {
access.addRole(role);
}
return true;
}
}