diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderMapper.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderMapper.java index 7f0827a6c3..5eaef8e10d 100755 --- a/server-spi-private/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderMapper.java +++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/AbstractIdentityProviderMapper.java @@ -57,4 +57,14 @@ public abstract class AbstractIdentityProviderMapper implements IdentityProvider public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { } + + @Override + public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + + } + + @Override + public void updateBrokeredUserLegacy(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + updateBrokeredUser(session, realm, user, mapperModel, context); + } } diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/BrokeredIdentityContext.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/BrokeredIdentityContext.java index a2b1dc5624..7e793a1644 100755 --- a/server-spi-private/src/main/java/org/keycloak/broker/provider/BrokeredIdentityContext.java +++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/BrokeredIdentityContext.java @@ -161,6 +161,11 @@ public class BrokeredIdentityContext { getContextData().put(Constants.USER_ATTRIBUTES_PREFIX + attributeName, list); } + // Remove an attribute attribute, which would otherwise be available on "Update profile" page and in authenticators + public void removeUserAttribute(String attributeName) { + getContextData().remove(Constants.USER_ATTRIBUTES_PREFIX + attributeName); + } + public void setUserAttribute(String attributeName, List attributeValues) { getContextData().put(Constants.USER_ATTRIBUTES_PREFIX + attributeName, attributeValues); } diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/IdentityProviderMapper.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/IdentityProviderMapper.java index 75a8da6b5a..7ea00b0215 100755 --- a/server-spi-private/src/main/java/org/keycloak/broker/provider/IdentityProviderMapper.java +++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/IdentityProviderMapper.java @@ -18,6 +18,7 @@ package org.keycloak.broker.provider; 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; @@ -25,17 +26,26 @@ import org.keycloak.provider.ConfiguredProvider; import org.keycloak.provider.Provider; import org.keycloak.provider.ProviderFactory; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + /** * @author Bill Burke * @version $Revision: 1 $ */ public interface IdentityProviderMapper extends Provider, ProviderFactory,ConfiguredProvider { String ANY_PROVIDER = "*"; + Set DEFAULT_IDENTITY_PROVIDER_MAPPER_SYNC_MODES = new HashSet<>(Arrays.asList(IdentityProviderSyncMode.LEGACY, IdentityProviderSyncMode.IMPORT)); String[] getCompatibleProviders(); String getDisplayCategory(); String getDisplayType(); + default boolean supportsSyncMode(IdentityProviderSyncMode syncMode) { + return DEFAULT_IDENTITY_PROVIDER_MAPPER_SYNC_MODES.contains(syncMode); + } + /** * Called to determine what keycloak username and email to use to process the login request from the external IDP. * It's called before "FirstBrokerLogin" flow, so can be used to map attributes to BrokeredIdentityContext ( BrokeredIdentityContext.setUserAttribute ), @@ -60,6 +70,18 @@ public interface IdentityProviderMapper extends Provider, ProviderFactory> MAP_TYPE_REPRESENTATION = new TypeReference>() { }; @@ -76,6 +77,14 @@ public class IdentityProviderMapperModel implements Serializable { this.identityProviderMapper = identityProviderMapper; } + public IdentityProviderMapperSyncMode getSyncMode() { + return IdentityProviderMapperSyncMode.valueOf(getConfig().getOrDefault(SYNC_MODE, "LEGACY")); + } + + public void setSyncMode(IdentityProviderMapperSyncMode syncMode) { + getConfig().put(SYNC_MODE, syncMode.toString()); + } + public Map getConfig() { return config; } diff --git a/server-spi/src/main/java/org/keycloak/models/IdentityProviderMapperSyncMode.java b/server-spi/src/main/java/org/keycloak/models/IdentityProviderMapperSyncMode.java new file mode 100644 index 0000000000..dcb14bcb67 --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/models/IdentityProviderMapperSyncMode.java @@ -0,0 +1,5 @@ +package org.keycloak.models; + +public enum IdentityProviderMapperSyncMode { + INHERIT, LEGACY, IMPORT, FORCE +} diff --git a/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java b/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java index 846df4e75f..a1f8040505 100755 --- a/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java +++ b/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java @@ -30,6 +30,8 @@ public class IdentityProviderModel implements Serializable { public static final String ALLOWED_CLOCK_SKEW = "allowedClockSkew"; + public static final String SYNC_MODE = "syncMode"; + private String internalId; /** @@ -64,6 +66,8 @@ public class IdentityProviderModel implements Serializable { private String displayName; + private IdentityProviderSyncMode syncMode; + /** *

A map containing the configuration and properties for a specific identity provider instance and implementation. The items * in the map are understood by the identity provider implementation.

@@ -205,6 +209,13 @@ public class IdentityProviderModel implements Serializable { * @param realm the realm */ public void validate(RealmModel realm) { + } + public IdentityProviderSyncMode getSyncMode() { + return IdentityProviderSyncMode.valueOf(getConfig().getOrDefault(SYNC_MODE, "LEGACY")); + } + + public void setSyncMode(IdentityProviderSyncMode syncMode) { + getConfig().put(SYNC_MODE, syncMode.toString()); } } diff --git a/server-spi/src/main/java/org/keycloak/models/IdentityProviderSyncMode.java b/server-spi/src/main/java/org/keycloak/models/IdentityProviderSyncMode.java new file mode 100644 index 0000000000..c694a18faa --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/models/IdentityProviderSyncMode.java @@ -0,0 +1,5 @@ +package org.keycloak.models; + +public enum IdentityProviderSyncMode { + LEGACY, IMPORT, FORCE +} diff --git a/services/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java b/services/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java index 6827c21e08..ca0ddff039 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java +++ b/services/src/main/java/org/keycloak/broker/oidc/mappers/AbstractJsonUserAttributeMapper.java @@ -23,6 +23,7 @@ import org.keycloak.broker.oidc.OIDCIdentityProvider; import org.keycloak.broker.provider.AbstractIdentityProviderMapper; 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; @@ -30,7 +31,10 @@ import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; import org.keycloak.provider.ProviderConfigProperty; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Abstract class for Social Provider mappers which allow mapping of JSON user profile field into Keycloak user @@ -41,6 +45,7 @@ import java.util.List; */ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityProviderMapper { + private static final Set IDENTITY_PROVIDER_SYNC_MODES = new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values())); protected static final Logger logger = Logger.getLogger(AbstractJsonUserAttributeMapper.class); @@ -97,6 +102,11 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr LOGGER_DUMP_USER_PROFILE.debug("User Profile JSON Data for provider "+provider+": " + profile); } + @Override + public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) { + return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode); + } + @Override public List getConfigProperties() { return configProperties; @@ -119,12 +129,10 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr @Override public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { - String attribute = mapperModel.getConfig().get(CONF_USER_ATTRIBUTE); - if (attribute == null || attribute.trim().isEmpty()) { - logger.warnf("Attribute is not configured for mapper %s", mapperModel.getName()); + String attribute = getAttribute(mapperModel); + if (attribute == null) { return; } - attribute = attribute.trim(); Object value = getJsonValue(mapperModel, context); if (value != null) { @@ -137,10 +145,37 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr } @Override - public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + public void updateBrokeredUserLegacy(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { // we do not update user profile from social provider } + @Override + public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + String attribute = getAttribute(mapperModel); + if (attribute == null) { + return; + } + + Object value = getJsonValue(mapperModel, context); + if (value == null) { + user.removeAttribute(attribute); + } else if (value instanceof List) { + user.setAttribute(attribute, (List) value); + } else { + user.setSingleAttribute(attribute, value.toString()); + } + } + + private String getAttribute(IdentityProviderMapperModel mapperModel) { + String attribute = mapperModel.getConfig().get(CONF_USER_ATTRIBUTE); + if (attribute == null || attribute.trim().isEmpty()) { + logger.warnf("Attribute is not configured for mapper %s", mapperModel.getName()); + return null; + } + attribute = attribute.trim(); + return attribute; + } + protected static Object getJsonValue(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { String jsonField = mapperModel.getConfig().get(CONF_JSON_FIELD); @@ -223,7 +258,7 @@ public abstract class AbstractJsonUserAttributeMapper extends AbstractIdentityPr if (values.isEmpty()) { return null; } - return values ; + return values ; } else if (currentNode.isNull()) { logger.debug("JsonNode is null node for name " + currentFieldName); diff --git a/services/src/main/java/org/keycloak/broker/oidc/mappers/AdvancedClaimToRoleMapper.java b/services/src/main/java/org/keycloak/broker/oidc/mappers/AdvancedClaimToRoleMapper.java index 7892175fc3..ae5eca3f3f 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/mappers/AdvancedClaimToRoleMapper.java +++ b/services/src/main/java/org/keycloak/broker/oidc/mappers/AdvancedClaimToRoleMapper.java @@ -23,6 +23,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.ConfigConstants; import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; @@ -31,8 +32,11 @@ import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.provider.ProviderConfigProperty; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * @author Bill Burke, Benjamin Weimer @@ -44,6 +48,7 @@ public class AdvancedClaimToRoleMapper extends AbstractClaimMapper { public static final String ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME = "are.claim.values.regex"; public 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 List configProperties = new ArrayList(); @@ -70,6 +75,10 @@ public class AdvancedClaimToRoleMapper extends AbstractClaimMapper { public static final String PROVIDER_ID = "oidc-advanced-role-idp-mapper"; + @Override + public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) { + return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode); + } @Override public List getConfigProperties() { @@ -107,7 +116,7 @@ public class AdvancedClaimToRoleMapper extends AbstractClaimMapper { } @Override - public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + public void updateBrokeredUserLegacy(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { String roleName = mapperModel.getConfig().get(ConfigConstants.ROLE); if (!hasAllClaimValues(mapperModel, context)) { RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName); @@ -117,6 +126,21 @@ public class AdvancedClaimToRoleMapper extends AbstractClaimMapper { } + @Override + public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + String roleName = mapperModel.getConfig().get(ConfigConstants.ROLE); + RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName); + if (role == null) { + throw new IdentityBrokerException("Unable to find role: " + roleName); + } + if (!hasAllClaimValues(mapperModel, context)) { + user.deleteRoleMapping(role); + } else { + user.grantRole(role); + } + } + + @Override public String getHelpText() { return "If all claims exists, grant the user the specified realm or application role."; diff --git a/services/src/main/java/org/keycloak/broker/oidc/mappers/ClaimToRoleMapper.java b/services/src/main/java/org/keycloak/broker/oidc/mappers/ClaimToRoleMapper.java index dcb1622cbd..5cdf059e67 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/mappers/ClaimToRoleMapper.java +++ b/services/src/main/java/org/keycloak/broker/oidc/mappers/ClaimToRoleMapper.java @@ -23,6 +23,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.ConfigConstants; import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; @@ -31,7 +32,10 @@ import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.provider.ProviderConfigProperty; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * @author Bill Burke @@ -42,6 +46,7 @@ public class ClaimToRoleMapper extends AbstractClaimMapper { public static final String[] COMPATIBLE_PROVIDERS = {KeycloakOIDCIdentityProviderFactory.PROVIDER_ID, OIDCIdentityProviderFactory.PROVIDER_ID}; private static final List configProperties = new ArrayList(); + private static final Set IDENTITY_PROVIDER_SYNC_MODES = new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values())); static { ProviderConfigProperty property; @@ -68,6 +73,10 @@ public class ClaimToRoleMapper extends AbstractClaimMapper { public static final String PROVIDER_ID = "oidc-role-idp-mapper"; + @Override + public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) { + return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode); + } @Override public List getConfigProperties() { @@ -105,7 +114,7 @@ public class ClaimToRoleMapper extends AbstractClaimMapper { } @Override - public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + public void updateBrokeredUserLegacy(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { String roleName = mapperModel.getConfig().get(ConfigConstants.ROLE); if (!hasClaimValue(mapperModel, context)) { RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName); @@ -115,6 +124,20 @@ public class ClaimToRoleMapper extends AbstractClaimMapper { } + @Override + public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + String roleName = mapperModel.getConfig().get(ConfigConstants.ROLE); + RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName); + if (role == null) { + throw new IdentityBrokerException("Unable to find role: " + roleName); + } + if (!hasClaimValue(mapperModel, context)) { + user.deleteRoleMapping(role); + } else { + user.grantRole(role); + } + } + @Override public String getHelpText() { return "If a claim exists, grant the user the specified realm or application role."; diff --git a/services/src/main/java/org/keycloak/broker/oidc/mappers/ExternalKeycloakRoleToRoleMapper.java b/services/src/main/java/org/keycloak/broker/oidc/mappers/ExternalKeycloakRoleToRoleMapper.java index 1c08c3a1c8..eeb1a49d03 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/mappers/ExternalKeycloakRoleToRoleMapper.java +++ b/services/src/main/java/org/keycloak/broker/oidc/mappers/ExternalKeycloakRoleToRoleMapper.java @@ -23,6 +23,7 @@ import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.ConfigConstants; import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; @@ -32,7 +33,10 @@ import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.JsonWebToken; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * @author Bill Burke @@ -44,6 +48,7 @@ public class ExternalKeycloakRoleToRoleMapper extends AbstractClaimMapper { private static final List configProperties = new ArrayList(); private static final String EXTERNAL_ROLE = "external.role"; + private static final Set IDENTITY_PROVIDER_SYNC_MODES = new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values())); static { ProviderConfigProperty property; @@ -64,6 +69,10 @@ public class ExternalKeycloakRoleToRoleMapper extends AbstractClaimMapper { public static final String PROVIDER_ID = "keycloak-oidc-role-to-role-idp-mapper"; + @Override + public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) { + return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode); + } @Override public List getConfigProperties() { @@ -92,16 +101,13 @@ public class ExternalKeycloakRoleToRoleMapper extends AbstractClaimMapper { @Override public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { - RoleModel role = hasRole(realm, mapperModel, context); - if (role != null) { - user.grantRole(role); + if (hasRole(realm, mapperModel, context)) { + user.grantRole(searchRole(realm, mapperModel)); } } - private RoleModel hasRole(RealmModel realm,IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + private boolean hasRole(RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { JsonWebToken token = (JsonWebToken)context.getContextData().get(KeycloakOIDCIdentityProvider.VALIDATED_ACCESS_TOKEN); - //if (token == null) return; - String roleName = mapperModel.getConfig().get(ConfigConstants.ROLE); String[] parseRole = KeycloakModelUtils.parseRole(mapperModel.getConfig().get(EXTERNAL_ROLE)); String externalRoleName = parseRole[1]; String claimName = null; @@ -111,19 +117,28 @@ public class ExternalKeycloakRoleToRoleMapper extends AbstractClaimMapper { claimName = "resource_access." + parseRole[0] + ".roles"; } Object claim = getClaimValue(token, claimName); - if (valueEquals(externalRoleName, claim)) { - RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName); - if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName); - return role; - } - return null; + return valueEquals(externalRoleName, claim); + } + + private RoleModel searchRole(RealmModel realm, IdentityProviderMapperModel mapperModel) { + String roleName = mapperModel.getConfig().get(ConfigConstants.ROLE); + RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName); + if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName); + return role; } + @Override + public void updateBrokeredUserLegacy(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + // The legacy mapper actually did nothing although it pretended to do something + } + + @Override public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { - RoleModel role = hasRole(realm, mapperModel, context); - if (role == null) { - user.deleteRoleMapping(role); + if (hasRole(realm, mapperModel, context)) { + user.grantRole(searchRole(realm, mapperModel)); + } else { + user.deleteRoleMapping(searchRole(realm, mapperModel)); } } diff --git a/services/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java b/services/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java index 4770cf3cff..f1f010402c 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java +++ b/services/src/main/java/org/keycloak/broker/oidc/mappers/UserAttributeMapper.java @@ -22,6 +22,7 @@ import org.keycloak.broker.oidc.OIDCIdentityProviderFactory; import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.common.util.CollectionUtil; 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; @@ -29,9 +30,12 @@ import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.saml.common.util.StringUtil; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -49,6 +53,7 @@ public class UserAttributeMapper extends AbstractClaimMapper { public static final String EMAIL = "email"; public static final String FIRST_NAME = "firstName"; public static final String LAST_NAME = "lastName"; + private static final Set IDENTITY_PROVIDER_SYNC_MODES = new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values())); static { ProviderConfigProperty property; @@ -69,6 +74,11 @@ public class UserAttributeMapper extends AbstractClaimMapper { public static final String PROVIDER_ID = "oidc-user-attribute-idp-mapper"; + @Override + public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) { + return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode); + } + @Override public List getConfigProperties() { return configProperties; diff --git a/services/src/main/java/org/keycloak/broker/oidc/mappers/UsernameTemplateMapper.java b/services/src/main/java/org/keycloak/broker/oidc/mappers/UsernameTemplateMapper.java index 6d150bdb83..3da249e066 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/mappers/UsernameTemplateMapper.java +++ b/services/src/main/java/org/keycloak/broker/oidc/mappers/UsernameTemplateMapper.java @@ -21,6 +21,7 @@ import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory; import org.keycloak.broker.oidc.OIDCIdentityProviderFactory; 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; @@ -41,7 +42,10 @@ import org.keycloak.social.stackoverflow.StackoverflowIdentityProviderFactory; import org.keycloak.social.twitter.TwitterIdentityProviderFactory; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -70,6 +74,7 @@ public class UsernameTemplateMapper extends AbstractClaimMapper { }; private static final List configProperties = new ArrayList(); + private static final Set IDENTITY_PROVIDER_SYNC_MODES = new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values())); public static final String TEMPLATE = "template"; @@ -86,6 +91,11 @@ public class UsernameTemplateMapper extends AbstractClaimMapper { public static final String PROVIDER_ID = "oidc-username-idp-mapper"; + @Override + public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) { + return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode); + } + @Override public List getConfigProperties() { return configProperties; @@ -111,14 +121,27 @@ public class UsernameTemplateMapper extends AbstractClaimMapper { return "Username Template Importer"; } + @Override + public void updateBrokeredUserLegacy(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + } + @Override public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + // preprocessFederatedIdentity gets called anyways, so we only need to set the username if necessary. + // However, we don't want to set the username when the email is used as username + if (!realm.isRegistrationEmailAsUsername()) { + user.setUsername(context.getModelUsername()); + } } static Pattern substitution = Pattern.compile("\\$\\{([^}]+)\\}"); @Override public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + setUserNameFromTemplate(mapperModel, context); + } + + private void setUserNameFromTemplate(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { String template = mapperModel.getConfig().get(TEMPLATE); Matcher m = substitution.matcher(template); StringBuffer sb = new StringBuffer(); @@ -141,7 +164,6 @@ public class UsernameTemplateMapper extends AbstractClaimMapper { m.appendTail(sb); String username = sb.toString(); context.setModelUsername(username); - } @Override diff --git a/services/src/main/java/org/keycloak/broker/provider/HardcodedAttributeMapper.java b/services/src/main/java/org/keycloak/broker/provider/HardcodedAttributeMapper.java index b31a2491c0..bc827ea559 100755 --- a/services/src/main/java/org/keycloak/broker/provider/HardcodedAttributeMapper.java +++ b/services/src/main/java/org/keycloak/broker/provider/HardcodedAttributeMapper.java @@ -18,13 +18,17 @@ package org.keycloak.broker.provider; 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; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * @author Bill Burke @@ -34,6 +38,7 @@ public class HardcodedAttributeMapper extends AbstractIdentityProviderMapper { public static final String ATTRIBUTE = "attribute"; public static final String ATTRIBUTE_VALUE = "attribute.value"; protected static final List configProperties = new ArrayList(); + private static final Set IDENTITY_PROVIDER_SYNC_MODES = new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values())); static { ProviderConfigProperty property; @@ -51,7 +56,10 @@ public class HardcodedAttributeMapper extends AbstractIdentityProviderMapper { configProperties.add(property); } - + @Override + public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) { + return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode); + } @Override public List getConfigProperties() { diff --git a/services/src/main/java/org/keycloak/broker/provider/HardcodedRoleMapper.java b/services/src/main/java/org/keycloak/broker/provider/HardcodedRoleMapper.java index 9d04d2f0b7..5789f15518 100755 --- a/services/src/main/java/org/keycloak/broker/provider/HardcodedRoleMapper.java +++ b/services/src/main/java/org/keycloak/broker/provider/HardcodedRoleMapper.java @@ -18,6 +18,7 @@ package org.keycloak.broker.provider; import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; @@ -26,14 +27,18 @@ import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.provider.ProviderConfigProperty; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * @author Bill Burke * @version $Revision: 1 $ */ public class HardcodedRoleMapper extends AbstractIdentityProviderMapper { - protected static final List configProperties = new ArrayList<>(); + protected static final List configProperties = new ArrayList(); + private static final Set IDENTITY_PROVIDER_SYNC_MODES = new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values())); static { ProviderConfigProperty property; @@ -65,6 +70,11 @@ public class HardcodedRoleMapper extends AbstractIdentityProviderMapper { public static final String PROVIDER_ID = "oidc-hardcoded-role-idp-mapper"; + @Override + public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) { + return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode); + } + @Override public String getId() { return PROVIDER_ID; @@ -77,6 +87,10 @@ public class HardcodedRoleMapper extends AbstractIdentityProviderMapper { @Override public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + grantUserRole(realm, user, mapperModel); + } + + private void grantUserRole(RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel) { String roleName = mapperModel.getConfig().get(ConfigConstants.ROLE); RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName); if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName); @@ -85,7 +99,11 @@ public class HardcodedRoleMapper extends AbstractIdentityProviderMapper { @Override public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + grantUserRole(realm, user, mapperModel); + } + @Override + public void updateBrokeredUserLegacy(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { } @Override diff --git a/services/src/main/java/org/keycloak/broker/provider/HardcodedUserSessionAttributeMapper.java b/services/src/main/java/org/keycloak/broker/provider/HardcodedUserSessionAttributeMapper.java index 5a8a2ed276..530e3318cd 100755 --- a/services/src/main/java/org/keycloak/broker/provider/HardcodedUserSessionAttributeMapper.java +++ b/services/src/main/java/org/keycloak/broker/provider/HardcodedUserSessionAttributeMapper.java @@ -18,13 +18,17 @@ package org.keycloak.broker.provider; 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; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * @author Bill Burke @@ -34,6 +38,7 @@ public class HardcodedUserSessionAttributeMapper extends AbstractIdentityProvide public static final String ATTRIBUTE = "attribute"; public static final String ATTRIBUTE_VALUE = "attribute.value"; protected static final List configProperties = new ArrayList(); + private static final Set IDENTITY_PROVIDER_SYNC_MODES = new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values())); static { ProviderConfigProperty property; @@ -51,7 +56,10 @@ public class HardcodedUserSessionAttributeMapper extends AbstractIdentityProvide configProperties.add(property); } - + @Override + public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) { + return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode); + } @Override public List getConfigProperties() { diff --git a/services/src/main/java/org/keycloak/broker/saml/mappers/AttributeToRoleMapper.java b/services/src/main/java/org/keycloak/broker/saml/mappers/AttributeToRoleMapper.java index 56088d729c..c2b8460d64 100755 --- a/services/src/main/java/org/keycloak/broker/saml/mappers/AttributeToRoleMapper.java +++ b/services/src/main/java/org/keycloak/broker/saml/mappers/AttributeToRoleMapper.java @@ -27,6 +27,7 @@ import org.keycloak.dom.saml.v2.assertion.AssertionType; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; import org.keycloak.dom.saml.v2.assertion.AttributeType; import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; @@ -35,8 +36,11 @@ import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.provider.ProviderConfigProperty; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Optional; +import java.util.Set; /** * @author Bill Burke @@ -51,6 +55,7 @@ public class AttributeToRoleMapper extends AbstractIdentityProviderMapper { public static final String ATTRIBUTE_NAME = "attribute.name"; public static final String ATTRIBUTE_FRIENDLY_NAME = "attribute.friendly.name"; public static final String ATTRIBUTE_VALUE = "attribute.value"; + private static final Set IDENTITY_PROVIDER_SYNC_MODES = new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values())); static { ProviderConfigProperty property; @@ -82,6 +87,11 @@ public class AttributeToRoleMapper extends AbstractIdentityProviderMapper { public static final String PROVIDER_ID = "saml-role-idp-mapper"; + @Override + public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) { + return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode); + } + @Override public List getConfigProperties() { return configProperties; diff --git a/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeMapper.java b/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeMapper.java index cf550ed513..419d11d658 100755 --- a/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeMapper.java +++ b/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeMapper.java @@ -25,11 +25,20 @@ import org.keycloak.common.util.CollectionUtil; import org.keycloak.dom.saml.v2.assertion.AssertionType; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; import org.keycloak.dom.saml.v2.assertion.AttributeType; -import org.keycloak.models.*; +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; import org.keycloak.saml.common.util.StringUtil; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -50,6 +59,7 @@ public class UserAttributeMapper extends AbstractIdentityProviderMapper { private static final String EMAIL = "email"; private static final String FIRST_NAME = "firstName"; private static final String LAST_NAME = "lastName"; + private static final Set IDENTITY_PROVIDER_SYNC_MODES = new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values())); static { ProviderConfigProperty property; @@ -75,6 +85,11 @@ public class UserAttributeMapper extends AbstractIdentityProviderMapper { public static final String PROVIDER_ID = "saml-user-attribute-idp-mapper"; + @Override + public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) { + return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode); + } + @Override public List getConfigProperties() { return configProperties; @@ -183,7 +198,7 @@ public class UserAttributeMapper extends AbstractIdentityProviderMapper { // attribute sent by brokered idp has different values as before, update it user.setAttribute(attribute, attributeValuesInContext); } - // attribute allready set + // attribute already set } } diff --git a/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeStatementMapper.java b/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeStatementMapper.java deleted file mode 100644 index 4856fb6473..0000000000 --- a/services/src/main/java/org/keycloak/broker/saml/mappers/UserAttributeStatementMapper.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright (c) eHealth - */ -package org.keycloak.broker.saml.mappers; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.function.Consumer; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -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.common.util.CollectionUtil; -import org.keycloak.dom.saml.v2.assertion.AssertionType; -import org.keycloak.dom.saml.v2.assertion.AttributeStatementType.ASTChoiceType; -import org.keycloak.dom.saml.v2.assertion.AttributeType; -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; - -/** - * @author Frederik Libert - * - */ -public class UserAttributeStatementMapper extends AbstractIdentityProviderMapper { - - private static final String USER_ATTR_LOCALE = "locale"; - - private static final String[] COMPATIBLE_PROVIDERS = {SAMLIdentityProviderFactory.PROVIDER_ID}; - - private static final List CONFIG_PROPERTIES = new ArrayList<>(); - - public static final String ATTRIBUTE_NAME_PATTERN = "attribute.name.pattern"; - - public static final String USER_ATTRIBUTE_FIRST_NAME = "user.attribute.firstName"; - - public static final String USER_ATTRIBUTE_LAST_NAME = "user.attribute.lastName"; - - public static final String USER_ATTRIBUTE_EMAIL = "user.attribute.email"; - - public static final String USER_ATTRIBUTE_LANGUAGE = "user.attribute.language"; - - private static final String USE_FRIENDLY_NAMES = "use.friendly.names"; - - static { - ProviderConfigProperty property; - property = new ProviderConfigProperty(); - property.setName(ATTRIBUTE_NAME_PATTERN); - property.setLabel("Attribute Name Pattern"); - property.setHelpText("Pattern of attribute names in assertion that must be mapped. Leave blank to map all attributes."); - property.setType(ProviderConfigProperty.STRING_TYPE); - CONFIG_PROPERTIES.add(property); - property = new ProviderConfigProperty(); - property.setName(USER_ATTRIBUTE_FIRST_NAME); - property.setLabel("User Attribute FirstName"); - property.setHelpText("Define which saml Attribute must be mapped to the User property firstName."); - property.setType(ProviderConfigProperty.STRING_TYPE); - CONFIG_PROPERTIES.add(property); - property = new ProviderConfigProperty(); - property.setName(USER_ATTRIBUTE_LAST_NAME); - property.setLabel("User Attribute LastName"); - property.setHelpText("Define which saml Attribute must be mapped to the User property lastName."); - property.setType(ProviderConfigProperty.STRING_TYPE); - CONFIG_PROPERTIES.add(property); - property = new ProviderConfigProperty(); - property.setName(USER_ATTRIBUTE_EMAIL); - property.setLabel("User Attribute Email"); - property.setHelpText("Define which saml Attribute must be mapped to the User property email."); - property.setType(ProviderConfigProperty.STRING_TYPE); - CONFIG_PROPERTIES.add(property); - property = new ProviderConfigProperty(); - property.setName(USER_ATTRIBUTE_LANGUAGE); - property.setLabel("User Attribute Language"); - property.setHelpText("Define which saml Attribute must be mapped to the User attribute locale."); - property.setType(ProviderConfigProperty.STRING_TYPE); - CONFIG_PROPERTIES.add(property); - property = new ProviderConfigProperty(); - property.setName(USE_FRIENDLY_NAMES); - property.setLabel("Use Attribute Friendly Name"); - property.setHelpText("Define which name to give to each mapped user attribute: name or friendlyName."); - property.setType(ProviderConfigProperty.BOOLEAN_TYPE); - CONFIG_PROPERTIES.add(property); - } - - public static final String PROVIDER_ID = "saml-user-attributestatement-idp-mapper"; - - @Override - public List getConfigProperties() { - return CONFIG_PROPERTIES; - } - - @Override - public String getId() { - return PROVIDER_ID; - } - - @Override - public String[] getCompatibleProviders() { - return COMPATIBLE_PROVIDERS.clone(); - } - - @Override - public String getDisplayCategory() { - return "AttributeStatement Importer"; - } - - @Override - public String getDisplayType() { - return "AttributeStatement Importer"; - } - - @Override - public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { - String firstNameAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_FIRST_NAME); - String lastNameAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_LAST_NAME); - String emailAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_EMAIL); - String langAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_LANGUAGE); - Boolean useFriendlyNames = Boolean.valueOf(mapperModel.getConfig().get(USE_FRIENDLY_NAMES)); - List attributesInContext = findAttributesInContext(context, getAttributePattern(mapperModel)); - for (AttributeType a : attributesInContext) { - String attribute = useFriendlyNames ? a.getFriendlyName() : a.getName(); - List attributeValuesInContext = a.getAttributeValue().stream().filter(Objects::nonNull).map(Object::toString).collect(Collectors.toList()); - if (!attributeValuesInContext.isEmpty()) { - // set as attribute anyway - context.setUserAttribute(attribute, attributeValuesInContext); - // set as special field ? - if (Objects.equals(attribute, emailAttribute)) { - setIfNotEmpty(context::setEmail, attributeValuesInContext); - } else if (Objects.equals(attribute, firstNameAttribute)) { - setIfNotEmpty(context::setFirstName, attributeValuesInContext); - } else if (Objects.equals(attribute, lastNameAttribute)) { - setIfNotEmpty(context::setLastName, attributeValuesInContext); - } else if (Objects.equals(attribute, langAttribute)) { - context.setUserAttribute(USER_ATTR_LOCALE, attributeValuesInContext); - } - } - } - } - - @Override - public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { - String firstNameAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_FIRST_NAME); - String lastNameAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_LAST_NAME); - String emailAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_EMAIL); - String langAttribute = mapperModel.getConfig().get(USER_ATTRIBUTE_LANGUAGE); - Boolean useFriendlyNames = Boolean.valueOf(mapperModel.getConfig().get(USE_FRIENDLY_NAMES)); - List attributesInContext = findAttributesInContext(context, getAttributePattern(mapperModel)); - - Set assertedUserAttributes = new HashSet(); - for (AttributeType a : attributesInContext) { - String attribute = useFriendlyNames ? a.getFriendlyName() : a.getName(); - List attributeValuesInContext = a.getAttributeValue().stream().filter(Objects::nonNull).map(Object::toString).collect(Collectors.toList()); - List currentAttributeValues = user.getAttributes().get(attribute); - if (attributeValuesInContext == null) { - // attribute no longer sent by brokered idp, remove it - user.removeAttribute(attribute); - } else if (currentAttributeValues == null) { - // new attribute sent by brokered idp, add it - user.setAttribute(attribute, attributeValuesInContext); - } else if (!CollectionUtil.collectionEquals(attributeValuesInContext, currentAttributeValues)) { - // attribute sent by brokered idp has different values as before, update it - user.setAttribute(attribute, attributeValuesInContext); - } - if (Objects.equals(attribute, emailAttribute)) { - setIfNotEmpty(context::setEmail, attributeValuesInContext); - } else if (Objects.equals(attribute, firstNameAttribute)) { - setIfNotEmpty(context::setFirstName, attributeValuesInContext); - } else if (Objects.equals(attribute, lastNameAttribute)) { - setIfNotEmpty(context::setLastName, attributeValuesInContext); - } else if (Objects.equals(attribute, langAttribute)) { - if(attributeValuesInContext == null) { - user.removeAttribute(USER_ATTR_LOCALE); - } else { - user.setAttribute(USER_ATTR_LOCALE, attributeValuesInContext); - } - assertedUserAttributes.add(USER_ATTR_LOCALE); - } - // Mark attribute as handled - assertedUserAttributes.add(attribute); - } - // Remove user attributes that were not referenced in assertion. - user.getAttributes().keySet().stream().filter(a -> !assertedUserAttributes.contains(a)).forEach(a -> user.removeAttribute(a)); - } - - @Override - public String getHelpText() { - return "Import all saml attributes found in attributestatements in assertion into user properties or attributes."; - } - - private Optional getAttributePattern(IdentityProviderMapperModel mapperModel) { - String attributePatternConfig = mapperModel.getConfig().get(ATTRIBUTE_NAME_PATTERN); - return Optional.ofNullable(attributePatternConfig != null ? Pattern.compile(attributePatternConfig) : null); - } - - private List findAttributesInContext(BrokeredIdentityContext context, Optional attributePattern) { - AssertionType assertion = (AssertionType) context.getContextData().get(SAMLEndpoint.SAML_ASSERTION); - - return assertion.getAttributeStatements().stream()// - .flatMap(statement -> statement.getAttributes().stream())// - .filter(item -> !attributePattern.isPresent() || attributePattern.get().matcher(item.getAttribute().getName()).matches())// - .map(ASTChoiceType::getAttribute)// - .collect(Collectors.toList()); - } - - private void setIfNotEmpty(Consumer consumer, List values) { - if (values != null && !values.isEmpty()) { - consumer.accept(values.get(0)); - } - } - -} diff --git a/services/src/main/java/org/keycloak/broker/saml/mappers/UsernameTemplateMapper.java b/services/src/main/java/org/keycloak/broker/saml/mappers/UsernameTemplateMapper.java index 76adddc7fc..befb917931 100755 --- a/services/src/main/java/org/keycloak/broker/saml/mappers/UsernameTemplateMapper.java +++ b/services/src/main/java/org/keycloak/broker/saml/mappers/UsernameTemplateMapper.java @@ -27,6 +27,7 @@ import org.keycloak.dom.saml.v2.assertion.AttributeType; import org.keycloak.dom.saml.v2.assertion.NameIDType; import org.keycloak.dom.saml.v2.assertion.SubjectType; 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; @@ -34,7 +35,10 @@ import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.provider.ProviderConfigProperty; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -50,6 +54,8 @@ public class UsernameTemplateMapper extends AbstractIdentityProviderMapper { public static final String TEMPLATE = "template"; + private static final Set IDENTITY_PROVIDER_SYNC_MODES = new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values())); + static { ProviderConfigProperty property; property = new ProviderConfigProperty(); @@ -63,6 +69,11 @@ public class UsernameTemplateMapper extends AbstractIdentityProviderMapper { public static final String PROVIDER_ID = "saml-username-idp-mapper"; + @Override + public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) { + return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode); + } + @Override public List getConfigProperties() { return configProperties; @@ -89,13 +100,26 @@ public class UsernameTemplateMapper extends AbstractIdentityProviderMapper { } @Override - public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { - + public void updateBrokeredUserLegacy(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { } + + @Override + public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + // preprocessFederatedIdentity gets called anyways, so we only need to set the username if necessary. + // However, we don't want to set the username when the email is used as username + if (!realm.isRegistrationEmailAsUsername()) { + user.setUsername(context.getModelUsername()); + } + } + static Pattern substitution = Pattern.compile("\\$\\{([^}]+)\\}"); @Override public void preprocessFederatedIdentity(KeycloakSession session, RealmModel realm, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { + setUserNameFromTemplate(mapperModel, context); + } + + private void setUserNameFromTemplate(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) { AssertionType assertion = (AssertionType)context.getContextData().get(SAMLEndpoint.SAML_ASSERTION); String template = mapperModel.getConfig().get(TEMPLATE); Matcher m = substitution.matcher(template); @@ -134,7 +158,6 @@ public class UsernameTemplateMapper extends AbstractIdentityProviderMapper { } m.appendTail(sb); context.setModelUsername(sb.toString()); - } @Override diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index d00b3c276e..6408355503 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -33,6 +33,7 @@ import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken; import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderFactory; import org.keycloak.broker.provider.IdentityProviderMapper; +import org.keycloak.broker.provider.IdentityProviderMapperSyncModeDelegate; import org.keycloak.common.ClientConnection; import org.keycloak.common.Profile; import org.keycloak.common.constants.ServiceAccountConstants; @@ -1077,7 +1078,7 @@ public class TokenEndpoint { KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); for (IdentityProviderMapperModel mapper : mappers) { IdentityProviderMapper target = (IdentityProviderMapper)sessionFactory.getProviderFactory(IdentityProviderMapper.class, mapper.getIdentityProviderMapper()); - target.updateBrokeredUser(session, realm, user, mapper, context); + IdentityProviderMapperSyncModeDelegate.delegateUpdateBrokeredUser(session, realm, user, mapper, context, target); } } } diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index af8744e0ac..6cfd2961fe 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -32,15 +32,14 @@ import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.broker.provider.IdentityProviderFactory; import org.keycloak.broker.provider.IdentityProviderMapper; +import org.keycloak.broker.provider.IdentityProviderMapperSyncModeDelegate; import org.keycloak.broker.provider.util.IdentityBrokerState; import org.keycloak.broker.saml.SAMLEndpoint; -import org.keycloak.broker.saml.SAMLIdentityProvider; import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.common.ClientConnection; import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.ObjectUtil; import org.keycloak.common.util.Time; -import org.keycloak.dom.saml.v2.SAML2Object; import org.keycloak.dom.saml.v2.protocol.StatusResponseType; import org.keycloak.events.Details; import org.keycloak.events.Errors; @@ -1001,7 +1000,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); for (IdentityProviderMapperModel mapper : mappers) { IdentityProviderMapper target = (IdentityProviderMapper)sessionFactory.getProviderFactory(IdentityProviderMapper.class, mapper.getIdentityProviderMapper()); - target.updateBrokeredUser(session, realmModel, federatedUser, mapper, context); + IdentityProviderMapperSyncModeDelegate.delegateUpdateBrokeredUser(session, realmModel, federatedUser, mapper, context, target); } } diff --git a/services/src/main/java/org/keycloak/social/github/GitHubUserAttributeMapper.java b/services/src/main/java/org/keycloak/social/github/GitHubUserAttributeMapper.java index 5b7d4da3a8..9f12c66371 100644 --- a/services/src/main/java/org/keycloak/social/github/GitHubUserAttributeMapper.java +++ b/services/src/main/java/org/keycloak/social/github/GitHubUserAttributeMapper.java @@ -25,6 +25,7 @@ import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper; */ public class GitHubUserAttributeMapper extends AbstractJsonUserAttributeMapper { + public static final String PROVIDER_ID = "github-user-attribute-mapper"; private static final String[] cp = new String[] { GitHubIdentityProviderFactory.PROVIDER_ID }; @Override @@ -34,7 +35,7 @@ public class GitHubUserAttributeMapper extends AbstractJsonUserAttributeMapper { @Override public String getId() { - return "github-user-attribute-mapper"; + return PROVIDER_ID; } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java index c6911e7742..b82212c832 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IdentityProviderTest.java @@ -32,6 +32,10 @@ import org.keycloak.dom.saml.v2.metadata.KeyTypes; import org.keycloak.dom.saml.v2.metadata.SPSSODescriptorType; import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.ResourceType; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderMapperSyncMode; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.models.utils.StripSecretsUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.representations.idm.AdminEventRepresentation; @@ -140,6 +144,7 @@ public class IdentityProviderTest extends AbstractAdminTest { public void testCreate() { IdentityProviderRepresentation newIdentityProvider = createRep("new-identity-provider", "oidc"); + newIdentityProvider.getConfig().put(IdentityProviderModel.SYNC_MODE, "IMPORT"); newIdentityProvider.getConfig().put("clientId", "clientId"); newIdentityProvider.getConfig().put("clientSecret", "some secret value"); @@ -156,6 +161,7 @@ public class IdentityProviderTest extends AbstractAdminTest { assertNotNull(representation.getInternalId()); assertEquals("new-identity-provider", representation.getAlias()); assertEquals("oidc", representation.getProviderId()); + assertEquals("IMPORT", representation.getConfig().get(IdentityProviderMapperModel.SYNC_MODE)); assertEquals("clientId", representation.getConfig().get("clientId")); assertEquals(ComponentRepresentation.SECRET_VALUE, representation.getConfig().get("clientSecret")); assertTrue(representation.isEnabled()); @@ -243,6 +249,7 @@ public class IdentityProviderTest extends AbstractAdminTest { public void testCreateWithBasicAuth() { IdentityProviderRepresentation newIdentityProvider = createRep("new-identity-provider", "oidc"); + newIdentityProvider.getConfig().put(IdentityProviderModel.SYNC_MODE, "IMPORT"); newIdentityProvider.getConfig().put("clientId", "clientId"); newIdentityProvider.getConfig().put("clientSecret", "some secret value"); newIdentityProvider.getConfig().put("clientAuthMethod",OIDCLoginProtocol.CLIENT_SECRET_BASIC); @@ -260,6 +267,7 @@ public class IdentityProviderTest extends AbstractAdminTest { assertNotNull(representation.getInternalId()); assertEquals("new-identity-provider", representation.getAlias()); assertEquals("oidc", representation.getProviderId()); + assertEquals("IMPORT", representation.getConfig().get(IdentityProviderMapperModel.SYNC_MODE)); assertEquals("clientId", representation.getConfig().get("clientId")); assertEquals(ComponentRepresentation.SECRET_VALUE, representation.getConfig().get("clientSecret")); assertEquals(OIDCLoginProtocol.CLIENT_SECRET_BASIC, representation.getConfig().get("clientAuthMethod")); @@ -278,6 +286,7 @@ public class IdentityProviderTest extends AbstractAdminTest { public void testCreateWithJWT() { IdentityProviderRepresentation newIdentityProvider = createRep("new-identity-provider", "oidc"); + newIdentityProvider.getConfig().put(IdentityProviderModel.SYNC_MODE, "IMPORT"); newIdentityProvider.getConfig().put("clientId", "clientId"); newIdentityProvider.getConfig().put("clientAuthMethod", OIDCLoginProtocol.PRIVATE_KEY_JWT); @@ -294,6 +303,7 @@ public class IdentityProviderTest extends AbstractAdminTest { assertNotNull(representation.getInternalId()); assertEquals("new-identity-provider", representation.getAlias()); assertEquals("oidc", representation.getProviderId()); + assertEquals("IMPORT", representation.getConfig().get(IdentityProviderMapperModel.SYNC_MODE)); assertEquals("clientId", representation.getConfig().get("clientId")); assertNull(representation.getConfig().get("clientSecret")); assertEquals(OIDCLoginProtocol.PRIVATE_KEY_JWT, representation.getConfig().get("clientAuthMethod")); @@ -306,6 +316,7 @@ public class IdentityProviderTest extends AbstractAdminTest { public void testUpdate() { IdentityProviderRepresentation newIdentityProvider = createRep("update-identity-provider", "oidc"); + newIdentityProvider.getConfig().put(IdentityProviderModel.SYNC_MODE, "IMPORT"); newIdentityProvider.getConfig().put("clientId", "clientId"); newIdentityProvider.getConfig().put("clientSecret", "some secret value"); @@ -676,6 +687,7 @@ public class IdentityProviderTest extends AbstractAdminTest { mapper.setIdentityProviderMapper("oidc-hardcoded-role-idp-mapper"); Map config = new HashMap<>(); config.put("role", "offline_access"); + config.put(IdentityProviderMapperModel.SYNC_MODE, IdentityProviderMapperSyncMode.INHERIT.toString()); mapper.setConfig(config); // createRep and add mapper @@ -692,6 +704,7 @@ public class IdentityProviderTest extends AbstractAdminTest { // get mapper mapper = provider.getMapperById(id); + Assert.assertEquals("INHERIT", mappers.get(0).getConfig().get(IdentityProviderMapperModel.SYNC_MODE)); Assert.assertNotNull("mapperById not null", mapper); Assert.assertEquals("mapper id", id, mapper.getId()); Assert.assertNotNull("mapper.config exists", mapper.getConfig()); @@ -734,6 +747,7 @@ public class IdentityProviderTest extends AbstractAdminTest { mapper.setName("my_mapper"); mapper.setIdentityProviderMapper("oidc-hardcoded-role-idp-mapper"); Map config = new HashMap<>(); + config.put(IdentityProviderMapperModel.SYNC_MODE, IdentityProviderMapperSyncMode.INHERIT.toString()); config.put("role", ""); mapper.setConfig(config); @@ -743,7 +757,7 @@ public class IdentityProviderTest extends AbstractAdminTest { List mappers = provider.getMappers(); assertEquals(1, mappers.size()); - assertEquals(0, mappers.get(0).getConfig().size()); + assertEquals(1, mappers.get(0).getConfig().size()); mapper = provider.getMapperById(mapperId); mapper.getConfig().put("role", "offline_access"); @@ -751,8 +765,9 @@ public class IdentityProviderTest extends AbstractAdminTest { provider.update(mapperId, mapper); mappers = provider.getMappers(); + assertEquals("INHERIT", mappers.get(0).getConfig().get(IdentityProviderMapperModel.SYNC_MODE)); assertEquals(1, mappers.size()); - assertEquals(1, mappers.get(0).getConfig().size()); + assertEquals(2, mappers.get(0).getConfig().size()); assertEquals("offline_access", mappers.get(0).getConfig().get("role")); } @@ -768,6 +783,7 @@ public class IdentityProviderTest extends AbstractAdminTest { mapper.setName("my_mapper"); mapper.setIdentityProviderMapper("oidc-hardcoded-role-idp-mapper"); Map config = new HashMap<>(); + config.put(IdentityProviderMapperModel.SYNC_MODE, IdentityProviderMapperSyncMode.INHERIT.toString()); config.put("role", "offline_access"); mapper.setConfig(config); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractAdvancedBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractAdvancedBrokerTest.java index 35dd083bcb..4936be846f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractAdvancedBrokerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractAdvancedBrokerTest.java @@ -1,23 +1,13 @@ package org.keycloak.testsuite.broker; -import java.net.URI; -import java.util.Collections; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; - -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientRequestFilter; -import javax.ws.rs.client.WebTarget; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.Response; - import org.junit.Test; import org.keycloak.admin.client.resource.IdentityProviderResource; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.common.util.Time; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderMapperSyncMode; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ComponentRepresentation; import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; @@ -35,6 +25,19 @@ import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.RealmBuilder; import org.openqa.selenium.TimeoutException; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; @@ -63,21 +66,25 @@ public abstract class AbstractAdvancedBrokerTest extends AbstractBrokerTest { protected void createRoleMappersForConsumerRealm() { + createRoleMappersForConsumerRealm(IdentityProviderMapperSyncMode.FORCE); + } + + protected void createRoleMappersForConsumerRealm(IdentityProviderMapperSyncMode syncMode) { log.debug("adding mappers to identity provider in realm " + bc.consumerRealmName()); RealmResource realm = adminClient.realm(bc.consumerRealmName()); IdentityProviderResource idpResource = realm.identityProviders().get(bc.getIDPAlias()); - for (IdentityProviderMapperRepresentation mapper : createIdentityProviderMappers()) { + for (IdentityProviderMapperRepresentation mapper : createIdentityProviderMappers(syncMode)) { mapper.setIdentityProviderAlias(bc.getIDPAlias()); Response resp = idpResource.addMapper(mapper); resp.close(); } } - protected abstract Iterable createIdentityProviderMappers(); - + protected abstract Iterable createIdentityProviderMappers(IdentityProviderMapperSyncMode syncMode); + protected abstract void createAdditionalMapperWithCustomSyncMode(IdentityProviderMapperSyncMode syncMode); /** * Refers to in old test suite: org.keycloak.testsuite.broker.AbstractKeycloakIdentityProviderTest#testAccountManagementLinkIdentity @@ -315,18 +322,35 @@ public abstract class AbstractAdvancedBrokerTest extends AbstractBrokerTest { assertEquals("Account is disabled, contact your administrator.", errorPage.getError()); } - - - - - // KEYCLOAK-3987 @Test - public void grantNewRoleFromToken() { + public void mapperDoesNotGrantNewRoleFromTokenWithSyncModeImport() { + testMapperAssigningRoles(IdentityProviderMapperSyncMode.IMPORT, false); + } + + @Test + public void mapperGrantsNewRoleFromTokenWithInheritedSyncModeForce() { + RealmResource realm = adminClient.realm(bc.consumerRealmName()); + realm.identityProviders().get(bc.getIDPAlias()) + .update(bc.setUpIdentityProvider(suiteContext, IdentityProviderSyncMode.FORCE)); + + testMapperAssigningRoles(IdentityProviderMapperSyncMode.INHERIT, true); + } + + @Test + public void mapperDoesNotGrantNewRoleFromTokenWithInheritedSyncModeImport() { + RealmResource realm = adminClient.realm(bc.consumerRealmName()); + realm.identityProviders().get(bc.getIDPAlias()) + .update(bc.setUpIdentityProvider(suiteContext, IdentityProviderSyncMode.IMPORT)); + + testMapperAssigningRoles(IdentityProviderMapperSyncMode.INHERIT, false); + } + + private void testMapperAssigningRoles(IdentityProviderMapperSyncMode anImport, boolean isAssigned) { createRolesForRealm(bc.providerRealmName()); createRolesForRealm(bc.consumerRealmName()); - createRoleMappersForConsumerRealm(); + createRoleMappersForConsumerRealm(anImport); RoleRepresentation managerRole = adminClient.realm(bc.providerRealmName()).roles().get(ROLE_MANAGER).toRepresentation(); RoleRepresentation userRole = adminClient.realm(bc.providerRealmName()).roles().get(ROLE_USER).toRepresentation(); @@ -336,7 +360,9 @@ public abstract class AbstractAdvancedBrokerTest extends AbstractBrokerTest { logInAsUserInIDPForFirstTime(); - Set currentRoles = userResource.roles().realmLevel().listAll().stream() + UserResource consumerUserResource = adminClient.realm(bc.consumerRealmName()).users().get( + adminClient.realm(bc.consumerRealmName()).users().search(bc.getUserLogin()).get(0).getId()); + Set currentRoles = consumerUserResource.roles().realmLevel().listAll().stream() .map(RoleRepresentation::getName) .collect(Collectors.toSet()); @@ -350,15 +376,63 @@ public abstract class AbstractAdvancedBrokerTest extends AbstractBrokerTest { logInAsUserInIDP(); - currentRoles = userResource.roles().realmLevel().listAll().stream() + currentRoles = consumerUserResource.roles().realmLevel().listAll().stream() .map(RoleRepresentation::getName) .collect(Collectors.toSet()); - assertThat(currentRoles, hasItems(ROLE_MANAGER, ROLE_USER)); + if (isAssigned) { + assertThat(currentRoles, hasItems(ROLE_MANAGER, ROLE_USER)); + } else { + assertThat(currentRoles, hasItems(ROLE_MANAGER)); + assertThat(currentRoles, not(hasItems(ROLE_USER))); + } - logoutFromRealm(bc.providerRealmName()); logoutFromRealm(bc.consumerRealmName()); + logoutFromRealm(bc.providerRealmName()); } + @Test + public void differentMappersCanHaveDifferentSyncModes() { + createRolesForRealm(bc.providerRealmName()); + createRolesForRealm(bc.consumerRealmName()); + + createRoleMappersForConsumerRealm(IdentityProviderMapperSyncMode.INHERIT); + createAdditionalMapperWithCustomSyncMode(IdentityProviderMapperSyncMode.FORCE); + + + RoleRepresentation managerRole = adminClient.realm(bc.providerRealmName()).roles().get(ROLE_MANAGER).toRepresentation(); + RoleRepresentation userRole = adminClient.realm(bc.providerRealmName()).roles().get(ROLE_USER).toRepresentation(); + RoleRepresentation friendlyManagerRole = adminClient.realm(bc.providerRealmName()).roles().get(ROLE_FRIENDLY_MANAGER).toRepresentation(); + + UserResource userResource = adminClient.realm(bc.providerRealmName()).users().get(userId); + userResource.roles().realmLevel().add(Collections.singletonList(managerRole)); + + logInAsUserInIDPForFirstTime(); + + UserResource consumerUserResource = adminClient.realm(bc.consumerRealmName()).users().get( + adminClient.realm(bc.consumerRealmName()).users().search(bc.getUserLogin()).get(0).getId()); + Set currentRoles = consumerUserResource.roles().realmLevel().listAll().stream() + .map(RoleRepresentation::getName) + .collect(Collectors.toSet()); + + assertThat(currentRoles, hasItems(ROLE_MANAGER)); + assertThat(currentRoles, not(hasItems(ROLE_USER, ROLE_FRIENDLY_MANAGER))); + + logoutFromRealm(bc.consumerRealmName()); + + + userResource.roles().realmLevel().add(Arrays.asList(userRole, friendlyManagerRole)); + + logInAsUserInIDP(); + + currentRoles = consumerUserResource.roles().realmLevel().listAll().stream() + .map(RoleRepresentation::getName) + .collect(Collectors.toSet()); + assertThat(currentRoles, hasItems(ROLE_MANAGER, ROLE_FRIENDLY_MANAGER)); + assertThat(currentRoles, not(hasItems(ROLE_USER))); + + logoutFromRealm(bc.consumerRealmName()); + logoutFromRealm(bc.providerRealmName()); + } // KEYCLOAK-4016 @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java index 61cb364374..2e0e7ebd56 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java @@ -14,6 +14,8 @@ import org.keycloak.admin.client.resource.IdentityProviderResource; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.broker.provider.HardcodedUserSessionAttributeMapper; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.models.UserModel; import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; @@ -582,6 +584,7 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa hardCodedSessionNoteMapper.setIdentityProviderAlias(bc.getIDPAlias()); hardCodedSessionNoteMapper.setIdentityProviderMapper(HardcodedUserSessionAttributeMapper.PROVIDER_ID); hardCodedSessionNoteMapper.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, IdentityProviderSyncMode.IMPORT.toString()) .put(HardcodedUserSessionAttributeMapper.ATTRIBUTE_VALUE, "sessionvalue") .put(HardcodedUserSessionAttributeMapper.ATTRIBUTE, "user-session-attr") .build()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderMapperTest.java new file mode 100644 index 0000000000..331b11a8a5 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderMapperTest.java @@ -0,0 +1,84 @@ +package org.keycloak.testsuite.broker; + +import org.junit.Before; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.MappingsRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.util.UserBuilder; + +import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.Assert.assertThat; +import static org.keycloak.testsuite.admin.ApiUtil.createUserAndResetPasswordWithAdminClient; + +/** + * @author hmlnarik + * Benjamin Weimer, + * Martin Idel, + */ +public abstract class AbstractIdentityProviderMapperTest extends AbstractBaseBrokerTest { + + protected RealmResource realm; + + @Before + public void addClients() { + addClientsToProviderAndConsumer(); + realm = adminClient.realm(bc.consumerRealmName()); + } + + protected IdentityProviderRepresentation setupIdentityProvider() { + log.debug("adding identity provider to realm " + bc.consumerRealmName()); + + final IdentityProviderRepresentation idp = bc.setUpIdentityProvider(suiteContext); + realm.identityProviders().create(idp).close(); + return idp; + } + + protected void createUserInProviderRealm(Map> attributes) { + log.debug("Creating user in realm " + bc.providerRealmName()); + + UserRepresentation user = UserBuilder.create() + .username(bc.getUserLogin()) + .email(bc.getUserEmail()) + .build(); + user.setEmailVerified(true); + user.setAttributes(attributes); + + this.userId = createUserAndResetPasswordWithAdminClient(adminClient.realm(bc.providerRealmName()), user, bc.getUserPassword()); + } + + protected UserRepresentation findUser(String realm, String userName, String email) { + UsersResource consumerUsers = adminClient.realm(realm).users(); + + List users = consumerUsers.list(); + assertThat("There must be exactly one user", users, hasSize(1)); + UserRepresentation user = users.get(0); + assertThat("Username has to match", user.getUsername(), equalTo(userName)); + assertThat("Email has to match", user.getEmail(), equalTo(email)); + + MappingsRepresentation roles = consumerUsers.get(user.getId()).roles().getAll(); + + List realmRoles = roles.getRealmMappings().stream() + .map(RoleRepresentation::getName) + .collect(Collectors.toList()); + user.setRealmRoles(realmRoles); + + Map> clientRoles = new HashMap<>(); + roles.getClientMappings().forEach((key, value) -> clientRoles.put(key, value.getMappings().stream() + .map(RoleRepresentation::getName) + .collect(Collectors.toList()))); + user.setClientRoles(clientRoles); + + return user; + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractRoleMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractRoleMapperTest.java new file mode 100644 index 0000000000..4aed848007 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractRoleMapperTest.java @@ -0,0 +1,79 @@ +package org.keycloak.testsuite.broker; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertThat; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.models.IdentityProviderMapperSyncMode; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; + +/** + * @author hmlnarik, + * Benjamin Weimer, + * Martin Idel, + */ +public abstract class AbstractRoleMapperTest extends AbstractIdentityProviderMapperTest { + + private static final String CLIENT = "realm-management"; + private static final String CLIENT_ROLE = "view-realm"; + public static final String ROLE_USER = "user"; + public static final String CLIENT_ROLE_MAPPER_REPRESENTATION = CLIENT + "." + CLIENT_ROLE; + + protected abstract void createMapperInIdp(IdentityProviderRepresentation idp, IdentityProviderMapperSyncMode syncMode); + + protected void updateUser() { + } + + protected UserRepresentation loginAsUserTwiceWithMapper( + IdentityProviderMapperSyncMode syncMode, boolean createAfterFirstLogin, Map> userConfig) { + final IdentityProviderRepresentation idp = setupIdentityProvider(); + if (!createAfterFirstLogin) { + createMapperInIdp(idp, syncMode); + } + createUserInProviderRealm(userConfig); + createUserRoleAndGrantToUserInProviderRealm(); + + logInAsUserInIDPForFirstTime(); + + UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + if (!createAfterFirstLogin) { + assertThatRoleHasBeenAssignedInConsumerRealmTo(user); + } else { + assertThatRoleHasNotBeenAssignedInConsumerRealmTo(user); + } + + if (createAfterFirstLogin) { + createMapperInIdp(idp, syncMode); + } + logoutFromRealm(bc.consumerRealmName()); + + updateUser(); + + logInAsUserInIDP(); + user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + return user; + } + + protected void createUserRoleAndGrantToUserInProviderRealm() { + RoleRepresentation userRole = new RoleRepresentation(ROLE_USER,null, false); + adminClient.realm(bc.providerRealmName()).roles().create(userRole); + RoleRepresentation role = adminClient.realm(bc.providerRealmName()).roles().get(ROLE_USER).toRepresentation(); + UserResource userResource = adminClient.realm(bc.providerRealmName()).users().get(userId); + userResource.roles().realmLevel().add(Collections.singletonList(role)); + } + + protected void assertThatRoleHasBeenAssignedInConsumerRealmTo(UserRepresentation user) { + assertThat(user.getClientRoles().get(CLIENT), contains(CLIENT_ROLE)); + } + + protected void assertThatRoleHasNotBeenAssignedInConsumerRealmTo(UserRepresentation user) { + assertThat(user.getClientRoles().get(CLIENT), not(contains(CLIENT_ROLE))); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractUserAttributeMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractUserAttributeMapperTest.java index 244fe4c3a2..7c2f900d4e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractUserAttributeMapperTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractUserAttributeMapperTest.java @@ -1,33 +1,32 @@ package org.keycloak.testsuite.broker; -import org.keycloak.admin.client.resource.IdentityProviderResource; -import org.keycloak.admin.client.resource.RealmResource; -import org.keycloak.admin.client.resource.UsersResource; -import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; -import org.keycloak.representations.idm.IdentityProviderRepresentation; -import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.testsuite.util.UserBuilder; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; -import javax.ws.rs.core.Response; -import org.junit.Before; -import org.junit.Test; -import static org.junit.Assert.*; -import static org.hamcrest.Matchers.*; -import static org.keycloak.testsuite.admin.ApiUtil.*; +import org.junit.Test; +import org.keycloak.admin.client.resource.IdentityProviderResource; +import org.keycloak.models.IdentityProviderMapperSyncMode; +import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.UserRepresentation; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; /** * * @author hmlnarik */ -public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBrokerTest { +public abstract class AbstractUserAttributeMapperTest extends AbstractIdentityProviderMapperTest { protected static final String MAPPED_ATTRIBUTE_NAME = "mapped-user-attribute"; protected static final String MAPPED_ATTRIBUTE_FRIENDLY_NAME = "mapped-user-attribute-friendly"; @@ -41,60 +40,18 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, MAPPED_ATTRIBUTE_NAME) .build(); - protected abstract Iterable createIdentityProviderMappers(); + protected abstract Iterable createIdentityProviderMappers(IdentityProviderMapperSyncMode syncMode); - @Before - public void addIdentityProviderToConsumerRealm() { - log.debug("adding identity provider to realm " + bc.consumerRealmName()); - - RealmResource realm = adminClient.realm(bc.consumerRealmName()); - final IdentityProviderRepresentation idp = bc.setUpIdentityProvider(suiteContext); - Response resp = realm.identityProviders().create(idp); - resp.close(); + public void addIdentityProviderToConsumerRealm(IdentityProviderMapperSyncMode syncMode) { + IdentityProviderRepresentation idp = setupIdentityProvider(); IdentityProviderResource idpResource = realm.identityProviders().get(idp.getAlias()); - for (IdentityProviderMapperRepresentation mapper : createIdentityProviderMappers()) { + for (IdentityProviderMapperRepresentation mapper : createIdentityProviderMappers(syncMode)) { mapper.setIdentityProviderAlias(bc.getIDPAlias()); - resp = idpResource.addMapper(mapper); - resp.close(); + idpResource.addMapper(mapper).close(); } } - @Before - public void addClients() { - addClientsToProviderAndConsumer(); - } - - protected void createUserInProviderRealm(Map> attributes) { - log.debug("creating user in realm " + bc.providerRealmName()); - - UserRepresentation user = UserBuilder.create() - .username(bc.getUserLogin()) - .email(bc.getUserEmail()) - .build(); - user.setEmailVerified(true); - user.setAttributes(attributes); - this.userId = createUserAndResetPasswordWithAdminClient(adminClient.realm(bc.providerRealmName()), user, bc.getUserPassword()); - } - - private UserRepresentation findUser(String realm, String userName, String email) { - UsersResource consumerUsers = adminClient.realm(realm).users(); - - int userCount = consumerUsers.count(); - assertThat("There must be at least one user", userCount, greaterThan(0)); - - List users = consumerUsers.search("", 0, userCount); - - for (UserRepresentation user : users) { - if (user.getUsername().equals(userName) && user.getEmail().equals(email)) { - return user; - } - } - - fail("User " + userName + " not found in " + realm + " realm"); - return null; - } - private void assertUserAttributes(Map> attrs, UserRepresentation userRep) { Set mappedAttrNames = attrs.entrySet().stream() .filter(me -> me.getValue() != null && ! me.getValue().isEmpty()) @@ -127,7 +84,22 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker } } - protected void testValueMapping(Map> initialUserAttributes, Map> modifiedUserAttributes) { + private void testValueMappingForImportSyncMode(Map> initialUserAttributes, Map> modifiedUserAttributes) { + addIdentityProviderToConsumerRealm(IdentityProviderMapperSyncMode.IMPORT); + testValueMapping(initialUserAttributes, modifiedUserAttributes, initialUserAttributes); + } + + private void testValueMappingForForceSyncMode(Map> initialUserAttributes, Map> modifiedUserAttributes) { + addIdentityProviderToConsumerRealm(IdentityProviderMapperSyncMode.FORCE); + testValueMapping(initialUserAttributes, modifiedUserAttributes, modifiedUserAttributes); + } + + private void testValueMappingForLegacySyncMode(Map> initialUserAttributes, Map> modifiedUserAttributes) { + addIdentityProviderToConsumerRealm(IdentityProviderMapperSyncMode.LEGACY); + testValueMapping(initialUserAttributes, modifiedUserAttributes, modifiedUserAttributes); + } + + private void testValueMapping(Map> initialUserAttributes, Map> modifiedUserAttributes, Map> assertedModifiedAttributes) { String email = bc.getUserEmail(); createUserInProviderRealm(initialUserAttributes); @@ -160,12 +132,23 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker logInAsUserInIDP(); userRep = findUser(bc.consumerRealmName(), bc.getUserLogin(), email); - assertUserAttributes(modifiedUserAttributes, userRep); + assertUserAttributes(assertedModifiedAttributes, userRep); } @Test - public void testBasicMappingSingleValue() { - testValueMapping(ImmutableMap.>builder() + public void testBasicMappingSingleValueForce() { + testValueMappingForForceSyncMode(ImmutableMap.>builder() + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) + .build(), + ImmutableMap.>builder() + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("second value").build()) + .build() + ); + } + + @Test + public void testBasicMappingSingleValueImport() { + testValueMappingForImportSyncMode(ImmutableMap.>builder() .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) .build(), ImmutableMap.>builder() @@ -176,7 +159,7 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker @Test public void testBasicMappingEmail() { - testValueMapping(ImmutableMap.>builder() + testValueMappingForForceSyncMode(ImmutableMap.>builder() .put("email", ImmutableList.builder().add(bc.getUserEmail()).build()) .put("nested.email", ImmutableList.builder().add(bc.getUserEmail()).build()) .put("dotted.email", ImmutableList.builder().add(bc.getUserEmail()).build()) @@ -190,8 +173,8 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker } @Test - public void testBasicMappingClearValue() { - testValueMapping(ImmutableMap.>builder() + public void testBasicMappingAttributeGetsModifiedInSyncModeForce() { + testValueMappingForForceSyncMode(ImmutableMap.>builder() .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) .build(), ImmutableMap.>builder() @@ -201,8 +184,8 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker } @Test - public void testBasicMappingRemoveValue() { - testValueMapping(ImmutableMap.>builder() + public void testBasicMappingAttributeGetsRemovedInSyncModeForce() { + testValueMappingForForceSyncMode(ImmutableMap.>builder() .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) .build(), ImmutableMap.>builder() @@ -211,8 +194,8 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker } @Test - public void testBasicMappingMultipleValues() { - testValueMapping(ImmutableMap.>builder() + public void testBasicMappingAttributeWithMultipleValuesIsModifiedInSyncModeForce() { + testValueMappingForForceSyncMode(ImmutableMap.>builder() .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").add("value 2").build()) .build(), ImmutableMap.>builder() @@ -222,8 +205,9 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker } @Test - public void testAddBasicMappingMultipleValues() { - testValueMapping(ImmutableMap.>builder() + public void testBasicMappingAttributeWithMultipleValuesIsModifiedInSyncModeLegacy() { + testValueMappingForLegacySyncMode(ImmutableMap.>builder() + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").add("value 2").build()) .build(), ImmutableMap.>builder() .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("second value").add("second value 2").build()) @@ -232,11 +216,32 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractBaseBroker } @Test - public void testDeleteBasicMappingMultipleValues() { - testValueMapping(ImmutableMap.>builder() - .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("second value").add("second value 2").build()) + public void testBasicMappingAttributeWithMultipleValuesDoesNotGetModifiedInSyncModeImport() { + testValueMappingForImportSyncMode(ImmutableMap.>builder() + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").add("value 2").build()) .build(), ImmutableMap.>builder() + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("second value").add("second value 2").build()) + .build() + ); + } + + @Test + public void testBasicMappingAttributeWithMultipleValuesGetsAddedInSyncModeForce() { + testValueMappingForForceSyncMode(ImmutableMap.>builder() + .build(), + ImmutableMap.>builder() + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("second value").add("second value 2").build()) + .build() + ); + } + + @Test + public void testBasicMappingAttributeWithMultipleValuesDoesNotGetAddedInSyncModeImport() { + testValueMappingForImportSyncMode(ImmutableMap.>builder() + .build(), + ImmutableMap.>builder() + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("second value").add("second value 2").build()) .build() ); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractUsernameTemplateMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractUsernameTemplateMapperTest.java new file mode 100644 index 0000000000..8500c5283b --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractUsernameTemplateMapperTest.java @@ -0,0 +1,102 @@ +package org.keycloak.testsuite.broker; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage; +import static org.keycloak.testsuite.broker.KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME; + +import java.util.List; + +import org.junit.Test; +import org.keycloak.models.IdentityProviderMapperSyncMode; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.Assert; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +/** + * @author Martin Idel, + */ +public abstract class AbstractUsernameTemplateMapperTest extends AbstractIdentityProviderMapperTest { + + protected abstract String getMapperTemplate(); + + protected abstract void createMapperInIdp(IdentityProviderRepresentation idp, IdentityProviderMapperSyncMode syncMode); + + @Test + public void testUsernameGetsInsertedFromClaim() { + loginAsUserTwiceWithMapperWillNotUpdateUsername(IdentityProviderMapperSyncMode.IMPORT); + } + + @Test + public void testUsernameGetsUpdatedFromClaimInForceMode() { + loginAsUserTwiceWithMapperUpdatesUsername(IdentityProviderMapperSyncMode.FORCE); + } + + @Test + public void testUsernameDoesNotGetUpdatedInLegacyMode() { + loginAsUserTwiceWithMapperWillNotUpdateUsername(IdentityProviderMapperSyncMode.LEGACY); + } + + private void loginAsUserTwiceWithMapperUpdatesUsername(IdentityProviderMapperSyncMode syncMode) { + loginAsUserTwiceWithMapper(syncMode, "customusername", "newname", true); + } + + private void loginAsUserTwiceWithMapperWillNotUpdateUsername(IdentityProviderMapperSyncMode syncMode) { + loginAsUserTwiceWithMapper(syncMode, "customusername", "newname", false); + } + + private void loginAsUserTwiceWithMapper( + IdentityProviderMapperSyncMode syncMode, String userName, String updatedUserName, boolean updatingUserName) { + final IdentityProviderRepresentation idp = setupIdentityProvider(); + createMapperInIdp(idp, syncMode); + // The ATTRIBUTE_TO_MAP_NAME gets mapped to a claim by the setup. It's value will always be an array, therefore the [] around the value + createUserInProviderRealm(ImmutableMap.>builder() + .put(ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add(userName).build()) + .build()); + + logInAsUserInIDPForFirstTime(); + + String mappedUserName = String.format(getMapperTemplate(), userName); + findUser(bc.consumerRealmName(), mappedUserName, bc.getUserEmail()); + + logoutFromRealm(bc.consumerRealmName()); + + updateUser(updatedUserName); + + logInAsUserInIDP(); + String updatedMappedUserName = String.format(getMapperTemplate(), updatedUserName); + UserRepresentation user = findUser(bc.consumerRealmName(), updatingUserName ? updatedMappedUserName : mappedUserName, bc.getUserEmail()); + if (updatingUserName) { + assertThat(user.getUsername(), is(updatedMappedUserName)); + } else { + assertThat(user.getUsername(), is(mappedUserName)); + } + } + + // We don't want to update the username - that needs to be done by the mapper + @Override + protected void logInAsUserInIDPForFirstTime() { + logInAsUserInIDP(); + + waitForPage(driver, "update account information", false); + + Assert.assertTrue(updateAccountInformationPage.isCurrent()); + Assert.assertTrue("We must be on correct realm right now", + driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/")); + + log.debug("Updating info on updateAccount page"); + updateAccountInformationPage.updateAccountInformation(bc.getUserEmail(), "FirstName", "LastName"); + } + + private void updateUser(String updatedUserName) { + UserRepresentation user = findUser(bc.providerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + ImmutableMap> matchingAttributes = ImmutableMap.>builder() + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add(updatedUserName).build()) + .build(); + user.setAttributes(matchingAttributes); + adminClient.realm(bc.providerRealmName()).users().get(user.getId()).update(user); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AttributeToRoleMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AttributeToRoleMapperTest.java new file mode 100644 index 0000000000..f757c30633 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AttributeToRoleMapperTest.java @@ -0,0 +1,83 @@ +package org.keycloak.testsuite.broker; + +import static org.keycloak.models.IdentityProviderMapperSyncMode.FORCE; +import static org.keycloak.models.IdentityProviderMapperSyncMode.LEGACY; + +import java.util.List; + +import org.junit.Test; +import org.keycloak.admin.client.resource.IdentityProviderResource; +import org.keycloak.broker.saml.mappers.AttributeToRoleMapper; +import org.keycloak.broker.saml.mappers.UserAttributeMapper; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderMapperSyncMode; +import org.keycloak.models.IdentityProviderSyncMode; +import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.UserRepresentation; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +/** + * @author Martin Idel + */ +public class AttributeToRoleMapperTest extends AbstractRoleMapperTest { + + @Override + protected BrokerConfiguration getBrokerConfiguration() { + return new KcSamlBrokerConfiguration(); + } + + @Test + public void mapperGrantsRoleOnFirstLogin() { + UserRepresentation user = createMapperThenLoginAsUserTwiceWithAttributeToRoleMapper(FORCE); + + assertThatRoleHasBeenAssignedInConsumerRealmTo(user); + } + + @Test + public void updateBrokeredUserGrantsRoleInLegacyMode() { + UserRepresentation user = loginAsUserThenCreateMapperAndLoginAgainWithAttributeToRoleMapper(LEGACY); + + assertThatRoleHasBeenAssignedInConsumerRealmTo(user); + } + + @Test + public void updateBrokeredUserGrantsRoleInForceMode() { + UserRepresentation user = loginAsUserThenCreateMapperAndLoginAgainWithAttributeToRoleMapper(FORCE); + + assertThatRoleHasBeenAssignedInConsumerRealmTo(user); + } + + private UserRepresentation createMapperThenLoginAsUserTwiceWithAttributeToRoleMapper(IdentityProviderMapperSyncMode syncMode) { + return loginAsUserTwiceWithMapper(syncMode, false, + ImmutableMap.>builder() + .put("Role", ImmutableList.builder().add(CLIENT_ROLE_MAPPER_REPRESENTATION).build()) + .build()); + } + + private UserRepresentation loginAsUserThenCreateMapperAndLoginAgainWithAttributeToRoleMapper(IdentityProviderMapperSyncMode syncMode) { + return loginAsUserTwiceWithMapper(syncMode, true, + ImmutableMap.>builder() + .put("Role", ImmutableList.builder().add(CLIENT_ROLE_MAPPER_REPRESENTATION).build()) + .build()); + } + + @Override + protected void createMapperInIdp(IdentityProviderRepresentation idp, IdentityProviderMapperSyncMode syncMode) { + IdentityProviderMapperRepresentation samlAttributeToRoleMapper = new IdentityProviderMapperRepresentation(); + samlAttributeToRoleMapper.setName("user-role-mapper"); + samlAttributeToRoleMapper.setIdentityProviderMapper(AttributeToRoleMapper.PROVIDER_ID); + samlAttributeToRoleMapper.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) + .put(UserAttributeMapper.ATTRIBUTE_NAME, "Role") + .put(ATTRIBUTE_VALUE, ROLE_USER) + .put("role", CLIENT_ROLE_MAPPER_REPRESENTATION) + .build()); + + IdentityProviderResource idpResource = realm.identityProviders().get(idp.getAlias()); + samlAttributeToRoleMapper.setIdentityProviderAlias(bc.getIDPAlias()); + idpResource.addMapper(samlAttributeToRoleMapper).close(); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/BrokerConfiguration.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/BrokerConfiguration.java index 5605cf6238..a1278787b0 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/BrokerConfiguration.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/BrokerConfiguration.java @@ -1,5 +1,6 @@ package org.keycloak.testsuite.broker; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.RealmRepresentation; @@ -30,7 +31,14 @@ public interface BrokerConfiguration { /** * @return Representation of the identity provider for declaration in the broker */ - IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext); + default IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) { + return setUpIdentityProvider(suiteContext, IdentityProviderSyncMode.IMPORT); + } + + /** + * @return Representation of the identity provider for declaration in the broker + */ + IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext, IdentityProviderSyncMode force); /** * @return Name of realm containing identity provider. Must be consistent with {@link #createProviderRealm()} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/ExternalKeycloakRoleToRoleMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/ExternalKeycloakRoleToRoleMapperTest.java new file mode 100644 index 0000000000..3140e4de71 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/ExternalKeycloakRoleToRoleMapperTest.java @@ -0,0 +1,109 @@ +package org.keycloak.testsuite.broker; + +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.keycloak.admin.client.resource.IdentityProviderResource; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.UserResource; +import org.keycloak.broker.oidc.mappers.ExternalKeycloakRoleToRoleMapper; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderMapperSyncMode; +import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.UserRepresentation; + +import com.google.common.collect.ImmutableMap; + +import static org.keycloak.models.IdentityProviderMapperSyncMode.*; + +/** + * @author Martin Idel + */ +public class ExternalKeycloakRoleToRoleMapperTest extends AbstractRoleMapperTest { + private RealmResource realm; + private boolean deleteRoleFromUser = true; + + @Override + protected BrokerConfiguration getBrokerConfiguration() { + return new KcOidcBrokerConfiguration(); + } + + @Before + public void setupRealm() { + super.addClients(); + realm = adminClient.realm(bc.consumerRealmName()); + } + + @Test + public void mapperGrantsRoleOnFirstLogin() { + UserRepresentation user = createMapperThenLoginAsUserTwiceWithExternalKeycloakRoleToRoleMapper(IMPORT); + + assertThatRoleHasBeenAssignedInConsumerRealmTo(user); + } + + @Test + public void updateBrokeredUserDoesNotGrantRoleInLegacyMode() { + UserRepresentation user = loginAsUserThenCreateMapperAndLoginAgainWithExternalKeycloakRoleToRoleMapper(LEGACY); + + assertThatRoleHasNotBeenAssignedInConsumerRealmTo(user); + } + + @Test + public void updateBrokeredUserGrantsRoleInForceMode() { + UserRepresentation user = loginAsUserThenCreateMapperAndLoginAgainWithExternalKeycloakRoleToRoleMapper(FORCE); + + assertThatRoleHasBeenAssignedInConsumerRealmTo(user); + } + + @Test + public void updateBrokeredUserMatchDeletesRoleInForceMode() { + UserRepresentation user = createMapperThenLoginAsUserTwiceWithExternalKeycloakRoleToRoleMapper(FORCE); + + assertThatRoleHasNotBeenAssignedInConsumerRealmTo(user); + } + + @Test + public void updateBrokeredUserMatchDoesNotDeleteRoleInLegacyMode() { + UserRepresentation user = createMapperThenLoginAsUserTwiceWithExternalKeycloakRoleToRoleMapper(LEGACY); + + assertThatRoleHasBeenAssignedInConsumerRealmTo(user); + } + + private UserRepresentation createMapperThenLoginAsUserTwiceWithExternalKeycloakRoleToRoleMapper(IdentityProviderMapperSyncMode syncMode) { + return loginAsUserTwiceWithMapper(syncMode, false, ImmutableMap.>builder().build()); + } + + private UserRepresentation loginAsUserThenCreateMapperAndLoginAgainWithExternalKeycloakRoleToRoleMapper(IdentityProviderMapperSyncMode syncMode) { + deleteRoleFromUser = false; + return loginAsUserTwiceWithMapper(syncMode, true, ImmutableMap.>builder().build()); + } + + @Override + protected void createMapperInIdp(IdentityProviderRepresentation idp, IdentityProviderMapperSyncMode syncMode) { + IdentityProviderMapperRepresentation externalRoleToRoleMapper = new IdentityProviderMapperRepresentation(); + externalRoleToRoleMapper.setName("external-keycloak-role-mapper"); + externalRoleToRoleMapper.setIdentityProviderMapper(ExternalKeycloakRoleToRoleMapper.PROVIDER_ID); + externalRoleToRoleMapper.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) + .put("external.role", ROLE_USER) + .put("role", CLIENT_ROLE_MAPPER_REPRESENTATION) + .build()); + + IdentityProviderResource idpResource = realm.identityProviders().get(idp.getAlias()); + externalRoleToRoleMapper.setIdentityProviderAlias(bc.getIDPAlias()); + idpResource.addMapper(externalRoleToRoleMapper).close(); + } + + @Override + public void updateUser() { + if (deleteRoleFromUser) { + RoleRepresentation role = adminClient.realm(bc.providerRealmName()).roles().get(ROLE_USER).toRepresentation(); + UserResource userResource = adminClient.realm(bc.providerRealmName()).users().get(userId); + userResource.roles().realmLevel().remove(Collections.singletonList(role)); + } + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/HardcodedRoleMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/HardcodedRoleMapperTest.java new file mode 100644 index 0000000000..70f46142f1 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/HardcodedRoleMapperTest.java @@ -0,0 +1,97 @@ +package org.keycloak.testsuite.broker; + +import static org.keycloak.models.IdentityProviderMapperSyncMode.FORCE; +import static org.keycloak.models.IdentityProviderMapperSyncMode.IMPORT; +import static org.keycloak.models.IdentityProviderMapperSyncMode.LEGACY; + +import java.util.HashMap; + +import org.junit.Before; +import org.junit.Test; +import org.keycloak.admin.client.resource.IdentityProviderResource; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.broker.provider.ConfigConstants; +import org.keycloak.broker.provider.HardcodedRoleMapper; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderMapperSyncMode; +import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.UserRepresentation; + +import com.google.common.collect.ImmutableMap; + +/** + * @author Martin Idel + */ +public class HardcodedRoleMapperTest extends AbstractRoleMapperTest { + private RealmResource realm; + + @Override + protected BrokerConfiguration getBrokerConfiguration() { + return new KcOidcBrokerConfiguration(); + } + + @Before + public void setupRealm() { + super.addClients(); + realm = adminClient.realm(bc.consumerRealmName()); + } + + @Test + public void mapperGrantsRoleOnFirstLogin() { + UserRepresentation user = createMapperThenLoginAsUserTwiceWithHardcodedRoleMapper(IMPORT); + + assertThatRoleHasBeenAssignedInConsumerRealmTo(user); + } + + @Test + public void mapperDoesNotGrantRoleInModeImportIfMapperIsAddedLater() { + UserRepresentation user = loginAsUserThenCreateMapperAndLoginAgainWithHardcodedRoleMapper(IMPORT); + + assertThatRoleHasNotBeenAssignedInConsumerRealmTo(user); + } + + @Test + public void updateBrokeredUserDoesNotGrantRoleInLegacyMode() { + UserRepresentation user = loginAsUserThenCreateMapperAndLoginAgainWithHardcodedRoleMapper(LEGACY); + + assertThatRoleHasNotBeenAssignedInConsumerRealmTo(user); + } + + @Test + public void updateBrokeredUserGrantsRoleInForceMode() { + UserRepresentation user = loginAsUserThenCreateMapperAndLoginAgainWithHardcodedRoleMapper(FORCE); + + assertThatRoleHasBeenAssignedInConsumerRealmTo(user); + } + + @Test + public void updateBrokeredUserMatchDoesntDeleteRole() { + UserRepresentation user = createMapperThenLoginAsUserTwiceWithHardcodedRoleMapper(FORCE); + + assertThatRoleHasBeenAssignedInConsumerRealmTo(user); + } + + private UserRepresentation createMapperThenLoginAsUserTwiceWithHardcodedRoleMapper(IdentityProviderMapperSyncMode syncMode) { + return loginAsUserTwiceWithMapper(syncMode, false, new HashMap<>()); + } + + private UserRepresentation loginAsUserThenCreateMapperAndLoginAgainWithHardcodedRoleMapper(IdentityProviderMapperSyncMode syncMode) { + return loginAsUserTwiceWithMapper(syncMode, true, new HashMap<>()); + } + + @Override + protected void createMapperInIdp(IdentityProviderRepresentation idp, IdentityProviderMapperSyncMode syncMode) { + IdentityProviderMapperRepresentation advancedClaimToRoleMapper = new IdentityProviderMapperRepresentation(); + advancedClaimToRoleMapper.setName("oidc-hardcoded-role-mapper"); + advancedClaimToRoleMapper.setIdentityProviderMapper(HardcodedRoleMapper.PROVIDER_ID); + advancedClaimToRoleMapper.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) + .put(ConfigConstants.ROLE, CLIENT_ROLE_MAPPER_REPRESENTATION) + .build()); + + IdentityProviderResource idpResource = realm.identityProviders().get(idp.getAlias()); + advancedClaimToRoleMapper.setIdentityProviderAlias(bc.getIDPAlias()); + idpResource.addMapper(advancedClaimToRoleMapper).close(); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/HardcodedUserAttributeMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/HardcodedUserAttributeMapperTest.java new file mode 100644 index 0000000000..dc8c8a1420 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/HardcodedUserAttributeMapperTest.java @@ -0,0 +1,134 @@ +package org.keycloak.testsuite.broker; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertThat; +import static org.keycloak.models.IdentityProviderMapperSyncMode.FORCE; +import static org.keycloak.models.IdentityProviderMapperSyncMode.IMPORT; + +import java.util.HashMap; + +import org.junit.Test; +import org.keycloak.admin.client.resource.IdentityProviderResource; +import org.keycloak.broker.provider.HardcodedAttributeMapper; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderMapperSyncMode; +import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.UserRepresentation; + +import com.google.common.collect.ImmutableMap; + +/** + * Martin Idel, + */ +public class HardcodedUserAttributeMapperTest extends AbstractIdentityProviderMapperTest { + + private static final String USER_ATTRIBUTE = "user-attribute"; + private static final String USER_ATTRIBUTE_VALUE = "user-attribute"; + + @Test + public void addHardcodedAttributeOnFirstLogin() { + final IdentityProviderRepresentation idp = setupIdentityProvider(); + createMapperInIdp(idp, IMPORT); + createUserInProviderRealm(); + + logInAsUserInIDPForFirstTime(); + + UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + assertThatAttributeHasBeenAssigned(user); + } + + @Test + public void hardcodedAttributeGetsAddedEvenIfMapperIsAddedLaterInSyncModeForce() { + UserRepresentation user = loginAsUserTwiceWithMapper(FORCE, true); + + assertThatAttributeHasBeenAssigned(user); + } + + @Test + public void hardcodedAttributeDoesNotGetAddedIfMapperIsAddedLaterInSyncModeImport() { + UserRepresentation user = loginAsUserTwiceWithMapper(IMPORT, true); + + assertThatAttributeHasNotBeenAssigned(user); + } + + @Test + public void hardcodedAttributeDoesNotGetAddedAgainInSyncModeImport() { + UserRepresentation user = loginAsUserTwiceWithMapper(IMPORT, false); + + assertThatAttributeHasNotBeenAssigned(user); + } + + @Test + public void hardcodedAttributeGetsUpdatedInSyncModeForce() { + UserRepresentation user = loginAsUserTwiceWithMapper(FORCE, false); + + assertThatAttributeHasBeenAssigned(user); + } + + protected UserRepresentation loginAsUserTwiceWithMapper( + IdentityProviderMapperSyncMode syncMode, boolean createAfterFirstLogin) { + final IdentityProviderRepresentation idp = setupIdentityProvider(); + if (!createAfterFirstLogin) { + createMapperInIdp(idp, syncMode); + } + createUserInProviderRealm(); + + logInAsUserInIDPForFirstTime(); + + UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + if (!createAfterFirstLogin) { + assertThatAttributeHasBeenAssigned(user); + } else { + assertThatAttributeHasNotBeenAssigned(user); + } + + if (createAfterFirstLogin) { + createMapperInIdp(idp, syncMode); + } + logoutFromRealm(bc.consumerRealmName()); + + if (user.getAttributes() != null) { + user.setAttributes(new HashMap<>()); + } + adminClient.realm(bc.consumerRealmName()).users().get(user.getId()).update(user); + + logInAsUserInIDP(); + return findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + } + + protected void createMapperInIdp(IdentityProviderRepresentation idp, IdentityProviderMapperSyncMode syncMode) { + IdentityProviderMapperRepresentation advancedClaimToRoleMapper = new IdentityProviderMapperRepresentation(); + advancedClaimToRoleMapper.setName("hardcoded-attribute-mapper"); + advancedClaimToRoleMapper.setIdentityProviderMapper(HardcodedAttributeMapper.PROVIDER_ID); + advancedClaimToRoleMapper.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) + .put(HardcodedAttributeMapper.ATTRIBUTE, USER_ATTRIBUTE) + .put(HardcodedAttributeMapper.ATTRIBUTE_VALUE, USER_ATTRIBUTE_VALUE) + .build()); + + IdentityProviderResource idpResource = realm.identityProviders().get(idp.getAlias()); + advancedClaimToRoleMapper.setIdentityProviderAlias(bc.getIDPAlias()); + idpResource.addMapper(advancedClaimToRoleMapper).close(); + } + + protected void createUserInProviderRealm() { + createUserInProviderRealm(new HashMap<>()); + } + + protected void assertThatAttributeHasBeenAssigned(UserRepresentation user) { + assertThat(user.getAttributes().get(USER_ATTRIBUTE), contains(USER_ATTRIBUTE_VALUE)); + } + + protected void assertThatAttributeHasNotBeenAssigned(UserRepresentation user) { + if (user.getAttributes() != null) { + assertThat(user.getAttributes().get(USER_ATTRIBUTE), not(contains(USER_ATTRIBUTE_VALUE))); + } + } + + @Override + protected BrokerConfiguration getBrokerConfiguration() { + return new KcOidcBrokerConfiguration(); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/JsonUserAttributeMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/JsonUserAttributeMapperTest.java new file mode 100644 index 0000000000..d3c57487ad --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/JsonUserAttributeMapperTest.java @@ -0,0 +1,162 @@ +package org.keycloak.testsuite.broker; + +import com.google.common.collect.ImmutableMap; +import org.junit.Test; +import org.keycloak.admin.client.resource.IdentityProviderResource; +import org.keycloak.admin.client.resource.ProtocolMappersResource; +import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderMapperSyncMode; +import org.keycloak.protocol.oidc.mappers.HardcodedClaim; +import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.ProtocolMapperRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.social.github.GitHubUserAttributeMapper; + +import java.util.HashMap; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.keycloak.models.IdentityProviderMapperSyncMode.FORCE; +import static org.keycloak.models.IdentityProviderMapperSyncMode.IMPORT; +import static org.keycloak.models.IdentityProviderMapperSyncMode.LEGACY; +import static org.keycloak.testsuite.broker.KcOidcBrokerConfiguration.HARDOCDED_CLAIM; +import static org.keycloak.testsuite.broker.KcOidcBrokerConfiguration.HARDOCDED_VALUE; +import static org.keycloak.testsuite.broker.KcOidcBrokerConfiguration.USER_INFO_CLAIM; + +/** + * @author Martin Idel + */ +public class JsonUserAttributeMapperTest extends AbstractIdentityProviderMapperTest { + + public static final String USER_ATTRIBUTE = "user-attribute"; + + @Override + protected BrokerConfiguration getBrokerConfiguration() { + return new KcOidcBrokerConfiguration(); + } + + @Test + public void loginWithIdentityProviderMapsJsonAttributeToUserAttributeButDoesNotModify() { + UserRepresentation user = createMapperThenModifyAttribute(IMPORT, "new-value"); + + assertUserAttribute(HARDOCDED_VALUE, user); + } + + @Test + public void loginWithIdentityProviderDeletesAttributeInForceMode() { + UserRepresentation user = createMapperThenDeleteAttribute(FORCE); + + assertAbsentUserAttribute(user); + } + + @Test + public void loginWithIdentityProviderDoesNotDeleteAttributeInLegacyMode() { + UserRepresentation user = createMapperThenDeleteAttribute(LEGACY); + + assertUserAttribute(HARDOCDED_VALUE, user); + } + + @Test + public void loginWithIdentityProviderModifiesAttributeInForceMode() { + UserRepresentation user = createMapperThenModifyAttribute(FORCE, "new-value"); + + assertUserAttribute("new-value", user); + } + + @Test + public void loginWithIdentityProviderAddsUserAttributeInForceNameWhenMapperIsCreatedLater() { + UserRepresentation user = loginAndThenCreateMapperThenLoginAgain(FORCE); + + assertUserAttribute(HARDOCDED_VALUE, user); + } + + @Test + public void loginWithIdentityProviderDoesNotAddUserAttributeInImportNameWhenMapperIsCreatedLater() { + UserRepresentation user = loginAndThenCreateMapperThenLoginAgain(IMPORT); + + assertAbsentUserAttribute(user); + } + + private UserRepresentation loginAndThenCreateMapperThenLoginAgain(IdentityProviderMapperSyncMode syncMode) { + return loginAsUserTwiceWithMapper(syncMode, true, HARDOCDED_CLAIM, HARDOCDED_VALUE); + } + + private UserRepresentation createMapperThenDeleteAttribute(IdentityProviderMapperSyncMode syncMode) { + return loginAsUserTwiceWithMapper(syncMode, false, "deleted", "deleted"); + } + + private UserRepresentation createMapperThenModifyAttribute(IdentityProviderMapperSyncMode syncMode, String updatedValue) { + return loginAsUserTwiceWithMapper(syncMode, false, HARDOCDED_CLAIM, updatedValue); + } + + private UserRepresentation loginAsUserTwiceWithMapper( + IdentityProviderMapperSyncMode syncMode, boolean createAfterFirstLogin, String claim, String updatedValue) { + final IdentityProviderRepresentation idp = setupIdentityProvider(); + if (!createAfterFirstLogin) { + createGithubProviderMapper(idp, syncMode); + } + createUserInProviderRealm(new HashMap<>()); + + logInAsUserInIDPForFirstTime(); + + UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + if (!createAfterFirstLogin) { + assertUserAttribute(HARDOCDED_VALUE, user); + } else { + assertAbsentUserAttribute(user); + } + + if (createAfterFirstLogin) { + createGithubProviderMapper(idp, syncMode); + } + logoutFromRealm(bc.consumerRealmName()); + + if (!createAfterFirstLogin) { + updateClaimSentToIDP(claim, updatedValue); + } + + logInAsUserInIDP(); + return findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + } + + private void updateClaimSentToIDP(String claim, String updatedValue) { + ProtocolMapperRepresentation claimMapper = null; + ProtocolMappersResource protocolMappers = adminClient.realm(bc.providerRealmName()).clients().get(BrokerTestConstants.CLIENT_ID).getProtocolMappers(); + for (ProtocolMapperRepresentation representation : protocolMappers.getMappers()) { + if (representation.getProtocolMapper().equals(HardcodedClaim.PROVIDER_ID)) { + claimMapper = representation; + } + } + assertThat(claimMapper, notNullValue()); + claimMapper.getConfig().put(HardcodedClaim.CLAIM_VALUE, "{\"" + claim + "\": \"" + updatedValue + "\"}"); + adminClient.realm(bc.providerRealmName()).clients().get(BrokerTestConstants.CLIENT_ID).getProtocolMappers().update(claimMapper.getId(), claimMapper); + } + + private void assertUserAttribute(String value, UserRepresentation userRep) { + assertThat(userRep.getAttributes(), notNullValue()); + assertThat(userRep.getAttributes().get(USER_ATTRIBUTE), containsInAnyOrder(value)); + } + + private void assertAbsentUserAttribute(UserRepresentation userRep) { + assertThat(userRep.getAttributes(), nullValue()); + } + + private void createGithubProviderMapper(IdentityProviderRepresentation idp, IdentityProviderMapperSyncMode syncMode) { + IdentityProviderMapperRepresentation githubProvider = new IdentityProviderMapperRepresentation(); + githubProvider.setName("json-attribute-mapper"); + githubProvider.setIdentityProviderMapper(GitHubUserAttributeMapper.PROVIDER_ID); + githubProvider.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) + .put(AbstractJsonUserAttributeMapper.CONF_JSON_FIELD, USER_INFO_CLAIM + "." + HARDOCDED_CLAIM) + .put(AbstractJsonUserAttributeMapper.CONF_USER_ATTRIBUTE, USER_ATTRIBUTE) + .build()); + + IdentityProviderResource idpResource = realm.identityProviders().get(idp.getAlias()); + githubProvider.setIdentityProviderAlias(bc.getIDPAlias()); + idpResource.addMapper(githubProvider).close(); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerClientSecretBasicAuthTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerClientSecretBasicAuthTest.java index 15e9d42beb..fd3fd7c1bf 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerClientSecretBasicAuthTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerClientSecretBasicAuthTest.java @@ -1,5 +1,6 @@ package org.keycloak.testsuite.broker; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.testsuite.arquillian.SuiteContext; @@ -21,10 +22,10 @@ public class KcOidcBrokerClientSecretBasicAuthTest extends AbstractBrokerTest { private class KcOidcBrokerConfigurationWithBasicAuthAuthentication extends KcOidcBrokerConfiguration { @Override - public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) { + public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext, IdentityProviderSyncMode syncMode) { IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID); Map config = idp.getConfig(); - applyDefaultConfiguration(suiteContext, config); + applyDefaultConfiguration(suiteContext, config, syncMode); config.put("clientAuthMethod", OIDCLoginProtocol.CLIENT_SECRET_BASIC); return idp; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerClientSecretJwtTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerClientSecretJwtTest.java index 706913386d..0aadee42ba 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerClientSecretJwtTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerClientSecretJwtTest.java @@ -9,6 +9,7 @@ import java.util.List; import java.util.Map; import org.keycloak.authentication.authenticators.client.JWTClientSecretAuthenticator; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; @@ -35,10 +36,10 @@ public class KcOidcBrokerClientSecretJwtTest extends AbstractBrokerTest { } @Override - public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) { + public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext, IdentityProviderSyncMode syncMode) { IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID); Map config = idp.getConfig(); - applyDefaultConfiguration(suiteContext, config); + applyDefaultConfiguration(suiteContext, config, syncMode); config.put("clientAuthMethod", OIDCLoginProtocol.CLIENT_SECRET_JWT); return idp; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java index 4b934ec17b..6a234010cd 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java @@ -1,7 +1,10 @@ package org.keycloak.testsuite.broker; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.mappers.HardcodedClaim; import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; import org.keycloak.protocol.oidc.mappers.UserAttributeMapper; import org.keycloak.protocol.oidc.mappers.UserPropertyMapper; @@ -29,6 +32,9 @@ public class KcOidcBrokerConfiguration implements BrokerConfiguration { protected static final String ATTRIBUTE_TO_MAP_NAME = "user-attribute"; protected static final String ATTRIBUTE_TO_MAP_NAME_2 = "user-attribute-2"; + public static final String USER_INFO_CLAIM = "user-claim"; + public static final String HARDOCDED_CLAIM = "test"; + public static final String HARDOCDED_VALUE = "value"; @Override public RealmRepresentation createProviderRealm() { @@ -131,7 +137,18 @@ public class KcOidcBrokerConfiguration implements BrokerConfiguration { userAttrMapperConfig2.put(OIDCAttributeMapperHelper.INCLUDE_IN_USERINFO, "true"); userAttrMapperConfig2.put(ProtocolMapperUtils.MULTIVALUED, "true"); - client.setProtocolMappers(Arrays.asList(emailMapper, userAttrMapper, userAttrMapper2, nestedAttrMapper, dottedAttrMapper)); + ProtocolMapperRepresentation hardcodedJsonClaim = new ProtocolMapperRepresentation(); + hardcodedJsonClaim.setName("json-mapper"); + hardcodedJsonClaim.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + hardcodedJsonClaim.setProtocolMapper(HardcodedClaim.PROVIDER_ID); + + Map hardcodedJsonClaimMapperConfig = hardcodedJsonClaim.getConfig(); + hardcodedJsonClaimMapperConfig.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, KcOidcBrokerConfiguration.USER_INFO_CLAIM); + hardcodedJsonClaimMapperConfig.put(OIDCAttributeMapperHelper.JSON_TYPE, "JSON"); + hardcodedJsonClaimMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true"); + hardcodedJsonClaimMapperConfig.put(HardcodedClaim.CLAIM_VALUE, "{\"" + HARDOCDED_CLAIM + "\": \"" + HARDOCDED_VALUE + "\"}"); + + client.setProtocolMappers(Arrays.asList(emailMapper, userAttrMapper, userAttrMapper2, nestedAttrMapper, dottedAttrMapper, hardcodedJsonClaim)); return Collections.singletonList(client); } @@ -156,16 +173,17 @@ public class KcOidcBrokerConfiguration implements BrokerConfiguration { } @Override - public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) { + public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext, IdentityProviderSyncMode syncMode) { IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID); Map config = idp.getConfig(); - applyDefaultConfiguration(suiteContext, config); + applyDefaultConfiguration(suiteContext, config, syncMode); return idp; } - protected void applyDefaultConfiguration(final SuiteContext suiteContext, final Map config) { + protected void applyDefaultConfiguration(final SuiteContext suiteContext, final Map config, IdentityProviderSyncMode syncMode) { + config.put(IdentityProviderModel.SYNC_MODE, syncMode.toString()); config.put("clientId", CLIENT_ID); config.put("clientSecret", CLIENT_SECRET); config.put("prompt", "login"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerHiddenIdpHintTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerHiddenIdpHintTest.java index c5f04fa284..db87003da2 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerHiddenIdpHintTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerHiddenIdpHintTest.java @@ -18,6 +18,7 @@ package org.keycloak.testsuite.broker; import java.util.Map; import org.junit.Test; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.representations.idm.IdentityProviderRepresentation; import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage; @@ -43,11 +44,11 @@ public class KcOidcBrokerHiddenIdpHintTest extends AbstractInitializedBaseBroker private class KcOidcHiddenBrokerConfiguration extends KcOidcBrokerConfiguration { @Override - public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) { + public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext, IdentityProviderSyncMode syncMode) { IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID); Map config = idp.getConfig(); - applyDefaultConfiguration(suiteContext, config); + applyDefaultConfiguration(suiteContext, config, syncMode); config.put("hideOnLoginPage", "true"); return idp; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerLoginHintTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerLoginHintTest.java index 01ef37ded4..7a49fd91de 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerLoginHintTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerLoginHintTest.java @@ -13,6 +13,7 @@ import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad; import org.junit.Test; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.Assert; @@ -30,11 +31,11 @@ public class KcOidcBrokerLoginHintTest extends AbstractBrokerTest { private class KcOidcBrokerConfigurationWithLoginHint extends KcOidcBrokerConfiguration { @Override - public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) { + public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext, IdentityProviderSyncMode syncMode) { IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID); Map config = idp.getConfig(); - applyDefaultConfiguration(suiteContext, config); + applyDefaultConfiguration(suiteContext, config, syncMode); config.put("loginHint", "true"); return idp; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerNoLoginHintTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerNoLoginHintTest.java index ffcce0a940..bd7531ea22 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerNoLoginHintTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerNoLoginHintTest.java @@ -10,6 +10,7 @@ import static org.keycloak.testsuite.broker.BrokerTestTools.createIdentityProvid import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage; import org.apache.commons.lang3.StringUtils; import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.Assert; @@ -25,11 +26,11 @@ public class KcOidcBrokerNoLoginHintTest extends AbstractBrokerTest { private class KcOidcBrokerConfigurationWithNoLoginHint extends KcOidcBrokerConfiguration { @Override - public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) { + public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext, IdentityProviderSyncMode syncMode) { IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID); Map config = idp.getConfig(); - applyDefaultConfiguration(suiteContext, config); + applyDefaultConfiguration(suiteContext, config, syncMode); config.put("loginHint", "false"); return idp; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerParameterForwardTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerParameterForwardTest.java index d40395fb10..58080a822a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerParameterForwardTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerParameterForwardTest.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.Map; import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.Assert; @@ -31,10 +32,10 @@ public class KcOidcBrokerParameterForwardTest extends AbstractBrokerTest { private class KcOidcBrokerConfigurationWithParameterForward extends KcOidcBrokerConfiguration { @Override - public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) { + public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext, IdentityProviderSyncMode syncMode) { IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID); Map config = idp.getConfig(); - applyDefaultConfiguration(suiteContext, config); + applyDefaultConfiguration(suiteContext, config, syncMode); config.put("forwardParameters", FORWARDED_PARAMETER +", " + PARAMETER_NOT_SET); return idp; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPrivateKeyJwtTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPrivateKeyJwtTest.java index c3e687bfe9..1a74a1447e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPrivateKeyJwtTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPrivateKeyJwtTest.java @@ -18,6 +18,7 @@ package org.keycloak.testsuite.broker; import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; import org.keycloak.crypto.Algorithm; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; @@ -58,10 +59,10 @@ public class KcOidcBrokerPrivateKeyJwtTest extends AbstractBrokerTest { } @Override - public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) { + public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext, IdentityProviderSyncMode syncMode) { IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID); Map config = idp.getConfig(); - applyDefaultConfiguration(suiteContext, config); + applyDefaultConfiguration(suiteContext, config, syncMode); config.put("clientSecret", null); config.put("clientAuthMethod", OIDCLoginProtocol.PRIVATE_KEY_JWT); return idp; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPromptNoneRedirectTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPromptNoneRedirectTest.java index afec644816..869fa651a9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPromptNoneRedirectTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPromptNoneRedirectTest.java @@ -21,6 +21,8 @@ import java.util.Map; import org.junit.Test; import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.Assert; @@ -226,8 +228,9 @@ public class KcOidcBrokerPromptNoneRedirectTest extends AbstractInitializedBaseB * Override the default configuration to unset the {@code prompt} parameter and specify that the IDP accepts forwarded * auth requests with {@code prompt=none}. */ - protected void applyDefaultConfiguration(final SuiteContext suiteContext, final Map config) { - super.applyDefaultConfiguration(suiteContext, config); + @Override + protected void applyDefaultConfiguration(final SuiteContext suiteContext, final Map config, IdentityProviderSyncMode syncMode) { + super.applyDefaultConfiguration(suiteContext, config, syncMode); config.remove("prompt"); config.put("acceptsPromptNoneForwardFromClient", "true"); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPromptParameterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPromptParameterTest.java index 23fec48340..1ea9733b39 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPromptParameterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerPromptParameterTest.java @@ -1,6 +1,7 @@ package org.keycloak.testsuite.broker; import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.Assert; @@ -75,8 +76,9 @@ public class KcOidcBrokerPromptParameterTest extends AbstractBrokerTest { } private class KcOidcBrokerConfiguration2 extends KcOidcBrokerConfiguration { - protected void applyDefaultConfiguration(final SuiteContext suiteContext, final Map config) { - super.applyDefaultConfiguration(suiteContext, config); + @Override + protected void applyDefaultConfiguration(final SuiteContext suiteContext, final Map config, IdentityProviderSyncMode syncMode) { + super.applyDefaultConfiguration(suiteContext, config, syncMode); config.remove("prompt"); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerTest.java index 56daaa4dcc..4ed7d44697 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerTest.java @@ -1,14 +1,5 @@ package org.keycloak.testsuite.broker; -import static org.keycloak.testsuite.admin.ApiUtil.removeUserByUsername; -import static org.keycloak.testsuite.broker.BrokerRunOnServerUtil.configurePostBrokerLoginWithOTP; -import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_PROV_NAME; -import static org.keycloak.testsuite.broker.BrokerTestTools.getAuthRoot; -import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage; -import static org.keycloak.testsuite.util.ProtocolMapperUtil.createHardcodedClaim; - -import java.util.List; - import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import org.junit.Test; @@ -16,20 +7,39 @@ import org.keycloak.admin.client.resource.ClientResource; import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.admin.client.resource.IdentityProviderResource; import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UsersResource; import org.keycloak.broker.oidc.OIDCIdentityProviderConfig; import org.keycloak.broker.oidc.mappers.ExternalKeycloakRoleToRoleMapper; import org.keycloak.broker.oidc.mappers.UserAttributeMapper; import org.keycloak.crypto.Algorithm; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderMapperSyncMode; import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.ProtocolMapperRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.Assert; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertThat; +import static org.keycloak.testsuite.admin.ApiUtil.removeUserByUsername; +import static org.keycloak.testsuite.broker.BrokerRunOnServerUtil.configurePostBrokerLoginWithOTP; +import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_PROV_NAME; +import static org.keycloak.testsuite.broker.BrokerTestTools.getAuthRoot; +import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage; +import static org.keycloak.testsuite.util.ProtocolMapperUtil.createHardcodedClaim; + /** * Final class as it's not intended to be overriden. Feel free to remove "final" if you really know what you are doing. */ @@ -41,11 +51,12 @@ public final class KcOidcBrokerTest extends AbstractAdvancedBrokerTest { } @Override - protected Iterable createIdentityProviderMappers() { + protected Iterable createIdentityProviderMappers(IdentityProviderMapperSyncMode syncMode) { IdentityProviderMapperRepresentation attrMapper1 = new IdentityProviderMapperRepresentation(); attrMapper1.setName("manager-role-mapper"); attrMapper1.setIdentityProviderMapper(ExternalKeycloakRoleToRoleMapper.PROVIDER_ID); attrMapper1.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) .put("external.role", ROLE_MANAGER) .put("role", ROLE_MANAGER) .build()); @@ -54,6 +65,7 @@ public final class KcOidcBrokerTest extends AbstractAdvancedBrokerTest { attrMapper2.setName("user-role-mapper"); attrMapper2.setIdentityProviderMapper(ExternalKeycloakRoleToRoleMapper.PROVIDER_ID); attrMapper2.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) .put("external.role", ROLE_USER) .put("role", ROLE_USER) .build()); @@ -61,6 +73,63 @@ public final class KcOidcBrokerTest extends AbstractAdvancedBrokerTest { return Lists.newArrayList(attrMapper1, attrMapper2); } + @Override + protected void createAdditionalMapperWithCustomSyncMode(IdentityProviderMapperSyncMode syncMode) { + IdentityProviderMapperRepresentation friendlyManagerMapper = new IdentityProviderMapperRepresentation(); + friendlyManagerMapper.setName("friendly-manager-role-mapper"); + friendlyManagerMapper.setIdentityProviderMapper(ExternalKeycloakRoleToRoleMapper.PROVIDER_ID); + friendlyManagerMapper.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) + .put("external.role", ROLE_FRIENDLY_MANAGER) + .put("role", ROLE_FRIENDLY_MANAGER) + .build()); + friendlyManagerMapper.setIdentityProviderAlias(bc.getIDPAlias()); + RealmResource realm = adminClient.realm(bc.consumerRealmName()); + IdentityProviderResource idpResource = realm.identityProviders().get(bc.getIDPAlias()); + idpResource.addMapper(friendlyManagerMapper).close(); + } + + @Test + public void mapperDoesNothingForLegacyMode() { + createRolesForRealm(bc.providerRealmName()); + createRolesForRealm(bc.consumerRealmName()); + + createRoleMappersForConsumerRealm(IdentityProviderMapperSyncMode.LEGACY); + + RoleRepresentation managerRole = adminClient.realm(bc.providerRealmName()).roles().get(ROLE_MANAGER).toRepresentation(); + RoleRepresentation userRole = adminClient.realm(bc.providerRealmName()).roles().get(ROLE_USER).toRepresentation(); + + UserResource userResource = adminClient.realm(bc.providerRealmName()).users().get(userId); + userResource.roles().realmLevel().add(Collections.singletonList(managerRole)); + + logInAsUserInIDPForFirstTime(); + + UserResource consumerUserResource = adminClient.realm(bc.consumerRealmName()).users().get( + adminClient.realm(bc.consumerRealmName()).users().search(bc.getUserLogin()).get(0).getId()); + Set currentRoles = consumerUserResource.roles().realmLevel().listAll().stream() + .map(RoleRepresentation::getName) + .collect(Collectors.toSet()); + + assertThat(currentRoles, hasItems(ROLE_MANAGER)); + assertThat(currentRoles, not(hasItems(ROLE_USER))); + + logoutFromRealm(bc.consumerRealmName()); + + + userResource.roles().realmLevel().add(Collections.singletonList(userRole)); + + logInAsUserInIDP(); + + currentRoles = consumerUserResource.roles().realmLevel().listAll().stream() + .map(RoleRepresentation::getName) + .collect(Collectors.toSet()); + assertThat(currentRoles, hasItems(ROLE_MANAGER)); + assertThat(currentRoles, not(hasItems(ROLE_USER))); + + logoutFromRealm(bc.consumerRealmName()); + logoutFromRealm(bc.providerRealmName()); + } + @Test public void loginFetchingUserFromUserEndpoint() { RealmResource realm = realmsResouce().realm(bc.providerRealmName()); @@ -131,6 +200,7 @@ public final class KcOidcBrokerTest extends AbstractAdvancedBrokerTest { hardCodedSessionNoteMapper.setIdentityProviderAlias(bc.getIDPAlias()); hardCodedSessionNoteMapper.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID); hardCodedSessionNoteMapper.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, IdentityProviderMapperSyncMode.INHERIT.toString()) .put(UserAttributeMapper.USER_ATTRIBUTE, "hard-coded") .put(UserAttributeMapper.CLAIM, "hard-coded") .build()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerUiLocalesDisabledTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerUiLocalesDisabledTest.java index ad4900aa9d..a1425d08db 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerUiLocalesDisabledTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerUiLocalesDisabledTest.java @@ -1,6 +1,7 @@ package org.keycloak.testsuite.broker; import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.Assert; @@ -28,10 +29,10 @@ public class KcOidcBrokerUiLocalesDisabledTest extends AbstractBrokerTest { private class KcOidcBrokerConfigurationWithUiLocalesDisabled extends KcOidcBrokerConfiguration { @Override - public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) { + public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext, IdentityProviderSyncMode syncMode) { IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID); Map config = idp.getConfig(); - applyDefaultConfiguration(suiteContext, config); + applyDefaultConfiguration(suiteContext, config, syncMode); config.put("uiLocales", "false"); return idp; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerUiLocalesEnabledTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerUiLocalesEnabledTest.java index e9a05ea9ae..f66ebb9f7f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerUiLocalesEnabledTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerUiLocalesEnabledTest.java @@ -1,6 +1,7 @@ package org.keycloak.testsuite.broker; import org.keycloak.admin.client.resource.UsersResource; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.Assert; @@ -27,10 +28,10 @@ public class KcOidcBrokerUiLocalesEnabledTest extends AbstractBrokerTest { private class KcOidcBrokerConfigurationWithUiLocalesEnabled extends KcOidcBrokerConfiguration { @Override - public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) { + public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext, IdentityProviderSyncMode syncMode) { IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID); Map config = idp.getConfig(); - applyDefaultConfiguration(suiteContext, config); + applyDefaultConfiguration(suiteContext, config, syncMode); config.put("uiLocales", "true"); return idp; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerVaultConfiguration.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerVaultConfiguration.java index 214f083389..289f92dfe9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerVaultConfiguration.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerVaultConfiguration.java @@ -1,5 +1,6 @@ package org.keycloak.testsuite.broker; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.testsuite.arquillian.SuiteContext; @@ -13,8 +14,8 @@ public class KcOidcBrokerVaultConfiguration extends KcOidcBrokerConfiguration { public static final KcOidcBrokerVaultConfiguration INSTANCE = new KcOidcBrokerVaultConfiguration(); @Override - public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) { - IdentityProviderRepresentation idpRep = super.setUpIdentityProvider(suiteContext); + public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext, IdentityProviderSyncMode syncMode) { + IdentityProviderRepresentation idpRep = super.setUpIdentityProvider(suiteContext, syncMode); idpRep.getConfig().put("clientSecret", VAULT_CLIENT_SECRET); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcUsernameTemplateMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcUsernameTemplateMapperTest.java new file mode 100644 index 0000000000..5933ba5daa --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcUsernameTemplateMapperTest.java @@ -0,0 +1,40 @@ +package org.keycloak.testsuite.broker; + +import org.keycloak.admin.client.resource.IdentityProviderResource; +import org.keycloak.broker.oidc.mappers.UsernameTemplateMapper; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderMapperSyncMode; +import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; + +import com.google.common.collect.ImmutableMap; + +/** + * @author Martin Idel + */ +public class KcOidcUsernameTemplateMapperTest extends AbstractUsernameTemplateMapperTest { + @Override + protected void createMapperInIdp(IdentityProviderRepresentation idp, IdentityProviderMapperSyncMode syncMode) { + IdentityProviderMapperRepresentation usernameTemplateMapper = new IdentityProviderMapperRepresentation(); + usernameTemplateMapper.setName("oidc-username-template-mapper"); + usernameTemplateMapper.setIdentityProviderMapper(UsernameTemplateMapper.PROVIDER_ID); + usernameTemplateMapper.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) + .put("template", "${ALIAS}-${CLAIM.user-attribute}") + .build()); + + IdentityProviderResource idpResource = realm.identityProviders().get(idp.getAlias()); + usernameTemplateMapper.setIdentityProviderAlias(bc.getIDPAlias()); + idpResource.addMapper(usernameTemplateMapper).close(); + } + + @Override + protected String getMapperTemplate() { + return "kc-oidc-idp-[%s]"; + } + + @Override + protected BrokerConfiguration getBrokerConfiguration() { + return new KcOidcBrokerConfiguration(); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java index 8d097ae1f0..d0fc4b172b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerConfiguration.java @@ -5,6 +5,8 @@ */ package org.keycloak.testsuite.broker; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.saml.SamlConfigAttributes; import org.keycloak.protocol.saml.SamlProtocol; @@ -187,7 +189,7 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration { } @Override - public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) { + public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext, IdentityProviderSyncMode syncMode) { IdentityProviderRepresentation idp = createIdentityProvider(IDP_SAML_ALIAS, IDP_SAML_PROVIDER_ID); idp.setTrustEmail(true); @@ -196,6 +198,7 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration { Map config = idp.getConfig(); + config.put(IdentityProviderModel.SYNC_MODE, syncMode.toString()); config.put(SINGLE_SIGN_ON_SERVICE_URL, getAuthRoot(suiteContext) + "/auth/realms/" + REALM_PROV_NAME + "/protocol/saml"); config.put(SINGLE_LOGOUT_SERVICE_URL, getAuthRoot(suiteContext) + "/auth/realms/" + REALM_PROV_NAME + "/protocol/saml"); config.put(NAME_ID_POLICY_FORMAT, "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerTest.java index 5f10f5d87b..c1028bd422 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlBrokerTest.java @@ -1,7 +1,10 @@ package org.keycloak.testsuite.broker; +import org.keycloak.admin.client.resource.IdentityProviderResource; +import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.UserResource; import com.google.common.collect.ImmutableMap; +import org.keycloak.broker.oidc.mappers.ExternalKeycloakRoleToRoleMapper; import org.keycloak.broker.saml.mappers.AttributeToRoleMapper; import org.keycloak.broker.saml.mappers.UserAttributeMapper; import org.keycloak.dom.saml.v2.assertion.AssertionType; @@ -10,6 +13,9 @@ import org.keycloak.dom.saml.v2.assertion.AttributeType; import org.keycloak.dom.saml.v2.assertion.StatementAbstractType; import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; import org.keycloak.dom.saml.v2.protocol.ResponseType; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderMapperSyncMode; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; @@ -59,11 +65,12 @@ public final class KcSamlBrokerTest extends AbstractAdvancedBrokerTest { private static final String EMPTY_ATTRIBUTE_NAME = "empty.attribute.name"; @Override - protected Iterable createIdentityProviderMappers() { + protected Iterable createIdentityProviderMappers(IdentityProviderMapperSyncMode syncMode) { IdentityProviderMapperRepresentation attrMapper1 = new IdentityProviderMapperRepresentation(); attrMapper1.setName("manager-role-mapper"); attrMapper1.setIdentityProviderMapper(AttributeToRoleMapper.PROVIDER_ID); attrMapper1.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) .put(UserAttributeMapper.ATTRIBUTE_NAME, "Role") .put(ATTRIBUTE_VALUE, ROLE_MANAGER) .put("role", ROLE_MANAGER) @@ -73,6 +80,7 @@ public final class KcSamlBrokerTest extends AbstractAdvancedBrokerTest { attrMapper2.setName("user-role-mapper"); attrMapper2.setIdentityProviderMapper(AttributeToRoleMapper.PROVIDER_ID); attrMapper2.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) .put(UserAttributeMapper.ATTRIBUTE_NAME, "Role") .put(ATTRIBUTE_VALUE, ROLE_USER) .put("role", ROLE_USER) @@ -82,6 +90,7 @@ public final class KcSamlBrokerTest extends AbstractAdvancedBrokerTest { attrMapper3.setName("friendly-mapper"); attrMapper3.setIdentityProviderMapper(AttributeToRoleMapper.PROVIDER_ID); attrMapper3.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) .put(UserAttributeMapper.ATTRIBUTE_FRIENDLY_NAME, AbstractUserAttributeMapperTest.ATTRIBUTE_TO_MAP_FRIENDLY_NAME) .put(ATTRIBUTE_VALUE, ROLE_FRIENDLY_MANAGER) .put("role", ROLE_FRIENDLY_MANAGER) @@ -91,6 +100,7 @@ public final class KcSamlBrokerTest extends AbstractAdvancedBrokerTest { attrMapper4.setName("user-role-dot-guide-mapper"); attrMapper4.setIdentityProviderMapper(AttributeToRoleMapper.PROVIDER_ID); attrMapper4.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) .put(UserAttributeMapper.ATTRIBUTE_NAME, "Role") .put(ATTRIBUTE_VALUE, ROLE_USER_DOT_GUIDE) .put("role", ROLE_USER_DOT_GUIDE) @@ -100,22 +110,37 @@ public final class KcSamlBrokerTest extends AbstractAdvancedBrokerTest { attrMapper5.setName("empty-attribute-to-role-mapper"); attrMapper5.setIdentityProviderMapper(AttributeToRoleMapper.PROVIDER_ID); attrMapper5.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) .put(UserAttributeMapper.ATTRIBUTE_NAME, EMPTY_ATTRIBUTE_NAME) .put(ATTRIBUTE_VALUE, "") .put("role", EMPTY_ATTRIBUTE_ROLE) .build()); - return Arrays.asList(new IdentityProviderMapperRepresentation[] { attrMapper1, attrMapper2, attrMapper3, attrMapper4, attrMapper5 }); + return Arrays.asList(attrMapper1, attrMapper2, attrMapper3, attrMapper4, attrMapper5 ); + } + + protected void createAdditionalMapperWithCustomSyncMode(IdentityProviderMapperSyncMode syncMode) { + IdentityProviderMapperRepresentation friendlyManagerMapper = new IdentityProviderMapperRepresentation(); + friendlyManagerMapper.setName("friendly-manager-role-mapper"); + friendlyManagerMapper.setIdentityProviderMapper(AttributeToRoleMapper.PROVIDER_ID); + friendlyManagerMapper.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) + .put(UserAttributeMapper.ATTRIBUTE_NAME, "Role") + .put(ATTRIBUTE_VALUE, ROLE_FRIENDLY_MANAGER) + .put("role", ROLE_FRIENDLY_MANAGER) + .build()); + friendlyManagerMapper.setIdentityProviderAlias(bc.getIDPAlias()); + RealmResource realm = adminClient.realm(bc.consumerRealmName()); + IdentityProviderResource idpResource = realm.identityProviders().get(bc.getIDPAlias()); + idpResource.addMapper(friendlyManagerMapper).close(); } - // KEYCLOAK-3987 @Test - @Override - public void grantNewRoleFromToken() { + public void mapperUpdatesRolesOnEveryLogInForLegacyMode() { createRolesForRealm(bc.providerRealmName()); createRolesForRealm(bc.consumerRealmName()); - createRoleMappersForConsumerRealm(); + createRoleMappersForConsumerRealm(IdentityProviderMapperSyncMode.FORCE); RoleRepresentation managerRole = adminClient.realm(bc.providerRealmName()).roles().get(ROLE_MANAGER).toRepresentation(); RoleRepresentation friendlyManagerRole = adminClient.realm(bc.providerRealmName()).roles().get(ROLE_FRIENDLY_MANAGER).toRepresentation(); @@ -171,7 +196,6 @@ public final class KcSamlBrokerTest extends AbstractAdvancedBrokerTest { createRoleMappersForConsumerRealm(); RoleRepresentation managerRole = adminClient.realm(bc.providerRealmName()).roles().get(ROLE_MANAGER).toRepresentation(); - RoleRepresentation friendlyManagerRole = adminClient.realm(bc.providerRealmName()).roles().get(ROLE_FRIENDLY_MANAGER).toRepresentation(); RoleRepresentation userRole = adminClient.realm(bc.providerRealmName()).roles().get(ROLE_USER).toRepresentation(); RoleRepresentation userRoleDotGuide = adminClient.realm(bc.providerRealmName()).roles().get(ROLE_USER_DOT_GUIDE).toRepresentation(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java index fb4dc08974..ce1b0ba75c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedBrokerTest.java @@ -3,6 +3,7 @@ package org.keycloak.testsuite.broker; import org.keycloak.broker.saml.SAMLIdentityProviderConfig; import org.keycloak.crypto.Algorithm; import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.protocol.saml.SamlConfigAttributes; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; @@ -112,8 +113,8 @@ public class KcSamlSignedBrokerTest extends AbstractBrokerTest { } @Override - public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) { - IdentityProviderRepresentation result = super.setUpIdentityProvider(suiteContext); + public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext, IdentityProviderSyncMode syncMode) { + IdentityProviderRepresentation result = super.setUpIdentityProvider(suiteContext, syncMode); String providerCert = KeyUtils.getActiveKey(adminClient.realm(providerRealmName()).keys().getKeyMetadata(), Algorithm.RS256).getCertificate(); Assert.assertThat(providerCert, Matchers.notNullValue()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedDocumentOnlyBrokerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedDocumentOnlyBrokerTest.java index bd2ed67c8d..dc4ab27371 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedDocumentOnlyBrokerTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlSignedDocumentOnlyBrokerTest.java @@ -1,5 +1,6 @@ package org.keycloak.testsuite.broker; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.RealmRepresentation; @@ -61,8 +62,8 @@ public class KcSamlSignedDocumentOnlyBrokerTest extends AbstractBrokerTest { } @Override - public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext) { - IdentityProviderRepresentation result = super.setUpIdentityProvider(suiteContext); + public IdentityProviderRepresentation setUpIdentityProvider(SuiteContext suiteContext, IdentityProviderSyncMode syncMode) { + IdentityProviderRepresentation result = super.setUpIdentityProvider(suiteContext, syncMode); Map config = result.getConfig(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlUsernameTemplateMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlUsernameTemplateMapperTest.java new file mode 100644 index 0000000000..d81cc68dc2 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcSamlUsernameTemplateMapperTest.java @@ -0,0 +1,42 @@ +package org.keycloak.testsuite.broker; + +import static org.keycloak.broker.saml.mappers.UsernameTemplateMapper.PROVIDER_ID; + +import org.keycloak.admin.client.resource.IdentityProviderResource; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderMapperSyncMode; +import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; + +import com.google.common.collect.ImmutableMap; + +/** + * @author Martin Idel + */ +public class KcSamlUsernameTemplateMapperTest extends AbstractUsernameTemplateMapperTest { + + @Override + protected void createMapperInIdp(IdentityProviderRepresentation idp, IdentityProviderMapperSyncMode syncMode) { + IdentityProviderMapperRepresentation usernameTemplateMapper = new IdentityProviderMapperRepresentation(); + usernameTemplateMapper.setName("saml-username-template-mapper"); + usernameTemplateMapper.setIdentityProviderMapper(PROVIDER_ID); + usernameTemplateMapper.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) + .put("template", "${ALIAS}-${ATTRIBUTE.user-attribute}") + .build()); + + IdentityProviderResource idpResource = realm.identityProviders().get(idp.getAlias()); + usernameTemplateMapper.setIdentityProviderAlias(bc.getIDPAlias()); + idpResource.addMapper(usernameTemplateMapper).close(); + } + + @Override + protected String getMapperTemplate() { + return "kc-saml-idp-%s"; + } + + @Override + protected BrokerConfiguration getBrokerConfiguration() { + return new KcSamlBrokerConfiguration(); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcAdvancedClaimToRoleMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcAdvancedClaimToRoleMapperTest.java index fbf5e36925..f57bbbe262 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcAdvancedClaimToRoleMapperTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcAdvancedClaimToRoleMapperTest.java @@ -1,45 +1,32 @@ package org.keycloak.testsuite.broker; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.keycloak.models.IdentityProviderMapperSyncMode.FORCE; +import static org.keycloak.models.IdentityProviderMapperSyncMode.IMPORT; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; +import org.keycloak.admin.client.resource.IdentityProviderResource; +import org.keycloak.broker.oidc.mappers.AdvancedClaimToRoleMapper; +import org.keycloak.broker.provider.ConfigConstants; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderMapperSyncMode; +import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.UserRepresentation; + import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import org.junit.Before; -import org.junit.Test; -import org.keycloak.admin.client.resource.IdentityProviderResource; -import org.keycloak.admin.client.resource.RealmResource; -import org.keycloak.admin.client.resource.UsersResource; -import org.keycloak.broker.oidc.mappers.AdvancedClaimToRoleMapper; -import org.keycloak.broker.provider.ConfigConstants; -import org.keycloak.representations.idm.ClientRepresentation; -import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; -import org.keycloak.representations.idm.IdentityProviderRepresentation; -import org.keycloak.representations.idm.MappingsRepresentation; -import org.keycloak.representations.idm.RoleRepresentation; -import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.testsuite.util.UserBuilder; - -import javax.ws.rs.core.Response; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.junit.Assert.assertThat; -import static org.keycloak.testsuite.admin.ApiUtil.createUserAndResetPasswordWithAdminClient; /** - * @author hmlnarik, Benjamin Weimer + * @author hmlnarik, + * Benjamin Weimer, + * Martin Idel */ -public class OidcAdvancedClaimToRoleMapperTest extends AbstractBaseBrokerTest { - - private static final String CLIENT = "realm-management"; - private static final String CLIENT_ROLE = "view-realm"; - private static final String CLIENT_ROLE_MAPPER_REPRESENTATION = CLIENT + "." + CLIENT_ROLE; +public class OidcAdvancedClaimToRoleMapperTest extends AbstractRoleMapperTest { private static final String CLAIMS = "[\n" + " {\n" + @@ -63,17 +50,13 @@ public class OidcAdvancedClaimToRoleMapperTest extends AbstractBaseBrokerTest { " }\n" + "]"; + private String newValueForAttribute2 = ""; @Override protected BrokerConfiguration getBrokerConfiguration() { return new KcOidcBrokerConfiguration(); } - @Before - public void addClients() { - addClientsToProviderAndConsumer(); - } - @Test public void valueMatchesRegexTest() { AdvancedClaimToRoleMapper advancedClaimToRoleMapper = new AdvancedClaimToRoleMapper(); @@ -98,9 +81,7 @@ public class OidcAdvancedClaimToRoleMapperTest extends AbstractBaseBrokerTest { logInAsUserInIDPForFirstTime(); UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); - assertThatRoleHasBeenAssigned(user); - - logoutFromRealm(bc.consumerRealmName()); + assertThatRoleHasBeenAssignedInConsumerRealmTo(user); } @Test @@ -114,9 +95,7 @@ public class OidcAdvancedClaimToRoleMapperTest extends AbstractBaseBrokerTest { logInAsUserInIDPForFirstTime(); UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); - assertThatRoleHasNotBeenAssigned(user); - - logoutFromRealm(bc.consumerRealmName()); + assertThatRoleHasNotBeenAssignedInConsumerRealmTo(user); } @Test @@ -130,9 +109,7 @@ public class OidcAdvancedClaimToRoleMapperTest extends AbstractBaseBrokerTest { logInAsUserInIDPForFirstTime(); UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); - assertThatRoleHasBeenAssigned(user); - - logoutFromRealm(bc.consumerRealmName()); + assertThatRoleHasBeenAssignedInConsumerRealmTo(user); } @Test @@ -146,9 +123,7 @@ public class OidcAdvancedClaimToRoleMapperTest extends AbstractBaseBrokerTest { logInAsUserInIDPForFirstTime(); UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); - assertThatRoleHasBeenAssigned(user); - - logoutFromRealm(bc.consumerRealmName()); + assertThatRoleHasBeenAssignedInConsumerRealmTo(user); } @@ -163,84 +138,76 @@ public class OidcAdvancedClaimToRoleMapperTest extends AbstractBaseBrokerTest { logInAsUserInIDPForFirstTime(); UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); - assertThatRoleHasNotBeenAssigned(user); - - logoutFromRealm(bc.consumerRealmName()); + assertThatRoleHasNotBeenAssignedInConsumerRealmTo(user); } @Test public void updateBrokeredUserMismatchDeletesRole() { - createAdvancedClaimToRoleMapper(CLAIMS, false); - createUserInProviderRealm(ImmutableMap.>builder() - .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) - .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.builder().add("value 2").build()) - .build()); + newValueForAttribute2 = "value mismatch"; + UserRepresentation user = createMapperAndLoginAsUserTwiceWithMapper(FORCE, false); - logInAsUserInIDPForFirstTime(); + assertThatRoleHasNotBeenAssignedInConsumerRealmTo(user); + } - UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); - assertThatRoleHasBeenAssigned(user); + @Test + public void updateBrokeredUserMismatchDoesNotDeleteRoleInImportMode() { + newValueForAttribute2 = "value mismatch"; + UserRepresentation user = createMapperAndLoginAsUserTwiceWithMapper(IMPORT, false); - logoutFromRealm(bc.consumerRealmName()); - - // update - user = findUser(bc.providerRealmName(), bc.getUserLogin(), bc.getUserEmail()); - ImmutableMap> mismatchingAttributes = ImmutableMap.>builder() - .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) - .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.builder().add("value mismatch").build()) - .build(); - user.setAttributes(mismatchingAttributes); - adminClient.realm(bc.providerRealmName()).users().get(user.getId()).update(user); - - logInAsUserInIDP(); - user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); - - assertThatRoleHasNotBeenAssigned(user); + assertThatRoleHasBeenAssignedInConsumerRealmTo(user); } @Test public void updateBrokeredUserMatchDoesntDeleteRole() { - createAdvancedClaimToRoleMapper(CLAIMS, false); - createUserInProviderRealm(ImmutableMap.>builder() - .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) - .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.builder().add("value 2").build()) - .build()); + newValueForAttribute2 = "value 2"; + UserRepresentation user = createMapperAndLoginAsUserTwiceWithMapper(FORCE, false); - logInAsUserInIDPForFirstTime(); + assertThatRoleHasBeenAssignedInConsumerRealmTo(user); + } - UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); - assertThatRoleHasBeenAssigned(user); + @Test + public void updateBrokeredUserAssignsRoleInForceModeWhenCreatingTheMapperAfterFirstLogin() { + newValueForAttribute2 = "value 2"; + UserRepresentation user = createMapperAndLoginAsUserTwiceWithMapper(FORCE, true); - logoutFromRealm(bc.consumerRealmName()); + assertThatRoleHasBeenAssignedInConsumerRealmTo(user); + } - // update - user = findUser(bc.providerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + public UserRepresentation createMapperAndLoginAsUserTwiceWithMapper(IdentityProviderMapperSyncMode syncMode, boolean createAfterFirstLogin) { + return loginAsUserTwiceWithMapper(syncMode, createAfterFirstLogin, ImmutableMap.>builder() + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.builder().add("value 2").build()) + .build()); + } + + @Override + protected void updateUser() { + UserRepresentation user = findUser(bc.providerRealmName(), bc.getUserLogin(), bc.getUserEmail()); ImmutableMap> matchingAttributes = ImmutableMap.>builder() - .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) - .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.builder().add("value 2").build()) - .put("some.other.attribute", ImmutableList.builder().add("some value").build()) - .build(); + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.builder().add("value 1").build()) + .put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.builder().add(newValueForAttribute2).build()) + .put("some.other.attribute", ImmutableList.builder().add("some value").build()) + .build(); user.setAttributes(matchingAttributes); adminClient.realm(bc.providerRealmName()).users().get(user.getId()).update(user); + } - logInAsUserInIDP(); - user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); - - assertThatRoleHasBeenAssigned(user); + @Override + protected void createMapperInIdp(IdentityProviderRepresentation idp, IdentityProviderMapperSyncMode syncMode) { + createAdvancedClaimToRoleMapperInIdp(idp, CLAIMS, false, syncMode); } private void createAdvancedClaimToRoleMapper(String claimsRepresentation, boolean areClaimValuesRegex) { - log.debug("adding identity provider to realm " + bc.consumerRealmName()); - - RealmResource realm = adminClient.realm(bc.consumerRealmName()); - final IdentityProviderRepresentation idp = bc.setUpIdentityProvider(suiteContext); - Response resp = realm.identityProviders().create(idp); - resp.close(); + IdentityProviderRepresentation idp = setupIdentityProvider(); + createAdvancedClaimToRoleMapperInIdp(idp, claimsRepresentation, areClaimValuesRegex, IMPORT); + } + protected void createAdvancedClaimToRoleMapperInIdp(IdentityProviderRepresentation idp , String claimsRepresentation, boolean areClaimValuesRegex, IdentityProviderMapperSyncMode syncMode) { IdentityProviderMapperRepresentation advancedClaimToRoleMapper = new IdentityProviderMapperRepresentation(); advancedClaimToRoleMapper.setName("advanced-claim-to-role-mapper"); advancedClaimToRoleMapper.setIdentityProviderMapper(AdvancedClaimToRoleMapper.PROVIDER_ID); advancedClaimToRoleMapper.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) .put(AdvancedClaimToRoleMapper.CLAIM_PROPERTY_NAME, claimsRepresentation) .put(AdvancedClaimToRoleMapper.ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME, areClaimValuesRegex ? "true" : "false") .put(ConfigConstants.ROLE, CLIENT_ROLE_MAPPER_REPRESENTATION) @@ -248,52 +215,6 @@ public class OidcAdvancedClaimToRoleMapperTest extends AbstractBaseBrokerTest { IdentityProviderResource idpResource = realm.identityProviders().get(idp.getAlias()); advancedClaimToRoleMapper.setIdentityProviderAlias(bc.getIDPAlias()); - resp = idpResource.addMapper(advancedClaimToRoleMapper); - resp.close(); - } - - private void createUserInProviderRealm(Map> attributes) { - log.debug("Creating user in realm " + bc.providerRealmName()); - - UserRepresentation user = UserBuilder.create() - .username(bc.getUserLogin()) - .email(bc.getUserEmail()) - .build(); - user.setEmailVerified(true); - user.setAttributes(attributes); - this.userId = createUserAndResetPasswordWithAdminClient(adminClient.realm(bc.providerRealmName()), user, bc.getUserPassword()); - } - - private UserRepresentation findUser(String realm, String userName, String email) { - UsersResource consumerUsers = adminClient.realm(realm).users(); - - List users = consumerUsers.list(); - assertThat("There must be exactly one user", users, hasSize(1)); - UserRepresentation user = users.get(0); - assertThat("Username has to match", user.getUsername(), equalTo(userName)); - assertThat("Email has to match", user.getEmail(), equalTo(email)); - - MappingsRepresentation roles = consumerUsers.get(user.getId()).roles().getAll(); - - List realmRoles = roles.getRealmMappings().stream() - .map(RoleRepresentation::getName) - .collect(Collectors.toList()); - user.setRealmRoles(realmRoles); - - Map> clientRoles = new HashMap<>(); - roles.getClientMappings().forEach((key, value) -> clientRoles.put(key, value.getMappings().stream() - .map(RoleRepresentation::getName) - .collect(Collectors.toList()))); - user.setClientRoles(clientRoles); - - return user; - } - - private void assertThatRoleHasBeenAssigned(UserRepresentation user) { - assertThat(user.getClientRoles().get(CLIENT), contains(CLIENT_ROLE)); - } - - private void assertThatRoleHasNotBeenAssigned(UserRepresentation user) { - assertThat(user.getClientRoles().get(CLIENT), not(contains(CLIENT_ROLE))); + idpResource.addMapper(advancedClaimToRoleMapper).close(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcClaimToRoleMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcClaimToRoleMapperTest.java new file mode 100644 index 0000000000..d1fecaa3e4 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcClaimToRoleMapperTest.java @@ -0,0 +1,149 @@ +package org.keycloak.testsuite.broker; + +import static org.keycloak.models.IdentityProviderMapperSyncMode.FORCE; +import static org.keycloak.models.IdentityProviderMapperSyncMode.LEGACY; + +import java.util.List; + +import org.jetbrains.annotations.NotNull; +import org.junit.Test; +import org.keycloak.admin.client.resource.IdentityProviderResource; +import org.keycloak.broker.oidc.mappers.ClaimToRoleMapper; +import org.keycloak.broker.provider.ConfigConstants; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderMapperSyncMode; +import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.UserRepresentation; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; + +/** + * @author Martin Idel + */ +public class OidcClaimToRoleMapperTest extends AbstractRoleMapperTest { + + private static final String CLAIM = KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME; + private static final String CLAIM_VALUE = "value 1"; + private String claimOnSecondLogin = ""; + + @Override + protected BrokerConfiguration getBrokerConfiguration() { + return new KcOidcBrokerConfiguration(); + } + + @Test + public void allClaimValuesMatch() { + createClaimToRoleMapper(CLAIM_VALUE); + createUserInProviderRealm(ImmutableMap.>builder() + .put(CLAIM, ImmutableList.builder().add(CLAIM_VALUE).build()) + .build()); + + logInAsUserInIDPForFirstTime(); + + UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + assertThatRoleHasBeenAssignedInConsumerRealmTo(user); + } + + @Test + public void claimValuesMismatch() { + createClaimToRoleMapper("other value"); + createUserInProviderRealm(ImmutableMap.>builder() + .put(CLAIM, ImmutableList.builder().add(CLAIM_VALUE).build()) + .build()); + + logInAsUserInIDPForFirstTime(); + + UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + assertThatRoleHasNotBeenAssignedInConsumerRealmTo(user); + } + + @Test + public void updateBrokeredUserMismatchDeletesRoleInForceMode() { + UserRepresentation user = loginWithClaimThenChangeClaimToValue("value mismatch", FORCE, false); + + assertThatRoleHasNotBeenAssignedInConsumerRealmTo(user); + } + + @Test + public void updateBrokeredUserMismatchDeletesRoleInLegacyMode() { + UserRepresentation user = createMapperThenLoginWithStandardClaimThenChangeClaimToValue("value mismatch", LEGACY); + + assertThatRoleHasNotBeenAssignedInConsumerRealmTo(user); + } + + @Test + public void updateBrokeredUserNewMatchGrantsRoleAfterFirstLoginInForceMode() { + UserRepresentation user = loginWithStandardClaimThenAddMapperAndLoginAgain(FORCE); + + assertThatRoleHasBeenAssignedInConsumerRealmTo(user); + } + + @Test + public void updateBrokeredUserNewMatchDoesNotGrantRoleAfterFirstLoginInLegacyMode() { + UserRepresentation user = loginWithStandardClaimThenAddMapperAndLoginAgain(LEGACY); + + assertThatRoleHasNotBeenAssignedInConsumerRealmTo(user); + } + + @Test + public void updateBrokeredUserDoesNotDeleteRoleIfClaimStillMatches() { + UserRepresentation user = createMapperThenLoginWithStandardClaimThenChangeClaimToValue(CLAIM_VALUE, FORCE); + + assertThatRoleHasBeenAssignedInConsumerRealmTo(user); + } + + private UserRepresentation loginWithStandardClaimThenAddMapperAndLoginAgain(IdentityProviderMapperSyncMode syncMode) { + return loginWithClaimThenChangeClaimToValue(OidcClaimToRoleMapperTest.CLAIM_VALUE, syncMode, true); + } + + private UserRepresentation createMapperThenLoginWithStandardClaimThenChangeClaimToValue(String claimOnSecondLogin, IdentityProviderMapperSyncMode syncMode) { + return loginWithClaimThenChangeClaimToValue(claimOnSecondLogin, syncMode, false); + } + + @NotNull + private UserRepresentation loginWithClaimThenChangeClaimToValue(String claimOnSecondLogin, IdentityProviderMapperSyncMode syncMode, boolean createAfterFirstLogin) { + this.claimOnSecondLogin = claimOnSecondLogin; + return loginAsUserTwiceWithMapper(syncMode, createAfterFirstLogin, + ImmutableMap.>builder() + .put(CLAIM, ImmutableList.builder().add(CLAIM_VALUE).build()) + .build()); + } + + private void createClaimToRoleMapper(String claimValue) { + IdentityProviderRepresentation idp = setupIdentityProvider(); + createClaimToRoleMapper(idp, claimValue, IdentityProviderMapperSyncMode.IMPORT); + } + + @Override + protected void createMapperInIdp(IdentityProviderRepresentation idp, IdentityProviderMapperSyncMode syncMode) { + createClaimToRoleMapper(idp, CLAIM_VALUE, syncMode); + } + + @Override + protected void updateUser() { + UserRepresentation user = findUser(bc.providerRealmName(), bc.getUserLogin(), bc.getUserEmail()); + ImmutableMap> mismatchingAttributes = ImmutableMap.>builder() + .put(CLAIM, ImmutableList.builder().add(claimOnSecondLogin).build()) + .build(); + user.setAttributes(mismatchingAttributes); + adminClient.realm(bc.providerRealmName()).users().get(user.getId()).update(user); + } + + private void createClaimToRoleMapper(IdentityProviderRepresentation idp, String claimValue, IdentityProviderMapperSyncMode syncMode) { + IdentityProviderMapperRepresentation claimToRoleMapper = new IdentityProviderMapperRepresentation(); + claimToRoleMapper.setName("claim-to-role-mapper"); + claimToRoleMapper.setIdentityProviderMapper(ClaimToRoleMapper.PROVIDER_ID); + claimToRoleMapper.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) + .put(ClaimToRoleMapper.CLAIM, OidcClaimToRoleMapperTest.CLAIM) + .put(ClaimToRoleMapper.CLAIM_VALUE, claimValue) + .put(ConfigConstants.ROLE, CLIENT_ROLE_MAPPER_REPRESENTATION) + .build()); + + IdentityProviderResource idpResource = realm.identityProviders().get(idp.getAlias()); + claimToRoleMapper.setIdentityProviderAlias(bc.getIDPAlias()); + idpResource.addMapper(claimToRoleMapper).close(); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcUserAttributeMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcUserAttributeMapperTest.java index d07ec0f70b..e146b7051d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcUserAttributeMapperTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/OidcUserAttributeMapperTest.java @@ -1,6 +1,8 @@ package org.keycloak.testsuite.broker; import org.keycloak.broker.oidc.mappers.UserAttributeMapper; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderMapperSyncMode; import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; import com.google.common.collect.ImmutableMap; @@ -14,11 +16,12 @@ public class OidcUserAttributeMapperTest extends AbstractUserAttributeMapperTest } @Override - protected Iterable createIdentityProviderMappers() { + protected Iterable createIdentityProviderMappers(IdentityProviderMapperSyncMode syncMode) { IdentityProviderMapperRepresentation attrMapper1 = new IdentityProviderMapperRepresentation(); attrMapper1.setName("attribute-mapper"); attrMapper1.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID); attrMapper1.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) .put(UserAttributeMapper.CLAIM, KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME) .put(UserAttributeMapper.USER_ATTRIBUTE, MAPPED_ATTRIBUTE_NAME) .build()); @@ -27,6 +30,7 @@ public class OidcUserAttributeMapperTest extends AbstractUserAttributeMapperTest emailAttrMapper.setName("attribute-mapper-email"); emailAttrMapper.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID); emailAttrMapper.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) .put(UserAttributeMapper.CLAIM, "email") .put(UserAttributeMapper.USER_ATTRIBUTE, "email") .build()); @@ -35,6 +39,7 @@ public class OidcUserAttributeMapperTest extends AbstractUserAttributeMapperTest nestedEmailAttrMapper.setName("nested-attribute-mapper-email"); nestedEmailAttrMapper.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID); nestedEmailAttrMapper.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) .put(UserAttributeMapper.CLAIM, "nested.email") .put(UserAttributeMapper.USER_ATTRIBUTE, "nested.email") .build()); @@ -43,6 +48,7 @@ public class OidcUserAttributeMapperTest extends AbstractUserAttributeMapperTest dottedEmailAttrMapper.setName("dotted-attribute-mapper-email"); dottedEmailAttrMapper.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID); dottedEmailAttrMapper.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) .put(UserAttributeMapper.CLAIM, "dotted\\.email") .put(UserAttributeMapper.USER_ATTRIBUTE, "dotted.email") .build()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SamlUserAttributeMapperTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SamlUserAttributeMapperTest.java index e6446dcb3d..8765fee624 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SamlUserAttributeMapperTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SamlUserAttributeMapperTest.java @@ -1,6 +1,8 @@ package org.keycloak.testsuite.broker; import org.keycloak.broker.saml.mappers.UserAttributeMapper; +import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderMapperSyncMode; import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; import com.google.common.collect.ImmutableMap; @@ -14,11 +16,12 @@ public class SamlUserAttributeMapperTest extends AbstractUserAttributeMapperTest } @Override - protected Iterable createIdentityProviderMappers() { + protected Iterable createIdentityProviderMappers(IdentityProviderMapperSyncMode syncMode) { IdentityProviderMapperRepresentation attrMapperEmail = new IdentityProviderMapperRepresentation(); attrMapperEmail.setName("attribute-mapper-email"); attrMapperEmail.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID); attrMapperEmail.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) .put(UserAttributeMapper.ATTRIBUTE_FRIENDLY_NAME, "email") .put(UserAttributeMapper.USER_ATTRIBUTE, "email") .build()); @@ -27,6 +30,7 @@ public class SamlUserAttributeMapperTest extends AbstractUserAttributeMapperTest attrMapperNestedEmail.setName("nested-attribute-mapper-email"); attrMapperNestedEmail.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID); attrMapperNestedEmail.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) .put(UserAttributeMapper.ATTRIBUTE_NAME, "nested.email") .put(UserAttributeMapper.USER_ATTRIBUTE, "nested.email") .build()); @@ -35,6 +39,7 @@ public class SamlUserAttributeMapperTest extends AbstractUserAttributeMapperTest attrMapperDottedEmail.setName("dotted-attribute-mapper-email"); attrMapperDottedEmail.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID); attrMapperDottedEmail.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) .put(UserAttributeMapper.ATTRIBUTE_NAME, "dotted.email") .put(UserAttributeMapper.USER_ATTRIBUTE, "dotted.email") .build()); @@ -43,6 +48,7 @@ public class SamlUserAttributeMapperTest extends AbstractUserAttributeMapperTest attrMapper1.setName("attribute-mapper"); attrMapper1.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID); attrMapper1.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) .put(UserAttributeMapper.ATTRIBUTE_NAME, KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME) .put(UserAttributeMapper.USER_ATTRIBUTE, MAPPED_ATTRIBUTE_NAME) .build()); @@ -51,6 +57,7 @@ public class SamlUserAttributeMapperTest extends AbstractUserAttributeMapperTest attrMapper2.setName("attribute-mapper-friendly"); attrMapper2.setIdentityProviderMapper(UserAttributeMapper.PROVIDER_ID); attrMapper2.setConfig(ImmutableMap.builder() + .put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString()) .put(UserAttributeMapper.ATTRIBUTE_FRIENDLY_NAME, ATTRIBUTE_TO_MAP_FRIENDLY_NAME) .put(UserAttributeMapper.USER_ATTRIBUTE, MAPPED_ATTRIBUTE_FRIENDLY_NAME) .build()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java index aba47ca2c9..32e3dd590c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java @@ -13,6 +13,7 @@ import org.keycloak.authorization.model.ResourceServer; import org.keycloak.common.Profile; import org.keycloak.models.ClientModel; import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.protocol.oidc.OIDCLoginProtocol; @@ -370,7 +371,10 @@ public class SocialLoginTest extends AbstractKeycloakTest { } public IdentityProviderRepresentation buildIdp(Provider provider) { - IdentityProviderRepresentation idp = IdentityProviderBuilder.create().alias(provider.id()).providerId(provider.id()).build(); + IdentityProviderRepresentation idp = IdentityProviderBuilder.create() + .alias(provider.id()) + .providerId(provider.id()) + .build(); idp.setEnabled(true); idp.setStoreToken(true); idp.getConfig().put("clientId", getConfig(provider, "clientId")); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmCreateTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmCreateTest.java index 6fc9f9a17e..c9734ed1d4 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmCreateTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/cli/admin/KcAdmCreateTest.java @@ -2,17 +2,29 @@ package org.keycloak.testsuite.cli.admin; import org.junit.Assert; import org.junit.Test; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.broker.saml.SAMLIdentityProviderConfig; +import org.keycloak.broker.saml.SAMLIdentityProviderFactory; import org.keycloak.client.admin.cli.config.FileConfigHandler; +import org.keycloak.models.IdentityProviderModel; import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.testsuite.cli.KcAdmExec; +import org.keycloak.testsuite.updaters.IdentityProviderCreator; +import org.keycloak.testsuite.util.IdentityProviderBuilder; import org.keycloak.testsuite.util.TempFileResource; import org.keycloak.util.JsonSerialization; +import java.io.Closeable; +import java.io.File; import java.io.IOException; import java.util.Arrays; + import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; import static org.keycloak.testsuite.cli.KcAdmExec.execute; /** @@ -40,6 +52,23 @@ public class KcAdmCreateTest extends AbstractAdmCliTest { } } + @Test + public void testCreateIDPWithoutSyncMode() throws IOException { + final String realm = "test"; + final RealmResource realmResource = adminClient.realm(realm); + + FileConfigHandler handler = initCustomConfigFile(); + try (TempFileResource configFile = new TempFileResource(handler.getConfigFile())) { + loginAsUser(configFile.getFile(), serverUrl, realm, "user1", "userpass"); + + final File idpJson = new File("target/test-classes/cli/idp-keycloak-without-sync-mode.json"); + KcAdmExec exe = execute("create identity-provider/instances/ -r " + realm + " -f " + idpJson.getAbsolutePath() + " --config " + configFile.getFile()); + assertExitCodeAndStdErrSize(exe, 0, 1); + } + + // If the sync mode is not present on creating the idp, it will never be added automatically. However, the model will always assume "LEGACY", so no errors should occur. + Assert.assertNull(realmResource.identityProviders().get("idpAlias").toRepresentation().getConfig().get(IdentityProviderModel.SYNC_MODE)); + } @Test public void testCreateThoroughly() throws IOException { diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/cli/idp-keycloak-9167.json b/testsuite/integration-arquillian/tests/base/src/test/resources/cli/idp-keycloak-9167.json index 79c23c0982..87877e4da9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/cli/idp-keycloak-9167.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/cli/idp-keycloak-9167.json @@ -11,6 +11,7 @@ "linkOnly" : false, "firstBrokerLoginFlowAlias" : "first broker login", "config" : { + "syncMode": "IMPORT", "nameIDPolicyFormat" : "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", "postBindingResponse" : "false", "singleLogoutServiceUrl" : "https://saml.idp/saml", diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/cli/idp-keycloak-without-sync-mode.json b/testsuite/integration-arquillian/tests/base/src/test/resources/cli/idp-keycloak-without-sync-mode.json new file mode 100644 index 0000000000..79c23c0982 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/cli/idp-keycloak-without-sync-mode.json @@ -0,0 +1,21 @@ +{ + "alias" : "idpAlias", + "displayName" : "SAML_UPDATED", + "providerId" : "saml", + "enabled" : true, + "updateProfileFirstLoginMode" : "on", + "trustEmail" : false, + "storeToken" : false, + "addReadTokenRoleOnCreate" : false, + "authenticateByDefault" : false, + "linkOnly" : false, + "firstBrokerLoginFlowAlias" : "first broker login", + "config" : { + "nameIDPolicyFormat" : "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", + "postBindingResponse" : "false", + "singleLogoutServiceUrl" : "https://saml.idp/saml", + "postBindingAuthnRequest" : "false", + "singleSignOnServiceUrl" : "https://saml.idp/saml", + "backchannelSupported" : "false" + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/export/partialexport-testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/export/partialexport-testrealm.json index 6e7ab3a3b4..4c80bae444 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/export/partialexport-testrealm.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/export/partialexport-testrealm.json @@ -1166,6 +1166,7 @@ "alias" : "google1", "enabled": true, "config": { + "syncMode": "IMPORT", "clientId": "googleId", "clientSecret": "googleSecret" } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json index 4b6d48461b..d2f28be2c2 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json @@ -24,6 +24,7 @@ "alias" : "google1", "enabled": true, "config": { + "syncMode": "IMPORT", "clientId": "googleId", "clientSecret": "googleSecret" } @@ -33,6 +34,7 @@ "alias" : "facebook1", "enabled": true, "config": { + "syncMode": "IMPORT", "clientId": "facebookId", "clientSecret": "facebookSecret" } @@ -42,6 +44,7 @@ "alias" : "twitter1", "enabled": true, "config": { + "syncMode": "IMPORT", "clientId": "twitterId", "clientSecret": "twitterSecret" } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/broker/kc3731-broker-realm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/broker/kc3731-broker-realm.json index 41d4d073ee..e003ee7231 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/broker/kc3731-broker-realm.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/org/keycloak/testsuite/broker/kc3731-broker-realm.json @@ -64,6 +64,7 @@ "authenticateByDefault" : false, "firstBrokerLoginFlowAlias" : "first broker login", "config" : { + "syncMode": "IMPORT", "nameIDPolicyFormat" : "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", "postBindingAuthnRequest" : "true", "postBindingResponse" : "true", @@ -78,6 +79,7 @@ "identityProviderAlias" : "saml-leaf", "identityProviderMapper" : "saml-role-idp-mapper", "config" : { + "syncMode": "INHERIT", "attribute.value" : "manager", "role" : "${url.realm.consumer}/app/auth.manager", "attribute.name" : "Role" diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/idp/CreateIdentityProviderMapper.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/idp/CreateIdentityProviderMapper.java new file mode 100644 index 0000000000..ba7751752f --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/idp/CreateIdentityProviderMapper.java @@ -0,0 +1,48 @@ +/* + * Copyright 2019 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.testsuite.console.page.idp; + +import org.jboss.arquillian.graphene.page.Page; +import org.keycloak.testsuite.console.page.AdminConsoleCreate; + +/** + * @author Martin Idel + */ +public class CreateIdentityProviderMapper extends AdminConsoleCreate { + public String idp; + + @Page + private IdentityProviderMapperForm form; + + public CreateIdentityProviderMapper() { + setEntity("identity-provider-mappers"); + } + + public void setIdp(String idp) { + this.idp = idp; + } + + @Override + public String getUriFragment() { + return super.getUriFragment() + "/" + idp + ""; + } + + public IdentityProviderMapperForm form() { + return form; + } +} diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/idp/IdentityProviderForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/idp/IdentityProviderForm.java index a6dd39abd4..6413e964ba 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/idp/IdentityProviderForm.java +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/idp/IdentityProviderForm.java @@ -21,6 +21,7 @@ import org.keycloak.testsuite.console.page.fragment.KcPassword; import org.keycloak.testsuite.page.Form; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; +import org.openqa.selenium.support.ui.Select; import static org.keycloak.testsuite.util.UIUtils.setTextInputValue; @@ -34,6 +35,15 @@ public class IdentityProviderForm extends Form { @FindBy(id = "clientSecret") private KcPassword clientSecretInput; + @FindBy(id = "syncMode") + private Select syncMode; + + @FindBy(linkText = "Mappers") + private WebElement mappersTab; + + @FindBy(linkText = "Create") + private WebElement mapperCreateButton; + public void setClientId(final String value) { setTextInputValue(clientIdInput, value); } @@ -42,7 +52,20 @@ public class IdentityProviderForm extends Form { clientSecretInput.setValue(value); } + public void setSyncMode(final String value) { + syncMode.selectByVisibleText(value); + } + + public String syncMode() { + return syncMode.getFirstSelectedOption().getText(); + } + public KcPassword clientSecret() { return clientSecretInput; } + + public void createMapper() { + mappersTab.click(); + mapperCreateButton.click(); + } } diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/idp/IdentityProviderMapperForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/idp/IdentityProviderMapperForm.java new file mode 100644 index 0000000000..b054a867ab --- /dev/null +++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/idp/IdentityProviderMapperForm.java @@ -0,0 +1,48 @@ +/* + * Copyright 2019 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.testsuite.console.page.idp; + +import org.keycloak.testsuite.page.Form; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; +import org.openqa.selenium.support.ui.Select; + +import static org.keycloak.testsuite.util.UIUtils.setTextInputValue; + +/** + * @author Martin Idel + */ +public class IdentityProviderMapperForm extends Form { + @FindBy(id = "name") + private WebElement name; + + @FindBy(id = "syncMode") + private Select syncMode; + + public void setName(final String value) { + setTextInputValue(name, value); + } + + public void setSyncMode(final String value) { + syncMode.selectByVisibleText(value); + } + + public String syncMode() { + return syncMode.getFirstSelectedOption().getText(); + } +} diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/idp/IdentityProviderTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/idp/IdentityProviderTest.java index d15a420185..78e9131d7b 100644 --- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/idp/IdentityProviderTest.java +++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/idp/IdentityProviderTest.java @@ -22,18 +22,22 @@ import org.junit.Before; import org.junit.Test; import org.keycloak.testsuite.console.AbstractConsoleTest; import org.keycloak.testsuite.console.page.idp.CreateIdentityProvider; +import org.keycloak.testsuite.console.page.idp.CreateIdentityProviderMapper; import org.keycloak.testsuite.console.page.idp.IdentityProvider; import org.keycloak.testsuite.console.page.idp.IdentityProviders; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.keycloak.testsuite.util.UIUtils.refreshPageAndWaitForLoad; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals; +import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; /** * * @author Petr Mensik * @author Vaclav Muzikar + * @author Martin Idel */ public class IdentityProviderTest extends AbstractConsoleTest { @Page @@ -45,6 +49,9 @@ public class IdentityProviderTest extends AbstractConsoleTest { @Page private CreateIdentityProvider createIdentityProviderPage; + @Page + private CreateIdentityProviderMapper createIdentityProviderMapperPage; + @Before public void beforeIdentityProviderTest() { identityProvidersPage.navigateTo(); @@ -91,6 +98,44 @@ public class IdentityProviderTest extends AbstractConsoleTest { assertPasswordIsMasked(); } + @Test + public void settingAndSavingSyncMode() { + createIdentityProviderPage.setProviderId("google"); + identityProviderPage.setIds("google", "google"); + + identityProvidersPage.addProvider("google"); + assertCurrentUrlEquals(createIdentityProviderPage); + identityProviderPage.form().setSyncMode("force"); + + createIdentityProviderPage.form().setClientId("test-google"); + createIdentityProviderPage.form().setClientSecret("secret"); + + createIdentityProviderPage.form().save(); + assertAlertSuccess(); + refreshPageAndWaitForLoad(); + assertCurrentUrlEquals(identityProviderPage); + assertSyncModeIsSetToForce(); + + identityProviderPage.form().createMapper(); + createIdentityProviderMapperPage.setIdp("google"); + assertCurrentUrlEquals(createIdentityProviderMapperPage); + createIdentityProviderMapperPage.form().setName("TestMapper"); + createIdentityProviderMapperPage.form().setSyncMode("import"); + + createIdentityProviderMapperPage.form().save(); + assertAlertSuccess(); + refreshPageAndWaitForLoad(); + assertMapperSyncModeIsSetToImport(); + } + + private void assertMapperSyncModeIsSetToImport() { + assertEquals("import", createIdentityProviderMapperPage.form().syncMode()); + } + + private void assertSyncModeIsSetToForce() { + assertEquals("force", identityProviderPage.form().syncMode()); + } + private void assertEyeButtonIsDisabled() { assertTrue("Eye button is not disabled", identityProviderPage.form().clientSecret().isEyeButtonDisabled()); } diff --git a/themes/src/main/resources-community/theme/base/admin/messages/admin-messages_de.properties b/themes/src/main/resources-community/theme/base/admin/messages/admin-messages_de.properties index d6daac3a96..4ed733ccec 100644 --- a/themes/src/main/resources-community/theme/base/admin/messages/admin-messages_de.properties +++ b/themes/src/main/resources-community/theme/base/admin/messages/admin-messages_de.properties @@ -501,6 +501,14 @@ last-refresh=Letzte Aktualisierung #gui-order=GUI order #first-broker-login-flow=First Login Flow #post-broker-login-flow=Post Login Flow +sync-mode=Synchronisationsmodus +sync-mode.tooltip=Standardsyncmodus für alle Mapper. Mögliche Werte sind: 'Legacy' um das alte Verhalten beizubehalten, 'Importieren' um den Nutzer einmalig zu importieren, 'Erzwingen' um den Nutzer immer zu importieren. +sync-mode.inherit=Standard erben +sync-mode.legacy=Legacy +sync-mode.import=Importieren +sync-mode.force=Erzwingen +sync-mode-override=Überschriebene Synchronisation +sync-mode-override.tooltip=Überschreibt den normalen Synchronisationsmodus des IDP für diesen Mapper. Were sind 'Legacy' um das alte Verhalten beizubehalten, 'Importieren' um den Nutzer einmalig zu importieren, 'Erzwingen' um den Nutzer immer zu updaten. #redirect-uri=Redirect URI #redirect-uri.tooltip=The redirect uri to use when configuring the identity provider. #alias=Alias diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index fb11d93f3f..aa6dc99698 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -540,6 +540,14 @@ provider=Provider gui-order=GUI order first-broker-login-flow=First Login Flow post-broker-login-flow=Post Login Flow +sync-mode=Sync Mode +sync-mode.tooltip=Default sync mode for all mappers. The sync mode determines when user data will be synced using the mappers. Possible values are: 'legacy' to keep the behaviour before this option was introduced, 'import' to only import the user once during first login of the user with this identity provider, 'force' to always update the user during every login with this identity provider". +sync-mode.inherit=inherit +sync-mode.legacy=legacy +sync-mode.import=import +sync-mode.force=force +sync-mode-override=Sync Mode Override +sync-mode-override.tooltip=Overrides the default sync mode of the IDP for this mapper. Values are: 'legacy' to keep the behaviour before this option was introduced, 'import' to only import the user once during first login of the user with this identity provider, 'force' to always update the user during every login with this identity provider" and 'inherit' to use the sync mode defined in the identity provider for this mapper. redirect-uri=Redirect URI redirect-uri.tooltip=The redirect uri to use when configuring the identity provider. alias=Alias diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index 74dcba93d4..63f97b2798 100644 --- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -900,6 +900,7 @@ module.controller('RealmIdentityProviderCtrl', function($scope, $filter, $upload $scope.identityProvider.authenticateByDefault = false; $scope.identityProvider.firstBrokerLoginFlowAlias = 'first broker login'; $scope.identityProvider.config.useJwksUrl = 'true'; + $scope.identityProvider.config.syncMode = 'IMPORT'; $scope.newIdentityProvider = true; } @@ -2090,6 +2091,7 @@ module.controller('IdentityProviderMapperCreateCtrl', function($scope, realm, id // make first type the default $scope.mapperType = mapperTypes[Object.keys(mapperTypes)[0]]; + $scope.mapper.config.syncMode = 'INHERIT'; $scope.$watch(function() { return $location.path(); diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mapper-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mapper-detail.html index a6b6c5e4f2..7e62b1449b 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mapper-detail.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/identity-provider-mapper-detail.html @@ -27,6 +27,22 @@ {{:: 'mapper.name.tooltip' | translate}} +
+ +
+
+ +
+
+ {{:: 'sync-mode-override.tooltip' | translate}} +
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-bitbucket.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-bitbucket.html index 9667b34518..fab751e2c6 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-bitbucket.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-bitbucket.html @@ -113,6 +113,21 @@
{{:: 'post-broker-login-flow.tooltip' | translate}}
+
+ +
+
+ +
+
+ {{:: 'sync-mode.tooltip' | translate}} +
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-gitlab.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-gitlab.html index 9a30c52188..00c0e947b8 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-gitlab.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-gitlab.html @@ -113,6 +113,21 @@
{{:: 'post-broker-login-flow.tooltip' | translate}} +
+ +
+
+ +
+
+ {{:: 'sync-mode.tooltip' | translate}} +
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html index d27555cc3f..19cfcb52c8 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html @@ -107,6 +107,21 @@
{{:: 'post-broker-login-flow.tooltip' | translate}} +
+ +
+
+ +
+
+ {{:: 'sync-mode.tooltip' | translate}} +
{{:: 'openid-connect-config' | translate}} {{:: 'openid-connect-config.tooltip' | translate}} diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-openshift-v3.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-openshift-v3.html index eeae6be9f5..eb42ba1fb9 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-openshift-v3.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-openshift-v3.html @@ -135,6 +135,21 @@ {{:: 'post-broker-login-flow.tooltip' | translate}} +
+ +
+
+ +
+
+ {{:: 'sync-mode.tooltip' | translate}} +
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-openshift-v4.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-openshift-v4.html index ea4639ade1..9e14154287 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-openshift-v4.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-openshift-v4.html @@ -135,6 +135,21 @@
{{:: 'post-broker-login-flow.tooltip' | translate}} +
+ +
+
+ +
+
+ {{:: 'sync-mode.tooltip' | translate}} +
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html index 7c9aa7feb1..7267ceede6 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html @@ -107,6 +107,21 @@
{{:: 'post-broker-login-flow.tooltip' | translate}} +
+ +
+
+ +
+
+ {{:: 'sync-mode.tooltip' | translate}} +
{{:: 'saml-config' | translate}} {{:: 'identity-provider.saml-config.tooltip' | translate}} diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html index 1ffdc7cee3..b745634801 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-social.html @@ -128,6 +128,21 @@ {{:: 'post-broker-login-flow.tooltip' | translate}} +
+ +
+
+ +
+
+ {{:: 'sync-mode.tooltip' | translate}} +