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

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


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

commit 0b70e34417a5bf070a7532d14e944cd098f8621e
Author: Francesco Chicchiriccò <ilgro...@apache.org>
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     | 103 ++++++++++++---------
 .../syncope/fit/core/LinkedAccountITCase.java      |  36 ++++++-
 .../apache/syncope/fit/core/UserIssuesITCase.java  |  13 +--
 pom.xml                                            |   2 +-
 5 files changed, 120 insertions(+), 63 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 3d141e3914..a4c75fc8b1 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.Stream;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.lib.Attr;
-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.VirSchemaDAO;
@@ -236,58 +238,69 @@ public class DefaultPropagationManager implements 
PropagationManager {
     public List<PropagationTaskInfo> getUserUpdateTasks(final 
UserWorkflowResult<Pair<UserUR, Boolean>> wfResult) {
         UserUR userUR = wfResult.getResult().getLeft();
 
+        List<String> urPwdResources = userUR.getPassword() == null
+                ? List.of()
+                : userUR.getPassword().getResources().stream().
+                        distinct().collect(Collectors.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().collect(Collectors.toList());
+
+        List<String> pwdResources = Stream.concat(urPwdResources.stream(), 
laPwdResources.stream()).
+                distinct().collect(Collectors.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).collect(Collectors.toList()));
-                toBeExcluded.removeAll(pwdResourceNames);
+            PropagationByResource<String> urNoPwdPropByRes = new 
PropagationByResource<>();
+            urNoPwdPropByRes.merge(wfResult.getPropByRes());
+            urNoPwdPropByRes.purge();
+            urNoPwdPropByRes.removeAll(urPwdResources);
 
-                tasks.addAll(getUserUpdateTasks(pwdWFResult, new 
ArrayList<>(pwdResourceNames), toBeExcluded));
-            }
+            PropagationByResource<Pair<String, String>> laNoPwdPropByRes = new 
PropagationByResource<>();
+            laNoPwdPropByRes.merge(wfResult.getPropByLinkedAccount());
+            laNoPwdPropByRes.purge();
+            laNoPwdPropByRes.removeIf(p -> 
laPwdResources.contains(p.getLeft()));
 
-            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().collect(Collectors.toList());
-            tasks.forEach(task -> 
task.setUpdateRequest(wfResult.getResult().getLeft()));
+            PropagationByResource<String> urPwdPropByRes = new 
PropagationByResource<>();
+            urPwdPropByRes.merge(wfResult.getPropByRes());
+            urPwdPropByRes.purge();
+            urPwdPropByRes.retainAll(urPwdResources);
+
+            PropagationByResource<Pair<String, String>> laPwdPropByRes = new 
PropagationByResource<>();
+            laPwdPropByRes.merge(wfResult.getPropByLinkedAccount());
+            laPwdPropByRes.purge();
+            laPwdPropByRes.removeIf(p -> 
!laPwdResources.contains(p.getLeft()));
+
+            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;
@@ -561,7 +574,7 @@ public class DefaultPropagationManager implements 
PropagationManager {
                             mappingItems,
                             Pair.of(account.getConnObjectKeyValue(),
                                     
mappingManager.prepareAttrsFromLinkedAccount(
-                                            user, account, password, 
+                                            user, account, password,
                                             
changePwdRes.contains(account.getResource().getKey()),
                                             provision)));
                     tasks.add(accountTask);
@@ -628,7 +641,7 @@ public class DefaultPropagationManager implements 
PropagationManager {
     }
 
     @Transactional(readOnly = true,
-                   propagation = Propagation.REQUIRES_NEW)
+            propagation = Propagation.REQUIRES_NEW)
     @Override
     public Map<Pair<String, String>, Set<Attribute>> prepareAttrs(
             final AnyTypeKind kind,
@@ -684,7 +697,7 @@ public class DefaultPropagationManager implements 
PropagationManager {
     }
 
     @Transactional(readOnly = true,
-                   propagation = Propagation.REQUIRES_NEW)
+            propagation = Propagation.REQUIRES_NEW)
     @Override
     public Map<Pair<String, String>, Set<Attribute>> prepareAttrs(final Realm 
realm) {
         Map<Pair<String, String>, Set<Attribute>> attrs = new HashMap<>();
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 46e7800130..2a8cf9e66c 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
@@ -71,10 +71,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.LinkedAccountSamplePullCorrelationRule;
 import 
org.apache.syncope.fit.core.reference.LinkedAccountSamplePullCorrelationRuleConf;
+import org.identityconnectors.framework.common.objects.OperationalAttributes;
 import org.junit.jupiter.api.Test;
 
 public class LinkedAccountITCase extends AbstractITCase {
@@ -283,22 +285,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().get(0).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().get(0).getConnObjectKey());
+        assertEquals(ExecStatus.SUCCESS.name(), 
tasks.getResult().get(0).getLatestExecStatus());
+        PropagationData propagationData = POJOHelper.deserialize(
+                tasks.getResult().get(0).getPropagationData(), 
PropagationData.class);
+        assertTrue(propagationData.getAttributes().stream().
+                anyMatch(a -> 
OperationalAttributes.PASSWORD_NAME.equals(a.getName())));
+
+        // 5. update linked account password
         String beforeUpdatePassword = 
user.getLinkedAccounts().get(0).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().get(0).getPassword());
         assertNotEquals(beforeUpdatePassword, 
user.getLinkedAccounts().get(0).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().get(0).getConnObjectKey());
+        assertEquals(ExecStatus.SUCCESS.name(), 
tasks.getResult().get(0).getLatestExecStatus());
+        propagationData = POJOHelper.deserialize(
+                tasks.getResult().get(0).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/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java
 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java
index b142ec2c29..4ecf9ededc 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java
@@ -270,11 +270,8 @@ public class UserIssuesITCase extends AbstractITCase {
         List<PropagationStatus> propagations = result.getPropagationStatuses();
         assertNotNull(propagations);
         assertEquals(1, propagations.size());
-
         assertEquals(ExecStatus.SUCCESS, propagations.get(0).getStatus());
-
-        String resource = propagations.get(0).getResource();
-        assertEquals(RESOURCE_NAME_TESTDB, resource);
+        assertEquals(RESOURCE_NAME_TESTDB, propagations.get(0).getResource());
     }
 
     @Test
@@ -1750,7 +1747,7 @@ public class UserIssuesITCase extends AbstractITCase {
                     + "'false' WHERE USERNAME = 'rossini'");
 
             // 5. pull again rossini from resource-db-pull
-            execution = AbstractTaskITCase.execProvisioningTask(TASK_SERVICE, 
TaskType.PULL, 
+            execution = AbstractTaskITCase.execProvisioningTask(TASK_SERVICE, 
TaskType.PULL,
                     "7c2242f4-14af-4ab5-af31-cdae23783655", MAX_WAIT_SECONDS, 
false);
             assertEquals("SUCCESS", execution.getStatus());
 
@@ -1952,8 +1949,8 @@ public class UserIssuesITCase extends AbstractITCase {
         createUser(userCR);
         try {
             await().until(() -> USER_SERVICE.search(new 
AnyQuery.Builder().fiql(
-                            
SyncopeClient.getUserSearchConditionBuilder().is("email").equalTo("*issuesyncope1906*")
-                                    
.query()).size(0).page(1).build()).getTotalCount() == 5);
+                    
SyncopeClient.getUserSearchConditionBuilder().is("email").equalTo("*issuesyncope1906*")
+                            .query()).size(0).page(1).build()).getTotalCount() 
== 5);
             List<UserTO> users = USER_SERVICE.search(new 
AnyQuery.Builder().fiql(
                     
SyncopeClient.getUserSearchConditionBuilder().is("email").equalTo("*issuesyncope1906*")
                             .query()).size(10).page(1).orderBy("ctype 
DESC").build()).getResult();
@@ -1989,7 +1986,7 @@ public class UserIssuesITCase extends AbstractITCase {
 
         } finally {
             USER_SERVICE.search(new AnyQuery.Builder().fiql(
-                            
SyncopeClient.getUserSearchConditionBuilder().is("ctype").equalTo("aa*").query())
+                    
SyncopeClient.getUserSearchConditionBuilder().is("ctype").equalTo("aa*").query())
                     .size(10).page(1).orderBy("ctype 
DESC").build()).getResult().forEach(u -> deleteUser(u.getKey()));
         }
     }
diff --git a/pom.xml b/pom.xml
index ba627fb16e..e3e46d3014 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1746,7 +1746,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