diff --git a/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java b/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java index f2a7c866..13df9b5c 100644 --- a/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java +++ b/docs-core/src/main/java/com/sismics/docs/core/constant/Constants.java @@ -29,7 +29,12 @@ public class Constants { * File Lucene directory storage. */ public static final String LUCENE_DIRECTORY_STORAGE_FILE = "FILE"; - + + /** + * Guest user ID. + */ + public static final String GUEST_USER_ID = "guest"; + /** * Default generic user role. */ diff --git a/docs-core/src/main/resources/db/update/dbupdate-010-0.sql b/docs-core/src/main/resources/db/update/dbupdate-010-0.sql index ca554b96..3bc9838b 100644 --- a/docs-core/src/main/resources/db/update/dbupdate-010-0.sql +++ b/docs-core/src/main/resources/db/update/dbupdate-010-0.sql @@ -1,2 +1,3 @@ insert into T_CONFIG(CFG_ID_C, CFG_VALUE_C) values('GUEST_LOGIN', 'false'); +insert into T_USER(USE_ID_C, USE_IDROLE_C, USE_USERNAME_C, USE_PASSWORD_C, USE_EMAIL_C, USE_CREATEDATE_D, USE_PRIVATEKEY_C) values('guest', 'user', 'guest', '', 'guest@localhost', NOW(), 'GuestPk'); update T_CONFIG set CFG_VALUE_C = '10' where CFG_ID_C = 'DB_VERSION'; diff --git a/docs-web-common/src/main/java/com/sismics/security/AnonymousPrincipal.java b/docs-web-common/src/main/java/com/sismics/security/AnonymousPrincipal.java index 5becb3ab..5e137f30 100644 --- a/docs-web-common/src/main/java/com/sismics/security/AnonymousPrincipal.java +++ b/docs-web-common/src/main/java/com/sismics/security/AnonymousPrincipal.java @@ -59,4 +59,9 @@ public class AnonymousPrincipal implements IPrincipal { public Set getGroupIdSet() { return Sets.newHashSet(); } + + @Override + public boolean isGuest() { + return false; + } } diff --git a/docs-web-common/src/main/java/com/sismics/security/IPrincipal.java b/docs-web-common/src/main/java/com/sismics/security/IPrincipal.java index 3129f363..0e9203df 100644 --- a/docs-web-common/src/main/java/com/sismics/security/IPrincipal.java +++ b/docs-web-common/src/main/java/com/sismics/security/IPrincipal.java @@ -18,6 +18,13 @@ public interface IPrincipal extends Principal { */ boolean isAnonymous(); + /** + * Checks if the principal is a guest. + * + * @return True if the principal is a guest + */ + boolean isGuest(); + /** * Returns the ID of the connected user, or null if the user is anonymous * diff --git a/docs-web-common/src/main/java/com/sismics/security/UserPrincipal.java b/docs-web-common/src/main/java/com/sismics/security/UserPrincipal.java index bfef8fe3..ff009040 100644 --- a/docs-web-common/src/main/java/com/sismics/security/UserPrincipal.java +++ b/docs-web-common/src/main/java/com/sismics/security/UserPrincipal.java @@ -2,6 +2,7 @@ package com.sismics.security; import java.util.Set; +import com.sismics.docs.core.constant.Constants; import org.joda.time.DateTimeZone; /** @@ -108,4 +109,9 @@ public class UserPrincipal implements IPrincipal { public void setGroupIdSet(Set groupIdSet) { this.groupIdSet = groupIdSet; } + + @Override + public boolean isGuest() { + return Constants.GUEST_USER_ID.equals(id); + } } diff --git a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java index d32042dd..7fa0f333 100644 --- a/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java +++ b/docs-web/src/main/java/com/sismics/docs/rest/resource/UserResource.java @@ -22,6 +22,8 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; +import com.sismics.docs.core.constant.ConfigType; +import com.sismics.docs.core.util.ConfigUtil; import org.apache.commons.lang.StringUtils; import com.google.common.base.Strings; @@ -150,7 +152,7 @@ public class UserResource extends BaseResource { * @apiParam {String{8..50}} password Password * @apiParam {String{1..100}} email E-mail * @apiSuccess {String} status Status OK - * @apiError (client) ForbiddenError Access denied + * @apiError (client) ForbiddenError Access denied or connected as guest * @apiError (client) ValidationError Validation error * @apiPermission user * @apiVersion 1.5.0 @@ -163,7 +165,7 @@ public class UserResource extends BaseResource { public Response update( @FormParam("password") String password, @FormParam("email") String email) { - if (!authenticate()) { + if (!authenticate() || principal.isGuest()) { throw new ForbiddenClientException(); } @@ -301,7 +303,7 @@ public class UserResource extends BaseResource { * @apiName PostUserLogin * @apiGroup User * @apiParam {String} username Username - * @apiParam {String} password Password + * @apiParam {String} password Password (optional for guest login) * @apiParam {String} code TOTP validation code * @apiParam {Boolean} remember If true, create a long lasted token * @apiSuccess {String} auth_token A cookie named auth_token containing the token ID @@ -328,7 +330,16 @@ public class UserResource extends BaseResource { // Get the user UserDao userDao = new UserDao(); - User user = userDao.authenticate(username, password); + User user = null; + if (username.equals(Constants.GUEST_USER_ID)) { + if (ConfigUtil.getConfigBooleanValue(ConfigType.GUEST_LOGIN)) { + // Login as guest + user = userDao.getActiveByUsername(Constants.GUEST_USER_ID); + } + } else { + // Login as a normal user + user = userDao.authenticate(username, password); + } if (user == null) { throw new ForbiddenClientException(); } @@ -429,7 +440,7 @@ public class UserResource extends BaseResource { * @apiName DeleteUser * @apiGroup User * @apiSuccess {String} status Status OK - * @apiError (client) ForbiddenError Access denied or the admin user cannot be deleted + * @apiError (client) ForbiddenError Access denied or the user cannot be deleted * @apiPermission user * @apiVersion 1.5.0 * @@ -442,8 +453,8 @@ public class UserResource extends BaseResource { } // Ensure that the admin user is not deleted - if (hasBaseFunction(BaseFunction.ADMIN)) { - throw new ClientException("ForbiddenError", "The admin user cannot be deleted"); + if (hasBaseFunction(BaseFunction.ADMIN) || principal.isGuest()) { + throw new ClientException("ForbiddenError", "This user cannot be deleted"); } // Find linked data @@ -486,7 +497,7 @@ public class UserResource extends BaseResource { * @apiName DeleteUserUsername * @apiGroup User * @apiSuccess {String} status Status OK - * @apiError (client) ForbiddenError Access denied or the admin user cannot be deleted + * @apiError (client) ForbiddenError Access denied or the user cannot be deleted * @apiError (client) UserNotFound The user does not exist * @apiPermission admin * @apiVersion 1.5.0 @@ -501,7 +512,12 @@ public class UserResource extends BaseResource { throw new ForbiddenClientException(); } checkBaseFunction(BaseFunction.ADMIN); - + + // Cannot delete the guest user + if (Constants.GUEST_USER_ID.equals(username)) { + throw new ClientException("ForbiddenError", "The guest user cannot be deleted"); + } + // Check if the user exists UserDao userDao = new UserDao(); User user = userDao.getActiveByUsername(username); @@ -768,18 +784,21 @@ public class UserResource extends BaseResource { String authToken = getAuthToken(); JsonArrayBuilder sessions = Json.createArrayBuilder(); - AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao(); - for (AuthenticationToken authenticationToken : authenticationTokenDao.getByUserId(principal.getId())) { - JsonObjectBuilder session = Json.createObjectBuilder() - .add("create_date", authenticationToken.getCreationDate().getTime()) - .add("ip", JsonUtil.nullable(authenticationToken.getIp())) - .add("user_agent", JsonUtil.nullable(authenticationToken.getUserAgent())); - if (authenticationToken.getLastConnectionDate() != null) { - session.add("last_connection_date", authenticationToken.getLastConnectionDate().getTime()); + // The guest user cannot see other sessions + if (!principal.isGuest()) { + AuthenticationTokenDao authenticationTokenDao = new AuthenticationTokenDao(); + for (AuthenticationToken authenticationToken : authenticationTokenDao.getByUserId(principal.getId())) { + JsonObjectBuilder session = Json.createObjectBuilder() + .add("create_date", authenticationToken.getCreationDate().getTime()) + .add("ip", JsonUtil.nullable(authenticationToken.getIp())) + .add("user_agent", JsonUtil.nullable(authenticationToken.getUserAgent())); + if (authenticationToken.getLastConnectionDate() != null) { + session.add("last_connection_date", authenticationToken.getLastConnectionDate().getTime()); + } + session.add("current", authenticationToken.getId().equals(authToken)); + sessions.add(session); } - session.add("current", authenticationToken.getId().equals(authToken)); - sessions.add(session); } JsonObjectBuilder response = Json.createObjectBuilder() @@ -795,7 +814,7 @@ public class UserResource extends BaseResource { * @apiName DeleteUserSession * @apiGroup User * @apiSuccess {String} status Status OK - * @apiError (client) ForbiddenError Access denied + * @apiError (client) ForbiddenError Access denied or connected as guest * @apiPermission user * @apiVersion 1.5.0 * @@ -804,10 +823,10 @@ public class UserResource extends BaseResource { @DELETE @Path("session") public Response deleteSession() { - if (!authenticate()) { + if (!authenticate() || principal.isGuest()) { throw new ForbiddenClientException(); } - + // Get the value of the session token String authToken = getAuthToken(); @@ -830,7 +849,7 @@ public class UserResource extends BaseResource { * @apiName PostUserEnableTotp * @apiGroup User * @apiSuccess {String} secret Secret TOTP seed to initiate the algorithm - * @apiError (client) ForbiddenError Access denied + * @apiError (client) ForbiddenError Access denied or connected as guest * @apiPermission user * @apiVersion 1.5.0 * @@ -839,7 +858,7 @@ public class UserResource extends BaseResource { @POST @Path("enable_totp") public Response enableTotp() { - if (!authenticate()) { + if (!authenticate() || principal.isGuest()) { throw new ForbiddenClientException(); } @@ -866,7 +885,7 @@ public class UserResource extends BaseResource { * @apiGroup User * @apiParam {String{1..100}} password Password * @apiSuccess {String} status Status OK - * @apiError (client) ForbiddenError Access denied + * @apiError (client) ForbiddenError Access denied or connected as guest * @apiError (client) ValidationError Validation error * @apiPermission user * @apiVersion 1.5.0 @@ -877,7 +896,7 @@ public class UserResource extends BaseResource { @POST @Path("disable_totp") public Response disableTotp(@FormParam("password") String password) { - if (!authenticate()) { + if (!authenticate() || principal.isGuest()) { throw new ForbiddenClientException(); } diff --git a/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java b/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java index 4389bc9a..41185645 100644 --- a/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java +++ b/docs-web/src/test/java/com/sismics/docs/rest/TestAppResource.java @@ -1,5 +1,13 @@ package com.sismics.docs.rest; +import com.sismics.docs.core.constant.PermType; +import com.sismics.docs.core.dao.jpa.AclDao; +import com.sismics.util.context.ThreadLocalContext; +import com.sismics.util.filter.TokenBasedSecurityFilter; +import com.sismics.util.jpa.EMF; +import org.junit.Assert; +import org.junit.Test; + import javax.json.JsonArray; import javax.json.JsonObject; import javax.persistence.EntityManager; @@ -9,15 +17,6 @@ import javax.ws.rs.core.Form; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; -import com.sismics.docs.core.constant.PermType; -import com.sismics.docs.core.dao.jpa.AclDao; -import com.sismics.util.context.ThreadLocalContext; -import com.sismics.util.jpa.EMF; -import org.junit.Assert; -import org.junit.Test; - -import com.sismics.util.filter.TokenBasedSecurityFilter; - /** * Test the app resource. @@ -134,9 +133,10 @@ public class TestAppResource extends BaseJerseyTest { // Login admin String adminToken = clientUtil.login("admin", "admin", false); - // Try to login without credentials + // Try to login as guest Response response = target().path("/user/login").request() - .post(Entity.form(new Form())); + .post(Entity.form(new Form() + .param("username", "guest"))); Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus()); // Enable guest login @@ -144,5 +144,49 @@ public class TestAppResource extends BaseJerseyTest { .cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken) .post(Entity.form(new Form() .param("enabled", "true")), JsonObject.class); + + // Login as guest + String guestToken = clientUtil.login("guest", "", false); + + // Guest cannot delete himself + response = target().path("/user").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken) + .delete(); + Assert.assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + + // Guest cannot see opened sessions + JsonObject json = target().path("/user/session").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken) + .get(JsonObject.class); + Assert.assertEquals(0, json.getJsonArray("sessions").size()); + + // Guest cannot delete opened sessions + response = target().path("/user/session").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken) + .delete(); + Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus()); + + // Guest cannot enable TOTP + response = target().path("/user/enable_totp").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken) + .post(Entity.form(new Form())); + Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus()); + + // Guest cannot disable TOTP + response = target().path("/user/disable_totp").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken) + .post(Entity.form(new Form())); + Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus()); + + // Guest cannot update itself + response = target().path("/user").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken) + .post(Entity.form(new Form())); + Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus()); + + // Guest can see its documents + target().path("/document/list").request() + .cookie(TokenBasedSecurityFilter.COOKIE_NAME, guestToken) + .get(JsonObject.class); } } \ No newline at end of file