Closes #167: disable users

This commit is contained in:
Benjamin Gamard 2017-11-20 21:21:50 +01:00
parent fb75bafe96
commit d786862a60
16 changed files with 138 additions and 22 deletions

View File

@ -10,6 +10,7 @@ import com.sismics.docs.core.util.jpa.QueryParam;
import com.sismics.docs.core.util.jpa.QueryUtil;
import com.sismics.docs.core.util.jpa.SortCriteria;
import com.sismics.util.context.ThreadLocalContext;
import org.joda.time.DateTime;
import org.mindrot.jbcrypt.BCrypt;
import javax.persistence.EntityManager;
@ -37,7 +38,7 @@ public class UserDao {
q.setParameter("username", username);
try {
User user = (User) q.getSingleResult();
if (!BCrypt.checkpw(password, user.getPassword())) {
if (!BCrypt.checkpw(password, user.getPassword()) || user.getDisableDate() != null) {
return null;
}
return user;
@ -98,7 +99,8 @@ public class UserDao {
userFromDb.setStorageQuota(user.getStorageQuota());
userFromDb.setStorageCurrent(user.getStorageCurrent());
userFromDb.setTotpKey(user.getTotpKey());
userFromDb.setDisableDate(user.getDisableDate());
// Create audit log
AuditLogUtil.create(userFromDb, AuditLogType.UPDATE, userId);
@ -246,7 +248,7 @@ public class UserDao {
Map<String, Object> parameterMap = new HashMap<>();
List<String> criteriaList = new ArrayList<>();
StringBuilder sb = new StringBuilder("select u.USE_ID_C as c0, u.USE_USERNAME_C as c1, u.USE_EMAIL_C as c2, u.USE_CREATEDATE_D as c3, u.USE_STORAGECURRENT_N as c4, u.USE_STORAGEQUOTA_N as c5, u.USE_TOTPKEY_C as c6");
StringBuilder sb = new StringBuilder("select u.USE_ID_C as c0, u.USE_USERNAME_C as c1, u.USE_EMAIL_C as c2, u.USE_CREATEDATE_D as c3, u.USE_STORAGECURRENT_N as c4, u.USE_STORAGEQUOTA_N as c5, u.USE_TOTPKEY_C as c6, u.USE_DISABLEDATE_D as c7");
sb.append(" from T_USER u ");
// Add search criterias
@ -283,7 +285,10 @@ public class UserDao {
userDto.setCreateTimestamp(((Timestamp) o[i++]).getTime());
userDto.setStorageCurrent(((Number) o[i++]).longValue());
userDto.setStorageQuota(((Number) o[i++]).longValue());
userDto.setTotpKey((String) o[i]);
userDto.setTotpKey((String) o[i++]);
if (o[i] != null) {
userDto.setDisableTimestamp(((Timestamp) o[i]).getTime());
}
userDtoList.add(userDto);
}
return userDtoList;
@ -299,4 +304,16 @@ public class UserDao {
Query query = em.createNativeQuery("select sum(u.USE_STORAGECURRENT_N) from T_USER u where u.USE_DELETEDATE_D is null");
return ((Number) query.getSingleResult()).longValue();
}
/**
* Returns the number of active users.
*
* @return Number of active users
*/
public long getActiveUserCount() {
EntityManager em = ThreadLocalContext.get().getEntityManager();
Query query = em.createNativeQuery("select count(u.USE_ID_C) from T_USER u where u.USE_DELETEDATE_D is null and (u.USE_DISABLEDATE_D is null or u.USE_DISABLEDATE_D > :date)");
query.setParameter("date", DateTime.now().minusMonths(1).toDate());
return ((Number) query.getSingleResult()).longValue();
}
}

View File

@ -26,6 +26,11 @@ public class UserDto {
*/
private Long createTimestamp;
/**
* Disable date of this user.
*/
private Long disableTimestamp;
/**
* Storage quota.
*/
@ -72,7 +77,16 @@ public class UserDto {
public void setCreateTimestamp(Long createTimestamp) {
this.createTimestamp = createTimestamp;
}
public Long getDisableTimestamp() {
return disableTimestamp;
}
public UserDto setDisableTimestamp(Long disableTimestamp) {
this.disableTimestamp = disableTimestamp;
return this;
}
public Long getStorageQuota() {
return storageQuota;
}

View File

@ -1,13 +1,12 @@
package com.sismics.docs.core.model.jpa;
import java.util.Date;
import com.google.common.base.MoreObjects;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import com.google.common.base.MoreObjects;
import java.util.Date;
/**
* User entity.
@ -84,6 +83,12 @@ public class User implements Loggable {
@Column(name = "USE_DELETEDATE_D")
private Date deleteDate;
/**
* Disable date.
*/
@Column(name = "USE_DISABLEDATE_D")
private Date disableDate;
public String getId() {
return id;
}
@ -147,7 +152,16 @@ public class User implements Loggable {
this.deleteDate = deleteDate;
return this;
}
public Date getDisableDate() {
return disableDate;
}
public User setDisableDate(Date disableDate) {
this.disableDate = disableDate;
return this;
}
public String getPrivateKey() {
return privateKey;
}

View File

@ -1 +1 @@
db.version=13
db.version=14

View File

@ -0,0 +1,2 @@
alter table T_USER add column USE_DISABLEDATE_D datetime;
update T_CONFIG set CFG_VALUE_C = '14' where CFG_ID_C = 'DB_VERSION';

View File

@ -57,7 +57,7 @@ public abstract class SecurityFilter implements Filter {
*/
private void injectUser(HttpServletRequest request, User user) {
// Check if the user is still valid
if (user != null && user.getDeleteDate() == null) {
if (user != null && user.getDeleteDate() == null && user.getDisableDate() == null) {
injectAuthenticatedUser(request, user);
} else {
injectAnonymousUser(request);

View File

@ -1,3 +1,3 @@
api.current_version=${project.version}
api.min_version=1.0
db.version=13
db.version=14

View File

@ -63,6 +63,7 @@ public class AppResource extends BaseResource {
* @apiSuccess {Boolean} guest_login True if guest login is enabled
* @apiSuccess {String} total_memory Allocated JVM memory (in bytes)
* @apiSuccess {String} free_memory Free JVM memory (in bytes)
* @apiSuccess {String} active_user_count Number of active users
* @apiSuccess {String} global_storage_current Global storage currently used (in bytes)
* @apiSuccess {String} global_storage_quota Maximum global storage (in bytes)
* @apiPermission none
@ -89,6 +90,7 @@ public class AppResource extends BaseResource {
.add("guest_login", guestLogin)
.add("total_memory", Runtime.getRuntime().totalMemory())
.add("free_memory", Runtime.getRuntime().freeMemory())
.add("active_user_count", userDao.getActiveUserCount())
.add("global_storage_current", userDao.getGlobalStorageCurrent());
if (globalQuota > 0) {
response.add("global_storage_quota", globalQuota);

View File

@ -186,6 +186,7 @@ public class UserResource extends BaseResource {
* @apiParam {String{8..50}} password Password
* @apiParam {String{1..100}} email E-mail
* @apiParam {Number} storage_quota Storage quota (in bytes)
* @apiParam {Boolean} disabled Disabled status
* @apiSuccess {String} status Status OK
* @apiError (client) ForbiddenError Access denied
* @apiError (client) ValidationError Validation error
@ -204,7 +205,8 @@ public class UserResource extends BaseResource {
@PathParam("username") String username,
@FormParam("password") String password,
@FormParam("email") String email,
@FormParam("storage_quota") String storageQuotaStr) {
@FormParam("storage_quota") String storageQuotaStr,
@FormParam("disabled") Boolean disabled) {
if (!authenticate()) {
throw new ForbiddenClientException();
}
@ -218,7 +220,7 @@ public class UserResource extends BaseResource {
UserDao userDao = new UserDao();
User user = userDao.getActiveByUsername(username);
if (user == null) {
throw new ClientException("UserNotFound", "The user doesn't exist");
throw new ClientException("UserNotFound", "The user does not exist");
}
// Update the user
@ -229,6 +231,22 @@ public class UserResource extends BaseResource {
Long storageQuota = ValidationUtil.validateLong(storageQuotaStr, "storage_quota");
user.setStorageQuota(storageQuota);
}
if (disabled != null) {
// Cannot disable the admin user or the guest user
RoleBaseFunctionDao userBaseFuction = new RoleBaseFunctionDao();
Set<String> baseFunctionSet = userBaseFuction.findByRoleId(Sets.newHashSet(user.getRoleId()));
if (Constants.GUEST_USER_ID.equals(username) || baseFunctionSet.contains(BaseFunction.ADMIN.name())) {
disabled = false;
}
if (disabled && user.getDisableDate() == null) {
// Recording the disabled date
user.setDisableDate(new Date());
} else if (!disabled && user.getDisableDate() != null) {
// Emptying the disabled date
user.setDisableDate(null);
}
}
user = userDao.update(user, principal.getId());
// Change the password
@ -631,6 +649,7 @@ public class UserResource extends BaseResource {
* @apiSuccess {Number} storage_quota Storage quota (in bytes)
* @apiSuccess {Number} storage_current Quota used (in bytes)
* @apiSuccess {String[]} groups Groups
* @apiSuccess {Boolean} disabled True if the user is disabled
* @apiError (client) ForbiddenError Access denied
* @apiError (client) UserNotFound The user does not exist
* @apiPermission user
@ -668,7 +687,8 @@ public class UserResource extends BaseResource {
.add("groups", groups)
.add("email", user.getEmail())
.add("storage_quota", user.getStorageQuota())
.add("storage_current", user.getStorageCurrent());
.add("storage_current", user.getStorageCurrent())
.add("disabled", user.getDisableDate() != null);
return Response.ok().entity(response.build()).build();
}
@ -688,6 +708,7 @@ public class UserResource extends BaseResource {
* @apiSuccess {Number} users.storage_quota Storage quota (in bytes)
* @apiSuccess {Number} users.storage_current Quota used (in bytes)
* @apiSuccess {Number} users.create_date Create date (timestamp)
* @apiSuccess {Number} users.disabled True if the user is disabled
* @apiError (client) ForbiddenError Access denied
* @apiPermission user
* @apiVersion 1.5.0
@ -730,7 +751,8 @@ public class UserResource extends BaseResource {
.add("email", userDto.getEmail())
.add("storage_quota", userDto.getStorageQuota())
.add("storage_current", userDto.getStorageCurrent())
.add("create_date", userDto.getCreateTimestamp()));
.add("create_date", userDto.getCreateTimestamp())
.add("disabled", userDto.getDisableTimestamp() != null));
}
JsonObjectBuilder response = Json.createObjectBuilder()

View File

@ -245,7 +245,8 @@
"storage_quota": "Storage quota",
"storage_quota_placeholder": "Storage quota (in MB)",
"password": "Password",
"password_confirm": "Password (confirm)"
"password_confirm": "Password (confirm)",
"disabled": "Disabled user"
}
},
"security": {

View File

@ -88,12 +88,23 @@
</div>
</div>
<div class="form-group" ng-show="isEdit() && user.username != 'admin' && user.username != 'guest'">
<div class="col-sm-offset-2 col-sm-10">
<div class="checkbox text-danger">
<label>
<input name="disabled" type="checkbox" ng-model="user.disabled" />
<strong>{{ 'settings.user.edit.disabled' | translate }}</strong>
</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary" ng-click="edit()" ng-disabled="!editUserForm.$valid">
<span class="glyphicon glyphicon-pencil"></span> {{ isEdit() ? 'edit' : 'add' | translate }}
</button>
<button type="button" class="btn btn-danger" ng-click="remove()" ng-show="isEdit() && user.username != 'guest'">
<button type="button" class="btn btn-danger" ng-click="remove()" ng-show="isEdit() && user.username != 'admin' && user.username != 'guest'">
<span class="glyphicon glyphicon-trash"></span> {{ 'delete' | translate }}
</button>
</div>

View File

@ -16,7 +16,8 @@
<tr ng-repeat="user in users | orderBy: 'username'" ng-click="editUser(user)"
ng-class="{ active: $stateParams.username == user.username }">
<td>
{{ user.username }}
<span ng-if="!user.disabled">{{ user.username }}</span>
<s ng-if="user.disabled">{{ user.username }}</s>
<span class="glyphicon glyphicon-lock" ng-show="user.totp_enabled" uib-tooltip="{{ 'settings.user.totp_enabled' | translate }}"></span>
</td>
<td>{{ user.create_date | date: dateFormat }}</td>

View File

@ -1,3 +1,3 @@
api.current_version=${project.version}
api.min_version=1.0
db.version=13
db.version=14

View File

@ -1,3 +1,3 @@
api.current_version=${project.version}
api.min_version=1.0
db.version=13
db.version=14

View File

@ -37,6 +37,7 @@ public class TestAppResource extends BaseJerseyTest {
Assert.assertTrue(totalMemory > 0 && totalMemory > freeMemory);
Assert.assertFalse(json.getBoolean("guest_login"));
Assert.assertTrue(json.containsKey("global_storage_current"));
Assert.assertTrue(json.getJsonNumber("active_user_count").longValue() > 0);
// Rebuild Lucene index
Response response = target().path("/app/batch/reindex").request()

View File

@ -57,6 +57,7 @@ public class TestUserResource extends BaseJerseyTest {
Assert.assertNotNull(user.getJsonNumber("storage_current"));
Assert.assertNotNull(user.getJsonNumber("create_date"));
Assert.assertFalse(user.getBoolean("totp_enabled"));
Assert.assertFalse(user.getBoolean("disabled"));
// Create a user KO (login length validation)
Response response = target().path("/user").request()
@ -262,7 +263,7 @@ public class TestUserResource extends BaseJerseyTest {
Assert.assertEquals("newadminemail@docs.com", json.getString("email"));
// User admin update admin_user1 information
json = target().path("/user").request()
json = target().path("/user/admin_user1").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
.post(Entity.form(new Form()
.param("email", " alice2@docs.com ")), JsonObject.class);
@ -276,6 +277,36 @@ public class TestUserResource extends BaseJerseyTest {
json = response.readEntity(JsonObject.class);
Assert.assertEquals("ForbiddenError", json.getString("type"));
// User admin disable admin_user1
json = target().path("/user/admin_user1").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
.post(Entity.form(new Form()
.param("disabled", "true")), JsonObject.class);
Assert.assertEquals("ok", json.getString("status"));
// User admin_user1 tries to authenticate
response = target().path("/user/login").request()
.post(Entity.form(new Form()
.param("username", "admin_user1")
.param("password", "12345678")
.param("remember", "false")));
Assert.assertEquals(Status.FORBIDDEN.getStatusCode(), response.getStatus());
// User admin enable admin_user1
json = target().path("/user/admin_user1").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)
.post(Entity.form(new Form()
.param("disabled", "false")), JsonObject.class);
Assert.assertEquals("ok", json.getString("status"));
// User admin_user1 tries to authenticate
response = target().path("/user/login").request()
.post(Entity.form(new Form()
.param("username", "admin_user1")
.param("password", "12345678")
.param("remember", "false")));
Assert.assertEquals(Status.OK.getStatusCode(), response.getStatus());
// User admin deletes user admin_user1
json = target().path("/user/admin_user1").request()
.cookie(TokenBasedSecurityFilter.COOKIE_NAME, adminToken)