saml role list mapper

This commit is contained in:
Bill Burke 2015-03-07 19:47:34 -05:00
parent 1de285b724
commit 5c6c30fef4
14 changed files with 344 additions and 78 deletions

View file

@ -19,10 +19,8 @@ import org.picketlink.identity.federation.saml.v2.assertion.AuthnStatementType;
import org.picketlink.identity.federation.saml.v2.protocol.ResponseType;
import org.w3c.dom.Document;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import static org.picketlink.common.util.StringUtil.isNotNull;
@ -36,7 +34,6 @@ import static org.picketlink.common.util.StringUtil.isNotNull;
public class SALM2LoginResponseBuilder {
protected static final PicketLinkLogger logger = PicketLinkLoggerFactory.getLogger();
protected List<String> roles = new LinkedList<String>();
protected String destination;
protected String issuer;
protected String nameId;
@ -68,18 +65,6 @@ public class SALM2LoginResponseBuilder {
return this;
}
public SALM2LoginResponseBuilder roles(List<String> roles) {
this.roles = roles;
return this;
}
public SALM2LoginResponseBuilder roles(String... roles) {
for (String role : roles) {
this.roles.add(role);
}
return this;
}
public SALM2LoginResponseBuilder authMethod(String authMethod) {
this.authMethod = authMethod;
return this;
@ -155,12 +140,6 @@ public class SALM2LoginResponseBuilder {
assertion.addStatement(authnStatement);
}
if (roles != null && !roles.isEmpty()) {
AttributeStatementType attrStatement = StatementUtil.createAttributeStatementForRoles(roles, multiValuedRoles);
assertion.addStatement(attrStatement);
}
return responseType;
}

View file

@ -19,6 +19,7 @@ import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.saml.mappers.SAMLAttributeStatementMapper;
import org.keycloak.protocol.saml.mappers.SAMLLoginResponseMapper;
import org.keycloak.protocol.saml.mappers.SAMLRoleListMapper;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.resources.RealmsResource;
@ -29,7 +30,6 @@ import org.picketlink.common.constants.JBossSAMLURIConstants;
import org.picketlink.common.exceptions.ConfigurationException;
import org.picketlink.common.exceptions.ParsingException;
import org.picketlink.common.exceptions.ProcessingException;
import org.picketlink.identity.federation.core.saml.v2.constants.X500SAMLProfileConstants;
import org.picketlink.identity.federation.saml.v2.assertion.AssertionType;
import org.picketlink.identity.federation.saml.v2.assertion.AttributeStatementType;
import org.picketlink.identity.federation.saml.v2.protocol.ResponseType;
@ -41,6 +41,8 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.security.PublicKey;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
@ -258,26 +260,38 @@ public class SamlProtocol implements LoginProtocol {
.requestIssuer(clientSession.getClient().getClientId())
.nameIdentifier(nameIdFormat, nameId)
.authMethod(JBossSAMLURIConstants.AC_UNSPECIFIED.get());
initClaims(builder, clientSession.getClient(), userSession.getUser());
if (clientSession.getRoles() != null) {
if (multivaluedRoles(client)) {
builder.multiValuedRoles(true);
}
for (String roleId : clientSession.getRoles()) {
// todo need a role mapping
RoleModel roleModel = clientSession.getRealm().getRoleById(roleId);
builder.roles(roleModel.getName());
}
}
if (!includeAuthnStatement(client)) {
builder.disableAuthnStatement(true);
}
List<ProtocolMapperProcessor<SAMLAttributeStatementMapper>> attributeStatementMappers = new LinkedList<>();
List<ProtocolMapperProcessor<SAMLLoginResponseMapper>> loginResponseMappers = new LinkedList<>();
ProtocolMapperProcessor<SAMLRoleListMapper> roleListMapper = null;
Set<ProtocolMapperModel> mappings = client.getProtocolMappers();
for (ProtocolMapperModel mapping : mappings) {
if (!mapping.getProtocol().equals(SamlProtocol.LOGIN_PROTOCOL)) continue;
ProtocolMapper mapper = (ProtocolMapper)session.getKeycloakSessionFactory().getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
if (mapper == null) continue;
if (mapper instanceof SAMLAttributeStatementMapper) {
attributeStatementMappers.add(new ProtocolMapperProcessor<SAMLAttributeStatementMapper>((SAMLAttributeStatementMapper)mapper, mapping));
}
if (mapper instanceof SAMLLoginResponseMapper) {
loginResponseMappers.add(new ProtocolMapperProcessor<SAMLLoginResponseMapper>((SAMLLoginResponseMapper)mapper, mapping));
}
if (mapper instanceof SAMLRoleListMapper) {
roleListMapper = new ProtocolMapperProcessor<SAMLRoleListMapper>((SAMLRoleListMapper)mapper, mapping);
}
}
Document samlDocument = null;
try {
ResponseType samlModel = builder.buildModel();
transformAttributeStatement(session, samlModel, client, userSession, clientSession);
samlModel = transformLoginResponse(session, samlModel, client, userSession, clientSession);
transformAttributeStatement(attributeStatementMappers, samlModel, session, userSession, clientSession);
populateRoles(roleListMapper, samlModel, session, userSession, clientSession);
samlModel = transformLoginResponse(loginResponseMappers, samlModel, session, userSession, clientSession);
samlDocument = builder.buildDocument(samlModel);
} catch (Exception e) {
logger.error("failed", e);
@ -348,52 +362,48 @@ public class SamlProtocol implements LoginProtocol {
return "true".equals(client.getAttribute(SAML_ENCRYPT));
}
public void initClaims(SALM2LoginResponseBuilder builder, ClientModel model, UserModel user) {
if (ClaimMask.hasEmail(model.getAllowedClaimsMask())) {
//builder.attribute(X500SAMLProfileConstants.EMAIL_ADDRESS.getFriendlyName(), user.getEmail());
}
if (ClaimMask.hasName(model.getAllowedClaimsMask())) {
//builder.attribute(X500SAMLProfileConstants.GIVEN_NAME.getFriendlyName(), user.getFirstName());
//builder.attribute(X500SAMLProfileConstants.SURNAME.getFriendlyName(), user.getLastName());
}
if (ClaimMask.hasUsername(model.getAllowedClaimsMask())) {
//builder.attribute(X500SAMLProfileConstants.USERID.getFriendlyName(), user.getUsername());
public static class ProtocolMapperProcessor<T> {
final public T mapper;
final public ProtocolMapperModel model;
public ProtocolMapperProcessor(T mapper, ProtocolMapperModel model) {
this.mapper = mapper;
this.model = model;
}
}
public ResponseType transformLoginResponse(KeycloakSession session, ResponseType response, ClientModel client,
public void transformAttributeStatement(List<ProtocolMapperProcessor<SAMLAttributeStatementMapper>> attributeStatementMappers,
ResponseType response,
KeycloakSession session,
UserSessionModel userSession, ClientSessionModel clientSession) {
Set<ProtocolMapperModel> mappings = client.getProtocolMappers();
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
for (ProtocolMapperModel mapping : mappings) {
if (!mapping.getProtocol().equals(SamlProtocol.LOGIN_PROTOCOL)) continue;
ProtocolMapper mapper = (ProtocolMapper)sessionFactory.getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
if (mapper == null || !(mapper instanceof SAMLLoginResponseMapper)) continue;
response = ((SAMLLoginResponseMapper)mapper).transformLoginResponse(response, mapping, session, userSession, clientSession);
AssertionType assertion = response.getAssertions().get(0).getAssertion();
AttributeStatementType attributeStatement = new AttributeStatementType();
assertion.addStatement(attributeStatement);
for (ProtocolMapperProcessor<SAMLAttributeStatementMapper> processor : attributeStatementMappers) {
processor.mapper.transformAttributeStatement(attributeStatement, processor.model, session, userSession, clientSession);
}
}
public ResponseType transformLoginResponse(List<ProtocolMapperProcessor<SAMLLoginResponseMapper>> mappers,
ResponseType response,
KeycloakSession session,
UserSessionModel userSession, ClientSessionModel clientSession) {
for (ProtocolMapperProcessor<SAMLLoginResponseMapper> processor : mappers) {
response = processor.mapper.transformLoginResponse(response, processor.model, session, userSession, clientSession);
}
return response;
}
public void transformAttributeStatement(KeycloakSession session, ResponseType response, ClientModel client,
public void populateRoles(ProtocolMapperProcessor<SAMLRoleListMapper> roleListMapper,
ResponseType response,
KeycloakSession session,
UserSessionModel userSession, ClientSessionModel clientSession) {
AttributeStatementType attributeStatement = new AttributeStatementType();
if (roleListMapper == null) return;
AssertionType assertion = response.getAssertions().get(0).getAssertion();
AttributeStatementType attributeStatement = new AttributeStatementType();
assertion.addStatement(attributeStatement);
Set<ProtocolMapperModel> mappings = client.getProtocolMappers();
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
for (ProtocolMapperModel mapping : mappings) {
if (!mapping.getProtocol().equals(SamlProtocol.LOGIN_PROTOCOL)) continue;
ProtocolMapper mapper = (ProtocolMapper)sessionFactory.getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
if (mapper == null || !(mapper instanceof SAMLAttributeStatementMapper)) continue;
((SAMLAttributeStatementMapper)mapper).transformAttributeStatement(attributeStatement, mapping, session, userSession, clientSession);
roleListMapper.mapper.mapRoles(attributeStatement, roleListMapper.model, session, userSession, clientSession);
}
}
@Override

View file

@ -8,6 +8,8 @@ import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.AbstractLoginProtocolFactory;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
import org.keycloak.protocol.saml.mappers.SAMLBasicRoleListMapper;
import org.keycloak.protocol.saml.mappers.UserPropertyAttributeStatementMapper;
import org.keycloak.services.managers.AuthenticationManager;
import org.picketlink.common.constants.JBossSAMLURIConstants;
@ -75,6 +77,9 @@ public class SamlProtocolFactory extends AbstractLoginProtocolFactory {
X500SAMLProfileConstants.SURNAME.getFriendlyName(),
true, "family name");
builtins.add(model);
model = SAMLBasicRoleListMapper.create("role list", "Role", AttributeStatementHelper.BASIC, null, false);
builtins.add(model);
defaultBuiltins.add(model);
}

View file

@ -1,6 +1,5 @@
package org.keycloak.protocol.saml;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
/**

View file

@ -4,7 +4,6 @@ import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.saml.SamlProtocol;
/**

View file

@ -1,7 +1,6 @@
package org.keycloak.protocol.saml.mappers;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.saml.SamlProtocol;
@ -31,6 +30,12 @@ public class AttributeStatementHelper {
public static void addAttribute(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel,
String attributeValue) {
AttributeType attribute = createAttributeType(mappingModel);
attribute.addAttributeValue(attributeValue);
attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attribute));
}
public static AttributeType createAttributeType(ProtocolMapperModel mappingModel) {
String attributeName = mappingModel.getConfig().get(SAML_ATTRIBUTE_NAME);
AttributeType attribute = new AttributeType(attributeName);
String attributeType = mappingModel.getConfig().get(SAML_ATTRIBUTE_NAMEFORMAT);
@ -40,8 +45,7 @@ public class AttributeStatementHelper {
attribute.setNameFormat(attributeNameFormat);
String friendlyName = mappingModel.getConfig().get(FRIENDLY_NAME);
if (friendlyName != null && !friendlyName.trim().equals("")) attribute.setFriendlyName(friendlyName);
attribute.addAttributeValue(attributeValue);
attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attribute));
return attribute;
}
public static void setConfigProperties(List<ProtocolMapper.ConfigProperty> configProperties) {

View file

@ -5,7 +5,6 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
import org.picketlink.identity.federation.saml.v2.assertion.AttributeStatementType;
import org.picketlink.identity.federation.saml.v2.protocol.ResponseType;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>

View file

@ -0,0 +1,153 @@
package org.keycloak.protocol.saml.mappers;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.saml.SamlProtocol;
import org.picketlink.identity.federation.saml.v2.assertion.AttributeStatementType;
import org.picketlink.identity.federation.saml.v2.assertion.AttributeType;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SAMLBasicRoleListMapper extends AbstractSAMLProtocolMapper implements SAMLRoleListMapper {
public static final String PROVIDER_ID = "saml-role-list-mapper";
public static final String SINGLE_ROLE_ATTRIBUTE = "single";
private static final List<ConfigProperty> configProperties = new ArrayList<ConfigProperty>();
static {
ConfigProperty property;
property = new ConfigProperty();
property.setName(AttributeStatementHelper.SAML_ATTRIBUTE_NAME);
property.setLabel("Role attribute name");
property.setDefaultValue("Role");
property.setHelpText("Name of the SAML attribute you want to put your roles into. i.e. 'Role', 'memberOf'.");
configProperties.add(property);
property = new ProtocolMapper.ConfigProperty();
property.setName(AttributeStatementHelper.FRIENDLY_NAME);
property.setLabel(AttributeStatementHelper.FRIENDLY_NAME_LABEL);
property.setHelpText(AttributeStatementHelper.FRIENDLY_NAME_HELP_TEXT);
configProperties.add(property);
property = new ProtocolMapper.ConfigProperty();
property.setName(AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT);
property.setLabel("SAML Attribute NameFormat");
property.setHelpText("SAML Attribute NameFormat. Can be basic, URI reference, or unspecified.");
List<String> types = new ArrayList(3);
types.add(AttributeStatementHelper.BASIC);
types.add(AttributeStatementHelper.URI_REFERENCE);
types.add(AttributeStatementHelper.UNSPECIFIED);
property.setType(ProtocolMapper.ConfigProperty.LIST_TYPE);
property.setDefaultValue(types);
configProperties.add(property);
property = new ConfigProperty();
property.setName(SINGLE_ROLE_ATTRIBUTE);
property.setLabel("Single Role Attribute");
property.setType(ConfigProperty.BOOLEAN_TYPE);
property.setDefaultValue("true");
property.setHelpText("If true, all roles will be stored under one attribute with multiple attribute values.");
configProperties.add(property);
}
@Override
public String getDisplayCategory() {
return "Role Mapper";
}
@Override
public String getDisplayType() {
return "Role list";
}
@Override
public String getHelpText() {
return "Role names are stored in an attribute value. There is either one attribute with multiple attribute values, or an attribute per role name depending on how you configure it. You can also specify the attribute name i.e. 'Role' or 'memberOf' being examples.";
}
@Override
public List<ConfigProperty> getConfigProperties() {
return configProperties;
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public void mapRoles(AttributeStatementType roleAttributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
String single = mappingModel.getConfig().get(SINGLE_ROLE_ATTRIBUTE);
boolean singleAttribute = Boolean.parseBoolean(single);
Map<ProtocolMapperModel, SAMLRoleNameMapper> roleNameMappers = new HashMap<>();
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
for (ProtocolMapperModel mapping : clientSession.getClient().getProtocolMappers()) {
if (!mapping.getProtocol().equals(SamlProtocol.LOGIN_PROTOCOL)) continue;
ProtocolMapper mapper = (ProtocolMapper)sessionFactory.getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
if (mapper == null || !(mapper instanceof SAMLRoleNameMapper)) continue;
roleNameMappers.put(mapping, (SAMLRoleNameMapper)mapper);
}
AttributeType singleAttributeType = null;
for (String roleId : clientSession.getRoles()) {
// todo need a role mapping
RoleModel roleModel = clientSession.getRealm().getRoleById(roleId);
AttributeType attributeType = null;
if (singleAttribute) {
if (singleAttributeType == null) {
singleAttributeType = AttributeStatementHelper.createAttributeType(mappingModel);
roleAttributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(singleAttributeType));
}
attributeType = singleAttributeType;
} else {
attributeType = AttributeStatementHelper.createAttributeType(mappingModel);
roleAttributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attributeType));
}
String roleName = roleModel.getName();
for (Map.Entry<ProtocolMapperModel, SAMLRoleNameMapper> entry : roleNameMappers.entrySet()) {
String newName = entry.getValue().mapName(entry.getKey(), roleModel);
if (newName != null) {
roleName = newName;
break;
}
}
attributeType.addAttributeValue(roleName);
}
}
public static ProtocolMapperModel create(String name, String samlAttributeName, String nameFormat, String friendlyName, boolean singleAttribute) {
ProtocolMapperModel mapper = new ProtocolMapperModel();
mapper.setName(name);
mapper.setProtocolMapper(PROVIDER_ID);
mapper.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
mapper.setConsentRequired(false);
Map<String, String> config = new HashMap<String, String>();
config.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAME, samlAttributeName);
if (friendlyName != null) {
config.put(AttributeStatementHelper.FRIENDLY_NAME, friendlyName);
}
if (nameFormat != null) {
config.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT, nameFormat);
}
config.put(SINGLE_ROLE_ATTRIBUTE, Boolean.toString(singleAttribute));
mapper.setConfig(config);
return mapper;
}
}

View file

@ -0,0 +1,91 @@
package org.keycloak.protocol.saml.mappers;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RoleContainerModel;
import org.keycloak.models.RoleModel;
import org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper;
import java.util.ArrayList;
import java.util.List;
/**
* Map an assigned role to a different position and name in the token
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SAMLBasicRoleNameMapper extends AbstractOIDCProtocolMapper implements SAMLRoleNameMapper {
private static final List<ConfigProperty> configProperties = new ArrayList<ConfigProperty>();
public static final String ROLE_CONFIG = "role";
public static String NEW_ROLE_NAME = "new.role.name";
static {
ConfigProperty property;
property = new ConfigProperty();
property.setName(ROLE_CONFIG);
property.setLabel("Role");
property.setHelpText("Role name you want changed. To reference an application role the syntax is appname.approle, i.e. myapp.myrole");
property.setType(ConfigProperty.STRING_TYPE);
configProperties.add(property);
property = new ConfigProperty();
property.setName(NEW_ROLE_NAME);
property.setLabel("New Role Name");
property.setHelpText("The new role name.");
property.setType(ConfigProperty.STRING_TYPE);
configProperties.add(property);
}
public static final String PROVIDER_ID = "saml-role-name-mapper";
public List<ConfigProperty> getConfigProperties() {
return configProperties;
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getDisplayType() {
return "Role Name Mapper";
}
@Override
public String getDisplayCategory() {
return "Role Mapper";
}
@Override
public String getHelpText() {
return "Map an assigned role to a new name";
}
@Override
public String mapName(ProtocolMapperModel model, RoleModel roleModel) {
RoleContainerModel container = roleModel.getContainer();
ApplicationModel app = null;
if (container instanceof ApplicationModel) {
app = ((ApplicationModel) container);
}
String role = model.getConfig().get(ROLE_CONFIG);
String newName = model.getConfig().get(NEW_ROLE_NAME);
String appName = null;
int scopeIndex = role.indexOf('.');
if (scopeIndex > -1) {
if (app == null) return null;
appName = role.substring(0, scopeIndex);
if (!app.getName().equals(appName)) return null;
role = role.substring(scopeIndex + 1);
} else {
if (app != null) return null;
}
if (roleModel.getName().equals(role)) return newName;
return null;
}
}

View file

@ -4,7 +4,6 @@ import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.AccessToken;
import org.picketlink.identity.federation.saml.v2.protocol.ResponseType;
/**

View file

@ -0,0 +1,12 @@
package org.keycloak.protocol.saml.mappers;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RoleModel;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface SAMLRoleNameMapper {
public String mapName(ProtocolMapperModel model, RoleModel role);
}

View file

@ -1,3 +1,5 @@
org.keycloak.protocol.saml.mappers.SAMLBasicRoleListMapper
org.keycloak.protocol.saml.mappers.SAMLBasicRoleNameMapper
org.keycloak.protocol.saml.mappers.UserAttributeStatementMapper
org.keycloak.protocol.saml.mappers.UserPropertyAttributeStatementMapper

View file

@ -1,8 +1,11 @@
package org.keycloak.protocol;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserModel;
import java.lang.reflect.Method;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>

View file

@ -256,6 +256,17 @@
"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"
}
}
]
},