Commit ada0273c authored by Gradl, Tobias's avatar Gradl, Tobias
Browse files

10: Work with SAML attributes

Task-Url: #10
parent fb0687c7
Pipeline #17635 passed with stage
in 1 minute and 54 seconds
...@@ -3,6 +3,7 @@ package eu.dariah.de.dariahsp.authenticator; ...@@ -3,6 +3,7 @@ package eu.dariah.de.dariahsp.authenticator;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import eu.dariah.de.dariahsp.config.model.RoleDefinition; import eu.dariah.de.dariahsp.config.model.RoleDefinition;
import eu.dariah.de.dariahsp.model.ExtendedUserProfile; import eu.dariah.de.dariahsp.model.ExtendedUserProfile;
...@@ -32,4 +33,25 @@ public abstract class BaseProfileCreator { ...@@ -32,4 +33,25 @@ public abstract class BaseProfileCreator {
} }
return roles; return roles;
} }
protected void mapAndAssignRoles(ExtendedUserProfile profile) {
if (this.canMapRoles(profile)) {
// Determine applicable role definitions
Set<RoleDefinition> mappedDefinitions = this.getMappedRoles(profile);
// Set applicable roles as Strings
profile.setRoles(mappedDefinitions.stream()
.map(mr -> ROLE_PREFIX + mr.getRole().toUpperCase())
.collect(Collectors.toSet()));
// Set maximum applicable level
profile.setLevel(mappedDefinitions.stream()
.mapToInt(RoleDefinition::getLevel)
.max().orElse(0));
}
}
private boolean canMapRoles(ExtendedUserProfile profile) {
return clientName!=null &&
profile.getExternalRoles()!=null && !profile.getExternalRoles().isEmpty() &&
roleDefinitions!=null && !roleDefinitions.isEmpty();
}
} }
package eu.dariah.de.dariahsp.authenticator; package eu.dariah.de.dariahsp.authenticator;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.pac4j.core.context.WebContext; import org.pac4j.core.context.WebContext;
import org.pac4j.core.credentials.UsernamePasswordCredentials; import org.pac4j.core.credentials.UsernamePasswordCredentials;
import org.pac4j.core.profile.UserProfile; import org.pac4j.core.profile.UserProfile;
import org.pac4j.core.profile.creator.ProfileCreator; import org.pac4j.core.profile.creator.ProfileCreator;
import eu.dariah.de.dariahsp.config.model.RoleDefinition;
import eu.dariah.de.dariahsp.model.ExtendedUserProfile; import eu.dariah.de.dariahsp.model.ExtendedUserProfile;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
...@@ -27,26 +23,7 @@ public class LocalProfileCreator extends BaseProfileCreator implements ProfileCr ...@@ -27,26 +23,7 @@ public class LocalProfileCreator extends BaseProfileCreator implements ProfileCr
return Optional.empty(); return Optional.empty();
} }
ExtendedUserProfile profile = new ExtendedUserProfile(credentials.getUserProfile()); ExtendedUserProfile profile = new ExtendedUserProfile(credentials.getUserProfile());
this.mapAndAssignRoles(profile);
Set<RoleDefinition> mappedDefinitions; return Optional.ofNullable(profile);
if (this.canMapRoles(profile)) {
// Determine applicable role definitions
mappedDefinitions = this.getMappedRoles(profile);
// Set applicable roles as Strings
profile.setRoles(mappedDefinitions.stream()
.map(mr -> ROLE_PREFIX + mr.getRole().toUpperCase())
.collect(Collectors.toSet()));
// Set maximum applicable level
profile.setLevel(mappedDefinitions.stream()
.mapToInt(RoleDefinition::getLevel)
.max().orElse(0));
}
return Optional.ofNullable(profile);
}
private boolean canMapRoles(ExtendedUserProfile profile) {
return clientName!=null &&
profile.getExternalRoles()!=null && !profile.getExternalRoles().isEmpty() &&
roleDefinitions!=null && !roleDefinitions.isEmpty();
} }
} }
package eu.dariah.de.dariahsp.authenticator; package eu.dariah.de.dariahsp.authenticator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.pac4j.core.context.WebContext; import org.pac4j.core.context.WebContext;
import org.pac4j.core.profile.UserProfile; import org.pac4j.core.profile.UserProfile;
import org.pac4j.core.profile.creator.ProfileCreator; import org.pac4j.core.profile.creator.ProfileCreator;
import org.pac4j.saml.credentials.SAML2Credentials; import org.pac4j.saml.credentials.SAML2Credentials;
import eu.dariah.de.dariahsp.config.Attribute;
import eu.dariah.de.dariahsp.config.model.RoleDefinition; import eu.dariah.de.dariahsp.config.SecurityConfig;
import eu.dariah.de.dariahsp.model.ExtendedUserProfile; import eu.dariah.de.dariahsp.model.ExtendedUserProfile;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Data @Data
@EqualsAndHashCode(callSuper=false) @EqualsAndHashCode(callSuper=false)
public class SamlProfileCreator extends BaseProfileCreator implements ProfileCreator<SAML2Credentials> { public class SamlProfileCreator extends BaseProfileCreator implements ProfileCreator<SAML2Credentials> {
public static final String EXTERNAL_ROLES_MAPPED_NAME = "externalRoles";
private final SecurityConfig securityConfig;
public SamlProfileCreator(String clientName) { public SamlProfileCreator(SecurityConfig securityConfig, String clientName) {
super(clientName); super(clientName);
this.securityConfig = securityConfig;
} }
@Override @Override
...@@ -29,26 +33,60 @@ public class SamlProfileCreator extends BaseProfileCreator implements ProfileCre ...@@ -29,26 +33,60 @@ public class SamlProfileCreator extends BaseProfileCreator implements ProfileCre
return Optional.empty(); return Optional.empty();
} }
ExtendedUserProfile profile = new ExtendedUserProfile(credentials.getUserProfile()); ExtendedUserProfile profile = new ExtendedUserProfile(credentials.getUserProfile());
this.assignExternalRoles(profile);
Set<RoleDefinition> mappedDefinitions; this.assignAttributes(profile);
if (this.canMapRoles(profile)) { this.mapAndAssignRoles(profile);
// Determine applicable role definitions return Optional.ofNullable(profile);
mappedDefinitions = this.getMappedRoles(profile); }
// Set applicable roles as Strings
profile.setRoles(mappedDefinitions.stream() private void assignExternalRoles(ExtendedUserProfile profile) {
.map(mr -> ROLE_PREFIX + mr.getRole().toUpperCase()) Attribute externalRolesAttribute = this.getExternalRolesAttribute();
.collect(Collectors.toSet())); if (externalRolesAttribute==null || !profile.containsAttribute(externalRolesAttribute.getName()) || profile.getAttribute(externalRolesAttribute.getName())==null) {
// Set maximum applicable level return;
profile.setLevel(mappedDefinitions.stream()
.mapToInt(RoleDefinition::getLevel)
.max().orElse(0));
} }
return Optional.ofNullable(profile); try {
@SuppressWarnings("unchecked")
List<String> memberOfs = (List<String>)profile.getAttribute(externalRolesAttribute.getName());
if (memberOfs.isEmpty()) {
return;
}
if (profile.getExternalRoles()!=null) {
for (String memberOf : memberOfs) {
if (!profile.getExternalRoles().contains(memberOf)) {
profile.getExternalRoles().add(memberOf);
}
}
} else {
profile.setExternalRoles(new LinkedHashSet<>(memberOfs));
}
} catch (Exception e) {
log.warn("Unable to map memberOf attribute to external roles of the profile", e);
}
} }
private boolean canMapRoles(ExtendedUserProfile profile) { private void assignAttributes(ExtendedUserProfile profile) {
return clientName!=null && List<Attribute> mappableAttributes = securityConfig.getSaml().getSp().getMappedProfileAttributes();
profile.getExternalRoles()!=null && !profile.getExternalRoles().isEmpty() && if (profile.getAttributes().isEmpty()) {
roleDefinitions!=null && !roleDefinitions.isEmpty(); return;
}
for (Attribute a : mappableAttributes) {
if (a.getMappedAttribute().equals("id") && profile.containsAttribute(a.getName())) {
List values = (List)profile.getAttribute(a.getName());
if (!values.isEmpty()) {
profile.setId(values.get(0).toString());
}
}
if (profile.containsAttribute(a.getName())) {
profile.addAttribute(a.getMappedAttribute(), profile.getAttribute(a.getName()));
}
}
}
private Attribute getExternalRolesAttribute() {
return securityConfig.getSaml().getSp().getMappedProfileAttributes().stream()
.filter(a -> a.getMappedAttribute().equals(EXTERNAL_ROLES_MAPPED_NAME))
.findFirst().orElse(null);
} }
} }
package eu.dariah.de.dariahsp.config;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
public class AttributeConfig implements InitializingBean {
@Autowired private SecurityConfig securityConfig;
@Override
public void afterPropertiesSet() throws Exception {
if (securityConfig.getSaml()==null || !securityConfig.getSaml().isEnabled()) {
}
securityConfig.getSaml().getSp().getAttributeConfig();
}
}
package eu.dariah.de.dariahsp.config; package eu.dariah.de.dariahsp.config;
import org.apache.http.client.HttpClient; import eu.dariah.de.dariahsp.config.model.SamlSpConfigProperties;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.pac4j.saml.util.SAML2HttpClientBuilder;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
...@@ -17,7 +10,7 @@ public class SamlProperties { ...@@ -17,7 +10,7 @@ public class SamlProperties {
private String authorizerName = "saml"; private String authorizerName = "saml";
private final KeystoreProperties keystore = new KeystoreProperties(); private final KeystoreProperties keystore = new KeystoreProperties();
private final MetadataProperties metadata = new MetadataProperties(); private final MetadataProperties metadata = new MetadataProperties();
private final SpProperties sp = new SpProperties(); private final SamlSpConfigProperties sp = new SamlSpConfigProperties();
@Getter @Setter @Getter @Setter
public class KeystoreProperties { public class KeystoreProperties {
...@@ -30,46 +23,5 @@ public class SamlProperties { ...@@ -30,46 +23,5 @@ public class SamlProperties {
@Getter @Setter @Getter @Setter
public class MetadataProperties { public class MetadataProperties {
private String url; private String url;
} }
@Getter @Setter
public class SpProperties {
private String metadataResource;
private boolean generateIfNotExists;
private int maxAuthAge = 3600;
private String entityId;
private int httpClientTimoutMs = 2000;
private boolean signMetadata;
private List<String> signingMethods;
private List<String> digestMethods;
private List<String> supportedProtocols;
private boolean authnRequestSigned = true;
private boolean logoutRequestSigned = false;
private boolean wantsAssertionsSigned = true;
private boolean wantsResponsesSigned = true;
private List<ConditionalAttributeSet> attributeConfig;
// ------------------------------------------
// Custom getters for complex default values
// ------------------------------------------
public List<String> getSupportedProtocols() {
List<String> p = supportedProtocols;
if (p==null) {
p = new ArrayList<>();
p.add(SAMLConstants.SAML20P_NS);
}
return p;
}
public int getMaxAuthAge() {
return maxAuthAge<=0 ? 86400 : maxAuthAge; // One day
}
public HttpClient getHttpClient() {
SAML2HttpClientBuilder httpClient = new SAML2HttpClientBuilder();
httpClient.setConnectionTimeout(Duration.ofSeconds(httpClientTimoutMs));
httpClient.setSocketTimeout(Duration.ofSeconds(httpClientTimoutMs));
return httpClient.build();
}
}
} }
\ No newline at end of file
...@@ -8,20 +8,14 @@ import java.util.ArrayList; ...@@ -8,20 +8,14 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import javax.servlet.ServletContext;
import org.pac4j.core.client.Client; import org.pac4j.core.client.Client;
import org.pac4j.core.client.Clients; import org.pac4j.core.client.Clients;
import org.pac4j.core.config.Config; import org.pac4j.core.config.Config;
import org.pac4j.core.credentials.UsernamePasswordCredentials;
import org.pac4j.http.client.indirect.FormClient; import org.pac4j.http.client.indirect.FormClient;
import org.pac4j.saml.client.SAML2Client; import org.pac4j.saml.client.SAML2Client;
import org.pac4j.saml.config.SAML2Configuration; import org.pac4j.saml.config.SAML2Configuration;
import org.pac4j.saml.credentials.SAML2Credentials;
import org.pac4j.springframework.annotation.AnnotationConfig; import org.pac4j.springframework.annotation.AnnotationConfig;
import org.pac4j.springframework.component.ComponentConfig; import org.pac4j.springframework.component.ComponentConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
...@@ -62,9 +56,6 @@ public class SecurityConfig { ...@@ -62,9 +56,6 @@ public class SecurityConfig {
public String getDefaultLoginUrl() { return defaultLoginUrl==null ? baseUrl : defaultLoginUrl; } public String getDefaultLoginUrl() { return defaultLoginUrl==null ? baseUrl : defaultLoginUrl; }
public String getDefaultLogoutUrl() { return defaultLogoutUrl==null ? baseUrl : defaultLogoutUrl; } public String getDefaultLogoutUrl() { return defaultLogoutUrl==null ? baseUrl : defaultLogoutUrl; }
@Bean AttributeConfig attributeConfig() {
return new AttributeConfig();
}
@Bean @Bean
public Optional<LocalUsernamePasswordAuthenticator> localUsernamePasswordAuthenticator() { public Optional<LocalUsernamePasswordAuthenticator> localUsernamePasswordAuthenticator() {
...@@ -183,7 +174,7 @@ public class SecurityConfig { ...@@ -183,7 +174,7 @@ public class SecurityConfig {
private SamlProfileCreator saml2ProfileCreator() { private SamlProfileCreator saml2ProfileCreator() {
SamlProfileCreator saml2ProfileCreator = new SamlProfileCreator(saml.getAuthorizerName()); SamlProfileCreator saml2ProfileCreator = new SamlProfileCreator(this, saml.getAuthorizerName());
saml2ProfileCreator.setRoleDefinitions(roleDefinitions); saml2ProfileCreator.setRoleDefinitions(roleDefinitions);
return saml2ProfileCreator; return saml2ProfileCreator;
} }
......
package eu.dariah.de.dariahsp.config.model;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.http.client.HttpClient;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.pac4j.saml.util.SAML2HttpClientBuilder;
import eu.dariah.de.dariahsp.config.Attribute;
import eu.dariah.de.dariahsp.config.ConditionalAttributeGroup;
import eu.dariah.de.dariahsp.config.ConditionalAttributeSet;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class SamlSpConfigProperties {
private String metadataResource;
private boolean generateIfNotExists;
private int maxAuthAge = 3600;
private String entityId;
private int httpClientTimoutMs = 2000;
private boolean signMetadata;
private List<String> signingMethods;
private List<String> digestMethods;
private List<String> supportedProtocols;
private boolean authnRequestSigned = true;
private boolean logoutRequestSigned = false;
private boolean wantsAssertionsSigned = true;
private boolean wantsResponsesSigned = true;
private List<ConditionalAttributeSet> attributeConfig;
private List<Attribute> mappedProfileAttributes = null;
// ------------------------------------------
// Custom getters for complex default values
// ------------------------------------------
public List<String> getSupportedProtocols() {
List<String> p = supportedProtocols;
if (p==null) {
p = new ArrayList<>();
p.add(SAMLConstants.SAML20P_NS);
}
return p;
}
public int getMaxAuthAge() {
return maxAuthAge<=0 ? 86400 : maxAuthAge; // One day
}
public HttpClient getHttpClient() {
SAML2HttpClientBuilder httpClient = new SAML2HttpClientBuilder();
httpClient.setConnectionTimeout(Duration.ofSeconds(httpClientTimoutMs));
httpClient.setSocketTimeout(Duration.ofSeconds(httpClientTimoutMs));
return httpClient.build();
}
public List<Attribute> getMappedProfileAttributes() {
if (mappedProfileAttributes!=null) {
return mappedProfileAttributes;
}
mappedProfileAttributes = new ArrayList<>();
if (attributeConfig!=null) {
for (ConditionalAttributeSet set : attributeConfig) {
mappedProfileAttributes.addAll(this.getAttributesFromSet(set).stream()
.filter(a -> a.getMappedAttribute()!=null)
.collect(Collectors.toList()));
}
}
return mappedProfileAttributes;
}
private List<Attribute> getAttributesFromSet(ConditionalAttributeSet set) {
List<Attribute> mappedAttributes = new ArrayList<>();
if (set.getAttributeGroup()!=null) {
for (ConditionalAttributeGroup group : set.getAttributeGroup()) {
mappedAttributes.addAll(getAttributesFromGroup(group));
}
}
return mappedAttributes;
}
private List<Attribute> getAttributesFromGroup(ConditionalAttributeGroup group) {
List<Attribute> attributes = new ArrayList<>();
if (group.getAttributes()!=null) {
for (Attribute a : group.getAttributes()) {
attributes.add(a);
}
}
return attributes;
}
}
...@@ -11,17 +11,17 @@ auth: ...@@ -11,17 +11,17 @@ auth:
level: 100 level: 100
mappings: mappings:
local: ["application_admin"] local: ["application_admin"]
saml2: ["application_admin"] saml: ["application_admin"]
- role: CONTRIBUTOR - role: CONTRIBUTOR
level: 50 level: 50
mappings: mappings:
local: ["application_contributor"] local: ["application_contributor"]
saml2: ["application_contributor"] saml: ["application_contributor"]
- role: USER - role: USER
level: 10 level: 10
mappings: mappings:
local: ["application_user"] local: ["application_user"]
saml2: ["application_user"] saml: ["application_user"]
local: local:
enabled: true enabled: true
authorizerName: local authorizerName: local
...@@ -60,14 +60,6 @@ auth: ...@@ -60,14 +60,6 @@ auth:
wantsResponsesSigned: false wantsResponsesSigned: false
httpClientTimoutMs: 2000 httpClientTimoutMs: 2000
attributeConfig: attributeConfig:
- stage: ATTRIBUTES
required: true
attributeGroup:
- check: AND
attributes:
- friendlyName: mail
name: urn:oid:0.9.2342.19200300.100.1.3
nameFormat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri
- stage: ATTRIBUTES - stage: ATTRIBUTES
required: true required: true
attributeGroup: attributeGroup:
...@@ -96,10 +88,11 @@ auth: ...@@ -96,10 +88,11 @@ auth:
- check: OPTIONAL - check: OPTIONAL
attributes: attributes:
- friendlyName: mail - friendlyName: mail
mappedAttribute: email
name: urn:oid:0.9.2342.19200300.100.1.3 name: urn:oid:0.9.2342.19200300.100.1.3
nameFormat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri nameFormat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri
- friendlyName: displayName - friendlyName: displayName
mappedAttribute: username mappedAttribute: display_name
name: urn:oid:2.16.840.1.113730.3.1.241 name: urn:oid:2.16.840.1.113730.3.1.241
nameFormat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri nameFormat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri
- friendlyName: isMemberOf - friendlyName: isMemberOf
......
...@@ -23,17 +23,17 @@ auth: ...@@ -23,17 +23,17 @@ auth:
level: 100 level: 100
mappings: mappings:
local: ["application_admin"] local: ["application_admin"]
saml2: ["application_admin"] saml: ["generic-search-admins"]
- role: CONTRIBUTOR