This is an automated email from the ASF dual-hosted git repository.

ilgrosso pushed a commit to branch 4_0_X
in repository https://gitbox.apache.org/repos/asf/syncope.git


The following commit(s) were added to refs/heads/4_0_X by this push:
     new 8c281caf12 [SYNCOPE-1912] Check LinkedAccounts for password 
propagation after User update
8c281caf12 is described below

commit 8c281caf1280a8397e46ef34ad92bf2b63d648e6
Author: Francesco Chicchiriccò <[email protected]>
AuthorDate: Mon Sep 15 10:52:19 2025 +0200

    [SYNCOPE-1912] Check LinkedAccounts for password propagation after User 
update
---
 .../provisioning/api/PropagationByResource.java    | 29 +++++--
 .../propagation/DefaultPropagationManager.java     | 98 ++++++++++++----------
 .../syncope/fit/core/LinkedAccountITCase.java      | 36 +++++++-
 pom.xml                                            |  4 +-
 4 files changed, 114 insertions(+), 53 deletions(-)

diff --git 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/PropagationByResource.java
 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/PropagationByResource.java
index 9c71d67f0c..f2141a068e 100644
--- 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/PropagationByResource.java
+++ 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/PropagationByResource.java
@@ -24,6 +24,7 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 import org.apache.syncope.common.lib.types.ResourceOperation;
 
@@ -52,7 +53,7 @@ public class PropagationByResource<T extends Serializable> 
implements Serializab
     private final Set<T> toBeDeleted;
 
     /**
-     * Mapping target resource names to old ConnObjectKeys (when applicable).
+     * Mapping target keys to old ConnObjectKeys (when applicable).
      */
     private final Map<String, String> oldConnObjectKeys;
 
@@ -189,11 +190,10 @@ public class PropagationByResource<T extends 
Serializable> implements Serializab
     }
 
     /**
-     * Removes only the resource names in the underlying resource name sets 
that are contained in the specified
-     * collection.
+     * Removes only the keys in the underlying sets that are contained in the 
specified collection.
      *
-     * @param keys collection containing resource names to be retained in the 
underlying resource name sets
-     * @return {@code true} if the underlying resource name sets changed as a 
result of the call
+     * @param keys collection containing keys to be retained in the underlying 
sets
+     * @return {@code true} if the underlying sets changed as a result of the 
call
      * @see Collection#removeAll(java.util.Collection)
      */
     public boolean removeAll(final Collection<T> keys) {
@@ -203,11 +203,24 @@ public class PropagationByResource<T extends 
Serializable> implements Serializab
     }
 
     /**
-     * Retains only the resource names in the underlying resource name sets 
that are contained in the specified
+     * Removes all of the keys in the underlying sets that satisfy the given 
predicate.
+     *
+     * @param filter a predicate which returns true for elements to be removed
+     * @return {@code true} if the underlying sets changed as a result of the 
call
+     * @see Collection#removeIf(java.util.function.Predicate)
+     */
+    public boolean removeIf(final Predicate<? super T> filter) {
+        return toBeCreated.removeIf(filter)
+                || toBeUpdated.removeIf(filter)
+                || toBeDeleted.removeIf(filter);
+    }
+
+    /**
+     * Retains only the keys in the underlying sets that are contained in the 
specified
      * collection.
      *
-     * @param keys collection containing resource names to be retained in the 
underlying resource name sets
-     * @return {@code true} if the underlying resource name sets changed as a 
result of the call
+     * @param keys collection containing keys to be retained in the underlying 
sets
+     * @return {@code true} if the underlying sets changed as a result of the 
call
      * @see Collection#retainAll(java.util.Collection)
      */
     public boolean retainAll(final Collection<T> keys) {
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DefaultPropagationManager.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DefaultPropagationManager.java
index 83281bddc1..c4536f16f9 100644
--- 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DefaultPropagationManager.java
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DefaultPropagationManager.java
@@ -31,14 +31,16 @@ import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.tuple.Pair;
-import org.apache.syncope.common.lib.request.AbstractPatchItem;
 import org.apache.syncope.common.lib.request.AnyUR;
+import org.apache.syncope.common.lib.request.LinkedAccountUR;
 import org.apache.syncope.common.lib.request.PasswordPatch;
 import org.apache.syncope.common.lib.request.UserUR;
 import org.apache.syncope.common.lib.to.Item;
+import org.apache.syncope.common.lib.to.LinkedAccountTO;
 import org.apache.syncope.common.lib.to.OrgUnit;
 import org.apache.syncope.common.lib.to.Provision;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.common.lib.types.PatchOperation;
 import org.apache.syncope.common.lib.types.ResourceOperation;
 import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
 import org.apache.syncope.core.persistence.api.dao.NotFoundException;
@@ -224,58 +226,70 @@ public class DefaultPropagationManager implements 
PropagationManager {
     public List<PropagationTaskInfo> getUserUpdateTasks(final 
UserWorkflowResult<Pair<UserUR, Boolean>> wfResult) {
         UserUR userUR = wfResult.getResult().getLeft();
 
+        Collection<String> assignedResources = 
anyUtilsFactory.getInstance(AnyTypeKind.USER).
+                dao().findAllResourceKeys(userUR.getKey());
+        List<String> urPwdResources = userUR.getPassword() == null
+                ? List.of()
+                : 
userUR.getPassword().getResources().stream().filter(assignedResources::contains).distinct().toList();
+
+        List<String> laPwdResources = userUR.getLinkedAccounts().stream().
+                filter(laur -> laur.getOperation() == 
PatchOperation.ADD_REPLACE).
+                map(LinkedAccountUR::getLinkedAccountTO).
+                filter(la -> la != null && la.getPassword() != null).
+                map(LinkedAccountTO::getResource).
+                distinct().toList();
+
+        List<String> pwdResources = Stream.concat(urPwdResources.stream(), 
laPwdResources.stream()).
+                distinct().toList();
+
         // Propagate password update only to requested resources
         List<PropagationTaskInfo> tasks;
-        if (userUR.getPassword() == null) {
+        if (pwdResources.isEmpty()) {
             // a. no specific password propagation request: generate 
propagation tasks for any resource associated
             tasks = getUserUpdateTasks(wfResult, List.of(), null);
         } else {
+            // b. generate the propagation task list in two phases: first the 
ones with no password, then the others
             tasks = new ArrayList<>();
 
-            // b. generate the propagation task list in two phases: first the 
ones containing password,
-            // then the rest (with no password)
-            UserWorkflowResult<Pair<UserUR, Boolean>> pwdWFResult = new 
UserWorkflowResult<>(
-                    wfResult.getResult(),
-                    new PropagationByResource<>(),
-                    wfResult.getPropByLinkedAccount(),
-                    wfResult.getPerformedTasks());
-
-            Set<String> pwdResourceNames = new 
HashSet<>(userUR.getPassword().getResources());
-            Collection<String> allResourceNames = 
anyUtilsFactory.getInstance(AnyTypeKind.USER).
-                    dao().findAllResourceKeys(userUR.getKey());
-            pwdResourceNames.retainAll(allResourceNames);
-
-            if (wfResult.getPropByRes() == null || 
wfResult.getPropByRes().isEmpty()) {
-                pwdWFResult.getPropByRes().addAll(ResourceOperation.UPDATE, 
pwdResourceNames);
-            } else {
-                Map<String, ResourceOperation> wfPropByResMap = 
wfResult.getPropByRes().asMap();
-                pwdResourceNames.forEach(r -> pwdWFResult.getPropByRes().
-                        add(wfPropByResMap.getOrDefault(r, 
ResourceOperation.UPDATE), r));
-            }
-            if (!pwdWFResult.getPropByRes().isEmpty()) {
-                Set<String> toBeExcluded = new HashSet<>(allResourceNames);
-                toBeExcluded.addAll(userUR.getResources().stream().
-                        map(AbstractPatchItem::getValue).toList());
-                toBeExcluded.removeAll(pwdResourceNames);
+            PropagationByResource<String> urNoPwdPropByRes = new 
PropagationByResource<>();
+            urNoPwdPropByRes.merge(wfResult.getPropByRes());
+            urNoPwdPropByRes.removeAll(urPwdResources);
+            urNoPwdPropByRes.purge();
 
-                tasks.addAll(getUserUpdateTasks(pwdWFResult, new 
ArrayList<>(pwdResourceNames), toBeExcluded));
-            }
+            PropagationByResource<Pair<String, String>> laNoPwdPropByRes = new 
PropagationByResource<>();
+            laNoPwdPropByRes.merge(wfResult.getPropByLinkedAccount());
+            laNoPwdPropByRes.removeIf(p -> 
laPwdResources.contains(p.getLeft()));
+            laNoPwdPropByRes.purge();
 
-            UserWorkflowResult<Pair<UserUR, Boolean>> noPwdWFResult = new 
UserWorkflowResult<>(
-                    wfResult.getResult(),
-                    new PropagationByResource<>(),
-                    new PropagationByResource<>(),
-                    wfResult.getPerformedTasks());
-
-            noPwdWFResult.getPropByRes().merge(wfResult.getPropByRes());
-            noPwdWFResult.getPropByRes().removeAll(pwdResourceNames);
-            noPwdWFResult.getPropByRes().purge();
-            if (!noPwdWFResult.getPropByRes().isEmpty()) {
-                tasks.addAll(getUserUpdateTasks(noPwdWFResult, List.of(), 
pwdResourceNames));
+            if (!urNoPwdPropByRes.isEmpty() || !laNoPwdPropByRes.isEmpty()) {
+                UserWorkflowResult<Pair<UserUR, Boolean>> noPwdWFResult = new 
UserWorkflowResult<>(
+                        wfResult.getResult(),
+                        urNoPwdPropByRes,
+                        laNoPwdPropByRes,
+                        wfResult.getPerformedTasks());
+
+                tasks.addAll(getUserUpdateTasks(noPwdWFResult, List.of(), 
null));
             }
 
-            tasks = tasks.stream().distinct().toList();
-            tasks.forEach(task -> 
task.setUpdateRequest(wfResult.getResult().getLeft()));
+            PropagationByResource<String> urPwdPropByRes = new 
PropagationByResource<>();
+            urPwdPropByRes.merge(wfResult.getPropByRes());
+            urPwdPropByRes.retainAll(urPwdResources);
+            urPwdPropByRes.purge();
+
+            PropagationByResource<Pair<String, String>> laPwdPropByRes = new 
PropagationByResource<>();
+            laPwdPropByRes.merge(wfResult.getPropByLinkedAccount());
+            laPwdPropByRes.removeIf(p -> 
!laPwdResources.contains(p.getLeft()));
+            laPwdPropByRes.purge();
+
+            if (!urPwdPropByRes.isEmpty() || !laPwdPropByRes.isEmpty()) {
+                UserWorkflowResult<Pair<UserUR, Boolean>> pwdWFResult = new 
UserWorkflowResult<>(
+                        wfResult.getResult(),
+                        urPwdPropByRes,
+                        laPwdPropByRes,
+                        wfResult.getPerformedTasks());
+
+                tasks.addAll(getUserUpdateTasks(pwdWFResult, pwdResources, 
null));
+            }
         }
 
         return tasks;
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/LinkedAccountITCase.java
 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/LinkedAccountITCase.java
index 313212fae4..02e1f2a74e 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/LinkedAccountITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/LinkedAccountITCase.java
@@ -70,10 +70,12 @@ import org.apache.syncope.common.lib.types.UnmatchingRule;
 import org.apache.syncope.common.rest.api.RESTHeaders;
 import org.apache.syncope.common.rest.api.beans.TaskQuery;
 import org.apache.syncope.common.rest.api.service.TaskService;
+import org.apache.syncope.core.persistence.api.entity.task.PropagationData;
 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 import org.apache.syncope.fit.AbstractITCase;
 import 
org.apache.syncope.fit.core.reference.LinkedAccountSampleInboundCorrelationRule;
 import 
org.apache.syncope.fit.core.reference.LinkedAccountSampleInboundCorrelationRuleConf;
+import org.identityconnectors.framework.common.objects.OperationalAttributes;
 import org.junit.jupiter.api.Test;
 
 public class LinkedAccountITCase extends AbstractITCase {
@@ -247,22 +249,54 @@ public class LinkedAccountITCase extends AbstractITCase {
                     sce.getMessage());
         }
 
+        // clean propagation tasks
+        TASK_SERVICE.search(new 
TaskQuery.Builder(TaskType.PROPAGATION).resource(RESOURCE_NAME_LDAP).
+                
anyTypeKind(AnyTypeKind.USER).entityKey(user.getKey()).build()).getResult().
+                forEach(task -> TASK_SERVICE.delete(TaskType.PROPAGATION, 
task.getKey()));
+
         // set a correct password
         account.setPassword("Password123");
         user = updateUser(userUR).getEntity();
         assertNotNull(user.getLinkedAccounts().getFirst().getPassword());
 
-        // 5. update linked account  password
+        PagedResult<PropagationTaskTO> tasks = TASK_SERVICE.search(
+                new 
TaskQuery.Builder(TaskType.PROPAGATION).resource(RESOURCE_NAME_LDAP).
+                        
anyTypeKind(AnyTypeKind.USER).entityKey(user.getKey()).build());
+        assertEquals(1, tasks.getTotalCount());
+        assertEquals(connObjectKeyValue, 
tasks.getResult().getFirst().getConnObjectKey());
+        assertEquals(ExecStatus.SUCCESS.name(), 
tasks.getResult().getFirst().getLatestExecStatus());
+        PropagationData propagationData = POJOHelper.deserialize(
+                tasks.getResult().getFirst().getPropagationData(), 
PropagationData.class);
+        assertTrue(propagationData.getAttributes().stream().
+                anyMatch(a -> 
OperationalAttributes.PASSWORD_NAME.equals(a.getName())));
+
+        // 5. update linked account password
         String beforeUpdatePassword = 
user.getLinkedAccounts().getFirst().getPassword();
         account.setPassword("Password123Updated");
         userUR = new UserUR();
         userUR.setKey(user.getKey());
 
+        // clean propagation tasks
+        TASK_SERVICE.search(new 
TaskQuery.Builder(TaskType.PROPAGATION).resource(RESOURCE_NAME_LDAP).
+                
anyTypeKind(AnyTypeKind.USER).entityKey(user.getKey()).build()).getResult().
+                forEach(task -> TASK_SERVICE.delete(TaskType.PROPAGATION, 
task.getKey()));
+
         userUR.getLinkedAccounts().add(new 
LinkedAccountUR.Builder().linkedAccountTO(account).build());
         user = updateUser(userUR).getEntity();
         assertNotNull(user.getLinkedAccounts().getFirst().getPassword());
         assertNotEquals(beforeUpdatePassword, 
user.getLinkedAccounts().getFirst().getPassword());
 
+        tasks = TASK_SERVICE.search(
+                new 
TaskQuery.Builder(TaskType.PROPAGATION).resource(RESOURCE_NAME_LDAP).
+                        
anyTypeKind(AnyTypeKind.USER).entityKey(user.getKey()).build());
+        assertEquals(1, tasks.getTotalCount());
+        assertEquals(connObjectKeyValue, 
tasks.getResult().getFirst().getConnObjectKey());
+        assertEquals(ExecStatus.SUCCESS.name(), 
tasks.getResult().getFirst().getLatestExecStatus());
+        propagationData = POJOHelper.deserialize(
+                tasks.getResult().getFirst().getPropagationData(), 
PropagationData.class);
+        assertTrue(propagationData.getAttributes().stream().
+                anyMatch(a -> 
OperationalAttributes.PASSWORD_NAME.equals(a.getName())));
+
         // 6. set linked account password to null
         account.setPassword(null);
         userUR = new UserUR();
diff --git a/pom.xml b/pom.xml
index bce1bcb765..a303992208 100644
--- a/pom.xml
+++ b/pom.xml
@@ -505,7 +505,7 @@ under the License.
     <cargo.rmi.port>9805</cargo.rmi.port>
     <cargo.deployable.ping.timeout>60000</cargo.deployable.ping.timeout>
 
-    <tomcat.version>10.1.45</tomcat.version>
+    <tomcat.version>10.1.46</tomcat.version>
     <wildfly.version>37.0.1.Final</wildfly.version>
     <payara.version>6.2025.9</payara.version>
     <jakarta.faces.version>4.1.3</jakarta.faces.version>
@@ -1742,7 +1742,7 @@ under the License.
         <plugin>
           <groupId>org.codehaus.cargo</groupId>
           <artifactId>cargo-maven3-plugin</artifactId>
-          <version>1.10.21</version>
+          <version>1.10.22</version>
           <configuration>
             <container>
               <log>${project.build.directory}/log/cargo.log</log>

Reply via email to