diff --git a/docs-android/app/src/main/java/com/sismics/docs/util/SpannableUtil.java b/docs-android/app/src/main/java/com/sismics/docs/util/SpannableUtil.java index c0daf560..5fdf712f 100644 --- a/docs-android/app/src/main/java/com/sismics/docs/util/SpannableUtil.java +++ b/docs-android/app/src/main/java/com/sismics/docs/util/SpannableUtil.java @@ -11,7 +11,7 @@ import org.json.JSONArray; import org.json.JSONObject; /** - * Utility class for tags. + * Utility class for spannable. * * @author bgamard. */ diff --git a/docs-web-common/src/main/java/com/sismics/util/filter/HeaderBasedSecurityFilter.java b/docs-web-common/src/main/java/com/sismics/util/filter/HeaderBasedSecurityFilter.java new file mode 100644 index 00000000..4fa6ede6 --- /dev/null +++ b/docs-web-common/src/main/java/com/sismics/util/filter/HeaderBasedSecurityFilter.java @@ -0,0 +1,47 @@ +package com.sismics.util.filter; + +import com.google.common.base.Strings; +import com.sismics.docs.core.dao.jpa.UserDao; +import com.sismics.docs.core.model.jpa.User; + +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; + +/** + * A header-based security filter that authenticates an user using the "X-Authenticated-User" request header as the user ID. + * This filter is intended to be used in conjunction with an external authenticating proxy. + * + * @author pacien + */ +public class HeaderBasedSecurityFilter extends SecurityFilter { + /** + * Authentication header. + */ + public static final String AUTHENTICATED_USER_HEADER = "X-Authenticated-User"; + + /** + * True if this authentication method is enabled. + */ + private boolean enabled; + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + enabled = Boolean.parseBoolean(filterConfig.getInitParameter("enabled")) + || Boolean.parseBoolean(System.getProperty("docs.header_authentication")); + } + + @Override + protected User authenticate(HttpServletRequest request) { + if (!enabled) { + return null; + } + + String username = request.getHeader(AUTHENTICATED_USER_HEADER); + if (Strings.isNullOrEmpty(username)) { + return null; + } + return new UserDao().getActiveByUsername(username); + } + +} \ No newline at end of file diff --git a/docs-web-common/src/main/java/com/sismics/util/filter/SecurityFilter.java b/docs-web-common/src/main/java/com/sismics/util/filter/SecurityFilter.java new file mode 100644 index 00000000..385210b1 --- /dev/null +++ b/docs-web-common/src/main/java/com/sismics/util/filter/SecurityFilter.java @@ -0,0 +1,145 @@ +package com.sismics.util.filter; + +import com.sismics.docs.core.constant.Constants; +import com.sismics.docs.core.dao.jpa.GroupDao; +import com.sismics.docs.core.dao.jpa.RoleBaseFunctionDao; +import com.sismics.docs.core.dao.jpa.criteria.GroupCriteria; +import com.sismics.docs.core.dao.jpa.dto.GroupDto; +import com.sismics.docs.core.model.jpa.User; +import com.sismics.security.AnonymousPrincipal; +import com.sismics.security.UserPrincipal; +import jersey.repackaged.com.google.common.collect.Sets; +import org.joda.time.DateTimeZone; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * An abstract security filter for user authentication, that injects corresponding users into the request. + * Successfully authenticated users are injected as UserPrincipal, or as AnonymousPrincipal otherwise. + * If an user has already been authenticated for the request, no further authentication attempt is made. + * + * @author pacien + * @author jtremeaux + */ +public abstract class SecurityFilter implements Filter { + /** + * Name of the attribute containing the principal. + */ + public static final String PRINCIPAL_ATTRIBUTE = "principal"; + + /** + * Logger. + */ + static final Logger LOG = LoggerFactory.getLogger(SecurityFilter.class); + + /** + * Returns true if the supplied request has an UserPrincipal. + * + * @param request HTTP request + * @return True if the supplied request has an UserPrincipal + */ + private boolean hasIdentifiedUser(HttpServletRequest request) { + return request.getAttribute(PRINCIPAL_ATTRIBUTE) instanceof UserPrincipal; + } + + /** + * Injects the given user into the request, with the appropriate authentication state. + * + * @param request HTTP request + * @param user nullable User to inject + */ + private void injectUser(HttpServletRequest request, User user) { + // Check if the user is still valid + if (user != null && user.getDeleteDate() == null) { + injectAuthenticatedUser(request, user); + } else { + injectAnonymousUser(request); + } + } + + /** + * Inject an authenticated user into the request attributes. + * + * @param request HTTP request + * @param user User to inject + */ + private void injectAuthenticatedUser(HttpServletRequest request, User user) { + UserPrincipal userPrincipal = new UserPrincipal(user.getId(), user.getUsername()); + + // Add groups + GroupDao groupDao = new GroupDao(); + Set groupRoleIdSet = new HashSet<>(); + List groupDtoList = groupDao.findByCriteria(new GroupCriteria() + .setUserId(user.getId()) + .setRecursive(true), null); + Set groupIdSet = Sets.newHashSet(); + for (GroupDto groupDto : groupDtoList) { + groupIdSet.add(groupDto.getId()); + if (groupDto.getRoleId() != null) { + groupRoleIdSet.add(groupDto.getRoleId()); + } + } + userPrincipal.setGroupIdSet(groupIdSet); + + // Add base functions + groupRoleIdSet.add(user.getRoleId()); + RoleBaseFunctionDao userBaseFuction = new RoleBaseFunctionDao(); + Set baseFunctionSet = userBaseFuction.findByRoleId(groupRoleIdSet); + userPrincipal.setBaseFunctionSet(baseFunctionSet); + + // Add email + userPrincipal.setEmail(user.getEmail()); + + request.setAttribute(PRINCIPAL_ATTRIBUTE, userPrincipal); + } + + /** + * Inject an anonymous user into the request attributes. + * + * @param request HTTP request + */ + private void injectAnonymousUser(HttpServletRequest request) { + AnonymousPrincipal anonymousPrincipal = new AnonymousPrincipal(); + anonymousPrincipal.setDateTimeZone(DateTimeZone.forID(Constants.DEFAULT_TIMEZONE_ID)); + + request.setAttribute(PRINCIPAL_ATTRIBUTE, anonymousPrincipal); + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + // NOP + } + + @Override + public void destroy() { + // NOP + } + + @Override + public void doFilter(ServletRequest req, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) req; + + if (!hasIdentifiedUser(request)) { + User user = authenticate(request); + injectUser(request, user); + } + + filterChain.doFilter(request, response); + } + + /** + * Authenticates an user from the given request parameters. + * + * @param request HTTP request + * @return nullable User + */ + protected abstract User authenticate(HttpServletRequest request); + +} \ No newline at end of file diff --git a/docs-web-common/src/main/java/com/sismics/util/filter/TokenBasedSecurityFilter.java b/docs-web-common/src/main/java/com/sismics/util/filter/TokenBasedSecurityFilter.java index f1ba671d..b65095e1 100644 --- a/docs-web-common/src/main/java/com/sismics/util/filter/TokenBasedSecurityFilter.java +++ b/docs-web-common/src/main/java/com/sismics/util/filter/TokenBasedSecurityFilter.java @@ -1,38 +1,14 @@ package com.sismics.util.filter; -import java.io.IOException; -import java.text.MessageFormat; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; - -import org.joda.time.DateTimeZone; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.sismics.docs.core.constant.Constants; import com.sismics.docs.core.dao.jpa.AuthenticationTokenDao; -import com.sismics.docs.core.dao.jpa.GroupDao; -import com.sismics.docs.core.dao.jpa.RoleBaseFunctionDao; import com.sismics.docs.core.dao.jpa.UserDao; -import com.sismics.docs.core.dao.jpa.criteria.GroupCriteria; -import com.sismics.docs.core.dao.jpa.dto.GroupDto; import com.sismics.docs.core.model.jpa.AuthenticationToken; import com.sismics.docs.core.model.jpa.User; -import com.sismics.security.AnonymousPrincipal; -import com.sismics.security.UserPrincipal; -import jersey.repackaged.com.google.common.collect.Sets; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import java.text.MessageFormat; +import java.util.Date; /** * This filter is used to authenticate the user having an active session via an authentication token stored in database. @@ -42,22 +18,12 @@ import jersey.repackaged.com.google.common.collect.Sets; * * @author jtremeaux */ -public class TokenBasedSecurityFilter implements Filter { - /** - * Logger. - */ - private static final Logger log = LoggerFactory.getLogger(TokenBasedSecurityFilter.class); - +public class TokenBasedSecurityFilter extends SecurityFilter { /** * Name of the cookie used to store the authentication token. */ public static final String COOKIE_NAME = "auth_token"; - /** - * Name of the attribute containing the principal. - */ - public static final String PRINCIPAL_ATTRIBUTE = "principal"; - /** * Lifetime of the authentication token in seconds, since login. */ @@ -66,68 +32,40 @@ public class TokenBasedSecurityFilter implements Filter { /** * Lifetime of the authentication token in seconds, since last connection. */ - public static final int TOKEN_SESSION_LIFETIME = 3600 * 24; - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - // NOP - } + private static final int TOKEN_SESSION_LIFETIME = 3600 * 24; - @Override - public void destroy() { - // NOP - } - - @Override - public void doFilter(ServletRequest req, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { - // Get the value of the client authentication token - HttpServletRequest request = (HttpServletRequest) req; - String authToken = null; - if (request.getCookies() != null) { - for (Cookie cookie : request.getCookies()) { - if (COOKIE_NAME.equals(cookie.getName())) { - authToken = cookie.getValue(); + /** + * Extracts and returns an authentication token from a cookie list. + * + * @param cookies Cookie list + * @return nullable auth token + */ + private String extractAuthToken(Cookie[] cookies) { + if (cookies != null) { + for (Cookie cookie : cookies) { + if (COOKIE_NAME.equals(cookie.getName()) && !cookie.getValue().isEmpty()) { + return cookie.getValue(); } } } - - // Get the corresponding server token - AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao(); - AuthenticationToken authenticationToken = null; - if (authToken != null) { - authenticationToken = authenticationTokenDao.get(authToken); - } - - if (authenticationToken == null) { - injectAnonymousUser(request); - } else { - // Check if the token is still valid - if (isTokenExpired(authenticationToken)) { - try { - injectAnonymousUser(request); - // Destroy the expired token - authenticationTokenDao.delete(authToken); - } catch (Exception e) { - if (log.isErrorEnabled()) { - log.error(MessageFormat.format("Error deleting authentication token {0} ", authToken), e); - } - } - } else { - // Check if the user is still valid - UserDao userDao = new UserDao(); - User user = userDao.getById(authenticationToken.getUserId()); - if (user != null && user.getDeleteDate() == null) { - injectAuthenticatedUser(request, user); - } else { - injectAnonymousUser(request); - } - } - } - - filterChain.doFilter(request, response); + return null; } - + + /** + * Deletes an expired authentication token. + * + * @param authTokenID auth token ID + */ + private void handleExpiredToken(AuthenticationTokenDao dao, String authTokenID) { + try { + dao.delete(authTokenID); + } catch (Exception e) { + if (LOG.isErrorEnabled()) + LOG.error(MessageFormat.format("Error deleting authentication token {0} ", authTokenID), e); + } + } + /** * Returns true if the token is expired. * @@ -146,51 +84,27 @@ public class TokenBasedSecurityFilter implements Filter { } } - /** - * Inject an authenticated user into the request attributes. - * - * @param request HTTP request - * @param user User to inject - */ - private void injectAuthenticatedUser(HttpServletRequest request, User user) { - UserPrincipal userPrincipal = new UserPrincipal(user.getId(), user.getUsername()); - - // Add groups - GroupDao groupDao = new GroupDao(); - Set groupRoleIdSet = new HashSet<>(); - List groupDtoList = groupDao.findByCriteria(new GroupCriteria() - .setUserId(user.getId()) - .setRecursive(true), null); - Set groupIdSet = Sets.newHashSet(); - for (GroupDto groupDto : groupDtoList) { - groupIdSet.add(groupDto.getId()); - if (groupDto.getRoleId() != null) { - groupRoleIdSet.add(groupDto.getRoleId()); - } + @Override + protected User authenticate(HttpServletRequest request) { + // Get the value of the client authentication token + String authTokenId = extractAuthToken(request.getCookies()); + if (authTokenId == null) { + return null; } - userPrincipal.setGroupIdSet(groupIdSet); - - // Add base functions - groupRoleIdSet.add(user.getRoleId()); - RoleBaseFunctionDao userBaseFuction = new RoleBaseFunctionDao(); - Set baseFunctionSet = userBaseFuction.findByRoleId(groupRoleIdSet); - userPrincipal.setBaseFunctionSet(baseFunctionSet); - - // Add email - userPrincipal.setEmail(user.getEmail()); - - request.setAttribute(PRINCIPAL_ATTRIBUTE, userPrincipal); - } - /** - * Inject an anonymous user into the request attributes. - * - * @param request HTTP request - */ - private void injectAnonymousUser(HttpServletRequest request) { - AnonymousPrincipal anonymousPrincipal = new AnonymousPrincipal(); - anonymousPrincipal.setDateTimeZone(DateTimeZone.forID(Constants.DEFAULT_TIMEZONE_ID)); + // Get the corresponding server token + AuthenticationTokenDao authTokenDao = new AuthenticationTokenDao(); + AuthenticationToken authToken = authTokenDao.get(authTokenId); + if (authToken == null) { + return null; + } - request.setAttribute(PRINCIPAL_ATTRIBUTE, anonymousPrincipal); + if (isTokenExpired(authToken)) { + handleExpiredToken(authTokenDao, authTokenId); + return null; + } + + authTokenDao.updateLastConnectionDate(authToken.getId()); + return new UserDao().getById(authToken.getUserId()); } } diff --git a/docs-web-common/src/test/java/com/sismics/docs/rest/BaseJerseyTest.java b/docs-web-common/src/test/java/com/sismics/docs/rest/BaseJerseyTest.java index e4244d6f..a344443d 100644 --- a/docs-web-common/src/test/java/com/sismics/docs/rest/BaseJerseyTest.java +++ b/docs-web-common/src/test/java/com/sismics/docs/rest/BaseJerseyTest.java @@ -5,6 +5,7 @@ import java.net.URI; import javax.ws.rs.core.Application; import javax.ws.rs.core.UriBuilder; +import com.sismics.util.filter.HeaderBasedSecurityFilter; import org.glassfish.grizzly.http.server.HttpServer; import org.glassfish.grizzly.servlet.ServletRegistration; import org.glassfish.grizzly.servlet.WebappContext; @@ -62,7 +63,8 @@ public abstract class BaseJerseyTest extends JerseyTest { @Before public void setUp() throws Exception { super.setUp(); - + System.setProperty("docs.header_authentication", "true"); + clientUtil = new ClientUtil(target()); httpServer = HttpServer.createSimpleServer(getClass().getResource("/").getFile(), "localhost", getPort()); @@ -71,6 +73,8 @@ public abstract class BaseJerseyTest extends JerseyTest { .addMappingForUrlPatterns(null, "/*"); context.addFilter("tokenBasedSecurityFilter", TokenBasedSecurityFilter.class) .addMappingForUrlPatterns(null, "/*"); + context.addFilter("headerBasedSecurityFilter", HeaderBasedSecurityFilter.class) + .addMappingForUrlPatterns(null, "/*"); ServletRegistration reg = context.addServlet("jerseyServlet", ServletContainer.class); reg.setInitParameter("jersey.config.server.provider.packages", "com.sismics.docs.rest.resource"); reg.setInitParameter("jersey.config.server.provider.classnames", "org.glassfish.jersey.media.multipart.MultiPartFeature"); diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/BaseResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/BaseResource.java index f05d6d56..1fd7c14d 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/BaseResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/BaseResource.java @@ -1,19 +1,18 @@ package com.sismics.docs.rest.resource; -import java.security.Principal; -import java.util.List; -import java.util.Set; - -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Context; - import com.google.common.collect.Lists; import com.sismics.docs.rest.constant.BaseFunction; import com.sismics.rest.exception.ForbiddenClientException; import com.sismics.security.IPrincipal; import com.sismics.security.UserPrincipal; -import com.sismics.util.filter.TokenBasedSecurityFilter; +import com.sismics.util.filter.SecurityFilter; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import java.security.Principal; +import java.util.List; +import java.util.Set; /** * Base class of REST resources. @@ -67,7 +66,7 @@ public abstract class BaseResource { * @return True if the user is authenticated and not anonymous */ protected boolean authenticate() { - Principal principal = (Principal) request.getAttribute(TokenBasedSecurityFilter.PRINCIPAL_ATTRIBUTE); + Principal principal = (Principal) request.getAttribute(SecurityFilter.PRINCIPAL_ATTRIBUTE); if (principal != null && principal instanceof IPrincipal) { this.principal = (IPrincipal) principal; return !this.principal.isAnonymous(); diff --git a/docs-web/src/main/webapp/WEB-INF/web.xml b/docs-web/src/main/webapp/WEB-INF/web.xml index 3b14ff94..3f82eb30 100644 --- a/docs-web/src/main/webapp/WEB-INF/web.xml +++ b/docs-web/src/main/webapp/WEB-INF/web.xml @@ -26,18 +26,33 @@ *.jsp - + tokenBasedSecurityFilter com.sismics.util.filter.TokenBasedSecurityFilter true + + + headerBasedSecurityFilter + com.sismics.util.filter.HeaderBasedSecurityFilter + true + + enabled + false + + tokenBasedSecurityFilter /api/* + + headerBasedSecurityFilter + /api/* + + JerseyServlet diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestSecurity.java b/docs-web/src/test/java/com/sismics/docs/rest/TestSecurity.java index ccf1f6c9..602415a3 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestSecurity.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestSecurity.java @@ -6,6 +6,7 @@ import javax.ws.rs.core.Form; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; +import com.sismics.util.filter.HeaderBasedSecurityFilter; import org.junit.Assert; import org.apache.commons.lang.StringUtils; @@ -28,7 +29,7 @@ public class TestSecurity extends BaseJerseyTest { clientUtil.createUser("testsecurity"); // Changes a user's email KO : the user is not connected - Response response = target().path("/user/update").request() + Response response = target().path("/user").request() .post(Entity.form(new Form().param("email", "testsecurity2@docs.com"))); Assert.assertEquals(Status.FORBIDDEN, Status.fromStatusCode(response.getStatus())); JsonObject json = response.readEntity(JsonObject.class); @@ -73,4 +74,29 @@ public class TestSecurity extends BaseJerseyTest { // User testsecurity logs out clientUtil.logout(testSecurityToken); } + + @Test + public void testHeaderBasedAuthentication() { + clientUtil.createUser("header_auth_test"); + + Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), target() + .path("/user/session") + .request() + .get() + .getStatus()); + + Assert.assertEquals(Status.OK.getStatusCode(), target() + .path("/user/session") + .request() + .header(HeaderBasedSecurityFilter.AUTHENTICATED_USER_HEADER, "header_auth_test") + .get() + .getStatus()); + + Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), target() + .path("/user/session") + .request() + .header(HeaderBasedSecurityFilter.AUTHENTICATED_USER_HEADER, "idontexist") + .get() + .getStatus()); + } } \ No newline at end of file