From 0ad1a9ef9ee5ac9a162e7bd8721601bc927db460 Mon Sep 17 00:00:00 2001
From: Matthias Piepkorn <mpiepk@gmail.com>
Date: Sun, 29 Jan 2017 11:01:01 +0000
Subject: [PATCH] Add more attribute mappers, cleanup existing mappers

---
 src/main/java/org/keycloak/protocol/cas/mappers/GroupMembershipMapper.java         |   30 +--
 src/main/java/org/keycloak/protocol/cas/mappers/CASAttributeMapperHelper.java      |   28 +++
 src/main/java/org/keycloak/protocol/cas/mappers/HardcodedClaim.java                |    8 
 src/main/java/org/keycloak/protocol/cas/mappers/UserRealmRoleMappingMapper.java    |   67 +++++++
 src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeMapper.java           |   26 --
 src/main/java/org/keycloak/protocol/cas/mappers/AbstractUserRoleMappingMapper.java |   98 ++++++++++
 src/main/java/org/keycloak/protocol/cas/mappers/UserPropertyMapper.java            |   25 --
 src/main/java/org/keycloak/protocol/cas/mappers/UserSessionNoteMapper.java         |   73 ++++++++
 src/main/java/org/keycloak/protocol/cas/mappers/AbstractCASProtocolMapper.java     |   16 +
 src/main/java/org/keycloak/protocol/cas/utils/AttributesMapAdapter.java            |    5 
 src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper          |    3 
 src/main/java/org/keycloak/protocol/cas/mappers/UserClientRoleMappingMapper.java   |  115 ++++++++++++
 src/main/java/org/keycloak/protocol/cas/mappers/FullNameMapper.java                |   22 --
 13 files changed, 427 insertions(+), 89 deletions(-)

diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/AbstractCASProtocolMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/AbstractCASProtocolMapper.java
index 2c19433..6838f6d 100644
--- a/src/main/java/org/keycloak/protocol/cas/mappers/AbstractCASProtocolMapper.java
+++ b/src/main/java/org/keycloak/protocol/cas/mappers/AbstractCASProtocolMapper.java
@@ -3,8 +3,12 @@
 import org.keycloak.Config;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.protocol.ProtocolMapper;
 import org.keycloak.protocol.cas.CASLoginProtocol;
+import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
+
+import java.util.Map;
 
 public abstract class AbstractCASProtocolMapper implements ProtocolMapper, CASAttributeMapper {
     public static final String TOKEN_MAPPER_CATEGORY = "Token mapper";
@@ -35,4 +39,16 @@
     public String getDisplayCategory() {
         return TOKEN_MAPPER_CATEGORY;
     }
+
+    protected void setMappedAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, Object attributeValue) {
+        setPlainAttribute(attributes, mappingModel, OIDCAttributeMapperHelper.mapAttributeValue(mappingModel, attributeValue));
+    }
+
+    protected void setPlainAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, Object attributeValue) {
+        String protocolClaim = mappingModel.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME);
+        if (protocolClaim == null || attributeValue == null) {
+            return;
+        }
+        attributes.put(protocolClaim, attributeValue);
+    }
 }
diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/AbstractUserRoleMappingMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/AbstractUserRoleMappingMapper.java
new file mode 100644
index 0000000..93f59f1
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/mappers/AbstractUserRoleMappingMapper.java
@@ -0,0 +1,98 @@
+/*
+ * 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.cas.mappers;
+
+import org.keycloak.models.*;
+import org.keycloak.models.utils.RoleUtils;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * 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 AbstractCASProtocolMapper {
+
+    /**
+     * Returns a stream with roles that come from:
+     * <ul>
+     * <li>Direct assignment of the role to the user</li>
+     * <li>Direct assignment of the role to any group of the user or any of its parent group</li>
+     * <li>Composite roles are expanded recursively, the composite role itself is also contained in the returned stream</li>
+     * </ul>
+     * @param user User to enumerate the roles for
+     */
+    public Stream<RoleModel> getAllUserRolesStream(UserModel user) {
+        return Stream.concat(
+          user.getRoleMappings().stream(),
+          user.getGroups().stream()
+            .flatMap(this::groupAndItsParentsStream)
+            .flatMap(g -> g.getRoleMappings().stream()))
+          .flatMap(RoleUtils::expandCompositeRolesStream);
+    }
+
+    /**
+     * Returns stream of the given group and its parents (recursively).
+     * @param group
+     * @return
+     */
+    private Stream<GroupModel> groupAndItsParentsStream(GroupModel group) {
+        Stream.Builder<GroupModel> sb = Stream.builder();
+        while (group != null) {
+            sb.add(group);
+            group = group.getParent();
+        }
+        return sb.build();
+    }
+
+    /**
+     * 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.
+     */
+    protected void setAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, UserSessionModel userSession,
+                                       Predicate<RoleModel> restriction, String prefix) {
+        String rolePrefix = prefix == null ? "" : prefix;
+        UserModel user = userSession.getUser();
+
+        // get a set of all realm roles assigned to the user or its group
+        Stream<RoleModel> clientUserRoles = getAllUserRolesStream(user).filter(restriction);
+
+        boolean dontLimitScope = userSession.getClientSessions().stream().anyMatch(cs -> cs.getClient().isFullScopeAllowed());
+        if (! dontLimitScope) {
+            Set<RoleModel> clientRoles = userSession.getClientSessions().stream()
+              .flatMap(cs -> cs.getClient().getScopeMappings().stream())
+              .collect(Collectors.toSet());
+
+            clientUserRoles = clientUserRoles.filter(clientRoles::contains);
+        }
+
+        Set<String> realmRoleNames = clientUserRoles
+          .map(m -> rolePrefix + m.getName())
+          .collect(Collectors.toSet());
+
+        setPlainAttribute(attributes, mappingModel, realmRoleNames);
+    }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/CASAttributeMapperHelper.java b/src/main/java/org/keycloak/protocol/cas/mappers/CASAttributeMapperHelper.java
new file mode 100644
index 0000000..53ba5d2
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/mappers/CASAttributeMapperHelper.java
@@ -0,0 +1,28 @@
+package org.keycloak.protocol.cas.mappers;
+
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.protocol.cas.CASLoginProtocol;
+import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class CASAttributeMapperHelper {
+    public static ProtocolMapperModel createClaimMapper(String name,
+                                                        String tokenClaimName, String claimType,
+                                                        boolean consentRequired, String consentText,
+                                                        String mapperId) {
+        ProtocolMapperModel mapper = new ProtocolMapperModel();
+        mapper.setName(name);
+        mapper.setProtocolMapper(mapperId);
+        mapper.setProtocol(CASLoginProtocol.LOGIN_PROTOCOL);
+        mapper.setConsentRequired(consentRequired);
+        mapper.setConsentText(consentText);
+        Map<String, String> config = new HashMap<String, String>();
+        config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, tokenClaimName);
+        config.put(OIDCAttributeMapperHelper.JSON_TYPE, claimType);
+        mapper.setConfig(config);
+        return mapper;
+    }
+
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/FullNameMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/FullNameMapper.java
index e66da7d..aef4b51 100644
--- a/src/main/java/org/keycloak/protocol/cas/mappers/FullNameMapper.java
+++ b/src/main/java/org/keycloak/protocol/cas/mappers/FullNameMapper.java
@@ -3,16 +3,12 @@
 import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
-import org.keycloak.protocol.cas.CASLoginProtocol;
 import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
 import org.keycloak.provider.ProviderConfigProperty;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-
-import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME;
 
 public class FullNameMapper extends AbstractCASProtocolMapper {
     private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
@@ -47,26 +43,14 @@
     @Override
     public void setAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
         UserModel user = userSession.getUser();
-        String protocolClaim = mappingModel.getConfig().get(TOKEN_CLAIM_NAME);
-        if (protocolClaim == null) {
-            return;
-        }
         String first = user.getFirstName() == null ? "" : user.getFirstName() + " ";
         String last = user.getLastName() == null ? "" : user.getLastName();
-        attributes.put(protocolClaim, first + last);
+        setMappedAttribute(attributes, mappingModel, first + last);
     }
 
     public static ProtocolMapperModel create(String name, String tokenClaimName,
                                              boolean consentRequired, String consentText) {
-        ProtocolMapperModel mapper = new ProtocolMapperModel();
-        mapper.setName(name);
-        mapper.setProtocolMapper(PROVIDER_ID);
-        mapper.setProtocol(CASLoginProtocol.LOGIN_PROTOCOL);
-        mapper.setConsentRequired(consentRequired);
-        mapper.setConsentText(consentText);
-        Map<String, String> config = new HashMap<String, String>();
-        config.put(TOKEN_CLAIM_NAME, tokenClaimName);
-        mapper.setConfig(config);
-        return mapper;
+        return CASAttributeMapperHelper.createClaimMapper(name, tokenClaimName,
+                "String", consentRequired, consentText, PROVIDER_ID);
     }
 }
diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/GroupMembershipMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/GroupMembershipMapper.java
index 6d9c8ac..a6db974 100644
--- a/src/main/java/org/keycloak/protocol/cas/mappers/GroupMembershipMapper.java
+++ b/src/main/java/org/keycloak/protocol/cas/mappers/GroupMembershipMapper.java
@@ -4,19 +4,23 @@
 import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.utils.ModelToRepresentation;
-import org.keycloak.protocol.cas.CASLoginProtocol;
 import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
 import org.keycloak.provider.ProviderConfigProperty;
 
-import java.util.*;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
 
 public class GroupMembershipMapper extends AbstractCASProtocolMapper {
     private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
 
+    private static final String FULL_PATH = "full.path";
+
     static {
         OIDCAttributeMapperHelper.addTokenClaimNameConfig(configProperties);
         ProviderConfigProperty property1 = new ProviderConfigProperty();
-        property1.setName("full.path");
+        property1.setName(FULL_PATH);
         property1.setLabel("Full group path");
         property1.setType(ProviderConfigProperty.BOOLEAN_TYPE);
         property1.setDefaultValue("true");
@@ -58,28 +62,18 @@
                 membership.add(group.getName());
             }
         }
-        String protocolClaim = mappingModel.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME);
-
-        attributes.put(protocolClaim, membership);
+        setPlainAttribute(attributes, mappingModel, membership);
     }
 
     public static boolean useFullPath(ProtocolMapperModel mappingModel) {
-        return "true".equals(mappingModel.getConfig().get("full.path"));
+        return "true".equals(mappingModel.getConfig().get(FULL_PATH));
     }
 
     public static ProtocolMapperModel create(String name, String tokenClaimName,
                                              boolean consentRequired, String consentText, boolean fullPath) {
-        ProtocolMapperModel mapper = new ProtocolMapperModel();
-        mapper.setName(name);
-        mapper.setProtocolMapper(PROVIDER_ID);
-        mapper.setProtocol(CASLoginProtocol.LOGIN_PROTOCOL);
-        mapper.setConsentRequired(consentRequired);
-        mapper.setConsentText(consentText);
-        Map<String, String> config = new HashMap<String, String>();
-        config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, tokenClaimName);
-        config.put("full.path", Boolean.toString(fullPath));
-        mapper.setConfig(config);
-
+        ProtocolMapperModel mapper = CASAttributeMapperHelper.createClaimMapper(name, tokenClaimName,
+                "String", consentRequired, consentText, PROVIDER_ID);
+        mapper.getConfig().put(FULL_PATH, Boolean.toString(fullPath));
         return mapper;
     }
 }
diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/HardcodedClaim.java b/src/main/java/org/keycloak/protocol/cas/mappers/HardcodedClaim.java
index df7000c..42c7535 100644
--- a/src/main/java/org/keycloak/protocol/cas/mappers/HardcodedClaim.java
+++ b/src/main/java/org/keycloak/protocol/cas/mappers/HardcodedClaim.java
@@ -54,13 +54,7 @@
 
     @Override
     public void setAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
-        String protocolClaim = mappingModel.getConfig().get(TOKEN_CLAIM_NAME);
-        if (protocolClaim == null) {
-            return;
-        }
-        String attributeValue = mappingModel.getConfig().get(CLAIM_VALUE);
-        if (attributeValue == null) return;
-        attributes.put(protocolClaim, OIDCAttributeMapperHelper.mapAttributeValue(mappingModel, attributeValue));
+        setMappedAttribute(attributes, mappingModel, mappingModel.getConfig().get(CLAIM_VALUE));
     }
 
 }
diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeMapper.java
index 3637069..19173c2 100644
--- a/src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeMapper.java
+++ b/src/main/java/org/keycloak/protocol/cas/mappers/UserAttributeMapper.java
@@ -5,17 +5,12 @@
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
 import org.keycloak.protocol.ProtocolMapperUtils;
-import org.keycloak.protocol.cas.CASLoginProtocol;
 import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
 import org.keycloak.provider.ProviderConfigProperty;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-
-import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.JSON_TYPE;
-import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME;
 
 public class UserAttributeMapper extends AbstractCASProtocolMapper {
     private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
@@ -66,33 +61,20 @@
     @Override
     public void setAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
         UserModel user = userSession.getUser();
-        String protocolClaim = mappingModel.getConfig().get(TOKEN_CLAIM_NAME);
-        if (protocolClaim == null) {
-            return;
-        }
         String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE);
         List<String> attributeValue = KeycloakModelUtils.resolveAttribute(user, attributeName);
-        if (attributeValue == null) return;
-        attributes.put(protocolClaim, OIDCAttributeMapperHelper.mapAttributeValue(mappingModel, attributeValue));
+        setMappedAttribute(attributes, mappingModel, attributeValue);
     }
 
     public static ProtocolMapperModel create(String name, String userAttribute,
                                              String tokenClaimName, String claimType,
                                              boolean consentRequired, String consentText, boolean multivalued) {
-        ProtocolMapperModel mapper = new ProtocolMapperModel();
-        mapper.setName(name);
-        mapper.setProtocolMapper(PROVIDER_ID);
-        mapper.setProtocol(CASLoginProtocol.LOGIN_PROTOCOL);
-        mapper.setConsentRequired(consentRequired);
-        mapper.setConsentText(consentText);
-        Map<String, String> config = new HashMap<String, String>();
-        config.put(ProtocolMapperUtils.USER_ATTRIBUTE, userAttribute);
-        config.put(TOKEN_CLAIM_NAME, tokenClaimName);
-        config.put(JSON_TYPE, claimType);
+        ProtocolMapperModel mapper = CASAttributeMapperHelper.createClaimMapper(name, tokenClaimName,
+                claimType, consentRequired, consentText, PROVIDER_ID);
+        mapper.getConfig().put(ProtocolMapperUtils.USER_ATTRIBUTE, userAttribute);
         if (multivalued) {
             mapper.getConfig().put(ProtocolMapperUtils.MULTIVALUED, "true");
         }
-        mapper.setConfig(config);
         return mapper;
     }
 }
diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/UserClientRoleMappingMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/UserClientRoleMappingMapper.java
new file mode 100644
index 0000000..15ff8ac
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/mappers/UserClientRoleMappingMapper.java
@@ -0,0 +1,115 @@
+package org.keycloak.protocol.cas.mappers;
+
+import org.keycloak.models.*;
+import org.keycloak.protocol.ProtocolMapperUtils;
+import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.*;
+import java.util.function.Predicate;
+
+public class UserClientRoleMappingMapper extends AbstractUserRoleMappingMapper {
+
+    public static final String PROVIDER_ID = "cas-usermodel-client-role-mapper";
+
+    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();
+
+    static {
+
+        ProviderConfigProperty clientId = new ProviderConfigProperty();
+        clientId.setName(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID);
+        clientId.setLabel(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID_LABEL);
+        clientId.setHelpText(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID_HELP_TEXT);
+        clientId.setType(ProviderConfigProperty.CLIENT_LIST_TYPE);
+        CONFIG_PROPERTIES.add(clientId);
+
+        ProviderConfigProperty clientRolePrefix = new ProviderConfigProperty();
+        clientRolePrefix.setName(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX);
+        clientRolePrefix.setLabel(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX_LABEL);
+        clientRolePrefix.setHelpText(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX_HELP_TEXT);
+        clientRolePrefix.setType(ProviderConfigProperty.STRING_TYPE);
+        CONFIG_PROPERTIES.add(clientRolePrefix);
+
+        OIDCAttributeMapperHelper.addTokenClaimNameConfig(CONFIG_PROPERTIES);
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return CONFIG_PROPERTIES;
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "User Client Role";
+    }
+
+    @Override
+    public String getDisplayCategory() {
+        return TOKEN_MAPPER_CATEGORY;
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Map a user client role to a token claim.";
+    }
+
+    @Override
+    public void setAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
+        String clientId = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID);
+        String rolePrefix = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX);
+
+        setAttribute(attributes, mappingModel, userSession, getClientRoleFilter(clientId, userSession), rolePrefix);
+    }
+
+    private static Predicate<RoleModel> getClientRoleFilter(String clientId, UserSessionModel userSession) {
+        if (clientId == null) {
+            return RoleModel::isClientRole;
+        }
+
+        RealmModel clientRealm = userSession.getRealm();
+        ClientModel client = clientRealm.getClientByClientId(clientId.trim());
+
+        if (client == null) {
+            return RoleModel::isClientRole;
+        }
+
+        ClientTemplateModel template = client.getClientTemplate();
+        boolean useTemplateScope = template != null && client.useTemplateScope();
+        boolean fullScopeAllowed = (useTemplateScope && template.isFullScopeAllowed()) || client.isFullScopeAllowed();
+
+        Set<RoleModel> clientRoleMappings = client.getRoles();
+        if (fullScopeAllowed) {
+            return clientRoleMappings::contains;
+        }
+
+        Set<RoleModel> scopeMappings = new HashSet<>();
+
+        if (useTemplateScope) {
+            Set<RoleModel> templateScopeMappings = template.getScopeMappings();
+            if (templateScopeMappings != null) {
+                scopeMappings.addAll(templateScopeMappings);
+            }
+        }
+
+        Set<RoleModel> clientScopeMappings = client.getScopeMappings();
+        if (clientScopeMappings != null) {
+            scopeMappings.addAll(clientScopeMappings);
+        }
+
+        return role -> clientRoleMappings.contains(role) && scopeMappings.contains(role);
+    }
+
+    public static ProtocolMapperModel create(String clientId, String clientRolePrefix,
+                                             String name, String tokenClaimName) {
+        ProtocolMapperModel mapper = CASAttributeMapperHelper.createClaimMapper(name, tokenClaimName,
+                "String", true, name, PROVIDER_ID);
+        mapper.getConfig().put(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_CLIENT_ID, clientId);
+        mapper.getConfig().put(ProtocolMapperUtils.USER_MODEL_CLIENT_ROLE_MAPPING_ROLE_PREFIX, clientRolePrefix);
+        return mapper;
+    }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/UserPropertyMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/UserPropertyMapper.java
index 057636d..b299b27 100644
--- a/src/main/java/org/keycloak/protocol/cas/mappers/UserPropertyMapper.java
+++ b/src/main/java/org/keycloak/protocol/cas/mappers/UserPropertyMapper.java
@@ -4,17 +4,12 @@
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
 import org.keycloak.protocol.ProtocolMapperUtils;
-import org.keycloak.protocol.cas.CASLoginProtocol;
 import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
 import org.keycloak.provider.ProviderConfigProperty;
 
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-
-import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.JSON_TYPE;
-import static org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME;
 
 public class UserPropertyMapper extends AbstractCASProtocolMapper {
     private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
@@ -57,29 +52,17 @@
     @Override
     public void setAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
         UserModel user = userSession.getUser();
-        String protocolClaim = mappingModel.getConfig().get(TOKEN_CLAIM_NAME);
-        if (protocolClaim == null) {
-            return;
-        }
         String propertyName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE);
         String propertyValue = ProtocolMapperUtils.getUserModelValue(user, propertyName);
-        attributes.put(protocolClaim, OIDCAttributeMapperHelper.mapAttributeValue(mappingModel, propertyValue));
+        setMappedAttribute(attributes, mappingModel, propertyValue);
     }
 
     public static ProtocolMapperModel create(String name, String userAttribute,
                                              String tokenClaimName, String claimType,
                                              boolean consentRequired, String consentText) {
-        ProtocolMapperModel mapper = new ProtocolMapperModel();
-        mapper.setName(name);
-        mapper.setProtocolMapper(PROVIDER_ID);
-        mapper.setProtocol(CASLoginProtocol.LOGIN_PROTOCOL);
-        mapper.setConsentRequired(consentRequired);
-        mapper.setConsentText(consentText);
-        Map<String, String> config = new HashMap<String, String>();
-        config.put(ProtocolMapperUtils.USER_ATTRIBUTE, userAttribute);
-        config.put(TOKEN_CLAIM_NAME, tokenClaimName);
-        config.put(JSON_TYPE, claimType);
-        mapper.setConfig(config);
+        ProtocolMapperModel mapper = CASAttributeMapperHelper.createClaimMapper(name, tokenClaimName,
+                claimType, consentRequired, consentText, PROVIDER_ID);
+        mapper.getConfig().put(ProtocolMapperUtils.USER_ATTRIBUTE, userAttribute);
         return mapper;
     }
 }
diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/UserRealmRoleMappingMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/UserRealmRoleMappingMapper.java
new file mode 100644
index 0000000..117264a
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/mappers/UserRealmRoleMappingMapper.java
@@ -0,0 +1,67 @@
+package org.keycloak.protocol.cas.mappers;
+
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.protocol.ProtocolMapperUtils;
+import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class UserRealmRoleMappingMapper extends AbstractUserRoleMappingMapper {
+    public static final String PROVIDER_ID = "cas-usermodel-realm-role-mapper";
+
+    private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();
+
+    static {
+
+        ProviderConfigProperty realmRolePrefix = new ProviderConfigProperty();
+        realmRolePrefix.setName(ProtocolMapperUtils.USER_MODEL_REALM_ROLE_MAPPING_ROLE_PREFIX);
+        realmRolePrefix.setLabel(ProtocolMapperUtils.USER_MODEL_REALM_ROLE_MAPPING_ROLE_PREFIX_LABEL);
+        realmRolePrefix.setHelpText(ProtocolMapperUtils.USER_MODEL_REALM_ROLE_MAPPING_ROLE_PREFIX_HELP_TEXT);
+        realmRolePrefix.setType(ProviderConfigProperty.STRING_TYPE);
+        CONFIG_PROPERTIES.add(realmRolePrefix);
+
+        OIDCAttributeMapperHelper.addTokenClaimNameConfig(CONFIG_PROPERTIES);
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return CONFIG_PROPERTIES;
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "User Realm Role";
+    }
+
+    @Override
+    public String getDisplayCategory() {
+        return TOKEN_MAPPER_CATEGORY;
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Map a user realm role to a token claim.";
+    }
+
+    @Override
+    public void setAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
+        String rolePrefix = mappingModel.getConfig().get(ProtocolMapperUtils.USER_MODEL_REALM_ROLE_MAPPING_ROLE_PREFIX);
+        setAttribute(attributes, mappingModel, userSession, role -> ! role.isClientRole(), rolePrefix);
+    }
+
+    public static ProtocolMapperModel create(String realmRolePrefix, String name, String tokenClaimName) {
+        ProtocolMapperModel mapper = CASAttributeMapperHelper.createClaimMapper(name, tokenClaimName,
+                "String", true, name, PROVIDER_ID);
+        mapper.getConfig().put(ProtocolMapperUtils.USER_MODEL_REALM_ROLE_MAPPING_ROLE_PREFIX, realmRolePrefix);
+        return mapper;
+    }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/mappers/UserSessionNoteMapper.java b/src/main/java/org/keycloak/protocol/cas/mappers/UserSessionNoteMapper.java
new file mode 100644
index 0000000..5c2881a
--- /dev/null
+++ b/src/main/java/org/keycloak/protocol/cas/mappers/UserSessionNoteMapper.java
@@ -0,0 +1,73 @@
+package org.keycloak.protocol.cas.mappers;
+
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.protocol.ProtocolMapperUtils;
+import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class UserSessionNoteMapper extends AbstractCASProtocolMapper {
+
+    private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+
+    static {
+        ProviderConfigProperty property;
+        property = new ProviderConfigProperty();
+        property.setName(ProtocolMapperUtils.USER_SESSION_NOTE);
+        property.setLabel(ProtocolMapperUtils.USER_SESSION_MODEL_NOTE_LABEL);
+        property.setHelpText(ProtocolMapperUtils.USER_SESSION_MODEL_NOTE_HELP_TEXT);
+        property.setType(ProviderConfigProperty.STRING_TYPE);
+        configProperties.add(property);
+        OIDCAttributeMapperHelper.addTokenClaimNameConfig(configProperties);
+        OIDCAttributeMapperHelper.addJsonTypeConfig(configProperties);
+    }
+
+    public static final String PROVIDER_ID = "cas-usersessionmodel-note-mapper";
+
+
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return configProperties;
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "User Session Note";
+    }
+
+    @Override
+    public String getDisplayCategory() {
+        return TOKEN_MAPPER_CATEGORY;
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Map a custom user session note to a token claim.";
+    }
+
+    @Override
+    public void setAttribute(Map<String, Object> attributes, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
+        String noteName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_SESSION_NOTE);
+        String noteValue = userSession.getNote(noteName);
+        if (noteValue == null) return;
+        setMappedAttribute(attributes, mappingModel, noteValue);
+    }
+
+    public static ProtocolMapperModel create(String name,
+                                             String userSessionNote,
+                                             String tokenClaimName, String jsonType,
+                                             boolean consentRequired, String consentText) {
+        ProtocolMapperModel mapper = CASAttributeMapperHelper.createClaimMapper(name, tokenClaimName,
+                jsonType, consentRequired, consentText, PROVIDER_ID);
+        mapper.getConfig().put(ProtocolMapperUtils.USER_SESSION_NOTE, userSessionNote);
+        return mapper;
+    }
+}
diff --git a/src/main/java/org/keycloak/protocol/cas/utils/AttributesMapAdapter.java b/src/main/java/org/keycloak/protocol/cas/utils/AttributesMapAdapter.java
index bf9b148..ca8f7ed 100644
--- a/src/main/java/org/keycloak/protocol/cas/utils/AttributesMapAdapter.java
+++ b/src/main/java/org/keycloak/protocol/cas/utils/AttributesMapAdapter.java
@@ -10,6 +10,7 @@
 import javax.xml.bind.annotation.adapters.XmlAdapter;
 import javax.xml.namespace.QName;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
@@ -37,8 +38,8 @@
         AttributeWrapperType(Map<String, Object> attributes) {
             this.elements = new ArrayList<>();
             for (Map.Entry<String, Object> entry : attributes.entrySet()) {
-                if (entry.getValue() instanceof List) {
-                    for (Object item : ((List) entry.getValue())) {
+                if (entry.getValue() instanceof Collection) {
+                    for (Object item : ((Collection) entry.getValue())) {
                         addElement(entry.getKey(), item);
                     }
                 } else {
diff --git a/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
index 46409eb..353f1c8 100644
--- a/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
+++ b/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper
@@ -19,4 +19,7 @@
 org.keycloak.protocol.cas.mappers.GroupMembershipMapper
 org.keycloak.protocol.cas.mappers.HardcodedClaim
 org.keycloak.protocol.cas.mappers.UserAttributeMapper
+org.keycloak.protocol.cas.mappers.UserClientRoleMappingMapper
 org.keycloak.protocol.cas.mappers.UserPropertyMapper
+org.keycloak.protocol.cas.mappers.UserRealmRoleMappingMapper
+org.keycloak.protocol.cas.mappers.UserSessionNoteMapper

--
Gitblit v1.9.1