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>