You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by il...@apache.org on 2019/09/27 13:33:49 UTC

[syncope] branch 2_1_X updated: [SYNCOPE-957] More tests for linked accounts

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

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


The following commit(s) were added to refs/heads/2_1_X by this push:
     new e06afa6  [SYNCOPE-957] More tests for linked accounts
e06afa6 is described below

commit e06afa66657bf19b7b855ff1d6ea75cd5bd8dfaf
Author: Francesco Chicchiriccò <il...@apache.org>
AuthorDate: Fri Sep 27 14:43:26 2019 +0200

    [SYNCOPE-957] More tests for linked accounts
---
 .../jpa/dao/DefaultPushCorrelationRule.java        |   8 +-
 .../jpa/entity/user/JPALinkedAccount.java          |   2 +-
 .../core/provisioning/api/MappingManager.java      |  10 +-
 .../core/provisioning/java/MappingManagerImpl.java |  25 ++-
 .../core/provisioning/java/VirAttrHandlerImpl.java |   1 -
 .../java/data/AbstractAnyDataBinder.java           |   1 -
 .../provisioning/java/data/UserDataBinderImpl.java |   4 +-
 .../java/propagation/DeletingLinkedAccount.java    | 187 +++++++++++++++++++++
 .../java/propagation/PropagationManagerImpl.java   | 119 +++++++------
 .../core/spring/security/EncryptorTest.java        |   1 -
 .../syncope/fit/core/LinkedAccountITCase.java      | 110 +++++++++++-
 11 files changed, 385 insertions(+), 83 deletions(-)

diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/DefaultPushCorrelationRule.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/DefaultPushCorrelationRule.java
index 78cad78..129258e 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/DefaultPushCorrelationRule.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/DefaultPushCorrelationRule.java
@@ -62,7 +62,13 @@ public class DefaultPushCorrelationRule implements PushCorrelationRule {
                 item -> item.getPurpose() == MappingPurpose.PROPAGATION || item.getPurpose() == MappingPurpose.BOTH).
                 forEach(item -> {
                     Pair<String, Attribute> attr = mappingManager.prepareAttr(
-                            provision, item, any, null, AccountGetter.DEFAULT, PlainAttrGetter.DEFAULT);
+                            provision,
+                            item,
+                            any,
+                            null,
+                            AccountGetter.DEFAULT,
+                            AccountGetter.DEFAULT,
+                            PlainAttrGetter.DEFAULT);
                     if (attr != null && attr.getRight() != null && conf.getSchemas().contains(item.getIntAttrName())) {
                         filters.add(provision.isIgnoreCaseMatch()
                                 ? FilterBuilder.equalsIgnoreCase(attr.getRight())
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPALinkedAccount.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPALinkedAccount.java
index 9175c73..c24a85c 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPALinkedAccount.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPALinkedAccount.java
@@ -186,7 +186,7 @@ public class JPALinkedAccount extends AbstractGeneratedKeyEntity implements Link
     @Override
     public boolean remove(final LAPlainAttr attr) {
         checkType(attr, JPALAPlainAttr.class);
-        return getPlainAttrs().remove((JPALAPlainAttr) attr);
+        return plainAttrs.remove((JPALAPlainAttr) attr);
     }
 
     @Override
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/MappingManager.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/MappingManager.java
index e5660f7..e970512 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/MappingManager.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/MappingManager.java
@@ -63,7 +63,7 @@ public interface MappingManager {
      * @param intAttrName int attr name
      * @param schemaType schema type
      * @param any any object
-     * @param accountGetter function to get actual account instance
+     * @param usernameAccountGetter function to get actual account instance for username
      * @param plainAttrGetter function to get PlainAttr instances
      * @return attribute values and their type
      */
@@ -73,7 +73,7 @@ public interface MappingManager {
             IntAttrName intAttrName,
             AttrSchemaType schemaType,
             Any<?> any,
-            AccountGetter accountGetter,
+            AccountGetter usernameAccountGetter,
             PlainAttrGetter plainAttrGetter);
 
     /**
@@ -83,7 +83,8 @@ public interface MappingManager {
      * @param item mapping item
      * @param any given any object
      * @param password clear-text password
-     * @param accountGetter function to get actual account instance
+     * @param usernameAccountGetter function to get actual account instance for username
+     * @param passwordAccountGetter function to get actual account instance for password
      * @param plainAttrGetter function to get PlainAttr instances
      * @return connObjectLink (if it is the case) + prepared attribute
      */
@@ -91,7 +92,8 @@ public interface MappingManager {
             Provision provision,
             Item item, Any<?> any,
             String password,
-            AccountGetter accountGetter,
+            AccountGetter usernameAccountGetter,
+            AccountGetter passwordAccountGetter,
             PlainAttrGetter plainAttrGetter);
 
     /**
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/MappingManagerImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/MappingManagerImpl.java
index 950b8f0..ae90118 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/MappingManagerImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/MappingManagerImpl.java
@@ -208,6 +208,7 @@ public class MappingManagerImpl implements MappingManager {
                                 any,
                                 password,
                                 AccountGetter.DEFAULT,
+                                AccountGetter.DEFAULT,
                                 PlainAttrGetter.DEFAULT),
                         attributes);
                 if (processedConnObjectKey != null) {
@@ -270,9 +271,8 @@ public class MappingManagerImpl implements MappingManager {
                                 mapItem,
                                 user,
                                 password,
-                                acct -> account.getUsername() == null
-                                ? AccountGetter.DEFAULT.apply(acct)
-                                : account,
+                                acct -> account.getUsername() == null ? AccountGetter.DEFAULT.apply(acct) : account,
+                                acct -> account.getPassword() == null ? AccountGetter.DEFAULT.apply(acct) : account,
                                 (attributable, schema) -> {
                                     PlainAttr<?> result = null;
                                     if (attributable instanceof User) {
@@ -309,12 +309,6 @@ public class MappingManagerImpl implements MappingManager {
         if (account.isSuspended() != null) {
             attributes.add(AttributeBuilder.buildEnabled(!BooleanUtils.negate(account.isSuspended())));
         }
-        if (!changePwd) {
-            Attribute pwdAttr = AttributeUtil.find(OperationalAttributes.PASSWORD_NAME, attributes);
-            if (pwdAttr != null) {
-                attributes.remove(pwdAttr);
-            }
-        }
 
         return attributes;
     }
@@ -416,7 +410,8 @@ public class MappingManagerImpl implements MappingManager {
             final Item item,
             final Any<?> any,
             final String password,
-            final AccountGetter accountGetter,
+            final AccountGetter usernameAccountGetter,
+            final AccountGetter passwordAccountGetter,
             final PlainAttrGetter plainAttrGetter) {
 
         IntAttrName intAttrName;
@@ -435,7 +430,7 @@ public class MappingManagerImpl implements MappingManager {
                 : false;
 
         Pair<AttrSchemaType, List<PlainAttrValue>> intValues =
-                getIntValues(provision, item, intAttrName, schemaType, any, accountGetter, plainAttrGetter);
+                getIntValues(provision, item, intAttrName, schemaType, any, usernameAccountGetter, plainAttrGetter);
         schemaType = intValues.getLeft();
         List<PlainAttrValue> values = intValues.getRight();
 
@@ -473,7 +468,8 @@ public class MappingManagerImpl implements MappingManager {
             if (item.isConnObjectKey()) {
                 result = Pair.of(objValues.isEmpty() ? null : objValues.iterator().next().toString(), null);
             } else if (item.isPassword() && any instanceof User) {
-                String passwordAttrValue = getPasswordAttrValue(provision, accountGetter.apply((User) any), password);
+                String passwordAttrValue =
+                        getPasswordAttrValue(provision, passwordAccountGetter.apply((User) any), password);
                 if (passwordAttrValue == null) {
                     result = null;
                 } else {
@@ -498,7 +494,7 @@ public class MappingManagerImpl implements MappingManager {
             final IntAttrName intAttrName,
             final AttrSchemaType schemaType,
             final Any<?> any,
-            final AccountGetter accountGetter,
+            final AccountGetter usernameAccountGetter,
             final PlainAttrGetter plainAttrGetter) {
 
         LOG.debug("Get internal values for {} as '{}' on {}", any, mapItem.getIntAttrName(), provision.getResource());
@@ -581,7 +577,7 @@ public class MappingManagerImpl implements MappingManager {
 
                     case "username":
                         if (ref instanceof Account) {
-                            attrValue.setStringValue(accountGetter.apply((Account) ref).getUsername());
+                            attrValue.setStringValue(usernameAccountGetter.apply((Account) ref).getUsername());
                             values.add(attrValue);
                         }
                         break;
@@ -757,6 +753,7 @@ public class MappingManagerImpl implements MappingManager {
                     any,
                     null,
                     AccountGetter.DEFAULT,
+                    AccountGetter.DEFAULT,
                     PlainAttrGetter.DEFAULT);
         }
 
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/VirAttrHandlerImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/VirAttrHandlerImpl.java
index 2af9933..6bc8db3 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/VirAttrHandlerImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/VirAttrHandlerImpl.java
@@ -186,5 +186,4 @@ public class VirAttrHandlerImpl implements VirAttrHandler {
                 anyUtilsFactory.getInstance(any).dao().findAllowedSchemas(any, VirSchema.class).
                         getForMembership(membership.getRightEnd()));
     }
-
 }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AbstractAnyDataBinder.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AbstractAnyDataBinder.java
index 4d957c6..7560a5a 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AbstractAnyDataBinder.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AbstractAnyDataBinder.java
@@ -423,7 +423,6 @@ abstract class AbstractAnyDataBinder {
                         ((PlainAttr) attr).setOwner(any);
                         attr.setSchema(schema);
                         any.add(attr);
-
                     }
                 }
                 if (attr != null) {
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderImpl.java
index a55b48a..6342a80 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderImpl.java
@@ -581,7 +581,7 @@ public class UserDataBinderImpl extends AbstractAnyDataBinder implements UserDat
 
                 user.getLinkedAccounts().remove(account);
                 account.setOwner(null);
-                account.getPlainAttrs().forEach(attr -> {
+                account.getPlainAttrs().stream().collect(Collectors.toSet()).forEach(attr -> {
                     account.remove(attr);
                     attr.setOwner(null);
                     attr.setAccount(null);
@@ -608,7 +608,7 @@ public class UserDataBinderImpl extends AbstractAnyDataBinder implements UserDat
                     ResourceOperation.CREATE,
                     Pair.of(account.getResource().getKey(), account.getConnObjectName()));
         });
-        
+
         // finalize resource management
         reasons.entrySet().stream().
                 filter(entry -> entry.getValue().isEmpty()).
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DeletingLinkedAccount.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DeletingLinkedAccount.java
new file mode 100644
index 0000000..331aa84
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DeletingLinkedAccount.java
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.provisioning.java.propagation;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.syncope.common.lib.types.CipherAlgorithm;
+import org.apache.syncope.core.persistence.api.entity.Privilege;
+import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
+import org.apache.syncope.core.persistence.api.entity.user.LAPlainAttr;
+import org.apache.syncope.core.persistence.api.entity.user.LinkedAccount;
+import org.apache.syncope.core.persistence.api.entity.user.User;
+
+public class DeletingLinkedAccount implements LinkedAccount {
+
+    private static final long serialVersionUID = -6828106363047119713L;
+
+    private final User user;
+
+    private final ExternalResource resource;
+
+    private final String connObjectName;
+
+    public DeletingLinkedAccount(final User user, final ExternalResource resource, final String connObjectName) {
+        this.user = user;
+        this.resource = resource;
+        this.connObjectName = connObjectName;
+    }
+
+    @Override
+    public String getKey() {
+        return null;
+    }
+
+    @Override
+    public String getConnObjectName() {
+        return connObjectName;
+    }
+
+    @Override
+    public void setConnObjectName(final String connObjectName) {
+        // unsupported
+    }
+
+    @Override
+    public User getOwner() {
+        return user;
+    }
+
+    @Override
+    public void setOwner(final User owner) {
+        // unsupported
+    }
+
+    @Override
+    public ExternalResource getResource() {
+        return resource;
+    }
+
+    @Override
+    public void setResource(final ExternalResource resource) {
+        // unsupported
+    }
+
+    @Override
+    public boolean add(final Privilege privilege) {
+        return false;
+    }
+
+    @Override
+    public Set<? extends Privilege> getPrivileges() {
+        return Collections.emptySet();
+    }
+
+    @Override
+    public String getUsername() {
+        return null;
+    }
+
+    @Override
+    public void setUsername(final String username) {
+        // unsupported
+    }
+
+    @Override
+    public CipherAlgorithm getCipherAlgorithm() {
+        return null;
+    }
+
+    @Override
+    public boolean canDecodePassword() {
+        return false;
+    }
+
+    @Override
+    public String getPassword() {
+        return null;
+    }
+
+    @Override
+    public void setEncodedPassword(final String password, final CipherAlgorithm cipherAlgoritm) {
+        // unsupported
+    }
+
+    @Override
+    public void setPassword(final String password, final CipherAlgorithm cipherAlgoritm) {
+        // unsupported
+    }
+
+    @Override
+    public Boolean isSuspended() {
+        return null;
+    }
+
+    @Override
+    public void setSuspended(final Boolean suspended) {
+        //
+    }
+
+    @Override
+    public boolean add(final LAPlainAttr attr) {
+        return false;
+    }
+
+    @Override
+    public boolean remove(final LAPlainAttr attr) {
+        return false;
+    }
+
+    @Override
+    public Optional<? extends LAPlainAttr> getPlainAttr(final String plainSchema) {
+        return Optional.empty();
+    }
+
+    @Override
+    public List<? extends LAPlainAttr> getPlainAttrs() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder().
+                append(user.getKey()).
+                append(resource).
+                append(connObjectName).
+                build();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final DeletingLinkedAccount other = (DeletingLinkedAccount) obj;
+        return new EqualsBuilder().
+                append(user.getKey(), other.user.getKey()).
+                append(resource, other.resource).
+                append(connObjectName, other.connObjectName).
+                build();
+    }
+}
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PropagationManagerImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PropagationManagerImpl.java
index 4b23adf..f519640 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PropagationManagerImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PropagationManagerImpl.java
@@ -53,6 +53,7 @@ import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
 import org.apache.syncope.core.persistence.api.entity.resource.Item;
 import org.apache.syncope.core.persistence.api.entity.resource.OrgUnit;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
+import org.apache.syncope.core.persistence.api.entity.user.LinkedAccount;
 import org.apache.syncope.core.persistence.api.entity.user.User;
 import org.apache.syncope.core.provisioning.api.MappingManager;
 import org.apache.syncope.core.provisioning.api.UserWorkflowResult;
@@ -66,6 +67,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
 
 /**
  * Manage the data propagation to external resources.
@@ -223,20 +225,20 @@ public class PropagationManagerImpl implements PropagationManager {
 
         return getUpdateTasks(
                 userDAO.authFind(wfResult.getResult().getLeft().getKey()),
-                wfResult.getResult().getKey().getPassword() == null
+                wfResult.getResult().getLeft().getPassword() == null
                 ? null
-                : wfResult.getResult().getKey().getPassword().getValue(),
+                : wfResult.getResult().getLeft().getPassword().getValue(),
                 changePwd,
-                wfResult.getResult().getValue(),
+                wfResult.getResult().getRight(),
                 wfResult.getPropByRes(),
                 wfResult.getPropByLinkedAccount(),
-                wfResult.getResult().getKey().getVirAttrs(),
+                wfResult.getResult().getLeft().getVirAttrs(),
                 noPropResourceKeys);
     }
 
     @Override
     public List<PropagationTaskInfo> getUserUpdateTasks(final UserWorkflowResult<Pair<UserPatch, Boolean>> wfResult) {
-        UserPatch userPatch = wfResult.getResult().getKey();
+        UserPatch userPatch = wfResult.getResult().getLeft();
 
         // Propagate password update only to requested resources
         List<PropagationTaskInfo> tasks;
@@ -308,7 +310,7 @@ public class PropagationManagerImpl implements PropagationManager {
                 enable,
                 false,
                 propByRes == null ? new PropagationByResource<>() : propByRes,
-                null,
+                propByLinkedAccount,
                 vAttrs);
     }
 
@@ -329,11 +331,6 @@ public class PropagationManagerImpl implements PropagationManager {
             final PropagationByResource<Pair<String, String>> propByLinkedAccount,
             final Collection<String> noPropResourceKeys) {
 
-        if (noPropResourceKeys != null) {
-            propByLinkedAccount.get(ResourceOperation.DELETE).
-                    removeIf(account -> noPropResourceKeys.contains(account.getLeft()));
-        }
-
         return getDeleteTasks(userDAO.authFind(key), propByRes, propByLinkedAccount, noPropResourceKeys);
     }
 
@@ -378,32 +375,32 @@ public class PropagationManagerImpl implements PropagationManager {
             task.setEntityKey(any.getKey());
         }
         task.setOperation(operation);
-        task.setConnObjectKey(preparedAttrs.getKey());
+        task.setConnObjectKey(preparedAttrs.getLeft());
 
         // Check if any of mandatory attributes (in the mapping) is missing or not received any value: 
         // if so, add special attributes that will be evaluated by PropagationTaskExecutor
         List<String> mandatoryMissing = new ArrayList<>();
         List<String> mandatoryNullOrEmpty = new ArrayList<>();
         mappingItems.stream().filter(item -> (!item.isConnObjectKey()
-                && JexlUtils.evaluateMandatoryCondition(item.getMandatoryCondition(), any))).
-                forEach(item -> {
-                    Attribute attr = AttributeUtil.find(item.getExtAttrName(), preparedAttrs.getValue());
-                    if (attr == null) {
-                        mandatoryMissing.add(item.getExtAttrName());
-                    } else if (attr.getValue() == null || attr.getValue().isEmpty()) {
-                        mandatoryNullOrEmpty.add(item.getExtAttrName());
-                    }
-                });
+                && JexlUtils.evaluateMandatoryCondition(item.getMandatoryCondition(), any))).forEach(item -> {
+
+            Attribute attr = AttributeUtil.find(item.getExtAttrName(), preparedAttrs.getRight());
+            if (attr == null) {
+                mandatoryMissing.add(item.getExtAttrName());
+            } else if (CollectionUtils.isEmpty(attr.getValue())) {
+                mandatoryNullOrEmpty.add(item.getExtAttrName());
+            }
+        });
         if (!mandatoryMissing.isEmpty()) {
-            preparedAttrs.getValue().add(AttributeBuilder.build(
+            preparedAttrs.getRight().add(AttributeBuilder.build(
                     PropagationTaskExecutor.MANDATORY_MISSING_ATTR_NAME, mandatoryMissing));
         }
         if (!mandatoryNullOrEmpty.isEmpty()) {
-            preparedAttrs.getValue().add(AttributeBuilder.build(
+            preparedAttrs.getRight().add(AttributeBuilder.build(
                     PropagationTaskExecutor.MANDATORY_NULL_OR_EMPTY_ATTR_NAME, mandatoryNullOrEmpty));
         }
 
-        task.setAttributes(POJOHelper.serialize(preparedAttrs.getValue()));
+        task.setAttributes(POJOHelper.serialize(preparedAttrs.getRight()));
 
         return task;
     }
@@ -494,7 +491,7 @@ public class PropagationManagerImpl implements PropagationManager {
                 Pair<String, Set<Attribute>> preparedAttrs =
                         mappingManager.prepareAttrs(any, password, changePwd, enable, provision);
                 if (vAttrMap.containsKey(resourceKey)) {
-                    preparedAttrs.getValue().addAll(vAttrMap.get(resourceKey));
+                    preparedAttrs.getRight().addAll(vAttrMap.get(resourceKey));
                 }
 
                 PropagationTaskInfo task = newTask(
@@ -514,34 +511,46 @@ public class PropagationManagerImpl implements PropagationManager {
         if (any instanceof User && propByLinkedAccount != null) {
             User user = (User) any;
             propByLinkedAccount.asMap().forEach((accountInfo, operation) -> {
-                user.getLinkedAccount(accountInfo.getLeft(), accountInfo.getRight()).ifPresent(account -> {
-                    Provision provision = account.getResource().getProvision(AnyTypeKind.USER.name()).orElse(null);
-                    List<? extends Item> mappingItems = provision == null
-                            ? Collections.<Item>emptyList()
-                            : MappingUtils.getPropagationItems(provision.getMapping().getItems());
-
-                    if (provision == null) {
-                        LOG.error("No provision specified on resource {} for type {}, ignoring...",
-                                account.getResource(), AnyTypeKind.USER.name());
-                    } else if (mappingItems.isEmpty()) {
-                        LOG.warn("Requesting propagation for {} but no propagation mapping provided for {}",
-                                AnyTypeKind.USER.name(), account.getResource());
-                    } else {
-                        PropagationTaskInfo accountTask = newTask(
-                                user,
-                                account.getResource().getKey(),
-                                operation,
-                                provision,
-                                deleteOnResource,
-                                mappingItems,
-                                Pair.of(account.getConnObjectName(),
-                                        mappingManager.prepareAttrs(user, account, password, changePwd, provision)));
-                        tasks.add(accountTask);
-
-                        LOG.debug("PropagationTask created for Linked Account {}: {}",
-                                account.getConnObjectName(), accountTask);
-                    }
-                });
+                LinkedAccount account = user.getLinkedAccount(accountInfo.getLeft(), accountInfo.getRight()).
+                        orElse(null);
+                if (account == null && operation == ResourceOperation.DELETE) {
+                    account = new DeletingLinkedAccount(
+                            user, resourceDAO.find(accountInfo.getLeft()), accountInfo.getRight());
+                }
+
+                Provision provision = account == null || account.getResource() == null
+                        ? null
+                        : account.getResource().getProvision(AnyTypeKind.USER.name()).orElse(null);
+                List<? extends Item> mappingItems = provision == null
+                        ? Collections.<Item>emptyList()
+                        : MappingUtils.getPropagationItems(provision.getMapping().getItems());
+
+                if (account == null) {
+                    LOG.error("Invalid operation {} on deleted account {} on resource {}, ignoring...",
+                            operation, accountInfo.getRight(), accountInfo.getLeft());
+                } else if (account.getResource() == null) {
+                    LOG.error("Invalid resource name specified: {}, ignoring...", accountInfo.getLeft());
+                } else if (provision == null) {
+                    LOG.error("No provision specified on resource {} for type {}, ignoring...",
+                            account.getResource(), AnyTypeKind.USER.name());
+                } else if (mappingItems.isEmpty()) {
+                    LOG.warn("Requesting propagation for {} but no propagation mapping provided for {}",
+                            AnyTypeKind.USER.name(), account.getResource());
+                } else {
+                    PropagationTaskInfo accountTask = newTask(
+                            user,
+                            account.getResource().getKey(),
+                            operation,
+                            provision,
+                            deleteOnResource,
+                            mappingItems,
+                            Pair.of(account.getConnObjectName(),
+                                    mappingManager.prepareAttrs(user, account, password, changePwd, provision)));
+                    tasks.add(accountTask);
+
+                    LOG.debug("PropagationTask created for Linked Account {}: {}",
+                            account.getConnObjectName(), accountTask);
+                }
             });
         }
 
@@ -586,8 +595,8 @@ public class PropagationManagerImpl implements PropagationManager {
                 task.setOldConnObjectKey(propByRes.getOldConnObjectKey(resource.getKey()));
 
                 Pair<String, Set<Attribute>> preparedAttrs = mappingManager.prepareAttrs(realm, orgUnit);
-                task.setConnObjectKey(preparedAttrs.getKey());
-                task.setAttributes(POJOHelper.serialize(preparedAttrs.getValue()));
+                task.setConnObjectKey(preparedAttrs.getLeft());
+                task.setAttributes(POJOHelper.serialize(preparedAttrs.getRight()));
 
                 tasks.add(task);
 
diff --git a/core/spring/src/test/java/org/apache/syncope/core/spring/security/EncryptorTest.java b/core/spring/src/test/java/org/apache/syncope/core/spring/security/EncryptorTest.java
index 4ec78b9..f7fb603 100644
--- a/core/spring/src/test/java/org/apache/syncope/core/spring/security/EncryptorTest.java
+++ b/core/spring/src/test/java/org/apache/syncope/core/spring/security/EncryptorTest.java
@@ -74,7 +74,6 @@ public class EncryptorTest {
     @Test
     public void testSaltedHash() throws Exception {
         String encPassword = encryptor.encode(password, CipherAlgorithm.SSHA256);
-        // System.out.println("ENC: " + encPassword);
         assertNotNull(encPassword);
 
         assertTrue(encryptor.verify(password, CipherAlgorithm.SSHA256, encPassword));
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 8c6a7c4..57d86bc 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
@@ -20,35 +20,139 @@ package org.apache.syncope.fit.core;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.util.Optional;
 import javax.naming.NamingException;
 import javax.naming.directory.Attributes;
 import javax.naming.ldap.LdapContext;
 import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.syncope.common.lib.patch.LinkedAccountPatch;
+import org.apache.syncope.common.lib.patch.UserPatch;
 import org.apache.syncope.common.lib.to.LinkedAccountTO;
+import org.apache.syncope.common.lib.to.PagedResult;
+import org.apache.syncope.common.lib.to.PropagationTaskTO;
 import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.common.lib.types.ExecStatus;
+import org.apache.syncope.common.lib.types.PatchOperation;
+import org.apache.syncope.common.lib.types.ResourceOperation;
+import org.apache.syncope.common.lib.types.TaskType;
+import org.apache.syncope.common.rest.api.beans.TaskQuery;
 import org.apache.syncope.fit.AbstractITCase;
 import org.junit.jupiter.api.Test;
 
 public class LinkedAccountITCase extends AbstractITCase {
 
     @Test
-    public void createWithLinkedAccount() throws NamingException {
+    public void createWithLinkedAccountThenRemove() throws NamingException {
+        // 1. create user with linked account
         UserTO user = UserITCase.getSampleTO(
                 "linkedAccount" + RandomStringUtils.randomNumeric(5) + "@syncope.apache.org");
+        String connObjectName = "uid=" + user.getUsername() + ",ou=People,o=isp";
+        String privilege = applicationService.read("mightyApp").getPrivileges().get(0).getKey();
 
         LinkedAccountTO account = new LinkedAccountTO.Builder().
-                connObjectName("uid=" + user.getUsername() + ",ou=People,o=isp").
+                connObjectName(connObjectName).
                 resource(RESOURCE_NAME_LDAP).
                 build();
         account.getPlainAttrs().add(attrTO("surname", "LINKED_SURNAME"));
+        account.getPrivileges().add(privilege);
         user.getLinkedAccounts().add(account);
 
         user = createUser(user).getEntity();
         assertNotNull(user.getKey());
+        assertEquals(privilege, user.getLinkedAccounts().get(0).getPrivileges().iterator().next());
+
+        // 2. verify that propagation task was generated and that account is found on resource
+        PagedResult<PropagationTaskTO> tasks = taskService.search(
+                new TaskQuery.Builder(TaskType.PROPAGATION).resource(RESOURCE_NAME_LDAP).
+                        anyTypeKind(AnyTypeKind.USER).entityKey(user.getKey()).build());
+        assertEquals(1, tasks.getTotalCount());
+        assertEquals(connObjectName, tasks.getResult().get(0).getConnObjectKey());
+        assertEquals(ResourceOperation.CREATE, tasks.getResult().get(0).getOperation());
+        assertEquals(ExecStatus.SUCCESS.name(), tasks.getResult().get(0).getLatestExecStatus());
+
+        LdapContext ldapObj = (LdapContext) getLdapRemoteObject(
+                RESOURCE_LDAP_ADMIN_DN, RESOURCE_LDAP_ADMIN_PWD, connObjectName);
+        assertNotNull(ldapObj);
+
+        Attributes ldapAttrs = ldapObj.getAttributes("");
+        assertEquals(
+                user.getPlainAttr("email").get().getValues().get(0),
+                ldapAttrs.get("mail").getAll().next().toString());
+        assertEquals("LINKED_SURNAME", ldapAttrs.get("sn").getAll().next().toString());
+
+        // 3. remove linked account from user
+        UserPatch userPatch = new UserPatch();
+        userPatch.setKey(user.getKey());
+        userPatch.getLinkedAccounts().add(new LinkedAccountPatch.Builder().
+                operation(PatchOperation.DELETE).
+                linkedAccountTO(user.getLinkedAccounts().get(0)).build());
+
+        user = updateUser(userPatch).getEntity();
+        assertTrue(user.getLinkedAccounts().isEmpty());
+
+        // 4. verify that propagation task was generated and that account is not any more on resource
+        tasks = taskService.search(
+                new TaskQuery.Builder(TaskType.PROPAGATION).resource(RESOURCE_NAME_LDAP).
+                        anyTypeKind(AnyTypeKind.USER).entityKey(user.getKey()).build());
+        assertEquals(2, tasks.getTotalCount());
+
+        Optional<PropagationTaskTO> deletTask =
+                tasks.getResult().stream().filter(task -> task.getOperation() == ResourceOperation.DELETE).findFirst();
+        assertTrue(deletTask.isPresent());
+        assertEquals(connObjectName, deletTask.get().getConnObjectKey());
+        assertEquals(ExecStatus.SUCCESS.name(), deletTask.get().getLatestExecStatus());
+
+        assertNull(getLdapRemoteObject(RESOURCE_LDAP_ADMIN_DN, RESOURCE_LDAP_ADMIN_PWD, connObjectName));
+    }
+
+    @Test
+    public void createWithoutLinkedAccountThenAdd() throws NamingException {
+        // 1. create user without linked account
+        UserTO user = UserITCase.getSampleTO(
+                "linkedAccount" + RandomStringUtils.randomNumeric(5) + "@syncope.apache.org");
+        String connObjectName = "uid=" + user.getUsername() + ",ou=People,o=isp";
+
+        user = createUser(user).getEntity();
+        assertNotNull(user.getKey());
+        assertTrue(user.getLinkedAccounts().isEmpty());
+
+        PagedResult<PropagationTaskTO> tasks = taskService.search(
+                new TaskQuery.Builder(TaskType.PROPAGATION).resource(RESOURCE_NAME_LDAP).
+                        anyTypeKind(AnyTypeKind.USER).entityKey(user.getKey()).build());
+        assertEquals(0, tasks.getTotalCount());
+
+        assertNull(getLdapRemoteObject(RESOURCE_LDAP_ADMIN_DN, RESOURCE_LDAP_ADMIN_PWD, connObjectName));
+
+        // 2. add linked account to user
+        UserPatch userPatch = new UserPatch();
+        userPatch.setKey(user.getKey());
+
+        LinkedAccountTO account = new LinkedAccountTO.Builder().
+                connObjectName(connObjectName).
+                resource(RESOURCE_NAME_LDAP).
+                build();
+        account.getPlainAttrs().add(attrTO("surname", "LINKED_SURNAME"));
+        account.setPassword("Password123");
+        userPatch.getLinkedAccounts().add(new LinkedAccountPatch.Builder().linkedAccountTO(account).build());
+
+        user = updateUser(userPatch).getEntity();
+        assertEquals(1, user.getLinkedAccounts().size());
+
+        // 3. verify that propagation task was generated and that account is found on resource
+        tasks = taskService.search(
+                new TaskQuery.Builder(TaskType.PROPAGATION).resource(RESOURCE_NAME_LDAP).
+                        anyTypeKind(AnyTypeKind.USER).entityKey(user.getKey()).build());
+        assertEquals(1, tasks.getTotalCount());
+        assertEquals(connObjectName, tasks.getResult().get(0).getConnObjectKey());
+        assertEquals(ResourceOperation.CREATE, tasks.getResult().get(0).getOperation());
+        assertEquals(ExecStatus.SUCCESS.name(), tasks.getResult().get(0).getLatestExecStatus());
 
         LdapContext ldapObj = (LdapContext) getLdapRemoteObject(
-                RESOURCE_LDAP_ADMIN_DN, RESOURCE_LDAP_ADMIN_PWD, account.getConnObjectName());
+                RESOURCE_LDAP_ADMIN_DN, RESOURCE_LDAP_ADMIN_PWD, connObjectName);
         assertNotNull(ldapObj);
 
         Attributes ldapAttrs = ldapObj.getAttributes("");