Add Attribute to Group Mapper for SAML IDP

Cleansing code as PR Comment

Add test for Advanced Attribute to Group Mapper

Closes #12950
This commit is contained in:
Denis Bernard 2023-01-18 01:11:23 +00:00 committed by Pedro Igor
parent 1a1ee78dbd
commit 5db64133b8
6 changed files with 327 additions and 2 deletions

View file

@ -0,0 +1,101 @@
/*
* Copyright 2023 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.jboss.logging.Logger;
import org.keycloak.broker.provider.AbstractIdentityProviderMapper;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.ConfigConstants;
import org.keycloak.models.GroupModel;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
/**
* Abstract class that handles the logic for importing and updating brokered users for all mappers that map a SAML
* attribute into a {@code Keycloak} group.
*
* @author <a href="mailto:denis.bernard@avanade.com">Denis Bernard</a>,
*/
public abstract class AbstractAttributeToGroupMapper extends AbstractIdentityProviderMapper {
private static final Logger LOG = Logger.getLogger(AbstractAttributeToGroupMapper.class);
@Override
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
GroupModel group = this.getGroup(realm, mapperModel);
if (group == null) {
return;
}
if (this.applies(mapperModel, context)) {
user.joinGroup(group);
}
}
@Override
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user, IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
GroupModel group = this.getGroup(realm, mapperModel);
if (group == null) {
return;
}
String groupId = group.getId();
if (!context.hasMapperAssignedGroup(groupId)) {
if (this.applies(mapperModel, context)) {
context.addMapperAssignedGroup(groupId);
user.joinGroup(group);
} else {
user.leaveGroup(group);
}
}
}
/**
* This method must be implemented by subclasses and they must return {@code true} if their mapping can be applied
* (i.e. user has the SAML attribute that should be mapped) or {@code false} otherwise.
*
* @param mapperModel a reference to the {@link IdentityProviderMapperModel}.
* @param context a reference to the {@link BrokeredIdentityContext}.
* @return {@code true} if the mapping can be applied or {@code false} otherwise.
*/
protected abstract boolean applies(final IdentityProviderMapperModel mapperModel, final BrokeredIdentityContext context);
/**
* Obtains the {@link GroupModel} corresponding the group configured in the specified
* {@link IdentityProviderMapperModel}.
* If the group doesn't correspond to one of the realm's client group or to one of the realm's group, this method
* returns {@code null}.
*
* @param realm a reference to the realm.
* @param mapperModel a reference to the {@link IdentityProviderMapperModel} containing the configured group.
* @return the {@link GroupModel} that corresponds to the mapper model group or {@code null}, if the group could not be found
*/
private GroupModel getGroup(final RealmModel realm, final IdentityProviderMapperModel mapperModel) {
String groupPath = mapperModel.getConfig().get(ConfigConstants.GROUP);
GroupModel group = KeycloakModelUtils.findGroupByPath(realm, groupPath);
if (group == null) {
LOG.warnf("Unable to find group by path '%s' referenced by mapper '%s' on realm '%s'.", groupPath, mapperModel.getName(), realm.getName());
}
return group;
}
}

View file

@ -0,0 +1,145 @@
/*
* Copyright 2023 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.BrokeredIdentityContext;
import org.keycloak.broker.provider.ConfigConstants;
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.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;
/**
* @author <a href="mailto:denis.bernard@avanade.com">Denis Bernard</a>
*/
public class AdvancedAttributeToGroupMapper extends AbstractAttributeToGroupMapper {
public static final String PROVIDER_ID = "saml-advanced-group-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 value of the attributes to search for in token. You can reference nested attributes using a '.', i.e. 'address.locality'. To use dot (.) literally, escape it with backslash (\\.)");
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 groupProperty = new ProviderConfigProperty();
groupProperty.setName(ConfigConstants.GROUP);
groupProperty.setLabel("Group");
groupProperty.setHelpText("Group to assign the user to if attribute is present.");
groupProperty.setType(ProviderConfigProperty.GROUP_TYPE);
configProperties.add(groupProperty);
}
@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 "Group Importer";
}
@Override
public String getDisplayType() {
return "Advanced Attribute to Group";
}
@Override
public String getHelpText() {
return "If all attributes exists, assign the user to the specified group.";
}
protected boolean applies(final IdentityProviderMapperModel mapperModel, final 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

@ -25,6 +25,7 @@ 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.AdvancedAttributeToGroupMapper
org.keycloak.broker.saml.mappers.AttributeToRoleMapper
org.keycloak.broker.saml.mappers.UserAttributeMapper
org.keycloak.broker.saml.mappers.XPathAttributeMapper

View file

@ -567,7 +567,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", "saml-advanced-role-idp-mapper", "saml-xpath-attribute-idp-mapper");
assertMapperTypes(mapperTypes, "saml-user-attribute-idp-mapper", "saml-role-idp-mapper", "saml-username-idp-mapper", "saml-advanced-role-idp-mapper", "saml-advanced-group-idp-mapper", "saml-xpath-attribute-idp-mapper");
}
private void assertMapperTypes(Map<String, IdentityProviderMapperTypeRepresentation> mapperTypes, String ... mapperIds) {

View file

@ -1,5 +1,5 @@
/*
* Copyright 2022 Red Hat, Inc. and/or its affiliates
* Copyright 2023 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");

View file

@ -0,0 +1,78 @@
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.CreatedResponseUtil;
import org.keycloak.admin.client.resource.IdentityProviderResource;
import org.keycloak.broker.provider.ConfigConstants;
import org.keycloak.broker.saml.mappers.AdvancedAttributeToGroupMapper;
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 javax.ws.rs.core.Response;
import java.util.List;
import static org.keycloak.testsuite.broker.KcSamlBrokerConfiguration.ATTRIBUTE_TO_MAP_FRIENDLY_NAME;
/**
* @author <a href="mailto:external.martin.idel@bosch.io">Martin Idel</a>,
* <a href="mailto:daniel.fesenmeyer@bosch.io">Daniel Fesenmeyer</a>
*/
public class KcSamlAdvancedAttributeToGroupMapperTest extends AbstractGroupBrokerMapperTest {
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 String createMapperInIdp(IdentityProviderRepresentation idp, String claimsOrAttributeRepresentation,
boolean areClaimsOrAttributeValuesRegexes, IdentityProviderMapperSyncMode syncMode, String groupValue) {
IdentityProviderMapperRepresentation advancedAttributeToGroupMapper = new IdentityProviderMapperRepresentation();
advancedAttributeToGroupMapper.setName("advanced-attribute-to-group-mapper");
advancedAttributeToGroupMapper.setIdentityProviderMapper(AdvancedAttributeToGroupMapper.PROVIDER_ID);
advancedAttributeToGroupMapper.setConfig(ImmutableMap.<String, String> builder()
.put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString())
.put(AdvancedAttributeToGroupMapper.ATTRIBUTE_PROPERTY_NAME, claimsOrAttributeRepresentation)
.put(AdvancedAttributeToGroupMapper.ARE_ATTRIBUTE_VALUES_REGEX_PROPERTY_NAME,
Boolean.valueOf(areClaimsOrAttributeValuesRegexes).toString())
.put(ConfigConstants.GROUP, MAPPER_TEST_GROUP_PATH)
.build());
IdentityProviderResource idpResource = realm.identityProviders().get(idp.getAlias());
advancedAttributeToGroupMapper.setIdentityProviderAlias(bc.getIDPAlias());
Response response = idpResource.addMapper(advancedAttributeToGroupMapper);
return CreatedResponseUtil.getCreatedId(response);
}
@Test
public void attributeFriendlyNameGetsConsideredAndMatchedToGroup() {
createAdvancedGroupMapper(ATTRIBUTES, false,KcOidcBrokerConfiguration.ATTRIBUTE_TO_MAP_NAME_2);
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());
assertThatUserHasBeenAssignedToGroup(user, MAPPER_TEST_GROUP_PATH);
}
}