diff --git a/core-library/src/integration-test/resources/org/silverpeas/core/admin/user/notification/role/create_database.sql b/core-library/src/integration-test/resources/org/silverpeas/core/admin/user/notification/role/create_database.sql index 377c2b7a8a..4458503157 100644 --- a/core-library/src/integration-test/resources/org/silverpeas/core/admin/user/notification/role/create_database.sql +++ b/core-library/src/integration-test/resources/org/silverpeas/core/admin/user/notification/role/create_database.sql @@ -219,6 +219,37 @@ CREATE TABLE ST_UserRole_Group_Rel CONSTRAINT FK_UserRole_Group_Rel_2 FOREIGN KEY (groupId) REFERENCES ST_Group (id) ); +CREATE TABLE SB_Node_Node +( + nodeId INT NOT NULL, + nodeName VARCHAR (1000) NOT NULL, + nodeDescription VARCHAR (2000) NULL, + nodeCreationDate VARCHAR (10) NOT NULL, + nodeCreatorId VARCHAR (100) NOT NULL, + nodePath VARCHAR (1000) NOT NULL, + nodeLevelNumber INT NOT NULL, + nodeFatherId INT NOT NULL, + modelId VARCHAR (1000) NULL, + nodeStatus VARCHAR (1000) NULL, + instanceId VARCHAR (50) NOT NULL, + type VARCHAR (50) NULL, + orderNumber INT DEFAULT (0) NULL, + lang CHAR(2), + rightsDependsOn INT DEFAULT (-1) NOT NULL, + CONSTRAINT PK_Node_Node PRIMARY KEY (nodeId, instanceId) +); + +CREATE TABLE SB_Node_NodeI18N +( + id INT NOT NULL, + nodeId INT NOT NULL, + lang CHAR (2) NOT NULL, + nodeName VARCHAR (1000) NOT NULL, + nodeDescription VARCHAR (2000), + CONSTRAINT PK_Node_NodeI18N PRIMARY KEY (id), + CONSTRAINT UN_Node_NodeI18N UNIQUE (nodeId, lang) +); + /* * The SQL tables of a given component used in tests */ diff --git a/core-library/src/main/java/org/silverpeas/core/admin/user/notification/role/ProfileInstUpdateEventListener.java b/core-library/src/main/java/org/silverpeas/core/admin/user/notification/role/ProfileInstUpdateEventListener.java index b27e2eba5d..78cc1bb507 100644 --- a/core-library/src/main/java/org/silverpeas/core/admin/user/notification/role/ProfileInstUpdateEventListener.java +++ b/core-library/src/main/java/org/silverpeas/core/admin/user/notification/role/ProfileInstUpdateEventListener.java @@ -30,12 +30,15 @@ import org.silverpeas.core.admin.user.model.ProfileInst; import org.silverpeas.core.admin.user.notification.ProfileInstEvent; import org.silverpeas.core.annotation.Service; -import org.silverpeas.core.notification.system.CDIResourceEventListener; +import org.silverpeas.core.notification.system.CDIAfterSuccessfulTransactionResourceEventListener; import org.silverpeas.core.notification.system.ResourceEvent; import javax.inject.Inject; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -50,7 +53,8 @@ * @author mmoquillon */ @Service -class ProfileInstUpdateEventListener extends CDIResourceEventListener { +class ProfileInstUpdateEventListener + extends CDIAfterSuccessfulTransactionResourceEventListener { @Inject private OrganizationController organization; @@ -62,15 +66,19 @@ class ProfileInstUpdateEventListener extends CDIResourceEventListener removedUsers = findRemovedUsersId(componentInst, before, after); - if (!removedUsers.isEmpty()) { - UserRoleEvent userRoleEvent = UserRoleEvent.builderFor(ResourceEvent.Type.DELETION) - .role(before.getName()) - .instanceId(componentInst.getId()) - .userIds(removedUsers) - .build(); - notifier.notify(userRoleEvent); + if (before.getObjectId().isNotDefined()) { + // we take in charge only changes in the right profiles of component instances, no those of + // resources managed by the component instances + ComponentInst componentInst = getComponentInstanceId(before.getComponentFatherId()); + Set removedUsers = findRemovedUsersId(componentInst, before, after); + if (!removedUsers.isEmpty()) { + UserRoleEvent userRoleEvent = UserRoleEvent.builderFor(ResourceEvent.Type.DELETION) + .role(before.getName()) + .instanceId(componentInst.getId()) + .userIds(removedUsers) + .build(); + notifier.notify(userRoleEvent); + } } } @@ -90,13 +98,15 @@ private Set findRemovedUsersId(ComponentInst componentInst, ProfileInst ProfileInst after) { List usersAfter = after.getAllUsers(); List groupsAfter = after.getAllGroups(); - String roleName = before.getName(); + String roleName = before.getName(); + NoAnymorePlayedRole roleNotAnymorePlayedByUser = new NoAnymorePlayedRole(roleName, + componentInst.getId()); + // get all the users directly removed from the profile instance and who don't play anymore // the role for the application (they can be play another role) Stream removedUsers = before.getAllUsers().stream() .filter(user -> !usersAfter.contains(user)) - .filter(u -> Stream.of(organization.getUserProfiles(u, componentInst.getId())) - .noneMatch(p -> p.equalsIgnoreCase(roleName))); + .filter(roleNotAnymorePlayedByUser); // get all the users belonging to the groups removed from the profile instance and who's not @@ -107,8 +117,7 @@ private Set findRemovedUsersId(ComponentInst componentInst, ProfileInst .map(g -> organization.getGroup(g)) .flatMap(g -> Stream.of(((Group) g).getUserIds())) .distinct() - .filter(u -> Stream.of(organization.getUserProfiles(u, componentInst.getId())) - .noneMatch(p -> p.equalsIgnoreCase(roleName))); + .filter(roleNotAnymorePlayedByUser); return Stream.concat(removedUsers, removedUsersInGroups).collect(Collectors.toSet()); } @@ -116,5 +125,25 @@ private Set findRemovedUsersId(ComponentInst componentInst, ProfileInst private ComponentInst getComponentInstanceId(int localComponentId) { return organization.getComponentInst(String.valueOf(localComponentId)); } + + private class NoAnymorePlayedRole implements Predicate { + + private final Map cache = new HashMap<>(); + private final String instanceId; + private final String roleName; + + public NoAnymorePlayedRole(String roleName, String componentInstanceId) { + this.instanceId = componentInstanceId; + this.roleName = roleName; + } + + @Override + public boolean test(String userId) { + String[] roles = cache.computeIfAbsent(userId, + u -> organization.getUserProfiles(u, instanceId)); + return roles.length == 0 || + Stream.of(roles).noneMatch(p -> p.equalsIgnoreCase(roleName)); + } + } } \ No newline at end of file diff --git a/core-library/src/main/java/org/silverpeas/core/node/service/NodeProfileInstEventListener.java b/core-library/src/main/java/org/silverpeas/core/node/service/NodeProfileInstEventListener.java new file mode 100644 index 0000000000..cfecdcdbee --- /dev/null +++ b/core-library/src/main/java/org/silverpeas/core/node/service/NodeProfileInstEventListener.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2000 - 2025 Silverpeas + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * As a special exception to the terms and conditions of version 3.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * Open Source Software ("FLOSS") applications as described in Silverpeas's + * FLOSS exception. You should have received a copy of the text describing + * the FLOSS exception, and it is also available here: + * "https://www.silverpeas.org/legal/floss_exception.html" + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.silverpeas.core.node.service; + +import org.silverpeas.core.admin.component.model.ComponentInst; +import org.silverpeas.core.admin.service.AdminException; +import org.silverpeas.core.admin.service.Administration; +import org.silverpeas.core.admin.user.model.ProfileInst; +import org.silverpeas.core.admin.user.notification.ProfileInstEvent; +import org.silverpeas.core.annotation.Service; +import org.silverpeas.core.notification.system.CDIResourceEventListener; +import org.silverpeas.kernel.SilverpeasRuntimeException; + +import javax.inject.Inject; +import javax.transaction.Transactional; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Listener of events about changes in a given right profile instance of a component instance. + * For all the user groups removed from the profile instance related by the event, an invocation + * to the {@link NodeProfileInstUpdater} is performed. + * + * @author mmoquillon + */ +@Service +public class NodeProfileInstEventListener extends CDIResourceEventListener { + + @Inject + private Administration admin; + @Inject + private NodeProfileInstUpdater updater; + + @Transactional + @Override + public void onUpdate(ProfileInstEvent event) { + ProfileInst before = event.getTransition().getBefore(); + if (before.isOnComponentInstance()) { + ProfileInst after = event.getTransition().getAfter(); + int instanceId = before.getComponentFatherId(); + ComponentInst instance = getComponentInstanceId(instanceId); + List groupsAfter = after.getAllGroups(); + Set removedGroups = before.getAllGroups().stream() + .filter(group -> !groupsAfter.contains(group)) + .collect(Collectors.toSet()); + updater.getRemoverFor(instance.getId()) + .ofGroups(removedGroups) + .apply(); + } + } + + private ComponentInst getComponentInstanceId(int localComponentId) { + try { + return admin.getComponentInst(String.valueOf(localComponentId)); + } catch (AdminException e) { + throw new SilverpeasRuntimeException(e); + } + } + +} + \ No newline at end of file diff --git a/core-library/src/main/java/org/silverpeas/core/node/service/NodeProfileInstUpdater.java b/core-library/src/main/java/org/silverpeas/core/node/service/NodeProfileInstUpdater.java new file mode 100644 index 0000000000..b352f1c93c --- /dev/null +++ b/core-library/src/main/java/org/silverpeas/core/node/service/NodeProfileInstUpdater.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2000 - 2025 Silverpeas + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * As a special exception to the terms and conditions of version 3.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * Open Source Software ("FLOSS") applications as described in Silverpeas's + * FLOSS exception. You should have received a copy of the text describing + * the FLOSS exception, and it is also available here: + * "https://www.silverpeas.org/legal/floss_exception.html" + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.silverpeas.core.node.service; + +import org.silverpeas.core.admin.ProfiledObjectId; +import org.silverpeas.core.admin.component.model.ComponentInst; +import org.silverpeas.core.admin.service.AdminException; +import org.silverpeas.core.admin.service.Administration; +import org.silverpeas.core.admin.user.model.ProfileInst; +import org.silverpeas.core.annotation.Service; +import org.silverpeas.core.node.model.NodeDetail; +import org.silverpeas.core.node.model.NodePK; +import org.silverpeas.kernel.SilverpeasRuntimeException; + +import javax.inject.Inject; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +/** + * An updater of all the right profiles of the nodes managed in a given component instance. This is + * a service dedicated to be used by the listeners of events about change in the roles played by + * some users or by some user groups in order to to synchronize those change to the right profiles + * of the nodes having specific local right accesses and belonging to the component instance. + * + * @author mmoquillon + */ +@Service +public class NodeProfileInstUpdater { + + @Inject + private Administration admin; + @Inject + private NodeService nodeService; + + /** + * Gets a remover of both users and groups from right profiles of nodes belonging to the specified + * component instance. + * + * @param componentInstanceId the unique identifier of a component instance. + * @return a remover instance. + */ + public Remover getRemoverFor(String componentInstanceId) { + return new Remover(componentInstanceId); + } + + /** + * A remover of users and user groups from the roles specific to the nodes having local access + * rights. The users and the groups are removed only if they don't play at least one role in the + * component instance. + */ + public class Remover { + + private final String instanceId; + private final Set userIds = new HashSet<>(); + private final Set groupIds = new HashSet<>(); + + /** + * Constructs a new remover object for the specified component instance. + * + * @param instanceId the unique identifier of a component instance + */ + Remover(String instanceId) { + this.instanceId = instanceId; + } + + /** + * Sets the users to be removed from the right profiles of the nodes. + * + * @param userIds a set with the unique identifier of the users to remove. + * @return itself. + */ + Remover ofUsers(Set userIds) { + this.userIds.clear(); + this.userIds.addAll(userIds); + return this; + } + + /** + * Sets the user groups to be removed from the right profiles of the nodes. + * + * @param groupIds a set with the unique identifier of the user groups to remove. + * @return itself. + */ + Remover ofGroups(Set groupIds) { + this.groupIds.clear(); + this.groupIds.addAll(groupIds); + return this; + } + + /** + * Applies the remove. All the users and user groups sets will be removed from the right + * profiles of each nodes of the component instance having a specific local access rights if and + * only if they don't play any other role in the component instance. + */ + public void apply() { + nodeService.getDescendantDetails(new NodePK(NodePK.ROOT_NODE_ID, instanceId)).stream() + .filter(NodeDetail::haveLocalRights) + .forEach(node -> + getNodeRoles(node) + .forEach(role -> { + groupIds.stream() + .filter(group -> role.getAllGroups().contains(group)) + .filter(group -> isGroupNotPlayedAnyRole(group, instanceId)) + .forEach(group -> removeGroupFromRole(group, role)); + userIds.stream() + .filter(user -> role.getAllUsers().contains(user)) + .filter(user -> isUserNotPlayingAnotherRole(user, instanceId)) + .forEach(user -> removeUserFromRole(user, role)); + })); + } + + private void removeUserFromRole(String userId, ProfileInst role) { + try { + role.removeUser(userId); + admin.updateProfileInst(role); + } catch (AdminException e) { + throw new SilverpeasRuntimeException(e); + } + } + + private void removeGroupFromRole(String groupId, ProfileInst role) { + try { + role.removeGroup(groupId); + admin.updateProfileInst(role); + } catch (AdminException e) { + throw new SilverpeasRuntimeException(e); + } + } + + private List getNodeRoles(NodeDetail node) { + try { + return admin.getProfilesByObject(ProfiledObjectId.fromNode(node.getId()), + node.getIdentifier().getComponentInstanceId()); + } catch (AdminException e) { + throw new SilverpeasRuntimeException(e); + } + } + + private ProfileInst getProfileInst(String profileId) { + try { + return admin.getProfileInst(profileId); + } catch (AdminException e) { + throw new SilverpeasRuntimeException(e); + } + } + + private boolean isUserNotPlayingAnotherRole(String userId, String instanceId) { + try { + String[] roleNames = admin.getCurrentProfiles(userId, instanceId); + return roleNames.length == 0; + } catch (AdminException e) { + throw new SilverpeasRuntimeException(e); + } + } + + private boolean isGroupNotPlayedAnyRole(String groupId, String instanceId) { + try { + int localId = ComponentInst.getComponentLocalId(instanceId); + return Stream.of(admin.getProfileIdsOfGroup(groupId)) + .map(this::getProfileInst) + .filter(ProfileInst::isOnComponentInstance) + .noneMatch(p -> p.getComponentFatherId() == localId); + } catch (AdminException e) { + throw new SilverpeasRuntimeException(e); + } + } + } +} + \ No newline at end of file diff --git a/core-library/src/main/java/org/silverpeas/core/node/service/UserRoleEventListener.java b/core-library/src/main/java/org/silverpeas/core/node/service/UserRoleEventListener.java new file mode 100644 index 0000000000..cbbe00467e --- /dev/null +++ b/core-library/src/main/java/org/silverpeas/core/node/service/UserRoleEventListener.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2000 - 2025 Silverpeas + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * As a special exception to the terms and conditions of version 3.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * Open Source Software ("FLOSS") applications as described in Silverpeas's + * FLOSS exception. You should have received a copy of the text describing + * the FLOSS exception, and it is also available here: + * "https://www.silverpeas.org/legal/floss_exception.html" + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package org.silverpeas.core.node.service; + +import org.silverpeas.core.admin.user.notification.role.UserRoleEvent; +import org.silverpeas.core.annotation.Service; +import org.silverpeas.core.notification.system.CDIResourceEventListener; + +import javax.inject.Inject; +import javax.transaction.Transactional; + +/** + * Listeners of events about changes in a user role of one or more component instances. When such a + * user role change is detected, the listener invokes the {@link NodeProfileInstUpdater} service to + * remove all the users from the right profiles of the nodes (having a specific local right + * accesses) of the concerned component instances. + * + * @author mmoquillon + */ +@Service +public class UserRoleEventListener extends CDIResourceEventListener { + + @Inject + private NodeProfileInstUpdater updater; + + @Override + @Transactional + public void onDeletion(UserRoleEvent event) { + event.getInstanceIds() + .forEach(instance -> + updater.getRemoverFor(instance) + .ofUsers(event.getUserIds()) + .apply()); + } +} + \ No newline at end of file