KEYCLOAK-13959 Add AdvancedAttribute mapper for SAML to allow regexes

This commit is contained in:
Martin Idel 2020-04-20 12:40:05 +02:00 committed by Hynek Mlnařík
parent f639cc82b7
commit 8fe25948f7
14 changed files with 552 additions and 225 deletions

View file

@ -38,6 +38,8 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.keycloak.utils.RegexUtils.valueMatchesRegex;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke, Benjamin Weimer</a>
* @version $Revision: 1 $
@ -108,9 +110,9 @@ public class AdvancedClaimToRoleMapper extends AbstractClaimMapper {
@Override
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
String roleName = mapperModel.getConfig().get(ConfigConstants.ROLE);
RoleModel role = getRoleModel(realm, roleName);
if (hasAllClaimValues(mapperModel, context)) {
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
user.grantRole(role);
}
}
@ -118,9 +120,9 @@ public class AdvancedClaimToRoleMapper extends AbstractClaimMapper {
@Override
public void updateBrokeredUserLegacy(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
String roleName = mapperModel.getConfig().get(ConfigConstants.ROLE);
RoleModel role = getRoleModel(realm, roleName);
if (!hasAllClaimValues(mapperModel, context)) {
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
if (role == null) throw new IdentityBrokerException("Unable to find role: " + roleName);
user.deleteRoleMapping(role);
}
@ -129,18 +131,22 @@ 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 = getRoleModel(realm, roleName);
if (hasAllClaimValues(mapperModel, context)) {
user.grantRole(role);
} else {
user.deleteRoleMapping(role);
}
}
private RoleModel getRoleModel(RealmModel realm, String roleName) {
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);
}
return role;
}
@Override
public String getHelpText() {
return "If all claims exists, grant the user the specified realm or application role.";
@ -148,7 +154,7 @@ public class AdvancedClaimToRoleMapper extends AbstractClaimMapper {
protected boolean hasAllClaimValues(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
Map<String, String> claims = mapperModel.getConfigMap(CLAIM_PROPERTY_NAME);
Boolean areClaimValuesRegex = Boolean.valueOf(mapperModel.getConfig().get(ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME));
boolean areClaimValuesRegex = Boolean.parseBoolean(mapperModel.getConfig().get(ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME));
for (Map.Entry<String, String> claim : claims.entrySet()) {
Object value = getClaimValue(context, claim.getKey());
@ -161,23 +167,4 @@ public class AdvancedClaimToRoleMapper extends AbstractClaimMapper {
return true;
}
public boolean valueMatchesRegex(String regex, Object value) {
if (value instanceof List) {
List list = (List) value;
for (Object val : list) {
if (valueMatchesRegex(regex, val)) {
return true;
}
}
} else {
if (value != null) {
String stringValue = value.toString();
if (stringValue != null && stringValue.matches(regex)) {
return true;
}
}
}
return false;
}
}

View file

@ -0,0 +1,189 @@
/*
* Copyright 2020 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.broker.saml.mappers;
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.ConfigConstants;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.broker.saml.SAMLEndpoint;
import org.keycloak.broker.saml.SAMLIdentityProviderFactory;
import org.keycloak.dom.saml.v2.assertion.AssertionType;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.IdentityProviderSyncMode;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
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;
import java.util.stream.Collectors;
import static org.keycloak.utils.RegexUtils.valueMatchesRegex;
/**
* <a href="mailto:external.benjamin.weimer@bosch.io">Benjamin Weimer</a>,
* <a href="mailto:external.martin.idel@bosch.io">Martin Idel</a>,
*/
public class AdvancedAttributeToRoleMapper extends AbstractIdentityProviderMapper {
public static final String PROVIDER_ID = "saml-advanced-role-idp-mapper";
public static final String ATTRIBUTE_PROPERTY_NAME = "attributes";
public static final String ARE_ATTRIBUTE_VALUES_REGEX_PROPERTY_NAME = "are.attribute.values.regex";
private static final Set<IdentityProviderSyncMode> IDENTITY_PROVIDER_SYNC_MODES = new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values()));
public static final String[] COMPATIBLE_PROVIDERS = {
SAMLIdentityProviderFactory.PROVIDER_ID
};
private static final List<ProviderConfigProperty> configProperties =
new ArrayList<>();
static {
ProviderConfigProperty attributeMappingProperty = new ProviderConfigProperty();
attributeMappingProperty.setName(ATTRIBUTE_PROPERTY_NAME);
attributeMappingProperty.setLabel("Attributes");
attributeMappingProperty.setHelpText(
"Name and (regex) value of the attributes to search for in token. "
+ " The configured name of an attribute is searched in SAML attribute name and attribute friendly name fields."
+ " Every given attribute description must be met to set the role."
+ " If the attribute is an array, then the value must be contained in the array."
+ " If an attribute can be found several times, then one match is sufficient.");
attributeMappingProperty.setType(ProviderConfigProperty.MAP_TYPE);
configProperties.add(attributeMappingProperty);
ProviderConfigProperty isAttributeRegexProperty = new ProviderConfigProperty();
isAttributeRegexProperty.setName(ARE_ATTRIBUTE_VALUES_REGEX_PROPERTY_NAME);
isAttributeRegexProperty.setLabel("Regex Attribute Values");
isAttributeRegexProperty.setHelpText("If enabled attribute values are interpreted as regular expressions.");
isAttributeRegexProperty.setType(ProviderConfigProperty.BOOLEAN_TYPE);
configProperties.add(isAttributeRegexProperty);
ProviderConfigProperty roleProperty = new ProviderConfigProperty();
roleProperty.setName(ConfigConstants.ROLE);
roleProperty.setLabel("Role");
roleProperty.setHelpText("Role to grant to user if all attributes are present."
+ " Click 'Select Role' button to browse roles, or just type it in the textbox."
+ " To reference an application role the syntax is appname.approle, i.e. myapp.myrole");
roleProperty.setType(ProviderConfigProperty.ROLE_TYPE);
configProperties.add(roleProperty);
}
@Override
public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) {
return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode);
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String[] getCompatibleProviders() {
return COMPATIBLE_PROVIDERS;
}
@Override
public String getDisplayCategory() {
return "Role Importer";
}
@Override
public String getDisplayType() {
return "Advanced Attribute to Role";
}
@Override
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
String roleName = mapperModel.getConfig().get(ConfigConstants.ROLE);
RoleModel role = getRoleModel(realm, roleName);
if (hasAllValues(mapperModel, context)) {
user.grantRole(role);
}
}
@Override
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
String roleName = mapperModel.getConfig().get(ConfigConstants.ROLE);
RoleModel role = getRoleModel(realm, roleName);
if (hasAllValues(mapperModel, context)) {
user.grantRole(role);
} else {
user.deleteRoleMapping(role);
}
}
@Override
public String getHelpText() {
return "If the set of attributes exists and can be matched, grant the user the specified realm or application role.";
}
static RoleModel getRoleModel(RealmModel realm, String roleName) {
RoleModel role = KeycloakModelUtils.getRoleFromString(realm, roleName);
if (role == null) {
throw new IdentityBrokerException("Unable to find role: " + roleName);
}
return role;
}
boolean hasAllValues(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
Map<String, String> attributes = mapperModel.getConfigMap(ATTRIBUTE_PROPERTY_NAME);
boolean areAttributeValuesRegexes = Boolean.parseBoolean(mapperModel.getConfig().get(ARE_ATTRIBUTE_VALUES_REGEX_PROPERTY_NAME));
AssertionType assertion = (AssertionType) context.getContextData().get(SAMLEndpoint.SAML_ASSERTION);
Set<AttributeStatementType> attributeAssertions = assertion.getAttributeStatements();
if (attributeAssertions == null) {
return false;
}
for (Map.Entry<String, String> attribute : attributes.entrySet()) {
String attributeKey = attribute.getKey();
List<Object> attributeValues = attributeAssertions.stream()
.flatMap(statements -> statements.getAttributes().stream())
.filter(choiceType -> attributeKey.equals(choiceType.getAttribute().getName())
|| attributeKey.equals(choiceType.getAttribute().getFriendlyName()))
// Several statements with same name are treated like one with several values
.flatMap(choiceType -> choiceType.getAttribute().getAttributeValue().stream())
.collect(Collectors.toList());
boolean attributeValueMatch = areAttributeValuesRegexes ? valueMatchesRegex(attribute.getValue(), attributeValues) : attributeValues.contains(attribute.getValue());
if (!attributeValueMatch) {
return false;
}
}
return true;
}
}

View file

@ -164,7 +164,7 @@ public class AttributeToRoleMapper extends AbstractIdentityProviderMapper {
@Override
public String getHelpText() {
return "If a claim exists, grant the user the specified realm or application role.";
return "If an attribute exists, grant the user the specified realm or application role.";
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright 2020 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.utils;
import java.util.List;
/**
* <a href="mailto:external.benjamin.weimer@bosch-si.com">Benjamin Weimer</a>,
*/
public class RegexUtils {
public static boolean valueMatchesRegex(String regex, Object value) {
if (value instanceof List) {
List list = (List) value;
for (Object val : list) {
if (valueMatchesRegex(regex, val)) {
return true;
}
}
} else {
if (value != null) {
String stringValue = value.toString();
return stringValue != null && stringValue.matches(regex);
}
}
return false;
}
}

View file

@ -23,6 +23,7 @@ org.keycloak.broker.oidc.mappers.AdvancedClaimToRoleMapper
org.keycloak.broker.oidc.mappers.ExternalKeycloakRoleToRoleMapper
org.keycloak.broker.oidc.mappers.UserAttributeMapper
org.keycloak.broker.oidc.mappers.UsernameTemplateMapper
org.keycloak.broker.saml.mappers.AdvancedAttributeToRoleMapper
org.keycloak.broker.saml.mappers.AttributeToRoleMapper
org.keycloak.broker.saml.mappers.UserAttributeMapper
org.keycloak.broker.saml.mappers.UsernameTemplateMapper

View file

@ -0,0 +1,25 @@
package org.keycloak.utils;
import org.junit.Test;
import java.util.Arrays;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
/**
* <a href="mailto:external.benjamin.weimer@bosch-si.com">Benjamin Weimer</a>,
*/
public class RegexUtilsTest {
@Test
public void valueMatchesRegexTest() {
assertThat(RegexUtils.valueMatchesRegex("AB.*", "AB_ADMIN"), is(true));
assertThat(RegexUtils.valueMatchesRegex("AB.*", "AA_ADMIN"), is(false));
assertThat(RegexUtils.valueMatchesRegex("99.*", 999), is(true));
assertThat(RegexUtils.valueMatchesRegex("98.*", 999), is(false));
assertThat(RegexUtils.valueMatchesRegex("99\\..*", 99.9), is(true));
assertThat(RegexUtils.valueMatchesRegex("AB.*", null), is(false));
assertThat(RegexUtils.valueMatchesRegex("AB.*", Arrays.asList("AB_ADMIN", "AA_ADMIN")), is(true));
}
}

View file

@ -569,7 +569,7 @@ public class IdentityProviderTest extends AbstractAdminTest {
create(createRep("saml", "saml"));
provider = realm.identityProviders().get("saml");
mapperTypes = provider.getMapperTypes();
assertMapperTypes(mapperTypes, "saml-user-attribute-idp-mapper", "saml-role-idp-mapper", "saml-username-idp-mapper");
assertMapperTypes(mapperTypes, "saml-user-attribute-idp-mapper", "saml-role-idp-mapper", "saml-username-idp-mapper", "saml-advanced-role-idp-mapper");
}
private void assertMapperTypes(Map<String, IdentityProviderMapperTypeRepresentation> mapperTypes, String ... mapperIds) {

View file

@ -0,0 +1,179 @@
package org.keycloak.testsuite.broker;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.junit.Test;
import org.keycloak.models.IdentityProviderMapperSyncMode;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import java.util.List;
import static org.keycloak.models.IdentityProviderMapperSyncMode.FORCE;
import static org.keycloak.models.IdentityProviderMapperSyncMode.IMPORT;
/**
* @author hmlnarik,
* <a href="mailto:external.benjamin.weimer@bosch-si.com">Benjamin Weimer</a>,
* <a href="mailto:external.martin.idel@bosch.io">Martin Idel</a>
*/
public abstract class AbstractAdvancedRoleMapperTest extends AbstractRoleMapperTest {
private static final String CLAIMS_OR_ATTRIBUTES = "[\n" +
" {\n" +
" \"key\": \"" + KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME + "\",\n" +
" \"value\": \"value 1\"\n" +
" },\n" +
" {\n" +
" \"key\": \"" + KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2 + "\",\n" +
" \"value\": \"value 2\"\n" +
" }\n" +
"]";
private static final String CLAIMS_OR_ATTRIBUTES_REGEX = "[\n" +
" {\n" +
" \"key\": \"" + KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME + "\",\n" +
" \"value\": \"va.*\"\n" +
" },\n" +
" {\n" +
" \"key\": \"" + KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2 + "\",\n" +
" \"value\": \"value 2\"\n" +
" }\n" +
"]";
private String newValueForAttribute2 = "";
@Test
public void allValuesMatch() {
createAdvancedRoleMapper(CLAIMS_OR_ATTRIBUTES, false);
createUserInProviderRealm(ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add("value 2").build())
.build());
logInAsUserInIDPForFirstTime();
UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail());
assertThatRoleHasBeenAssignedInConsumerRealmTo(user);
}
@Test
public void valuesMismatch() {
createAdvancedRoleMapper(CLAIMS_OR_ATTRIBUTES, false);
createUserInProviderRealm(ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add("value mismatch").build())
.build());
logInAsUserInIDPForFirstTime();
UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail());
assertThatRoleHasNotBeenAssignedInConsumerRealmTo(user);
}
@Test
public void valuesMatchIfNoClaimsSpecified() {
createAdvancedRoleMapper("[]", false);
createUserInProviderRealm(ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("some value").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add("some value").build())
.build());
logInAsUserInIDPForFirstTime();
UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail());
assertThatRoleHasBeenAssignedInConsumerRealmTo(user);
}
@Test
public void allValuesMatchRegex() {
createAdvancedRoleMapper(CLAIMS_OR_ATTRIBUTES_REGEX, true);
createUserInProviderRealm(ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add("value 2").build())
.build());
logInAsUserInIDPForFirstTime();
UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail());
assertThatRoleHasBeenAssignedInConsumerRealmTo(user);
}
@Test
public void valuesMismatchRegex() {
createAdvancedRoleMapper(CLAIMS_OR_ATTRIBUTES_REGEX, true);
createUserInProviderRealm(ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("mismatch").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add("value 2").build())
.build());
logInAsUserInIDPForFirstTime();
UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail());
assertThatRoleHasNotBeenAssignedInConsumerRealmTo(user);
}
@Test
public void updateBrokeredUserMismatchDeletesRole() {
newValueForAttribute2 = "value mismatch";
UserRepresentation user = createMapperAndLoginAsUserTwiceWithMapper(FORCE, false);
assertThatRoleHasNotBeenAssignedInConsumerRealmTo(user);
}
@Test
public void updateBrokeredUserMismatchDoesNotDeleteRoleInImportMode() {
newValueForAttribute2 = "value mismatch";
UserRepresentation user = createMapperAndLoginAsUserTwiceWithMapper(IMPORT, false);
assertThatRoleHasBeenAssignedInConsumerRealmTo(user);
}
@Test
public void updateBrokeredUserMatchDoesntDeleteRole() {
newValueForAttribute2 = "value 2";
UserRepresentation user = createMapperAndLoginAsUserTwiceWithMapper(FORCE, false);
assertThatRoleHasBeenAssignedInConsumerRealmTo(user);
}
@Test
public void updateBrokeredUserAssignsRoleInForceModeWhenCreatingTheMapperAfterFirstLogin() {
newValueForAttribute2 = "value 2";
UserRepresentation user = createMapperAndLoginAsUserTwiceWithMapper(FORCE, true);
assertThatRoleHasBeenAssignedInConsumerRealmTo(user);
}
public UserRepresentation createMapperAndLoginAsUserTwiceWithMapper(IdentityProviderMapperSyncMode syncMode, boolean createAfterFirstLogin) {
return loginAsUserTwiceWithMapper(syncMode, createAfterFirstLogin, ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add("value 2").build())
.build());
}
@Override
protected void updateUser() {
UserRepresentation user = findUser(bc.providerRealmName(), bc.getUserLogin(), bc.getUserEmail());
ImmutableMap<String, List<String>> matchingAttributes = ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add(newValueForAttribute2).build())
.put("some.other.attribute", ImmutableList.<String>builder().add("some value").build())
.build();
user.setAttributes(matchingAttributes);
adminClient.realm(bc.providerRealmName()).users().get(user.getId()).update(user);
}
@Override
protected void createMapperInIdp(IdentityProviderRepresentation idp, IdentityProviderMapperSyncMode syncMode) {
createMapperInIdp(idp, CLAIMS_OR_ATTRIBUTES, false, syncMode);
}
protected void createAdvancedRoleMapper(String claimsOrAttributeRepresentation, boolean areClaimsOrAttributeValuesRegexes) {
IdentityProviderRepresentation idp = setupIdentityProvider();
createMapperInIdp(idp, claimsOrAttributeRepresentation, areClaimsOrAttributeValuesRegexes, IMPORT);
}
abstract protected void createMapperInIdp(
IdentityProviderRepresentation idp, String claimsOrAttributeRepresentation, boolean areClaimsOrAttributeValuesRegexes, IdentityProviderMapperSyncMode syncMode);
}

View file

@ -6,6 +6,7 @@ import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.keycloak.testsuite.broker.BrokerTestTools.getConsumerRoot;
import static org.keycloak.testsuite.broker.KcSamlBrokerConfiguration.ATTRIBUTE_TO_MAP_FRIENDLY_NAME;
import java.util.List;
import java.util.Map;
@ -31,7 +32,6 @@ public abstract class AbstractUserAttributeMapperTest extends AbstractIdentityPr
protected static final String MAPPED_ATTRIBUTE_NAME = "mapped-user-attribute";
protected static final String MAPPED_ATTRIBUTE_FRIENDLY_NAME = "mapped-user-attribute-friendly";
protected static final String ATTRIBUTE_TO_MAP_FRIENDLY_NAME = "user-attribute-friendly";
private static final Set<String> PROTECTED_NAMES = ImmutableSet.<String>builder().add("email").add("lastName").add("firstName").build();
private static final Map<String, String> ATTRIBUTE_NAME_TRANSLATION = ImmutableMap.<String, String>builder()

View file

@ -0,0 +1,72 @@
package org.keycloak.testsuite.broker;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.junit.Test;
import org.keycloak.admin.client.resource.IdentityProviderResource;
import org.keycloak.broker.provider.ConfigConstants;
import org.keycloak.broker.saml.mappers.AdvancedAttributeToRoleMapper;
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 java.util.List;
import static org.keycloak.testsuite.broker.KcSamlBrokerConfiguration.ATTRIBUTE_TO_MAP_FRIENDLY_NAME;
/**
* <a href="mailto:external.martin.idel@bosch.io">Martin Idel</a>
*/
public class KcSamlAdvancedAttributeToRoleMapperTest extends AbstractAdvancedRoleMapperTest {
private static final String ATTRIBUTES = "[\n" +
" {\n" +
" \"key\": \"" + ATTRIBUTE_TO_MAP_FRIENDLY_NAME + "\",\n" +
" \"value\": \"value 1\"\n" +
" },\n" +
" {\n" +
" \"key\": \"" + KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2 + "\",\n" +
" \"value\": \"value 2\"\n" +
" }\n" +
"]";
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return new KcSamlBrokerConfiguration();
}
@Override
protected void createMapperInIdp(IdentityProviderRepresentation idp, String claimsOrAttributeRepresentation, boolean areClaimsOrAttributeValuesRegexes, IdentityProviderMapperSyncMode syncMode) {
IdentityProviderMapperRepresentation advancedAttributeToRoleMapper = new IdentityProviderMapperRepresentation();
advancedAttributeToRoleMapper.setName("advanced-attribute-to-role-mapper");
advancedAttributeToRoleMapper.setIdentityProviderMapper(AdvancedAttributeToRoleMapper.PROVIDER_ID);
advancedAttributeToRoleMapper.setConfig(ImmutableMap.<String, String>builder()
.put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString())
.put(AdvancedAttributeToRoleMapper.ATTRIBUTE_PROPERTY_NAME, claimsOrAttributeRepresentation)
.put(AdvancedAttributeToRoleMapper.ARE_ATTRIBUTE_VALUES_REGEX_PROPERTY_NAME, areClaimsOrAttributeValuesRegexes ? "true" : "false")
.put(ConfigConstants.ROLE, CLIENT_ROLE_MAPPER_REPRESENTATION)
.build());
IdentityProviderResource idpResource = realm.identityProviders().get(idp.getAlias());
advancedAttributeToRoleMapper.setIdentityProviderAlias(bc.getIDPAlias());
idpResource.addMapper(advancedAttributeToRoleMapper).close();
}
@Test
public void attributeFriendlyNameGetsConsideredAndMatchedToRole() {
createAdvancedRoleMapper(ATTRIBUTES, false);
createUserInProviderRealm(ImmutableMap.<String, List<String>>builder()
.put(ATTRIBUTE_TO_MAP_FRIENDLY_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add("value 2").build())
.build());
logInAsUserInIDPForFirstTime();
UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail());
assertThatRoleHasBeenAssignedInConsumerRealmTo(user);
}
}

View file

@ -35,6 +35,7 @@ import static org.keycloak.testsuite.broker.BrokerTestTools.*;
public class KcSamlBrokerConfiguration implements BrokerConfiguration {
public static final KcSamlBrokerConfiguration INSTANCE = new KcSamlBrokerConfiguration();
public static final String ATTRIBUTE_TO_MAP_FRIENDLY_NAME = "user-attribute-friendly";
@Override
public RealmRepresentation createProviderRealm() {
@ -131,18 +132,29 @@ public class KcSamlBrokerConfiguration implements BrokerConfiguration {
userAttrMapperConfig.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT, AttributeStatementHelper.BASIC);
userAttrMapperConfig.put(AttributeStatementHelper.FRIENDLY_NAME, "");
ProtocolMapperRepresentation userAttrMapper2 = new ProtocolMapperRepresentation();
userAttrMapper2.setName("attribute - name 2");
userAttrMapper2.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
userAttrMapper2.setProtocolMapper(UserAttributeStatementMapper.PROVIDER_ID);
Map<String, String> userAttrMapper2Config = userAttrMapper2.getConfig();
userAttrMapper2Config.put(ProtocolMapperUtils.USER_ATTRIBUTE, KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2);
userAttrMapper2Config.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAME, KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2);
userAttrMapper2Config.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT, AttributeStatementHelper.BASIC);
userAttrMapper2Config.put(AttributeStatementHelper.FRIENDLY_NAME, "");
ProtocolMapperRepresentation userFriendlyAttrMapper = new ProtocolMapperRepresentation();
userFriendlyAttrMapper.setName("attribute - friendly name");
userFriendlyAttrMapper.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
userFriendlyAttrMapper.setProtocolMapper(UserAttributeStatementMapper.PROVIDER_ID);
Map<String, String> userFriendlyAttrMapperConfig = userFriendlyAttrMapper.getConfig();
userFriendlyAttrMapperConfig.put(ProtocolMapperUtils.USER_ATTRIBUTE, AbstractUserAttributeMapperTest.ATTRIBUTE_TO_MAP_FRIENDLY_NAME);
userFriendlyAttrMapperConfig.put(ProtocolMapperUtils.USER_ATTRIBUTE, ATTRIBUTE_TO_MAP_FRIENDLY_NAME);
userFriendlyAttrMapperConfig.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAME, "urn:oid:1.2.3.4.5.6.7");
userFriendlyAttrMapperConfig.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT, AttributeStatementHelper.BASIC);
userFriendlyAttrMapperConfig.put(AttributeStatementHelper.FRIENDLY_NAME, AbstractUserAttributeMapperTest.ATTRIBUTE_TO_MAP_FRIENDLY_NAME);
userFriendlyAttrMapperConfig.put(AttributeStatementHelper.FRIENDLY_NAME, ATTRIBUTE_TO_MAP_FRIENDLY_NAME);
client.setProtocolMappers(Arrays.asList(emailMapper, dottedAttrMapper, nestedAttrMapper, userAttrMapper, userFriendlyAttrMapper));
client.setProtocolMappers(Arrays.asList(emailMapper, dottedAttrMapper, nestedAttrMapper, userAttrMapper, userAttrMapper2, userFriendlyAttrMapper));
return client;
}

View file

@ -88,7 +88,7 @@ public final class KcSamlBrokerTest extends AbstractAdvancedBrokerTest {
attrMapper3.setIdentityProviderMapper(AttributeToRoleMapper.PROVIDER_ID);
attrMapper3.setConfig(ImmutableMap.<String,String>builder()
.put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString())
.put(UserAttributeMapper.ATTRIBUTE_FRIENDLY_NAME, AbstractUserAttributeMapperTest.ATTRIBUTE_TO_MAP_FRIENDLY_NAME)
.put(UserAttributeMapper.ATTRIBUTE_FRIENDLY_NAME, KcSamlBrokerConfiguration.ATTRIBUTE_TO_MAP_FRIENDLY_NAME)
.put(ATTRIBUTE_VALUE, ROLE_FRIENDLY_MANAGER)
.put("role", ROLE_FRIENDLY_MANAGER)
.build());
@ -216,7 +216,7 @@ public final class KcSamlBrokerTest extends AbstractAdvancedBrokerTest {
UserRepresentation urp = userResourceProv.toRepresentation();
urp.setAttributes(new HashMap<>());
urp.getAttributes().put(AbstractUserAttributeMapperTest.ATTRIBUTE_TO_MAP_FRIENDLY_NAME, Collections.singletonList(ROLE_FRIENDLY_MANAGER));
urp.getAttributes().put(KcSamlBrokerConfiguration.ATTRIBUTE_TO_MAP_FRIENDLY_NAME, Collections.singletonList(ROLE_FRIENDLY_MANAGER));
userResourceProv.update(urp);
userResourceProv.roles().realmLevel().add(Collections.singletonList(userRole));
userResourceProv.roles().realmLevel().add(Collections.singletonList(userRoleDotGuide));

View file

@ -1,14 +1,6 @@
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 com.google.common.collect.ImmutableMap;
import org.keycloak.admin.client.resource.IdentityProviderResource;
import org.keycloak.broker.oidc.mappers.AdvancedClaimToRoleMapper;
import org.keycloak.broker.provider.ConfigConstants;
@ -16,200 +8,26 @@ 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 hmlnarik,
* <a href="mailto:external.benjamin.weimer@bosch-si.com">Benjamin Weimer</a>,
* <a href="mailto:external.martin.idel@bosch.io">Martin Idel</a>
*/
public class OidcAdvancedClaimToRoleMapperTest extends AbstractRoleMapperTest {
private static final String CLAIMS = "[\n" +
" {\n" +
" \"key\": \"" + KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME + "\",\n" +
" \"value\": \"value 1\"\n" +
" },\n" +
" {\n" +
" \"key\": \"" + KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2 + "\",\n" +
" \"value\": \"value 2\"\n" +
" }\n" +
"]";
private static final String CLAIMS_REGEX = "[\n" +
" {\n" +
" \"key\": \"" + KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME + "\",\n" +
" \"value\": \"va.*\"\n" +
" },\n" +
" {\n" +
" \"key\": \"" + KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2 + "\",\n" +
" \"value\": \"value 2\"\n" +
" }\n" +
"]";
private String newValueForAttribute2 = "";
public class OidcAdvancedClaimToRoleMapperTest extends AbstractAdvancedRoleMapperTest {
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return new KcOidcBrokerConfiguration();
}
@Test
public void valueMatchesRegexTest() {
AdvancedClaimToRoleMapper advancedClaimToRoleMapper = new AdvancedClaimToRoleMapper();
assertThat(advancedClaimToRoleMapper.valueMatchesRegex("AB.*", "AB_ADMIN"), is(true));
assertThat(advancedClaimToRoleMapper.valueMatchesRegex("AB.*", "AA_ADMIN"), is(false));
assertThat(advancedClaimToRoleMapper.valueMatchesRegex("99.*", 999), is(true));
assertThat(advancedClaimToRoleMapper.valueMatchesRegex("98.*", 999), is(false));
assertThat(advancedClaimToRoleMapper.valueMatchesRegex("99\\..*", 99.9), is(true));
assertThat(advancedClaimToRoleMapper.valueMatchesRegex("AB.*", null), is(false));
assertThat(advancedClaimToRoleMapper.valueMatchesRegex("AB.*", Arrays.asList("AB_ADMIN", "AA_ADMIN")), is(true));
}
@Test
public void allClaimValuesMatch() {
createAdvancedClaimToRoleMapper(CLAIMS, false);
createUserInProviderRealm(ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add("value 2").build())
.build());
logInAsUserInIDPForFirstTime();
UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail());
assertThatRoleHasBeenAssignedInConsumerRealmTo(user);
}
@Test
public void claimValuesMismatch() {
createAdvancedClaimToRoleMapper(CLAIMS, false);
createUserInProviderRealm(ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add("value mismatch").build())
.build());
logInAsUserInIDPForFirstTime();
UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail());
assertThatRoleHasNotBeenAssignedInConsumerRealmTo(user);
}
@Test
public void claimValuesMatchIfNoClaimsSpecified() {
createAdvancedClaimToRoleMapper("[]", false);
createUserInProviderRealm(ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("some value").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add("some value").build())
.build());
logInAsUserInIDPForFirstTime();
UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail());
assertThatRoleHasBeenAssignedInConsumerRealmTo(user);
}
@Test
public void allClaimValuesMatchRegex() {
createAdvancedClaimToRoleMapper(CLAIMS_REGEX, true);
createUserInProviderRealm(ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add("value 2").build())
.build());
logInAsUserInIDPForFirstTime();
UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail());
assertThatRoleHasBeenAssignedInConsumerRealmTo(user);
}
@Test
public void claimValuesMismatchRegex() {
createAdvancedClaimToRoleMapper(CLAIMS_REGEX, true);
createUserInProviderRealm(ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("mismatch").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add("value 2").build())
.build());
logInAsUserInIDPForFirstTime();
UserRepresentation user = findUser(bc.consumerRealmName(), bc.getUserLogin(), bc.getUserEmail());
assertThatRoleHasNotBeenAssignedInConsumerRealmTo(user);
}
@Test
public void updateBrokeredUserMismatchDeletesRole() {
newValueForAttribute2 = "value mismatch";
UserRepresentation user = createMapperAndLoginAsUserTwiceWithMapper(FORCE, false);
assertThatRoleHasNotBeenAssignedInConsumerRealmTo(user);
}
@Test
public void updateBrokeredUserMismatchDoesNotDeleteRoleInImportMode() {
newValueForAttribute2 = "value mismatch";
UserRepresentation user = createMapperAndLoginAsUserTwiceWithMapper(IMPORT, false);
assertThatRoleHasBeenAssignedInConsumerRealmTo(user);
}
@Test
public void updateBrokeredUserMatchDoesntDeleteRole() {
newValueForAttribute2 = "value 2";
UserRepresentation user = createMapperAndLoginAsUserTwiceWithMapper(FORCE, false);
assertThatRoleHasBeenAssignedInConsumerRealmTo(user);
}
@Test
public void updateBrokeredUserAssignsRoleInForceModeWhenCreatingTheMapperAfterFirstLogin() {
newValueForAttribute2 = "value 2";
UserRepresentation user = createMapperAndLoginAsUserTwiceWithMapper(FORCE, true);
assertThatRoleHasBeenAssignedInConsumerRealmTo(user);
}
public UserRepresentation createMapperAndLoginAsUserTwiceWithMapper(IdentityProviderMapperSyncMode syncMode, boolean createAfterFirstLogin) {
return loginAsUserTwiceWithMapper(syncMode, createAfterFirstLogin, ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add("value 2").build())
.build());
}
@Override
protected void updateUser() {
UserRepresentation user = findUser(bc.providerRealmName(), bc.getUserLogin(), bc.getUserEmail());
ImmutableMap<String, List<String>> matchingAttributes = ImmutableMap.<String, List<String>>builder()
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME, ImmutableList.<String>builder().add("value 1").build())
.put(KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2, ImmutableList.<String>builder().add(newValueForAttribute2).build())
.put("some.other.attribute", ImmutableList.<String>builder().add("some value").build())
.build();
user.setAttributes(matchingAttributes);
adminClient.realm(bc.providerRealmName()).users().get(user.getId()).update(user);
}
@Override
protected void createMapperInIdp(IdentityProviderRepresentation idp, IdentityProviderMapperSyncMode syncMode) {
createAdvancedClaimToRoleMapperInIdp(idp, CLAIMS, false, syncMode);
}
private void createAdvancedClaimToRoleMapper(String claimsRepresentation, boolean areClaimValuesRegex) {
IdentityProviderRepresentation idp = setupIdentityProvider();
createAdvancedClaimToRoleMapperInIdp(idp, claimsRepresentation, areClaimValuesRegex, IMPORT);
}
protected void createAdvancedClaimToRoleMapperInIdp(IdentityProviderRepresentation idp , String claimsRepresentation, boolean areClaimValuesRegex, IdentityProviderMapperSyncMode syncMode) {
protected void createMapperInIdp(IdentityProviderRepresentation idp, String claimsOrAttributeRepresentation, boolean areClaimsOrAttributeValuesRegexes, IdentityProviderMapperSyncMode syncMode) {
IdentityProviderMapperRepresentation advancedClaimToRoleMapper = new IdentityProviderMapperRepresentation();
advancedClaimToRoleMapper.setName("advanced-claim-to-role-mapper");
advancedClaimToRoleMapper.setIdentityProviderMapper(AdvancedClaimToRoleMapper.PROVIDER_ID);
advancedClaimToRoleMapper.setConfig(ImmutableMap.<String, String>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(AdvancedClaimToRoleMapper.CLAIM_PROPERTY_NAME, claimsOrAttributeRepresentation)
.put(AdvancedClaimToRoleMapper.ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME, areClaimsOrAttributeValuesRegexes ? "true" : "false")
.put(ConfigConstants.ROLE, CLIENT_ROLE_MAPPER_REPRESENTATION)
.build());

View file

@ -8,6 +8,8 @@ import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import static org.keycloak.testsuite.broker.KcSamlBrokerConfiguration.ATTRIBUTE_TO_MAP_FRIENDLY_NAME;
public class SamlUserAttributeMapperTest extends AbstractUserAttributeMapperTest {
@Override