[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.
This commit is contained in:
Stefan Guilhen 2019-07-29 15:10:55 -03:00 committed by Bruno Oliveira da Silva
parent a726e625e9
commit 60205845a8
42 changed files with 2505 additions and 24 deletions

View file

@ -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";
}
}

View file

@ -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);
}
/**

View file

@ -118,6 +118,8 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
readPrincipalNameMapping(addServiceProvider, reader);
} else if (Constants.XML.ROLE_IDENTIFIERS.equals(tagName)) {
readRoleIdentifiers(addServiceProvider, reader);
} else if (Constants.XML.ROLE_MAPPINGS_PROVIDER.equals(tagName)) {
readRoleMappingsProvider(addServiceProvider, reader);
} else if (Constants.XML.IDENTITY_PROVIDER.equals(tagName)) {
readIdentityProvider(list, reader, addr);
} else {
@ -339,6 +341,21 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
}
}
void readRoleMappingsProvider(final ModelNode addServiceProvider, final XMLExtendedStreamReader reader) throws XMLStreamException {
String providerId = readRequiredAttribute(reader, Constants.XML.ID);
ServiceProviderDefinition.ROLE_MAPPINGS_PROVIDER_ID.parseAndSetParameter(providerId, addServiceProvider, reader);
while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
String tagName = reader.getLocalName();
if (!Constants.XML.PROPERTY.equals(tagName)) {
throw ParseUtils.unexpectedElement(reader);
}
final String[] array = ParseUtils.requireAttributes(reader, Constants.XML.NAME, Constants.XML.VALUE);
ServiceProviderDefinition.ROLE_MAPPINGS_PROVIDER_CONFIG.parseAndAddParameterElement(array[0], array[1], addServiceProvider, reader);
ParseUtils.requireNoContent(reader);
}
}
void readPrincipalNameMapping(ModelNode addServiceProvider, XMLExtendedStreamReader reader) throws XMLStreamException {
boolean policySet = false;
@ -386,7 +403,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
*/
@Override
public void writeContent(final XMLExtendedStreamWriter writer, final SubsystemMarshallingContext context) throws XMLStreamException {
context.startSubsystemElement(KeycloakSamlExtension.NAMESPACE, false);
context.startSubsystemElement(KeycloakSamlExtension.CURRENT_NAMESPACE, false);
writeSecureDeployment(writer, context.getModelNode());
writer.writeEndElement();
}
@ -419,6 +436,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
writeKeys(writer, spAttributes.get(Constants.Model.KEY));
writePrincipalNameMapping(writer, spAttributes);
writeRoleIdentifiers(writer, spAttributes);
writeRoleMappingsProvider(writer, spAttributes);
writeIdentityProvider(writer, spAttributes.get(Constants.Model.IDENTITY_PROVIDER));
writer.writeEndElement();
@ -554,6 +572,17 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
writer.writeEndElement();
}
void writeRoleMappingsProvider(final XMLExtendedStreamWriter writer, final ModelNode model) throws XMLStreamException {
ModelNode providerId = model.get(Constants.Model.ROLE_MAPPINGS_PROVIDER_ID);
if (!providerId.isDefined()) {
return;
}
writer.writeStartElement(Constants.XML.ROLE_MAPPINGS_PROVIDER);
writer.writeAttribute(Constants.XML.ID, providerId.asString());
ServiceProviderDefinition.ROLE_MAPPINGS_PROVIDER_CONFIG.marshallAsElement(model, false, writer);
writer.writeEndElement();
}
void writePrincipalNameMapping(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
ModelNode policy = model.get(Constants.Model.PRINCIPAL_NAME_MAPPING_POLICY);

View file

@ -20,6 +20,7 @@ import org.jboss.as.controller.AttributeDefinition;
import org.jboss.as.controller.ListAttributeDefinition;
import org.jboss.as.controller.OperationStepHandler;
import org.jboss.as.controller.PathElement;
import org.jboss.as.controller.PropertiesAttributeDefinition;
import org.jboss.as.controller.ReloadRequiredRemoveStepHandler;
import org.jboss.as.controller.ReloadRequiredWriteAttributeHandler;
import org.jboss.as.controller.SimpleAttributeDefinition;
@ -83,8 +84,21 @@ public class ServiceProviderDefinition extends SimpleResourceDefinition {
.setAllowNull(true)
.build();
static final SimpleAttributeDefinition[] ATTRIBUTES = {SSL_POLICY, NAME_ID_POLICY_FORMAT, LOGOUT_PAGE, FORCE_AUTHENTICATION, IS_PASSIVE, TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN};
static final AttributeDefinition[] ELEMENTS = {PRINCIPAL_NAME_MAPPING_POLICY, PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME, ROLE_ATTRIBUTES};
static final SimpleAttributeDefinition ROLE_MAPPINGS_PROVIDER_ID =
new SimpleAttributeDefinitionBuilder(Constants.Model.ROLE_MAPPINGS_PROVIDER_ID, ModelType.STRING, true)
.setXmlName(Constants.XML.ID)
.build();
static final PropertiesAttributeDefinition ROLE_MAPPINGS_PROVIDER_CONFIG =
new PropertiesAttributeDefinition.Builder(Constants.Model.ROLE_MAPPINGS_PROVIDER_CONFIG, true)
.setXmlName(Constants.XML.PROPERTY)
.setWrapXmlElement(false)
.build();
static final SimpleAttributeDefinition[] ATTRIBUTES = {SSL_POLICY, NAME_ID_POLICY_FORMAT, LOGOUT_PAGE, FORCE_AUTHENTICATION,
IS_PASSIVE, TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN};
static final AttributeDefinition[] ELEMENTS = {PRINCIPAL_NAME_MAPPING_POLICY, PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME, ROLE_ATTRIBUTES,
ROLE_MAPPINGS_PROVIDER_ID, ROLE_MAPPINGS_PROVIDER_CONFIG};
static final HashMap<String, SimpleAttributeDefinition> ATTRIBUTE_MAP = new HashMap<>();

View file

@ -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

View file

@ -0,0 +1,334 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2016 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.
-->
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="urn:jboss:domain:keycloak-saml:1.2"
xmlns="urn:jboss:domain:keycloak-saml:1.2"
elementFormDefault="qualified"
attributeFormDefault="unqualified"
version="1.0">
<!-- The subsystem root element -->
<xs:element name="subsystem" type="subsystem-type"/>
<xs:complexType name="subsystem-type">
<xs:annotation>
<xs:documentation>
<![CDATA[
The Keycloak SAML adapter subsystem, used to register deployments managed by Keycloak SAML adapter
]]>
</xs:documentation>
</xs:annotation>
<xs:all>
<xs:element name="secure-deployment" minOccurs="0" type="secure-deployment-type"/>
</xs:all>
</xs:complexType>
<xs:complexType name="secure-deployment-type">
<xs:all>
<xs:element name="SP" minOccurs="1" maxOccurs="1" type="sp-type"/>
</xs:all>
<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The name of the realm.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="sp-type">
<xs:all>
<xs:element name="Keys" minOccurs="0" maxOccurs="1" type="keys-type"/>
<xs:element name="PrincipalNameMapping" minOccurs="0" maxOccurs="1" type="principal-name-mapping-type"/>
<xs:element name="RoleIdentifiers" minOccurs="0" maxOccurs="1" type="role-identifiers-type"/>
<xs:element name="RoleMappingsProvider" minOccurs="0" maxOccurs="1" type="role-mappings-provider-type"/>
<xs:element name="IDP" minOccurs="1" maxOccurs="1" type="identity-provider-type"/>
</xs:all>
<xs:attribute name="entityID" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The entity ID for SAML service provider</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="sslPolicy" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The ssl policy</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="nameIDPolicyFormat" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Name ID policy format URN</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="logoutPage" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>URI to a logout page</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="forceAuthentication" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Redirected unauthenticated request to a login page</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="isPassive" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>If user isn't logged in just return with an error. Used to check if a user is already logged in or not</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="turnOffChangeSessionIdOnLogin" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>The session id is changed by default on a successful login. Change this to true if you want to turn this off</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="identity-provider-type">
<xs:all minOccurs="1" maxOccurs="1">
<xs:element name="SingleSignOnService" minOccurs="1" maxOccurs="1" type="single-signon-type"/>
<xs:element name="SingleLogoutService" minOccurs="0" maxOccurs="1" type="single-logout-type"/>
<xs:element name="Keys" minOccurs="0" maxOccurs="1" type="keys-type"/>
</xs:all>
<xs:attribute name="entityID" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The entity ID for SAML service provider</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="signaturesRequired" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Require signatures for single-sign-on and single-logout</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="signatureAlgorithm" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Algorithm used for signatures</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="signatureCanonicalizationMethod" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Canonicalization method used for signatures</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="single-signon-type">
<xs:attribute name="signRequest" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Sign the SSO requests</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Validate the SSO response signature</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="validateAssertionSignature" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Validate the SSO assertion signature</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="requestBinding" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>HTTP method to use for requests</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="responseBinding" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>HTTP method to use for response</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="bindingUrl" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>SSO endpoint URL</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="assertionConsumerServiceUrl" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Endpoint of Assertion Consumer Service at SP</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="single-logout-type">
<xs:attribute name="validateRequestSignature" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Validate a single-logout request signature</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Validate a single-logout response signature</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="signRequest" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Sign single-logout requests</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="signResponse" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Sign single-logout responses</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="requestBinding" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>HTTP method to use for request</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="responseBinding" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>HTTP method to use for response</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="postBindingUrl" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Endpoint URL for posting</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="redirectBindingUrl" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Endpoint URL for redirects</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="keys-type">
<xs:sequence>
<xs:element name="Key" minOccurs="1" maxOccurs="2" type="key-type"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="key-type">
<xs:all>
<xs:element name="KeyStore" minOccurs="0" maxOccurs="1" type="keystore-type"/>
<xs:element name="PrivateKeyPem" minOccurs="0" maxOccurs="1" type="xs:string"/>
<xs:element name="PublicKeyPem" minOccurs="0" maxOccurs="1" type="xs:string"/>
<xs:element name="CertificatePem" minOccurs="0" maxOccurs="1" type="xs:string"/>
</xs:all>
<xs:attribute name="signing" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Key can be used for signing</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="encryption" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Key can be used for encryption</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="keystore-type">
<xs:sequence minOccurs="0" maxOccurs="1">
<xs:element name="PrivateKey" minOccurs="0" maxOccurs="1" type="privatekey-type"/>
<xs:element name="Certificate" minOccurs="0" maxOccurs="1" type="certificate-type"/>
</xs:sequence>
<xs:attribute name="file" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Key store filesystem path</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="resource" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Key store resource URI</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="password" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Key store password</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="type" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Key store format</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="alias" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Key alias</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="privatekey-type">
<xs:attribute name="alias" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Private key alias</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="password" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Private key password</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="certificate-type">
<xs:attribute name="alias" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Certificate alias</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="principal-name-mapping-type">
<xs:attribute name="policy" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Principal name mapping policy. Possible values: FROM_NAME_ID</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="attribute" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Name of the attribute to use for principal name mapping</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="role-identifiers-type">
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="Attribute" minOccurs="0" maxOccurs="unbounded" type="attribute-type"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="attribute-type">
<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Role attribute</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="role-mappings-provider-type">
<xs:sequence>
<xs:element name="Property" type="property-type" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Specifies a configuration property for the provider.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
<xs:attribute name="id" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The id of the role mappings provider that is to be used. Example: properties-based-provider.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="property-type">
<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The name (key) of the configuration property.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="value" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The value of the configuration property.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:schema>

View file

@ -308,6 +308,7 @@ public class DefaultSamlDeployment implements SamlDeployment {
private PrivateKey decryptionKey;
private KeyPair signingKeyPair;
private Set<String> 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;
}

View file

@ -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}.
* <p/>
* 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:
*
* <pre>
* <RoleMappingsProvider id="properties-based-role-mapper">
* <Property name="properties.file.location" value="/opt/mappers/roles.properties"/>
* </RoleMappingsProvider>
* </pre>
*
* 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:
*
* <pre>
* <RoleMappingsProvider id="properties-based-role-mapper">
* <Property name="properties.resource.location" value="/WEB-INF/conf/roles.properties"/>
* </RoleMappingsProvider>
* </pre>
*
* 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:
*
* <pre>
* # role to roles mappings
* samlRoleA=jeeRoleX,jeeRoleY
* samlRoleB=
*
* # principal to roles mappings
* kc-user=jeeRoleZ
* </pre>
*
* 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 <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
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<String> map(final String principalName, final Set<String> roles) {
if (this.roleMappings == null || this.roleMappings.isEmpty())
return roles;
Set<String> 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<String>} into which the extracted roles are to be added.
*/
private void extractRolesIntoSet(final String entry, final Set<String> 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);
}
}
}
}
}

View file

@ -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.
* <p/>
* 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.
* <p/>
* 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).
* <p/>
* 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:
*
* <pre>
* ...
* <RoleIdentifiers>
* ...
* </RoleIdentifiers>
* <RoleMappingsProvider id="ldap-based-role-mapper">
* <Property name="connection.url" value="some.url"/>
* <Property name="username" value="some.user"/>
* ...
* </RoleMappingsProvider>
* </pre>
*
* NOTE: The SPI is not yet finished and method signatures are still subject to change in future versions.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
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<String>} containing the final set of roles that are to be assigned to the principal.
*/
Set<String> map(final String principalName, final Set<String> roles);
}

View file

@ -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 <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
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<String, RoleMappingsProvider> 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<String, RoleMappingsProvider> providers, ClassLoader classLoader) {
for (RoleMappingsProvider provider : ServiceLoader.load(RoleMappingsProvider.class, classLoader)) {
logger.debugf("Loaded RoleMappingsProvider %s", provider.getId());
providers.put(provider.getId(), provider);
}
}
}

View file

@ -168,6 +168,13 @@ public interface SamlDeployment {
Set<String> 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

View file

@ -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<String> 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;
}

View file

@ -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;
}

View file

@ -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("")
;

View file

@ -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 <RoleMappingsProvider>} element., represented by the role-mappings-provider-type in the schema.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class RoleMappingsProviderParser extends AbstractKeycloakSamlAdapterV1Parser<SP.RoleMappingsProviderConfig> {
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;
}
}
}

View file

@ -72,6 +72,10 @@ public class SpParser extends AbstractKeycloakSamlAdapterV1Parser<SP> {
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;

View file

@ -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<String> roles = new HashSet<>();
Set<String> roles = new HashSet<>();
MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
MultivaluedHashMap<String, String> 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) {

View file

@ -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

View file

@ -0,0 +1,525 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<xs:schema version="1.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns="urn:keycloak:saml:adapter"
targetNamespace="urn:keycloak:saml:adapter"
elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xs:element name="keycloak-saml-adapter" type="adapter-type"/>
<xs:complexType name="adapter-type">
<xs:annotation>
<xs:documentation>Keycloak SAML Adapter configuration file.</xs:documentation>
</xs:annotation>
<xs:all>
<xs:element name="SP" maxOccurs="1" minOccurs="0" type="sp-type">
<xs:annotation>
<xs:documentation>Describes SAML service provider configuration.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:all>
</xs:complexType>
<xs:complexType name="sp-type">
<xs:all>
<xs:element name="Keys" type="keys-type" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>
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.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="PrincipalNameMapping" type="principal-name-mapping-type" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="RoleIdentifiers" type="role-identifiers-type" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="RoleMappingsProvider" type="role-mappings-provider-type" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="IDP" type="idp-type" minOccurs="1" maxOccurs="1">
<xs:annotation>
<xs:documentation>Describes configuration of SAML identity provider for this service provider.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:all>
<xs:attribute name="entityID" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>This is the identifier for this client. The IDP needs this value to determine who the client is that is communicating with it.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="sslPolicy" type="ssl-policy-type" use="optional">
<xs:annotation>
<xs:documentation>SSL policy the adapter will enforce.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="nameIDPolicyFormat" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="logoutPage" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>URL of the logout page.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="forceAuthentication" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>SAML clients can request that a user is re-authenticated even if they are already logged in at the IDP. Default value is false.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="isPassive" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="turnOffChangeSessionIdOnLogin" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="autodetectBearerOnly" type="xs:boolean" use="optional" default="false">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="keys-type">
<xs:sequence>
<xs:element name="Key" type="key-type" minOccurs="1" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Describes a single key used for signing or encryption.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
<xs:complexType name="key-type">
<xs:all>
<xs:element name="KeyStore" maxOccurs="1" minOccurs="0" type="key-store-type">
<xs:annotation>
<xs:documentation>Java keystore to load keys and certificates from.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="PrivateKeyPem" type="xs:string" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Private key (PEM format)</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="PublicKeyPem" type="xs:string" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Public key (PEM format)</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="CertificatePem" type="xs:string" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Certificate key (PEM format)</xs:documentation>
</xs:annotation>
</xs:element>
</xs:all>
<xs:attribute name="signing" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Flag defining whether the key should be used for signing.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="encryption" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Flag defining whether the key should be used for encryption</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="key-store-type">
<xs:all>
<xs:element name="PrivateKey" maxOccurs="1" minOccurs="0" type="private-key-type">
<xs:annotation>
<xs:documentation>Private key declaration</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="Certificate" type="certificate-type" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Certificate declaration</xs:documentation>
</xs:annotation>
</xs:element>
</xs:all>
<xs:attribute name="file" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>File path to the key store.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="resource" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>WAR resource path to the key store. This is a path used in method call to ServletContext.getResourceAsStream().</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="password" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The password of the key store.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="private-key-type">
<xs:attribute name="alias" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Alias that points to the key or cert within the keystore.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="password" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Keystores require an additional password to access private keys. In the PrivateKey element you must define this password within a password attribute.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="certificate-type">
<xs:attribute name="alias" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Alias that points to the key or cert within the keystore.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="principal-name-mapping-type">
<xs:attribute name="policy" type="principal-name-mapping-policy-type" use="required">
<xs:annotation>
<xs:documentation>Policy used to populate value of Java Principal object obtained from methods like HttpServletRequest.getUserPrincipal().</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="attribute" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Name of the SAML assertion attribute to use within.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:simpleType name="principal-name-mapping-policy-type">
<xs:restriction base="xs:string">
<xs:enumeration value="FROM_NAME_ID">
<xs:annotation>
<xs:documentation>This policy just uses whatever the SAML subject value is. This is the default setting</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="FROM_ATTRIBUTE">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:enumeration>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="ssl-policy-type">
<xs:restriction base="xs:string">
<xs:enumeration value="ALL">
<xs:annotation>
<xs:documentation>All requests must come in via HTTPS.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="EXTERNAL">
<xs:annotation>
<xs:documentation>Only non-private IP addresses must come over the wire via HTTPS.</xs:documentation>
</xs:annotation>
</xs:enumeration>
<xs:enumeration value="NONE">
<xs:annotation>
<xs:documentation>no requests are required to come over via HTTPS.</xs:documentation>
</xs:annotation>
</xs:enumeration>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="signature-algorithm-type">
<xs:restriction base="xs:string">
<xs:enumeration value="RSA_SHA1"/>
<xs:enumeration value="RSA_SHA256"/>
<xs:enumeration value="RSA_SHA512"/>
<xs:enumeration value="DSA_SHA1"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="binding-type">
<xs:restriction base="xs:string">
<xs:enumeration value="POST"/>
<xs:enumeration value="REDIRECT"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="role-identifiers-type">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="Attribute" maxOccurs="unbounded" minOccurs="0" type="attribute-type">
<xs:annotation>
<xs:documentation>Specifies SAML attribute to be converted into roles.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:choice>
</xs:complexType>
<xs:complexType name="attribute-type">
<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Specifies name of the SAML attribute to be converted into roles.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="role-mappings-provider-type">
<xs:sequence>
<xs:element name="Property" type="property-type" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Specifies a configuration property for the provider.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
<xs:attribute name="id" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The id of the role mappings provider that is to be used. Example: properties-based-provider.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="property-type">
<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The name (key) of the configuration property.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="value" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The value of the configuration property.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="idp-type">
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="SingleSignOnService" maxOccurs="1" minOccurs="1" type="sign-on-type">
<xs:annotation>
<xs:documentation>Configuration of the login SAML endpoint of the IDP.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="SingleLogoutService" type="logout-type" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Configuration of the logout SAML endpoint of the IDP</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="Keys" type="keys-type" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="HttpClient" type="http-client-type" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>Configuration of HTTP client used for automatic obtaining of certificates containing public keys for IDP signature verification via SAML descriptor of the IDP.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="AllowedClockSkew" type="allowed-clock-skew-type" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>This defines the allowed clock skew between IDP and SP in milliseconds. The default value is 0.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
<xs:attribute name="entityID" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>issuer ID of the IDP.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="signaturesRequired" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="signatureAlgorithm" type="signature-algorithm-type" use="optional">
<xs:annotation>
<xs:documentation>Signature algorithm that the IDP expects signed documents to use. Defaults to RSA_SHA256</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="signatureCanonicalizationMethod" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="encryption" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation></xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="metadataUrl" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="sign-on-type">
<xs:attribute name="signRequest" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Should the client sign authn requests? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="validateAssertionSignature" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="requestBinding" type="binding-type" use="optional">
<xs:annotation>
<xs:documentation>SAML binding type used for communicating with the IDP. The default value is POST, but you can set it to REDIRECT as well.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="responseBinding" type="binding-type" use="optional">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="bindingUrl" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>This is the URL for the IDP login service that the client will send requests to.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="assertionConsumerServiceUrl" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="logout-type">
<xs:attribute name="signRequest" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Should the client sign authn requests? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="signResponse" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Should the client sign logout responses it sends to the IDP requests? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="validateRequestSignature" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Should the client expect signed logout request documents from the IDP? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Should the client expect signed logout response documents from the IDP? Defaults to whatever the IDP signaturesRequired element value is.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="requestBinding" type="binding-type" use="optional">
<xs:annotation>
<xs:documentation>This is the SAML binding type used for communicating SAML requests to the IDP. The default value is POST.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="responseBinding" type="binding-type" use="optional">
<xs:annotation>
<xs:documentation>This is the SAML binding type used for communicating SAML responses to the IDP. The default value is POST.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="postBindingUrl" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>This is the URL for the IDP's logout service when using the POST binding. This setting is REQUIRED if using the POST binding.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="redirectBindingUrl" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>This is the URL for the IDP's logout service when using the REDIRECT binding. This setting is REQUIRED if using the REDIRECT binding.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="http-client-type">
<xs:attribute name="allowAnyHostname" type="xs:boolean" use="optional" default="false">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="clientKeystore" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="clientKeystorePassword" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Password for the client keystore and for the client's key.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="connectionPoolSize" type="xs:int" use="optional" default="10">
<xs:annotation>
<xs:documentation>Defines number of pooled connections.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="disableTrustManager" type="xs:boolean" use="optional" default="false">
<xs:annotation>
<xs:documentation>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.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="proxyUrl" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>URL to HTTP proxy to use for HTTP connections.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="truststore" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>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.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="truststorePassword" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Password for the truststore keystore.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="allowed-clock-skew-type">
<xs:annotation>
<xs:documentation>The value is the allowed clock skew between the IDP and the SP.</xs:documentation>
</xs:annotation>
<xs:simpleContent>
<xs:extension base="xs:positiveInteger">
<xs:attribute name="unit" type="clock-skew-unit-type"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
<xs:simpleType name="clock-skew-unit-type">
<xs:annotation>
<xs:documentation>Time unit for the value of the clock skew.</xs:documentation>
</xs:annotation>
<xs:restriction base="xs:string">
<xs:enumeration value="MINUTES" />
<xs:enumeration value="SECONDS" />
<xs:enumeration value="MILLISECONDS" />
<xs:enumeration value="MICROSECONDS" />
<xs:enumeration value="NANOSECONDS" />
</xs:restriction>
</xs:simpleType>
</xs:schema>

View file

@ -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 <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
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<String> samlRoles = new HashSet<>(Arrays.asList(new String[]{"samlRoleA", "samlRoleB", "samlRoleC"}));
final Set<String> 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<String> expectedRoles = new HashSet<>(Arrays.asList(new String[]{"samlRoleC", "jeeRoleX", "jeeRoleY", "jeeRoleZ"}));
assertEquals(expectedRoles, mappedRoles);
}
}

View file

@ -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"));
}
}

View file

@ -0,0 +1,57 @@
<!--
~ 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.
-->
<keycloak-saml-adapter xmlns="urn:keycloak:saml:adapter"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:keycloak:saml:adapter http://www.keycloak.org/schema/keycloak_saml_adapter_1_12.xsd">
<SP entityID="sp"
sslPolicy="EXTERNAL"
nameIDPolicyFormat="format"
forceAuthentication="true"
isPassive="true">
<PrincipalNameMapping policy="FROM_ATTRIBUTE" attribute="attribute"/>
<RoleIdentifiers>
<Attribute name="member"/>
</RoleIdentifiers>
<RoleMappingsProvider id="properties-based-role-mapper">
<Property name="properties.resource.location" value="role-mappings.properties"/>
<Property name="another.property" value="another.value"/>
</RoleMappingsProvider>
<IDP entityID="idp"
signatureAlgorithm="RSA_SHA256"
signatureCanonicalizationMethod="canon"
signaturesRequired="true"
>
<SingleSignOnService signRequest="true"
validateResponseSignature="true"
requestBinding="POST"
bindingUrl="url"
/>
<SingleLogoutService
validateRequestSignature="true"
validateResponseSignature="true"
signRequest="false"
signResponse="true"
requestBinding="REDIRECT"
responseBinding="POST"
postBindingUrl="posturl"
redirectBindingUrl="redirecturl"
/>
</IDP>
</SP>
</keycloak-saml-adapter>

View file

@ -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

View file

@ -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";
}
}

View file

@ -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);
}
/**

View file

@ -116,6 +116,8 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
readPrincipalNameMapping(addServiceProvider, reader);
} else if (Constants.XML.ROLE_IDENTIFIERS.equals(tagName)) {
readRoleIdentifiers(addServiceProvider, reader);
} else if (Constants.XML.ROLE_MAPPINGS_PROVIDER.equals(tagName)) {
readRoleMappingsProvider(addServiceProvider, reader);
} else if (Constants.XML.IDENTITY_PROVIDER.equals(tagName)) {
readIdentityProvider(list, reader, addr);
} else {
@ -337,6 +339,21 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
}
}
void readRoleMappingsProvider(final ModelNode addServiceProvider, final XMLExtendedStreamReader reader) throws XMLStreamException {
String providerId = readRequiredAttribute(reader, Constants.XML.ID);
ServiceProviderDefinition.ROLE_MAPPINGS_PROVIDER_ID.parseAndSetParameter(providerId, addServiceProvider, reader);
while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
String tagName = reader.getLocalName();
if (!Constants.XML.PROPERTY.equals(tagName)) {
throw ParseUtils.unexpectedElement(reader);
}
final String[] array = ParseUtils.requireAttributes(reader, Constants.XML.NAME, Constants.XML.VALUE);
ServiceProviderDefinition.ROLE_MAPPINGS_PROVIDER_CONFIG.parseAndAddParameterElement(array[0], array[1], addServiceProvider, reader);
ParseUtils.requireNoContent(reader);
}
}
void readPrincipalNameMapping(ModelNode addServiceProvider, XMLExtendedStreamReader reader) throws XMLStreamException {
boolean policySet = false;
@ -384,7 +401,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
*/
@Override
public void writeContent(final XMLExtendedStreamWriter writer, final SubsystemMarshallingContext context) throws XMLStreamException {
context.startSubsystemElement(KeycloakSamlExtension.NAMESPACE, false);
context.startSubsystemElement(KeycloakSamlExtension.CURRENT_NAMESPACE, false);
writeSecureDeployment(writer, context.getModelNode());
writer.writeEndElement();
}
@ -417,6 +434,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
writeKeys(writer, spAttributes.get(Constants.Model.KEY));
writePrincipalNameMapping(writer, spAttributes);
writeRoleIdentifiers(writer, spAttributes);
writeRoleMappingsProvider(writer, spAttributes);
writeIdentityProvider(writer, spAttributes.get(Constants.Model.IDENTITY_PROVIDER));
writer.writeEndElement();
@ -552,6 +570,17 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
writer.writeEndElement();
}
void writeRoleMappingsProvider(final XMLExtendedStreamWriter writer, final ModelNode model) throws XMLStreamException {
ModelNode providerId = model.get(Constants.Model.ROLE_MAPPINGS_PROVIDER_ID);
if (!providerId.isDefined()) {
return;
}
writer.writeStartElement(Constants.XML.ROLE_MAPPINGS_PROVIDER);
writer.writeAttribute(Constants.XML.ID, providerId.asString());
ServiceProviderDefinition.ROLE_MAPPINGS_PROVIDER_CONFIG.marshallAsElement(model, false, writer);
writer.writeEndElement();
}
void writePrincipalNameMapping(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
ModelNode policy = model.get(Constants.Model.PRINCIPAL_NAME_MAPPING_POLICY);

View file

@ -17,9 +17,12 @@
package org.keycloak.subsystem.adapter.saml.extension;
import org.jboss.as.controller.AttributeDefinition;
import org.jboss.as.controller.AttributeMarshallers;
import org.jboss.as.controller.AttributeParsers;
import org.jboss.as.controller.ListAttributeDefinition;
import org.jboss.as.controller.OperationStepHandler;
import org.jboss.as.controller.PathElement;
import org.jboss.as.controller.PropertiesAttributeDefinition;
import org.jboss.as.controller.ReloadRequiredRemoveStepHandler;
import org.jboss.as.controller.ReloadRequiredWriteAttributeHandler;
import org.jboss.as.controller.SimpleAttributeDefinition;
@ -83,8 +86,20 @@ public class ServiceProviderDefinition extends SimpleResourceDefinition {
.setAllowNull(true)
.build();
static final SimpleAttributeDefinition[] ATTRIBUTES = {SSL_POLICY, NAME_ID_POLICY_FORMAT, LOGOUT_PAGE, FORCE_AUTHENTICATION, IS_PASSIVE, TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN};
static final AttributeDefinition[] ELEMENTS = {PRINCIPAL_NAME_MAPPING_POLICY, PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME, ROLE_ATTRIBUTES};
static final SimpleAttributeDefinition ROLE_MAPPINGS_PROVIDER_ID =
new SimpleAttributeDefinitionBuilder(Constants.Model.ROLE_MAPPINGS_PROVIDER_ID, ModelType.STRING, true)
.setXmlName(Constants.XML.ID)
.build();
static final PropertiesAttributeDefinition ROLE_MAPPINGS_PROVIDER_CONFIG =
new PropertiesAttributeDefinition.Builder(Constants.Model.ROLE_MAPPINGS_PROVIDER_CONFIG, true)
.setAttributeMarshaller(new AttributeMarshallers.PropertiesAttributeMarshaller(null, Constants.XML.PROPERTY, false))
.build();
static final SimpleAttributeDefinition[] ATTRIBUTES = {SSL_POLICY, NAME_ID_POLICY_FORMAT, LOGOUT_PAGE, FORCE_AUTHENTICATION,
IS_PASSIVE, TURN_OFF_CHANGE_SESSSION_ID_ON_LOGIN};
static final AttributeDefinition[] ELEMENTS = {PRINCIPAL_NAME_MAPPING_POLICY, PRINCIPAL_NAME_MAPPING_ATTRIBUTE_NAME, ROLE_ATTRIBUTES,
ROLE_MAPPINGS_PROVIDER_ID, ROLE_MAPPINGS_PROVIDER_CONFIG};
static final HashMap<String, SimpleAttributeDefinition> ATTRIBUTE_MAP = new HashMap<>();

View file

@ -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

View file

@ -0,0 +1,334 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="urn:jboss:domain:keycloak-saml:1.2"
xmlns="urn:jboss:domain:keycloak-saml:1.2"
elementFormDefault="qualified"
attributeFormDefault="unqualified"
version="1.0">
<!-- The subsystem root element -->
<xs:element name="subsystem" type="subsystem-type"/>
<xs:complexType name="subsystem-type">
<xs:annotation>
<xs:documentation>
<![CDATA[
The Keycloak SAML adapter subsystem, used to register deployments managed by Keycloak SAML adapter
]]>
</xs:documentation>
</xs:annotation>
<xs:all>
<xs:element name="secure-deployment" minOccurs="0" type="secure-deployment-type"/>
</xs:all>
</xs:complexType>
<xs:complexType name="secure-deployment-type">
<xs:all>
<xs:element name="SP" minOccurs="1" maxOccurs="1" type="sp-type"/>
</xs:all>
<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The name of the realm.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="sp-type">
<xs:all>
<xs:element name="Keys" minOccurs="0" maxOccurs="1" type="keys-type"/>
<xs:element name="PrincipalNameMapping" minOccurs="0" maxOccurs="1" type="principal-name-mapping-type"/>
<xs:element name="RoleIdentifiers" minOccurs="0" maxOccurs="1" type="role-identifiers-type"/>
<xs:element name="RoleMappingsProvider" minOccurs="0" maxOccurs="1" type="role-mappings-provider-type"/>
<xs:element name="IDP" minOccurs="1" maxOccurs="1" type="identity-provider-type"/>
</xs:all>
<xs:attribute name="entityID" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The entity ID for SAML service provider</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="sslPolicy" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The ssl policy</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="nameIDPolicyFormat" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Name ID policy format URN</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="logoutPage" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>URI to a logout page</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="forceAuthentication" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Redirected unauthenticated request to a login page</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="isPassive" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>If user isn't logged in just return with an error. Used to check if a user is already logged in or not</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="turnOffChangeSessionIdOnLogin" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>The session id is changed by default on a successful login. Change this to true if you want to turn this off</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="identity-provider-type">
<xs:all minOccurs="1" maxOccurs="1">
<xs:element name="SingleSignOnService" minOccurs="1" maxOccurs="1" type="single-signon-type"/>
<xs:element name="SingleLogoutService" minOccurs="0" maxOccurs="1" type="single-logout-type"/>
<xs:element name="Keys" minOccurs="0" maxOccurs="1" type="keys-type"/>
</xs:all>
<xs:attribute name="entityID" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The entity ID for SAML service provider</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="signaturesRequired" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Require signatures for single-sign-on and single-logout</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="signatureAlgorithm" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Algorithm used for signatures</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="signatureCanonicalizationMethod" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Canonicalization method used for signatures</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="single-signon-type">
<xs:attribute name="signRequest" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Sign the SSO requests</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Validate the SSO response signature</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="validateAssertionSignature" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Validate the SSO assertion signature</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="requestBinding" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>HTTP method to use for requests</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="responseBinding" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>HTTP method to use for response</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="bindingUrl" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>SSO endpoint URL</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="assertionConsumerServiceUrl" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Endpoint of Assertion Consumer Service at SP</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="single-logout-type">
<xs:attribute name="validateRequestSignature" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Validate a single-logout request signature</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="validateResponseSignature" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Validate a single-logout response signature</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="signRequest" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Sign single-logout requests</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="signResponse" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Sign single-logout responses</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="requestBinding" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>HTTP method to use for request</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="responseBinding" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>HTTP method to use for response</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="postBindingUrl" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Endpoint URL for posting</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="redirectBindingUrl" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Endpoint URL for redirects</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="keys-type">
<xs:sequence>
<xs:element name="Key" minOccurs="1" maxOccurs="2" type="key-type"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="key-type">
<xs:all>
<xs:element name="KeyStore" minOccurs="0" maxOccurs="1" type="keystore-type"/>
<xs:element name="PrivateKeyPem" minOccurs="0" maxOccurs="1" type="xs:string"/>
<xs:element name="PublicKeyPem" minOccurs="0" maxOccurs="1" type="xs:string"/>
<xs:element name="CertificatePem" minOccurs="0" maxOccurs="1" type="xs:string"/>
</xs:all>
<xs:attribute name="signing" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Key can be used for signing</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="encryption" type="xs:boolean" use="optional">
<xs:annotation>
<xs:documentation>Key can be used for encryption</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="keystore-type">
<xs:sequence minOccurs="0" maxOccurs="1">
<xs:element name="PrivateKey" minOccurs="0" maxOccurs="1" type="privatekey-type"/>
<xs:element name="Certificate" minOccurs="0" maxOccurs="1" type="certificate-type"/>
</xs:sequence>
<xs:attribute name="file" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Key store filesystem path</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="resource" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Key store resource URI</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="password" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Key store password</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="type" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Key store format</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="alias" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Key alias</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="privatekey-type">
<xs:attribute name="alias" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Private key alias</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="password" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Private key password</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="certificate-type">
<xs:attribute name="alias" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Certificate alias</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="principal-name-mapping-type">
<xs:attribute name="policy" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Principal name mapping policy. Possible values: FROM_NAME_ID</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="attribute" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>Name of the attribute to use for principal name mapping</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="role-identifiers-type">
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="Attribute" minOccurs="0" maxOccurs="unbounded" type="attribute-type"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="attribute-type">
<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>Role attribute</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="role-mappings-provider-type">
<xs:sequence>
<xs:element name="Property" type="property-type" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>Specifies a configuration property for the provider.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
<xs:attribute name="id" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The id of the role mappings provider that is to be used. Example: properties-based-provider.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
<xs:complexType name="property-type">
<xs:attribute name="name" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The name (key) of the configuration property.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="value" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The value of the configuration property.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:schema>

View file

@ -19,6 +19,6 @@
<!-- Template used by WildFly build when directed to include Keycloak SAML subsystem in a configuration. -->
<config>
<extension-module>org.keycloak.keycloak-saml-adapter-subsystem</extension-module>
<subsystem xmlns="urn:jboss:domain:keycloak-saml:1.1">
<subsystem xmlns="urn:jboss:domain:keycloak-saml:1.2">
</subsystem>
</config>

View file

@ -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

View file

@ -0,0 +1,75 @@
<!--
~ 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.
-->
<subsystem xmlns="urn:jboss:domain:keycloak-saml:1.2">
<secure-deployment name="my-app.war">
<SP entityID="http://localhost:8080/sales-post-enc/"
sslPolicy="EXTERNAL"
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
logoutPage="/logout.jsp"
forceAuthentication="false"
isPassive="true"
turnOffChangeSessionIdOnLogin="true">
<Keys>
<Key encryption="true" signing="true">
<PrivateKeyPem>my_key.pem</PrivateKeyPem>
<PublicKeyPem>my_key.pub</PublicKeyPem>
<CertificatePem>cert.cer</CertificatePem>
<KeyStore resource="/WEB-INF/keystore.jks" password="store123" file="test" alias="test" type="jks">
<PrivateKey alias="http://localhost:8080/sales-post-enc/" password="test123"/>
<Certificate alias="http://localhost:8080/sales-post-enc/"/>
</KeyStore>
</Key>
</Keys>
<PrincipalNameMapping policy="FROM_NAME_ID" attribute="test"/>
<RoleIdentifiers>
<Attribute name="Role"/>
<Attribute name="Role2"/>
</RoleIdentifiers>
<RoleMappingsProvider id="properties-based-role-mapper">
<Property name="properties.file.location" value="test-roles.properties"/>
<Property name="another.property" value="another.value"/>
</RoleMappingsProvider>
<IDP entityID="idp" signaturesRequired="true" signatureAlgorithm="test" signatureCanonicalizationMethod="test">
<SingleSignOnService signRequest="true"
validateResponseSignature="true"
validateAssertionSignature="true"
requestBinding="POST"
responseBinding="POST"
bindingUrl="http://localhost:8080/auth/realms/saml-demo/protocol/saml"
assertionConsumerServiceUrl="acsUrl"/>
<SingleLogoutService
validateRequestSignature="true"
validateResponseSignature="true"
signRequest="true"
signResponse="true"
requestBinding="POST"
responseBinding="POST"
postBindingUrl="http://localhost:8080/auth/realms/saml-demo/protocol/saml"
redirectBindingUrl="http://localhost:8080/auth/realms/saml-demo/protocol/saml"/>
<Keys>
<Key signing="true">
<KeyStore resource="/WEB-INF/keystore.jks" password="store123">
<Certificate alias="saml-demo"/>
</KeyStore>
</Key>
</Keys>
</IDP>
</SP>
</secure-deployment>
</subsystem>

View file

@ -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 = "<html><head><title>Error Page</title></head><body><h1>There was an error</h1>";
if (statusCode != null)

View file

@ -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 <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
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);
}
}
}

View file

@ -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<String> 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;
}
}

View file

@ -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"))

View file

@ -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);
}
}
}

View file

@ -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();
}

View file

@ -0,0 +1,47 @@
<!--
~ 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.
-->
<keycloak-saml-adapter xmlns="urn:keycloak:saml:adapter"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:keycloak:saml:adapter http://www.keycloak.org/schema/keycloak_saml_adapter_1_12.xsd">
<SP entityID="http://localhost:8280/employee-role-mapping/"
sslPolicy="EXTERNAL"
nameIDPolicyFormat="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
logoutPage="/logout.jsp"
forceAuthentication="false">
<PrincipalNameMapping policy="FROM_ATTRIBUTE" attribute="email"/>
<RoleIdentifiers>
<Attribute name="memberOf"/>
<Attribute name="Role"/>
</RoleIdentifiers>
<RoleMappingsProvider id="properties-based-role-mapper">
<Property name="properties.resource.location" value="/WEB-INF/role-mappings.properties"/>
</RoleMappingsProvider>
<IDP entityID="idp">
<SingleSignOnService requestBinding="POST"
bindingUrl="http://localhost:8080/auth/realms/demo/protocol/saml"
/>
<SingleLogoutService
requestBinding="POST"
responseBinding="POST"
postBindingUrl="http://localhost:8080/auth/realms/demo/protocol/saml"
redirectBindingUrl="http://localhost:8080/auth/realms/demo/protocol/saml"
/>
</IDP>
</SP>
</keycloak-saml-adapter>

View file

@ -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

View file

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<module-name>%CONTEXT_PATH%</module-name>
<servlet>
<servlet-name>javax.ws.rs.core.Application</servlet-name>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>javax.ws.rs.core.Application</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<error-page>
<location>/error.html</location>
</error-page>
<security-constraint>
<web-resource-collection>
<web-resource-name>Application</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>coordinator</role-name>
</auth-constraint>
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>Unsecured-setCheckRoles</web-resource-name>
<url-pattern>/setCheckRoles/*</url-pattern>
</web-resource-collection>
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>Unsecured-uncheckRoles</web-resource-name>
<url-pattern>/uncheckRoles/*</url-pattern>
</web-resource-collection>
</security-constraint>
<login-config>
<auth-method>KEYCLOAK-SAML</auth-method>
<realm-name>demo</realm-name>
</login-config>
<security-role>
<role-name>coordinator</role-name>
</security-role>
</web-app>

View file

@ -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"
}
]
}
}
}