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ò <[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 | 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>