Closes #306: Prevent deleting/renaming users/groups used in route models

This commit is contained in:
Benjamin Gamard 2019-05-02 16:19:50 +02:00
parent cea0d4887d
commit 58bc374e64
8 changed files with 109 additions and 13 deletions

View File

@ -4,6 +4,7 @@ import com.google.common.base.Joiner;
import com.sismics.docs.core.constant.AuditLogType; import com.sismics.docs.core.constant.AuditLogType;
import com.sismics.docs.core.dao.criteria.RouteModelCriteria; import com.sismics.docs.core.dao.criteria.RouteModelCriteria;
import com.sismics.docs.core.dao.dto.RouteModelDto; import com.sismics.docs.core.dao.dto.RouteModelDto;
import com.sismics.docs.core.model.jpa.File;
import com.sismics.docs.core.model.jpa.RouteModel; import com.sismics.docs.core.model.jpa.RouteModel;
import com.sismics.docs.core.util.AuditLogUtil; import com.sismics.docs.core.util.AuditLogUtil;
import com.sismics.docs.core.util.SecurityUtil; import com.sismics.docs.core.util.SecurityUtil;
@ -88,6 +89,18 @@ public class RouteModelDao {
} }
} }
/**
* Returns the list of all route models.
*
* @return List of route models
*/
@SuppressWarnings("unchecked")
public List<RouteModel> findAll() {
EntityManager em = ThreadLocalContext.get().getEntityManager();
Query q = em.createQuery("select r from RouteModel r where r.deleteDate is null");
return q.getResultList();
}
/** /**
* Deletes a route model. * Deletes a route model.
* *

View File

@ -6,6 +6,7 @@ import com.sismics.docs.core.constant.AclType;
import com.sismics.docs.core.constant.PermType; import com.sismics.docs.core.constant.PermType;
import com.sismics.docs.core.dao.AclDao; import com.sismics.docs.core.dao.AclDao;
import com.sismics.docs.core.dao.DocumentDao; import com.sismics.docs.core.dao.DocumentDao;
import com.sismics.docs.core.dao.RouteModelDao;
import com.sismics.docs.core.dao.UserDao; import com.sismics.docs.core.dao.UserDao;
import com.sismics.docs.core.dao.criteria.UserCriteria; import com.sismics.docs.core.dao.criteria.UserCriteria;
import com.sismics.docs.core.dao.dto.RouteStepDto; import com.sismics.docs.core.dao.dto.RouteStepDto;
@ -15,8 +16,14 @@ import com.sismics.docs.core.event.RouteStepValidateEvent;
import com.sismics.docs.core.model.context.AppContext; import com.sismics.docs.core.model.context.AppContext;
import com.sismics.docs.core.model.jpa.Acl; import com.sismics.docs.core.model.jpa.Acl;
import com.sismics.docs.core.model.jpa.Document; import com.sismics.docs.core.model.jpa.Document;
import com.sismics.docs.core.model.jpa.RouteModel;
import com.sismics.util.context.ThreadLocalContext; import com.sismics.util.context.ThreadLocalContext;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonReader;
import java.io.StringReader;
import java.util.List; import java.util.List;
/** /**
@ -87,4 +94,31 @@ public class RoutingUtil {
AppContext.getInstance().getMailEventBus().post(routeStepValidateEvent); AppContext.getInstance().getMailEventBus().post(routeStepValidateEvent);
} }
} }
/**
* Find the first route model name matching a target type and name.
*
* @param targetType Target type
* @param targetName Target name
* @return Route model name or null if none is matching
*/
public static String findRouteModelNameByTargetName(AclTargetType targetType, String targetName) {
RouteModelDao routeModelDao = new RouteModelDao();
List<RouteModel> routeModelList = routeModelDao.findAll();
for (RouteModel routeModel : routeModelList) {
try (JsonReader reader = Json.createReader(new StringReader(routeModel.getSteps()))) {
JsonArray stepsJson = reader.readArray();
for (int order = 0; order < stepsJson.size(); order++) {
JsonObject step = stepsJson.getJsonObject(order);
JsonObject target = step.getJsonObject("target");
AclTargetType routeTargetType = AclTargetType.valueOf(target.getString("type"));
String routeTargetName = target.getString("name");
if (targetType == routeTargetType && targetName.equals(routeTargetName)) {
return routeModel.getName();
}
}
}
}
return null;
}
} }

View File

@ -2,6 +2,7 @@ package com.sismics.docs.rest.resource;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.sismics.docs.core.constant.AclTargetType;
import com.sismics.docs.core.dao.GroupDao; import com.sismics.docs.core.dao.GroupDao;
import com.sismics.docs.core.dao.RoleBaseFunctionDao; import com.sismics.docs.core.dao.RoleBaseFunctionDao;
import com.sismics.docs.core.dao.UserDao; import com.sismics.docs.core.dao.UserDao;
@ -12,6 +13,7 @@ import com.sismics.docs.core.dao.dto.UserDto;
import com.sismics.docs.core.model.jpa.Group; import com.sismics.docs.core.model.jpa.Group;
import com.sismics.docs.core.model.jpa.User; import com.sismics.docs.core.model.jpa.User;
import com.sismics.docs.core.model.jpa.UserGroup; import com.sismics.docs.core.model.jpa.UserGroup;
import com.sismics.docs.core.util.RoutingUtil;
import com.sismics.docs.core.util.jpa.SortCriteria; import com.sismics.docs.core.util.jpa.SortCriteria;
import com.sismics.docs.rest.constant.BaseFunction; import com.sismics.docs.rest.constant.BaseFunction;
import com.sismics.rest.exception.ClientException; import com.sismics.rest.exception.ClientException;
@ -149,6 +151,14 @@ public class GroupResource extends BaseResource {
parentId = parentGroup.getId(); parentId = parentGroup.getId();
} }
// Check that this group is not used in any workflow in case of renaming
if (!name.equals(groupName)) {
String routeModelName = RoutingUtil.findRouteModelNameByTargetName(AclTargetType.GROUP, groupName);
if (routeModelName != null) {
throw new ClientException("GroupUsedInRouteModel", routeModelName);
}
}
// Update the group // Update the group
groupDao.update(group.setName(name) groupDao.update(group.setName(name)
.setParentId(parentId), principal.getId()); .setParentId(parentId), principal.getId());
@ -198,6 +208,12 @@ public class GroupResource extends BaseResource {
} }
} }
// Check that this group is not used in any workflow
String routeModelName = RoutingUtil.findRouteModelNameByTargetName(AclTargetType.GROUP, groupName);
if (routeModelName != null) {
throw new ClientException("GroupUsedInRouteModel", routeModelName);
}
// Delete the group // Delete the group
groupDao.delete(group.getId(), principal.getId()); groupDao.delete(group.getId(), principal.getId());

View File

@ -2,6 +2,7 @@ package com.sismics.docs.rest.resource;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.sismics.docs.core.constant.AclTargetType;
import com.sismics.docs.core.constant.ConfigType; import com.sismics.docs.core.constant.ConfigType;
import com.sismics.docs.core.constant.Constants; import com.sismics.docs.core.constant.Constants;
import com.sismics.docs.core.dao.*; import com.sismics.docs.core.dao.*;
@ -15,6 +16,7 @@ import com.sismics.docs.core.event.PasswordLostEvent;
import com.sismics.docs.core.model.context.AppContext; import com.sismics.docs.core.model.context.AppContext;
import com.sismics.docs.core.model.jpa.*; import com.sismics.docs.core.model.jpa.*;
import com.sismics.docs.core.util.ConfigUtil; import com.sismics.docs.core.util.ConfigUtil;
import com.sismics.docs.core.util.RoutingUtil;
import com.sismics.docs.core.util.authentication.AuthenticationUtil; import com.sismics.docs.core.util.authentication.AuthenticationUtil;
import com.sismics.docs.core.util.jpa.SortCriteria; import com.sismics.docs.core.util.jpa.SortCriteria;
import com.sismics.docs.rest.constant.BaseFunction; import com.sismics.docs.rest.constant.BaseFunction;
@ -450,6 +452,12 @@ public class UserResource extends BaseResource {
throw new ClientException("ForbiddenError", "This user cannot be deleted"); throw new ClientException("ForbiddenError", "This user cannot be deleted");
} }
// Check that this user is not used in any workflow
String routeModelName = RoutingUtil.findRouteModelNameByTargetName(AclTargetType.USER, principal.getName());
if (routeModelName != null) {
throw new ClientException("UserUsedInRouteModel", routeModelName);
}
// Find linked data // Find linked data
DocumentDao documentDao = new DocumentDao(); DocumentDao documentDao = new DocumentDao();
List<Document> documentList = documentDao.findByUserId(principal.getId()); List<Document> documentList = documentDao.findByUserId(principal.getId());
@ -512,7 +520,7 @@ public class UserResource extends BaseResource {
throw new ClientException("ForbiddenError", "The guest user cannot be deleted"); throw new ClientException("ForbiddenError", "The guest user cannot be deleted");
} }
// Check if the user exists // Check that the user exists
UserDao userDao = new UserDao(); UserDao userDao = new UserDao();
User user = userDao.getActiveByUsername(username); User user = userDao.getActiveByUsername(username);
if (user == null) { if (user == null) {
@ -526,6 +534,12 @@ public class UserResource extends BaseResource {
throw new ClientException("ForbiddenError", "The admin user cannot be deleted"); throw new ClientException("ForbiddenError", "The admin user cannot be deleted");
} }
// Check that this user is not used in any workflow
String routeModelName = RoutingUtil.findRouteModelNameByTargetName(AclTargetType.USER, username);
if (routeModelName != null) {
throw new ClientException("UserUsedInRouteModel", routeModelName);
}
// Find linked data // Find linked data
DocumentDao documentDao = new DocumentDao(); DocumentDao documentDao = new DocumentDao();
List<Document> documentList = documentDao.findByUserId(user.getId()); List<Document> documentList = documentDao.findByUserId(user.getId());

View File

@ -51,6 +51,11 @@ angular.module('docs').controller('SettingsGroupEdit', function($scope, $dialog,
var msg = $translate.instant('settings.group.edit.edit_group_failed_message'); var msg = $translate.instant('settings.group.edit.edit_group_failed_message');
var btns = [{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}]; var btns = [{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}];
$dialog.messageBox(title, msg, btns); $dialog.messageBox(title, msg, btns);
} else if (e.data.type === 'GroupUsedInRouteModel') {
var title = $translate.instant('settings.group.edit.group_used_title');
var msg = $translate.instant('settings.group.edit.group_used_message', { name: e.data.message });
var btns = [{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}];
$dialog.messageBox(title, msg, btns);
} }
}); });
}; };
@ -71,8 +76,13 @@ angular.module('docs').controller('SettingsGroupEdit', function($scope, $dialog,
Restangular.one('group', $stateParams.name).remove().then(function() { Restangular.one('group', $stateParams.name).remove().then(function() {
$scope.loadGroups(); $scope.loadGroups();
$state.go('settings.group'); $state.go('settings.group');
}, function() { }, function(e) {
$state.go('settings.group'); if (e.data.type === 'GroupUsedInRouteModel') {
var title = $translate.instant('settings.group.edit.group_used_title');
var msg = $translate.instant('settings.group.edit.group_used_message', { name: e.data.message });
var btns = [{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}];
$dialog.messageBox(title, msg, btns);
}
}); });
} }
}); });

View File

@ -70,8 +70,13 @@ angular.module('docs').controller('SettingsUserEdit', function($scope, $dialog,
Restangular.one('user', $stateParams.username).remove().then(function () { Restangular.one('user', $stateParams.username).remove().then(function () {
$scope.loadUsers(); $scope.loadUsers();
$state.go('settings.user'); $state.go('settings.user');
}, function() { }, function(e) {
$state.go('settings.user'); if (e.data.type === 'UserUsedInRouteModel') {
var title = $translate.instant('settings.user.edit.user_used_title');
var msg = $translate.instant('settings.user.edit.user_used_message', { name: e.data.message });
var btns = [{result: 'ok', label: $translate.instant('ok'), cssClass: 'btn-primary'}];
$dialog.messageBox(title, msg, btns);
}
}); });
} }
}); });

View File

@ -288,6 +288,8 @@
"edit": { "edit": {
"delete_user_title": "Delete user", "delete_user_title": "Delete user",
"delete_user_message": "Do you really want to delete this user? All associated documents, files and tags will be deleted", "delete_user_message": "Do you really want to delete this user? All associated documents, files and tags will be deleted",
"user_used_title": "User in use",
"user_used_message": "This user is used in the workflow \"{{ name }}\"",
"edit_user_failed_title": "User already exists", "edit_user_failed_title": "User already exists",
"edit_user_failed_message": "This username is already taken by another user", "edit_user_failed_message": "This username is already taken by another user",
"edit_user_title": "Edit \"{{ username }}\"", "edit_user_title": "Edit \"{{ username }}\"",
@ -363,6 +365,8 @@
"delete_group_message": "Do you really want to delete this group?", "delete_group_message": "Do you really want to delete this group?",
"edit_group_failed_title": "Group already exists", "edit_group_failed_title": "Group already exists",
"edit_group_failed_message": "This group name is already taken by another group", "edit_group_failed_message": "This group name is already taken by another group",
"group_used_title": "Group in use",
"group_used_message": "This group is used in the workflow \"{{ name }}\"",
"edit_group_title": "Edit \"{{ name }}\"", "edit_group_title": "Edit \"{{ name }}\"",
"add_group_title": "Add a group", "add_group_title": "Add a group",
"name": "Name", "name": "Name",

View File

@ -325,7 +325,7 @@
"type_approve": "Approbation", "type_approve": "Approbation",
"type_validate": "Validation", "type_validate": "Validation",
"target": "Assigné à", "target": "Assigné à",
"target_help": "<strong>Approbation :</strong> Accepter ou rejeter l'étape de workflow<br/><strong>Validation :</strong> Examiner et poursuivre le workflow", "target_help": "<strong>Approbation :</strong> Accepter ou rejeter l\\'étape de workflow<br/><strong>Validation :</strong> Examiner et poursuivre le workflow",
"add_step": "Ajouter une étape de workflow", "add_step": "Ajouter une étape de workflow",
"actions": "Qu'est-ce qui se passe après?", "actions": "Qu'est-ce qui se passe après?",
"remove_action": "Supprimer l'action", "remove_action": "Supprimer l'action",