From 60205845a892d2a29d3ce0e80c5a7f03217db0a4 Mon Sep 17 00:00:00 2001 From: Stefan Guilhen Date: Mon, 29 Jul 2019 15:10:55 -0300 Subject: [PATCH] [KEYCLOAK-7264] Add a RoleMappingsProvider SPI to allow for the configuration of custom role mappers in the SAML adapters. - Provides a default implementation based on mappings loaded from a properties file. - Role mappers can also be configured in the keycloak-saml susbsytem. --- .../subsystem/saml/as7/Constants.java | 8 + .../saml/as7/KeycloakSamlExtension.java | 7 +- .../saml/as7/KeycloakSubsystemParser.java | 31 +- .../saml/as7/ServiceProviderDefinition.java | 18 +- .../saml/as7/LocalDescriptions.properties | 3 + .../schema/wildfly-keycloak-saml_1_2.xsd | 334 +++++++++++ .../adapters/saml/DefaultSamlDeployment.java | 10 + .../saml/PropertiesBasedRoleMapper.java | 185 ++++++ .../adapters/saml/RoleMappingsProvider.java | 97 ++++ .../saml/RoleMappingsProviderUtils.java | 87 +++ .../adapters/saml/SamlDeployment.java | 7 + .../org/keycloak/adapters/saml/config/SP.java | 39 ++ .../config/parsers/DeploymentBuilder.java | 5 + .../parsers/KeycloakSamlAdapterV1QNames.java | 4 + .../parsers/RoleMappingsProviderParser.java | 64 +++ .../saml/config/parsers/SpParser.java | 4 + .../AbstractSamlAuthenticationHandler.java | 15 +- ...eycloak.adapters.saml.RoleMappingsProvider | 18 + .../schema/keycloak_saml_adapter_1_12.xsd | 525 ++++++++++++++++++ .../saml/PropertiesBasedRoleMapperTest.java | 62 +++ .../KeycloakSamlAdapterXMLParserTest.java | 26 +- ...cloak-saml-with-role-mappings-provider.xml | 57 ++ .../test/resources/role-mappings.properties | 23 + .../adapter/saml/extension/Constants.java | 8 + .../saml/extension/KeycloakSamlExtension.java | 7 +- .../extension/KeycloakSubsystemParser.java | 31 +- .../extension/ServiceProviderDefinition.java | 19 +- .../extension/LocalDescriptions.properties | 2 + .../schema/wildfly-keycloak-saml_1_2.xsd | 334 +++++++++++ .../keycloak-saml-adapter.xml | 2 +- .../extension/SubsystemParsingTestCase.java | 4 +- .../saml/extension/keycloak-saml-1.2.xml | 75 +++ .../adapter/servlet/SendUsernameServlet.java | 10 + .../page/EmployeeRoleMappingServlet.java | 97 ++++ .../testsuite/adapter/page/SAMLServlet.java | 25 + .../adapter/AbstractServletsAdapterTest.java | 6 + .../servlet/SAMLFilterServletAdapterTest.java | 20 + .../servlet/SAMLServletAdapterTest.java | 56 +- .../WEB-INF/keycloak-saml.xml | 47 ++ .../WEB-INF/role-mappings.properties | 23 + .../employee-role-mapping/WEB-INF/web.xml | 69 +++ .../adapter-test/keycloak-saml/testsaml.json | 65 ++- 42 files changed, 2505 insertions(+), 24 deletions(-) create mode 100755 adapters/saml/as7-eap6/subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_2.xsd create mode 100644 adapters/saml/core/src/main/java/org/keycloak/adapters/saml/PropertiesBasedRoleMapper.java create mode 100644 adapters/saml/core/src/main/java/org/keycloak/adapters/saml/RoleMappingsProvider.java create mode 100644 adapters/saml/core/src/main/java/org/keycloak/adapters/saml/RoleMappingsProviderUtils.java create mode 100644 adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/RoleMappingsProviderParser.java create mode 100644 adapters/saml/core/src/main/resources/META-INF/services/org.keycloak.adapters.saml.RoleMappingsProvider create mode 100644 adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_12.xsd create mode 100644 adapters/saml/core/src/test/java/org/keycloak/adapters/saml/PropertiesBasedRoleMapperTest.java create mode 100755 adapters/saml/core/src/test/resources/org/keycloak/adapters/saml/config/parsers/keycloak-saml-with-role-mappings-provider.xml create mode 100644 adapters/saml/core/src/test/resources/role-mappings.properties create mode 100755 adapters/saml/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_2.xsd create mode 100755 adapters/saml/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/saml/extension/keycloak-saml-1.2.xml create mode 100644 testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeRoleMappingServlet.java create mode 100755 testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-role-mapping/WEB-INF/keycloak-saml.xml create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-role-mapping/WEB-INF/role-mappings.properties create mode 100644 testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-role-mapping/WEB-INF/web.xml diff --git a/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/Constants.java b/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/Constants.java index aec147c0c0..21f2608694 100755 --- a/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/Constants.java +++ b/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/Constants.java @@ -68,6 +68,9 @@ public class Constants { static final String ALIAS = "alias"; static final String FILE = "file"; static final String SIGNATURES_REQUIRED = "signaturesRequired"; + + static final String ROLE_MAPPINGS_PROVIDER_ID = "roleMappingsProviderId"; + static final String ROLE_MAPPINGS_PROVIDER_CONFIG = "roleMappingsProviderConfig"; } static class XML { @@ -123,5 +126,10 @@ public class Constants { static final String FILE = "file"; static final String SIGNATURES_REQUIRED = "signaturesRequired"; static final String ASSERTION_CONSUMER_SERVICE_URL = "assertionConsumerServiceUrl"; + + static final String ID = "id"; + static final String VALUE = "value"; + static final String PROPERTY = "Property"; + static final String ROLE_MAPPINGS_PROVIDER = "RoleMappingsProvider"; } } diff --git a/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSamlExtension.java b/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSamlExtension.java index 3383587a08..e393af67ed 100755 --- a/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSamlExtension.java +++ b/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSamlExtension.java @@ -36,7 +36,9 @@ import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUB public class KeycloakSamlExtension implements Extension { public static final String SUBSYSTEM_NAME = "keycloak-saml"; - public static final String NAMESPACE = "urn:jboss:domain:keycloak-saml:1.1"; + public static final String NAMESPACE_1_1 = "urn:jboss:domain:keycloak-saml:1.1"; + public static final String NAMESPACE_1_2 = "urn:jboss:domain:keycloak-saml:1.2"; + public static final String CURRENT_NAMESPACE = NAMESPACE_1_2; private static final KeycloakSubsystemParser PARSER = new KeycloakSubsystemParser(); static final PathElement PATH_SUBSYSTEM = PathElement.pathElement(SUBSYSTEM, SUBSYSTEM_NAME); private static final String RESOURCE_NAME = KeycloakSamlExtension.class.getPackage().getName() + ".LocalDescriptions"; @@ -56,7 +58,8 @@ public class KeycloakSamlExtension implements Extension { */ @Override public void initializeParsers(final ExtensionParsingContext context) { - context.setSubsystemXmlMapping(SUBSYSTEM_NAME, KeycloakSamlExtension.NAMESPACE, PARSER); + context.setSubsystemXmlMapping(SUBSYSTEM_NAME, KeycloakSamlExtension.NAMESPACE_1_1, PARSER); + context.setSubsystemXmlMapping(SUBSYSTEM_NAME, KeycloakSamlExtension.NAMESPACE_1_2, PARSER); } /** diff --git a/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemParser.java b/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemParser.java index 42683fb99e..1560008967 100755 --- a/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemParser.java +++ b/adapters/saml/as7-eap6/subsystem/src/main/java/org/keycloak/subsystem/saml/as7/KeycloakSubsystemParser.java @@ -118,6 +118,8 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader
  • ATTRIBUTE_MAP = new HashMap<>(); diff --git a/adapters/saml/as7-eap6/subsystem/src/main/resources/org/keycloak/subsystem/saml/as7/LocalDescriptions.properties b/adapters/saml/as7-eap6/subsystem/src/main/resources/org/keycloak/subsystem/saml/as7/LocalDescriptions.properties index 0339a8abdd..a247ee7858 100755 --- a/adapters/saml/as7-eap6/subsystem/src/main/resources/org/keycloak/subsystem/saml/as7/LocalDescriptions.properties +++ b/adapters/saml/as7-eap6/subsystem/src/main/resources/org/keycloak/subsystem/saml/as7/LocalDescriptions.properties @@ -39,6 +39,9 @@ keycloak-saml.SP.PrincipalNameMapping-policy=Principal name mapping policy keycloak-saml.SP.PrincipalNameMapping-attribute-name=Principal name mapping attribute name keycloak-saml.SP.Key=A key definition keycloak-saml.SP.IDP=Identity provider definition +keycloak-saml.SP.roleMappingsProviderId=The string that identifies the role mappings provider to be used within the SP +keycloak-saml.SP.roleMappingsProviderConfig=The configuration properties of the role mappings provider + keycloak-saml.Key=A key configuration for service provider or identity provider keycloak-saml.Key.add=Add a key definition diff --git a/adapters/saml/as7-eap6/subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_2.xsd b/adapters/saml/as7-eap6/subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_2.xsd new file mode 100755 index 0000000000..5eca1ac311 --- /dev/null +++ b/adapters/saml/as7-eap6/subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_2.xsd @@ -0,0 +1,334 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + The name of the realm. + + + + + + + + + + + + + + + The entity ID for SAML service provider + + + + + The ssl policy + + + + + Name ID policy format URN + + + + + URI to a logout page + + + + + Redirected unauthenticated request to a login page + + + + + If user isn't logged in just return with an error. Used to check if a user is already logged in or not + + + + + The session id is changed by default on a successful login. Change this to true if you want to turn this off + + + + + + + + + + + + The entity ID for SAML service provider + + + + + Require signatures for single-sign-on and single-logout + + + + + Algorithm used for signatures + + + + + Canonicalization method used for signatures + + + + + + + Sign the SSO requests + + + + + Validate the SSO response signature + + + + + Validate the SSO assertion signature + + + + + HTTP method to use for requests + + + + + HTTP method to use for response + + + + + SSO endpoint URL + + + + + Endpoint of Assertion Consumer Service at SP + + + + + + + Validate a single-logout request signature + + + + + Validate a single-logout response signature + + + + + Sign single-logout requests + + + + + Sign single-logout responses + + + + + HTTP method to use for request + + + + + HTTP method to use for response + + + + + Endpoint URL for posting + + + + + Endpoint URL for redirects + + + + + + + + + + + + + + + + + + Key can be used for signing + + + + + Key can be used for encryption + + + + + + + + + + + Key store filesystem path + + + + + Key store resource URI + + + + + Key store password + + + + + Key store format + + + + + Key alias + + + + + + + + Private key alias + + + + + Private key password + + + + + + + + Certificate alias + + + + + + + + Principal name mapping policy. Possible values: FROM_NAME_ID + + + + + Name of the attribute to use for principal name mapping + + + + + + + + + + + + + + Role attribute + + + + + + + + + Specifies a configuration property for the provider. + + + + + + The id of the role mappings provider that is to be used. Example: properties-based-provider. + + + + + + + + The name (key) of the configuration property. + + + + + The value of the configuration property. + + + + diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java index 93097749a9..85aa952564 100755 --- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/DefaultSamlDeployment.java @@ -308,6 +308,7 @@ public class DefaultSamlDeployment implements SamlDeployment { private PrivateKey decryptionKey; private KeyPair signingKeyPair; private Set roleAttributeNames; + private RoleMappingsProvider roleMappingsProvider; private PrincipalNamePolicy principalNamePolicy = PrincipalNamePolicy.FROM_NAME_ID; private String principalAttributeName; private String logoutPage; @@ -375,6 +376,11 @@ public class DefaultSamlDeployment implements SamlDeployment { return roleAttributeNames; } + @Override + public RoleMappingsProvider getRoleMappingsProvider() { + return this.roleMappingsProvider; + } + @Override public PrincipalNamePolicy getPrincipalNamePolicy() { return principalNamePolicy; @@ -425,6 +431,10 @@ public class DefaultSamlDeployment implements SamlDeployment { this.roleAttributeNames = roleAttributeNames; } + public void setRoleMappingsProvider(final RoleMappingsProvider provider) { + this.roleMappingsProvider = provider; + } + public void setPrincipalNamePolicy(PrincipalNamePolicy principalNamePolicy) { this.principalNamePolicy = principalNamePolicy; } diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/PropertiesBasedRoleMapper.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/PropertiesBasedRoleMapper.java new file mode 100644 index 0000000000..1c2d841a12 --- /dev/null +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/PropertiesBasedRoleMapper.java @@ -0,0 +1,185 @@ +/* + * 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.adapters.saml; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.util.Collections; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; + +import org.jboss.logging.Logger; +import org.keycloak.adapters.saml.config.parsers.ResourceLoader; + +/** + * A {@link RoleMappingsProvider} implementation that uses a {@code properties} file to determine the mappings that should be applied + * to the SAML principal and roles. It is always identified by the id {@code properties-based-role-mapper} in {@code keycloak-saml.xml}. + *

    + * This provider relies on two configuration properties that can be used to specify the location of the {@code properties} file + * that will be used. First, it checks if the {@code properties.file.location} property has been specified, using the configured + * value to locate the {@code properties} file in the filesystem. If the configured file is not located, the provider throws a + * {@link RuntimeException}. The following snippet shows an example of provider using the {@code properties.file.configuration} + * option to load the {@code roles.properties} file from the {@code /opt/mappers/} directory in the filesystem: + * + *

    + *     
    + *         
    + *     
    + * 
    + * + * If the {@code properties.file.location} configuration property is not present, the provider checks the {@code properties.resource.location} + * property, using the configured value to load the {@code properties} file from the WAR resource. If no value is found, it + * finally attempts to load a file named {@code role-mappings.properties} from the {@code WEB-INF} directory of the application. + * Failure to load the file from the resource will result in the provider throwing a {@link RuntimeException}. The following + * snippet shows an example of provider using the {@code properties.resource.location} to load the {@code roles.properties} + * file from the application's {@code /WEB-INF/conf/} directory: + * + *
    + *     
    + *         
    + *     
    + * 
    + * + * The {@code properties} file can contain both roles and principals as keys, and a list of zero or more roles separated by comma + * as values. When the {@code {@link #map(String, Set)}} method is called, the implementation iterates through the set of roles + * that were extracted from the assertion and checks, for eache role, if a mapping exists. If the role maps to an empty role, + * it is discarded. If it maps to a set of one ore more different roles, then these roles are set in the result set. If no + * mapping is found for the role then it is included as is in the result set. + * + * Once the roles have been processed, the implementation checks if the principal extracted from the assertion contains an entry + * in the {@code properties} file. If a mapping for the principal exists, any roles listed as value are added to the result set. This + * allows the assignment of extra roles to a principal. + * + * For example, consider the following {@code properties} file: + * + *
    + *     # role to roles mappings
    + *     samlRoleA=jeeRoleX,jeeRoleY
    + *     samlRoleB=
    + *
    + *     # principal to roles mappings
    + *     kc-user=jeeRoleZ
    + * 
    + * + * If the {@code {@link #map(String, Set)}} method is called with {@code kc-user} as principal and a set containing roles + * {@code samlRoleA,samlRoleB,samlRoleC}, the result set will be formed by the roles {@code jeeRoleX,jeeRoleY,samlRoleC,jeeRoleZ}. + * In this case, {@code samlRoleA} is mapped to two roles ({@code jeeRoleX,jeeRoleY}), {@code samlRoleB} is discarded as it is + * mapped to an empty role, {@code samlRoleC} is used as is and the principal is also assigned {@code jeeRoleZ}. + * + * @author Stefan Guilhen + */ +public class PropertiesBasedRoleMapper implements RoleMappingsProvider { + + private static final Logger logger = Logger.getLogger(PropertiesBasedRoleMapper.class); + + public static final String PROVIDER_ID = "properties-based-role-mapper"; + + private static final String PROPERTIES_FILE_LOCATION = "properties.file.location"; + + private static final String PROPERTIES_RESOURCE_LOCATION = "properties.resource.location"; + + private static final String DEFAULT_RESOURCE_LOCATION = "/WEB-INF/role-mappings.properties"; + + private Properties roleMappings; + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public void init(final SamlDeployment deployment, final ResourceLoader loader, final Properties config) { + + this.roleMappings = new Properties(); + // try to load the properties from the filesystem first. + String path = config.getProperty(PROPERTIES_FILE_LOCATION); + if (path != null) { + File file = new File(path); + if (file.exists()) { + try { + this.roleMappings.load(new FileInputStream(file)); + logger.debugf("Successfully loaded role mappings from %s", path); + } catch (Exception e) { + logger.debugv(e, "Unable to load role mappings from %s", path); + } + } else { + throw new RuntimeException("Unable to load role mappings from " + path + ": file does not exist in filesystem"); + } + } else { + // try to load the properties from the resource (WAR). + path = config.getProperty(PROPERTIES_RESOURCE_LOCATION, DEFAULT_RESOURCE_LOCATION); + InputStream is = loader.getResourceAsStream(path); + if (is != null) { + try { + this.roleMappings.load(is); + logger.debugf("Resource loader successfully loaded role mappings from %s", path); + } catch (Exception e) { + logger.debugv(e, "Resource loader unable to load role mappings from %s", path); + } + } else { + throw new RuntimeException("Unable to load role mappings from " + path + ": file does not exist in the resource"); + } + } + } + + @Override + public Set map(final String principalName, final Set roles) { + if (this.roleMappings == null || this.roleMappings.isEmpty()) + return roles; + + Set resolvedRoles = new HashSet<>(); + // first check if we have role -> role(s) mappings. + for (String role : roles) { + if (this.roleMappings.containsKey(role)) { + // role that was mapped to empty string is not considered (it is discarded from the set of specified roles). + this.extractRolesIntoSet(role, resolvedRoles); + } else { + // no mapping found for role - add it as is. + resolvedRoles.add(role); + } + } + + // now check if we have a principal -> role(s) mapping with additional roles to be added. + if (this.roleMappings.containsKey(principalName)) { + this.extractRolesIntoSet(principalName, resolvedRoles); + } + return resolvedRoles; + } + + /** + * Obtains the list of comma separated roles associated with the specified entry, trims any whitespaces from said roles + * and adds them to the specified set. + * + * @param entry the entry in the properties file. + * @param roles the {@link Set} into which the extracted roles are to be added. + */ + private void extractRolesIntoSet(final String entry, final Set roles) { + String value = this.roleMappings.getProperty(entry); + if (!value.isEmpty()) { + String[] mappedRoles = value.split(","); + for (String mappedRole : mappedRoles) { + String trimmedRole = mappedRole.trim(); + if (!trimmedRole.isEmpty()) { + roles.add(trimmedRole); + } + } + } + } +} diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/RoleMappingsProvider.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/RoleMappingsProvider.java new file mode 100644 index 0000000000..b781eda9b7 --- /dev/null +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/RoleMappingsProvider.java @@ -0,0 +1,97 @@ +/* + * 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.adapters.saml; + +import java.util.Properties; +import java.util.Set; + +import org.keycloak.adapters.saml.config.parsers.ResourceLoader; + +/** + * A simple SPI for mapping SAML roles into roles that exist in the SP application environment. The roles returned by an external + * IDP might not always correspond to the roles that were defined for the application so there is a need for a mechanism that + * allows mapping the SAML roles into different roles. It is used by the SAML adapter after it extracts the roles from the SAML + * assertion to set up the container's security context. + *

    + * This SPI doesn't impose any restrictions on the mappings that can be performed. Implementations can not only map roles into + * other roles but also add or remove roles (and thus augmenting/reducing the number of roles assigned to the SAML principal) + * depending on the use case. + *

    + * To install a custom role mappings provider, a {@code META-INF/services/org.keycloak.adapters.saml.RoleMappingsProvider} file + * containing the FQN of the custom implementation class must be added to the WAR that contains the provider implementation + * class (or the JAR that is attached to the {@code WEB-INF/lib} or as a {@code jboss module} if one wants to share the + * implementation among more WARs). + *

    + * The role mappings provider implementation that will be selected for the SP application is identified in the {@code keycloak-saml.xml} + * by its id. The provider declaration can also contain one or more configuration properties that will be passed to the implementation + * in the {@code {@link #init(SamlDeployment, ResourceLoader, Properties)}} method. For example, if an LDAP-based implementation + * with id {@code ldap-based-role-mapper} is made available via {@code META-INF/services}, it can be selected in {@code keycloak-saml.xml} + * as follows: + * + *

    + *     ...
    + *     
    + *         ...
    + *     
    + *     
    + *         
    + *         
    + *         ...
    + *     
    + * 
    + * + * NOTE: The SPI is not yet finished and method signatures are still subject to change in future versions. + * + * @author Stefan Guilhen + */ +public interface RoleMappingsProvider { + + /** + * Obtains the provider's identifier. This id is specified in {@code keycloak-saml.xml} to identify the provider implementation + * to be used. + * + * @return a {@link String} representing the provider's id. + */ + String getId(); + + /** + * Initializes the provider. This method is called by the adapter in deployment time after the contents of {@code keycloak-saml.xml} + * have been parsed and a provider whose id matches the one in the descriptor is successfully loaded. + * + * @param deployment a reference to the constructed {@link SamlDeployment}. + * @param loader a reference to a {@link ResourceLoader} that can be used to load additional resources from the WAR. + * @param config a {@link Properties} object containing the provider config as read from {@code keycloak-saml.xml} + */ + void init(final SamlDeployment deployment, final ResourceLoader loader, final Properties config); + + /** + * Produces the final set of roles that should be assigned to the specified principal. This method makes the principal + * and roles that were read from the SAML assertion available to implementations so they can apply their specific logic + * to produce the final set of roles for the principal. + * + * This method imposes no restrictions on the kind of mappings that can be performed. A simple implementation may, for + * example, just use a properties file to map some of the assertion roles into JEE roles while a more complex implementation + * may also connect to external databases or LDAP servers to retrieve extra roles and add those roles to the set of + * roles already extracted from the assertion. + * + * @param principalName the principal name as extracted from the SAML assertion. + * @param roles the set of roles extracted from the SAML assertion. + * @return a {@link Set} containing the final set of roles that are to be assigned to the principal. + */ + Set map(final String principalName, final Set roles); +} diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/RoleMappingsProviderUtils.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/RoleMappingsProviderUtils.java new file mode 100644 index 0000000000..cef1022724 --- /dev/null +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/RoleMappingsProviderUtils.java @@ -0,0 +1,87 @@ +/* + * 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.adapters.saml; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.ServiceLoader; + +import org.jboss.logging.Logger; +import org.keycloak.adapters.saml.config.SP; +import org.keycloak.adapters.saml.config.parsers.ResourceLoader; + +/** + * Utility class that allows for the instantiation and configuration of role mappings providers. + * + * @author Stefan Guilhen + */ +public class RoleMappingsProviderUtils { + + private static final Logger logger = Logger.getLogger(RoleMappingsProviderUtils.class); + + /** + * Loads the available implementations of {@link RoleMappingsProvider} and selects the provider that matches the id + * that was configured in {@code keycloak-saml.xml}. The selected provider is then initialized with the specified + * {@link SamlDeployment}, {@link ResourceLoader} and configuration as specified in {@code keycloak-saml.xml}. If no + * provider was configured for the SP then {@code null} is returned. + * + * @param deployment a reference to the {@link SamlDeployment} that is being built. + * @param loader a reference to the {@link ResourceLoader} that allows the provider implementation to load additional + * resources from the SP application WAR. + * @param providerConfig the provider configuration properties as configured in {@code keycloak-saml.xml}. Can contain + * an empty properties object if no configuration properties were specified for the provider. + * @return the instantiated and initialized {@link RoleMappingsProvider} or {@code null} if no provider was configured + * for the SP. + */ + public static RoleMappingsProvider bootstrapRoleMappingsProvider(final SamlDeployment deployment, final ResourceLoader loader, final SP.RoleMappingsProviderConfig providerConfig) { + String providerId; + if (providerConfig == null || providerConfig.getId() == null) { + return null; + } else { + providerId = providerConfig.getId(); + } + + // load the available role mappings providers and check if one corresponds to the specified id. + Map roleMappingsProviders = new HashMap<>(); + loadProviders(roleMappingsProviders, RoleMappingsProviderUtils.class.getClassLoader()); + loadProviders(roleMappingsProviders, Thread.currentThread().getContextClassLoader()); + + RoleMappingsProvider provider = roleMappingsProviders.get(providerId); + if (provider == null) { + throw new RuntimeException("Couldn't find RoleMappingsProvider implementation class with id: " + providerId + + ". Loaded role mappings providers: " + roleMappingsProviders.keySet()); + } + + provider.init(deployment, loader, providerConfig != null ? providerConfig.getConfiguration() : new Properties()); + return provider; + } + + /** + * Loads the {@code RoleMappingsProvider} implementations using the specified {@code ClassLoader}. + * + * @param providers the {@code Map} used to store the loaded providers by id. + * @param classLoader the {@code ClassLoader} that is to be used to load to provider implementations. + */ + private static void loadProviders(Map providers, ClassLoader classLoader) { + for (RoleMappingsProvider provider : ServiceLoader.load(RoleMappingsProvider.class, classLoader)) { + logger.debugf("Loaded RoleMappingsProvider %s", provider.getId()); + providers.put(provider.getId(), provider); + } + } +} diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java index f59c42d6ae..492e92a681 100755 --- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/SamlDeployment.java @@ -168,6 +168,13 @@ public interface SamlDeployment { Set getRoleAttributeNames(); + /** + * Obtains the {@link RoleMappingsProvider} that was configured for the SP. + * + * @return a reference to the configured {@link RoleMappingsProvider}. + */ + RoleMappingsProvider getRoleMappingsProvider(); + enum PrincipalNamePolicy { FROM_NAME_ID, FROM_ATTRIBUTE diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/SP.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/SP.java index 9b1ba35417..1e3347ea8e 100755 --- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/SP.java +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/SP.java @@ -19,6 +19,7 @@ package org.keycloak.adapters.saml.config; import java.io.Serializable; import java.util.List; +import java.util.Properties; import java.util.Set; /** @@ -47,6 +48,35 @@ public class SP implements Serializable { } } + /** + * Holds the configuration of the {@code RoleMappingsProvider}. Contains the provider's id and a {@link Properties} + * object that holds the provider's configuration options. + */ + public static class RoleMappingsProviderConfig implements Serializable { + private String id; + private Properties configuration; + + public String getId() { + return this.id; + } + + public void setId(final String id) { + this.id = id; + } + + public Properties getConfiguration() { + return this.configuration; + } + + public void setConfiguration(final Properties configuration) { + this.configuration = configuration; + } + + public void addConfigurationProperty(final String name, final String value) { + this.configuration.setProperty(name, value); + } + } + private String entityID; private String sslPolicy; private boolean forceAuthentication; @@ -57,6 +87,7 @@ public class SP implements Serializable { private String nameIDPolicyFormat; private PrincipalNameMapping principalNameMapping; private Set roleAttributes; + private RoleMappingsProviderConfig roleMappingsProviderConfig; private IDP idp; private boolean autodetectBearerOnly; @@ -132,6 +163,14 @@ public class SP implements Serializable { this.roleAttributes = roleAttributes; } + public RoleMappingsProviderConfig getRoleMappingsProviderConfig() { + return this.roleMappingsProviderConfig; + } + + public void setRoleMappingsProviderConfig(final RoleMappingsProviderConfig provider) { + this.roleMappingsProviderConfig = provider; + } + public IDP getIdp() { return idp; } diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java index f02d88e6c6..91a6d34e37 100755 --- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/DeploymentBuilder.java @@ -19,6 +19,7 @@ package org.keycloak.adapters.saml.config.parsers; import org.jboss.logging.Logger; import org.keycloak.adapters.saml.DefaultSamlDeployment; +import org.keycloak.adapters.saml.RoleMappingsProviderUtils; import org.keycloak.adapters.saml.SamlDeployment; import org.keycloak.adapters.saml.config.IDP; import org.keycloak.adapters.saml.config.Key; @@ -212,6 +213,10 @@ public class DeploymentBuilder { defaultIDP.setClient(new HttpClientBuilder().build(idp.getHttpClientConfig())); defaultIDP.refreshKeyLocatorConfiguration(); + // set the role mappings provider. + deployment.setRoleMappingsProvider(RoleMappingsProviderUtils.bootstrapRoleMappingsProvider(deployment, resourceLoader, + sp.getRoleMappingsProviderConfig())); + return deployment; } diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/KeycloakSamlAdapterV1QNames.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/KeycloakSamlAdapterV1QNames.java index e0f8288b7d..478a9f2fe6 100644 --- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/KeycloakSamlAdapterV1QNames.java +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/KeycloakSamlAdapterV1QNames.java @@ -38,8 +38,10 @@ public enum KeycloakSamlAdapterV1QNames implements HasQName { PRINCIPAL_NAME_MAPPING("PrincipalNameMapping"), PRIVATE_KEY("PrivateKey"), PRIVATE_KEY_PEM("PrivateKeyPem"), + PROPERTY("Property"), PUBLIC_KEY_PEM("PublicKeyPem"), ROLE_IDENTIFIERS("RoleIdentifiers"), + ROLE_MAPPINGS_PROVIDER("RoleMappingsProvider"), SINGLE_LOGOUT_SERVICE("SingleLogoutService"), SINGLE_SIGN_ON_SERVICE("SingleSignOnService"), SP("SP"), @@ -58,6 +60,7 @@ public enum KeycloakSamlAdapterV1QNames implements HasQName { ATTR_ENTITY_ID(null, "entityID"), ATTR_FILE(null, "file"), ATTR_FORCE_AUTHENTICATION(null, "forceAuthentication"), + ATTR_ID(null, "id"), ATTR_IS_PASSIVE(null, "isPassive"), ATTR_LOGOUT_PAGE(null, "logoutPage"), ATTR_METADATA_URL(null, "metadataUrl"), @@ -86,6 +89,7 @@ public enum KeycloakSamlAdapterV1QNames implements HasQName { ATTR_VALIDATE_ASSERTION_SIGNATURE(null, "validateAssertionSignature"), ATTR_VALIDATE_REQUEST_SIGNATURE(null, "validateRequestSignature"), ATTR_VALIDATE_RESPONSE_SIGNATURE(null, "validateResponseSignature"), + ATTR_VALUE(null, "value"), UNKNOWN_ELEMENT("") ; diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/RoleMappingsProviderParser.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/RoleMappingsProviderParser.java new file mode 100644 index 0000000000..1e1baeb2da --- /dev/null +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/RoleMappingsProviderParser.java @@ -0,0 +1,64 @@ +/* + * 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.adapters.saml.config.parsers; + +import java.util.Properties; + +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.events.StartElement; + +import org.keycloak.adapters.saml.config.SP; +import org.keycloak.saml.common.exceptions.ParsingException; +import org.keycloak.saml.common.util.StaxParserUtil; + +/** + * A parser for the {@code } element., represented by the role-mappings-provider-type in the schema. + * + * @author Stefan Guilhen + */ +public class RoleMappingsProviderParser extends AbstractKeycloakSamlAdapterV1Parser { + + private static final RoleMappingsProviderParser INSTANCE = new RoleMappingsProviderParser(); + + private RoleMappingsProviderParser() { + super(KeycloakSamlAdapterV1QNames.ROLE_MAPPINGS_PROVIDER); + } + + public static RoleMappingsProviderParser getInstance() { + return INSTANCE; + } + + @Override + protected SP.RoleMappingsProviderConfig instantiateElement(XMLEventReader xmlEventReader, StartElement element) throws ParsingException { + SP.RoleMappingsProviderConfig providerConfig = new SP.RoleMappingsProviderConfig(); + providerConfig.setId(StaxParserUtil.getRequiredAttributeValueRP(element, KeycloakSamlAdapterV1QNames.ATTR_ID)); + providerConfig.setConfiguration(new Properties()); + return providerConfig; + } + + @Override + protected void processSubElement(XMLEventReader xmlEventReader, SP.RoleMappingsProviderConfig target, KeycloakSamlAdapterV1QNames element, StartElement elementDetail) throws ParsingException { + switch(element) { + case PROPERTY: + final String name = StaxParserUtil.getRequiredAttributeValueRP(elementDetail, KeycloakSamlAdapterV1QNames.ATTR_NAME); + final String value = StaxParserUtil.getRequiredAttributeValueRP(elementDetail, KeycloakSamlAdapterV1QNames.ATTR_VALUE); + target.addConfigurationProperty(name, value); + break; + } + } +} diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SpParser.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SpParser.java index eac4db8b50..29c6252c8e 100644 --- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SpParser.java +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/config/parsers/SpParser.java @@ -72,6 +72,10 @@ public class SpParser extends AbstractKeycloakSamlAdapterV1Parser { target.setRoleAttributes(RoleMappingParser.getInstance().parse(xmlEventReader)); break; + case ROLE_MAPPINGS_PROVIDER: + target.setRoleMappingsProviderConfig(RoleMappingsProviderParser.getInstance().parse(xmlEventReader)); + break; + case IDP: target.setIdp(IdpParser.getInstance().parse(xmlEventReader)); break; diff --git a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java index fa52cf91d3..0195d869cf 100644 --- a/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java +++ b/adapters/saml/core/src/main/java/org/keycloak/adapters/saml/profile/AbstractSamlAuthenticationHandler.java @@ -423,7 +423,7 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic NameIDType subjectNameID = subType == null ? null : (NameIDType) subType.getBaseID(); String principalName = subjectNameID == null ? null : subjectNameID.getValue(); - final Set roles = new HashSet<>(); + Set roles = new HashSet<>(); MultivaluedHashMap attributes = new MultivaluedHashMap<>(); MultivaluedHashMap friendlyAttributes = new MultivaluedHashMap<>(); @@ -462,10 +462,6 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic } } - // roles should also be there as regular attributes - // this mainly required for elytron and its ABAC nature - attributes.put(DEFAULT_ROLE_ATTRIBUTE_NAME, new ArrayList<>(roles)); - if (deployment.getPrincipalNamePolicy() == SamlDeployment.PrincipalNamePolicy.FROM_ATTRIBUTE) { if (deployment.getPrincipalAttributeName() != null) { String attribute = attributes.getFirst(deployment.getPrincipalAttributeName()); @@ -477,6 +473,15 @@ public abstract class AbstractSamlAuthenticationHandler implements SamlAuthentic } } + // use the configured role mappings provider to map roles if necessary. + if (deployment.getRoleMappingsProvider() != null) { + roles = deployment.getRoleMappingsProvider().map(principalName, roles); + } + + // roles should also be there as regular attributes + // this mainly required for elytron and its ABAC nature + attributes.put(DEFAULT_ROLE_ATTRIBUTE_NAME, new ArrayList<>(roles)); + AuthnStatementType authn = null; for (Object statement : assertion.getStatements()) { if (statement instanceof AuthnStatementType) { diff --git a/adapters/saml/core/src/main/resources/META-INF/services/org.keycloak.adapters.saml.RoleMappingsProvider b/adapters/saml/core/src/main/resources/META-INF/services/org.keycloak.adapters.saml.RoleMappingsProvider new file mode 100644 index 0000000000..ba1f7f850f --- /dev/null +++ b/adapters/saml/core/src/main/resources/META-INF/services/org.keycloak.adapters.saml.RoleMappingsProvider @@ -0,0 +1,18 @@ +# +# 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. +# + +org.keycloak.adapters.saml.PropertiesBasedRoleMapper \ No newline at end of file diff --git a/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_12.xsd b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_12.xsd new file mode 100644 index 0000000000..4b7b9573fd --- /dev/null +++ b/adapters/saml/core/src/main/resources/schema/keycloak_saml_adapter_1_12.xsd @@ -0,0 +1,525 @@ + + + + + + + + + + Keycloak SAML Adapter configuration file. + + + + + Describes SAML service provider configuration. + + + + + + + + + + + List of service provider encryption and validation keys. + + If the IDP requires that the client application (SP) sign all of its requests and/or if the IDP will encrypt assertions, you must define the keys used to do this. For client signed documents you must define both the private and public key or certificate that will be used to sign documents. For encryption, you only have to define the private key that will be used to decrypt. + + + + + When creating a Java Principal object that you obtain from methods like HttpServletRequest.getUserPrincipal(), you can define what name that is returned by the Principal.getName() method. + + + + + Defines what SAML attributes within the assertion received from the user should be used as role identifiers within the Java EE Security Context for the user. + By default Role attribute values are converted to Java EE roles. Some IDPs send roles via a member or memberOf attribute assertion. You can define one or more Attribute elements to specify which SAML attributes must be converted into roles. + + + + + Specifies the role mappings provider implementation that will be used to map the roles extracted from the SAML assertion into the final set of roles + that will be assigned to the principal. A provider is typically used to map roles retrieved from third party IDPs into roles that exist in the JEE application environment. It can also + assign extra roles to the assertion principal (for example, by connecting to an LDAP server to obtain more roles) or remove some of the roles that were set by the IDP. + + + + + Describes configuration of SAML identity provider for this service provider. + + + + + + This is the identifier for this client. The IDP needs this value to determine who the client is that is communicating with it. + + + + + SSL policy the adapter will enforce. + + + + + SAML clients can request a specific NameID Subject format. Fill in this value if you want a specific format. It must be a standard SAML format identifier, i.e. urn:oasis:names:tc:SAML:2.0:nameid-format:transient. By default, no special format is requested. + + + + + URL of the logout page. + + + + + SAML clients can request that a user is re-authenticated even if they are already logged in at the IDP. Default value is false. + + + + + SAML clients can request that a user is never asked to authenticate even if they are not logged in at the IDP. Set this to true if you want this. Do not use together with forceAuthentication as they are opposite. Default value is false. + + + + + The session id is changed by default on a successful login on some platforms to plug a security attack vector. Change this to true to disable this. It is recommended you do not turn it off. Default value is false. + + + + + This should be set to true if your application serves both a web application and web services (e.g. SOAP or REST). It allows you to redirect unauthenticated users of the web application to the Keycloak login page, but send an HTTP 401 status code to unauthenticated SOAP or REST clients instead as they would not understand a redirect to the login page. Keycloak auto-detects SOAP or REST clients based on typical headers like X-Requested-With, SOAPAction or Accept. The default value is false. + + + + + + + + + Describes a single key used for signing or encryption. + + + + + + + + + Java keystore to load keys and certificates from. + + + + + Private key (PEM format) + + + + + Public key (PEM format) + + + + + Certificate key (PEM format) + + + + + + Flag defining whether the key should be used for signing. + + + + + Flag defining whether the key should be used for encryption + + + + + + + + Private key declaration + + + + + Certificate declaration + + + + + + File path to the key store. + + + + + WAR resource path to the key store. This is a path used in method call to ServletContext.getResourceAsStream(). + + + + + The password of the key store. + + + + + + + Alias that points to the key or cert within the keystore. + + + + + Keystores require an additional password to access private keys. In the PrivateKey element you must define this password within a password attribute. + + + + + + + Alias that points to the key or cert within the keystore. + + + + + + + Policy used to populate value of Java Principal object obtained from methods like HttpServletRequest.getUserPrincipal(). + + + + + Name of the SAML assertion attribute to use within. + + + + + + + + This policy just uses whatever the SAML subject value is. This is the default setting + + + + + This will pull the value from one of the attributes declared in the SAML assertion received from the server. You'll need to specify the name of the SAML assertion attribute to use within the attribute XML attribute. + + + + + + + + + All requests must come in via HTTPS. + + + + + Only non-private IP addresses must come over the wire via HTTPS. + + + + + no requests are required to come over via HTTPS. + + + + + + + + + + + + + + + + + + + + + + + Specifies SAML attribute to be converted into roles. + + + + + + + + Specifies name of the SAML attribute to be converted into roles. + + + + + + + + Specifies a configuration property for the provider. + + + + + + The id of the role mappings provider that is to be used. Example: properties-based-provider. + + + + + + + The name (key) of the configuration property. + + + + + The value of the configuration property. + + + + + + + + Configuration of the login SAML endpoint of the IDP. + + + + + Configuration of the logout SAML endpoint of the IDP + + + + + The Keys sub element of IDP is only used to define the certificate or public key to use to verify documents signed by the IDP. + + + + + Configuration of HTTP client used for automatic obtaining of certificates containing public keys for IDP signature verification via SAML descriptor of the IDP. + + + + + This defines the allowed clock skew between IDP and SP in milliseconds. The default value is 0. + + + + + + issuer ID of the IDP. + + + + + If set to true, the client adapter will sign every document it sends to the IDP. Also, the client will expect that the IDP will be signing any documents sent to it. This switch sets the default for all request and response types. + + + + + Signature algorithm that the IDP expects signed documents to use. Defaults to RSA_SHA256 + + + + + This is the signature canonicalization method that the IDP expects signed documents to use. The default value is https://www.w3.org/2001/10/xml-exc-c14n# and should be good for most IDPs. + + + + + + + + + + The URL used to retrieve the IDP metadata, currently this is only used to pick up signing and encryption keys periodically which allow cycling of these keys on the IDP without manual changes on the SP side. + + + + + + + Should the client sign authn requests? Defaults to whatever the IDP signaturesRequired element value is. + + + + + Should the client expect the IDP to sign the assertion response document sent back from an auhtn request? Defaults to whatever the IDP signaturesRequired element value is. + + + + + Should the client expect the IDP to sign the individual assertions sent back from an auhtn request? Defaults to whatever the IDP signaturesRequired element value is. + + + + + SAML binding type used for communicating with the IDP. The default value is POST, but you can set it to REDIRECT as well. + + + + + SAML allows the client to request what binding type it wants authn responses to use. This value maps to ProtocolBinding attribute in SAML AuthnRequest. The default is that the client will not request a specific binding type for responses. + + + + + This is the URL for the IDP login service that the client will send requests to. + + + + + URL of the assertion consumer service (ACS) where the IDP login service should send responses to. By default it is unset, relying on the IdP settings. When set, it must end in "/saml". This property is typically accompanied by the responseBinding attribute. + + + + + + + + Should the client sign authn requests? Defaults to whatever the IDP signaturesRequired element value is. + + + + + Should the client sign logout responses it sends to the IDP requests? Defaults to whatever the IDP signaturesRequired element value is. + + + + + Should the client expect signed logout request documents from the IDP? Defaults to whatever the IDP signaturesRequired element value is. + + + + + Should the client expect signed logout response documents from the IDP? Defaults to whatever the IDP signaturesRequired element value is. + + + + + This is the SAML binding type used for communicating SAML requests to the IDP. The default value is POST. + + + + + This is the SAML binding type used for communicating SAML responses to the IDP. The default value is POST. + + + + + This is the URL for the IDP's logout service when using the POST binding. This setting is REQUIRED if using the POST binding. + + + + + This is the URL for the IDP's logout service when using the REDIRECT binding. This setting is REQUIRED if using the REDIRECT binding. + + + + + + + + If the the IDP server requires HTTPS and this config option is set to true the IDP's certificate + is validated via the truststore, but host name validation is not done. This setting should only be used during + development and never in production as it will partly disable verification of SSL certificates. + This seting may be useful in test environments. The default value is false. + + + + + This is the file path to a keystore file. This keystore contains client certificate + for two-way SSL when the adapter makes HTTPS requests to the IDP server. + + + + + Password for the client keystore and for the client's key. + + + + + Defines number of pooled connections. + + + + + If the the IDP server requires HTTPS and this config option is set to true you do not have to specify a truststore. + This setting should only be used during development and never in production as it will disable verification of SSL certificates. + The default value is false. + + + + + URL to HTTP proxy to use for HTTP connections. + + + + + The value is the file path to a keystore file. If you prefix the path with classpath:, + then the truststore will be obtained from the deployment's classpath instead. Used for outgoing + HTTPS communications to the IDP server. Client making HTTPS requests need + a way to verify the host of the server they are talking to. This is what the trustore does. + The keystore contains one or more trusted host certificates or certificate authorities. + You can create this truststore by extracting the public certificate of the IDP's SSL keystore. + + + + + + Password for the truststore keystore. + + + + + + The value is the allowed clock skew between the IDP and the SP. + + + + + + + + + + Time unit for the value of the clock skew. + + + + + + + + + + diff --git a/adapters/saml/core/src/test/java/org/keycloak/adapters/saml/PropertiesBasedRoleMapperTest.java b/adapters/saml/core/src/test/java/org/keycloak/adapters/saml/PropertiesBasedRoleMapperTest.java new file mode 100644 index 0000000000..4045c4e479 --- /dev/null +++ b/adapters/saml/core/src/test/java/org/keycloak/adapters/saml/PropertiesBasedRoleMapperTest.java @@ -0,0 +1,62 @@ +/* + * 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.adapters.saml; + +import java.io.InputStream; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.junit.Test; +import org.keycloak.adapters.saml.config.parsers.DeploymentBuilder; +import org.keycloak.adapters.saml.config.parsers.ResourceLoader; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * Tests for the {@link PropertiesBasedRoleMapper} implementation. + * + * @author Stefan Guilhen + */ +public class PropertiesBasedRoleMapperTest { + + @Test + public void testPropertiesBasedRoleMapper() throws Exception { + InputStream is = getClass().getResourceAsStream("config/parsers/keycloak-saml-with-role-mappings-provider.xml"); + SamlDeployment deployment = new DeploymentBuilder().build(is, new ResourceLoader() { + @Override + public InputStream getResourceAsStream(String resource) { + return this.getClass().getClassLoader().getResourceAsStream(resource); + } + }); + // retrieve the configured role mappings provider - in this case we know it is the properties-based implementation. + RoleMappingsProvider provider = deployment.getRoleMappingsProvider(); + + // if provider was properly configured we should be able to see the mappings as specified in the properties file. + final Set samlRoles = new HashSet<>(Arrays.asList(new String[]{"samlRoleA", "samlRoleB", "samlRoleC"})); + final Set mappedRoles = provider.map("kc-user", samlRoles); + + // we expect samlRoleB to be removed, samlRoleA to be mapped into two roles (jeeRoleX, jeeRoleY) and also the principal should + // be granted an extra role (jeeRoleZ). + assertNotNull(mappedRoles); + assertEquals(4, mappedRoles.size()); + Set expectedRoles = new HashSet<>(Arrays.asList(new String[]{"samlRoleC", "jeeRoleX", "jeeRoleY", "jeeRoleZ"})); + assertEquals(expectedRoles, mappedRoles); + } +} diff --git a/adapters/saml/core/src/test/java/org/keycloak/adapters/saml/config/parsers/KeycloakSamlAdapterXMLParserTest.java b/adapters/saml/core/src/test/java/org/keycloak/adapters/saml/config/parsers/KeycloakSamlAdapterXMLParserTest.java index c2a1f1f7df..b7613e0fe1 100755 --- a/adapters/saml/core/src/test/java/org/keycloak/adapters/saml/config/parsers/KeycloakSamlAdapterXMLParserTest.java +++ b/adapters/saml/core/src/test/java/org/keycloak/adapters/saml/config/parsers/KeycloakSamlAdapterXMLParserTest.java @@ -19,6 +19,7 @@ package org.keycloak.adapters.saml.config.parsers; import static org.junit.Assert.*; import static org.hamcrest.CoreMatchers.*; + import org.junit.Test; import org.keycloak.adapters.saml.config.IDP; import org.keycloak.adapters.saml.config.Key; @@ -31,6 +32,7 @@ import org.junit.Rule; import org.junit.rules.ExpectedException; import org.keycloak.saml.common.exceptions.ParsingException; import java.io.IOException; +import java.util.Properties; import java.util.concurrent.TimeUnit; import org.hamcrest.Matchers; @@ -41,7 +43,7 @@ import org.hamcrest.Matchers; */ public class KeycloakSamlAdapterXMLParserTest { - private static final String CURRENT_XSD_LOCATION = "/schema/keycloak_saml_adapter_1_11.xsd"; + private static final String CURRENT_XSD_LOCATION = "/schema/keycloak_saml_adapter_1_12.xsd"; @Rule public ExpectedException expectedException = ExpectedException.none(); @@ -79,6 +81,11 @@ public class KeycloakSamlAdapterXMLParserTest { testValidationValid("keycloak-saml-with-allowed-clock-skew-with-unit.xml"); } + @Test + public void testValidationWithRoleMappingsProvider() throws Exception { + testValidationValid("keycloak-saml-with-role-mappings-provider.xml"); + } + @Test public void testValidationKeyInvalid() throws Exception { InputStream schemaIs = KeycloakSamlAdapterV1Parser.class.getResourceAsStream(CURRENT_XSD_LOCATION); @@ -276,6 +283,7 @@ public class KeycloakSamlAdapterXMLParserTest { assertThat(idp.getAllowedClockSkew(), is(3)); assertThat(idp.getAllowedClockSkewUnit(), is(TimeUnit.SECONDS)); } + @Test public void testAllowedClockSkewWithUnit() throws Exception { KeycloakSamlAdapter config = parseKeycloakSamlAdapterConfig("keycloak-saml-with-allowed-clock-skew-with-unit.xml", KeycloakSamlAdapter.class); @@ -287,4 +295,20 @@ public class KeycloakSamlAdapterXMLParserTest { assertThat(idp.getAllowedClockSkewUnit(), is (TimeUnit.MILLISECONDS)); } + @Test + public void testParseRoleMappingsProvider() throws Exception { + KeycloakSamlAdapter config = parseKeycloakSamlAdapterConfig("keycloak-saml-with-role-mappings-provider.xml", KeycloakSamlAdapter.class); + assertNotNull(config); + assertThat(config.getSps(), Matchers.contains(instanceOf(SP.class))); + SP sp = config.getSps().get(0); + SP.RoleMappingsProviderConfig roleMapperConfig = sp.getRoleMappingsProviderConfig(); + assertNotNull(roleMapperConfig); + assertThat(roleMapperConfig.getId(), is("properties-based-role-mapper")); + Properties providerConfig = roleMapperConfig.getConfiguration(); + assertThat(providerConfig.size(), is(2)); + assertTrue(providerConfig.containsKey("properties.resource.location")); + assertEquals("role-mappings.properties", providerConfig.getProperty("properties.resource.location")); + assertTrue(providerConfig.containsKey("another.property")); + assertEquals("another.value", providerConfig.getProperty("another.property")); + } } diff --git a/adapters/saml/core/src/test/resources/org/keycloak/adapters/saml/config/parsers/keycloak-saml-with-role-mappings-provider.xml b/adapters/saml/core/src/test/resources/org/keycloak/adapters/saml/config/parsers/keycloak-saml-with-role-mappings-provider.xml new file mode 100755 index 0000000000..3222da95da --- /dev/null +++ b/adapters/saml/core/src/test/resources/org/keycloak/adapters/saml/config/parsers/keycloak-saml-with-role-mappings-provider.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + diff --git a/adapters/saml/core/src/test/resources/role-mappings.properties b/adapters/saml/core/src/test/resources/role-mappings.properties new file mode 100644 index 0000000000..df9295171b --- /dev/null +++ b/adapters/saml/core/src/test/resources/role-mappings.properties @@ -0,0 +1,23 @@ +# +# 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. +# + +# role to roles mappings +samlRoleA=jeeRoleX,jeeRoleY +samlRoleB= + +# principal to roles mappings +kc-user=jeeRoleZ \ No newline at end of file diff --git a/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/Constants.java b/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/Constants.java index c262e2b484..1d671c188e 100755 --- a/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/Constants.java +++ b/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/Constants.java @@ -70,6 +70,9 @@ public class Constants { static final String ALIAS = "alias"; static final String FILE = "file"; static final String SIGNATURES_REQUIRED = "signaturesRequired"; + + static final String ROLE_MAPPINGS_PROVIDER_ID = "roleMappingsProviderId"; + static final String ROLE_MAPPINGS_PROVIDER_CONFIG = "roleMappingsProviderConfig"; } @@ -126,6 +129,11 @@ public class Constants { static final String FILE = "file"; static final String SIGNATURES_REQUIRED = "signaturesRequired"; static final String ASSERTION_CONSUMER_SERVICE_URL = "assertionConsumerServiceUrl"; + + static final String ID = "id"; + static final String VALUE = "value"; + static final String PROPERTY = "Property"; + static final String ROLE_MAPPINGS_PROVIDER = "RoleMappingsProvider"; } } diff --git a/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSamlExtension.java b/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSamlExtension.java index a14e3d587f..dfa43bd213 100755 --- a/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSamlExtension.java +++ b/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSamlExtension.java @@ -36,7 +36,9 @@ import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUB public class KeycloakSamlExtension implements Extension { public static final String SUBSYSTEM_NAME = "keycloak-saml"; - public static final String NAMESPACE = "urn:jboss:domain:keycloak-saml:1.1"; + public static final String NAMESPACE_1_1 = "urn:jboss:domain:keycloak-saml:1.1"; + public static final String NAMESPACE_1_2 = "urn:jboss:domain:keycloak-saml:1.2"; + public static final String CURRENT_NAMESPACE = NAMESPACE_1_2; private static final KeycloakSubsystemParser PARSER = new KeycloakSubsystemParser(); static final PathElement PATH_SUBSYSTEM = PathElement.pathElement(SUBSYSTEM, SUBSYSTEM_NAME); private static final String RESOURCE_NAME = KeycloakSamlExtension.class.getPackage().getName() + ".LocalDescriptions"; @@ -56,7 +58,8 @@ public class KeycloakSamlExtension implements Extension { */ @Override public void initializeParsers(final ExtensionParsingContext context) { - context.setSubsystemXmlMapping(SUBSYSTEM_NAME, KeycloakSamlExtension.NAMESPACE, PARSER); + context.setSubsystemXmlMapping(SUBSYSTEM_NAME, KeycloakSamlExtension.NAMESPACE_1_1, PARSER); + context.setSubsystemXmlMapping(SUBSYSTEM_NAME, KeycloakSamlExtension.NAMESPACE_1_2, PARSER); } /** diff --git a/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSubsystemParser.java b/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSubsystemParser.java index f938402108..153a42d078 100755 --- a/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSubsystemParser.java +++ b/adapters/saml/wildfly/wildfly-subsystem/src/main/java/org/keycloak/subsystem/adapter/saml/extension/KeycloakSubsystemParser.java @@ -116,6 +116,8 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader
  • ATTRIBUTE_MAP = new HashMap<>(); diff --git a/adapters/saml/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/saml/extension/LocalDescriptions.properties b/adapters/saml/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/saml/extension/LocalDescriptions.properties index 0339a8abdd..23e604dd9d 100755 --- a/adapters/saml/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/saml/extension/LocalDescriptions.properties +++ b/adapters/saml/wildfly/wildfly-subsystem/src/main/resources/org/keycloak/subsystem/adapter/saml/extension/LocalDescriptions.properties @@ -39,6 +39,8 @@ keycloak-saml.SP.PrincipalNameMapping-policy=Principal name mapping policy keycloak-saml.SP.PrincipalNameMapping-attribute-name=Principal name mapping attribute name keycloak-saml.SP.Key=A key definition keycloak-saml.SP.IDP=Identity provider definition +keycloak-saml.SP.roleMappingsProviderId=The string that identifies the role mappings provider to be used within the SP +keycloak-saml.SP.roleMappingsProviderConfig=The configuration properties of the role mappings provider keycloak-saml.Key=A key configuration for service provider or identity provider keycloak-saml.Key.add=Add a key definition diff --git a/adapters/saml/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_2.xsd b/adapters/saml/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_2.xsd new file mode 100755 index 0000000000..baa10c6ac6 --- /dev/null +++ b/adapters/saml/wildfly/wildfly-subsystem/src/main/resources/schema/wildfly-keycloak-saml_1_2.xsd @@ -0,0 +1,334 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + The name of the realm. + + + + + + + + + + + + + + + The entity ID for SAML service provider + + + + + The ssl policy + + + + + Name ID policy format URN + + + + + URI to a logout page + + + + + Redirected unauthenticated request to a login page + + + + + If user isn't logged in just return with an error. Used to check if a user is already logged in or not + + + + + The session id is changed by default on a successful login. Change this to true if you want to turn this off + + + + + + + + + + + + The entity ID for SAML service provider + + + + + Require signatures for single-sign-on and single-logout + + + + + Algorithm used for signatures + + + + + Canonicalization method used for signatures + + + + + + + Sign the SSO requests + + + + + Validate the SSO response signature + + + + + Validate the SSO assertion signature + + + + + HTTP method to use for requests + + + + + HTTP method to use for response + + + + + SSO endpoint URL + + + + + Endpoint of Assertion Consumer Service at SP + + + + + + + Validate a single-logout request signature + + + + + Validate a single-logout response signature + + + + + Sign single-logout requests + + + + + Sign single-logout responses + + + + + HTTP method to use for request + + + + + HTTP method to use for response + + + + + Endpoint URL for posting + + + + + Endpoint URL for redirects + + + + + + + + + + + + + + + + + + Key can be used for signing + + + + + Key can be used for encryption + + + + + + + + + + + Key store filesystem path + + + + + Key store resource URI + + + + + Key store password + + + + + Key store format + + + + + Key alias + + + + + + + + Private key alias + + + + + Private key password + + + + + + + + Certificate alias + + + + + + + + Principal name mapping policy. Possible values: FROM_NAME_ID + + + + + Name of the attribute to use for principal name mapping + + + + + + + + + + + + + + Role attribute + + + + + + + + + Specifies a configuration property for the provider. + + + + + + The id of the role mappings provider that is to be used. Example: properties-based-provider. + + + + + + + + The name (key) of the configuration property. + + + + + The value of the configuration property. + + + + diff --git a/adapters/saml/wildfly/wildfly-subsystem/src/main/resources/subsystem-templates/keycloak-saml-adapter.xml b/adapters/saml/wildfly/wildfly-subsystem/src/main/resources/subsystem-templates/keycloak-saml-adapter.xml index f22beb9183..d05812d054 100755 --- a/adapters/saml/wildfly/wildfly-subsystem/src/main/resources/subsystem-templates/keycloak-saml-adapter.xml +++ b/adapters/saml/wildfly/wildfly-subsystem/src/main/resources/subsystem-templates/keycloak-saml-adapter.xml @@ -19,6 +19,6 @@ org.keycloak.keycloak-saml-adapter-subsystem - + diff --git a/adapters/saml/wildfly/wildfly-subsystem/src/test/java/org/keycloak/subsystem/adapter/saml/extension/SubsystemParsingTestCase.java b/adapters/saml/wildfly/wildfly-subsystem/src/test/java/org/keycloak/subsystem/adapter/saml/extension/SubsystemParsingTestCase.java index 0cba877b3f..620958608f 100755 --- a/adapters/saml/wildfly/wildfly-subsystem/src/test/java/org/keycloak/subsystem/adapter/saml/extension/SubsystemParsingTestCase.java +++ b/adapters/saml/wildfly/wildfly-subsystem/src/test/java/org/keycloak/subsystem/adapter/saml/extension/SubsystemParsingTestCase.java @@ -39,12 +39,12 @@ public class SubsystemParsingTestCase extends AbstractSubsystemBaseTest { @Override protected String getSubsystemXml() throws IOException { - return readResource("keycloak-saml-1.1.xml"); + return readResource("keycloak-saml-1.2.xml"); } @Override protected String getSubsystemXsdPath() throws Exception { - return "schema/wildfly-keycloak-saml_1_1.xsd"; + return "schema/wildfly-keycloak-saml_1_2.xsd"; } @Override diff --git a/adapters/saml/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/saml/extension/keycloak-saml-1.2.xml b/adapters/saml/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/saml/extension/keycloak-saml-1.2.xml new file mode 100755 index 0000000000..71f400a30b --- /dev/null +++ b/adapters/saml/wildfly/wildfly-subsystem/src/test/resources/org/keycloak/subsystem/adapter/saml/extension/keycloak-saml-1.2.xml @@ -0,0 +1,75 @@ + + + + + + + + + my_key.pem + my_key.pub + cert.cer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java index b4794eac4e..7ff56ce7cf 100755 --- a/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java +++ b/testsuite/integration-arquillian/test-apps/servlets/src/main/java/org/keycloak/testsuite/adapter/servlet/SendUsernameServlet.java @@ -179,6 +179,7 @@ public class SendUsernameServlet { output += principal.getName() + "\n"; output += getSessionInfo() + "\n"; + output += getRoles() + "\n"; return output; } @@ -202,6 +203,15 @@ public class SendUsernameServlet { return "Session doesn't exists"; } + private String getRoles() { + StringBuilder output = new StringBuilder("Roles: "); + for (String role : ((SamlPrincipal) httpServletRequest.getUserPrincipal()).getAttributes("Roles")) { + output.append(role).append(","); + } + + return output.toString(); + } + private String getErrorOutput(Integer statusCode) { String output = "Error Page

    There was an error

    "; if (statusCode != null) diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeRoleMappingServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeRoleMappingServlet.java new file mode 100644 index 0000000000..f8909d7e49 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/EmployeeRoleMappingServlet.java @@ -0,0 +1,97 @@ +/* + * 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.adapter.page; + +import java.net.URL; + +import org.jboss.arquillian.container.test.api.OperateOnDeployment; +import org.jboss.arquillian.test.api.ArquillianResource; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.testsuite.auth.page.login.SAMLPostLogin; +import org.keycloak.testsuite.util.WaitUtils; +import org.openqa.selenium.By; + +/** + * A {@code Page} for the {@code EmployeeRoleMapping} application. + * + * @author Stefan Guilhen + */ +public class EmployeeRoleMappingServlet extends SAMLServlet { + + public static final String DEPLOYMENT_NAME = "employee-role-mapping"; + + @ArquillianResource + @OperateOnDeployment(DEPLOYMENT_NAME) + private URL url; + + private SAMLPostLogin loginPage; + private UserRepresentation user; + + @Override + public URL getInjectedUrl() { + return url; + } + + /** + * For scenarios in which the access to every method in the servlet is required to be authenticated (for example, when + * running the servlet filter tests) we need to setup the info required to perform the login before calling the servlet + * methods used by the test (such as {@code setCheckRoles} or {@code (un)checkRoles}). + * + * @param page the login page to be used to authenticate an user. + * @param user the user being authenticated. + */ + public void setupLoginInfo(final SAMLPostLogin page, final UserRepresentation user) { + this.loginPage = page; + this.user = user; + } + + /** + * Clears the login info (login page and user) so that no authentication is performed before the calling the servlet + * methods that don't require authentication. + */ + public void clearLoginInfo() { + this.loginPage = null; + this.user = null; + } + + @Override + public void setRolesToCheck(String roles) { + if (this.loginPage != null) { + // authenticates user before calling setCheckRoles on the servlet - required in filter tests. + driver.navigate().to(getUriBuilder().clone().path("setCheckRoles").queryParam("roles", roles).build().toASCIIString()); + loginPage.form().login(user); + WaitUtils.waitUntilElement(By.tagName("body")).text().contains("These roles will be checked:"); + this.logout(); + } else { + super.setRolesToCheck(roles); + } + } + + @Override + public void checkRolesEndPoint(boolean value) { + if (this.loginPage != null) { + // authenticates user before calling setCheckRoles on the servlet - required in filter tests. + driver.navigate().to(getUriBuilder().clone().path((value ? "" : "un") + "checkRoles").build().toASCIIString()); + loginPage.form().login(user); + WaitUtils.waitUntilElement(By.tagName("body")).text().contains("Roles will " + (value ? "" : "not ") + "be checked"); + this.logout(); + } else { + super.checkRolesEndPoint(value); + } + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServlet.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServlet.java index b77cb5843f..54c4807112 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServlet.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/SAMLServlet.java @@ -17,6 +17,13 @@ package org.keycloak.testsuite.adapter.page; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + import javax.ws.rs.core.UriBuilder; import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl; import org.keycloak.testsuite.util.WaitUtils; @@ -55,4 +62,22 @@ public abstract class SAMLServlet extends AbstractPageWithInjectedUrl { waitForPageToLoad(); WaitUtils.waitUntilElement(By.tagName("body")).text().contains("These roles will be checked:"); } + + public List rolesList() { + String rolesPattern = getFromPageByPattern("Roles"); + if (rolesPattern != null) { + return Arrays.stream(rolesPattern.split(",")).filter(s -> !s.isEmpty()).collect(Collectors.toList()); + } else { + return new ArrayList<>(); + } + } + + public String getFromPageByPattern(String text) { + Pattern p = Pattern.compile(text + ": (.*)"); + Matcher m = p.matcher(driver.getPageSource()); + if (m.find()) { + return m.group(1); + } + return null; + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java index 413b109754..c2a3bd210c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java @@ -121,6 +121,12 @@ public abstract class AbstractServletsAdapterTest extends AbstractAdapterTest { .addClasses(servletClasses) .addAsWebInfResource(jbossDeploymentStructure, JBOSS_DEPLOYMENT_STRUCTURE_XML); + // if a role-mappings.properties file exist in WEB-INF, include it in the deployment. + URL roleMappingsConfig = AbstractServletsAdapterTest.class.getResource(webInfPath + "role-mappings.properties"); + if(roleMappingsConfig != null) { + deployment.addAsWebInfResource(roleMappingsConfig, "role-mappings.properties"); + } + String webXMLContent; try { webXMLContent = IOUtils.toString(webXML.openStream(), Charset.forName("UTF-8")) diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SAMLFilterServletAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SAMLFilterServletAdapterTest.java index 121ae33837..685935522e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SAMLFilterServletAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SAMLFilterServletAdapterTest.java @@ -48,6 +48,7 @@ public class SAMLFilterServletAdapterTest extends SAMLServletAdapterTest { employeeSigPostNoIdpKeyServletPage.checkRoles(true); employeeSigRedirNoIdpKeyServletPage.checkRoles(true); employeeSigRedirOptNoIdpKeyServletPage.checkRoles(true); + employeeRoleMappingPage.setupLoginInfo(testRealmSAMLPostLoginPage, bburkeUser); //using endpoint instead of query param because we are not able to put query param to IDP initiated login employee2ServletPage.navigateTo(); @@ -81,6 +82,7 @@ public class SAMLFilterServletAdapterTest extends SAMLServletAdapterTest { employeeSigPostNoIdpKeyServletPage.checkRoles(false); employeeSigRedirNoIdpKeyServletPage.checkRoles(false); employeeSigRedirOptNoIdpKeyServletPage.checkRoles(false); + employeeRoleMappingPage.clearLoginInfo(); } @Test @@ -117,4 +119,22 @@ public class SAMLFilterServletAdapterTest extends SAMLServletAdapterTest { public void multiTenant2SamlTest() throws Exception { } + + /** + * Tests that the adapter is using the configured role mappings provider to map the roles extracted from the assertion + * into roles that exist in the application domain. For this test a {@link org.keycloak.adapters.saml.PropertiesBasedRoleMapper} + * has been setup in the adapter, performing the mappings as specified in the {@code role-mappings.properties} file. + * + * @throws Exception if an error occurs while running the test. + */ + @Test + @Override + public void testAdapterRoleMappings() throws Exception { + try { + employeeRoleMappingPage.setRolesToCheck("manager,coordinator,team-lead,employee"); + super.testAdapterRoleMappings(); + } finally { + employeeRoleMappingPage.checkRolesEndPoint(false); + } + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SAMLServletAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SAMLServletAdapterTest.java index 9416c6a0f5..b0bfd038d1 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SAMLServletAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/SAMLServletAdapterTest.java @@ -18,11 +18,12 @@ package org.keycloak.testsuite.adapter.servlet; import static javax.ws.rs.core.Response.Status.OK; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.*; import static org.keycloak.OAuth2Constants.PASSWORD; +import static org.keycloak.testsuite.admin.Users.getPasswordOf; import static org.keycloak.testsuite.admin.Users.setPasswordFor; -import static org.keycloak.testsuite.AbstractAuthTest.createUserRepresentation; -import static org.keycloak.testsuite.adapter.AbstractServletsAdapterTest.samlServletDeployment; +import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO; import static org.keycloak.testsuite.auth.page.AuthRealm.SAMLSERVLETDEMO; import static org.keycloak.testsuite.saml.AbstractSamlTest.REALM_PRIVATE_KEY; import static org.keycloak.testsuite.saml.AbstractSamlTest.REALM_PUBLIC_KEY; @@ -203,6 +204,9 @@ public class SAMLServletAdapterTest extends AbstractSAMLServletAdapterTest { @Page protected EmployeeSigFrontServlet employeeSigFrontServletPage; + @Page + protected EmployeeRoleMappingServlet employeeRoleMappingPage; + @Page protected SalesMetadataServlet salesMetadataServletPage; @@ -328,6 +332,11 @@ public class SAMLServletAdapterTest extends AbstractSAMLServletAdapterTest { return samlServletDeployment(EmployeeSigFrontServlet.DEPLOYMENT_NAME, SendUsernameServlet.class); } + @Deployment(name = EmployeeRoleMappingServlet.DEPLOYMENT_NAME) + protected static WebArchive employeeRoleMapping() { + return samlServletDeployment(EmployeeRoleMappingServlet.DEPLOYMENT_NAME, "employee-role-mapping/WEB-INF/web.xml", SendUsernameServlet.class); + } + @Deployment(name = SalesMetadataServlet.DEPLOYMENT_NAME) protected static WebArchive salesMetadata() { return samlServletDeployment(SalesMetadataServlet.DEPLOYMENT_NAME, SendUsernameServlet.class); @@ -1795,6 +1804,44 @@ public class SAMLServletAdapterTest extends AbstractSAMLServletAdapterTest { Assert.assertThat(statusCode.getStatusCode().getValue().toString(), is(not(JBossSAMLURIConstants.STATUS_SUCCESS.get()))); } + /** + * Tests that the adapter is using the configured role mappings provider to map the roles extracted from the assertion + * into roles that exist in the application domain. For this test a {@link org.keycloak.adapters.saml.PropertiesBasedRoleMapper} + * has been setup in the adapter, performing the mappings as specified in the {@code role-mappings.properties} file. + * + * @throws Exception if an error occurs while running the test. + */ + @Test + public void testAdapterRoleMappings() throws Exception { + // bburke user is missing required coordinator role, which is only available via mapping of the supervisor role. + assertForbiddenLogin(employeeRoleMappingPage, bburkeUser.getUsername(), getPasswordOf(bburkeUser), + testRealmSAMLPostLoginPage, "bburke@redhat.com"); + employeeRoleMappingPage.logout(); + checkLoggedOut(employeeRoleMappingPage, testRealmSAMLPostLoginPage); + + // assign the supervisor role to user bburke - it should be mapped to coordinator next time he logs in. + UserRepresentation bburke = adminClient.realm(DEMO).users().search("bburke", 0, 1).get(0); + ClientRepresentation clientRepresentation = adminClient.realm(DEMO) + .clients().findByClientId("http://localhost:8280/employee-role-mapping/").get(0); + RoleRepresentation role = adminClient.realm(DEMO).clients().get(clientRepresentation.getId()) + .roles().get("supervisor").toRepresentation(); + + adminClient.realm(DEMO).users().get(bburke.getId()).roles() + .clientLevel(clientRepresentation.getId()).add(Collections.singletonList(role)); + + // now check for the set of expected mapped roles: supervisor should have been mapped to coordinator, team-lead should + // have been added to bburke, and user should have been discarded; manager and employed unchanged from mappings. + assertSuccessfulLogin(employeeRoleMappingPage, bburkeUser, testRealmSAMLPostLoginPage, "bburke@redhat.com"); + assertThat(employeeRoleMappingPage.rolesList()) + .contains("manager", "coordinator", "team-lead", "employee") + .doesNotContain("supervisor", "user"); + employeeRoleMappingPage.logout(); + checkLoggedOut(employeeRoleMappingPage, testRealmSAMLPostLoginPage); + + adminClient.realm(DEMO).users().get(bburke.getId()).roles().clientLevel(clientRepresentation.getId()).remove(Collections.singletonList(role)); + + } + public static void printDocument(Source doc, OutputStream out) throws IOException, TransformerException { TransformerFactory tf = TransformerFactory.newInstance(); Transformer transformer = tf.newTransformer(); @@ -1848,11 +1895,12 @@ public class SAMLServletAdapterTest extends AbstractSAMLServletAdapterTest { } } - private void setRolesToCheck(String roles) { + private void setRolesToCheck(String roles) throws Exception { employee2ServletPage.navigateTo(); assertCurrentUrlStartsWith(testRealmSAMLPostLoginPage); testRealmSAMLPostLoginPage.form().login(bburkeUser); - driver.navigate().to(employee2ServletPage.toString() + "/setCheckRoles?roles=" + roles); + driver.navigate().to(employee2ServletPage.getUriBuilder().clone().path("setCheckRoles").queryParam("roles", roles).build().toURL()); + WaitUtils.waitUntilElement(By.tagName("body")).text().contains("These roles will be checked:"); employee2ServletPage.logout(); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-role-mapping/WEB-INF/keycloak-saml.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-role-mapping/WEB-INF/keycloak-saml.xml new file mode 100755 index 0000000000..0860dba555 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-role-mapping/WEB-INF/keycloak-saml.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-role-mapping/WEB-INF/role-mappings.properties b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-role-mapping/WEB-INF/role-mappings.properties new file mode 100644 index 0000000000..beb2f0382c --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-role-mapping/WEB-INF/role-mappings.properties @@ -0,0 +1,23 @@ +# +# 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. +# + +# role to roles mappings +supervisor=coordinator +user= + +# principal to roles mappings +bburke@redhat.com=team-lead \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-role-mapping/WEB-INF/web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-role-mapping/WEB-INF/web.xml new file mode 100644 index 0000000000..132f151db0 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/employee-role-mapping/WEB-INF/web.xml @@ -0,0 +1,69 @@ + + + + + + %CONTEXT_PATH% + + + javax.ws.rs.core.Application + 1 + + + javax.ws.rs.core.Application + /* + + + + /error.html + + + + + Application + /* + + + coordinator + + + + + Unsecured-setCheckRoles + /setCheckRoles/* + + + + + Unsecured-uncheckRoles + /uncheckRoles/* + + + + + KEYCLOAK-SAML + demo + + + + coordinator + + diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json index 093311b88a..3bc1d70dba 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/keycloak-saml/testsaml.json @@ -33,7 +33,8 @@ "realmRoles": ["manager", "user"], "applicationRoles": { "http://localhost:8280/employee/": [ "employee" ], - "http://localhost:8280/employee2/": [ "empl.oyee", "employee" ] + "http://localhost:8280/employee2/": [ "empl.oyee", "employee" ], + "http://localhost:8280/employee-role-mapping/": ["employee"] } }, { @@ -662,6 +663,57 @@ "saml.authnstatement": "true", "saml.signing.certificate": "MIIB1DCCAT0CBgFJGP5dZDANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVodHRwOi8vbG9jYWxob3N0OjgwODAvc2FsZXMtcG9zdC1zaWcvMB4XDTE0MTAxNjEyNDQyM1oXDTI0MTAxNjEyNDYwM1owMDEuMCwGA1UEAxMlaHR0cDovL2xvY2FsaG9zdDo4MDgwL3NhbGVzLXBvc3Qtc2lnLzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1RvGu8RjemSJA23nnMksoHA37MqY1DDTxOECY4rPAd9egr7GUNIXE0y1MokaR5R2crNpN8RIRwR8phQtQDjXL82c6W+NLQISxztarQJ7rdNJIYwHY0d5ri1XRpDP8zAuxubPYiMAVYcDkIcvlbBpwh/dRM5I2eElRK+eSiaMkCUCAwEAATANBgkqhkiG9w0BAQsFAAOBgQCLms6htnPaY69k1ntm9a5jgwSn/K61cdai8R8B0ccY7zvinn9AfRD7fiROQpFyY29wKn8WCLrJ86NBXfgFUGyR5nLNHVy3FghE36N2oHy53uichieMxffE6vhkKJ4P8ChfJMMOZlmCPsQPDvjoAghHt4mriFiQgRdPgIy/zDjSNw==" } + }, + { + "clientId": "http://localhost:8280/employee-role-mapping/", + "enabled": true, + "protocol": "saml", + "fullScopeAllowed": true, + "baseUrl": "http://localhost:8080/employee-role-mapping", + "redirectUris": [ + "http://localhost:8080/employee-role-mapping/*" + ], + "adminUrl": "http://localhost:8080/employee-role-mapping", + "attributes": { + "saml.authnstatement": "true", + "saml_idp_initiated_sso_url_name" : "employee-role-mapping" + }, + "protocolMappers": [ + { + "name": "email", + "protocol": "saml", + "protocolMapper": "saml-user-property-mapper", + "consentRequired": false, + "config": { + "user.attribute": "email", + "friendly.name": "email", + "attribute.name": "urn:oid:1.2.840.113549.1.9.1", + "attribute.nameformat": "URI Reference" + } + }, + { + "name": "phone", + "protocol": "saml", + "protocolMapper": "saml-user-attribute-mapper", + "consentRequired": false, + "config": { + "user.attribute": "phone", + "attribute.name": "phone", + "attribute.nameformat": "Basic" + } + }, + { + "name": "role-list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "attribute.name": "Role", + "attribute.nameformat": "Basic", + "single": "false" + } + } + ] } ], "groups" : [ @@ -716,7 +768,18 @@ "name": "empl.oyee", "description": "Have Employee privileges with dots" } + ], + "http://localhost:8280/employee-role-mapping/" : [ + { + "name": "employee", + "description": "Have Employee privileges" + }, + { + "name": "supervisor", + "description": "Have Supervisor privileges" + } ] + } } }