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:
parent
1a1ee78dbd
commit
5db64133b8
6 changed files with 327 additions and 2 deletions
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue