diff --git a/pom.xml b/pom.xml index 94f8e7d67f11d90e160c99a46ed68e50600c1a7a..f6cb96265368cae373ea520eeaa983182a6effef 100644 --- a/pom.xml +++ b/pom.xml @@ -14,6 +14,9 @@ <mobile.production.path>../arsnova-mobile/src/main/webapp/build/production/ARSnova</mobile.production.path> <mobile.testing.path>../arsnova-mobile/src/main/webapp/build/testing/ARSnova</mobile.testing.path> <mobile.path>${mobile.production.path}</mobile.path> + <customization.path>${basedir}/../arsnova-customization/src/main/webapp</customization.path> + <presenter.rootPath>${basedir}/../arsnova-presenter</presenter.rootPath> + <presenter.outputDir>target/arsnova-presenter-1.1.0-SNAPSHOT</presenter.outputDir> </properties> <developers> @@ -134,12 +137,6 @@ </repositories> <dependencies> - <dependency> - <groupId>de.thm.arsnova</groupId> - <artifactId>arsnova-mobile</artifactId> - <version>0.0.1-SNAPSHOT</version> - <type>war</type> - </dependency> <!-- Spring --> <dependency> <groupId>org.springframework</groupId> @@ -179,6 +176,11 @@ <artifactId>spring-security-cas</artifactId> <version>${org.springframework.security-version}</version> </dependency> + <dependency> + <groupId>org.springframework.security</groupId> + <artifactId>spring-security-ldap</artifactId> + <version>${org.springframework.security-version}</version> + </dependency> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> @@ -227,6 +229,11 @@ <version>3.0.1</version> <scope>provided</scope> </dependency> + <dependency> + <groupId>javax.mail</groupId> + <artifactId>mail</artifactId> + <version>1.4.7</version> + </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> @@ -310,6 +317,20 @@ <version>0.9.1</version> <scope>test</scope> </dependency> + + <dependency> + <groupId>org.apache.directory.server</groupId> + <artifactId>apacheds-core</artifactId> + <version>1.5.5</version> + <scope>runtime</scope> + </dependency> + <dependency> + <groupId>org.apache.directory.server</groupId> + <artifactId>apacheds-server-jndi</artifactId> + <version>1.5.5</version> + <scope>runtime</scope> + </dependency> + </dependencies> <build> <plugins> @@ -323,18 +344,32 @@ </configuration> </plugin> <plugin> - <groupId>org.mortbay.jetty</groupId> + <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-maven-plugin</artifactId> - <version>7.6.5.v20120716</version> + <version>9.2.1.v20140609</version> <configuration> <scanIntervalSeconds>1</scanIntervalSeconds> - <webApp> - <contextPath>/</contextPath> - <baseResource implementation="org.eclipse.jetty.util.resource.ResourceCollection"> - <resourcesAsCSV>src/main/webapp,${mobile.path}</resourcesAsCSV> - </baseResource> + <webApp> <overrideDescriptor>src/main/webapp/WEB-INF/web-dev.xml</overrideDescriptor> </webApp> + <contextHandlers> + <contextHandler implementation="org.eclipse.jetty.webapp.WebAppContext"> + <contextPath>/mobile</contextPath> + <resourceBase>${mobile.path}</resourceBase> + </contextHandler> + <contextHandler implementation="org.eclipse.jetty.webapp.WebAppContext"> + <contextPath>/presenter</contextPath> + <baseResource implementation="org.eclipse.jetty.util.resource.ResourceCollection"> + <resourcesAsCSV>${presenter.rootPath}/${presenter.outputDir},${presenter.rootPath}/src/main/websources,${presenter.rootPath}/src/main/webapp</resourcesAsCSV> + </baseResource> + <descriptor>${presenter.rootPath}/src/main/config/WEB-INF/web.dev.xml</descriptor> + <aliasCheck implementation="org.eclipse.jetty.server.handler.AllowSymLinkAliasChecker" /> + </contextHandler> + <contextHandler implementation="org.eclipse.jetty.webapp.WebAppContext"> + <contextPath>/customization</contextPath> + <resourceBase>${customization.path}</resourceBase> + </contextHandler> + </contextHandlers> </configuration> </plugin> <plugin> @@ -398,7 +433,7 @@ </plugins> </build> - <name>ARSnova</name> + <name>ARSnova Backend</name> <description>ARSnova is a great audience response system</description> <organization> <name>Technische Hochschule Mittelhessen</name> diff --git a/src/main/java/de/thm/arsnova/controller/ConfigurationController.java b/src/main/java/de/thm/arsnova/controller/ConfigurationController.java new file mode 100644 index 0000000000000000000000000000000000000000..9d4d96c73978e0a84782ce3b1a4b708a804a73bd --- /dev/null +++ b/src/main/java/de/thm/arsnova/controller/ConfigurationController.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2014 THM webMedia + * + * This file is part of ARSnova. + * + * ARSnova 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 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.controller; + +import java.util.HashMap; + +import javax.servlet.http.HttpServletRequest; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * The ConfigurationController provides frontend clients with information necessary to correctly interact with the + * backend and other frontends as well as settings for ARSnova. The the alternative /arsnova-config route is necessary + * in case the backend application is deployed as root context. + */ +@Controller +@RequestMapping({"/configuration", "/arsnova-config"}) +public class ConfigurationController extends AbstractController { + @Value("${security.guest.enabled}") + private String guestEnabled; + + public static final Logger LOGGER = LoggerFactory + .getLogger(ConfigurationController.class); + + @Value("${customization.path}") + private String customizationPath; + + @Value("${mobile.path}") + private String mobilePath; + + @Value("${presenter.path}") + private String presenterPath; + + @Value("${links.overlay.url}") + private String overlayUrl; + + @Value("${links.organization.url}") + private String organizationUrl; + + @Value("${links.imprint.url}") + private String imprintUrl; + + @Value("${links.privacy-policy.url}") + private String privacyPolicyUrl; + + @Value("${links.documentation.url}") + private String documentationUrl; + + @Value("${features.mathjax.enabled:true}") + private String mathJaxEnabled; + + @Value("${features.markdown.enabled:false}") + private String markdownEnabled; + + @Value("${features.learning-progress.enabled:false}") + private String learningProgressEnabled; + + @Value("${features.question-format.flashcard.enabled:false}") + private String flashcardEnabled; + + @Value("${features.question-format.grid-square.enabled:false}") + private String gridSquareEnabled; + + @Value("${question.answer-option-limit:8}") + private String answerOptionLimit; + + @Value("${question.parse-answer-option-formatting:false}") + private String parseAnswerOptionFormatting; + + @RequestMapping(method = RequestMethod.GET) + @ResponseBody + public final HashMap<String, Object> getConfiguration(HttpServletRequest request) { + HashMap<String, Object> config = new HashMap<String, Object>(); + HashMap<String, Boolean> features = new HashMap<String, Boolean>(); + + /* The API path could be unknown to the client in case the request was forwarded */ + config.put("apiPath", request.getContextPath()); + + if (!"".equals(customizationPath)) { + config.put("customizationPath", customizationPath); + } + if (!"".equals(mobilePath)) { + config.put("mobilePath", mobilePath); + } + if (!"".equals(presenterPath)) { + config.put("presenterPath", presenterPath); + } + + if (!"".equals(documentationUrl)) { + config.put("documentationUrl", documentationUrl); + } + if (!"".equals(overlayUrl)) { + config.put("overlayUrl", overlayUrl); + } + if (!"".equals(organizationUrl)) { + config.put("organizationUrl", organizationUrl); + } + if (!"".equals(imprintUrl)) { + config.put("imprintUrl", imprintUrl); + } + if (!"".equals(privacyPolicyUrl)) { + config.put("privacyPolicyUrl", privacyPolicyUrl); + } + + config.put("answerOptionLimit", Integer.valueOf(answerOptionLimit)); + config.put("parseAnswerOptionFormatting", Boolean.valueOf(parseAnswerOptionFormatting)); + + config.put("features", features); + + features.put("mathJax", "true".equals(mathJaxEnabled)); + features.put("markdown", "true".equals(markdownEnabled)); + features.put("learningProgress", "true".equals(learningProgressEnabled)); + features.put("flashcard", "true".equals(flashcardEnabled)); + features.put("gridSquare", "true".equals(gridSquareEnabled)); + + return config; + } +} diff --git a/src/main/java/de/thm/arsnova/controller/LoginController.java b/src/main/java/de/thm/arsnova/controller/LoginController.java index 9e4fe2404efa1d74a7aaa1b783d12fb92d7dc9d4..6ab3234e05815d722c5a112f00503a0591364b10 100644 --- a/src/main/java/de/thm/arsnova/controller/LoginController.java +++ b/src/main/java/de/thm/arsnova/controller/LoginController.java @@ -19,7 +19,11 @@ package de.thm.arsnova.controller; import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.text.MessageFormat; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import javax.servlet.ServletException; @@ -33,14 +37,19 @@ import org.scribe.up.session.HttpUserSession; 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.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.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; @@ -51,6 +60,7 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.View; import org.springframework.web.servlet.view.RedirectView; +import de.thm.arsnova.entities.ServiceDescription; import de.thm.arsnova.entities.Session; import de.thm.arsnova.entities.User; import de.thm.arsnova.exceptions.UnauthorizedException; @@ -63,6 +73,72 @@ public class LoginController extends AbstractController { private static final int MAX_USERNAME_LENGTH = 15; private static final int MAX_GUESTHASH_LENGTH = 10; + @Value("${customization.path}") + private String customizationPath; + + @Value("${security.guest.enabled}") + private String guestEnabled; + + @Value("${security.guest.lecturer.enabled}") + private String guestLecturerEnabled; + + @Value("${security.custom-login.enabled}") + private String customLoginEnabled; + + @Value("${security.custom-login.title:University}") + private String customLoginTitle; + + @Value("${security.custom-login.login-dialog-path}") + private String customLoginDialog; + + @Value("${security.custom-login.image:}") + private String customLoginImage; + + @Value("${security.user-db.enabled}") + private String dbAuthEnabled; + + @Value("${security.user-db.title:ARSnova}") + private String dbAuthTitle; + + @Value("${security.user-db.login-dialog-path}") + private String dbAuthDialog; + + @Value("${security.user-db.image:}") + private String dbAuthImage; + + @Value("${security.ldap.enabled}") + private String ldapEnabled; + + @Value("${security.ldap.title:LDAP}") + private String ldapTitle; + + @Value("${security.ldap.login-dialog-path}") + private String ldapDialog; + + @Value("${security.ldap.image:}") + private String ldapImage; + + @Value("${security.cas.enabled}") + private String casEnabled; + + @Value("${security.cas.title:CAS}") + private String casTitle; + + @Value("${security.cas.image:}") + private String casImage; + + @Value("${security.facebook.enabled}") + private String facebookEnabled; + + @Value("${security.google.enabled}") + private String googleEnabled; + + @Value("${security.twitter.enabled}") + private String twitterEnabled; + + @Autowired + private DaoAuthenticationProvider daoProvider; + @Autowired private TwitterProvider twitterProvider; @@ -71,6 +147,9 @@ public class LoginController extends AbstractController { @Autowired private FacebookProvider facebookProvider; + + @Autowired + private LdapAuthenticationProvider ldapAuthenticationProvider; @Autowired private CasAuthenticationEntryPoint casEntryPoint; @@ -83,37 +162,117 @@ public class LoginController extends AbstractController { public static final Logger LOGGER = LoggerFactory.getLogger(LoginController.class); - @RequestMapping(value = { "/auth/login", "/doLogin" }, method = RequestMethod.GET) - public final View doLogin( + @RequestMapping(value = { "/auth/login", "/doLogin" }, method = { RequestMethod.POST, RequestMethod.GET }) + public final void doLogin( @RequestParam("type") final String type, - @RequestParam(value = "user", required = false) final String guestName, - @RequestParam(value = "referer", required = false) final String forcedReferer, - @RequestParam(value = "successurl", required = false) final String successUrl, - @RequestParam(value = "failureurl", required = false) final String failureUrl, + @RequestParam(value = "user", required = false) String username, + @RequestParam(required = false) final String password, @RequestParam(value = "role", required = false) final UserSessionService.Role role, final HttpServletRequest request, final HttpServletResponse response - ) throws IOException, ServletException { + ) throws IOException { + String addr = request.getRemoteAddr(); + if (userService.isBannedFromLogin(addr)) { + response.sendError(429, "Too Many Requests"); + + return; + } + userSessionService.setRole(role); - String referer = request.getHeader("referer"); - if (null != forcedReferer && null != referer && !UrlUtils.isAbsoluteUrl(referer)) { - /* Use a url from a request parameter as referer as long as the url is not absolute (to prevent - * abuse of the redirection). */ - referer = forcedReferer; + if ("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; + } + } catch (AuthenticationException e) { + LOGGER.info("Authentication failed: {}", e.getMessage()); + } + + userService.increaseFailedLoginCount(addr); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + } else if ("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() + ); + + Authentication token = new UsernamePasswordAuthenticationToken(user, password, getAuthorities()); + try { + Authentication auth = ldapAuthenticationProvider.authenticate(token); + if (auth.isAuthenticated()) { + SecurityContextHolder.getContext().setAuthentication(token); + request.getSession(true).setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, + SecurityContextHolder.getContext()); + + return; + } + LOGGER.info("LDAPLOGIN: {}", auth.isAuthenticated()); + } + catch (AuthenticationException e) { + LOGGER.info("No LDAP login: {}", e); + } + + userService.increaseFailedLoginCount(addr); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + } + } else if ("guest".equals(type)) { + List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); + authorities.add(new SimpleGrantedAuthority("ROLE_GUEST")); + if (username == null || !username.startsWith("Guest") || username.length() != MAX_USERNAME_LENGTH) { + username = "Guest" + Sha512DigestUtils.shaHex(request.getSession().getId()).substring(0, MAX_GUESTHASH_LENGTH); + } + 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()); + } + } + + @RequestMapping(value = { "/auth/dialog" }, method = RequestMethod.GET) + @ResponseBody + public final View dialog( + @RequestParam("type") final String type, + @RequestParam(value = "successurl", defaultValue = "/") String successUrl, + @RequestParam(value = "failureurl", defaultValue = "/") String failureUrl, + final HttpServletRequest request, + final HttpServletResponse response + ) throws IOException, ServletException { + View result = null; + + /* Use URLs from a request parameters for redirection as long as the + * URL is not absolute (to prevent abuse of the redirection). */ + if (UrlUtils.isAbsoluteUrl(successUrl)) { + successUrl = "/"; } - if (null == referer) { - referer = "/"; + if (UrlUtils.isAbsoluteUrl(failureUrl)) { + failureUrl = "/"; } - request.getSession().setAttribute("ars-login-success-url", - null == successUrl ? referer + "#auth/checkLogin" : successUrl - ); - request.getSession().setAttribute("ars-login-failure-url", - null == failureUrl ? referer : failureUrl - ); + /* Workaround until a solution is found to do a redirect which is + * relative to the server root instead of the context path */ + String port; + if ("https".equals(request.getScheme())) { + port = 443 != request.getServerPort() ? ":" + request.getLocalPort() : ""; + } else { + port = 80 != request.getServerPort() ? ":" + request.getLocalPort() : ""; + } + String serverUrl = request.getScheme() + "://" + request.getServerName() + port; - View result = null; + request.getSession().setAttribute("ars-login-success-url", serverUrl + successUrl); + request.getSession().setAttribute("ars-login-failure-url", serverUrl + failureUrl); if ("cas".equals(type)) { casEntryPoint.commence(request, response, null); @@ -121,63 +280,124 @@ public class LoginController extends AbstractController { final String authUrl = twitterProvider.getAuthorizationUrl(new HttpUserSession(request)); result = new RedirectView(authUrl); } else if ("facebook".equals(type)) { + facebookProvider.setFields("id,link"); + facebookProvider.setScope(""); final String authUrl = facebookProvider.getAuthorizationUrl(new HttpUserSession(request)); result = new RedirectView(authUrl); } else if ("google".equals(type)) { final String authUrl = googleProvider.getAuthorizationUrl(new HttpUserSession(request)); result = new RedirectView(authUrl); - } else if ("guest".equals(type)) { - result = handleGuestLogin(guestName, successUrl, request, referer); } return result; } - private View handleGuestLogin(final String guestName, - final String successUrl, final HttpServletRequest request, - final String referer) { - View result; - final List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); - authorities.add(new SimpleGrantedAuthority("ROLE_GUEST")); - String username = ""; - if (guestName != null && guestName.startsWith("Guest") && guestName.length() == MAX_USERNAME_LENGTH) { - username = guestName; - } else { - username = "Guest" + Sha512DigestUtils.shaHex( - request.getSession().getId() - ).substring(0, MAX_GUESTHASH_LENGTH); - } - final org.springframework.security.core.userdetails.User user = - new org.springframework.security.core.userdetails.User( - username, "", true, true, true, true, authorities - ); - final Authentication token = new UsernamePasswordAuthenticationToken(user, null, authorities); - - SecurityContextHolder.getContext().setAuthentication(token); - request.getSession(true).setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, - SecurityContextHolder.getContext()); - result = new RedirectView(null == successUrl ? referer + "#auth/checkLogin" : successUrl); - return result; - } - @RequestMapping(value = { "/auth/", "/whoami" }, method = RequestMethod.GET) @ResponseBody public final User whoami() { - userSessionService.setUser(userService.getCurrentUser()); + userSessionService.setUser(userService.getCurrentUser()); return userService.getCurrentUser(); } - @RequestMapping(value = { "/auth/logout", "/logout" }, method = RequestMethod.GET) + @RequestMapping(value = { "/auth/logout", "/logout" }, method = { RequestMethod.POST, RequestMethod.GET } ) public final View 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("/j_spring_cas_security_logout"); + return new RedirectView("/j_spring_cas_security_logout", true); } return new RedirectView(request.getHeader("referer") != null ? request.getHeader("referer") : "/"); } + + @RequestMapping(value = { "/auth/services" }, method = RequestMethod.GET) + @ResponseBody + public final List<ServiceDescription> getServices(final HttpServletRequest request) { + List<ServiceDescription> services = new ArrayList<ServiceDescription>(); + + /* The first parameter is replaced by the backend, the second one by the frondend */ + String dialogUrl = request.getContextPath() + "/auth/dialog?type={0}&successurl='{0}'"; + + if ("true".equals(guestEnabled)) { + ServiceDescription sdesc = new ServiceDescription( + "guest", + "Guest", + null + ); + if (!"true".equals(guestLecturerEnabled)) { + sdesc.setAllowLecturer(false); + } + services.add(sdesc); + } + + if ("true".equals(customLoginEnabled) && !"".equals(customLoginDialog)) { + services.add(new ServiceDescription( + "custom", + customLoginTitle, + customizationPath + "/" + customLoginDialog + "?redirect={0}", + customLoginImage + )); + } + + if ("true".equals(dbAuthEnabled) && !"".equals(dbAuthDialog)) { + services.add(new ServiceDescription( + "arsnova", + dbAuthTitle, + customizationPath + "/" + dbAuthDialog + "?redirect={0}", + dbAuthImage + )); + } + + if ("true".equals(ldapEnabled) && !"".equals(ldapDialog)) { + services.add(new ServiceDescription( + "ldap", + ldapTitle, + customizationPath + "/" + ldapDialog + "?redirect={0}", + ldapImage + )); + } + + if ("true".equals(casEnabled)) { + services.add(new ServiceDescription( + "cas", + casTitle, + MessageFormat.format(dialogUrl, "cas") + )); + } + + if ("true".equals(facebookEnabled)) { + services.add(new ServiceDescription( + "facebook", + "Facebook", + MessageFormat.format(dialogUrl, "facebook") + )); + } + + if ("true".equals(googleEnabled)) { + services.add(new ServiceDescription( + "google", + "Google", + MessageFormat.format(dialogUrl, "google") + )); + } + + if ("true".equals(twitterEnabled)) { + services.add(new ServiceDescription( + "twitter", + "Twitter", + MessageFormat.format(dialogUrl, "twitter") + )); + } + + return services; + } + + private Collection<GrantedAuthority> getAuthorities() { + List<GrantedAuthority> authList = new ArrayList<GrantedAuthority>(); + authList.add(new SimpleGrantedAuthority("ROLE_USER")); + return authList; + } @RequestMapping(value = { "/test/me" }, method = RequestMethod.GET) @ResponseBody diff --git a/src/main/java/de/thm/arsnova/controller/UserController.java b/src/main/java/de/thm/arsnova/controller/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..9d3fe607d20642fb5d2d99f19915f89357d9ef5f --- /dev/null +++ b/src/main/java/de/thm/arsnova/controller/UserController.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2012 THM webMedia + * + * This file is part of ARSnova. + * + * ARSnova 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 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.controller; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +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.authentication.dao.DaoAuthenticationProvider; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; + +import de.thm.arsnova.entities.DbUser; +import de.thm.arsnova.services.IUserService; +import de.thm.arsnova.services.UserSessionService; + +@Controller +@RequestMapping("/user") +public class UserController extends AbstractController { + @Value("${security.guest.enabled}") + private String guestEnabled; + @Value("${security.guest.lecturer.enabled}") + private String guestLecturerEnabled; + @Value("${security.cas.enabled}") + private String casEnabled; + @Value("${security.ldap.enabled}") + private String ldapEnabled; + @Value("${security.facebook.enabled}") + private String facebookEnabled; + @Value("${security.google.enabled}") + private String googleEnabled; + @Value("${security.twitter.enabled}") + private String twitterEnabled; + + @Autowired + private DaoAuthenticationProvider daoProvider; + + @Autowired + private IUserService userService; + + @Autowired + private UserSessionService userSessionService; + + public static final Logger LOGGER = LoggerFactory + .getLogger(UserController.class); + + @RequestMapping(value = { "/register" }, method = RequestMethod.POST) + public final void register(@RequestParam final String username, + @RequestParam final String password, + final HttpServletRequest request, final HttpServletResponse response) { + if (null != userService.createDbUser(username, password)) { + return; + } + + /* TODO: Improve error handling: send reason to client */ + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + } + + @RequestMapping(value = { "/{username}/activate" }, method = { RequestMethod.POST, + RequestMethod.GET }) + public final void activate( + @PathVariable final String username, + @RequestParam final String key, final HttpServletRequest request, + final HttpServletResponse response) { + DbUser dbUser = userService.getDbUser(username); + if (null != dbUser && key.equals(dbUser.getActivationKey())) { + dbUser.setActivationKey(null); + userService.updateDbUser(dbUser); + + return; + } + + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + } + + @RequestMapping(value = { "/{username}" }, method = RequestMethod.DELETE) + public final void activate( + @PathVariable final String username, + final HttpServletRequest request, + final HttpServletResponse response) { + if (null == userService.deleteDbUser(username)) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + } + } + + @RequestMapping(value = { "/{username}/resetpassword" }, method = RequestMethod.POST) + public final void resetPassword( + @PathVariable final String username, + @RequestParam(required = false) final String key, + @RequestParam(required = false) final String password, + final HttpServletRequest request, + final HttpServletResponse response) { + DbUser dbUser = userService.getDbUser(username); + if (null == dbUser) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + if (null != key) { + if (!userService.resetPassword(dbUser, key, password)) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + } + } else { + userService.initiatePasswordReset(username); + } + } +} diff --git a/src/main/java/de/thm/arsnova/controller/WelcomeController.java b/src/main/java/de/thm/arsnova/controller/WelcomeController.java index 588dad75e08a442d13c23a4152e9a6140ec23c83..f54afa314f2ec301eab3fb672eddf3c3ddd30c1d 100644 --- a/src/main/java/de/thm/arsnova/controller/WelcomeController.java +++ b/src/main/java/de/thm/arsnova/controller/WelcomeController.java @@ -18,22 +18,33 @@ */ package de.thm.arsnova.controller; +import java.util.HashMap; + import javax.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.servlet.View; +import org.springframework.web.servlet.view.RedirectView; @Controller public class WelcomeController extends AbstractController { + + @Value("${mobile.path}") + String mobileContextPath; + @RequestMapping(value = "/", method = RequestMethod.GET) - public final ModelAndView home(final HttpServletRequest request) { - String referer = request.getHeader("referer"); - String target = "index.html"; - if (referer != null && referer.endsWith("dojo-index.html")) { - target = "dojo-index.html"; - } - return new ModelAndView("redirect:/" + target); + public final View home(final HttpServletRequest request) { + return new RedirectView(mobileContextPath + "/", false); } + + @RequestMapping(value = "/", method = RequestMethod.GET, produces = "application/json") + @ResponseBody + public final HashMap<String, Object> jsonHome(final HttpServletRequest request) { + return new HashMap<String, Object>(); + } + } diff --git a/src/main/java/de/thm/arsnova/dao/CouchDBDao.java b/src/main/java/de/thm/arsnova/dao/CouchDBDao.java index edc69e4bfab1585d9dc312d50e19488a9483f81d..56a26feae04be2283ed48103085b43f599c58c94 100644 --- a/src/main/java/de/thm/arsnova/dao/CouchDBDao.java +++ b/src/main/java/de/thm/arsnova/dao/CouchDBDao.java @@ -41,6 +41,7 @@ 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.stereotype.Component; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; @@ -51,6 +52,7 @@ import com.fourspaces.couchdb.ViewResults; import de.thm.arsnova.connector.model.Course; import de.thm.arsnova.entities.Answer; +import de.thm.arsnova.entities.DbUser; import de.thm.arsnova.entities.FoodVote; import de.thm.arsnova.entities.InterposedQuestion; import de.thm.arsnova.entities.InterposedReadingCount; @@ -63,6 +65,7 @@ import de.thm.arsnova.entities.VisitedSession; import de.thm.arsnova.exceptions.NotFoundException; import de.thm.arsnova.services.ISessionService; +@Component("databaseDao") public class CouchDBDao implements IDatabaseDao { @Autowired @@ -392,6 +395,11 @@ public class CouchDBDao implements IDatabaseDao { final Collection<VisitedSession> visitedSessions = JSONArray.toCollection(vs, VisitedSession.class); loggedIn.setVisitedSessions(new ArrayList<VisitedSession>(visitedSessions)); } + + /* Do not clutter CouchDB. Only update once every 3 hours per session. */ + if (loggedIn.getSessionId().equals(session.get_id()) && loggedIn.getTimestamp() > System.currentTimeMillis() - 3 * 3600000) { + return loggedIn; + } } loggedIn.setUser(user.getUsername()); @@ -427,6 +435,11 @@ public class CouchDBDao implements IDatabaseDao { @Override public final void updateSessionOwnerActivity(final Session session) { try { + /* Do not clutter CouchDB. Only update once every 3 hours. */ + if (session.getLastOwnerActivity() > System.currentTimeMillis() - 3 * 3600000) { + return; + } + session.setLastOwnerActivity(System.currentTimeMillis()); final JSONObject json = JSONObject.fromObject(session); getDatabase().saveDocument(new Document(json)); @@ -1407,4 +1420,64 @@ public class CouchDBDao implements IDatabaseDao { return new AbstractMap.SimpleEntry<Integer, Integer>((int)Math.round(myProgress*100), courseProgress); } + + public DbUser createOrUpdateUser(DbUser user) { + try { + String id = user.getId(); + String rev = user.getRev(); + Document d = new Document(); + + if (null != id) { + d = database.getDocument(id, rev); + } + + d.put("type", "userdetails"); + d.put("username", user.getUsername()); + d.put("password", user.getPassword()); + d.put("activationKey", user.getActivationKey()); + d.put("passwordResetKey", user.getPasswordResetKey()); + d.put("passwordResetTime", user.getPasswordResetTime()); + d.put("creation", user.getCreation()); + d.put("lastLogin", user.getLastLogin()); + + database.saveDocument(d, id); + user.setId(d.getId()); + user.setRev(d.getRev()); + + return user; + } catch (IOException e) { + LOGGER.error("Could not save user {}", user); + } + + return null; + } + + @Override + public DbUser getUser(String username) { + NovaView view = new NovaView("user/all"); + view.setKey(username); + ViewResults results = this.getDatabase().view(view); + + if (results.getJSONArray("rows").optJSONObject(0) == null) { + return null; + } + + return (DbUser) JSONObject.toBean( + results.getJSONArray("rows").optJSONObject(0).optJSONObject("value"), + DbUser.class + ); + } + + @Override + public boolean deleteUser(DbUser dbUser) { + try { + this.deleteDocument(dbUser.getId()); + + return true; + } catch (IOException e) { + LOGGER.error("Could not delete user {}", dbUser.getId()); + } + + return false; + } } diff --git a/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java b/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java index 40bb3f97ca33db3970cc8dd5f04f072f5d879471..67eaeaede0dd6e5c367b0d2465a56cd72a2cf73b 100644 --- a/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java +++ b/src/main/java/de/thm/arsnova/dao/IDatabaseDao.java @@ -24,6 +24,7 @@ import java.util.List; import de.thm.arsnova.connector.model.Course; import de.thm.arsnova.entities.Answer; +import de.thm.arsnova.entities.DbUser; import de.thm.arsnova.entities.FoodVote; import de.thm.arsnova.entities.InterposedQuestion; import de.thm.arsnova.entities.InterposedReadingCount; @@ -163,6 +164,12 @@ public interface IDatabaseDao { void deleteAllQuestionsAnswers(Session session); + DbUser createOrUpdateUser(DbUser user); + + DbUser getUser(String username); + + boolean deleteUser(DbUser dbUser); + int getLearningProgress(Session session); SimpleEntry<Integer, Integer> getMyLearningProgress(Session session, User user); diff --git a/src/main/java/de/thm/arsnova/entities/DbUser.java b/src/main/java/de/thm/arsnova/entities/DbUser.java new file mode 100644 index 0000000000000000000000000000000000000000..7d185c25e5b4b435bec2bfc20f3f78715899f3e9 --- /dev/null +++ b/src/main/java/de/thm/arsnova/entities/DbUser.java @@ -0,0 +1,100 @@ +package de.thm.arsnova.entities; + +public class DbUser { + private String id; + private String rev; + private String username; + private String password; + private String activationKey; + private String passwordResetKey; + private long passwordResetTime; + private long creation; + private long lastLogin; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + /* CouchDB deserialization */ + public void set_id(String id) { + this.id = id; + } + + public String getRev() { + return rev; + } + + public void setRev(String rev) { + this.rev = rev; + } + + /* CouchDB deserialization */ + public void set_rev(String rev) { + this.rev = rev; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getActivationKey() { + return activationKey; + } + + public void setActivationKey(String activationKey) { + this.activationKey = activationKey; + } + + public String getPasswordResetKey() { + return passwordResetKey; + } + + public void setPasswordResetKey(String passwordResetKey) { + this.passwordResetKey = passwordResetKey; + } + + public long getPasswordResetTime() { + return passwordResetTime; + } + + public void setPasswordResetTime(long passwordResetTime) { + this.passwordResetTime = passwordResetTime; + } + + public long getCreation() { + return creation; + } + + public void setCreation(long creation) { + this.creation = creation; + } + + public long getLastLogin() { + return lastLogin; + } + + public void setLastLogin(long lastLogin) { + this.lastLogin = lastLogin; + } + + /* CouchDB deserialization */ + public void setType(String type) { + /* no op */ + } +} diff --git a/src/main/java/de/thm/arsnova/entities/ServiceDescription.java b/src/main/java/de/thm/arsnova/entities/ServiceDescription.java new file mode 100644 index 0000000000000000000000000000000000000000..f9199ac039a0917062dd4c2c1e6b9528f2013ad1 --- /dev/null +++ b/src/main/java/de/thm/arsnova/entities/ServiceDescription.java @@ -0,0 +1,80 @@ +package de.thm.arsnova.entities; + +public class ServiceDescription { + private String id; + private String name; + private String dialogUrl; + private String image; + private int order = 0; + private boolean allowLecturer = true; + + public ServiceDescription(String id, String name, String dialogUrl) { + this.id = id; + this.name = name; + this.dialogUrl = dialogUrl; + } + + public ServiceDescription(String id, String name, String dialogUrl, String image) { + this.id = id; + this.name = name; + this.dialogUrl = dialogUrl; + if (!"".equals(image)) { + this.image = image; + } + } + + public ServiceDescription(String id, String name, String dialogUrl, boolean allowLecturer) { + this.id = id; + this.name = name; + this.dialogUrl = dialogUrl; + this.allowLecturer = allowLecturer; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDialogUrl() { + return dialogUrl; + } + + public void setDialogUrl(String dialogUrl) { + this.dialogUrl = dialogUrl; + } + + public String getImage() { + return image; + } + + public void setImage(String image) { + this.image = image; + } + + public int getOrder() { + return order; + } + + public void setOrder(int order) { + this.order = order; + } + + public boolean isAllowLecturer() { + return allowLecturer; + } + + public void setAllowLecturer(boolean allowLecturer) { + this.allowLecturer = allowLecturer; + } +} diff --git a/src/main/java/de/thm/arsnova/entities/User.java b/src/main/java/de/thm/arsnova/entities/User.java index 2f9c290bf2af3a41d6eac5b16d329d413e0ee4ff..ba64e2ffb0f94cbac3dc93e45c40f1b8545669ef 100644 --- a/src/main/java/de/thm/arsnova/entities/User.java +++ b/src/main/java/de/thm/arsnova/entities/User.java @@ -17,7 +17,9 @@ public class User implements Serializable { public static final String FACEBOOK = "facebook"; public static final String THM = "thm"; public static final String LDAP = "ldap"; + public static final String ARSNOVA = "arsnova"; public static final String ANONYMOUS = "anonymous"; + public static final String GUEST = "guest"; private static final long serialVersionUID = 1L; private String username; diff --git a/src/main/java/de/thm/arsnova/security/DbUserDetailsService.java b/src/main/java/de/thm/arsnova/security/DbUserDetailsService.java new file mode 100644 index 0000000000000000000000000000000000000000..cbee6375e0b28776e1bead12adfedbeaf0b7e1e7 --- /dev/null +++ b/src/main/java/de/thm/arsnova/security/DbUserDetailsService.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2014 THM webMedia + * + * This file is part of ARSnova. + * + * ARSnova 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 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 java.util.ArrayList; +import java.util.List; + +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 de.thm.arsnova.dao.IDatabaseDao; +import de.thm.arsnova.entities.DbUser; + +@Service +public class DbUserDetailsService implements UserDetailsService { + @Autowired + private IDatabaseDao dao; + + public static final Logger LOGGER = LoggerFactory + .getLogger(DbUserDetailsService.class); + + @Override + public UserDetails loadUserByUsername(String username) { + LOGGER.debug("Load user: " + username); + DbUser dbUser = dao.getUser(username); + if (null == dbUser) { + throw new UsernameNotFoundException("User does not exist."); + } + + final List<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority>(); + grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER")); + grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_DB_USER")); + + return new User(username, dbUser.getPassword(), + null == dbUser.getActivationKey(), true, true, true, + grantedAuthorities); + } +} diff --git a/src/main/java/de/thm/arsnova/services/IUserService.java b/src/main/java/de/thm/arsnova/services/IUserService.java index 8716c3ebcaba693e88441cd0894bd32c46ce73d0..ee4258519f9b674dd2fd7159384ccd9e3d5710cf 100644 --- a/src/main/java/de/thm/arsnova/services/IUserService.java +++ b/src/main/java/de/thm/arsnova/services/IUserService.java @@ -23,10 +23,15 @@ import java.util.Map; import java.util.Set; import java.util.UUID; +import de.thm.arsnova.entities.DbUser; import de.thm.arsnova.entities.User; public interface IUserService { User getCurrentUser(); + + boolean isBannedFromLogin(String addr); + + void increaseFailedLoginCount(String addr); User getUser2SocketId(UUID socketId); @@ -51,4 +56,16 @@ public interface IUserService { void removeUserFromMaps(User user); int loggedInUsers(); + + DbUser getDbUser(String username); + + DbUser createDbUser(String username, String password); + + DbUser updateDbUser(DbUser dbUser); + + DbUser deleteDbUser(String username); + + void initiatePasswordReset(String username); + + boolean resetPassword(DbUser dbUser, String key, String password); } diff --git a/src/main/java/de/thm/arsnova/services/SessionService.java b/src/main/java/de/thm/arsnova/services/SessionService.java index 595c0567a9e7b27320fafc439ab3ffe7b891c309..d2742c182e907e67e451b35661d7baa360b378c7 100644 --- a/src/main/java/de/thm/arsnova/services/SessionService.java +++ b/src/main/java/de/thm/arsnova/services/SessionService.java @@ -84,9 +84,16 @@ public class SessionService implements ISessionService { public final Session joinSession(final String keyword, final UUID socketId) { /* Socket.IO solution */ - final Session session = databaseDao.getSession(keyword); + Session session = null; + try { + session = databaseDao.getSession(keyword); + } catch (NotFoundException e) { + + } if (null == session) { - throw new NotFoundException(); + userService.removeUserFromSessionBySocketId(socketId); + + return null; } final User user = userService.getUser2SocketId(socketId); diff --git a/src/main/java/de/thm/arsnova/services/StatisticsService.java b/src/main/java/de/thm/arsnova/services/StatisticsService.java index 435e249cfc47ca91ec1522bd615095d3236d7b5a..5446fd0d8cc1cfad28c6258ecf355402fb9ca715 100644 --- a/src/main/java/de/thm/arsnova/services/StatisticsService.java +++ b/src/main/java/de/thm/arsnova/services/StatisticsService.java @@ -15,6 +15,9 @@ public class StatisticsService implements IStatisticsService { @Autowired private IDatabaseDao databaseDao; + @Autowired + private IUserService userService; + @Autowired private SessionRegistry sessionRegistry; @@ -27,8 +30,10 @@ public class StatisticsService implements IStatisticsService { statistics.setClosedSessions(databaseDao.countClosedSessions()); statistics.setAnswers(databaseDao.countAnswers()); statistics.setQuestions(databaseDao.countQuestions()); - statistics.setActiveUsers(databaseDao.countActiveUsers(since)); + /* TODO: Are both of the following do the same, now? If so, remove one of them. */ + statistics.setActiveUsers(userService.loggedInUsers()); statistics.setLoggedinUsers(countLoggedInUsers()); + return statistics; } diff --git a/src/main/java/de/thm/arsnova/services/UserService.java b/src/main/java/de/thm/arsnova/services/UserService.java index 96785274ba47d69828fe815f27436bf06563def5..adc41130b4ab431d0878f2f0828ce7214afb4f07 100644 --- a/src/main/java/de/thm/arsnova/services/UserService.java +++ b/src/main/java/de/thm/arsnova/services/UserService.java @@ -1,5 +1,10 @@ package de.thm.arsnova.services; +import java.io.UnsupportedEncodingException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -7,37 +12,64 @@ import java.util.Map.Entry; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; import javax.annotation.PreDestroy; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import org.apache.commons.lang.RandomStringUtils; +import org.apache.commons.lang.StringUtils; import org.scribe.up.profile.facebook.FacebookProfile; import org.scribe.up.profile.google.Google2Profile; import org.scribe.up.profile.twitter.TwitterProfile; 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.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.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.keygen.BytesKeyGenerator; +import org.springframework.security.crypto.keygen.KeyGenerators; +import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.util.UriUtils; import com.github.leleuj.ss.oauth.client.authentication.OAuthAuthenticationToken; import de.thm.arsnova.dao.IDatabaseDao; +import de.thm.arsnova.entities.DbUser; import de.thm.arsnova.entities.User; +import de.thm.arsnova.exceptions.BadRequestException; +import de.thm.arsnova.exceptions.NotFoundException; import de.thm.arsnova.exceptions.UnauthorizedException; import de.thm.arsnova.socket.ARSnovaSocketIOServer; +@Service public class UserService implements IUserService { private static final int DEFAULT_SCHEDULER_DELAY_MS = 60000; + private static final int LOGIN_TRY_RESET_DELAY_MS = 30 * 1000; + + private static final int LOGIN_BAN_RESET_DELAY_MS = 2 * 60 * 1000; + private static final int MAX_USER_INACTIVE_SECONDS = 120; + private static final int REPEATED_PASSWORD_RESET_DELAY_MS = 3 * 60 * 1000; + + private static final int PASSWORD_RESET_KEY_DURABILITY_MS = 2 * 60 * 60 * 1000; + public static final Logger LOGGER = LoggerFactory.getLogger(UserService.class); private static final ConcurrentHashMap<UUID, User> socketid2user = new ConcurrentHashMap<UUID, User>(); @@ -54,6 +86,72 @@ public class UserService implements IUserService { @Autowired private ARSnovaSocketIOServer socketIoServer; + @Autowired + private JavaMailSender mailSender; + + @Value("${root-url}") + private String rootUrl; + + @Value("${customization.path}") + private String customizationPath; + + @Value("${security.user-db.allowed-email-domains}") + private String allowedEmailDomains; + + @Value("${security.user-db.activation-path}") + private String activationPath; + + @Value("${security.user-db.reset-password-path}") + private String resetPasswordPath; + + @Value("${mail.sender.address}") + private String mailSenderAddress; + + @Value("${mail.sender.name}") + private String mailSenderName; + + @Value("${security.user-db.registration-mail.subject}") + private String regMailSubject; + + @Value("${security.user-db.registration-mail.body}") + private String regMailBody; + + @Value("${security.user-db.reset-password-mail.subject}") + private String resetPasswordMailSubject; + + @Value("${security.user-db.reset-password-mail.body}") + private String resetPasswordMailBody; + + @Value("${security.authentication.login-try-limit}") + private int loginTryLimit; + + private Pattern mailPattern; + private BytesKeyGenerator keygen; + private BCryptPasswordEncoder encoder; + private ConcurrentHashMap<String, Byte> loginTries; + private Set<String> loginBans; + + { + loginTries = new ConcurrentHashMap<String, Byte>(); + loginBans = Collections.synchronizedSet(new HashSet<String>()); + } + + @Scheduled(fixedDelay = LOGIN_TRY_RESET_DELAY_MS) + public void resetLoginTries() { + if (loginTries.size() > 0) { + LOGGER.debug("Reset failed login counters."); + loginTries.clear(); + } + } + + @Scheduled(fixedDelay = LOGIN_BAN_RESET_DELAY_MS) + public void resetLoginBans() { + if (loginBans.size() > 0) { + LOGGER.info("Reset temporary login bans."); + loginBans.clear(); + } + } + @Scheduled(fixedDelay = DEFAULT_SCHEDULER_DELAY_MS) public final void removeInactiveUsersFromLegacyMap() { final List<String> usernames = databaseDao.getActiveUsers(MAX_USER_INACTIVE_SECONDS); @@ -96,6 +194,11 @@ public class UserService implements IUserService { } else if (authentication instanceof UsernamePasswordAuthenticationToken) { final UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication; user = new User(token); + if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_GUEST"))) { + user.setType(User.GUEST); + } else if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_DB_USER"))) { + user.setType(User.ARSNOVA); + } } if (user == null || user.getUsername().equals("anonymous")) { @@ -121,6 +224,24 @@ public class UserService implements IUserService { return user; } + public boolean isBannedFromLogin(String addr) { + return loginBans.contains(addr); + } + + public void increaseFailedLoginCount(String addr) { + Byte tries = (Byte) loginTries.get(addr); + if (null == tries) { + tries = 0; + } + if (tries < 5) { + loginTries.put(addr, ++tries); + if (5 == tries) { + LOGGER.info("Temporarily banned {} from login.", new Object[] {addr}); + loginBans.add(addr); + } + } + } + @Override public User getUser2SocketId(final UUID socketId) { return socketid2user.get(socketId); @@ -236,4 +357,191 @@ public class UserService implements IUserService { public int loggedInUsers() { return user2sessionLegacy.size(); } + + @Override + public DbUser getDbUser(String username) { + return databaseDao.getUser(username); + } + + @Override + public DbUser createDbUser(String username, String password) { + if (null == keygen) { + keygen = KeyGenerators.secureRandom(32); + } + + if (null == mailPattern) { + parseMailAddressPattern(); + } + + if (null == mailPattern || !mailPattern.matcher(username).matches()) { + return null; + } + + if (null != databaseDao.getUser(username)) { + return null; + } + + DbUser dbUser = new DbUser(); + dbUser.setUsername(username); + dbUser.setPassword(encodePassword(password)); + dbUser.setActivationKey(RandomStringUtils.randomAlphanumeric(32)); + dbUser.setCreation(System.currentTimeMillis()); + + DbUser result = databaseDao.createOrUpdateUser(dbUser); + if (null != result) { + sendActivationEmail(result); + } + + return result; + } + + public String encodePassword(String password) { + if (null == encoder) { + encoder = new BCryptPasswordEncoder(12); + } + + return encoder.encode(password); + } + + public void sendActivationEmail(DbUser dbUser) { + String activationUrl; + try { + activationUrl = MessageFormat.format( + "{0}{1}/{2}?action=activate&username={3}&key={4}", + rootUrl, + customizationPath, + activationPath, + UriUtils.encodeQueryParam(dbUser.getUsername(), "UTF-8"), + dbUser.getActivationKey() + ); + } catch (UnsupportedEncodingException e1) { + LOGGER.error(e1.getMessage()); + + return; + } + + sendEmail(dbUser, regMailSubject, MessageFormat.format(regMailBody, activationUrl)); + } + + private void parseMailAddressPattern() { + /* TODO: Add Unicode support */ + + List<String> domainList = Arrays.asList(allowedEmailDomains.split(",")); + + if (domainList.size() > 0) { + List<String> patterns = new ArrayList<String>(); + if (domainList.contains("*")) { + patterns.add("([a-z0-9-]+\\.)+[a-z0-9-]+"); + } else { + Pattern patternPattern = Pattern.compile("[a-z0-9.*-]+", Pattern.CASE_INSENSITIVE); + for (String patternStr : domainList) { + if (patternPattern.matcher(patternStr).matches()) { + patterns.add(patternStr.replaceAll("[.]", "[.]").replaceAll("[*]", "[a-z0-9-]+?")); + } + } + } + + mailPattern = Pattern.compile("[a-z0-9._-]+?@(" + StringUtils.join(patterns, "|") + ")", Pattern.CASE_INSENSITIVE); + LOGGER.info("Allowed e-mail addresses (pattern) for registration: " + mailPattern.pattern()); + } + } + + @Override + public DbUser updateDbUser(DbUser dbUser) { + if (null != dbUser.getId()) { + return databaseDao.createOrUpdateUser(dbUser); + } + + return null; + } + + @Override + public DbUser deleteDbUser(String username) { + User user = getCurrentUser(); + if (!user.getUsername().equals(username) + && SecurityContextHolder.getContext().getAuthentication().getAuthorities() + .contains(new SimpleGrantedAuthority("ROLE_ADMIN"))) { + throw new UnauthorizedException(); + } + + DbUser dbUser = databaseDao.getUser(username); + if (null == dbUser) { + throw new NotFoundException(); + } + + databaseDao.deleteUser(dbUser); + + return dbUser; + } + + @Override + public void initiatePasswordReset(String username) { + DbUser dbUser = databaseDao.getUser(username); + if (null == dbUser) { + throw new NotFoundException(); + } + if (System.currentTimeMillis() < dbUser.getPasswordResetTime() + REPEATED_PASSWORD_RESET_DELAY_MS) { + throw new BadRequestException(); + } + + dbUser.setPasswordResetKey(RandomStringUtils.randomAlphanumeric(32)); + dbUser.setPasswordResetTime(System.currentTimeMillis()); + databaseDao.createOrUpdateUser(dbUser); + + String resetPasswordUrl; + try { + resetPasswordUrl = MessageFormat.format( + "{0}{1}/{2}?action=resetpassword&username={3}&key={4}", + rootUrl, + customizationPath, + resetPasswordPath, + UriUtils.encodeQueryParam(dbUser.getUsername(), "UTF-8"), + dbUser.getPasswordResetKey() + ); + } catch (UnsupportedEncodingException e1) { + LOGGER.error(e1.getMessage()); + + return; + } + + sendEmail(dbUser, resetPasswordMailSubject, MessageFormat.format(resetPasswordMailBody, resetPasswordUrl)); + } + + @Override + public boolean resetPassword(DbUser dbUser, String key, String password) { + if (null == key || "".equals(key) || !key.equals(dbUser.getPasswordResetKey())) { + return false; + } + if (System.currentTimeMillis() > dbUser.getPasswordResetTime() + PASSWORD_RESET_KEY_DURABILITY_MS) { + dbUser.setPasswordResetKey(null); + dbUser.setPasswordResetTime(0); + updateDbUser(dbUser); + + return false; + } + + dbUser.setPassword(encodePassword(password)); + dbUser.setPasswordResetKey(null); + updateDbUser(dbUser); + + return true; + } + + private void sendEmail(DbUser dbUser, String subject, String body) { + MimeMessage msg = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(msg, "UTF-8"); + try { + helper.setFrom(mailSenderName + "<" + mailSenderAddress + ">"); + helper.setTo(dbUser.getUsername()); + helper.setSubject(subject); + helper.setText(body); + + LOGGER.info("Sending mail \"{}\" from \"{}\" to \"{}\"", new Object[] {subject, msg.getFrom(), dbUser.getUsername()}); + mailSender.send(msg); + } catch (MessagingException e) { + LOGGER.warn("Mail \"{}\" could not be sent: {}", subject, e); + } catch (MailException e) { + LOGGER.warn("Mail \"{}\" could not be sent: {}", subject, e); + } + } } diff --git a/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java b/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java index 389b03ea4c2af948e9bbab7cddd0b2c6d6597e82..9348cf97c0146e180d14905ad71317bcf9e1f07d 100644 --- a/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java +++ b/src/main/java/de/thm/arsnova/socket/ARSnovaSocketIOServer.java @@ -55,8 +55,6 @@ public class ARSnovaSocketIOServer { private final Configuration config; private SocketIOServer server; - private int lastActiveUserCount = 0; - public ARSnovaSocketIOServer() { config = new Configuration(); } @@ -99,26 +97,38 @@ public class ARSnovaSocketIOServer { server.addEventListener("setFeedback", Feedback.class, new DataListener<Feedback>() { @Override public void onData(final SocketIOClient client, final Feedback data, final AckRequest ackSender) { - /** - * do a check if user is in the session, for which he would give - * a feedback - */ final User u = userService.getUser2SocketId(client.getSessionId()); - if (u == null || !userService.isUserInSession(u, data.getSessionkey())) { - return; + final String sessionKey = userService.getSessionForUser(u.getUsername()); + LOGGER.debug("Feedback recieved: {}", new Object[] {u, sessionKey, data.getValue()}); + if (null != sessionKey) { + feedbackService.saveFeedback(sessionKey, data.getValue(), u); } - feedbackService.saveFeedback(data.getSessionkey(), data.getValue(), u); } }); server.addEventListener("setSession", Session.class, new DataListener<Session>() { @Override public void onData(final SocketIOClient client, final Session session, final AckRequest ackSender) { - sessionService.joinSession(session.getKeyword(), client.getSessionId()); - /* active user count has to be sent to the client since the broadcast is - * not always sent as long as the polling solution is active simultaneously */ - reportActiveUserCountForSession(session.getKeyword()); - reportSessionDataToClient(session.getKeyword(), client); + final User u = userService.getUser2SocketId(client.getSessionId()); + if (null == u) { + LOGGER.info("Client {} requested to join session but is not mapped to a user", client.getSessionId()); + + return; + } + final String oldSessionKey = userService.getSessionForUser(u.getUsername()); + if (session.getKeyword() == oldSessionKey) { + return; + } + if (null != oldSessionKey) { + reportActiveUserCountForSession(oldSessionKey); + } + + if (null != sessionService.joinSession(session.getKeyword(), client.getSessionId())) { + /* active user count has to be sent to the client since the broadcast is + * not always sent as long as the polling solution is active simultaneously */ + reportActiveUserCountForSession(session.getKeyword()); + reportSessionDataToClient(session.getKeyword(), client); + } } }); @@ -281,11 +291,7 @@ public class ARSnovaSocketIOServer { public void reportActiveUserCountForSession(final String sessionKey) { /* This check is needed as long as the HTTP polling solution is active simultaneously. */ - final int count = sessionService.activeUsers(sessionKey); - if (count == lastActiveUserCount) { - return; - } - lastActiveUserCount = count; + final int count = userService.getUsersInSession(sessionKey).size(); broadcastInSession(sessionKey, "activeUserCountData", count); } diff --git a/src/main/java/de/thm/arsnova/socket/message/Feedback.java b/src/main/java/de/thm/arsnova/socket/message/Feedback.java index 8fdff90a09aec0d8945d99a793c6520b9b014831..2168e8d59574e1d3187dbf638f4bffcd19b3ccf1 100644 --- a/src/main/java/de/thm/arsnova/socket/message/Feedback.java +++ b/src/main/java/de/thm/arsnova/socket/message/Feedback.java @@ -3,15 +3,6 @@ package de.thm.arsnova.socket.message; public class Feedback { private int value; - private String sessionkey; - - public String getSessionkey() { - return sessionkey; - } - - public void setSessionkey(String keyword) { - this.sessionkey = keyword; - } public int getValue() { return value; @@ -23,6 +14,6 @@ public class Feedback { @Override public String toString() { - return "Feedback, sessionkey: " + sessionkey + ", value: " + value; + return "Feedback, value: " + value; } } diff --git a/src/main/resources/test.ldif b/src/main/resources/test.ldif new file mode 100644 index 0000000000000000000000000000000000000000..54bc2b1fbbf12326ce0a581f33b83524600afaf6 --- /dev/null +++ b/src/main/resources/test.ldif @@ -0,0 +1,26 @@ +dn: dc=example, dc=com +objectclass: organization +objectclass: top +o: Dummy Organisation + +dn: ou=people, dc=example, dc=com +objectclass: organizationalunit +ou: people + +dn: uid=ptsr00, ou=people, dc=example, dc=com +objectclass: person +objectclass: organizationalperson +objectclass: inetorgperson +uid: ptsr00 +sn: Tester +givenName: Patrick +userPassword:: VGVzdA== + +dn: uid=ptsr01, ou=people, dc=example, dc=com +objectclass: person +objectclass: organizationalperson +objectclass: inetorgperson +uid: ptsr01 +sn: Tester +givenName: Patricia +userPassword:: VGVzdA== diff --git a/src/main/webapp/WEB-INF/spring/arsnova-servlet.xml b/src/main/webapp/WEB-INF/spring/arsnova-servlet.xml index c06457d5453f8ad978b86f2346c2428a93d2e76d..1dc289bf8d65a31be351a0f1609a250039b5ae22 100644 --- a/src/main/webapp/WEB-INF/spring/arsnova-servlet.xml +++ b/src/main/webapp/WEB-INF/spring/arsnova-servlet.xml @@ -1,7 +1,10 @@ <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" - xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:aop="http://www.springframework.org/schema/aop" + xmlns:mvc="http://www.springframework.org/schema/mvc" + xmlns:context="http://www.springframework.org/schema/context" + xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd @@ -10,6 +13,7 @@ <!-- ARSnova Servlet Context --> <context:component-scan base-package="de.thm.arsnova.controller,de.thm.arsnova.web" /> + <context:property-placeholder location="file:///etc/arsnova/arsnova.properties" file-encoding="UTF-8" /> <mvc:annotation-driven content-negotiation-manager="contentNegotiationManager" /> diff --git a/src/main/webapp/WEB-INF/spring/spring-main.xml b/src/main/webapp/WEB-INF/spring/spring-main.xml index d39f43aae035f3e195f8f3f48bad878d44f3ccee..c94ed0de36aafc55edaee52ebc5d97e7b5ab0137 100644 --- a/src/main/webapp/WEB-INF/spring/spring-main.xml +++ b/src/main/webapp/WEB-INF/spring/spring-main.xml @@ -20,10 +20,13 @@ <value>file:///etc/arsnova/arsnova.properties</value> </list> </property> + <property name="fileEncoding" value="UTF-8" /> </bean> - <context:component-scan - base-package="de.thm.arsnova.dao,de.thm.arsnova.services,de.thm.arsnova.events,de.thm.arsnova.config" /> + <import resource="spring-security.xml" /> + + <context:component-scan base-package="de.thm.arsnova.dao,de.thm.arsnova.events,de.thm.arsnova.security,de.thm.arsnova.services,de.thm.arsnova.config" /> + <context:annotation-config /> <task:annotation-driven /> @@ -34,7 +37,9 @@ <bean id="userSessionAspect" class="de.thm.arsnova.aop.UserSessionAspect" /> - <bean id="userService" scope="singleton" class="de.thm.arsnova.services.UserService" /> + <bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl"> + <property name="host" value="${mail.host}"/> + </bean> <bean id="databaseDao" class="de.thm.arsnova.dao.CouchDBDao" /> </beans> diff --git a/src/main/webapp/WEB-INF/spring/spring-security.xml b/src/main/webapp/WEB-INF/spring/spring-security.xml index 8c32335b1d358d4a89f04b2a4237d3cdf44d6520..0d843b17bc58c460b7ee4aab2094b2f92ec9b302 100644 --- a/src/main/webapp/WEB-INF/spring/spring-security.xml +++ b/src/main/webapp/WEB-INF/spring/spring-security.xml @@ -1,94 +1,99 @@ <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" - xmlns:p="http://www.springframework.org/schema/p" xmlns:security="http://www.springframework.org/schema/security" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:security="http://www.springframework.org/schema/security" + xmlns:context="http://www.springframework.org/schema/context" + xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd"> <security:authentication-manager alias="authenticationManager"> - <security:authentication-provider - ref="facebookAuthProvider" /> - <security:authentication-provider - ref="twitterAuthProvider" /> - <security:authentication-provider - ref="googleAuthProvider" /> - <security:authentication-provider - ref="casAuthenticationProvider" /> + <!-- <security:ldap-authentication-provider + user-search-filter="${security.ldap.user-search-filter}" + user-search-base="${security.ldap.user-search-base}" /> + --> + <security:ldap-authentication-provider user-dn-pattern="${security.ldap.user-dn-pattern}" /> + <security:authentication-provider ref="facebookAuthProvider" /> + <security:authentication-provider ref="twitterAuthProvider" /> + <security:authentication-provider ref="googleAuthProvider" /> + <security:authentication-provider ref="casAuthenticationProvider" /> + <security:authentication-provider ref="daoAuthenticationProvider" /> </security:authentication-manager> - <security:http entry-point-ref="facebookEntryPoint"> - <security:custom-filter ref="facebookFilter" - before="CAS_FILTER" /> - <security:custom-filter ref="twitterFilter" - after="CAS_FILTER" /> - <security:custom-filter ref="googleFilter" - before="FORM_LOGIN_FILTER" /> - - <security:custom-filter ref="casAuthenticationFilter" - position="CAS_FILTER" /> - <security:custom-filter ref="requestSingleLogoutFilter" - before="LOGOUT_FILTER" /> + <security:http entry-point-ref="restLoginEntryPoint"> + <security:custom-filter ref="facebookFilter" before="CAS_FILTER" /> + <security:custom-filter ref="twitterFilter" after="CAS_FILTER" /> + <security:custom-filter ref="googleFilter" before="FORM_LOGIN_FILTER" /> + <security:custom-filter ref="casAuthenticationFilter" position="CAS_FILTER" /> + <security:custom-filter ref="requestSingleLogoutFilter" before="LOGOUT_FILTER" /> </security:http> - <security:global-method-security - pre-post-annotations="enabled"> - <security:expression-handler ref="expressionHandler" /> - - </security:global-method-security> - - <bean id="permissionEvaluator" class="de.thm.arsnova.security.ApplicationPermissionEvaluator" /> - - <bean id="expressionHandler" - class="org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler"> - <property name="permissionEvaluator" ref="permissionEvaluator" /> + <!-- ######################### DB auth ############################# --> + <bean id="daoAuthenticationProvider" + class="org.springframework.security.authentication.dao.DaoAuthenticationProvider"> + <property name="userDetailsService" ref="dbUserDetailsService" /> + <property name="passwordEncoder" ref="passwordEncoder" /> + </bean> + + <bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" /> + + <!-- ######################### LDAP ############################# --> + <security:ldap-server url="${security.ldap.url}" /> + <!-- <security:ldap-server ldif="classpath:/test.ldif" root="dc=example,dc=com" /> --> + + <bean id="restLoginEntryPoint" class="org.springframework.security.web.authentication.Http403ForbiddenEntryPoint" /> + + <bean id="loginUrlAuthenticationEntryPoint" + class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint"> + <property name="loginFormUrl" value="/login.html" /> + <property name="forceHttps" value="false" /> </bean> <!-- ######################### FACEBOOK ######################### --> - <bean id="facebookEntryPoint" - class="com.github.leleuj.ss.oauth.client.web.OAuthAuthenticationEntryPoint" + <bean id="facebookEntryPoint" class="com.github.leleuj.ss.oauth.client.web.OAuthAuthenticationEntryPoint" p:provider-ref="facebookProvider" /> <bean id="facebookProvider" class="org.scribe.up.provider.impl.FacebookProvider" - p:key="${security.facebook.key}" p:secret="${security.facebook.secret}" - p:callbackUrl="${security.arsnova-url}/j_spring_facebook_security_check" /> + p:key="${security.facebook.key}" + p:secret="${security.facebook.secret}" + p:callbackUrl="${root-url}#{servletContext.contextPath}/j_spring_facebook_security_check" /> - <bean id="facebookFilter" - class="com.github.leleuj.ss.oauth.client.web.OAuthAuthenticationFilter" + <bean id="facebookFilter" class="com.github.leleuj.ss.oauth.client.web.OAuthAuthenticationFilter" p:filterProcessesUrl="/j_spring_facebook_security_check" - p:provider-ref="facebookProvider" p:authenticationManager-ref="authenticationManager" + p:provider-ref="facebookProvider" + p:authenticationManager-ref="authenticationManager" p:authenticationFailureHandler-ref="failureHandler" p:authenticationSuccessHandler-ref="successHandler" /> - <bean id="facebookAuthProvider" - class="com.github.leleuj.ss.oauth.client.authentication.OAuthAuthenticationProvider" + <bean id="facebookAuthProvider" class="com.github.leleuj.ss.oauth.client.authentication.OAuthAuthenticationProvider" p:provider-ref="facebookProvider" /> <!-- ######################### TWITTER ######################### --> <bean id="twitterProvider" class="org.scribe.up.provider.impl.TwitterProvider" - p:key="${security.twitter.key}" p:secret="${security.twitter.secret}" - p:callbackUrl="${security.arsnova-url}/j_spring_twitter_security_check" /> + p:key="${security.twitter.key}" + p:secret="${security.twitter.secret}" + p:callbackUrl="${root-url}#{servletContext.contextPath}/j_spring_twitter_security_check" /> - <bean id="twitterFilter" - class="com.github.leleuj.ss.oauth.client.web.OAuthAuthenticationFilter" + <bean id="twitterFilter" class="com.github.leleuj.ss.oauth.client.web.OAuthAuthenticationFilter" p:filterProcessesUrl="/j_spring_twitter_security_check" - p:provider-ref="twitterProvider" p:authenticationManager-ref="authenticationManager" + p:provider-ref="twitterProvider" + p:authenticationManager-ref="authenticationManager" p:authenticationFailureHandler-ref="failureHandler" p:authenticationSuccessHandler-ref="successHandler" /> - <bean id="twitterAuthProvider" - class="com.github.leleuj.ss.oauth.client.authentication.OAuthAuthenticationProvider" + <bean id="twitterAuthProvider" class="com.github.leleuj.ss.oauth.client.authentication.OAuthAuthenticationProvider" p:provider-ref="twitterProvider" /> <!-- ######################### GOOGLE ######################### --> <bean id="googleProvider" class="org.scribe.up.provider.impl.Google2Provider" - p:key="${security.google.key}" p:secret="${security.google.secret}" - p:scope-ref="googleScope" p:callbackUrl="${security.arsnova-url}/j_spring_google_security_check" /> - - <bean id="googleScope" - class="org.scribe.up.provider.impl.Google2Provider.Google2Scope" - factory-method="valueOf"> + p:key="${security.google.key}" + p:secret="${security.google.secret}" + p:scope-ref="googleScope" + p:callbackUrl="${root-url}#{servletContext.contextPath}/j_spring_google_security_check" /> + + <bean id="googleScope" class="org.scribe.up.provider.impl.Google2Provider.Google2Scope" factory-method="valueOf"> <constructor-arg index="0" value="EMAIL" /> </bean> @@ -112,10 +117,12 @@ <bean id="casEntryPoint" class="org.springframework.security.cas.web.CasAuthenticationEntryPoint" - p:loginUrl="${security.cas-server-url}/login" p:serviceProperties-ref="casServiceProperties" /> - - <bean id="casServiceProperties" class="org.springframework.security.cas.ServiceProperties" - p:service="${security.arsnova-url}/j_spring_cas_security_check" + p:loginUrl="${security.cas-server-url}/login" + p:serviceProperties-ref="casServiceProperties" /> + + <bean id="casServiceProperties" + class="org.springframework.security.cas.ServiceProperties" + p:service="${root-url}#{servletContext.contextPath}/j_spring_cas_security_check" p:sendRenew="false" /> <bean id="casAuthenticationProvider" @@ -141,13 +148,14 @@ </bean> <bean id="casLogoutSuccessHandler" class="de.thm.arsnova.CASLogoutSuccessHandler" - p:casUrl="${security.cas-server-url}" p:defaultTarget="${security.arsnova-url}" /> - + p:casUrl="${security.cas-server-url}" + p:defaultTarget="${root-url}"/> + <bean id="successHandler" class="de.thm.arsnova.LoginAuthenticationSucessHandler" - p:targetUrl="#auth/checkLogin" /> + p:targetUrl="${root-url}"/> <bean id="failureHandler" class="de.thm.arsnova.LoginAuthenticationFailureHandler" - p:defaultFailureUrl="/index.html" /> + p:defaultFailureUrl="${root-url}" /> <!-- Session Registry --> <bean id="sessionRegistry" diff --git a/src/main/webapp/arsnova.properties.example b/src/main/webapp/arsnova.properties.example index e92e71bf6b7326eac6ad958b634304ee741472dc..0fa0a198ebe3ad3123f24c8a8a8b824f899462bd 100644 --- a/src/main/webapp/arsnova.properties.example +++ b/src/main/webapp/arsnova.properties.example @@ -1,35 +1,199 @@ -security.arsnova-url=http://localhost:8080 -security.cas-server-url=https://cas.thm.de/cas +################################################################################ +# General server settings +################################################################################ +# The URL under which the ARSnova server is reachable. Use +# http://localhost:8080 for development. +root-url=https://example.com -security.facebook.key=318531508227494 -security.facebook.secret=e3f38cfc72bb63e35641b637081a6177 - -security.twitter.key=PEVtidSG0HzSrxVRPpsCXw -security.twitter.secret=mC0HOvxiEgqwdDWCcDoy3q75nUQPu1bYRp1ncHWGd0 - -security.google.key=110959746118.apps.googleusercontent.com -security.google.secret=CkzUJZswY8rjWCCYnHVovyGA +# The context paths where the ARSnova modules have been deployed +customization.path=/customization +mobile.path=/mobile +presenter.path=/presenter +# SSL configuration security.ssl=false -security.keystore=/etc/arsnova.thm.de.jks +security.keystore=/etc/arsnova/arsnova.jks security.storepass=arsnova -# minutes, after which the feedback is deleted -feedback.cleanup=10 +# WebSockets server +socketio.ip=0.0.0.0 +socketio.port=10443 -# maximal filesize in bytes -upload.filesize_b=1048576 +################################################################################ +# Database +################################################################################ couchdb.host=localhost couchdb.port=5984 couchdb.name=arsnova couchdb.username=admin couchdb.password= -socketio.ip=0.0.0.0 -socketio.port=10443 +################################################################################ +# E-Mail +################################################################################ +mail.sender.name=ARSnova +mail.sender.address= +mail.host= + + +################################################################################ +# Authentication +################################################################################ +# After the specified number of login tries the client IP will be banned for +# several minutes +security.authentication.login-try-limit=50 + +# Configuration parameters for authentication services: +# enabled: enable or disable the service +# title: the title which is displayed by frontends +# login-dialog-path: URL of a login dialog page +# image: an image which is used for frontend buttons + +# Guest authentication +# +security.guest.enabled=true +security.guest.order=0 +security.guest.lecturer.enabled=true + +# Setup combined login if you want to use a single, customized login page +# which is used for multiple authentication services. +# +security.custom-login.enabled=false +security.custom-login.title=University +security.custom-login.login-dialog-path= +security.custom-login.image= +security.custom-login.order=0 + +# Internal authentication +# +# Specific parameters: +# activation-path: URL of the account activation page +# allowed-email-domains: Allows you to restrict registration to specific +# domains. You can use wildcards (*), e. g. *.*.example.com. Multiple +# entries are separated by commas. +# registration-mail.subject: Subject used for registration e-mail +# registration-mail.body: Text body used for registration e-mail. {0} will be +# replaced by the value of activation-path. +# +security.user-db.enabled=true +security.user-db.title=ARSnova +security.user-db.login-dialog-path=account.html +security.user-db.activation-path=account.html +security.user-db.reset-password-path=account.html +security.user-db.image= +security.user-db.order=0 +security.user-db.allowed-email-domains=* +security.user-db.registration-mail.subject=ARSnova Registration +security.user-db.registration-mail.body=Welcome to ARSnova!\n\nPlease confirm \ + your registration by visiting the following web address:\n{0}\n\n\ + Afterwards, you can log into ARSnova with your e-mail address and password. +security.user-db.reset-password-mail.subject=ARSnova Password Reset +security.user-db.reset-password-mail.body=You requested to reset your \ + password.\n\nPlease follow the link below to set a new password:\n{0} + +# LDAP authentication +# +# Specific parameters: +# url: LDAP server URL +# user-dn-pattern: Pattern used to check user credentials against the LDAP +# server. {0} will be replaced with the user ID by ARSnova. +# +security.ldap.enabled=true +security.ldap.title=LDAP +security.ldap.login-dialog-path=login-ldap.html +security.ldap.image= +security.ldap.order=0 +security.ldap.url=ldap://example.com:33389/dc=example,dc=com +security.ldap.user-dn-pattern=uid={0},ou=arsnova +# Not yet implemented parameters +#security.ldap.user-search-filter=(uid={0}) +#security.ldap.user-search-base="ou=people" + +# CAS authentication +# +security.cas.enabled=true +security.cas.title=CAS +security.cas.image= +security.cas.order=0 +security.cas-server-url=https://example.com/cas + +# OAuth authentication with third party services +# Specific parameters: +# key: OAuth key/id provided by a third party auth service +# secret: OAuth secret provided by a third party auth service + +# Facebook +# +security.facebook.enabled=true +security.facebook.order=0 +security.facebook.key= +security.facebook.secret= + +# Twitter +# +security.twitter.enabled=true +security.twitter.order=0 +security.twitter.key= +security.twitter.secret= + +# Google +# +security.google.enabled=true +security.google.order=0 +security.google.key= +security.google.secret= + + +################################################################################ +# ARSnova Connector (for LMS) +################################################################################ connector.enable=false connector.uri=http://localhost:8080/connector-service connector.username=test connector.password=test + + +################################################################################ +# Features +################################################################################ +# Enable MathJax to allow the use of Math formulas written in TeX syntax in +# text fields. +features.mathjax.enabled=true + +# The following features are considered experimental because they have not been +# tested in a production environment over a longer time frame and/or their +# behavior will change in future releases. +# +features.markdown.enabled=false +features.learning-progress.enabled=false +features.question-format.flashcard.enabled=false +features.question-format.grid-square.enabled=false + + +################################################################################ +# Customization +################################################################################ +# minutes, after which the feedback is deleted +feedback.cleanup=10 + +# maximal filesize in bytes +upload.filesize_b=1048576 + +# maximal number of answer options allowed for a skill question +question.answer-option-limit=8 + +# Enable Markdown and MathJax parsing in answer options. Formatting in answer +# options should be used cautiously since it could lead to display errors. +# Answer options will still not be parsed in diagrams. This setting has no +# effect if neither MathJax nor Markdown are enabled. +question.parse-answer-option-formatting=false + +# Links which are displayed in the frontend applications +# +links.documentation.url=https://arsnova.eu/manual/ +links.overlay.url=https://arsnova.eu/overlay/ +links.organization.url= +links.imprint.url= +links.privacy-policy.url= diff --git a/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java b/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java index c73b88f96c0560c885ed9865c881c3a530cd54eb..8dcafd979a2068b3220978605480cabd57c21586 100644 --- a/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java +++ b/src/test/java/de/thm/arsnova/dao/StubDatabaseDao.java @@ -26,6 +26,7 @@ import java.util.concurrent.ConcurrentHashMap; import de.thm.arsnova.connector.model.Course; import de.thm.arsnova.entities.Answer; +import de.thm.arsnova.entities.DbUser; import de.thm.arsnova.entities.Feedback; import de.thm.arsnova.entities.FoodVote; import de.thm.arsnova.entities.InterposedQuestion; @@ -500,6 +501,18 @@ public class StubDatabaseDao implements IDatabaseDao { } + @Override + public DbUser createOrUpdateUser(DbUser user) { + // TODO Auto-generated method stub + return null; + } + + @Override + public DbUser getUser(String username) { + // TODO Auto-generated method stub + return null; + } + @Override public int getLearningProgress(Session session) { // TODO Auto-generated method stub @@ -511,4 +524,10 @@ public class StubDatabaseDao implements IDatabaseDao { // TODO Auto-generated method stub return null; } + + @Override + public boolean deleteUser(DbUser dbUser) { + // TODO Auto-generated method stub + return false; + } } diff --git a/src/test/resources/arsnova.properties.example b/src/test/resources/arsnova.properties.example index f5830dcb6d2a79fb1f5a9e5564e36a895a9ea5da..0fa0a198ebe3ad3123f24c8a8a8b824f899462bd 100644 --- a/src/test/resources/arsnova.properties.example +++ b/src/test/resources/arsnova.properties.example @@ -1,33 +1,199 @@ -security.arsnova-url=http://localhost:8080 -security.cas-server-url=https://cas.thm.de/cas +################################################################################ +# General server settings +################################################################################ +# The URL under which the ARSnova server is reachable. Use +# http://localhost:8080 for development. +root-url=https://example.com -security.facebook.key=318531508227494 -security.facebook.secret=e3f38cfc72bb63e35641b637081a6177 - -security.twitter.key=PEVtidSG0HzSrxVRPpsCXw -security.twitter.secret=mC0HOvxiEgqwdDWCcDoy3q75nUQPu1bYRp1ncHWGd0 - -security.google.key=110959746118.apps.googleusercontent.com -security.google.secret=CkzUJZswY8rjWCCYnHVovyGA +# The context paths where the ARSnova modules have been deployed +customization.path=/customization +mobile.path=/mobile +presenter.path=/presenter +# SSL configuration security.ssl=false -security.keystore=/etc/arsnova.thm.de.jks +security.keystore=/etc/arsnova/arsnova.jks security.storepass=arsnova +# WebSockets server +socketio.ip=0.0.0.0 +socketio.port=10443 + + +################################################################################ +# Database +################################################################################ couchdb.host=localhost couchdb.port=5984 couchdb.name=arsnova +couchdb.username=admin +couchdb.password= -# minutes, after which the feedback is deleted -feedback.cleanup=10 -# maximal filesize in bytes -upload.filesize_b=1048576 +################################################################################ +# E-Mail +################################################################################ +mail.sender.name=ARSnova +mail.sender.address= +mail.host= -socketio.ip=0.0.0.0 -socketio.port=10443 +################################################################################ +# Authentication +################################################################################ +# After the specified number of login tries the client IP will be banned for +# several minutes +security.authentication.login-try-limit=50 + +# Configuration parameters for authentication services: +# enabled: enable or disable the service +# title: the title which is displayed by frontends +# login-dialog-path: URL of a login dialog page +# image: an image which is used for frontend buttons + +# Guest authentication +# +security.guest.enabled=true +security.guest.order=0 +security.guest.lecturer.enabled=true + +# Setup combined login if you want to use a single, customized login page +# which is used for multiple authentication services. +# +security.custom-login.enabled=false +security.custom-login.title=University +security.custom-login.login-dialog-path= +security.custom-login.image= +security.custom-login.order=0 + +# Internal authentication +# +# Specific parameters: +# activation-path: URL of the account activation page +# allowed-email-domains: Allows you to restrict registration to specific +# domains. You can use wildcards (*), e. g. *.*.example.com. Multiple +# entries are separated by commas. +# registration-mail.subject: Subject used for registration e-mail +# registration-mail.body: Text body used for registration e-mail. {0} will be +# replaced by the value of activation-path. +# +security.user-db.enabled=true +security.user-db.title=ARSnova +security.user-db.login-dialog-path=account.html +security.user-db.activation-path=account.html +security.user-db.reset-password-path=account.html +security.user-db.image= +security.user-db.order=0 +security.user-db.allowed-email-domains=* +security.user-db.registration-mail.subject=ARSnova Registration +security.user-db.registration-mail.body=Welcome to ARSnova!\n\nPlease confirm \ + your registration by visiting the following web address:\n{0}\n\n\ + Afterwards, you can log into ARSnova with your e-mail address and password. +security.user-db.reset-password-mail.subject=ARSnova Password Reset +security.user-db.reset-password-mail.body=You requested to reset your \ + password.\n\nPlease follow the link below to set a new password:\n{0} + +# LDAP authentication +# +# Specific parameters: +# url: LDAP server URL +# user-dn-pattern: Pattern used to check user credentials against the LDAP +# server. {0} will be replaced with the user ID by ARSnova. +# +security.ldap.enabled=true +security.ldap.title=LDAP +security.ldap.login-dialog-path=login-ldap.html +security.ldap.image= +security.ldap.order=0 +security.ldap.url=ldap://example.com:33389/dc=example,dc=com +security.ldap.user-dn-pattern=uid={0},ou=arsnova +# Not yet implemented parameters +#security.ldap.user-search-filter=(uid={0}) +#security.ldap.user-search-base="ou=people" + +# CAS authentication +# +security.cas.enabled=true +security.cas.title=CAS +security.cas.image= +security.cas.order=0 +security.cas-server-url=https://example.com/cas + +# OAuth authentication with third party services +# Specific parameters: +# key: OAuth key/id provided by a third party auth service +# secret: OAuth secret provided by a third party auth service + +# Facebook +# +security.facebook.enabled=true +security.facebook.order=0 +security.facebook.key= +security.facebook.secret= + +# Twitter +# +security.twitter.enabled=true +security.twitter.order=0 +security.twitter.key= +security.twitter.secret= + +# Google +# +security.google.enabled=true +security.google.order=0 +security.google.key= +security.google.secret= + + +################################################################################ +# ARSnova Connector (for LMS) +################################################################################ connector.enable=false connector.uri=http://localhost:8080/connector-service connector.username=test connector.password=test + + +################################################################################ +# Features +################################################################################ +# Enable MathJax to allow the use of Math formulas written in TeX syntax in +# text fields. +features.mathjax.enabled=true + +# The following features are considered experimental because they have not been +# tested in a production environment over a longer time frame and/or their +# behavior will change in future releases. +# +features.markdown.enabled=false +features.learning-progress.enabled=false +features.question-format.flashcard.enabled=false +features.question-format.grid-square.enabled=false + + +################################################################################ +# Customization +################################################################################ +# minutes, after which the feedback is deleted +feedback.cleanup=10 + +# maximal filesize in bytes +upload.filesize_b=1048576 + +# maximal number of answer options allowed for a skill question +question.answer-option-limit=8 + +# Enable Markdown and MathJax parsing in answer options. Formatting in answer +# options should be used cautiously since it could lead to display errors. +# Answer options will still not be parsed in diagrams. This setting has no +# effect if neither MathJax nor Markdown are enabled. +question.parse-answer-option-formatting=false + +# Links which are displayed in the frontend applications +# +links.documentation.url=https://arsnova.eu/manual/ +links.overlay.url=https://arsnova.eu/overlay/ +links.organization.url= +links.imprint.url= +links.privacy-policy.url= diff --git a/src/test/resources/test-config.xml b/src/test/resources/test-config.xml index 91833913381472c84ee33c392050884ac7765427..1da5cc9d310d3894a29db0740ee8842e93a2d000 100644 --- a/src/test/resources/test-config.xml +++ b/src/test/resources/test-config.xml @@ -8,6 +8,10 @@ http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd"> + <bean id="servletContext" class="org.springframework.mock.web.MockServletContext"> + <property name="contextPath" value="/" /> + </bean> + <bean id="sessionService" class="de.thm.arsnova.services.SessionService"> <property name="databaseDao" ref="databaseDao" /> </bean> diff --git a/src/test/resources/test.ldif b/src/test/resources/test.ldif new file mode 100644 index 0000000000000000000000000000000000000000..f0a60c365b38d3589649a507818bac4b8701500d --- /dev/null +++ b/src/test/resources/test.ldif @@ -0,0 +1,22 @@ +dn: dc=example, dc=com +objectclass: organization +objectclass: top +o: Dummy Organisation + +dn: uid=ptsr00, dc=example, dc=com +objectclass: person +objectclass: organizationalperson +objectclass: inetorgperson +uid: ptsr00 +sn: Tester +givenName: Patrick +userPassword:: VGVzdA== + +dn: uid=ptsr01, dc=example, dc=com +objectclass: person +objectclass: organizationalperson +objectclass: inetorgperson +uid: ptsr01 +sn: Tester +givenName: Patricia +userPassword:: VGVzdA==