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>