diff --git a/pom.xml b/pom.xml
index 837822b9..54299d1d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -32,8 +32,8 @@
UTF-8
io.phasetwo.service
5.11.2
- 26.0.2
- 26.0.1
+ 26.0.7
+ 26.0.3
6.2.7.Final
1.18.34
33.0.0-jre
diff --git a/src/main/java/io/phasetwo/service/auth/action/PortalLinkActionTokenHandlerFactory.java b/src/main/java/io/phasetwo/service/auth/action/PortalLinkActionTokenHandlerFactory.java
index 5f2cab6e..ce2e8415 100644
--- a/src/main/java/io/phasetwo/service/auth/action/PortalLinkActionTokenHandlerFactory.java
+++ b/src/main/java/io/phasetwo/service/auth/action/PortalLinkActionTokenHandlerFactory.java
@@ -6,6 +6,7 @@
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
+@SuppressWarnings("rawtypes")
@AutoService(ActionTokenHandlerFactory.class)
public class PortalLinkActionTokenHandlerFactory
implements ActionTokenHandlerFactory {
diff --git a/src/main/java/io/phasetwo/service/broker/Mappers.java b/src/main/java/io/phasetwo/service/broker/Mappers.java
new file mode 100644
index 00000000..fcd2a137
--- /dev/null
+++ b/src/main/java/io/phasetwo/service/broker/Mappers.java
@@ -0,0 +1,23 @@
+package io.phasetwo.service.broker;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import lombok.extern.jbosslog.JBossLog;
+import org.keycloak.models.IdentityProviderSyncMode;
+
+@JBossLog
+public final class Mappers {
+
+ public static final String ARE_ATTRIBUTE_VALUES_REGEX_PROPERTY_NAME =
+ "are.attribute.values.regex";
+ public static final String ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME = "are.claim.values.regex";
+ public static final String ATTRIBUTE_PROPERTY_NAME = "attributes";
+ public static final String CLAIM_PROPERTY_NAME = "claims";
+ public static final String ORG_ADD_PROPERTY_NAME = "org_add";
+ public static final String ORG_PROPERTY_NAME = "org";
+ public static final String ORG_ROLE_PROPERTY_NAME = "org_role";
+
+ public static final Set IDENTITY_PROVIDER_SYNC_MODES =
+ new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values()));
+}
diff --git a/src/main/java/io/phasetwo/service/broker/oidc/mappers/AdvancedClaimToOrgRoleMapper.java b/src/main/java/io/phasetwo/service/broker/oidc/mappers/AdvancedClaimToOrgRoleMapper.java
new file mode 100644
index 00000000..679695e1
--- /dev/null
+++ b/src/main/java/io/phasetwo/service/broker/oidc/mappers/AdvancedClaimToOrgRoleMapper.java
@@ -0,0 +1,181 @@
+package io.phasetwo.service.broker.oidc.mappers;
+
+import static io.phasetwo.service.broker.Mappers.*;
+import static org.keycloak.utils.RegexUtils.valueMatchesRegex;
+
+import com.google.auto.service.AutoService;
+import io.phasetwo.service.broker.OrgRoleMapper;
+import io.phasetwo.service.model.OrganizationModel;
+import io.phasetwo.service.model.OrganizationRoleModel;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import lombok.extern.jbosslog.JBossLog;
+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.broker.provider.IdentityProviderMapper;
+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;
+
+@JBossLog
+@AutoService(IdentityProviderMapper.class)
+public class AdvancedClaimToOrgRoleMapper extends AbstractClaimMapper implements OrgRoleMapper {
+
+ public static final String[] COMPATIBLE_PROVIDERS = {
+ KeycloakOIDCIdentityProviderFactory.PROVIDER_ID, OIDCIdentityProviderFactory.PROVIDER_ID
+ };
+
+ private static final List configProperties = new ArrayList<>();
+
+ static {
+ ProviderConfigProperty claimsProperty = new ProviderConfigProperty();
+ claimsProperty.setName(CLAIM_PROPERTY_NAME);
+ claimsProperty.setLabel("Claims");
+ claimsProperty.setHelpText(
+ "Name and value of the claims to search for in token. You can reference nested claims using a '.', i.e. 'address.locality'. To use dot (.) literally, escape it with backslash (\\.)");
+ claimsProperty.setType(ProviderConfigProperty.MAP_TYPE);
+ configProperties.add(claimsProperty);
+ ProviderConfigProperty isClaimValueRegexProperty = new ProviderConfigProperty();
+ isClaimValueRegexProperty.setName(ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME);
+ isClaimValueRegexProperty.setLabel("Regex Claim Values");
+ isClaimValueRegexProperty.setHelpText(
+ "If enabled claim values are interpreted as regular expressions.");
+ isClaimValueRegexProperty.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+ configProperties.add(isClaimValueRegexProperty);
+
+ OrgRoleMapper.addOrgConfigProperties(configProperties);
+ }
+
+ public static final String PROVIDER_ID = "oidc-advanced-org-role-idp-mapper";
+
+ @Override
+ public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) {
+ return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode);
+ }
+
+ @Override
+ public List getConfigProperties() {
+ return configProperties;
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public String[] getCompatibleProviders() {
+ return COMPATIBLE_PROVIDERS;
+ }
+
+ @Override
+ public String getDisplayCategory() {
+ return "Role Importer";
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Advanced Claim to Org Role";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "If all claims exists, grant the user the specified organization role.";
+ }
+
+ protected boolean applies(
+ IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
+ Map> claims = mapperModel.getConfigMap(CLAIM_PROPERTY_NAME);
+ boolean areClaimValuesRegex =
+ Boolean.parseBoolean(
+ mapperModel.getConfig().getOrDefault(ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME, "false"));
+
+ for (Map.Entry> claim : claims.entrySet()) {
+ Object claimValue = getClaimValue(context, claim.getKey());
+ for (String value : claim.getValue()) {
+ boolean claimValuesMismatch =
+ !(areClaimValuesRegex
+ ? valueMatchesRegex(value, claimValue)
+ : valueEquals(value, claimValue));
+ if (claimValuesMismatch) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public void importNewUser(
+ KeycloakSession session,
+ RealmModel realm,
+ UserModel user,
+ IdentityProviderMapperModel mapperModel,
+ BrokeredIdentityContext context) {
+ OrganizationModel org = getOrg(session, realm, mapperModel);
+ if (org == null) {
+ return;
+ }
+ OrganizationRoleModel role = getOrgRole(session, realm, mapperModel);
+ if (role == null) {
+ return;
+ }
+
+ if (applies(mapperModel, context)) {
+ addUserToOrg(org, user, mapperModel);
+ grantOrgRole(org, role, user);
+ }
+ }
+
+ @Override
+ public void updateBrokeredUserLegacy(
+ KeycloakSession session,
+ RealmModel realm,
+ UserModel user,
+ IdentityProviderMapperModel mapperModel,
+ BrokeredIdentityContext context) {
+ OrganizationModel org = getOrg(session, realm, mapperModel);
+ if (org == null) {
+ return;
+ }
+ OrganizationRoleModel role = getOrgRole(session, realm, mapperModel);
+ if (role == null) {
+ return;
+ }
+
+ if (!applies(mapperModel, context)) {
+ revokeOrgRole(org, role, user);
+ }
+ }
+
+ @Override
+ public void updateBrokeredUser(
+ KeycloakSession session,
+ RealmModel realm,
+ UserModel user,
+ IdentityProviderMapperModel mapperModel,
+ BrokeredIdentityContext context) {
+ OrganizationModel org = getOrg(session, realm, mapperModel);
+ if (org == null) {
+ return;
+ }
+ OrganizationRoleModel role = getOrgRole(session, realm, mapperModel);
+ if (role == null) {
+ return;
+ }
+
+ if (applies(mapperModel, context)) {
+ addUserToOrg(org, user, mapperModel);
+ grantOrgRole(org, role, user);
+ } else {
+ revokeOrgRole(org, role, user);
+ }
+ }
+}
diff --git a/src/main/java/io/phasetwo/service/broker/oidc/mappers/OrgRoleMapper.java b/src/main/java/io/phasetwo/service/broker/oidc/mappers/OrgRoleMapper.java
new file mode 100644
index 00000000..8043ac8a
--- /dev/null
+++ b/src/main/java/io/phasetwo/service/broker/oidc/mappers/OrgRoleMapper.java
@@ -0,0 +1,123 @@
+package io.phasetwo.service.broker;
+
+import static io.phasetwo.service.broker.Mappers.*;
+
+import com.google.common.base.Strings;
+import io.phasetwo.service.model.OrganizationModel;
+import io.phasetwo.service.model.OrganizationProvider;
+import io.phasetwo.service.model.OrganizationRoleModel;
+import java.util.List;
+import org.jboss.logging.Logger;
+import org.keycloak.models.IdentityProviderMapperModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.provider.ProviderConfigProperty;
+
+public interface OrgRoleMapper {
+
+ static final Logger log = Logger.getLogger(OrgRoleMapper.class);
+
+ static void addOrgConfigProperties(List configProperties) {
+ ProviderConfigProperty orgAdd = new ProviderConfigProperty();
+ orgAdd.setName(ORG_ADD_PROPERTY_NAME);
+ orgAdd.setLabel("Add To Organization");
+ orgAdd.setHelpText("Add user to the organization as a member if not already.");
+ orgAdd.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+ configProperties.add(orgAdd);
+ ProviderConfigProperty org = new ProviderConfigProperty();
+ org.setName(ORG_PROPERTY_NAME);
+ org.setLabel("Organization");
+ org.setHelpText("Organization containing the role to grant to user.");
+ org.setType(ProviderConfigProperty.STRING_TYPE);
+ configProperties.add(org);
+ ProviderConfigProperty orgRole = new ProviderConfigProperty();
+ orgRole.setName(ORG_ROLE_PROPERTY_NAME);
+ orgRole.setLabel("Organization Role");
+ orgRole.setHelpText("Organization role to grant to user.");
+ orgRole.setType(ProviderConfigProperty.STRING_TYPE);
+ configProperties.add(orgRole);
+ }
+
+ default boolean orgAdd(IdentityProviderMapperModel mapperModel) {
+ return Boolean.parseBoolean(
+ mapperModel.getConfig().getOrDefault(ORG_ADD_PROPERTY_NAME, "false"));
+ }
+
+ default void addUserToOrg(
+ OrganizationModel org, UserModel user, IdentityProviderMapperModel mapperModel) {
+ if (orgAdd(mapperModel) && !org.hasMembership(user)) {
+ org.grantMembership(user);
+ }
+ }
+
+ default OrganizationModel getOrg(
+ KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel) {
+ OrganizationProvider orgs = session.getProvider(OrganizationProvider.class);
+ String orgName = mapperModel.getConfig().getOrDefault(ORG_PROPERTY_NAME, null);
+ if (Strings.isNullOrEmpty(orgName)) return null;
+ return orgs.getOrganizationByName(realm, orgName);
+ }
+
+ default OrganizationRoleModel getOrgRole(
+ KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel) {
+ OrganizationProvider orgs = session.getProvider(OrganizationProvider.class);
+ String orgName = mapperModel.getConfig().getOrDefault(ORG_PROPERTY_NAME, null);
+ String orgRoleName = mapperModel.getConfig().getOrDefault(ORG_ROLE_PROPERTY_NAME, null);
+ if (Strings.isNullOrEmpty(orgName) || Strings.isNullOrEmpty(orgRoleName)) return null;
+ else return getOrgRole(orgs, orgName, orgRoleName, realm);
+ }
+
+ default OrganizationRoleModel getOrgRole(
+ OrganizationProvider orgs, String orgName, String orgRoleName, RealmModel realm) {
+ OrganizationModel org = orgs.getOrganizationByName(realm, orgName);
+ if (org == null) {
+ log.debugf("Cannot map non-existent org %s", orgName);
+ return null;
+ }
+ OrganizationRoleModel role = org.getRoleByName(orgRoleName);
+ if (role == null) {
+ log.debugf("Cannot map non-existent org role %s - %s", orgName, orgRoleName);
+ return null;
+ }
+ return role;
+ }
+
+ default void grantOrgRole(OrganizationModel org, OrganizationRoleModel role, UserModel user) {
+ if (org.hasMembership(user)) {
+ role.grantRole(user);
+ }
+ }
+
+ default void revokeOrgRole(OrganizationModel org, OrganizationRoleModel role, UserModel user) {
+ if (org.hasMembership(user)) {
+ role.revokeRole(user);
+ }
+ }
+
+ default void grantOrgRole(
+ KeycloakSession session,
+ RealmModel realm,
+ UserModel user,
+ IdentityProviderMapperModel mapperModel) {
+ OrganizationProvider orgs = session.getProvider(OrganizationProvider.class);
+ String orgName = mapperModel.getConfig().getOrDefault(ORG_PROPERTY_NAME, null);
+ String orgRoleName = mapperModel.getConfig().getOrDefault(ORG_ROLE_PROPERTY_NAME, null);
+ if (Strings.isNullOrEmpty(orgName) || Strings.isNullOrEmpty(orgRoleName)) return;
+
+ OrganizationModel org = orgs.getOrganizationByName(realm, orgName);
+ OrganizationRoleModel role = getOrgRole(orgs, orgName, orgRoleName, realm);
+ if (org != null && role != null) {
+ if (orgAdd(mapperModel)) {
+ if (!org.hasMembership(user)) {
+ log.infof("Granting org: %s membership to %s", orgName, user.getUsername());
+ org.grantMembership(user);
+ }
+ }
+ if (org.hasMembership(user)) {
+ log.infof("Granting org: %s - role: %s to %s", orgName, orgRoleName, user.getUsername());
+ role.grantRole(user);
+ }
+ }
+ }
+}
diff --git a/src/main/java/io/phasetwo/service/broker/provider/HardcodedOrgRoleMapper.java b/src/main/java/io/phasetwo/service/broker/provider/HardcodedOrgRoleMapper.java
new file mode 100644
index 00000000..f0abda8b
--- /dev/null
+++ b/src/main/java/io/phasetwo/service/broker/provider/HardcodedOrgRoleMapper.java
@@ -0,0 +1,96 @@
+package io.phasetwo.service.broker.provider;
+
+import static io.phasetwo.service.broker.Mappers.*;
+
+import com.google.auto.service.AutoService;
+import io.phasetwo.service.broker.OrgRoleMapper;
+import java.util.ArrayList;
+import java.util.List;
+import lombok.extern.jbosslog.JBossLog;
+import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
+import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.broker.provider.IdentityProviderMapper;
+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;
+
+@JBossLog
+@AutoService(IdentityProviderMapper.class)
+public class HardcodedOrgRoleMapper extends AbstractIdentityProviderMapper
+ implements OrgRoleMapper {
+ protected static final List configProperties = new ArrayList<>();
+
+ static {
+ OrgRoleMapper.addOrgConfigProperties(configProperties);
+ }
+
+ @Override
+ public List getConfigProperties() {
+ return configProperties;
+ }
+
+ @Override
+ public String getDisplayCategory() {
+ return "Role Importer";
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Hardcoded Organization Role";
+ }
+
+ public static final String[] COMPATIBLE_PROVIDERS = {ANY_PROVIDER};
+
+ public static final String PROVIDER_ID = "oidc-hardcoded-org-role-idp-mapper";
+
+ @Override
+ public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) {
+ return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode);
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public String[] getCompatibleProviders() {
+ return COMPATIBLE_PROVIDERS;
+ }
+
+ @Override
+ public void importNewUser(
+ KeycloakSession session,
+ RealmModel realm,
+ UserModel user,
+ IdentityProviderMapperModel mapperModel,
+ BrokeredIdentityContext context) {
+ grantOrgRole(session, realm, user, mapperModel);
+ }
+
+ @Override
+ public void updateBrokeredUser(
+ KeycloakSession session,
+ RealmModel realm,
+ UserModel user,
+ IdentityProviderMapperModel mapperModel,
+ BrokeredIdentityContext context) {
+ grantOrgRole(session, realm, user, mapperModel);
+ }
+
+ @Override
+ public void updateBrokeredUserLegacy(
+ KeycloakSession session,
+ RealmModel realm,
+ UserModel user,
+ IdentityProviderMapperModel mapperModel,
+ BrokeredIdentityContext context) {}
+
+ @Override
+ public String getHelpText() {
+ return "When user is imported from provider, hardcode an organization role mapping for it.";
+ }
+}
diff --git a/src/main/java/io/phasetwo/service/broker/saml/mappers/AdvancedAttributeToOrgRoleMapper.java b/src/main/java/io/phasetwo/service/broker/saml/mappers/AdvancedAttributeToOrgRoleMapper.java
new file mode 100644
index 00000000..36a42073
--- /dev/null
+++ b/src/main/java/io/phasetwo/service/broker/saml/mappers/AdvancedAttributeToOrgRoleMapper.java
@@ -0,0 +1,188 @@
+package io.phasetwo.service.broker.saml.mappers;
+
+import static io.phasetwo.service.broker.Mappers.*;
+import static org.keycloak.utils.RegexUtils.valueMatchesRegex;
+
+import com.google.auto.service.AutoService;
+import io.phasetwo.service.broker.OrgRoleMapper;
+import io.phasetwo.service.model.OrganizationModel;
+import io.phasetwo.service.model.OrganizationRoleModel;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import lombok.extern.jbosslog.JBossLog;
+import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
+import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.broker.provider.IdentityProviderMapper;
+import org.keycloak.broker.saml.SAMLEndpoint;
+import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
+import org.keycloak.dom.saml.v2.assertion.AssertionType;
+import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
+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;
+
+@JBossLog
+@AutoService(IdentityProviderMapper.class)
+public class AdvancedAttributeToOrgRoleMapper extends AbstractIdentityProviderMapper
+ implements OrgRoleMapper {
+
+ public static final String PROVIDER_ID = "saml-advanced-org-role-idp-mapper";
+
+ public static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID};
+
+ private static final List configProperties = new ArrayList<>();
+
+ static {
+ ProviderConfigProperty attributeMappingProperty = new ProviderConfigProperty();
+ attributeMappingProperty.setName(ATTRIBUTE_PROPERTY_NAME);
+ attributeMappingProperty.setLabel("Attributes");
+ attributeMappingProperty.setHelpText(
+ "Name and (regex) value of the attributes to search for in token. "
+ + " The configured name of an attribute is searched in SAML attribute name and attribute friendly name fields."
+ + " Every given attribute description must be met to set the role."
+ + " If the attribute is an array, then the value must be contained in the array."
+ + " If an attribute can be found several times, then one match is sufficient.");
+ attributeMappingProperty.setType(ProviderConfigProperty.MAP_TYPE);
+ configProperties.add(attributeMappingProperty);
+
+ ProviderConfigProperty isAttributeRegexProperty = new ProviderConfigProperty();
+ isAttributeRegexProperty.setName(ARE_ATTRIBUTE_VALUES_REGEX_PROPERTY_NAME);
+ isAttributeRegexProperty.setLabel("Regex Attribute Values");
+ isAttributeRegexProperty.setHelpText(
+ "If enabled attribute values are interpreted as regular expressions.");
+ isAttributeRegexProperty.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+ configProperties.add(isAttributeRegexProperty);
+
+ OrgRoleMapper.addOrgConfigProperties(configProperties);
+ }
+
+ @Override
+ public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) {
+ return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode);
+ }
+
+ @Override
+ public List getConfigProperties() {
+ return configProperties;
+ }
+
+ @Override
+ public String getId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public String[] getCompatibleProviders() {
+ return COMPATIBLE_PROVIDERS;
+ }
+
+ @Override
+ public String getDisplayCategory() {
+ return "Role Importer";
+ }
+
+ @Override
+ public String getDisplayType() {
+ return "Advanced Attribute to Org Role";
+ }
+
+ @Override
+ public String getHelpText() {
+ return "If the set of attributes exists and can be matched, grant the user the specified organization role.";
+ }
+
+ protected boolean applies(
+ final IdentityProviderMapperModel mapperModel, final BrokeredIdentityContext context) {
+ Map> attributes = mapperModel.getConfigMap(ATTRIBUTE_PROPERTY_NAME);
+ boolean areAttributeValuesRegexes =
+ Boolean.parseBoolean(
+ mapperModel
+ .getConfig()
+ .getOrDefault(ARE_ATTRIBUTE_VALUES_REGEX_PROPERTY_NAME, "false"));
+
+ AssertionType assertion =
+ (AssertionType) context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
+ Set attributeAssertions = assertion.getAttributeStatements();
+ if (attributeAssertions == null) {
+ return false;
+ }
+
+ for (Map.Entry> entry : attributes.entrySet()) {
+ String attributeKey = entry.getKey();
+ for (String value : entry.getValue()) {
+ List