From 18e8d1c1c05a6d4b9efdade20d09a2d5d1e67c6f Mon Sep 17 00:00:00 2001 From: Daniel Gerhardt <code@dgerhardt.net> Date: Wed, 7 Feb 2018 20:48:14 +0100 Subject: [PATCH] Refactor authentication management * AuthenticationProvider-agnostic User object allows access to common user attributes: userId, loginId, authProvider, etc. * Auto-create UserProfiles for external accounts * Use userId (instead of loginId) for permission checks * Use custom implementation for Pac4j integration (remove org.pac4j.spring-security-pac4j) * Move authentication logic from controller to service layer * Remove user room role handling * Rename targetDomainType 'session' to 'room' --- pom.xml | 5 - .../de/thm/arsnova/aop/UserRoomAspect.java | 48 ------ .../de/thm/arsnova/config/SecurityConfig.java | 51 +++--- .../v2/AuthenticationController.java | 112 ++++--------- .../controller/v2/SocketController.java | 5 - .../arsnova/controller/v2/UserController.java | 4 - .../arsnova/entities/UserAuthentication.java | 73 +++----- .../de/thm/arsnova/entities/UserProfile.java | 10 ++ .../ApplicationPermissionEvaluator.java | 91 +++++----- .../security/CasUserDetailsService.java | 20 +-- .../security/CustomLdapUserDetailsMapper.java | 23 ++- .../security/GuestUserDetailsService.java | 60 +++++++ ...java => RegisteredUserDetailsService.java} | 55 +++--- .../java/de/thm/arsnova/security/User.java | 113 +++++++++++++ .../arsnova/security/pac4j/OAuthToken.java | 57 +++++++ .../security/pac4j/OauthCallbackFilter.java | 69 ++++++++ .../security/pac4j/OauthCallbackHandler.java | 58 +++++++ .../pac4j/OauthUserDetailsService.java | 74 ++++++++ .../arsnova/services/ContentServiceImpl.java | 6 +- .../thm/arsnova/services/MotdServiceImpl.java | 8 +- .../thm/arsnova/services/RoomServiceImpl.java | 33 ++-- .../thm/arsnova/services/UserRoomService.java | 49 ------ .../arsnova/services/UserRoomServiceImpl.java | 97 ----------- .../de/thm/arsnova/services/UserService.java | 16 +- .../thm/arsnova/services/UserServiceImpl.java | 158 ++++++++++-------- src/main/resources/META-INF/aop.xml | 1 - .../de/thm/arsnova/config/TestAppConfig.java | 9 +- .../v2/AuthenticationControllerTest.java | 19 ++- .../thm/arsnova/services/StubUserService.java | 13 +- .../thm/arsnova/services/UserServiceTest.java | 8 +- 30 files changed, 771 insertions(+), 574 deletions(-) delete mode 100644 src/main/java/de/thm/arsnova/aop/UserRoomAspect.java create mode 100644 src/main/java/de/thm/arsnova/security/GuestUserDetailsService.java rename src/main/java/de/thm/arsnova/security/{DbUserDetailsService.java => RegisteredUserDetailsService.java} (51%) create mode 100644 src/main/java/de/thm/arsnova/security/User.java create mode 100644 src/main/java/de/thm/arsnova/security/pac4j/OAuthToken.java create mode 100644 src/main/java/de/thm/arsnova/security/pac4j/OauthCallbackFilter.java create mode 100644 src/main/java/de/thm/arsnova/security/pac4j/OauthCallbackHandler.java create mode 100644 src/main/java/de/thm/arsnova/security/pac4j/OauthUserDetailsService.java delete mode 100644 src/main/java/de/thm/arsnova/services/UserRoomService.java delete mode 100644 src/main/java/de/thm/arsnova/services/UserRoomServiceImpl.java diff --git a/pom.xml b/pom.xml index 6f042b624..87103fb5c 100644 --- a/pom.xml +++ b/pom.xml @@ -250,11 +250,6 @@ <artifactId>junit</artifactId> <scope>test</scope> </dependency> - <dependency> - <groupId>org.pac4j</groupId> - <artifactId>spring-security-pac4j</artifactId> - <version>3.1.0</version> - </dependency> <dependency> <groupId>org.pac4j</groupId> <artifactId>pac4j-oauth</artifactId> diff --git a/src/main/java/de/thm/arsnova/aop/UserRoomAspect.java b/src/main/java/de/thm/arsnova/aop/UserRoomAspect.java deleted file mode 100644 index 0c108f438..000000000 --- a/src/main/java/de/thm/arsnova/aop/UserRoomAspect.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors - * - * ARSnova Backend is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ARSnova Backend is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -package de.thm.arsnova.aop; - -import de.thm.arsnova.entities.migration.v2.Room; -import de.thm.arsnova.services.UserRoomService; -import org.aspectj.lang.JoinPoint; -import org.aspectj.lang.annotation.AfterReturning; -import org.aspectj.lang.annotation.Aspect; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Configurable; - -/** - * Assigns a room to the {@link UserRoomService} whenever a user joins a - * room. - */ -@Aspect -@Configurable -public class UserRoomAspect { - - @Autowired - private UserRoomService userRoomService; - - /** Sets current user and ARSnova room in session scoped UserRoomService - */ - @AfterReturning( - pointcut = "execution(public * de.thm.arsnova.services.RoomService.join(..)) && args(keyword)", - returning = "room" - ) - public void joinSessionAdvice(final JoinPoint jp, final String keyword, final Room room) { - userRoomService.setRoom(room); - } -} diff --git a/src/main/java/de/thm/arsnova/config/SecurityConfig.java b/src/main/java/de/thm/arsnova/config/SecurityConfig.java index 6874343f3..c44247199 100644 --- a/src/main/java/de/thm/arsnova/config/SecurityConfig.java +++ b/src/main/java/de/thm/arsnova/config/SecurityConfig.java @@ -21,16 +21,17 @@ import de.thm.arsnova.security.CasLogoutSuccessHandler; import de.thm.arsnova.security.CasUserDetailsService; import de.thm.arsnova.security.LoginAuthenticationFailureHandler; import de.thm.arsnova.security.LoginAuthenticationSucessHandler; -import de.thm.arsnova.security.ApplicationPermissionEvaluator; import de.thm.arsnova.security.CustomLdapUserDetailsMapper; -import de.thm.arsnova.security.DbUserDetailsService; +import de.thm.arsnova.security.RegisteredUserDetailsService; +import de.thm.arsnova.security.pac4j.OauthCallbackFilter; +import de.thm.arsnova.security.pac4j.OauthCallbackHandler; import org.jasig.cas.client.validation.Cas20ProxyTicketValidator; import org.pac4j.core.client.Client; import org.pac4j.core.config.Config; +import org.pac4j.core.context.J2EContext; import org.pac4j.oauth.client.FacebookClient; import org.pac4j.oauth.client.Google2Client; import org.pac4j.oauth.client.TwitterClient; -import org.pac4j.springframework.security.web.CallbackFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -43,8 +44,6 @@ import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileSystemResource; import org.springframework.ldap.core.support.LdapContextSource; -import org.springframework.security.access.PermissionEvaluator; -import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.cas.ServiceProperties; import org.springframework.security.cas.authentication.CasAuthenticationProvider; @@ -88,7 +87,9 @@ import java.util.List; @EnableWebSecurity @Profile("!test") public class SecurityConfig extends WebSecurityConfigurerAdapter { - private static final String OAUTH_CALLBACK_PATH_SUFFIX = "/auth/oauth_callback"; + public static final String OAUTH_CALLBACK_PATH_SUFFIX = "/auth/oauth_callback"; + public static final String CAS_LOGIN_PATH_SUFFIX = "/auth/login/cas"; + public static final String CAS_LOGOUT_PATH_SUFFIX = "/auth/logout/cas"; private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class); @Autowired @@ -143,7 +144,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { } if (facebookEnabled || googleEnabled || twitterEnabled) { - CallbackFilter callbackFilter = new CallbackFilter(oauthConfig()); + OauthCallbackFilter callbackFilter = new OauthCallbackFilter(oauthCallbackHandler(), oauthConfig()); callbackFilter.setSuffix(OAUTH_CALLBACK_PATH_SUFFIX); callbackFilter.setDefaultUrl(rootUrl + apiPath + "/"); http.addFilterAfter(callbackFilter, CasAuthenticationFilter.class); @@ -153,10 +154,6 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { List<String> providers = new ArrayList<>(); - if (dbAuthEnabled) { - providers.add("user-db"); - auth.authenticationProvider(daoAuthenticationProvider()); - } if (ldapEnabled) { providers.add("ldap"); auth.authenticationProvider(ldapAuthenticationProvider()); @@ -165,6 +162,10 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { providers.add("cas"); auth.authenticationProvider(casAuthenticationProvider()); } + if (dbAuthEnabled) { + providers.add("user-db"); + auth.authenticationProvider(daoAuthenticationProvider()); + } if (googleEnabled) { providers.add("google"); } @@ -177,12 +178,6 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { logger.info("Enabled authentication providers: {}", providers); } - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManager(); - } - @Bean public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { final PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer(); @@ -201,11 +196,6 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { return new SessionRegistryImpl(); } - @Bean - public PermissionEvaluator permissionEvaluator() { - return new ApplicationPermissionEvaluator(); - } - @Bean public static AuthenticationEntryPoint restAuthenticationEntryPoint() { return new Http403ForbiddenEntryPoint(); @@ -232,7 +222,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public DaoAuthenticationProvider daoAuthenticationProvider() { final DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); - authProvider.setUserDetailsService(dbUserDetailsService()); + authProvider.setUserDetailsService(registeredUserDetailsService()); authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; @@ -244,8 +234,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { } @Bean - public DbUserDetailsService dbUserDetailsService() { - return new DbUserDetailsService(); + public RegisteredUserDetailsService registeredUserDetailsService() { + return new RegisteredUserDetailsService(); } @Bean @@ -324,7 +314,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public ServiceProperties casServiceProperties() { ServiceProperties properties = new ServiceProperties(); - properties.setService(rootUrl + apiPath + "/login/cas"); + properties.setService(rootUrl + apiPath + CAS_LOGIN_PATH_SUFFIX); properties.setSendRenew(false); return properties; @@ -348,6 +338,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { public CasAuthenticationFilter casAuthenticationFilter() throws Exception { CasAuthenticationFilter filter = new CasAuthenticationFilter(); filter.setAuthenticationManager(authenticationManager()); + filter.setServiceProperties(casServiceProperties()); + filter.setFilterProcessesUrl("/**" + CAS_LOGIN_PATH_SUFFIX); filter.setAuthenticationSuccessHandler(successHandler()); filter.setAuthenticationFailureHandler(failureHandler()); @@ -357,7 +349,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public LogoutFilter casLogoutFilter() { LogoutFilter filter = new LogoutFilter(casLogoutSuccessHandler(), logoutHandler()); - filter.setLogoutRequestMatcher(new AntPathRequestMatcher("/j_spring_cas_security_logout")); + filter.setLogoutRequestMatcher(new AntPathRequestMatcher("/**" + CAS_LOGOUT_PATH_SUFFIX)); return filter; } @@ -389,6 +381,11 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { return new Config(rootUrl + apiPath + OAUTH_CALLBACK_PATH_SUFFIX, clients); } + @Bean + public OauthCallbackHandler<Object, J2EContext> oauthCallbackHandler() { + return new OauthCallbackHandler<>(); + } + @Bean public FacebookClient facebookClient() { final FacebookClient client = new FacebookClient(facebookKey, facebookSecret); diff --git a/src/main/java/de/thm/arsnova/controller/v2/AuthenticationController.java b/src/main/java/de/thm/arsnova/controller/v2/AuthenticationController.java index 4be1d4923..42448b049 100644 --- a/src/main/java/de/thm/arsnova/controller/v2/AuthenticationController.java +++ b/src/main/java/de/thm/arsnova/controller/v2/AuthenticationController.java @@ -17,12 +17,13 @@ */ package de.thm.arsnova.controller.v2; +import de.thm.arsnova.config.SecurityConfig; import de.thm.arsnova.controller.AbstractController; import de.thm.arsnova.entities.ServiceDescription; import de.thm.arsnova.entities.UserAuthentication; -import de.thm.arsnova.entities.migration.v2.Room; +import de.thm.arsnova.entities.UserProfile; import de.thm.arsnova.exceptions.UnauthorizedException; -import de.thm.arsnova.services.UserRoomService; +import de.thm.arsnova.security.User; import de.thm.arsnova.services.UserService; import org.pac4j.core.context.J2EContext; import org.pac4j.core.exception.HttpAction; @@ -35,17 +36,15 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.cas.authentication.CasAuthenticationToken; import org.springframework.security.cas.web.CasAuthenticationEntryPoint; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.token.Sha512DigestUtils; import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; -import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.security.web.util.UrlUtils; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -72,10 +71,6 @@ import java.util.List; @Controller("v2AuthenticationController") @RequestMapping("/v2/auth") public class AuthenticationController extends AbstractController { - - private static final int MAX_USERNAME_LENGTH = 15; - private static final int MAX_GUESTHASH_LENGTH = 10; - @Value("${api.path:}") private String apiPath; @Value("${customization.path}") private String customizationPath; @@ -125,9 +120,6 @@ public class AuthenticationController extends AbstractController { @Autowired private ServletContext servletContext; - @Autowired(required = false) - private DaoAuthenticationProvider daoProvider; - @Autowired(required = false) private TwitterClient twitterClient; @@ -137,18 +129,12 @@ public class AuthenticationController extends AbstractController { @Autowired(required = false) private FacebookClient facebookClient; - @Autowired(required = false) - private LdapAuthenticationProvider ldapAuthenticationProvider; - @Autowired(required = false) private CasAuthenticationEntryPoint casEntryPoint; @Autowired private UserService userService; - @Autowired - private UserRoomService userRoomService; - private static final Logger logger = LoggerFactory.getLogger(AuthenticationController.class); @PostConstruct @@ -163,7 +149,6 @@ public class AuthenticationController extends AbstractController { @RequestParam("type") final String type, @RequestParam(value = "user", required = false) String username, @RequestParam(required = false) final String password, - @RequestParam(value = "role", required = false) final UserRoomService.Role role, final HttpServletRequest request, final HttpServletResponse response ) throws IOException { @@ -173,76 +158,33 @@ public class AuthenticationController extends AbstractController { return; } - - userRoomService.setRole(role); + final UsernamePasswordAuthenticationToken authRequest = + new UsernamePasswordAuthenticationToken(username, password); if (dbAuthEnabled && "arsnova".equals(type)) { - Authentication authRequest = new UsernamePasswordAuthenticationToken(username, password); try { - Authentication auth = daoProvider.authenticate(authRequest); - if (auth.isAuthenticated()) { - SecurityContextHolder.getContext().setAuthentication(auth); - request.getSession(true).setAttribute( - HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, - SecurityContextHolder.getContext()); - - return; - } + userService.authenticate(authRequest, UserProfile.AuthProvider.ARSNOVA); } catch (AuthenticationException e) { logger.info("Database authentication failed.", e); + userService.increaseFailedLoginCount(addr); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); } - - userService.increaseFailedLoginCount(addr); - response.setStatus(HttpStatus.UNAUTHORIZED.value()); } else if (ldapEnabled && "ldap".equals(type)) { - if (!"".equals(username) && !"".equals(password)) { - org.springframework.security.core.userdetails.User user = - new org.springframework.security.core.userdetails.User( - username, password, true, true, true, true, this.getAuthorities(userService.isAdmin(username)) - ); - - Authentication token = new UsernamePasswordAuthenticationToken(user, password, getAuthorities(userService.isAdmin(username))); - try { - Authentication auth = ldapAuthenticationProvider.authenticate(token); - if (auth.isAuthenticated()) { - SecurityContextHolder.getContext().setAuthentication(auth); - request.getSession(true).setAttribute( - HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, - SecurityContextHolder.getContext()); - - return; - } - logger.info("LDAP authentication failed."); - } catch (AuthenticationException e) { - logger.info("LDAP authentication failed.", e); - } - + try { + userService.authenticate(authRequest, UserProfile.AuthProvider.LDAP); + } catch (AuthenticationException e) { + logger.info("LDAP authentication failed.", e); userService.increaseFailedLoginCount(addr); response.setStatus(HttpStatus.UNAUTHORIZED.value()); } } else if (guestEnabled && "guest".equals(type)) { - if (username == null || !username.startsWith("Guest")) { - username = "Guest" + userService.createGuest(); - } else { - if (!userService.guestExists(username.substring(5))) { - userService.increaseFailedLoginCount(addr); - response.setStatus(HttpStatus.UNAUTHORIZED.value()); - logger.debug("Guest authentication failed."); - - return; - } + try { + userService.authenticate(authRequest, UserProfile.AuthProvider.ARSNOVA_GUEST); + } catch (final AuthenticationException e) { + logger.debug("Guest authentication failed.", e); + userService.increaseFailedLoginCount(addr); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); } - List<GrantedAuthority> authorities = new ArrayList<>(); - authorities.add(new SimpleGrantedAuthority("ROLE_GUEST")); - org.springframework.security.core.userdetails.User user = - new org.springframework.security.core.userdetails.User( - username, "", true, true, true, true, authorities - ); - Authentication token = new UsernamePasswordAuthenticationToken(user, null, authorities); - - SecurityContextHolder.getContext().setAuthentication(token); - request.getSession(true).setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, - SecurityContextHolder.getContext()); } else { response.setStatus(HttpStatus.BAD_REQUEST.value()); } @@ -309,21 +251,23 @@ public class AuthenticationController extends AbstractController { @RequestMapping(value = { "/", "/whoami" }, method = RequestMethod.GET) @ResponseBody - public UserAuthentication whoami() { - userRoomService.setUser(userService.getCurrentUser()); - return userService.getCurrentUser(); + public UserAuthentication whoami(@AuthenticationPrincipal User user) { + if (user == null) { + throw new UnauthorizedException(); + } + return new UserAuthentication(user); } @RequestMapping(value = { "/logout" }, method = { RequestMethod.POST, RequestMethod.GET }) - public View doLogout(final HttpServletRequest request) { + public String doLogout(final HttpServletRequest request) { final Authentication auth = SecurityContextHolder.getContext().getAuthentication(); userService.removeUserFromMaps(userService.getCurrentUser()); request.getSession().invalidate(); SecurityContextHolder.clearContext(); if (auth instanceof CasAuthenticationToken) { - return new RedirectView(apiPath + "/j_spring_cas_security_logout"); + return "redirect:" + apiPath + SecurityConfig.CAS_LOGOUT_PATH_SUFFIX; } - return new RedirectView(request.getHeader("referer") != null ? request.getHeader("referer") : "/"); + return "redirect:" + request.getHeader("referer") != null ? request.getHeader("referer") : "/"; } @RequestMapping(value = { "/services" }, method = RequestMethod.GET) @@ -332,7 +276,7 @@ public class AuthenticationController extends AbstractController { List<ServiceDescription> services = new ArrayList<>(); /* The first parameter is replaced by the backend, the second one by the frondend */ - String dialogUrl = apiPath + "/v2/auth/dialog?type={0}&successurl='{0}'"; + String dialogUrl = apiPath + "/auth/dialog?type={0}&successurl='{0}'"; if (guestEnabled) { ServiceDescription sdesc = new ServiceDescription( diff --git a/src/main/java/de/thm/arsnova/controller/v2/SocketController.java b/src/main/java/de/thm/arsnova/controller/v2/SocketController.java index b76e8aa94..8899dfa50 100644 --- a/src/main/java/de/thm/arsnova/controller/v2/SocketController.java +++ b/src/main/java/de/thm/arsnova/controller/v2/SocketController.java @@ -19,7 +19,6 @@ package de.thm.arsnova.controller.v2; import de.thm.arsnova.controller.AbstractController; import de.thm.arsnova.entities.UserAuthentication; -import de.thm.arsnova.services.UserRoomService; import de.thm.arsnova.services.UserService; import de.thm.arsnova.websocket.ArsnovaSocketioServer; import io.swagger.annotations.Api; @@ -52,9 +51,6 @@ public class SocketController extends AbstractController { @Autowired private UserService userService; - @Autowired - private UserRoomService userRoomService; - @Autowired private ArsnovaSocketioServer server; @@ -82,7 +78,6 @@ public class SocketController extends AbstractController { return; } userService.putUserToSocketId(UUID.fromString(socketid), u); - userRoomService.setSocketId(UUID.fromString(socketid)); response.setStatus(HttpStatus.NO_CONTENT.value()); } diff --git a/src/main/java/de/thm/arsnova/controller/v2/UserController.java b/src/main/java/de/thm/arsnova/controller/v2/UserController.java index 9f684b5f6..d07000979 100644 --- a/src/main/java/de/thm/arsnova/controller/v2/UserController.java +++ b/src/main/java/de/thm/arsnova/controller/v2/UserController.java @@ -19,7 +19,6 @@ package de.thm.arsnova.controller.v2; import de.thm.arsnova.controller.AbstractController; import de.thm.arsnova.entities.UserProfile; -import de.thm.arsnova.services.UserRoomService; import de.thm.arsnova.services.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; @@ -44,9 +43,6 @@ public class UserController extends AbstractController { @Autowired private UserService userService; - @Autowired - private UserRoomService userRoomService; - @RequestMapping(value = "/register", method = RequestMethod.POST) public void register(@RequestParam final String username, @RequestParam final String password, diff --git a/src/main/java/de/thm/arsnova/entities/UserAuthentication.java b/src/main/java/de/thm/arsnova/entities/UserAuthentication.java index 69b9e0dd1..fb8dea946 100644 --- a/src/main/java/de/thm/arsnova/entities/UserAuthentication.java +++ b/src/main/java/de/thm/arsnova/entities/UserAuthentication.java @@ -19,13 +19,9 @@ package de.thm.arsnova.entities; import com.fasterxml.jackson.annotation.JsonView; import de.thm.arsnova.entities.serialization.View; -import de.thm.arsnova.services.UserRoomService; -import org.jasig.cas.client.authentication.AttributePrincipal; -import org.pac4j.oauth.profile.facebook.FacebookProfile; -import org.pac4j.oauth.profile.google2.Google2Profile; -import org.pac4j.oauth.profile.twitter.TwitterProfile; +import de.thm.arsnova.security.User; import org.springframework.security.authentication.AnonymousAuthenticationToken; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import java.io.Serializable; import java.util.Objects; @@ -40,36 +36,33 @@ public class UserAuthentication implements Serializable { private String id; private String username; private UserProfile.AuthProvider authProvider; - private UserRoomService.Role role; private boolean isAdmin; - public UserAuthentication(final Google2Profile profile) { - setUsername(profile.getEmail()); - setAuthProvider(UserProfile.AuthProvider.GOOGLE); - } - - public UserAuthentication(final TwitterProfile profile) { - setUsername(profile.getUsername()); - setAuthProvider(UserProfile.AuthProvider.TWITTER); - } - - public UserAuthentication(final FacebookProfile profile) { - setUsername(profile.getProfileUrl().toString()); - setAuthProvider(UserProfile.AuthProvider.FACEBOOK); - } - - public UserAuthentication(final AttributePrincipal principal) { - setUsername(principal.getName()); - setAuthProvider(UserProfile.AuthProvider.CAS); - } - - public UserAuthentication(final UsernamePasswordAuthenticationToken token) { - setUsername(token.getName()); - setAuthProvider(UserProfile.AuthProvider.LDAP); - } - - public UserAuthentication(final AnonymousAuthenticationToken token) { - setUsername(UserAuthentication.ANONYMOUS); + public UserAuthentication() { + username = ANONYMOUS; + authProvider = UserProfile.AuthProvider.NONE; + } + + public UserAuthentication(User user) { + id = user.getId(); + username = user.getUsername(); + authProvider = user.getAuthProvider(); + isAdmin = user.isAdmin(); + } + + public UserAuthentication(Authentication authentication) { + if (authentication instanceof AnonymousAuthenticationToken) { + setUsername(UserAuthentication.ANONYMOUS); + } else { + if (!(authentication.getPrincipal() instanceof User)) { + throw new IllegalArgumentException("Unsupported authentication token"); + } + User user = (User) authentication.getPrincipal(); + id = user.getId(); + username = user.getUsername(); + authProvider = user.getAuthProvider(); + isAdmin = user.isAdmin(); + } } public String getId() { @@ -98,18 +91,6 @@ public class UserAuthentication implements Serializable { this.authProvider = authProvider; } - public UserRoomService.Role getRole() { - return role; - } - - public void setRole(final UserRoomService.Role role) { - this.role = role; - } - - public boolean hasRole(UserRoomService.Role role) { - return this.role == role; - } - public void setAdmin(final boolean a) { this.isAdmin = a; } diff --git a/src/main/java/de/thm/arsnova/entities/UserProfile.java b/src/main/java/de/thm/arsnova/entities/UserProfile.java index 3b2229aa6..8474a8778 100644 --- a/src/main/java/de/thm/arsnova/entities/UserProfile.java +++ b/src/main/java/de/thm/arsnova/entities/UserProfile.java @@ -12,6 +12,7 @@ import java.util.Set; public class UserProfile implements Entity { public enum AuthProvider { + NONE, UNKNOWN, ARSNOVA, ARSNOVA_GUEST, @@ -111,6 +112,15 @@ public class UserProfile implements Entity { private Set<String> acknowledgedMotds = new HashSet<>(); private Map<String, Map<String, ?>> extensions; + public UserProfile() { + + } + + public UserProfile(final AuthProvider authProvider, final String loginId) { + this.authProvider = authProvider; + this.loginId = loginId; + } + @Override @JsonView({View.Persistence.class, View.Public.class}) public String getId() { diff --git a/src/main/java/de/thm/arsnova/security/ApplicationPermissionEvaluator.java b/src/main/java/de/thm/arsnova/security/ApplicationPermissionEvaluator.java index 7e0957e49..cbe620a40 100644 --- a/src/main/java/de/thm/arsnova/security/ApplicationPermissionEvaluator.java +++ b/src/main/java/de/thm/arsnova/security/ApplicationPermissionEvaluator.java @@ -18,21 +18,20 @@ package de.thm.arsnova.security; import de.thm.arsnova.entities.Room; -import de.thm.arsnova.entities.UserAuthentication; import de.thm.arsnova.entities.Comment; import de.thm.arsnova.entities.Content; +import de.thm.arsnova.entities.UserProfile; import de.thm.arsnova.persistance.CommentRepository; import de.thm.arsnova.persistance.ContentRepository; import de.thm.arsnova.persistance.RoomRepository; -import org.pac4j.oauth.profile.facebook.FacebookProfile; -import org.pac4j.oauth.profile.google2.Google2Profile; -import org.pac4j.oauth.profile.twitter.TwitterProfile; -import org.pac4j.springframework.security.authentication.Pac4jAuthenticationToken; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; import java.io.Serializable; import java.util.Arrays; @@ -40,7 +39,9 @@ import java.util.Arrays; /** * Provides access control methods that can be used in annotations. */ +@Component public class ApplicationPermissionEvaluator implements PermissionEvaluator { + private static final Logger logger = LoggerFactory.getLogger(ApplicationPermissionEvaluator.class); @Value("${security.admin-accounts}") private String[] adminAccounts; @@ -59,19 +60,22 @@ public class ApplicationPermissionEvaluator implements PermissionEvaluator { final Authentication authentication, final Object targetDomainObject, final Object permission) { + logger.debug("Evaluating permission: hasPermission({}, {}. {})", authentication, targetDomainObject, permission); if (authentication == null || targetDomainObject == null || !(permission instanceof String)) { return false; } - final String username = getUsername(authentication); + final String userId = getUserId(authentication); - return hasAdminRole(username) + return hasAdminRole(userId) + || (targetDomainObject instanceof UserProfile + && hasUserProfilePermission(userId, ((UserProfile) targetDomainObject), permission.toString())) || (targetDomainObject instanceof Room - && hasRoomPermission(username, ((Room) targetDomainObject), permission.toString())) + && hasRoomPermission(userId, ((Room) targetDomainObject), permission.toString())) || (targetDomainObject instanceof Content - && hasContentPermission(username, ((Content) targetDomainObject), permission.toString())) + && hasContentPermission(userId, ((Content) targetDomainObject), permission.toString())) || (targetDomainObject instanceof Comment - && hasCommentPermission(username, ((Comment) targetDomainObject), permission.toString())); + && hasCommentPermission(userId, ((Comment) targetDomainObject), permission.toString())); } @Override @@ -80,25 +84,48 @@ public class ApplicationPermissionEvaluator implements PermissionEvaluator { final Serializable targetId, final String targetType, final Object permission) { + logger.debug("Evaluating permission: hasPermission({}, {}, {}, {})", authentication, targetId, targetType, permission); if (authentication == null || targetId == null || targetType == null || !(permission instanceof String)) { return false; } - final String username = getUsername(authentication); - if (hasAdminRole(username)) { + final String userId = getUserId(authentication); + if (hasAdminRole(userId)) { return true; } switch (targetType) { - case "session": + case "userprofile": + final UserProfile targetUserProfile = new UserProfile(); + targetUserProfile.setId(targetId.toString()); + return hasUserProfilePermission(userId, targetUserProfile, permission.toString()); + case "room": final Room targetRoom = roomRepository.findByShortId(targetId.toString()); - return targetRoom != null && hasRoomPermission(username, targetRoom, permission.toString()); + return targetRoom != null && hasRoomPermission(userId, targetRoom, permission.toString()); case "content": final Content targetContent = contentRepository.findOne(targetId.toString()); - return targetContent != null && hasContentPermission(username, targetContent, permission.toString()); + return targetContent != null && hasContentPermission(userId, targetContent, permission.toString()); case "comment": final Comment targetComment = commentRepository.findOne(targetId.toString()); - return targetComment != null && hasCommentPermission(username, targetComment, permission.toString()); + return targetComment != null && hasCommentPermission(userId, targetComment, permission.toString()); + default: + return false; + } + } + + private boolean hasUserProfilePermission( + final String userId, + final UserProfile targetUserProfile, + final String permission) { + switch (permission) { + case "read": + return userId.equals(targetUserProfile.getId()); + case "create": + return true; + case "owner": + case "update": + case "delete": + return userId.equals(targetUserProfile.getId()); default: return false; } @@ -123,7 +150,7 @@ public class ApplicationPermissionEvaluator implements PermissionEvaluator { } private boolean hasContentPermission( - final String username, + final String userId, final Content targetContent, final String permission) { switch (permission) { @@ -134,7 +161,7 @@ public class ApplicationPermissionEvaluator implements PermissionEvaluator { case "update": case "delete": final Room room = roomRepository.findOne(targetContent.getRoomId()); - return room != null && room.getOwnerId().equals(username); + return room != null && room.getOwnerId().equals(userId); default: return false; } @@ -170,32 +197,14 @@ public class ApplicationPermissionEvaluator implements PermissionEvaluator { return Arrays.asList(adminAccounts).contains(username); } - private String getUsername(final Authentication authentication) { - if (authentication == null || authentication instanceof AnonymousAuthenticationToken) { + private String getUserId(final Authentication authentication) { + if (authentication == null || authentication instanceof AnonymousAuthenticationToken || + !(authentication.getPrincipal() instanceof User)) { return ""; } + User user = (User) authentication.getPrincipal(); - if (authentication instanceof Pac4jAuthenticationToken) { - UserAuthentication user = null; - - final Pac4jAuthenticationToken token = (Pac4jAuthenticationToken) authentication; - if (token.getProfile() instanceof Google2Profile) { - final Google2Profile profile = (Google2Profile) token.getProfile(); - user = new UserAuthentication(profile); - } else if (token.getProfile() instanceof TwitterProfile) { - final TwitterProfile profile = (TwitterProfile) token.getProfile(); - user = new UserAuthentication(profile); - } else if (token.getProfile() instanceof FacebookProfile) { - final FacebookProfile profile = (FacebookProfile) token.getProfile(); - user = new UserAuthentication(profile); - } - - if (user != null) { - return user.getUsername(); - } - } - - return authentication.getName(); + return user.getId(); } private boolean isWebsocketAccess(Authentication auth) { diff --git a/src/main/java/de/thm/arsnova/security/CasUserDetailsService.java b/src/main/java/de/thm/arsnova/security/CasUserDetailsService.java index fc048b2ff..430bb7493 100644 --- a/src/main/java/de/thm/arsnova/security/CasUserDetailsService.java +++ b/src/main/java/de/thm/arsnova/security/CasUserDetailsService.java @@ -17,18 +17,18 @@ */ package de.thm.arsnova.security; +import de.thm.arsnova.entities.UserProfile; import de.thm.arsnova.services.UserService; import org.jasig.cas.client.validation.Assertion; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.cas.userdetails.AbstractCasAssertionUserDetailsService; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.List; +import java.util.HashSet; +import java.util.Set; /** * Class to load a user based on the results from CAS. @@ -40,21 +40,15 @@ public class CasUserDetailsService extends AbstractCasAssertionUserDetailsServic @Override protected UserDetails loadUserDetails(final Assertion assertion) { - final List<GrantedAuthority> grantedAuthorities = new ArrayList<>(); + final Set<GrantedAuthority> grantedAuthorities = new HashSet<>(); grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER")); + grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_CAS_USER")); final String uid = assertion.getPrincipal().getName(); if (userService.isAdmin(uid)) { grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); } - return new User( - uid, - "", - true, - true, - true, - true, - grantedAuthorities - ); + return userService.loadUser(UserProfile.AuthProvider.CAS, assertion.getPrincipal().getName(), + grantedAuthorities, true); } } diff --git a/src/main/java/de/thm/arsnova/security/CustomLdapUserDetailsMapper.java b/src/main/java/de/thm/arsnova/security/CustomLdapUserDetailsMapper.java index c8086a0e5..822e3d31c 100644 --- a/src/main/java/de/thm/arsnova/security/CustomLdapUserDetailsMapper.java +++ b/src/main/java/de/thm/arsnova/security/CustomLdapUserDetailsMapper.java @@ -17,14 +17,19 @@ */ package de.thm.arsnova.security; +import de.thm.arsnova.entities.UserProfile; +import de.thm.arsnova.services.UserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.ldap.core.DirContextOperations; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper; import java.util.Collection; +import java.util.HashSet; /** * Replaces the user ID provided by the authenticating user with the one that is part of LDAP object. This is necessary @@ -35,18 +40,30 @@ public class CustomLdapUserDetailsMapper extends LdapUserDetailsMapper { private String userIdAttr; + @Autowired + private UserService userService; + public CustomLdapUserDetailsMapper(String ldapUserIdAttr) { this.userIdAttr = ldapUserIdAttr; } - public UserDetails mapUserFromContext(DirContextOperations ctx, String username, - Collection<? extends GrantedAuthority> authorities) { + public UserDetails mapUserFromContext( + final DirContextOperations ctx, + final String username, + final Collection<? extends GrantedAuthority> authorities) { String ldapUsername = ctx.getStringAttribute(userIdAttr); if (ldapUsername == null) { logger.warn("LDAP attribute {} not set. Falling back to lowercased user provided username.", userIdAttr); ldapUsername = username.toLowerCase(); } - return super.mapUserFromContext(ctx, ldapUsername, authorities); + final Collection<GrantedAuthority> grantedAuthorities = (Collection<GrantedAuthority>) authorities; + final Collection<GrantedAuthority> additionalAuthorities = new HashSet<>(); + additionalAuthorities.add(new SimpleGrantedAuthority("ROLE_USER")); + additionalAuthorities.add(new SimpleGrantedAuthority("ROLE_LDAP_USER")); + grantedAuthorities.addAll(additionalAuthorities); + + return userService.loadUser(UserProfile.AuthProvider.LDAP, ldapUsername, + grantedAuthorities, true); } } diff --git a/src/main/java/de/thm/arsnova/security/GuestUserDetailsService.java b/src/main/java/de/thm/arsnova/security/GuestUserDetailsService.java new file mode 100644 index 000000000..b262ee38f --- /dev/null +++ b/src/main/java/de/thm/arsnova/security/GuestUserDetailsService.java @@ -0,0 +1,60 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.security; + +import de.thm.arsnova.entities.UserProfile; +import de.thm.arsnova.services.UserService; +import de.thm.arsnova.services.UserService; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.HashSet; + +/** + * Loads UserDetails for a guest user ({@link UserProfile.AuthProvider#ARSNOVA_GUEST}) based on the username (guest + * token). + * + * @author Daniel Gerhardt + */ +@Service +public class GuestUserDetailsService implements UserDetailsService { + private final UserService userService; + private final Collection<GrantedAuthority> grantedAuthorities; + + public GuestUserDetailsService(UserService userService) { + this.userService = userService; + grantedAuthorities = new HashSet<>(); + grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER")); + grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_GUEST_USER")); + } + + @Override + public UserDetails loadUserByUsername(final String loginId) throws UsernameNotFoundException { + return loadUserByUsername(loginId, false); + } + + public UserDetails loadUserByUsername(final String loginId, final boolean autoCreate) { + return userService.loadUser(UserProfile.AuthProvider.ARSNOVA_GUEST, loginId, + grantedAuthorities, autoCreate); + } +} diff --git a/src/main/java/de/thm/arsnova/security/DbUserDetailsService.java b/src/main/java/de/thm/arsnova/security/RegisteredUserDetailsService.java similarity index 51% rename from src/main/java/de/thm/arsnova/security/DbUserDetailsService.java rename to src/main/java/de/thm/arsnova/security/RegisteredUserDetailsService.java index da0b3521c..bb68e9dc1 100644 --- a/src/main/java/de/thm/arsnova/security/DbUserDetailsService.java +++ b/src/main/java/de/thm/arsnova/security/RegisteredUserDetailsService.java @@ -1,6 +1,6 @@ /* * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * Copyright (C) 2012-2018 The ARSnova Team * * ARSnova Backend is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,54 +18,47 @@ package de.thm.arsnova.security; import de.thm.arsnova.entities.UserProfile; -import de.thm.arsnova.persistance.UserRepository; import de.thm.arsnova.services.UserService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.ArrayList; -import java.util.List; +import java.util.Collection; +import java.util.HashSet; /** - * Class to load a user based on the username. + * Loads UserDetails for a registered user ({@link UserProfile.AuthProvider#ARSNOVA}) based on the username (loginId). + * + * @author Daniel Gerhardt */ @Service -public class DbUserDetailsService implements UserDetailsService { - @Autowired - private UserRepository userRepository; - - @Autowired +public class RegisteredUserDetailsService implements UserDetailsService { private UserService userService; + private final Collection<GrantedAuthority> grantedAuthorities; - private static final Logger logger = LoggerFactory - .getLogger(DbUserDetailsService.class); + public RegisteredUserDetailsService() { + grantedAuthorities = new HashSet<>(); + grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER")); + grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_REGISTERED_USER")); + } @Override - public UserDetails loadUserByUsername(String username) { - String uid = username.toLowerCase(); - logger.debug("Load user: " + uid); - UserProfile userProfile = userRepository.findByAuthProviderAndLoginId(UserProfile.AuthProvider.ARSNOVA, uid); - if (null == userProfile) { - throw new UsernameNotFoundException("User does not exist."); - } - - final List<GrantedAuthority> grantedAuthorities = new ArrayList<>(); - grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER")); - grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_DB_USER")); - if (userService.isAdmin(uid)) { - grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); + public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException { + final String loginId = username.toLowerCase(); + Collection<GrantedAuthority> ga = grantedAuthorities; + if (userService.isAdmin(loginId)) { + ga = new ArrayList<>(grantedAuthorities); + ga.add(new SimpleGrantedAuthority("ROLE_ADMIN")); } + return userService.loadUser(UserProfile.AuthProvider.ARSNOVA, loginId, + ga, false); + } - return new User(uid, userProfile.getAccount().getPassword(), - null == userProfile.getAccount().getActivationKey(), - true, true, true, grantedAuthorities); + public void setUserService(final UserService userService) { + this.userService = userService; } } diff --git a/src/main/java/de/thm/arsnova/security/User.java b/src/main/java/de/thm/arsnova/security/User.java new file mode 100644 index 000000000..d8cd8f574 --- /dev/null +++ b/src/main/java/de/thm/arsnova/security/User.java @@ -0,0 +1,113 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.security; + +import de.thm.arsnova.entities.UserProfile; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +/** + * UserDetails implementation which identifies a user by internal (database) and external (AuthProvider + loginId) ID. + * + * @author Daniel Gerhardt + */ +public class User implements org.springframework.security.core.userdetails.UserDetails { + private static final long serialVersionUID = 1L; + + private String id; + private String loginId; + private UserProfile.AuthProvider authProvider; + private org.springframework.security.core.userdetails.UserDetails providerUserDetails; + private Collection<? extends GrantedAuthority> authorities; + private boolean enabled; + + public User(final UserProfile profile, final Collection<? extends GrantedAuthority> authorities) { + if (profile == null || profile.getId() == null) { + throw new IllegalArgumentException(); + } + id = profile.getId(); + loginId = profile.getLoginId(); + authProvider = profile.getAuthProvider(); + this.authorities = authorities; + enabled = profile.getAccount() == null || profile.getAccount().getActivationKey() == null; + } + + public User(final UserProfile profile, final Collection<? extends GrantedAuthority> authorities, + final org.springframework.security.core.userdetails.UserDetails details) { + this(profile, authorities); + providerUserDetails = details; + } + + @Override + public Collection<? extends GrantedAuthority> getAuthorities() { + return authorities; + } + + @Override + public String getPassword() { + return providerUserDetails != null ? providerUserDetails.getPassword() : null; + } + + @Override + public String getUsername() { + return loginId; + } + + @Override + public boolean isAccountNonExpired() { + return providerUserDetails == null || providerUserDetails.isAccountNonExpired(); + } + + @Override + public boolean isAccountNonLocked() { + return providerUserDetails == null || providerUserDetails.isAccountNonLocked(); + } + + @Override + public boolean isCredentialsNonExpired() { + return providerUserDetails == null || providerUserDetails.isCredentialsNonExpired(); + } + + @Override + public boolean isEnabled() { + return enabled && (providerUserDetails == null || providerUserDetails.isEnabled()); + } + + public UserProfile.AuthProvider getAuthProvider() { + return authProvider; + } + + public String getId() { + return id; + } + + public boolean hasRole(final String role) { + return getAuthorities().stream().anyMatch(ga -> ga.getAuthority().equals("ROLE_" + role)); + } + + public boolean isAdmin() { + return hasRole("ADMIN"); + } + + @Override + public String toString() { + return String.format("Id: %s, LoginId: %s, AuthProvider: %s, Admin: %b", + id, loginId, authProvider, isAdmin()); + } +} diff --git a/src/main/java/de/thm/arsnova/security/pac4j/OAuthToken.java b/src/main/java/de/thm/arsnova/security/pac4j/OAuthToken.java new file mode 100644 index 000000000..994bf74b6 --- /dev/null +++ b/src/main/java/de/thm/arsnova/security/pac4j/OAuthToken.java @@ -0,0 +1,57 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.security.pac4j; + +import de.thm.arsnova.security.User; +import org.pac4j.core.profile.CommonProfile; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +/** + * Authentication token implementation for OAuth. + * + * @author Daniel Gerhardt + */ +public class OAuthToken extends AbstractAuthenticationToken { + private User principal; + + public OAuthToken(User principal, CommonProfile profile, + Collection<? extends GrantedAuthority> grantedAuthorities) { + super(grantedAuthorities); + this.principal = principal; + this.setDetails(profile); + setAuthenticated(!grantedAuthorities.isEmpty()); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return principal; + } + + @Override + public boolean isAuthenticated() { + return super.isAuthenticated(); + } +} diff --git a/src/main/java/de/thm/arsnova/security/pac4j/OauthCallbackFilter.java b/src/main/java/de/thm/arsnova/security/pac4j/OauthCallbackFilter.java new file mode 100644 index 000000000..d05e37d0c --- /dev/null +++ b/src/main/java/de/thm/arsnova/security/pac4j/OauthCallbackFilter.java @@ -0,0 +1,69 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.security.pac4j; + +import org.pac4j.core.config.Config; +import org.pac4j.core.context.J2EContext; +import org.pac4j.core.http.J2ENopHttpActionAdapter; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Handles callback requests by login redirects from OAuth providers. + * + * @author Daniel Gerhardt + */ +@Component +public class OauthCallbackFilter extends OncePerRequestFilter { + private OauthCallbackHandler<Object, J2EContext> oauthCallbackHandler; + private Config config; + private String defaultUrl; + private String suffix; + + public OauthCallbackFilter(OauthCallbackHandler<Object, J2EContext> oauthCallbackHandler, Config pac4jConfig) { + this.oauthCallbackHandler = oauthCallbackHandler; + this.config = pac4jConfig; + } + + @Override + protected void doFilterInternal(final HttpServletRequest httpServletRequest, + final HttpServletResponse httpServletResponse, final FilterChain filterChain) + throws ServletException, IOException { + if (httpServletRequest.getServletPath().endsWith(suffix)) { + final J2EContext context = new J2EContext(httpServletRequest, httpServletResponse, config.getSessionStore()); + oauthCallbackHandler.perform(context, config, J2ENopHttpActionAdapter.INSTANCE, defaultUrl, + false, false); + } else { + filterChain.doFilter(httpServletRequest, httpServletResponse); + } + } + + public void setDefaultUrl(final String defaultUrl) { + this.defaultUrl = defaultUrl; + } + + public void setSuffix(final String suffix) { + this.suffix = suffix; + } +} diff --git a/src/main/java/de/thm/arsnova/security/pac4j/OauthCallbackHandler.java b/src/main/java/de/thm/arsnova/security/pac4j/OauthCallbackHandler.java new file mode 100644 index 000000000..576a72d94 --- /dev/null +++ b/src/main/java/de/thm/arsnova/security/pac4j/OauthCallbackHandler.java @@ -0,0 +1,58 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.security.pac4j; + +import de.thm.arsnova.security.User; +import org.pac4j.core.config.Config; +import org.pac4j.core.context.WebContext; +import org.pac4j.core.engine.DefaultCallbackLogic; +import org.pac4j.core.profile.CommonProfile; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import java.util.Collections; + +/** + * Sets up the SecurityContext OAuth users. + * + * @author Daniel Gerhardt + */ +@Component +public class OauthCallbackHandler<R, C extends WebContext> extends DefaultCallbackLogic<R, C> { + private OauthUserDetailsService oauthUserDetailsService; + + @Override + protected void saveUserProfile(final C context, final Config config, final CommonProfile profile, + final boolean multiProfile, final boolean renewSession) { + User user = oauthUserDetailsService.loadUserDetails( + new OAuthToken(null, profile, Collections.emptyList())); + SecurityContextHolder.getContext().setAuthentication( + new OAuthToken(user, profile, user.getAuthorities())); + } + + @Override + protected void renewSession(final C context, final Config config) { + /* NOOP */ + } + + @Autowired + public void setOauthUserDetailsService(final OauthUserDetailsService oauthUserDetailsService) { + this.oauthUserDetailsService = oauthUserDetailsService; + } +} diff --git a/src/main/java/de/thm/arsnova/security/pac4j/OauthUserDetailsService.java b/src/main/java/de/thm/arsnova/security/pac4j/OauthUserDetailsService.java new file mode 100644 index 000000000..f3f415be8 --- /dev/null +++ b/src/main/java/de/thm/arsnova/security/pac4j/OauthUserDetailsService.java @@ -0,0 +1,74 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.security.pac4j; + +import de.thm.arsnova.entities.UserProfile; +import de.thm.arsnova.security.User; +import de.thm.arsnova.services.UserService; +import org.pac4j.oauth.profile.facebook.FacebookProfile; +import org.pac4j.oauth.profile.google2.Google2Profile; +import org.pac4j.oauth.profile.twitter.TwitterProfile; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.HashSet; + +/** + * Loads UserDetails for an OAuth user (e.g. {@link UserProfile.AuthProvider#GOOGLE}) based on an unique identifier + * extracted from the OAuth profile. + * + * @author Daniel Gerhardt + */ +@Service +public class OauthUserDetailsService implements AuthenticationUserDetailsService<OAuthToken> { + private final UserService userService; + protected final Collection<GrantedAuthority> grantedAuthorities; + + public OauthUserDetailsService(UserService userService) { + this.userService = userService; + grantedAuthorities = new HashSet<>(); + grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER")); + grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_OAUTH_USER")); + } + + public User loadUserDetails(final OAuthToken token) + throws UsernameNotFoundException { + User user; + if (token.getDetails() instanceof Google2Profile) { + final Google2Profile profile = (Google2Profile) token.getDetails(); + user = userService.loadUser(UserProfile.AuthProvider.GOOGLE, profile.getEmail(), + grantedAuthorities, true); + } else if (token.getDetails() instanceof TwitterProfile) { + final TwitterProfile profile = (TwitterProfile) token.getDetails(); + user = userService.loadUser(UserProfile.AuthProvider.TWITTER, profile.getUsername(), + grantedAuthorities, true); + } else if (token.getDetails() instanceof FacebookProfile) { + final FacebookProfile profile = (FacebookProfile) token.getDetails(); + user = userService.loadUser(UserProfile.AuthProvider.FACEBOOK, profile.getId(), + grantedAuthorities, true); + } else { + throw new IllegalArgumentException("AuthenticationToken not supported"); + } + + return user; + } +} diff --git a/src/main/java/de/thm/arsnova/services/ContentServiceImpl.java b/src/main/java/de/thm/arsnova/services/ContentServiceImpl.java index b49ddc007..4a834fc9e 100644 --- a/src/main/java/de/thm/arsnova/services/ContentServiceImpl.java +++ b/src/main/java/de/thm/arsnova/services/ContentServiceImpl.java @@ -220,7 +220,7 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem /* FIXME: #content.getShortId() cannot be checked since keyword is no longer set for content. */ @Override - @PreAuthorize("hasPermission(#content.getShortId(), 'session', 'owner')") + @PreAuthorize("hasPermission(#content.getShortId(), 'room', 'owner')") public Content save(final Content content) { final Room room = roomRepository.findOne(content.getRoomId()); content.setTimestamp(new Date()); @@ -918,7 +918,7 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem /* TODO: Only evict cache entry for the answer's content. This requires some refactoring. */ @Override - @PreAuthorize("hasPermission(#roomShortId, 'session', 'owner')") + @PreAuthorize("hasPermission(#roomShortId, 'room', 'owner')") @CacheEvict(value = "answerlists", allEntries = true) public void deleteAllPreparationAnswers(String roomShortId) { final Room room = getRoom(roomShortId); @@ -933,7 +933,7 @@ public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implem /* TODO: Only evict cache entry for the answer's content. This requires some refactoring. */ @Override - @PreAuthorize("hasPermission(#roomShortId, 'session', 'owner')") + @PreAuthorize("hasPermission(#roomShortId, 'room', 'owner')") @CacheEvict(value = "answerlists", allEntries = true) public void deleteAllLectureAnswers(String roomShortId) { final Room room = getRoom(roomShortId); diff --git a/src/main/java/de/thm/arsnova/services/MotdServiceImpl.java b/src/main/java/de/thm/arsnova/services/MotdServiceImpl.java index 19d3f08b2..0a1e52285 100644 --- a/src/main/java/de/thm/arsnova/services/MotdServiceImpl.java +++ b/src/main/java/de/thm/arsnova/services/MotdServiceImpl.java @@ -62,7 +62,7 @@ public class MotdServiceImpl extends DefaultEntityServiceImpl<Motd> implements M } @Override - @PreAuthorize("hasPermission(#roomId, 'session', 'owner')") + @PreAuthorize("hasPermission(#roomId, 'room', 'owner')") public List<Motd> getAllRoomMotds(final String roomId) { return motdRepository.findByRoomId(roomId); } @@ -112,7 +112,7 @@ public class MotdServiceImpl extends DefaultEntityServiceImpl<Motd> implements M } @Override - @PreAuthorize("hasPermission(#roomId, 'session', 'owner')") + @PreAuthorize("hasPermission(#roomId, 'room', 'owner')") public Motd save(final String roomId, final Motd motd) { Room room = roomService.getByShortId(roomId); motd.setRoomId(room.getId()); @@ -127,7 +127,7 @@ public class MotdServiceImpl extends DefaultEntityServiceImpl<Motd> implements M } @Override - @PreAuthorize("hasPermission(#roomShortId, 'session', 'owner')") + @PreAuthorize("hasPermission(#roomShortId, 'room', 'owner')") public Motd update(final String roomShortId, final Motd motd) { return createOrUpdateMotd(motd); } @@ -159,7 +159,7 @@ public class MotdServiceImpl extends DefaultEntityServiceImpl<Motd> implements M } @Override - @PreAuthorize("hasPermission(#roomId, 'session', 'owner')") + @PreAuthorize("hasPermission(#roomId, 'room', 'owner')") public void deleteByRoomId(final String roomId, Motd motd) { motdRepository.delete(motd); } diff --git a/src/main/java/de/thm/arsnova/services/RoomServiceImpl.java b/src/main/java/de/thm/arsnova/services/RoomServiceImpl.java index 3e4634ac7..53f9452e5 100644 --- a/src/main/java/de/thm/arsnova/services/RoomServiceImpl.java +++ b/src/main/java/de/thm/arsnova/services/RoomServiceImpl.java @@ -217,7 +217,7 @@ public class RoomServiceImpl extends DefaultEntityServiceImpl<Room> implements R return this.getInternal(shortId, user); } - @PreAuthorize("hasPermission(#shortId, 'session', 'owner')") + @PreAuthorize("hasPermission(#shortId, 'room', 'owner')") public Room getForAdmin(final String shortId) { return roomRepository.findByShortId(shortId); } @@ -232,12 +232,8 @@ public class RoomServiceImpl extends DefaultEntityServiceImpl<Room> implements R if (room == null) { throw new NotFoundException(); } - if (room.isClosed()) { - if (user.hasRole(UserRoomService.Role.STUDENT)) { - throw new ForbiddenException("User is not room creator."); - } else if (user.hasRole(UserRoomService.Role.SPEAKER) && !room.getOwnerId().equals(user.getId())) { - throw new ForbiddenException("User is not room creator."); - } + if (room.isClosed() && !room.getOwnerId().equals(user.getId())) { + throw new ForbiddenException("User is not room creator."); } /* FIXME: migrate LMS course support @@ -252,8 +248,9 @@ public class RoomServiceImpl extends DefaultEntityServiceImpl<Room> implements R return room; } + /* TODO: Updated SpEL expression has not been tested yet */ @Override - @PreAuthorize("isAuthenticated() and hasPermission(#shortId, 'session', 'owner')") + @PreAuthorize("isAuthenticated() and hasPermission(#userId, 'userprofile', 'owner')") public List<Room> getUserRooms(String userId) { return roomRepository.findByOwnerId(userId, 0, 0); } @@ -373,7 +370,7 @@ public class RoomServiceImpl extends DefaultEntityServiceImpl<Room> implements R } @Override - @PreAuthorize("hasPermission(#shortId, 'session', 'owner')") + @PreAuthorize("hasPermission(#shortId, 'room', 'owner')") public Room setActive(final String shortId, final Boolean lock) { final Room room = roomRepository.findByShortId(shortId); room.setClosed(!lock); @@ -433,7 +430,7 @@ public class RoomServiceImpl extends DefaultEntityServiceImpl<Room> implements R } @Override - @PreAuthorize("hasPermission(#shortId, 'session', 'read')") + @PreAuthorize("hasPermission(#shortId, 'room', 'read')") public ScoreStatistics getLearningProgress(final String shortId, final String type, final String questionVariant) { final Room room = roomRepository.findByShortId(shortId); ScoreCalculator scoreCalculator = scoreCalculatorFactory.create(type, questionVariant); @@ -441,7 +438,7 @@ public class RoomServiceImpl extends DefaultEntityServiceImpl<Room> implements R } @Override - @PreAuthorize("hasPermission(#shortId, 'session', 'read')") + @PreAuthorize("hasPermission(#shortId, 'room', 'read')") public ScoreStatistics getMyLearningProgress(final String shortId, final String type, final String questionVariant) { final Room room = roomRepository.findByShortId(shortId); final UserAuthentication user = userService.getCurrentUser(); @@ -450,7 +447,7 @@ public class RoomServiceImpl extends DefaultEntityServiceImpl<Room> implements R } @Override - @PreAuthorize("hasPermission('', 'session', 'create')") + @PreAuthorize("hasPermission('', 'room', 'create')") public Room importRooms(ImportExportContainer importRoom) { final UserAuthentication user = userService.getCurrentUser(); final Room info = roomRepository.importRoom(user, importRoom); @@ -461,13 +458,13 @@ public class RoomServiceImpl extends DefaultEntityServiceImpl<Room> implements R } @Override - @PreAuthorize("hasPermission(#shortId, 'session', 'owner')") + @PreAuthorize("hasPermission(#shortId, 'room', 'owner')") public ImportExportContainer exportRoom(String shortId, Boolean withAnswerStatistics, Boolean withFeedbackQuestions) { return roomRepository.exportRoom(shortId, withAnswerStatistics, withFeedbackQuestions); } @Override - @PreAuthorize("hasPermission(#shortId, 'session', 'owner')") + @PreAuthorize("hasPermission(#shortId, 'room', 'owner')") public Room copyRoomToPublicPool(String shortId, ImportExportContainer.PublicPool pp) { ImportExportContainer temp = roomRepository.exportRoom(shortId, false, false); temp.getSession().setPublicPool(pp); @@ -482,13 +479,13 @@ public class RoomServiceImpl extends DefaultEntityServiceImpl<Room> implements R } @Override - @PreAuthorize("hasPermission(#shortId, 'session', 'read')") + @PreAuthorize("hasPermission(#shortId, 'room', 'read')") public Room.Settings getFeatures(String shortId) { return roomRepository.findByShortId(shortId).getSettings(); } @Override - @PreAuthorize("hasPermission(#shortId, 'session', 'owner')") + @PreAuthorize("hasPermission(#shortId, 'room', 'owner')") public Room.Settings updateFeatures(String shortId, Room.Settings settings) { final Room room = roomRepository.findByShortId(shortId); final UserAuthentication user = userService.getCurrentUser(); @@ -500,7 +497,7 @@ public class RoomServiceImpl extends DefaultEntityServiceImpl<Room> implements R } @Override - @PreAuthorize("hasPermission(#shortId, 'session', 'owner')") + @PreAuthorize("hasPermission(#shortId, 'room', 'owner')") public boolean lockFeedbackInput(String shortId, Boolean lock) { final Room room = roomRepository.findByShortId(shortId); final UserAuthentication user = userService.getCurrentUser(); @@ -516,7 +513,7 @@ public class RoomServiceImpl extends DefaultEntityServiceImpl<Room> implements R } @Override - @PreAuthorize("hasPermission(#shortId, 'session', 'owner')") + @PreAuthorize("hasPermission(#shortId, 'room', 'owner')") public boolean flipFlashcards(String shortId, Boolean flip) { final Room room = roomRepository.findByShortId(shortId); this.publisher.publishEvent(new FlipFlashcardsEvent(this, room)); diff --git a/src/main/java/de/thm/arsnova/services/UserRoomService.java b/src/main/java/de/thm/arsnova/services/UserRoomService.java deleted file mode 100644 index 972a075f8..000000000 --- a/src/main/java/de/thm/arsnova/services/UserRoomService.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors - * - * ARSnova Backend is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ARSnova Backend is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -package de.thm.arsnova.services; - -import de.thm.arsnova.entities.UserAuthentication; -import de.thm.arsnova.entities.migration.v2.Room; - -import java.util.UUID; - -/** - * The functionality the user-session service should provide. - */ -public interface UserRoomService { - - enum Role { - STUDENT, - SPEAKER - } - - void setUser(UserAuthentication user); - UserAuthentication getUser(); - - void setRoom(Room room); - Room getRoom(); - - void setSocketId(UUID socketId); - UUID getSocketId(); - - void setRole(Role role); - Role getRole(); - - boolean inRoom(); - boolean isAuthenticated(); -} diff --git a/src/main/java/de/thm/arsnova/services/UserRoomServiceImpl.java b/src/main/java/de/thm/arsnova/services/UserRoomServiceImpl.java deleted file mode 100644 index 847dbd2ac..000000000 --- a/src/main/java/de/thm/arsnova/services/UserRoomServiceImpl.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors - * - * ARSnova Backend is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ARSnova Backend is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -package de.thm.arsnova.services; - -import de.thm.arsnova.entities.UserAuthentication; -import de.thm.arsnova.entities.migration.v2.Room; -import org.springframework.context.annotation.Scope; -import org.springframework.context.annotation.ScopedProxyMode; -import org.springframework.stereotype.Component; - -import java.io.Serializable; -import java.util.UUID; - -/** - * This service is used to assign and check for a specific role. - */ -@Component -@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS) -public class UserRoomServiceImpl implements UserRoomService, Serializable { - private static final long serialVersionUID = 1L; - - private UserAuthentication user; - private Room room; - private UUID socketId; - private Role role; - - @Override - public void setUser(final UserAuthentication u) { - user = u; - user.setRole(role); - } - - @Override - public UserAuthentication getUser() { - return user; - } - - @Override - public void setRoom(final Room room) { - this.room = room; - } - - @Override - public Room getRoom() { - return room; - } - - @Override - public void setSocketId(final UUID sId) { - socketId = sId; - } - - @Override - public UUID getSocketId() { - return socketId; - } - - @Override - public boolean inRoom() { - return isAuthenticated() - && getRoom() != null; - } - - @Override - public boolean isAuthenticated() { - return getUser() != null - && getRole() != null; - } - - @Override - public void setRole(final Role r) { - role = r; - if (user != null) { - user.setRole(role); - } - } - - @Override - public Role getRole() { - return role; - } -} diff --git a/src/main/java/de/thm/arsnova/services/UserService.java b/src/main/java/de/thm/arsnova/services/UserService.java index 5f2080cd4..64c2dc322 100644 --- a/src/main/java/de/thm/arsnova/services/UserService.java +++ b/src/main/java/de/thm/arsnova/services/UserService.java @@ -19,7 +19,12 @@ package de.thm.arsnova.services; import de.thm.arsnova.entities.UserAuthentication; import de.thm.arsnova.entities.UserProfile; +import de.thm.arsnova.security.User; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import java.util.Collection; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -27,7 +32,7 @@ import java.util.UUID; /** * The functionality the user service should provide. */ -public interface UserService { +public interface UserService extends EntityService<UserProfile> { UserProfile getCurrentUserProfile(); UserAuthentication getCurrentUser(); @@ -60,6 +65,11 @@ public interface UserService { int loggedInUsers(); + void authenticate(UsernamePasswordAuthenticationToken token, UserProfile.AuthProvider authProvider); + + User loadUser(UserProfile.AuthProvider authProvider, String loginId, + Collection<GrantedAuthority> grantedAuthorities, boolean autoCreate) throws UsernameNotFoundException; + UserProfile getByAuthProviderAndLoginId(UserProfile.AuthProvider authProvider, String loginId); UserProfile getByUsername(String username); @@ -73,8 +83,4 @@ public interface UserService { void initiatePasswordReset(String username); boolean resetPassword(UserProfile userProfile, String key, String password); - - String createGuest(); - - boolean guestExists(String loginId); } diff --git a/src/main/java/de/thm/arsnova/services/UserServiceImpl.java b/src/main/java/de/thm/arsnova/services/UserServiceImpl.java index ba74b49cd..e689e2ef9 100644 --- a/src/main/java/de/thm/arsnova/services/UserServiceImpl.java +++ b/src/main/java/de/thm/arsnova/services/UserServiceImpl.java @@ -24,29 +24,33 @@ import de.thm.arsnova.exceptions.BadRequestException; import de.thm.arsnova.exceptions.NotFoundException; import de.thm.arsnova.exceptions.UnauthorizedException; import de.thm.arsnova.persistance.UserRepository; +import de.thm.arsnova.security.GuestUserDetailsService; +import de.thm.arsnova.security.User; import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang.StringUtils; -import org.pac4j.oauth.profile.facebook.FacebookProfile; -import org.pac4j.oauth.profile.google2.Google2Profile; -import org.pac4j.oauth.profile.twitter.TwitterProfile; -import org.pac4j.springframework.security.authentication.Pac4jAuthenticationToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.mail.MailException; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.cas.authentication.CasAuthenticationToken; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.codec.Hex; import org.springframework.security.crypto.keygen.BytesKeyGenerator; import org.springframework.security.crypto.keygen.KeyGenerators; +import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; @@ -58,16 +62,8 @@ import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.io.UnsupportedEncodingException; import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.Set; -import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; @@ -76,7 +72,7 @@ import java.util.regex.Pattern; */ @Service @MonitorGauges -public class UserServiceImpl implements UserService { +public class UserServiceImpl extends DefaultEntityServiceImpl<UserProfile> implements UserService { private static final int LOGIN_TRY_RESET_DELAY_MS = 30 * 1000; @@ -100,6 +96,15 @@ public class UserServiceImpl implements UserService { private JavaMailSender mailSender; + @Autowired(required = false) + private GuestUserDetailsService guestUserDetailsService; + + @Autowired(required = false) + private DaoAuthenticationProvider daoProvider; + + @Autowired(required = false) + private LdapAuthenticationProvider ldapAuthenticationProvider; + @Value("${root-url}") private String rootUrl; @@ -150,7 +155,11 @@ public class UserServiceImpl implements UserService { loginBans = Collections.synchronizedSet(new HashSet<String>()); } - public UserServiceImpl(UserRepository repository, JavaMailSender mailSender) { + public UserServiceImpl( + UserRepository repository, + JavaMailSender mailSender, + @Qualifier("defaultJsonMessageConverter") MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) { + super(UserProfile.class, repository, jackson2HttpMessageConverter.getObjectMapper()); this.userRepository = repository; this.mailSender = mailSender; } @@ -192,60 +201,15 @@ public class UserServiceImpl implements UserService { return null; } - UserAuthentication user = null; - - if (authentication instanceof Pac4jAuthenticationToken) { - user = getOAuthUser(authentication); - } else if (authentication instanceof CasAuthenticationToken) { - final CasAuthenticationToken token = (CasAuthenticationToken) authentication; - user = new UserAuthentication(token.getAssertion().getPrincipal()); - } else if (authentication instanceof AnonymousAuthenticationToken) { - final AnonymousAuthenticationToken token = (AnonymousAuthenticationToken) authentication; - user = new UserAuthentication(token); - } else if (authentication instanceof UsernamePasswordAuthenticationToken) { - final UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication; - user = new UserAuthentication(token); - if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_GUEST"))) { - user.setAuthProvider(UserProfile.AuthProvider.ARSNOVA_GUEST); - } else if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_DB_USER"))) { - user.setAuthProvider(UserProfile.AuthProvider.ARSNOVA); - } - } - + UserAuthentication user = new UserAuthentication(authentication); if (user == null || "anonymous".equals(user.getUsername())) { throw new UnauthorizedException(); } - - UserProfile userProfile = userRepository.findByAuthProviderAndLoginId(user.getAuthProvider(), user.getUsername()); - if (userProfile == null && user.getAuthProvider() != UserProfile.AuthProvider.ARSNOVA) { - userProfile = new UserProfile(); - userProfile.setAuthProvider(user.getAuthProvider()); - userProfile.setLoginId(user.getUsername()); - userRepository.save(userProfile); - } - user.setId(userProfile.getId()); - user.setAdmin(Arrays.asList(adminAccounts).contains(user.getUsername())); return user; } - private UserAuthentication getOAuthUser(final Authentication authentication) { - UserAuthentication user = null; - final Pac4jAuthenticationToken token = (Pac4jAuthenticationToken) authentication; - if (token.getProfile() instanceof Google2Profile) { - final Google2Profile profile = (Google2Profile) token.getProfile(); - user = new UserAuthentication(profile); - } else if (token.getProfile() instanceof TwitterProfile) { - final TwitterProfile profile = (TwitterProfile) token.getProfile(); - user = new UserAuthentication(profile); - } else if (token.getProfile() instanceof FacebookProfile) { - final FacebookProfile profile = (FacebookProfile) token.getProfile(); - user = new UserAuthentication(profile); - } - return user; - } - @Override public boolean isAdmin(final String username) { return Arrays.asList(adminAccounts).contains(username); @@ -361,6 +325,60 @@ public class UserServiceImpl implements UserService { return userToRoomId.size(); } + @Override + public void authenticate(final UsernamePasswordAuthenticationToken token, + final UserProfile.AuthProvider authProvider) { + Authentication auth; + switch (authProvider) { + case LDAP: + auth = ldapAuthenticationProvider.authenticate(token); + break; + case ARSNOVA: + auth = daoProvider.authenticate(token); + break; + case ARSNOVA_GUEST: + String id = token.getName(); + boolean autoCreate = false; + if (id == null || id.isEmpty()) { + id = generateGuestId(); + autoCreate = true; + } + UserDetails userDetails = guestUserDetailsService.loadUserByUsername(id, autoCreate); + if (userDetails == null) { + throw new UsernameNotFoundException("Guest user does not exist"); + } + auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + + break; + default: + throw new IllegalArgumentException("Unsupported authentication provider"); + } + + if (!auth.isAuthenticated()) { + throw new BadRequestException(); + } + SecurityContextHolder.getContext().setAuthentication(auth); + } + + @Override + public User loadUser(final UserProfile.AuthProvider authProvider, final String loginId, + final Collection<GrantedAuthority> grantedAuthorities, final boolean autoCreate) + throws UsernameNotFoundException { + logger.debug("Load user: LoginId: {}, AuthProvider: {}", loginId, authProvider); + UserProfile userProfile = userRepository.findByAuthProviderAndLoginId(authProvider, loginId); + if (userProfile == null) { + if (autoCreate) { + userProfile = new UserProfile(authProvider, loginId); + /* Repository is accessed directly without EntityService to skip permission check */ + userRepository.save(userProfile); + } else { + throw new UsernameNotFoundException("User does not exist."); + } + } + + return new User(userProfile, grantedAuthorities); + } + @Override public UserProfile getByAuthProviderAndLoginId(final UserProfile.AuthProvider authProvider, final String loginId) { return userRepository.findByAuthProviderAndLoginId(authProvider, loginId); @@ -404,6 +422,7 @@ public class UserServiceImpl implements UserService { account.setActivationKey(RandomStringUtils.randomAlphanumeric(32)); userProfile.setCreationTimestamp(new Date()); + /* Repository is accessed directly without EntityService to skip permission check */ UserProfile result = userRepository.save(userProfile); if (null != result) { sendActivationEmail(result); @@ -577,20 +596,11 @@ public class UserServiceImpl implements UserService { } } - public String createGuest() { + private String generateGuestId() { if (null == keygen) { keygen = KeyGenerators.secureRandom(16); } - UserProfile userProfile = new UserProfile(); - userProfile.setLoginId(new String(Hex.encode(keygen.generateKey()))); - userProfile.setAuthProvider(UserProfile.AuthProvider.ARSNOVA_GUEST); - userRepository.save(userProfile); - - return userProfile.getLoginId(); - } - - public boolean guestExists(final String loginId) { - return userRepository.findByAuthProviderAndLoginId(UserProfile.AuthProvider.ARSNOVA_GUEST, loginId) != null; + return new String(Hex.encode(keygen.generateKey())); } } diff --git a/src/main/resources/META-INF/aop.xml b/src/main/resources/META-INF/aop.xml index 5b05bbbe0..3b810cd0d 100644 --- a/src/main/resources/META-INF/aop.xml +++ b/src/main/resources/META-INF/aop.xml @@ -7,6 +7,5 @@ <aspects> <aspect name="de.thm.arsnova.aop.RangeAspect"/> - <aspect name="de.thm.arsnova.aop.UserRoomAspect"/> </aspects> </aspectj> diff --git a/src/test/java/de/thm/arsnova/config/TestAppConfig.java b/src/test/java/de/thm/arsnova/config/TestAppConfig.java index eea3d564f..979ce8add 100644 --- a/src/test/java/de/thm/arsnova/config/TestAppConfig.java +++ b/src/test/java/de/thm/arsnova/config/TestAppConfig.java @@ -4,6 +4,7 @@ import de.thm.arsnova.persistance.UserRepository; import de.thm.arsnova.services.StubUserService; import de.thm.arsnova.websocket.ArsnovaSocketioServer; import de.thm.arsnova.websocket.ArsnovaSocketioServerImpl; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.CustomScopeConfigurer; import org.springframework.cache.annotation.EnableCaching; @@ -16,6 +17,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.PropertySource; import org.springframework.context.annotation.aspectj.EnableSpringConfigured; import org.springframework.context.support.SimpleThreadScope; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mock.web.MockServletContext; import org.springframework.test.context.ContextConfiguration; @@ -72,7 +74,10 @@ public class TestAppConfig { @Bean @Primary - public StubUserService stubUserService(UserRepository repository, JavaMailSender mailSender) { - return new StubUserService(repository, mailSender); + public StubUserService stubUserService( + UserRepository repository, + JavaMailSender mailSender, + @Qualifier("defaultJsonMessageConverter") MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) { + return new StubUserService(repository, mailSender, jackson2HttpMessageConverter); } } diff --git a/src/test/java/de/thm/arsnova/controller/v2/AuthenticationControllerTest.java b/src/test/java/de/thm/arsnova/controller/v2/AuthenticationControllerTest.java index 526b86c61..5222561e4 100644 --- a/src/test/java/de/thm/arsnova/controller/v2/AuthenticationControllerTest.java +++ b/src/test/java/de/thm/arsnova/controller/v2/AuthenticationControllerTest.java @@ -32,6 +32,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -59,17 +60,23 @@ public class AuthenticationControllerTest extends AbstractControllerTest { } @Test + @Ignore("Mockup needed for UserService") public void testReuseGuestLogin() throws Exception { mockMvc.perform( get("/v2/auth/doLogin") - .param("type", "guest").param("user","Guest1234567890") + .param("type", "guest") + ).andExpect(status().isOk()); + final Authentication auth1 = SecurityContextHolder.getContext().getAuthentication(); + cleanup(); + mockMvc.perform( + get("/v2/auth/doLogin") + .param("type", "guest").param("user", auth1.getName()) ).andExpect(status().isOk()); - final Authentication auth = SecurityContextHolder.getContext() - .getAuthentication(); - assertEquals(auth.getClass(), UsernamePasswordAuthenticationToken.class); - assertEquals("Guest1234567890", auth.getName()); - + final Authentication auth2 = SecurityContextHolder.getContext().getAuthentication(); + assertEquals(auth2.getClass(), UsernamePasswordAuthenticationToken.class); + assertNotSame(auth1, auth2); + assertEquals(auth1, auth2); } @Test diff --git a/src/test/java/de/thm/arsnova/services/StubUserService.java b/src/test/java/de/thm/arsnova/services/StubUserService.java index c2b8ec157..7e3813dae 100644 --- a/src/test/java/de/thm/arsnova/services/StubUserService.java +++ b/src/test/java/de/thm/arsnova/services/StubUserService.java @@ -19,6 +19,8 @@ package de.thm.arsnova.services; import de.thm.arsnova.entities.UserAuthentication; import de.thm.arsnova.persistance.UserRepository; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -26,8 +28,11 @@ public class StubUserService extends UserServiceImpl { private UserAuthentication stubUser = null; - public StubUserService(UserRepository repository, JavaMailSender mailSender) { - super(repository, mailSender); + public StubUserService( + UserRepository repository, + JavaMailSender mailSender, + @Qualifier("defaultJsonMessageConverter") MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) { + super(repository, mailSender, jackson2HttpMessageConverter); } public void setUserAuthenticated(boolean isAuthenticated) { @@ -50,8 +55,4 @@ public class StubUserService extends UserServiceImpl { public UserAuthentication getCurrentUser() { return stubUser; } - - public void setRole(UserRoomService.Role role) { - stubUser.setRole(role); - } } diff --git a/src/test/java/de/thm/arsnova/services/UserServiceTest.java b/src/test/java/de/thm/arsnova/services/UserServiceTest.java index 1083f40f5..808cee0d2 100644 --- a/src/test/java/de/thm/arsnova/services/UserServiceTest.java +++ b/src/test/java/de/thm/arsnova/services/UserServiceTest.java @@ -22,6 +22,8 @@ import de.thm.arsnova.config.TestAppConfig; import de.thm.arsnova.config.TestPersistanceConfig; import de.thm.arsnova.config.TestSecurityConfig; import de.thm.arsnova.entities.UserAuthentication; +import de.thm.arsnova.security.User; +import de.thm.arsnova.security.pac4j.OAuthToken; import org.jasig.cas.client.authentication.AttributePrincipalImpl; import org.junit.Test; import org.junit.runner.RunWith; @@ -43,6 +45,7 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; @@ -62,7 +65,7 @@ public class UserServiceTest { @Test public void testSocket2UserPersistence() throws IOException, ClassNotFoundException { socketid2user.put(UUID.randomUUID(), new UserAuthentication(new UsernamePasswordAuthenticationToken("ptsr00", UUID.randomUUID()))); - socketid2user.put(UUID.randomUUID(), new UserAuthentication(new AttributePrincipalImpl("ptstr0"))); + //socketid2user.put(UUID.randomUUID(), new UserAuthentication(new AttributePrincipalImpl("ptstr0"))); Google2Email email = new Google2Email(); email.setEmail("mail@host.com"); @@ -71,8 +74,9 @@ public class UserServiceTest { Google2Profile profile = new Google2Profile(); profile.addAttribute(Google2ProfileDefinition.DISPLAY_NAME, "ptsr00"); profile.addAttribute(Google2ProfileDefinition.EMAILS, emails); + OAuthToken token = new OAuthToken(null, profile, Collections.emptyList()); - socketid2user.put(UUID.randomUUID(), new UserAuthentication(profile)); + socketid2user.put(UUID.randomUUID(), new UserAuthentication(token)); List<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("ROLE_GUEST")); socketid2user.put(UUID.randomUUID(), new UserAuthentication(new AnonymousAuthenticationToken("ptsr00", UUID.randomUUID(), authorities))); -- GitLab