Commit 6a7cf6f8 authored by Gradl, Tobias's avatar Gradl, Tobias
Browse files

6: Implement configurable role mapping and hierarchy

Task-Url: #6
parent 977dcd76
Pipeline #17546 passed with stage
in 1 minute and 52 seconds
......@@ -4,9 +4,5 @@ public class Constants {
public enum AUTHENTICATION_STAGE { AUTHENTICATION, ATTRIBUTES }
public enum REQUIRED_ATTRIBUTE_CHECKLOGIC { AND, OR, OPTIONAL }
public final static String SAML_CLIENT_NAME = "Saml2Client";
public final static String LOCAL_CLIENT_NAME = "FormClient";
}
package eu.dariah.de.dariahsp.authenticator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.pac4j.core.context.WebContext;
import org.pac4j.core.credentials.Credentials;
import org.pac4j.core.profile.UserProfile;
import org.pac4j.core.profile.creator.AuthenticatorProfileCreator;
import org.pac4j.core.profile.creator.ProfileCreator;
import eu.dariah.de.dariahsp.config.model.RoleMapping;
import eu.dariah.de.dariahsp.model.ExtendedUserProfile;
import eu.dariah.de.dariahsp.model.UserImpl;
import lombok.Data;
@Data
public class UserProfileCreator<C extends Credentials> implements ProfileCreator<C> {
public final static UserProfileCreator INSTANCE = new UserProfileCreator<>();
private static final String ROLE_PREFIX = "ROLE_";
private final String clientName;
private List<RoleMapping> roleMappings;
@Override
public Optional<UserProfile> create(final C credentials, final WebContext context) {
if (credentials.getUserProfile()==null) {
return Optional.empty();
}
ExtendedUserProfile profile = new ExtendedUserProfile(credentials.getUserProfile());
return Optional.ofNullable(profile);
if (this.canMapRoles(profile)) {
profile.setRoles(this.getMappedRoles(profile));
}
return Optional.ofNullable(profile);
}
private boolean canMapRoles(ExtendedUserProfile profile) {
return clientName!=null &&
profile.getExternalRoles()!=null && !profile.getExternalRoles().isEmpty() &&
roleMappings!=null && !roleMappings.isEmpty();
}
private Set<String> getMappedRoles(ExtendedUserProfile profile) {
Set<String> roles = new LinkedHashSet<>();
for (RoleMapping rm : roleMappings) {
for (String client : rm.getMappings().keySet()) {
if (!client.equals(clientName)) {
continue;
}
for (String extRole : profile.getExternalRoles()) {
if (!extRole.trim().isEmpty() && rm.getMappings().get(client).contains(extRole.trim())) {
roles.add(ROLE_PREFIX + rm.getRole().toUpperCase());
}
}
}
}
return roles;
}
}
package eu.dariah.de.dariahsp.config;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.pac4j.core.client.Client;
......@@ -8,10 +9,8 @@ import org.pac4j.core.config.Config;
import org.pac4j.springframework.security.web.Pac4jEntryPoint;
import org.pac4j.springframework.security.web.SecurityFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
......@@ -22,14 +21,8 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationFi
public class CombinedSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
@Autowired private Config config;
@Autowired private Optional<RoleHierarchy> roleHierarchy;
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl r = new RoleHierarchyImpl();
r.setHierarchy("ROLE_ADMINISTRATOR > ROLE_CONTRIBUTOR > ROLE_USER");
return r;
}
@Override
protected void configure(final HttpSecurity http) throws Exception {
......@@ -57,7 +50,9 @@ public class CombinedSecurityConfigurationAdapter extends WebSecurityConfigurerA
protected SecurityExpressionHandler<FilterInvocation> webExpressionHandler() {
DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler();
defaultWebSecurityExpressionHandler.setRoleHierarchy(roleHierarchy());
if (roleHierarchy.isPresent()) {
defaultWebSecurityExpressionHandler.setRoleHierarchy(roleHierarchy.get());
}
return defaultWebSecurityExpressionHandler;
}
}
package eu.dariah.de.dariahsp.config;
import eu.dariah.de.dariahsp.local.LocalUsers;
import lombok.Getter;
import lombok.Setter;
import lombok.Data;
@Getter @Setter
@Data
public class LocalSecurityProperties {
private boolean enabled;
private String authorizerName = "local";
private LocalUsers[] users;
}
\ No newline at end of file
......@@ -14,6 +14,7 @@ import lombok.Setter;
@Getter @Setter
public class SamlProperties {
private boolean enabled = true;
private String authorizerName = "saml2";
private final KeystoreProperties keystore = new KeystoreProperties();
private final MetadataProperties metadata = new MetadataProperties();
private final SpProperties sp = new SpProperties();
......
......@@ -6,18 +6,19 @@ import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.pac4j.core.authorization.authorizer.RequireAnyRoleAuthorizer;
import org.pac4j.core.client.Client;
import org.pac4j.core.client.Clients;
import org.pac4j.core.config.Config;
import org.pac4j.core.context.WebContext;
import org.pac4j.core.profile.ProfileManager;
import org.pac4j.core.profile.factory.ProfileManagerFactory;
import org.pac4j.core.credentials.UsernamePasswordCredentials;
import org.pac4j.http.client.indirect.FormClient;
import org.pac4j.saml.client.SAML2Client;
import org.pac4j.saml.config.SAML2Configuration;
import org.pac4j.saml.credentials.SAML2Credentials;
import org.pac4j.springframework.annotation.AnnotationConfig;
import org.pac4j.springframework.component.ComponentConfig;
import org.springframework.boot.context.properties.ConfigurationProperties;
......@@ -32,8 +33,10 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import eu.dariah.de.dariahsp.CustomAuthorizer;
import eu.dariah.de.dariahsp.authenticator.LocalUsernamePasswordAuthenticator;
import eu.dariah.de.dariahsp.authenticator.UserProfileCreator;
import eu.dariah.de.dariahsp.config.model.RoleMapping;
import eu.dariah.de.dariahsp.metadata.MetadataHelper;
import lombok.Data;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@Data
......@@ -43,10 +46,12 @@ import lombok.extern.slf4j.Slf4j;
@ConfigurationProperties(prefix = "auth")
@Import({ComponentConfig.class, AnnotationConfig.class})
public class SecurityConfig {
private String salt;
private final LocalSecurityProperties local = new LocalSecurityProperties();
private final SamlProperties saml = new SamlProperties();
@Getter private String salt;
@Getter private String roleHierarchy;
@Getter private final List<RoleMapping> roleMappings;
@Bean
public Optional<LocalUsernamePasswordAuthenticator> localUsernamePasswordAuthenticator() {
......@@ -69,6 +74,18 @@ public class SecurityConfig {
return new MetadataHelper();
}
@Bean
public Optional<RoleHierarchy> roleHierarchy() {
if (roleHierarchy==null || roleHierarchy.isEmpty()) {
log.info("RoleHierarchy not configured; no role hierarchy available");
return Optional.empty();
}
RoleHierarchyImpl r = new RoleHierarchyImpl();
r.setHierarchy(roleHierarchy);
log.info("RoleHierarchy configured: {}", roleHierarchy);
return Optional.of(r);
}
@Bean
@SuppressWarnings("rawtypes")
public Config config() throws URISyntaxException {
......@@ -77,11 +94,11 @@ public class SecurityConfig {
Optional<FormClient> formClient = getFormClient();
if (samlClient.isPresent()) {
samlClient.get().setProfileCreator(UserProfileCreator.INSTANCE);
samlClient.get().setProfileCreator(saml2ProfileCreator());
clients.add(samlClient.get());
}
if (formClient.isPresent()) {
formClient.get().setProfileCreator(UserProfileCreator.INSTANCE);
formClient.get().setProfileCreator(localProfileCreator());
clients.add(formClient.get());
}
......@@ -135,14 +152,32 @@ public class SecurityConfig {
cfg.setSupportedProtocols(saml.getSp().getSupportedProtocols());
cfg.setHttpClient(saml.getSp().getHttpClient());
return Optional.of(new SAML2Client(cfg));
SAML2Client c = new SAML2Client(cfg);
c.setName(saml.getAuthorizerName());
return Optional.of(c);
}
private Optional<FormClient> getFormClient() {
Optional<LocalUsernamePasswordAuthenticator> localUsernamePasswordAuthenticator = localUsernamePasswordAuthenticator();
if (localUsernamePasswordAuthenticator.isPresent()) {
return Optional.of(new FormClient("/login", localUsernamePasswordAuthenticator.get()));
FormClient c = new FormClient("/login", localUsernamePasswordAuthenticator.get());
c.setName(local.getAuthorizerName());
return Optional.of(c);
}
return Optional.empty();
}
private UserProfileCreator<SAML2Credentials> saml2ProfileCreator() {
UserProfileCreator<SAML2Credentials> saml2ProfileCreator = new UserProfileCreator<>(saml.getAuthorizerName());
saml2ProfileCreator.setRoleMappings(roleMappings);
return saml2ProfileCreator;
}
private UserProfileCreator<UsernamePasswordCredentials> localProfileCreator() {
UserProfileCreator<UsernamePasswordCredentials> localProfileCreator = new UserProfileCreator<>(local.getAuthorizerName());
localProfileCreator.setRoleMappings(roleMappings);
return localProfileCreator;
}
}
package eu.dariah.de.dariahsp.config.model;
import java.util.Map;
import java.util.Set;
import lombok.Data;
@Data
public class RoleMapping {
private String role;
private Map<String,Set<String>> mappings;
}
......@@ -15,16 +15,19 @@ import org.springframework.core.io.Resource;
import org.springframework.util.FileCopyUtils;
import eu.dariah.de.dariahsp.Constants;
import eu.dariah.de.dariahsp.config.SecurityConfig;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MetadataHelper implements InitializingBean {
@Autowired private SecurityConfig securityConfig;
@Autowired private Config config;
private SAML2Configuration saml2Config = null;
@Override
public void afterPropertiesSet() throws Exception {
Optional<?> client = config.getClients().findClient(Constants.SAML_CLIENT_NAME);
Optional<?> client = config.getClients().findClient(securityConfig.getSaml().getAuthorizerName());
if (client.isPresent()) {
saml2Config = SAML2Client.class.cast(client.get()).getConfiguration();
}
......
# Config options of the dariahsp core library
# Commented properties reflect default values
auth:
salt: Qmwp4CO7LDkOUDouAcCcUqd9ZGNbRG5Jyr5lpntOuB9
rolehierarchy: ROLE_ADMINISTRATOR > ROLE_CONTRIBUTOR > ROLE_USER
rolemappings:
- role: ADMINISTRATOR
mappings:
local: ["application_admin"]
saml2: ["application_admin"]
- role: CONTRIBUTOR
mappings:
local: ["application_contributor"]
saml2: ["application_contributor"]
- role: USER
mappings:
local: ["application_user"]
saml2: ["application_user"]
local:
enabled: true
# Same password for each user: 1234
users:
- username: 'admin'
passhash: '$2y$10$nmTcpRxs.RFUstkJJms6U.AW61Jmr64s9VNQLuhpU8gYrgzCapwka'
roles: ["application_admin"]
- username: 'contributor'
passhash: '$2y$10$nmTcpRxs.RFUstkJJms6U.AW61Jmr64s9VNQLuhpU8gYrgzCapwka'
roles: ["application_contributor"]
- username: 'user'
passhash: '$2y$10$nmTcpRxs.RFUstkJJms6U.AW61Jmr64s9VNQLuhpU8gYrgzCapwka'
roles: ["application_user"]
saml:
enabled: false
keystore:
path: /data/_srv/dariahsp/c105-229.cloud.gwdg.de.jks
pass: clariah
alias: c105-229.cloud.gwdg.de
aliaspass: clariah6
metadata:
url: https://aaiproxy.de.dariah.eu/idp/
sp:
#metadataResource: /data/_srv/dariahsp/sp_metadata.xml
maxAuthAge: -1
#baseUrl: https://c105-229.cloud.gwdg.de/dme
#entityId: ${auth.saml.sp.baseUrl}
signMetadata: true
#signingMethods: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
#digestMethods: http://www.w3.org/2001/04/xmlenc#sha256, http://www.w3.org/2001/04/xmlenc#sha512
#supportedProtocols: urn:oasis:names:tc:SAML:2.0:protocol
authnRequestSigned: true
logoutRequestSigned: true
wantsAssertionsSigned: true
wantsResponsesSigned: false
httpClientTimoutMs: 2000
requiredAttributes:
- 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
required: true
attributeGroup:
- check: OR
attributes:
- friendlyName: dariahTermsOfUse
name: urn:oid:1.3.6.1.4.1.10126.1.52.4.15
nameFormat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri
value: Terms_of_Use_v5.pdf
- friendlyName: dariahTermsOfUse
name: urn:oid:1.3.6.1.4.1.10126.1.52.4.15
nameFormat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri
value: foobar-service-agreement_version1.pdf
- stage: AUTHENTICATION
required: true
attributeGroup:
- check: AND
attributes:
- friendlyName: eduPersonPrincipalName
name: urn:oid:1.3.6.1.4.1.5923.1.1.1.6
nameFormat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri
- stage: AUTHENTICATION
required: false
attributeGroup:
- check: OPTIONAL
attributes:
- friendlyName: mail
name: urn:oid:0.9.2342.19200300.100.1.3
nameFormat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri
- friendlyName: displayName
name: urn:oid:2.16.840.1.113730.3.1.241
nameFormat: urn:oasis:names:tc:SAML:2.0:attrname-format:uri
\ No newline at end of file
......@@ -23,6 +23,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import eu.dariah.de.dariahsp.Constants;
import eu.dariah.de.dariahsp.config.SecurityConfig;
import eu.dariah.de.dariahsp.error.AuthenticatorNotAvailable;
import eu.dariah.de.dariahsp.error.NotFoundException;
import eu.dariah.de.dariahsp.error.SAML2MetadataNotFoundException;
......@@ -37,6 +38,7 @@ public class SampleController {
@Value("${auth.salt}")
private String salt;
@Autowired private SecurityConfig securityConfig;
@Autowired private Config config;
@Autowired private JEEContext jeeContext;
@Autowired private ProfileManager profileManager;
......@@ -59,7 +61,7 @@ public class SampleController {
@RequestMapping("/login")
public String loginForm(Map<String, Object> map) {
final FormClient formClient = (FormClient) config.getClients().findClient(Constants.LOCAL_CLIENT_NAME).get();
final FormClient formClient = (FormClient) config.getClients().findClient(securityConfig.getLocal().getAuthorizerName()).get();
map.put("callbackUrl", formClient.getCallbackUrl());
return "form";
}
......
......@@ -10,19 +10,33 @@ logging:
auth:
salt: Qmwp4CO7LDkOUDouAcCcUqd9ZGNbRG5Jyr5lpntOuB9
rolehierarchy: ROLE_ADMINISTRATOR > ROLE_CONTRIBUTOR > ROLE_USER
rolemappings:
- role: ADMINISTRATOR
mappings:
local: ["application_admin"]
saml2: ["application_admin"]
- role: CONTRIBUTOR
mappings:
local: ["application_contributor"]
saml2: ["application_contributor"]
- role: USER
mappings:
local: ["application_user"]
saml2: ["application_user"]
local:
enabled: true
# Same password for each user: 1234
users:
- username: 'admin'
passhash: '$2y$10$nmTcpRxs.RFUstkJJms6U.AW61Jmr64s9VNQLuhpU8gYrgzCapwka'
roles: ["ROLE_ADMINISTRATOR"]
roles: ["application_admin"]
- username: 'contributor'
passhash: '$2y$10$nmTcpRxs.RFUstkJJms6U.AW61Jmr64s9VNQLuhpU8gYrgzCapwka'
roles: ["ROLE_CONTRIBUTOR"]
roles: ["application_contributor"]
- username: 'user'
passhash: '$2y$10$nmTcpRxs.RFUstkJJms6U.AW61Jmr64s9VNQLuhpU8gYrgzCapwka'
roles: ["ROLE_CONTRIBUTOR"]
roles: ["application_user"]
saml:
enabled: false
keystore:
......
......@@ -14,8 +14,8 @@
<a href="/pac4jLogout?url=/?forcepostlogouturl">pac4j local logout</a><br />
<a href="/pac4jCentralLogout?url=/?forcepostlogouturlafteridp">pac4j central local logout</a>
<br />
<a href="forceLogin?client_name=Saml2Client">Force SAML login</a> (even if already authenticated)<br />
<a href="forceLogin?client_name=FormClient">Force local login</a> (even if already authenticated)<br />
<a href="forceLogin?client_name=saml2">Force SAML login</a> (even if already authenticated)<br />
<a href="forceLogin?client_name=local">Force local login</a> (even if already authenticated)<br />
<br /><br />
profiles: ${profiles}<br />
<br />
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment