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 { ...@@ -4,9 +4,5 @@ public class Constants {
public enum AUTHENTICATION_STAGE { AUTHENTICATION, ATTRIBUTES } public enum AUTHENTICATION_STAGE { AUTHENTICATION, ATTRIBUTES }
public enum REQUIRED_ATTRIBUTE_CHECKLOGIC { AND, OR, OPTIONAL } 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; 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 org.pac4j.core.context.WebContext; import org.pac4j.core.context.WebContext;
import org.pac4j.core.credentials.Credentials; import org.pac4j.core.credentials.Credentials;
import org.pac4j.core.profile.UserProfile; import org.pac4j.core.profile.UserProfile;
import org.pac4j.core.profile.creator.AuthenticatorProfileCreator;
import org.pac4j.core.profile.creator.ProfileCreator; 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.ExtendedUserProfile;
import eu.dariah.de.dariahsp.model.UserImpl; import lombok.Data;
@Data
public class UserProfileCreator<C extends Credentials> implements ProfileCreator<C> { public class UserProfileCreator<C extends Credentials> implements ProfileCreator<C> {
private static final String ROLE_PREFIX = "ROLE_";
public final static UserProfileCreator INSTANCE = new UserProfileCreator<>(); private final String clientName;
private List<RoleMapping> roleMappings;
@Override @Override
public Optional<UserProfile> create(final C credentials, final WebContext context) { public Optional<UserProfile> create(final C credentials, final WebContext context) {
if (credentials.getUserProfile()==null) { if (credentials.getUserProfile()==null) {
return Optional.empty(); return Optional.empty();
} }
ExtendedUserProfile profile = new ExtendedUserProfile(credentials.getUserProfile()); ExtendedUserProfile profile = new ExtendedUserProfile(credentials.getUserProfile());
if (this.canMapRoles(profile)) {
profile.setRoles(this.getMappedRoles(profile));
return Optional.ofNullable(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; package eu.dariah.de.dariahsp.config;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.pac4j.core.client.Client; import org.pac4j.core.client.Client;
...@@ -8,10 +9,8 @@ import org.pac4j.core.config.Config; ...@@ -8,10 +9,8 @@ import org.pac4j.core.config.Config;
import org.pac4j.springframework.security.web.Pac4jEntryPoint; import org.pac4j.springframework.security.web.Pac4jEntryPoint;
import org.pac4j.springframework.security.web.SecurityFilter; import org.pac4j.springframework.security.web.SecurityFilter;
import org.springframework.beans.factory.annotation.Autowired; 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.expression.SecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy; 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.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
...@@ -22,14 +21,8 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationFi ...@@ -22,14 +21,8 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationFi
public class CombinedSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter { public class CombinedSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
@Autowired private Config config; @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 @Override
protected void configure(final HttpSecurity http) throws Exception { protected void configure(final HttpSecurity http) throws Exception {
...@@ -57,7 +50,9 @@ public class CombinedSecurityConfigurationAdapter extends WebSecurityConfigurerA ...@@ -57,7 +50,9 @@ public class CombinedSecurityConfigurationAdapter extends WebSecurityConfigurerA
protected SecurityExpressionHandler<FilterInvocation> webExpressionHandler() { protected SecurityExpressionHandler<FilterInvocation> webExpressionHandler() {
DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler(); DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler();
defaultWebSecurityExpressionHandler.setRoleHierarchy(roleHierarchy()); if (roleHierarchy.isPresent()) {
defaultWebSecurityExpressionHandler.setRoleHierarchy(roleHierarchy.get());
}
return defaultWebSecurityExpressionHandler; return defaultWebSecurityExpressionHandler;
} }
} }
package eu.dariah.de.dariahsp.config; package eu.dariah.de.dariahsp.config;
import eu.dariah.de.dariahsp.local.LocalUsers; import eu.dariah.de.dariahsp.local.LocalUsers;
import lombok.Getter; import lombok.Data;
import lombok.Setter;
@Getter @Setter @Data
public class LocalSecurityProperties { public class LocalSecurityProperties {
private boolean enabled; private boolean enabled;
private String authorizerName = "local";
private LocalUsers[] users; private LocalUsers[] users;
} }
\ No newline at end of file
...@@ -14,6 +14,7 @@ import lombok.Setter; ...@@ -14,6 +14,7 @@ import lombok.Setter;
@Getter @Setter @Getter @Setter
public class SamlProperties { public class SamlProperties {
private boolean enabled = true; private boolean enabled = true;
private String authorizerName = "saml2";
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 SpProperties sp = new SpProperties();
......
...@@ -6,18 +6,19 @@ import java.nio.file.Files; ...@@ -6,18 +6,19 @@ import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import org.pac4j.core.authorization.authorizer.RequireAnyRoleAuthorizer; import org.pac4j.core.authorization.authorizer.RequireAnyRoleAuthorizer;
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.context.WebContext; import org.pac4j.core.credentials.UsernamePasswordCredentials;
import org.pac4j.core.profile.ProfileManager;
import org.pac4j.core.profile.factory.ProfileManagerFactory;
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.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
...@@ -32,8 +33,10 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; ...@@ -32,8 +33,10 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import eu.dariah.de.dariahsp.CustomAuthorizer; import eu.dariah.de.dariahsp.CustomAuthorizer;
import eu.dariah.de.dariahsp.authenticator.LocalUsernamePasswordAuthenticator; import eu.dariah.de.dariahsp.authenticator.LocalUsernamePasswordAuthenticator;
import eu.dariah.de.dariahsp.authenticator.UserProfileCreator; import eu.dariah.de.dariahsp.authenticator.UserProfileCreator;
import eu.dariah.de.dariahsp.config.model.RoleMapping;
import eu.dariah.de.dariahsp.metadata.MetadataHelper; import eu.dariah.de.dariahsp.metadata.MetadataHelper;
import lombok.Data; import lombok.Data;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Data @Data
...@@ -43,10 +46,12 @@ import lombok.extern.slf4j.Slf4j; ...@@ -43,10 +46,12 @@ import lombok.extern.slf4j.Slf4j;
@ConfigurationProperties(prefix = "auth") @ConfigurationProperties(prefix = "auth")
@Import({ComponentConfig.class, AnnotationConfig.class}) @Import({ComponentConfig.class, AnnotationConfig.class})
public class SecurityConfig { public class SecurityConfig {
private String salt;
private final LocalSecurityProperties local = new LocalSecurityProperties(); private final LocalSecurityProperties local = new LocalSecurityProperties();
private final SamlProperties saml = new SamlProperties(); private final SamlProperties saml = new SamlProperties();
@Getter private String salt;
@Getter private String roleHierarchy;
@Getter private final List<RoleMapping> roleMappings;
@Bean @Bean
public Optional<LocalUsernamePasswordAuthenticator> localUsernamePasswordAuthenticator() { public Optional<LocalUsernamePasswordAuthenticator> localUsernamePasswordAuthenticator() {
...@@ -69,6 +74,18 @@ public class SecurityConfig { ...@@ -69,6 +74,18 @@ public class SecurityConfig {
return new MetadataHelper(); 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 @Bean
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
public Config config() throws URISyntaxException { public Config config() throws URISyntaxException {
...@@ -77,11 +94,11 @@ public class SecurityConfig { ...@@ -77,11 +94,11 @@ public class SecurityConfig {
Optional<FormClient> formClient = getFormClient(); Optional<FormClient> formClient = getFormClient();
if (samlClient.isPresent()) { if (samlClient.isPresent()) {
samlClient.get().setProfileCreator(UserProfileCreator.INSTANCE); samlClient.get().setProfileCreator(saml2ProfileCreator());
clients.add(samlClient.get()); clients.add(samlClient.get());
} }
if (formClient.isPresent()) { if (formClient.isPresent()) {
formClient.get().setProfileCreator(UserProfileCreator.INSTANCE); formClient.get().setProfileCreator(localProfileCreator());
clients.add(formClient.get()); clients.add(formClient.get());
} }
...@@ -135,14 +152,32 @@ public class SecurityConfig { ...@@ -135,14 +152,32 @@ public class SecurityConfig {
cfg.setSupportedProtocols(saml.getSp().getSupportedProtocols()); cfg.setSupportedProtocols(saml.getSp().getSupportedProtocols());
cfg.setHttpClient(saml.getSp().getHttpClient()); 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() { private Optional<FormClient> getFormClient() {
Optional<LocalUsernamePasswordAuthenticator> localUsernamePasswordAuthenticator = localUsernamePasswordAuthenticator(); Optional<LocalUsernamePasswordAuthenticator> localUsernamePasswordAuthenticator = localUsernamePasswordAuthenticator();
if (localUsernamePasswordAuthenticator.isPresent()) { 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(); 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; ...@@ -15,16 +15,19 @@ import org.springframework.core.io.Resource;
import org.springframework.util.FileCopyUtils; import org.springframework.util.FileCopyUtils;
import eu.dariah.de.dariahsp.Constants; import eu.dariah.de.dariahsp.Constants;
import eu.dariah.de.dariahsp.config.SecurityConfig;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class MetadataHelper implements InitializingBean { public class MetadataHelper implements InitializingBean {
@Autowired private SecurityConfig securityConfig;
@Autowired private Config config; @Autowired private Config config;
private SAML2Configuration saml2Config = null; private SAML2Configuration saml2Config = null;
@Override @Override
public void afterPropertiesSet() throws Exception { 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()) { if (client.isPresent()) {
saml2Config = SAML2Client.class.cast(client.get()).getConfiguration(); 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; ...@@ -23,6 +23,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import eu.dariah.de.dariahsp.Constants; 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.AuthenticatorNotAvailable;
import eu.dariah.de.dariahsp.error.NotFoundException; import eu.dariah.de.dariahsp.error.NotFoundException;
import eu.dariah.de.dariahsp.error.SAML2MetadataNotFoundException; import eu.dariah.de.dariahsp.error.SAML2MetadataNotFoundException;
...@@ -37,6 +38,7 @@ public class SampleController { ...@@ -37,6 +38,7 @@ public class SampleController {
@Value("${auth.salt}") @Value("${auth.salt}")
private String salt; private String salt;
@Autowired private SecurityConfig securityConfig;
@Autowired private Config config; @Autowired private Config config;
@Autowired private JEEContext jeeContext; @Autowired private JEEContext jeeContext;
@Autowired private ProfileManager profileManager; @Autowired private ProfileManager profileManager;
...@@ -59,7 +61,7 @@ public class SampleController { ...@@ -59,7 +61,7 @@ public class SampleController {
@RequestMapping("/login") @RequestMapping("/login")
public String loginForm(Map<String, Object> map) { 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()); map.put("callbackUrl", formClient.getCallbackUrl());
return "form"; return "form";
} }
......
...@@ -10,19 +10,33 @@ logging: ...@@ -10,19 +10,33 @@ logging:
auth: auth:
salt: Qmwp4CO7LDkOUDouAcCcUqd9ZGNbRG5Jyr5lpntOuB9 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: local:
enabled: true enabled: true
# Same password for each user: 1234 # Same password for each user: 1234
users: users:
- username: 'admin' - username: 'admin'
passhash: '$2y$10$nmTcpRxs.RFUstkJJms6U.AW61Jmr64s9VNQLuhpU8gYrgzCapwka' passhash: '$2y$10$nmTcpRxs.RFUstkJJms6U.AW61Jmr64s9VNQLuhpU8gYrgzCapwka'
roles: ["ROLE_ADMINISTRATOR"] roles: ["application_admin"]
- username: 'contributor'