Commit 5a1294c2 authored by Gradl, Tobias's avatar Gradl, Tobias
Browse files

Merge branch 'v2.1-dev' into 'v2.x-master'

V2.1 dev

See merge request !6
parents d5d9d86f 5e46c384
Pipeline #18616 passed with stages
in 2 minutes and 37 seconds
...@@ -5,7 +5,7 @@ plugins { ...@@ -5,7 +5,7 @@ plugins {
allprojects { allprojects {
group = 'eu.dariah.de' group = 'eu.dariah.de'
version = '2.1.2-SNAPSHOT' version = '2.1.3-SNAPSHOT'
apply plugin: 'eclipse' apply plugin: 'eclipse'
......
package eu.dariah.de.dariahsp.config;
import org.pac4j.core.client.BaseClient;
import lombok.Data;
@Data
public class OrderedClient<T extends BaseClient<?>> implements Comparable<OrderedClient<?>> {
private final int order;
private final T client;
@Override
public int compareTo(OrderedClient<?> o) {
if (order<o.getOrder()) {
return -1;
} else if (order==o.getOrder()) {
return 0;
} else {
return 1;
}
}
}
\ No newline at end of file
...@@ -5,6 +5,7 @@ import java.net.URISyntaxException; ...@@ -5,6 +5,7 @@ import java.net.URISyntaxException;
import java.nio.file.Files; 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.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
...@@ -55,7 +56,7 @@ public class SecurityConfig { ...@@ -55,7 +56,7 @@ public class SecurityConfig {
@Setter private String defaultLoginUrl = null; @Setter private String defaultLoginUrl = null;
@Setter private String defaultLogoutUrl = null; @Setter private String defaultLogoutUrl = null;
@Getter private List<String> enabledClientNames = null; @Getter private List<String> enabledClientNames = new ArrayList<>(0);
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; }
...@@ -102,34 +103,34 @@ public class SecurityConfig { ...@@ -102,34 +103,34 @@ public class SecurityConfig {
} }
@Bean @Bean
@SuppressWarnings("rawtypes")
public Config config(Optional<ProfileActionHandler> profileActionHandler) throws URISyntaxException { public Config config(Optional<ProfileActionHandler> profileActionHandler) throws URISyntaxException {
List<Client> clients = new ArrayList<>(); List<OrderedClient<?>> clients = new ArrayList<>();
SAML2Client samlClient = getSamlClient(); OrderedClient<SAML2Client> samlClient = getSamlClient();
FormClient formClient = getFormClient(); OrderedClient<FormClient> formClient = getFormClient();
if (samlClient!=null) { if (samlClient!=null) {
clients.add(samlClient); clients.add(samlClient);
} }
if (formClient!=null) { if (formClient!=null) {
clients.add(formClient); clients.add(formClient);
} }
Collections.sort(clients);
Config.setProfileManagerFactory("customizableProfileManager", ctx -> new CustomizableProfileManager(ctx, profileActionHandler.orElse(null))); Config.setProfileManagerFactory("customizableProfileManager", ctx -> new CustomizableProfileManager(ctx, profileActionHandler.orElse(null)));
Config c = new Config(new Clients(baseUrl().getAbsoluteUrl("/callback"), clients)); Config conf = new Config(
new Clients(baseUrl().getAbsoluteUrl("/callback"),
clients.stream().map(OrderedClient::getClient).collect(Collectors.toList())));
enabledClientNames = c.getClients().findAllClients().stream() enabledClientNames = conf.getClients().findAllClients().stream()
.map(Client::getName) .map(Client::getName)
.collect(Collectors.toList()); .collect(Collectors.toList());
return c; return conf;
} }
private OrderedClient<SAML2Client> getSamlClient() throws URISyntaxException {
private SAML2Client getSamlClient() throws URISyntaxException {
if (!saml.isEnabled()) { if (!saml.isEnabled()) {
return null; return null;
} }
final SAML2Configuration cfg = new SAML2Configuration(); final SAML2Configuration cfg = new SAML2Configuration();
// Keystore // Keystore
...@@ -175,17 +176,17 @@ public class SecurityConfig { ...@@ -175,17 +176,17 @@ public class SecurityConfig {
SAML2Client c = new SAML2Client(cfg); SAML2Client c = new SAML2Client(cfg);
c.setName(saml.getAuthorizerName()); c.setName(saml.getAuthorizerName());
c.setProfileCreator(saml2ProfileCreator()); c.setProfileCreator(saml2ProfileCreator());
c.setAuthenticator(new SAMLRequiredAttributeAuthenticator(cfg.getAttributeAsId(), cfg.getMappedAttributes(), saml.getSp())); c.setAuthenticator(new SAMLRequiredAttributeAuthenticator(cfg.getAttributeAsId(), cfg.getMappedAttributes(), saml.getSp()));
return c; return new OrderedClient<>(saml.getOrder(), c);
} }
private FormClient getFormClient() throws URISyntaxException { private OrderedClient<FormClient> getFormClient() throws URISyntaxException {
Optional<LocalUsernamePasswordAuthenticator> localUsernamePasswordAuthenticator = localUsernamePasswordAuthenticator(); Optional<LocalUsernamePasswordAuthenticator> localUsernamePasswordAuthenticator = localUsernamePasswordAuthenticator();
if (localUsernamePasswordAuthenticator.isPresent()) { if (localUsernamePasswordAuthenticator.isPresent()) {
FormClient c = new FormClient(baseUrl().getAbsoluteUrl("/login"), localUsernamePasswordAuthenticator.get()); FormClient c = new FormClient(baseUrl().getAbsoluteUrl("/login"), localUsernamePasswordAuthenticator.get());
c.setName(local.getAuthorizerName()); c.setName(local.getAuthorizerName());
c.setProfileCreator(localProfileCreator()); c.setProfileCreator(localProfileCreator());
return c; return new OrderedClient<>(local.getOrder(), c);
} }
return null; return null;
} }
......
...@@ -6,5 +6,6 @@ import lombok.Data; ...@@ -6,5 +6,6 @@ import lombok.Data;
public class LocalSecurity { public class LocalSecurity {
private boolean enabled; private boolean enabled;
private String authorizerName = "local"; private String authorizerName = "local";
private int order = 1;
private LocalUsers[] users; private LocalUsers[] users;
} }
\ No newline at end of file
...@@ -7,6 +7,7 @@ import lombok.Setter; ...@@ -7,6 +7,7 @@ import lombok.Setter;
public class SAMLSecurity { public class SAMLSecurity {
private boolean enabled; private boolean enabled;
private String authorizerName = "saml"; private String authorizerName = "saml";
private int order = 0;
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 ServiceProvider sp = new ServiceProvider(); private final ServiceProvider sp = new ServiceProvider();
......
...@@ -4,6 +4,7 @@ import org.springframework.beans.factory.annotation.Autowired; ...@@ -4,6 +4,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import eu.dariah.de.dariahsp.config.SecurityConfig;
import eu.dariah.de.dariahsp.web.AuthInfoHandlerInterceptor; import eu.dariah.de.dariahsp.web.AuthInfoHandlerInterceptor;
import eu.dariah.de.dariahsp.web.AuthInfoHelper; import eu.dariah.de.dariahsp.web.AuthInfoHelper;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
...@@ -18,6 +19,7 @@ import lombok.extern.slf4j.Slf4j; ...@@ -18,6 +19,7 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class AuthInfoConfigurer implements WebMvcConfigurer { public class AuthInfoConfigurer implements WebMvcConfigurer {
@Autowired private AuthInfoHelper authInfoHelper; @Autowired private AuthInfoHelper authInfoHelper;
@Autowired private SecurityConfig securityConfig;
/** /**
* Adds an {@link AuthInfoHandlerInterceptor} to the {@link InterceptorRegistry} * Adds an {@link AuthInfoHandlerInterceptor} to the {@link InterceptorRegistry}
...@@ -29,8 +31,6 @@ public class AuthInfoConfigurer implements WebMvcConfigurer { ...@@ -29,8 +31,6 @@ public class AuthInfoConfigurer implements WebMvcConfigurer {
} }
private AuthInfoHandlerInterceptor authInfoHandlerInterceptor() { private AuthInfoHandlerInterceptor authInfoHandlerInterceptor() {
AuthInfoHandlerInterceptor i = new AuthInfoHandlerInterceptor(); return new AuthInfoHandlerInterceptor(authInfoHelper, securityConfig.getEnabledClientNames());
i.setAuthInfoHelper(authInfoHelper);
return i;
} }
} }
\ No newline at end of file
package eu.dariah.de.dariahsp.web; package eu.dariah.de.dariahsp.web;
import java.util.List;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
...@@ -9,18 +11,17 @@ import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; ...@@ -9,18 +11,17 @@ import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.support.RequestContextUtils; import org.springframework.web.servlet.support.RequestContextUtils;
import eu.dariah.de.dariahsp.web.model.AuthPojo; import eu.dariah.de.dariahsp.web.model.AuthPojo;
import lombok.RequiredArgsConstructor;
/** /**
* Interceptor that adds basic authentication and authorization information as {@link AuthPojo} to each request model * Interceptor that adds basic authentication and authorization information as {@link AuthPojo} to each request model
* *
* @author Tobias Gradl * @author Tobias Gradl
*/ */
@RequiredArgsConstructor
public class AuthInfoHandlerInterceptor extends HandlerInterceptorAdapter { public class AuthInfoHandlerInterceptor extends HandlerInterceptorAdapter {
private AuthInfoHelper authInfoHelper; private final AuthInfoHelper authInfoHelper;
private final List<String> configuredAuthClientNames;
public AuthInfoHelper getAuthInfoHelper() { return authInfoHelper; }
public void setAuthInfoHelper(AuthInfoHelper authInfoHelper) { this.authInfoHelper = authInfoHelper; }
/* /*
* Adds the _sessionId and _auth request model attributes * Adds the _sessionId and _auth request model attributes
...@@ -33,6 +34,7 @@ public class AuthInfoHandlerInterceptor extends HandlerInterceptorAdapter { ...@@ -33,6 +34,7 @@ public class AuthInfoHandlerInterceptor extends HandlerInterceptorAdapter {
AuthPojo auth = authInfoHelper.getAuth(); AuthPojo auth = authInfoHelper.getAuth();
String sessionId = authInfoHelper.getOrCreateSessionId(); String sessionId = authInfoHelper.getOrCreateSessionId();
modelAndView.addObject("_sessionId", sessionId); modelAndView.addObject("_sessionId", sessionId);
modelAndView.addObject("_authClients", configuredAuthClientNames);
if (auth!=null && auth.isAuth()) { if (auth!=null && auth.isAuth()) {
modelAndView.addObject("_auth", auth); modelAndView.addObject("_auth", auth);
setUserLocale(request, response, auth.getLanguage()); setUserLocale(request, response, auth.getLanguage());
......
package eu.dariah.de.dariahsp.web.controller;
import java.util.Map;
import java.util.Optional;
import org.pac4j.core.client.Client;
import org.pac4j.core.config.Config;
import org.pac4j.core.context.JEEContext;
import org.pac4j.core.exception.http.HttpAction;
import org.pac4j.core.exception.http.RedirectionActionHelper;
import org.pac4j.core.http.adapter.JEEHttpActionAdapter;
import org.pac4j.http.client.indirect.FormClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import eu.dariah.de.dariahsp.config.SecurityConfig;
import eu.dariah.de.dariahsp.error.AuthenticatorNotAvailable;
import eu.dariah.de.dariahsp.web.AuthInfoHelper;
@Controller
public class CommonLoginController {
@Autowired protected AuthInfoHelper authInfoHelper;
@Autowired private SecurityConfig securityConfig;
@Autowired private Config config;
@Autowired private JEEContext jeeContext;
private final String loginView;
public CommonLoginController() {
this.loginView = "login";
}
public CommonLoginController(String loginView) {
this.loginView = loginView;
}
/**
* Present the local login form; override if the desired view is not 'login'
*
* @param map
* @param requestUrl
* @return login view
*/
@RequestMapping("/login")
public String loginForm(Map<String, Object> map, @RequestParam(value = "url", required = false) String requestUrl, @RequestParam(value = "error", required = false) String error) {
final FormClient formClient = (FormClient) config.getClients().findClient(securityConfig.getLocal().getAuthorizerName()).orElseThrow();
map.put("callbackUrl", formClient.getCallbackUrl());
map.put("requestUrl", requestUrl);
map.put("error", error);
return loginView;
}
/**
* Mapping that requires authentication and thus redirects to configured client[0] if a user is not authenticated
* A provided callbackUrl is used for redirecting a user to a specified URL post-login.
*
* @param requestUrl
* @return redirection
*/
@PreAuthorize("isAuthenticated()")
@RequestMapping("/filteredLogin")
@ResponseBody
public String filteredLogin(@RequestParam(value = "url", required = false) String requestUrl) {
HttpAction action;
try {
action = RedirectionActionHelper.buildRedirectUrlAction(jeeContext, requestUrl!=null ? requestUrl : securityConfig.getDefaultLoginUrl());
} catch (final HttpAction e) {
action = e;
}
JEEHttpActionAdapter.INSTANCE.adapt(action, jeeContext);
return null;
}
/**
* Initiate a login based on a requested authentication client specified in terms of a client_name GET parameter
*
* @param client_name name of the authentication client, null selects default
* @return redirection
*/
@RequestMapping("/startLogin")
@ResponseBody
public String forceLogin() {
final String clientName = Optional.ofNullable(authInfoHelper.getRequestedClientName()).orElse(securityConfig.getEnabledClientNames().get(0));
final Client<?> client = config.getClients().findClient(clientName).orElse(null);
if (client==null) {
throw new AuthenticatorNotAvailable(clientName);
}
HttpAction action;
try {
action = client.getRedirectionAction(jeeContext).orElseThrow(() -> new AuthenticatorNotAvailable(clientName));
} catch (final HttpAction e) {
action = e;
}
JEEHttpActionAdapter.INSTANCE.adapt(action, jeeContext);
return null;
}
}
...@@ -33,6 +33,8 @@ auth: ...@@ -33,6 +33,8 @@ auth:
local: ["application_user"] local: ["application_user"]
saml: ["application_user"] saml: ["application_user"]
local: local:
# The client with the lowest number here is used when selecting a default authenticator
order: 0
# Enable local authentication # Enable local authentication
enabled: true enabled: true
# Name of the method # Name of the method
...@@ -52,6 +54,7 @@ auth: ...@@ -52,6 +54,7 @@ auth:
passhash: '$2y$10$nmTcpRxs.RFUstkJJms6U.AW61Jmr64s9VNQLuhpU8gYrgzCapwka' passhash: '$2y$10$nmTcpRxs.RFUstkJJms6U.AW61Jmr64s9VNQLuhpU8gYrgzCapwka'
roles: ["application_user"] roles: ["application_user"]
saml: saml:
order: 1
# Enable SAML authentication # Enable SAML authentication
enabled: false enabled: false
# Name of the method # Name of the method
......
...@@ -11,6 +11,7 @@ import org.springframework.context.annotation.Configuration; ...@@ -11,6 +11,7 @@ import org.springframework.context.annotation.Configuration;
import eu.dariah.de.dariahsp.CustomizableProfileManager; import eu.dariah.de.dariahsp.CustomizableProfileManager;
import eu.dariah.de.dariahsp.ProfileActionHandler; import eu.dariah.de.dariahsp.ProfileActionHandler;
import eu.dariah.de.dariahsp.sample.profiles.SampleProfileActionHandler; import eu.dariah.de.dariahsp.sample.profiles.SampleProfileActionHandler;
import eu.dariah.de.dariahsp.web.controller.CommonLoginController;
import eu.dariah.de.dariahsp.web.controller.SAMLMetadataController; import eu.dariah.de.dariahsp.web.controller.SAMLMetadataController;
@Configuration @Configuration
...@@ -57,4 +58,14 @@ public class SampleConfig { ...@@ -57,4 +58,14 @@ public class SampleConfig {
public SAMLMetadataController samlMetadataController() { public SAMLMetadataController samlMetadataController() {
return new SAMLMetadataController(); return new SAMLMetadataController();
} }
/**
* Controller that binds to common login mappings
*
* @return LoginLogoutController bean
*/
@Bean
public CommonLoginController loginLogoutController() {
return new CommonLoginController();
}
} }
...@@ -2,23 +2,13 @@ package eu.dariah.de.dariahsp.sample.controller; ...@@ -2,23 +2,13 @@ package eu.dariah.de.dariahsp.sample.controller;
import java.util.Map; import java.util.Map;
import org.pac4j.core.client.Client;
import org.pac4j.core.config.Config;
import org.pac4j.core.context.JEEContext;
import org.pac4j.core.exception.http.HttpAction;
import org.pac4j.core.http.adapter.JEEHttpActionAdapter;
import org.pac4j.http.client.indirect.FormClient;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.annotation.Secured; import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import eu.dariah.de.dariahsp.config.SecurityConfig; import eu.dariah.de.dariahsp.config.SecurityConfig;
import eu.dariah.de.dariahsp.error.AuthenticatorNotAvailable;
import eu.dariah.de.dariahsp.web.AuthInfoHelper;
/** /**
* Main controller of the sample application. All views are handled in the index JSP. * Main controller of the sample application. All views are handled in the index JSP.
...@@ -32,11 +22,7 @@ public class SampleController { ...@@ -32,11 +22,7 @@ public class SampleController {
private static String INDEX_PAGE = "index"; private static String INDEX_PAGE = "index";
@Autowired private SecurityConfig securityConfig; @Autowired private SecurityConfig securityConfig;
@Autowired private Config config;
@Autowired private JEEContext jeeContext;
@Autowired private AuthInfoHelper authInfoHelper;
@GetMapping("/") @GetMapping("/")
public String home(Map<String, Object> map) { public String home(Map<String, Object> map) {
this.assembleMap(map, this.assembleMap(map,
...@@ -94,33 +80,7 @@ public class SampleController { ...@@ -94,33 +80,7 @@ public class SampleController {
"No access at all"); "No access at all");
return INDEX_PAGE; return INDEX_PAGE;
} }
@RequestMapping("/login")
public String loginForm(Map<String, Object> map) {
final FormClient formClient = (FormClient) config.getClients().findClient(securityConfig.getLocal().getAuthorizerName()).get();
map.put("callbackUrl", formClient.getCallbackUrl());
return "login";
}
@RequestMapping("/forceLogin")
@ResponseBody
public String forceLogin() {
final String clientName = authInfoHelper.getRequestedClientName();
final Client<?> client = config.getClients().findClient(clientName).orElse(null);
if (client==null) {
throw new AuthenticatorNotAvailable(clientName);
}
HttpAction action;
try {
action = client.getRedirectionAction(jeeContext).orElseThrow(() -> new AuthenticatorNotAvailable(clientName));
} catch (final HttpAction e) {
action = e;
}
JEEHttpActionAdapter.INSTANCE.adapt(action, jeeContext);
return null;
}
private void assembleMap(Map<String, Object> map, String page, String restrictions) { private void assembleMap(Map<String, Object> map, String page, String restrictions) {
// _auth and _sessionId are inserted by the AuthInfoHandlerInterceptor // _auth and _sessionId are inserted by the AuthInfoHandlerInterceptor
map.put("requestedPage", page); map.put("requestedPage", page);
......
#contextPath: /contextpath #contextPath: /dme
#baseUrl: https://externally.visible.example.com${contextPath:/} #baseUrl: https://c105-229.cloud.gwdg.de${contextPath:/}
baseUrl: http://localhost:8080 baseUrl: http://localhost:8080
spring: spring:
...@@ -35,6 +35,7 @@ auth: ...@@ -35,6 +35,7 @@ auth:
local: ["application_user"] local: ["application_user"]
saml: ["application_user"] saml: ["application_user"]
local: local:
order: 0
enabled: true enabled: true
authorizerName: local authorizerName: local
# Same password for each user: 1234 # Same password for each user: 1234
...@@ -49,7 +50,8 @@ auth: ...@@ -49,7 +50,8 @@ auth:
passhash: '$2y$10$nmTcpRxs.RFUstkJJms6U.AW61Jmr64s9VNQLuhpU8gYrgzCapwka' passhash: '$2y$10$nmTcpRxs.RFUstkJJms6U.AW61Jmr64s9VNQLuhpU8gYrgzCapwka'
roles: ["application_user"] roles: ["application_user"]
saml: saml:
enabled: false order: 1
enabled: true
authorizerName: saml authorizerName: saml
keystore: keystore: