diff --git a/.gitignore b/.gitignore
index ea8f090..108912c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,8 @@
# Copyright 2021 Luca Filipozzi. Some rights reserved. See LICENCE.
+# direnv
+.envrc
+
# idea
.idea/
*.iml
diff --git a/bundle/pom.xml b/bundle/pom.xml
new file mode 100644
index 0000000..9642839
--- /dev/null
+++ b/bundle/pom.xml
@@ -0,0 +1,47 @@
+
+
+
+ 4.0.0
+
+
+ com.github.lucafilipozzi
+ keycloak-regex-mapper
+ ${revision}
+
+
+ keycloak-regex-mapper-bundle
+ ear
+ bundle
+
+
+
+ ${project.parent.groupId}
+ ${project.parent.artifactId}-module
+
+
+
+
+
+ ${project.parent.artifactId}-${revision}
+
+
+ org.apache.maven.plugins
+ maven-ear-plugin
+
+ ${project.parent.artifactId}
+ ${project.parent.description}
+
+
+ ${project.groupId}
+ ${project.parent.artifactId}-module
+
+ ${project.parent.groupId}-${project.parent.artifactId}-${revision}.jar
+ true
+
+
+
+
+
+
+
diff --git a/bundle/src/main/application/META-INF/jboss-deployment-structure.xml b/bundle/src/main/application/META-INF/jboss-deployment-structure.xml
new file mode 100644
index 0000000..30dc55c
--- /dev/null
+++ b/bundle/src/main/application/META-INF/jboss-deployment-structure.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/module/pom.xml b/module/pom.xml
new file mode 100644
index 0000000..78eb47d
--- /dev/null
+++ b/module/pom.xml
@@ -0,0 +1,85 @@
+
+
+
+ 4.0.0
+
+
+ com.github.lucafilipozzi
+ keycloak-regex-mapper
+ ${revision}
+
+
+ keycloak-regex-mapper-module
+ jar
+ module
+
+
+
+
+ com.google.guava
+ guava
+
+
+ org.jboss.logging
+ jboss-logging
+
+
+ org.keycloak
+ keycloak-common
+
+
+ org.keycloak
+ keycloak-core
+
+
+ org.keycloak
+ keycloak-saml-core-public
+
+
+ org.keycloak
+ keycloak-server-spi
+
+
+ org.keycloak
+ keycloak-server-spi-private
+
+
+ org.keycloak
+ keycloak-services
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+
+
+ org.owasp
+ dependency-check-maven
+
+
+ org.codehaus.mojo
+ versions-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ org.keycloak:keycloak-common
+ org.keycloak:keycloak-core
+ org.keycloak:keycloak-server-spi
+
+
+
+
+ org.basepom.maven
+ duplicate-finder-maven-plugin
+
+
+
+
diff --git a/module/src/main/java/com/github/lucafilipozzi/keycloak/broker/oidc/mappers/RegexRealmAndClientRoleClaimMapper.java b/module/src/main/java/com/github/lucafilipozzi/keycloak/broker/oidc/mappers/RegexRealmAndClientRoleClaimMapper.java
new file mode 100644
index 0000000..dae5a23
--- /dev/null
+++ b/module/src/main/java/com/github/lucafilipozzi/keycloak/broker/oidc/mappers/RegexRealmAndClientRoleClaimMapper.java
@@ -0,0 +1,138 @@
+// Copyright 2021 Luca Filipozzi. Some rights reserved. See LICENSE.
+
+package com.github.lucafilipozzi.keycloak.broker.oidc.mappers;
+
+import com.github.lucafilipozzi.keycloak.broker.util.RegexRealmAndClientRoleMapperUtil;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.jboss.logging.Logger;
+import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
+import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
+import org.keycloak.broker.oidc.mappers.AbstractClaimMapper;
+import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.models.IdentityProviderMapperModel;
+import org.keycloak.models.IdentityProviderSyncMode;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.ProviderConfigProperty;
+
+/**
+ * Map a claim to realm and client roles via regex.
+ */
+public class RegexRealmAndClientRoleClaimMapper extends AbstractClaimMapper {
+
+ public static final String PROVIDER_ID = "lucafilipozzi-oidc-regex-claim-mapper";
+
+ public static final String OIDC_CLAIM_NAME = "oidc-claim-name";
+
+ protected static final String[] COMPATIBLE_PROVIDERS = { KeycloakOIDCIdentityProviderFactory.PROVIDER_ID, OIDCIdentityProviderFactory.PROVIDER_ID };
+
+ private static final Set IDENTITY_PROVIDER_SYNC_MODES = new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values()));
+
+ private static final Logger LOG = Logger.getLogger(RegexRealmAndClientRoleClaimMapper.class);
+
+ private static final List configProperties = new ArrayList<>();
+
+ static {
+ ProviderConfigProperty oidcClaimNameConfigProperty = new ProviderConfigProperty();
+ oidcClaimNameConfigProperty.setName(OIDC_CLAIM_NAME);
+ oidcClaimNameConfigProperty.setLabel("OIDC claim name");
+ oidcClaimNameConfigProperty.setHelpText("name of OIDC claim to search");
+ oidcClaimNameConfigProperty.setType(ProviderConfigProperty.STRING_TYPE);
+ configProperties.add(oidcClaimNameConfigProperty);
+
+ ProviderConfigProperty clientRolesAttributeNameConfigProperty = new ProviderConfigProperty();
+ clientRolesAttributeNameConfigProperty.setName(RegexRealmAndClientRoleMapperUtil.CLIENT_ROLES_ATTRIBUTE_NAME);
+ clientRolesAttributeNameConfigProperty.setLabel("client roles attribute name");
+ clientRolesAttributeNameConfigProperty.setHelpText("only evaluate client roles having an attribute with this name");
+ clientRolesAttributeNameConfigProperty.setType(ProviderConfigProperty.STRING_TYPE);
+ configProperties.add(clientRolesAttributeNameConfigProperty);
+
+ ProviderConfigProperty clientRolesRegularExpressionConfigProperty = new ProviderConfigProperty();
+ clientRolesRegularExpressionConfigProperty.setName(RegexRealmAndClientRoleMapperUtil.CLIENT_ROLES_REGULAR_EXPRESSION);
+ clientRolesRegularExpressionConfigProperty.setLabel("client roles regular expression");
+ clientRolesRegularExpressionConfigProperty.setHelpText("regular expression to apply to the OIDC claim to extract client roles; must specify two named-capturing groups: client and role");
+ clientRolesRegularExpressionConfigProperty.setType(ProviderConfigProperty.STRING_TYPE);
+ configProperties.add(clientRolesRegularExpressionConfigProperty);
+
+ ProviderConfigProperty realmRolesAttributeNameConfigProperty = new ProviderConfigProperty();
+ realmRolesAttributeNameConfigProperty.setName(RegexRealmAndClientRoleMapperUtil.REALM_ROLES_ATTRIBUTE_NAME);
+ realmRolesAttributeNameConfigProperty.setLabel("realm roles attribute name");
+ realmRolesAttributeNameConfigProperty.setHelpText("only evaluate realm roles having an attribute with this name");
+ realmRolesAttributeNameConfigProperty.setType(ProviderConfigProperty.STRING_TYPE);
+ configProperties.add(realmRolesAttributeNameConfigProperty);
+
+ ProviderConfigProperty realmRolesRegularExpressionConfigProperty = new ProviderConfigProperty();
+ realmRolesRegularExpressionConfigProperty.setName(RegexRealmAndClientRoleMapperUtil.REALM_ROLES_REGULAR_EXPRESSION);
+ realmRolesRegularExpressionConfigProperty.setLabel("realm roles regular expression");
+ realmRolesRegularExpressionConfigProperty.setHelpText("regular expression to apply to the OIDC claim to extract realm roles; must specify one named-capturing groups: role");
+ realmRolesRegularExpressionConfigProperty.setType(ProviderConfigProperty.STRING_TYPE);
+ configProperties.add(realmRolesRegularExpressionConfigProperty);
+ }
+
+ @Override
+ public String[] getCompatibleProviders() {
+ return COMPATIBLE_PROVIDERS;
+ }
+
+ @Override
+ public List getConfigProperties() {
+ return configProperties;
+ }
+
+ @Override
+ public String getDisplayCategory() {
+ return "Role Importer";
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Regex Realm and Client Role Importer";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "implements regex realm and client role importer";
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) {
+ return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode);
+ }
+
+ @Override
+ public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapper, BrokeredIdentityContext context) {
+ LOG.trace("import user");
+ processUser(realm, user, mapper, context);
+ }
+
+ @Override
+ public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapper, BrokeredIdentityContext context) {
+ LOG.trace("update user");
+ processUser(realm, user, mapper, context);
+ }
+
+ private void processUser(RealmModel realm, UserModel user, IdentityProviderMapperModel mapper, BrokeredIdentityContext context) {
+ LOG.trace("process user");
+ String oidcClaimName = mapper.getConfig().getOrDefault(OIDC_CLAIM_NAME, "");
+ Set assertedValues;
+ Object claimValue = getClaimValue(context, oidcClaimName);
+ if (claimValue instanceof List) {
+ assertedValues = ((List>) claimValue).stream().map(String.class::cast).collect(Collectors.toSet());
+ } else {
+ assertedValues = Collections.emptySet();
+ }
+ RegexRealmAndClientRoleMapperUtil.processUser(realm, user, mapper, assertedValues);
+ }
+}
diff --git a/module/src/main/java/com/github/lucafilipozzi/keycloak/broker/saml/mappers/RegexRealmAndClientRoleAttributeMapper.java b/module/src/main/java/com/github/lucafilipozzi/keycloak/broker/saml/mappers/RegexRealmAndClientRoleAttributeMapper.java
new file mode 100644
index 0000000..88b509b
--- /dev/null
+++ b/module/src/main/java/com/github/lucafilipozzi/keycloak/broker/saml/mappers/RegexRealmAndClientRoleAttributeMapper.java
@@ -0,0 +1,138 @@
+// Copyright 2021 Luca Filipozzi. Some rights reserved. See LICENSE.
+
+package com.github.lucafilipozzi.keycloak.broker.saml.mappers;
+
+import com.github.lucafilipozzi.keycloak.broker.util.RegexRealmAndClientRoleMapperUtil;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.jboss.logging.Logger;
+import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
+import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.broker.saml.SAMLEndpoint;
+import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
+import org.keycloak.dom.saml.v2.assertion.AssertionType;
+import org.keycloak.models.IdentityProviderMapperModel;
+import org.keycloak.models.IdentityProviderSyncMode;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.ProviderConfigProperty;
+
+/**
+ * Map an attribute to realm and client roles via regex.
+ */
+public class RegexRealmAndClientRoleAttributeMapper extends AbstractIdentityProviderMapper {
+
+ public static final String PROVIDER_ID = "lucafilipozzi-saml-regex-attribute-mapper";
+
+ public static final String SAML_ATTRIBUTE_NAME = "saml-attribute-name";
+
+ protected static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID};
+
+ private static final Set IDENTITY_PROVIDER_SYNC_MODES = new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values()));
+
+ private static final Logger LOG = Logger.getLogger(RegexRealmAndClientRoleAttributeMapper.class);
+
+ private static final List configProperties = new ArrayList<>();
+
+ static {
+ ProviderConfigProperty samlAttributeNameConfigProperty = new ProviderConfigProperty();
+ samlAttributeNameConfigProperty.setName(SAML_ATTRIBUTE_NAME);
+ samlAttributeNameConfigProperty.setLabel("SAML attribute name");
+ samlAttributeNameConfigProperty.setHelpText("name of SAML attribute to search (friendly or otherwise)");
+ samlAttributeNameConfigProperty.setType(ProviderConfigProperty.STRING_TYPE);
+ configProperties.add(samlAttributeNameConfigProperty);
+
+ ProviderConfigProperty clientRolesAttributeNameConfigProperty = new ProviderConfigProperty();
+ clientRolesAttributeNameConfigProperty.setName(RegexRealmAndClientRoleMapperUtil.CLIENT_ROLES_ATTRIBUTE_NAME);
+ clientRolesAttributeNameConfigProperty.setLabel("client roles attribute name");
+ clientRolesAttributeNameConfigProperty.setHelpText("only evaluate client roles having an attribute with this name");
+ clientRolesAttributeNameConfigProperty.setType(ProviderConfigProperty.STRING_TYPE);
+ configProperties.add(clientRolesAttributeNameConfigProperty);
+
+ ProviderConfigProperty clientRolesRegularExpressionConfigProperty = new ProviderConfigProperty();
+ clientRolesRegularExpressionConfigProperty.setName(RegexRealmAndClientRoleMapperUtil.CLIENT_ROLES_REGULAR_EXPRESSION);
+ clientRolesRegularExpressionConfigProperty.setLabel("client roles regular expression");
+ clientRolesRegularExpressionConfigProperty.setHelpText("regular expression to apply to the SAML attribute to extract client roles; must specify two named-capturing groups: client and role");
+ clientRolesRegularExpressionConfigProperty.setType(ProviderConfigProperty.STRING_TYPE);
+ configProperties.add(clientRolesRegularExpressionConfigProperty);
+
+ ProviderConfigProperty realmRolesAttributeNameConfigProperty = new ProviderConfigProperty();
+ realmRolesAttributeNameConfigProperty.setName(RegexRealmAndClientRoleMapperUtil.REALM_ROLES_ATTRIBUTE_NAME);
+ realmRolesAttributeNameConfigProperty.setLabel("realm roles attribute name");
+ realmRolesAttributeNameConfigProperty.setHelpText("only evaluate realm roles having an attribute with this name");
+ realmRolesAttributeNameConfigProperty.setType(ProviderConfigProperty.STRING_TYPE);
+ configProperties.add(realmRolesAttributeNameConfigProperty);
+
+ ProviderConfigProperty realmRolesRegularExpressionConfigProperty = new ProviderConfigProperty();
+ realmRolesRegularExpressionConfigProperty.setName(RegexRealmAndClientRoleMapperUtil.REALM_ROLES_REGULAR_EXPRESSION);
+ realmRolesRegularExpressionConfigProperty.setLabel("realm roles regular expression");
+ realmRolesRegularExpressionConfigProperty.setHelpText("regular expression to apply to the SAML attribute to extract realm roles; must specify one named-capturing group: role");
+ realmRolesRegularExpressionConfigProperty.setType(ProviderConfigProperty.STRING_TYPE);
+ configProperties.add(realmRolesRegularExpressionConfigProperty);
+ }
+
+ @Override
+ public String[] getCompatibleProviders() {
+ return COMPATIBLE_PROVIDERS;
+ }
+
+ @Override
+ public List getConfigProperties() {
+ return configProperties;
+ }
+
+ @Override
+ public String getDisplayCategory() {
+ return "Role Importer";
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Regex Realm and Client Role Importer";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "implements regex realm and client role importer";
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) {
+ return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode);
+ }
+
+ @Override
+ public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapper, BrokeredIdentityContext context) {
+ LOG.trace("import user");
+ processUser(realm, user, mapper, context);
+ }
+
+ @Override
+ public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapper, BrokeredIdentityContext context) {
+ LOG.trace("update user");
+ processUser(realm, user, mapper, context);
+ }
+
+ private void processUser(RealmModel realm, UserModel user, IdentityProviderMapperModel mapper, BrokeredIdentityContext context) {
+ LOG.trace("process user");
+ String samlAttributeName = mapper.getConfig().getOrDefault(SAML_ATTRIBUTE_NAME, "");
+ AssertionType assertion = (AssertionType) context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
+ Set assertedValues = assertion.getAttributeStatements().stream()
+ .flatMap(statement -> statement.getAttributes().stream())
+ .filter(choice -> choice.getAttribute().getFriendlyName().equals(samlAttributeName) || choice.getAttribute().getName().equals(samlAttributeName))
+ .flatMap(choice -> choice.getAttribute().getAttributeValue().stream())
+ .map(Object::toString)
+ .collect(Collectors.toSet());
+ RegexRealmAndClientRoleMapperUtil.processUser(realm, user, mapper, assertedValues);
+ }
+}
diff --git a/module/src/main/java/com/github/lucafilipozzi/keycloak/broker/util/RegexRealmAndClientRoleMapperUtil.java b/module/src/main/java/com/github/lucafilipozzi/keycloak/broker/util/RegexRealmAndClientRoleMapperUtil.java
new file mode 100644
index 0000000..52caa8f
--- /dev/null
+++ b/module/src/main/java/com/github/lucafilipozzi/keycloak/broker/util/RegexRealmAndClientRoleMapperUtil.java
@@ -0,0 +1,113 @@
+// Copyright 2021 Luca Filipozzi. Some rights reserved. See LICENSE.
+
+package com.github.lucafilipozzi.keycloak.broker.util;
+
+import com.google.common.collect.Sets;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import org.jboss.logging.Logger;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.IdentityProviderMapperModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+
+/**
+ * Utilities for adjusting user's realm and client role assignments.
+ */
+public final class RegexRealmAndClientRoleMapperUtil {
+
+ public static final String CLIENT_ROLES_ATTRIBUTE_NAME = "client-roles-attribute-name";
+
+ public static final String CLIENT_ROLES_REGULAR_EXPRESSION = "client-roles-regular-expression";
+
+ public static final String REALM_ROLES_ATTRIBUTE_NAME = "realm-roles-attribute-name";
+
+ public static final String REALM_ROLES_REGULAR_EXPRESSION = "realm-roles-regular-expression";
+
+ private static final Logger LOG = Logger.getLogger(RegexRealmAndClientRoleMapperUtil.class);
+
+ private RegexRealmAndClientRoleMapperUtil() {
+ throw new UnsupportedOperationException();
+ }
+
+ public static void processUser(RealmModel realm, UserModel user, IdentityProviderMapperModel mapper, Set assertedValues) {
+ LOG.trace("process user");
+
+ // adjust the user's client role assignments
+ String clientRolesRegularExpression = mapper.getConfig().getOrDefault(CLIENT_ROLES_REGULAR_EXPRESSION, "");
+ String clientRolesAttributeName = mapper.getConfig().getOrDefault(CLIENT_ROLES_ATTRIBUTE_NAME, "");
+ RegexRealmAndClientRoleMapperUtil.adjustUserClientRoleAssignments(realm, user, assertedValues, clientRolesRegularExpression, clientRolesAttributeName);
+
+ // adjust the user's realm role assignments
+ String realmRolesRegularExpression = mapper.getConfig().getOrDefault(REALM_ROLES_REGULAR_EXPRESSION, "");
+ String realmRolesAttributeName = mapper.getConfig().getOrDefault(REALM_ROLES_ATTRIBUTE_NAME, "");
+ RegexRealmAndClientRoleMapperUtil.adjustUserRealmRoleAssignments(realm, user, assertedValues, realmRolesRegularExpression, realmRolesAttributeName);
+ }
+
+ private static void adjustUserClientRoleAssignments(RealmModel realm, UserModel user, Set assertedValues, String regularExpression, String attributeName) {
+ LOG.trace("adjust user client role assignments");
+
+ Pattern pattern = Pattern.compile(regularExpression);
+
+ // determine the client roles that the user should have
+ Set wantRoles = assertedValues.stream()
+ .map(pattern::matcher)
+ .filter(Matcher::matches)
+ .filter(matcher -> matcher.groupCount() == 2)
+ .filter(matcher -> matcher.group("client") != null)
+ .filter(matcher -> matcher.group("role") != null)
+ .flatMap(matcher ->
+ realm.getClientsStream()
+ .filter(client -> client.getClientId().equalsIgnoreCase(matcher.group("client")))
+ .flatMap(ClientModel::getRolesStream)
+ .filter(clientRole -> clientRole.getAttributeStream(attributeName).findAny().isPresent())
+ .filter(clientRole -> clientRole.getName().equalsIgnoreCase(matcher.group("role"))))
+ .collect(Collectors.toSet());
+
+ // determine the client roles that user does have
+ Set haveRoles = user.getRoleMappingsStream()
+ .filter(RoleModel::isClientRole)
+ .filter(clientRole -> clientRole.getAttributes().containsKey(attributeName))
+ .collect(Collectors.toSet());
+
+ // assign the client roles that the user should have but doesn't
+ Sets.difference(wantRoles, haveRoles).forEach(user::grantRole);
+
+ // un-assign the client roles that the user has but shouldn't
+ Sets.difference(haveRoles, wantRoles).forEach(user::deleteRoleMapping);
+ }
+
+ private static void adjustUserRealmRoleAssignments(RealmModel realm, UserModel user, Set assertedValues, String regularExpression, String attributeName) {
+ LOG.trace("adjust user realm role assignments");
+
+ Pattern pattern = Pattern.compile(regularExpression);
+
+ // determine the realm roles that the user should have
+ Set wantRoles = assertedValues.stream()
+ .map(pattern::matcher)
+ .filter(Matcher::matches)
+ .filter(matcher -> matcher.groupCount() == 1)
+ .filter(matcher -> matcher.group("role") != null)
+ .flatMap(matcher ->
+ realm.getRolesStream()
+ .filter(realmRole -> !realmRole.isClientRole())
+ .filter(realmRole -> realmRole.getAttributeStream(attributeName).findAny().isPresent())
+ .filter(realmRole -> realmRole.getName().equalsIgnoreCase(matcher.group("role"))))
+ .collect(Collectors.toSet());
+
+ // determine the realm roles that the user does have
+ Set haveRoles = user.getRoleMappingsStream()
+ .filter(realmRole -> !realmRole.isClientRole())
+ .filter(realmRole -> realmRole.getAttributes().containsKey(attributeName))
+ .collect(Collectors.toSet());
+
+ // assign the realm roles that the user should have but doesn't
+ Sets.difference(wantRoles, haveRoles).forEach(user::grantRole);
+
+ // un-assign the realm roles that the user has but shouldn't
+ Sets.difference(haveRoles, wantRoles).forEach(user::deleteRoleMapping);
+ }
+}
diff --git a/module/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper b/module/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper
new file mode 100644
index 0000000..8c66d3e
--- /dev/null
+++ b/module/src/main/resources/META-INF/services/org.keycloak.broker.provider.IdentityProviderMapper
@@ -0,0 +1,4 @@
+# Copyright 2021 Luca Filipozzi. Some rights reserved. See LICENSE.
+
+com.github.lucafilipozzi.keycloak.broker.oidc.mappers.RegexRealmAndClientRoleClaimMapper
+com.github.lucafilipozzi.keycloak.broker.saml.mappers.RegexRealmAndClientRoleAttributeMapper
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..1bcd323
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,342 @@
+
+
+
+ 4.0.0
+
+
+ keycloak-parent
+ org.keycloak
+ 13.0.1
+
+
+ com.github.lucafilipozzi
+ keycloak-regex-mapper
+ ${revision}
+ pom
+ parent
+ Keycloak Extensions Prototype
+
+
+
+ Luca Filipozzi
+
+
+
+
+ GitHub Issues
+ https://github.com/lucafilipozzi/keycloak-regex-mapper/issues
+
+
+
+
+ Apache License, Version 2.0
+ https://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+ scm:git:https://github.com/lucafilipozzi/keycloak-regex-mapper
+ scm:git:git://github.com/lucafilipozzi/keycloak-regex-mapper
+ https://github.com/lucafilipozzi/keycloak-regex-mapper
+
+
+
+ 1.8
+ 1.8
+ UTF-8
+ 1.0.0-SNAPSHOT
+ 13.0.1
+
+
+
+
+
+ com.google.guava
+ guava
+ ${google.guava.version}
+ provided
+
+
+ com.github.lucafilipozzi
+ keycloak-regex-mapper-module
+ ${revision}
+
+
+ org.jboss.logging
+ jboss-logging
+ ${jboss.logging.version}
+ provided
+
+
+ org.keycloak
+ keycloak-common
+ ${keycloak.version}
+ provided
+
+
+ org.keycloak
+ keycloak-core
+ ${keycloak.version}
+ provided
+
+
+ org.keycloak
+ keycloak-saml-core-public
+ ${keycloak.version}
+ provided
+
+
+ org.keycloak
+ keycloak-server-spi
+ ${keycloak.version}
+ provided
+
+
+ org.keycloak
+ keycloak-server-spi-private
+ ${keycloak.version}
+ provided
+
+
+ org.keycloak
+ keycloak-services
+ ${keycloak.version}
+ provided
+
+
+
+
+
+ module
+ bundle
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-clean-plugin
+ 3.1.0
+
+
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+ 3.0.0-M3
+
+
+
+
+ 1.8
+
+
+ 3.6.0
+
+
+ true
+ true
+ true
+
+
+
+ ${project.groupId}:*
+
+
+
+
+ org.apache.maven.plugins:maven-surefire-plugin
+ org.apache.maven.plugins:maven-failsafe-plugin
+
+
+
+
+
+
+
+
+ validate
+
+ enforce
+
+
+
+
+
+ org.codehaus.mojo
+ extra-enforcer-rules
+ 1.3
+
+
+
+
+ org.codehaus.mojo
+ buildnumber-maven-plugin
+ 1.4
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.2.0
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.8.1-jboss-2
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.0.0-M5
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ 3.0.0-M5
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.2.0
+
+
+ org.apache.maven.plugins
+ maven-ear-plugin
+ 3.2.0
+
+
+ org.codehaus.plexus
+ plexus-archiver
+ 4.2.5
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.2.1
+
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 3.1.2
+
+
+ com.puppycrawl.tools
+ checkstyle
+ 8.45.1
+
+
+
+ google_checks.xml
+ true
+ ${project.build.sourceEncoding}
+ true
+ .checkstyle-suppressions.xml
+
+
+
+ verify
+
+ check
+
+
+
+
+
+ org.owasp
+ dependency-check-maven
+ 6.2.2
+
+
+ verify
+
+ check
+
+
+
+
+
+ org.codehaus.mojo
+ versions-maven-plugin
+ 2.8.1
+
+ false
+ false
+ false
+ false
+ false
+
+
+
+ verify
+
+ display-dependency-updates
+ display-plugin-updates
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ 3.2.0
+
+
+ verify
+
+ analyze
+
+
+
+
+
+ org.basepom.maven
+ duplicate-finder-maven-plugin
+ 1.5.0
+
+
+ verify
+
+ check
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-install-plugin
+ 3.0.0-M1
+
+
+
+
+ org.apache.maven.plugins
+ maven-site-plugin
+ 3.9.1
+
+
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+ 3.0.0-M1
+
+
+
+
+