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 75b799adfb [SYNCOPE-1750] Reactoring Account and Password policy 
enforcement (#437) (#438)
75b799adfb is described below

commit 75b799adfbe541b48376ceb90d01e2c316f0669f
Author: Francesco Chicchiriccò <[email protected]>
AuthorDate: Mon Apr 10 11:03:20 2023 +0200

    [SYNCOPE-1750] Reactoring Account and Password policy enforcement (#437) 
(#438)
---
 .../console/implementations/MyPasswordRule.groovy  |   2 +-
 .../apache/syncope/common/lib/patch/UserPatch.java |  17 +-
 .../core/persistence/api/dao/PasswordRule.java     |   2 +-
 .../syncope/core/persistence/api/dao/UserDAO.java  |   2 -
 .../core/persistence/api/entity/user/User.java     |   8 -
 .../core/persistence/jpa/dao/JPAJSONUserDAO.java   |  17 --
 .../core/persistence/jpa/dao/JPAUserDAO.java       | 241 +-----------------
 .../core/persistence/jpa/entity/user/JPAUser.java  |  45 +---
 .../core/persistence/jpa/inner/UserTest.java       |  41 +--
 .../core/persistence/jpa/outer/UserTest.java       |  79 ------
 .../java/data/ResourceDataBinderTest.java          |  24 +-
 .../provisioning/java/data/UserDataBinderTest.java | 148 +++++++++++
 .../core/spring/policy/DefaultPasswordRule.java    |   6 +-
 .../spring/policy/HaveIBeenPwnedPasswordRule.java  |  19 +-
 .../core/spring/security/TestPasswordRule.java     |   2 +-
 core/workflow-java/pom.xml                         |  63 +++++
 .../workflow/java/AbstractUserWorkflowAdapter.java | 280 ++++++++++++++++++++-
 .../workflow/java/DefaultUserWorkflowAdapter.java  |   5 -
 .../java/DefaultUserWorkflowAdapterTest.java       | 110 ++++++++
 .../core/workflow/java/DummyConnectorRegistry.java |  24 +-
 .../workflow/java/DummyImplementationLookup.java   | 101 ++++++++
 .../core/workflow/java/TestInitializer.java        |  49 ++++
 .../core/workflow/java/WorkflowTestContext.java    |  94 +++++++
 .../org.mockito.plugins.MockMaker                  |  18 ++
 .../src/test/resources/workflowTest.xml            |  39 +++
 .../flowable/impl/FlowableUserWorkflowAdapter.java |   9 +-
 .../fit/core/reference/TestPasswordRule.java       |   4 +-
 .../org/apache/syncope/fit/core/UserITCase.java    |   2 +-
 .../apache/syncope/fit/core/UserIssuesITCase.java  |  28 ++-
 pom.xml                                            |   3 +-
 30 files changed, 989 insertions(+), 493 deletions(-)

diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/implementations/MyPasswordRule.groovy
 
b/client/console/src/main/resources/org/apache/syncope/client/console/implementations/MyPasswordRule.groovy
index 0bb15dbed0..03a1595a9a 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/implementations/MyPasswordRule.groovy
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/implementations/MyPasswordRule.groovy
@@ -24,7 +24,7 @@ import 
org.apache.syncope.core.persistence.api.entity.user.User
 @CompileStatic
 class MyPasswordRule implements PasswordRule {
   
-  void enforce(User user) {
+  void enforce(User user, String clearPassword) {
   }
 
   void enforce(LinkedAccount account) {
diff --git 
a/common/lib/src/main/java/org/apache/syncope/common/lib/patch/UserPatch.java 
b/common/lib/src/main/java/org/apache/syncope/common/lib/patch/UserPatch.java
index 1e5b68031c..a14b2e598b 100644
--- 
a/common/lib/src/main/java/org/apache/syncope/common/lib/patch/UserPatch.java
+++ 
b/common/lib/src/main/java/org/apache/syncope/common/lib/patch/UserPatch.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.common.lib.patch;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
 import java.util.ArrayList;
@@ -131,14 +132,24 @@ public class UserPatch extends AnyPatch {
         return linkedAccounts;
     }
 
-    @Override
-    public boolean isEmpty() {
+    @JsonIgnore
+    protected boolean isEmptyNotConsideringPassword() {
         return super.isEmpty()
-                && username == null && password == null && securityQuestion == 
null && securityAnswer == null
+                && username == null && securityQuestion == null && 
securityAnswer == null
                 && mustChangePassword == null && relationships.isEmpty() && 
memberships.isEmpty() && roles.isEmpty()
                 && linkedAccounts.isEmpty();
     }
 
+    @JsonIgnore
+    public boolean isEmptyButPassword() {
+        return isEmptyNotConsideringPassword() && password != null;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return isEmptyNotConsideringPassword() && password == null;
+    }
+
     @Override
     public int hashCode() {
         return new HashCodeBuilder().
diff --git 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/PasswordRule.java
 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/PasswordRule.java
index 24903707e3..b8dc790347 100644
--- 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/PasswordRule.java
+++ 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/PasswordRule.java
@@ -34,7 +34,7 @@ public interface PasswordRule {
     default void setConf(PasswordRuleConf conf) {
     }
 
-    void enforce(User user);
+    void enforce(User user, String clearPassword);
 
     void enforce(LinkedAccount account);
 }
diff --git 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/UserDAO.java
 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/UserDAO.java
index 98302bfc60..bd4a54dc60 100644
--- 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/UserDAO.java
+++ 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/UserDAO.java
@@ -84,7 +84,5 @@ public interface UserDAO extends AnyDAO<User> {
 
     List<LinkedAccount> findLinkedAccountsByPrivilege(Privilege privilege);
 
-    Pair<Boolean, Boolean> enforcePolicies(User user);
-
     Pair<Set<String>, Set<String>> saveAndGetDynGroupMembs(User user);
 }
diff --git 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/user/User.java
 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/user/User.java
index 8406759ef5..2371a49bcd 100644
--- 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/user/User.java
+++ 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/entity/user/User.java
@@ -39,10 +39,6 @@ public interface User extends Account, 
GroupableRelatable<User, UMembership, UPl
 
     boolean hasTokenExpired();
 
-    String getClearPassword();
-
-    void removeClearPassword();
-
     Date getChangePwdDate();
 
     void setChangePwdDate(Date changePwdDate);
@@ -54,11 +50,7 @@ public interface User extends Account, 
GroupableRelatable<User, UMembership, UPl
     void setSecurityQuestion(SecurityQuestion securityQuestion);
 
     String getSecurityAnswer();
-    
-    String getClearSecurityAnswer();
 
-    void setEncodedSecurityAnswer(String securityAnswer);
-    
     void setSecurityAnswer(String securityAnswer);
 
     Integer getFailedLogins();
diff --git 
a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAJSONUserDAO.java
 
b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAJSONUserDAO.java
index 80daf70f65..2cc14374cb 100644
--- 
a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAJSONUserDAO.java
+++ 
b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAJSONUserDAO.java
@@ -22,7 +22,6 @@ import java.util.List;
 import java.util.Optional;
 import java.util.Set;
 import org.apache.commons.lang3.tuple.Pair;
-import 
org.apache.syncope.core.persistence.api.attrvalue.validation.InvalidEntityException;
 import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
 import org.apache.syncope.core.persistence.api.entity.user.User;
 import org.apache.syncope.core.persistence.jpa.entity.user.JPAJSONUser;
@@ -31,7 +30,6 @@ import 
org.apache.syncope.core.persistence.api.dao.JPAJSONAnyDAO;
 import org.apache.syncope.core.persistence.api.entity.DerSchema;
 import org.apache.syncope.core.persistence.api.entity.PlainAttrUniqueValue;
 import org.apache.syncope.core.persistence.api.entity.PlainSchema;
-import org.apache.syncope.core.persistence.jpa.entity.user.JPAUser;
 
 public class JPAJSONUserDAO extends JPAUserDAO {
 
@@ -75,24 +73,9 @@ public class JPAJSONUserDAO extends JPAUserDAO {
 
     @Override
     protected Pair<User, Pair<Set<String>, Set<String>>> doSave(final User 
user) {
-        // 1. save clear password value before save
-        String clearPwd = user.getClearPassword();
-
-        // 2. save
         entityManager().flush();
         User merged = entityManager().merge(user);
 
-        // 3. set back the sole clear password value
-        JPAUser.class.cast(merged).setClearPassword(clearPwd);
-
-        // 4. enforce password and account policies
-        try {
-            enforcePolicies(merged);
-        } catch (InvalidEntityException e) {
-            entityManager().remove(merged);
-            throw e;
-        }
-
         // ensure that entity listeners are invoked at this point
         entityManager().flush();
 
diff --git 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAUserDAO.java
 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAUserDAO.java
index 2ca5f2553b..786276e234 100644
--- 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAUserDAO.java
+++ 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAUserDAO.java
@@ -27,42 +27,27 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.stream.Collectors;
 import javax.annotation.Resource;
 import javax.persistence.NoResultException;
-import javax.persistence.PersistenceException;
 import javax.persistence.Query;
 import javax.persistence.TypedQuery;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
-import org.apache.syncope.common.lib.types.EntityViolationType;
 import org.apache.syncope.common.lib.types.StandardEntitlement;
-import org.apache.syncope.core.spring.policy.AccountPolicyException;
-import org.apache.syncope.core.spring.policy.PasswordPolicyException;
-import org.apache.syncope.core.spring.security.AuthContextUtils;
-import 
org.apache.syncope.core.spring.security.DelegatedAdministrationException;
-import 
org.apache.syncope.core.persistence.api.attrvalue.validation.InvalidEntityException;
 import org.apache.syncope.core.persistence.api.dao.AccessTokenDAO;
-import org.apache.syncope.core.persistence.api.dao.AccountRule;
 import org.apache.syncope.core.persistence.api.dao.DelegationDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
-import org.apache.syncope.core.persistence.api.dao.PasswordRule;
-import org.apache.syncope.core.persistence.api.dao.RealmDAO;
 import org.apache.syncope.core.persistence.api.dao.RoleDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.entity.AccessToken;
 import org.apache.syncope.core.persistence.api.entity.AnyUtils;
 import org.apache.syncope.core.persistence.api.entity.Delegation;
-import org.apache.syncope.core.persistence.api.entity.Entity;
-import org.apache.syncope.core.persistence.api.entity.Implementation;
 import org.apache.syncope.core.persistence.api.entity.Membership;
 import org.apache.syncope.core.persistence.api.entity.Privilege;
 import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.apache.syncope.core.persistence.api.entity.Role;
 import org.apache.syncope.core.persistence.api.entity.group.Group;
-import org.apache.syncope.core.persistence.api.entity.policy.AccountPolicy;
-import org.apache.syncope.core.persistence.api.entity.policy.PasswordPolicy;
 import 
org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
 import org.apache.syncope.core.persistence.api.entity.user.LinkedAccount;
 import org.apache.syncope.core.persistence.api.entity.user.SecurityQuestion;
@@ -72,41 +57,29 @@ import 
org.apache.syncope.core.persistence.jpa.entity.user.JPALinkedAccount;
 import org.apache.syncope.core.persistence.jpa.entity.user.JPAUMembership;
 import org.apache.syncope.core.persistence.jpa.entity.user.JPAUser;
 import org.apache.syncope.core.provisioning.api.utils.RealmUtils;
-import org.apache.syncope.core.spring.implementation.ImplementationManager;
-import org.apache.syncope.core.spring.security.Encryptor;
+import org.apache.syncope.core.spring.security.AuthContextUtils;
+import 
org.apache.syncope.core.spring.security.DelegatedAdministrationException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 
 public class JPAUserDAO extends AbstractAnyDAO<User> implements UserDAO {
 
-    protected static final Encryptor ENCRYPTOR = Encryptor.getInstance();
-
     @Autowired
     protected RoleDAO roleDAO;
 
     @Autowired
     protected AccessTokenDAO accessTokenDAO;
 
-    @Autowired
-    protected RealmDAO realmDAO;
-
     @Autowired
     protected GroupDAO groupDAO;
 
     @Autowired
     protected DelegationDAO delegationDAO;
 
-    @Resource(name = "adminUser")
-    protected String adminUser;
-
     @Resource(name = "anonymousUser")
     protected String anonymousUser;
 
-    protected final Map<String, AccountRule> perContextAccountRules = new 
ConcurrentHashMap<>();
-
-    protected final Map<String, PasswordRule> perContextPasswordRules = new 
ConcurrentHashMap<>();
-
     @Override
     protected AnyUtils init() {
         return anyUtilsFactory.getInstance(AnyTypeKind.USER);
@@ -261,30 +234,6 @@ public class JPAUserDAO extends AbstractAnyDAO<User> 
implements UserDAO {
         return entityManager().find(JPAUMembership.class, key);
     }
 
-    protected List<PasswordPolicy> getPasswordPolicies(final User user) {
-        List<PasswordPolicy> policies = new ArrayList<>();
-
-        PasswordPolicy policy;
-
-        // add resource policies
-        for (ExternalResource resource : findAllResources(user)) {
-            policy = resource.getPasswordPolicy();
-            if (policy != null) {
-                policies.add(policy);
-            }
-        }
-
-        // add realm policies
-        for (Realm realm : realmDAO.findAncestors(user.getRealm())) {
-            policy = realm.getPasswordPolicy();
-            if (policy != null) {
-                policies.add(policy);
-            }
-        }
-
-        return policies;
-    }
-
     @Override
     public List<User> findAll(final int page, final int itemsPerPage) {
         TypedQuery<User> query = entityManager().createQuery(
@@ -300,194 +249,8 @@ public class JPAUserDAO extends AbstractAnyDAO<User> 
implements UserDAO {
         return findAllKeys(JPAUser.TABLE, page, itemsPerPage);
     }
 
-    protected List<AccountPolicy> getAccountPolicies(final User user) {
-        List<AccountPolicy> policies = new ArrayList<>();
-
-        // add resource policies
-        findAllResources(user).stream().
-                map(resource -> resource.getAccountPolicy()).
-                filter(policy -> policy != null).
-                forEach(policy -> policies.add(policy));
-
-        // add realm policies
-        realmDAO.findAncestors(user.getRealm()).stream().
-                map(realm -> realm.getAccountPolicy()).
-                filter(policy -> policy != null).
-                forEach(policy -> policies.add(policy));
-
-        return policies;
-    }
-
-    protected List<AccountRule> getAccountRules(final AccountPolicy policy) {
-        List<AccountRule> result = new ArrayList<>();
-
-        for (Implementation impl : policy.getRules()) {
-            try {
-                ImplementationManager.buildAccountRule(
-                        impl,
-                        () -> perContextAccountRules.get(impl.getKey()),
-                        instance -> perContextAccountRules.put(impl.getKey(), 
instance)).
-                        ifPresent(result::add);
-            } catch (Exception e) {
-                LOG.warn("While building {}", impl, e);
-            }
-        }
-
-        return result;
-    }
-
-    protected List<PasswordRule> getPasswordRules(final PasswordPolicy policy) 
{
-        List<PasswordRule> result = new ArrayList<>();
-
-        for (Implementation impl : policy.getRules()) {
-            try {
-                ImplementationManager.buildPasswordRule(
-                        impl,
-                        () -> perContextPasswordRules.get(impl.getKey()),
-                        instance -> perContextPasswordRules.put(impl.getKey(), 
instance)).
-                        ifPresent(result::add);
-            } catch (Exception e) {
-                LOG.warn("While building {}", impl, e);
-            }
-        }
-
-        return result;
-    }
-
-    @Transactional(readOnly = true)
-    @Override
-    public Pair<Boolean, Boolean> enforcePolicies(final User user) {
-        // ------------------------------
-        // Verify password policies
-        // ------------------------------
-        LOG.debug("Password Policy enforcement");
-
-        try {
-            int maxPPSpecHistory = 0;
-            for (PasswordPolicy policy : getPasswordPolicies(user)) {
-                if (user.getPassword() == null && 
!policy.isAllowNullPassword()) {
-                    throw new PasswordPolicyException("Password mandatory");
-                }
-
-                getPasswordRules(policy).forEach(rule -> {
-                    rule.enforce(user);
-
-                    user.getLinkedAccounts().stream().
-                            filter(account -> account.getPassword() != null).
-                            forEach(rule::enforce);
-                });
-
-                boolean matching = false;
-                if (policy.getHistoryLength() > 0) {
-                    List<String> pwdHistory = user.getPasswordHistory();
-                    matching = pwdHistory.subList(policy.getHistoryLength() >= 
pwdHistory.size()
-                            ? 0
-                            : pwdHistory.size() - policy.getHistoryLength(), 
pwdHistory.size()).stream().
-                            map(old -> 
ENCRYPTOR.verify(user.getClearPassword(), user.getCipherAlgorithm(), old)).
-                            reduce(matching, (accumulator, item) -> 
accumulator | item);
-                }
-                if (matching) {
-                    throw new PasswordPolicyException("Password value was used 
in the past: not allowed");
-                }
-
-                if (policy.getHistoryLength() > maxPPSpecHistory) {
-                    maxPPSpecHistory = policy.getHistoryLength();
-                }
-            }
-
-            // update user's password history with encrypted password
-            if (maxPPSpecHistory > 0 && user.getPassword() != null
-                    && 
!user.getPasswordHistory().contains(user.getPassword())) {
-
-                user.getPasswordHistory().add(user.getPassword());
-            }
-            // keep only the last maxPPSpecHistory items in user's password 
history
-            if (maxPPSpecHistory < user.getPasswordHistory().size()) {
-                for (int i = 0; i < user.getPasswordHistory().size() - 
maxPPSpecHistory; i++) {
-                    user.getPasswordHistory().remove(i);
-                }
-            }
-        } catch (PersistenceException | InvalidEntityException e) {
-            throw e;
-        } catch (Exception e) {
-            LOG.error("Invalid password for {}", user, e);
-            throw new InvalidEntityException(User.class, 
EntityViolationType.InvalidPassword, e.getMessage());
-        } finally {
-            // password has been validated, let's remove its clear version
-            user.removeClearPassword();
-        }
-
-        // ------------------------------
-        // Verify account policies
-        // ------------------------------
-        LOG.debug("Account Policy enforcement");
-
-        boolean suspend = false;
-        boolean propagateSuspension = false;
-        try {
-            if (user.getUsername() == null) {
-                throw new AccountPolicyException("Null username");
-            }
-
-            if (adminUser.equals(user.getUsername()) || 
anonymousUser.equals(user.getUsername())) {
-                throw new AccountPolicyException("Not allowed: " + 
user.getUsername());
-            }
-
-            List<AccountPolicy> accountPolicies = getAccountPolicies(user);
-            if (accountPolicies.isEmpty()) {
-                if (!Entity.ID_PATTERN.matcher(user.getUsername()).matches()) {
-                    throw new AccountPolicyException("Character(s) not 
allowed: " + user.getUsername());
-                }
-                user.getLinkedAccounts().stream().
-                        filter(account -> account.getUsername() != null).
-                        forEach(account -> {
-                            if 
(!Entity.ID_PATTERN.matcher(account.getUsername()).matches()) {
-                                throw new AccountPolicyException("Character(s) 
not allowed: " + account.getUsername());
-                            }
-                        });
-            } else {
-                for (AccountPolicy policy : accountPolicies) {
-                    getAccountRules(policy).forEach(rule -> {
-                        rule.enforce(user);
-
-                        user.getLinkedAccounts().stream().
-                                filter(account -> account.getUsername() != 
null).
-                                forEach(rule::enforce);
-                    });
-
-                    suspend |= user.getFailedLogins() != null && 
policy.getMaxAuthenticationAttempts() > 0
-                            && user.getFailedLogins() > 
policy.getMaxAuthenticationAttempts() && !user.isSuspended();
-                    propagateSuspension |= policy.isPropagateSuspension();
-                }
-            }
-        } catch (PersistenceException | InvalidEntityException e) {
-            throw e;
-        } catch (Exception e) {
-            LOG.error("Invalid username for {}", user, e);
-            throw new InvalidEntityException(User.class, 
EntityViolationType.InvalidUsername, e.getMessage());
-        }
-
-        return Pair.of(suspend, propagateSuspension);
-    }
-
     protected Pair<User, Pair<Set<String>, Set<String>>> doSave(final User 
user) {
-        // 1. save clear password value before save
-        String clearPwd = user.getClearPassword();
-
-        // 2. save
         User merged = super.save(user);
-
-        // 3. set back the sole clear password value
-        JPAUser.class.cast(merged).setClearPassword(clearPwd);
-
-        // 4. enforce password and account policies
-        try {
-            enforcePolicies(merged);
-        } catch (InvalidEntityException e) {
-            entityManager().remove(merged);
-            throw e;
-        }
-
         roleDAO.refreshDynMemberships(merged);
         Pair<Set<String>, Set<String>> dynGroupMembs = 
groupDAO.refreshDynMemberships(merged);
         dynRealmDAO.refreshDynMemberships(merged);
diff --git 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPAUser.java
 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPAUser.java
index 1e59265186..8b56a2fc42 100644
--- 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPAUser.java
+++ 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/user/JPAUser.java
@@ -42,7 +42,6 @@ import javax.persistence.OneToMany;
 import javax.persistence.Table;
 import javax.persistence.Temporal;
 import javax.persistence.TemporalType;
-import javax.persistence.Transient;
 import javax.persistence.UniqueConstraint;
 import javax.validation.Valid;
 import javax.validation.constraints.NotNull;
@@ -85,9 +84,6 @@ public class JPAUser
     @Column(nullable = true)
     private String password;
 
-    @Transient
-    private String clearPassword;
-
     @ManyToMany(fetch = FetchType.EAGER)
     @JoinTable(joinColumns =
             @JoinColumn(name = "user_id"),
@@ -186,9 +182,6 @@ public class JPAUser
     @Column(nullable = true)
     private String securityAnswer;
 
-    @Transient
-    private String clearSecurityAnswer;
-    
     @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = 
"owner")
     @Valid
     private List<JPALinkedAccount> linkedAccounts = new ArrayList<>();
@@ -230,24 +223,8 @@ public class JPAUser
         return password;
     }
 
-    @Override
-    public String getClearPassword() {
-        return clearPassword;
-    }
-
-    public void setClearPassword(final String clearPassword) {
-        this.clearPassword = clearPassword;
-    }
-
-    @Override
-    public void removeClearPassword() {
-        setClearPassword(null);
-    }
-
     @Override
     public void setEncodedPassword(final String password, final 
CipherAlgorithm cipherAlgorithm) {
-        this.clearPassword = null;
-
         this.password = password;
         this.cipherAlgorithm = cipherAlgorithm;
         setMustChangePassword(false);
@@ -255,12 +232,10 @@ public class JPAUser
 
     @Override
     public void setPassword(final String password) {
-        this.clearPassword = password;
-
         try {
             this.password = ENCRYPTOR.encode(password, cipherAlgorithm == null
                     ? 
CipherAlgorithm.valueOf(ApplicationContextProvider.getBeanFactory().getBean(ConfDAO.class).
-                    find("password.cipher.algorithm", 
CipherAlgorithm.AES.name()))
+                            find("password.cipher.algorithm", 
CipherAlgorithm.AES.name()))
                     : cipherAlgorithm);
             setMustChangePassword(false);
         } catch (Exception e) {
@@ -282,7 +257,7 @@ public class JPAUser
             throw new IllegalArgumentException("Cannot override existing 
cipher algorithm");
         }
     }
-    
+
     @Override
     public boolean canDecodeSecrets() {
         return this.cipherAlgorithm != null && 
this.cipherAlgorithm.isInvertible();
@@ -439,26 +414,12 @@ public class JPAUser
         return securityAnswer;
     }
 
-    @Override
-    public String getClearSecurityAnswer() {
-        return clearSecurityAnswer;
-    }
-
-    @Override
-    public void setEncodedSecurityAnswer(final String securityAnswer) {
-        this.clearSecurityAnswer = null;
-
-        this.securityAnswer = securityAnswer;
-    }
-
     @Override
     public void setSecurityAnswer(final String securityAnswer) {
-        this.securityAnswer = securityAnswer;
-
         try {
             this.securityAnswer = ENCRYPTOR.encode(securityAnswer, 
cipherAlgorithm == null
                     ? 
CipherAlgorithm.valueOf(ApplicationContextProvider.getBeanFactory().getBean(ConfDAO.class).
-                    find("password.cipher.algorithm", 
CipherAlgorithm.AES.name()))
+                            find("password.cipher.algorithm", 
CipherAlgorithm.AES.name()))
                     : cipherAlgorithm);
         } catch (Exception e) {
             LOG.error("Could not encode security answer", e);
diff --git 
a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/UserTest.java
 
b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/UserTest.java
index 2b7e6e19ac..8fcb05ab03 100644
--- 
a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/UserTest.java
+++ 
b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/UserTest.java
@@ -30,7 +30,6 @@ import java.util.Date;
 import java.util.List;
 import java.util.Optional;
 import org.apache.syncope.common.lib.types.CipherAlgorithm;
-import 
org.apache.syncope.core.persistence.api.attrvalue.validation.InvalidEntityException;
 import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO;
 import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
 import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
@@ -201,42 +200,6 @@ public class UserTest extends AbstractTest {
         assertEquals("1417acbe-cbf6-4277-9372-e75e04f97000", 
memb.getLeftEnd().getKey());
     }
 
-    @Test
-    public void saveInvalidPassword() {
-        User user = entityFactory.newEntity(User.class);
-        user.setUsername("username");
-        user.setRealm(realmDAO.findByFullPath("/even/two"));
-        user.setCreator("admin");
-        user.setCreationDate(new Date());
-        user.setCipherAlgorithm(CipherAlgorithm.SHA256);
-        user.setPassword("pass");
-
-        try {
-            userDAO.save(user);
-            fail("This should not happen");
-        } catch (InvalidEntityException e) {
-            assertNotNull(e);
-        }
-    }
-
-    @Test
-    public void saveInvalidUsername() {
-        User user = entityFactory.newEntity(User.class);
-        user.setUsername("username!");
-        user.setRealm(realmDAO.findByFullPath("/even/two"));
-        user.setCreator("admin");
-        user.setCreationDate(new Date());
-        user.setCipherAlgorithm(CipherAlgorithm.SHA256);
-        user.setPassword("password123");
-
-        try {
-            userDAO.save(user);
-            fail("This should not happen");
-        } catch (InvalidEntityException e) {
-            assertNotNull(e);
-        }
-    }
-
     @Test
     public void save() {
         User user = entityFactory.newEntity(User.class);
@@ -249,7 +212,9 @@ public class UserTest extends AbstractTest {
 
         User actual = userDAO.save(user);
         assertNotNull(actual);
-        assertEquals(1, actual.getPasswordHistory().size());
+
+        entityManager().flush();
+
         assertNotNull(userDAO.findLastChange(actual.getKey()));
         assertEquals(actual.getLastChangeDate(), 
userDAO.findLastChange(actual.getKey()));
     }
diff --git 
a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/UserTest.java
 
b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/UserTest.java
index 4e436ead63..ea249155bd 100644
--- 
a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/UserTest.java
+++ 
b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/outer/UserTest.java
@@ -23,7 +23,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
 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 static org.junit.jupiter.api.Assertions.fail;
 
 import java.util.Collections;
 import java.util.Date;
@@ -32,7 +31,6 @@ import java.util.Objects;
 import java.util.UUID;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.CipherAlgorithm;
-import 
org.apache.syncope.core.persistence.api.attrvalue.validation.InvalidEntityException;
 import 
org.apache.syncope.core.persistence.api.attrvalue.validation.PlainAttrValidationManager;
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.ApplicationDAO;
@@ -168,83 +166,6 @@ public class UserTest extends AbstractTest {
                 user.getRelationships().get(0).getRightEnd().getKey());
     }
 
-    @Test
-    public void membershipWithAttrNotAllowed() {
-        User user = userDAO.findByUsername("vivaldi");
-        assertNotNull(user);
-        user.getMemberships().clear();
-
-        // add 'obscure' to user (no membership): works because 'obscure' is 
from 'other', default class for USER
-        UPlainAttr attr = entityFactory.newEntity(UPlainAttr.class);
-        attr.setOwner(user);
-        attr.setSchema(plainSchemaDAO.find("obscure"));
-        attr.add(validator, "testvalue", 
anyUtilsFactory.getInstance(AnyTypeKind.USER));
-        user.add(attr);
-
-        // add 'obscure' to user (via 'artDirector' membership): does not work 
because 'obscure' is from 'other'
-        // but 'artDirector' defines no type extension
-        UMembership membership = entityFactory.newEntity(UMembership.class);
-        membership.setLeftEnd(user);
-        membership.setRightEnd(groupDAO.findByName("artDirector"));
-        user.add(membership);
-
-        attr = entityFactory.newEntity(UPlainAttr.class);
-        attr.setOwner(user);
-        attr.setMembership(membership);
-        attr.setSchema(plainSchemaDAO.find("obscure"));
-        attr.add(validator, "testvalue2", 
anyUtilsFactory.getInstance(AnyTypeKind.USER));
-        user.add(attr);
-
-        try {
-            userDAO.save(user);
-            fail("This should not happen");
-        } catch (InvalidEntityException e) {
-            assertNotNull(e);
-        }
-    }
-
-    @Test
-    public void membershipWithAttr() {
-        User user = userDAO.findByUsername("vivaldi");
-        assertNotNull(user);
-        user.getMemberships().clear();
-
-        // add 'obscure' (no membership): works because 'obscure' is from 
'other', default class for USER
-        UPlainAttr attr = entityFactory.newEntity(UPlainAttr.class);
-        attr.setOwner(user);
-        attr.setSchema(plainSchemaDAO.find("obscure"));
-        attr.add(validator, "testvalue", 
anyUtilsFactory.getInstance(AnyTypeKind.USER));
-        user.add(attr);
-
-        // add 'obscure' (via 'additional' membership): that group defines 
type extension with classes 'other' and 'csv'
-        UMembership membership = entityFactory.newEntity(UMembership.class);
-        membership.setLeftEnd(user);
-        membership.setRightEnd(groupDAO.findByName("additional"));
-        user.add(membership);
-
-        attr = entityFactory.newEntity(UPlainAttr.class);
-        attr.setOwner(user);
-        attr.setMembership(membership);
-        attr.setSchema(plainSchemaDAO.find("obscure"));
-        attr.add(validator, "testvalue2", 
anyUtilsFactory.getInstance(AnyTypeKind.USER));
-        user.add(attr);
-
-        userDAO.save(user);
-        entityManager().flush();
-
-        user = userDAO.findByUsername("vivaldi");
-        assertEquals(1, user.getMemberships().size());
-
-        UMembership newM = 
user.getMembership(groupDAO.findByName("additional").getKey()).get();
-        assertEquals(1, user.getPlainAttrs(newM).size());
-
-        assertNull(user.getPlainAttr("obscure").get().getMembership());
-        assertEquals(2, user.getPlainAttrs("obscure").size());
-        
assertTrue(user.getPlainAttrs("obscure").contains(user.getPlainAttr("obscure").get()));
-        assertTrue(user.getPlainAttrs("obscure").stream().anyMatch(plainAttr 
-> plainAttr.getMembership() == null));
-        assertTrue(user.getPlainAttrs("obscure").stream().anyMatch(plainAttr 
-> newM.equals(plainAttr.getMembership())));
-    }
-
     private LinkedAccount newLinkedAccount(final String connObjectKeyValue) {
         User user = userDAO.findByUsername("vivaldi");
         
user.getLinkedAccounts().stream().filter(Objects::nonNull).forEach(account -> 
account.setOwner(null));
diff --git 
a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderTest.java
 
b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderTest.java
index 935ea20e12..8ec7842a65 100644
--- 
a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderTest.java
+++ 
b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderTest.java
@@ -56,18 +56,6 @@ import 
org.springframework.transaction.annotation.Transactional;
 @Transactional("Master")
 public class ResourceDataBinderTest extends AbstractTest {
 
-    @Autowired
-    private AnyTypeDAO anyTypeDAO;
-
-    @Autowired
-    private ExternalResourceDAO resourceDAO;
-
-    @Autowired
-    private ResourceDataBinder resourceDataBinder;
-
-    @Autowired
-    private PlainSchemaDAO plainSchemaDAO;
-
     @BeforeAll
     public static void setAuthContext() {
         List<GrantedAuthority> authorities = 
StandardEntitlement.values().stream().
@@ -86,6 +74,18 @@ public class ResourceDataBinderTest extends AbstractTest {
         SecurityContextHolder.getContext().setAuthentication(null);
     }
 
+    @Autowired
+    private AnyTypeDAO anyTypeDAO;
+
+    @Autowired
+    private ExternalResourceDAO resourceDAO;
+
+    @Autowired
+    private ResourceDataBinder resourceDataBinder;
+
+    @Autowired
+    private PlainSchemaDAO plainSchemaDAO;
+
     @Test
     public void issue42() {
         PlainSchema userId = plainSchemaDAO.find("userId");
diff --git 
a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderTest.java
 
b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderTest.java
new file mode 100644
index 0000000000..9da27e79a3
--- /dev/null
+++ 
b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.patch.AttrPatch;
+import org.apache.syncope.common.lib.patch.MembershipPatch;
+import org.apache.syncope.common.lib.patch.UserPatch;
+import org.apache.syncope.common.lib.to.AttrTO;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.lib.types.StandardEntitlement;
+import 
org.apache.syncope.core.persistence.api.attrvalue.validation.InvalidEntityException;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.user.UMembership;
+import org.apache.syncope.core.persistence.api.entity.user.User;
+import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
+import org.apache.syncope.core.provisioning.java.AbstractTest;
+import org.apache.syncope.core.spring.security.SyncopeAuthenticationDetails;
+import org.apache.syncope.core.spring.security.SyncopeGrantedAuthority;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import 
org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.transaction.annotation.Transactional;
+
+@Transactional("Master")
+public class UserDataBinderTest extends AbstractTest {
+
+    @BeforeAll
+    public static void setAuthContext() {
+        List<GrantedAuthority> authorities = 
StandardEntitlement.values().stream().
+                map(entitlement -> new SyncopeGrantedAuthority(entitlement, 
SyncopeConstants.ROOT_REALM)).
+                collect(Collectors.toList());
+
+        UsernamePasswordAuthenticationToken auth = new 
UsernamePasswordAuthenticationToken(
+                new org.springframework.security.core.userdetails.User(
+                        "admin", "FAKE_PASSWORD", authorities), 
"FAKE_PASSWORD", authorities);
+        auth.setDetails(new 
SyncopeAuthenticationDetails(SyncopeConstants.MASTER_DOMAIN, null));
+        SecurityContextHolder.getContext().setAuthentication(auth);
+    }
+
+    @AfterAll
+    public static void unsetAuthContext() {
+        SecurityContextHolder.getContext().setAuthentication(null);
+    }
+
+    @Autowired
+    private UserDataBinder dataBinder;
+
+    @Autowired
+    private UserDAO userDAO;
+
+    @Autowired
+    private EntityFactory entityFactory;
+
+    private String userKey;
+
+    @BeforeEach
+    public void createUser() {
+        UserTO userTO = new UserTO();
+        userTO.setRealm("/even");
+        userTO.setUsername("test");
+        userTO.setPassword("password123");
+        userTO.getPlainAttrs().add(new 
AttrTO.Builder().schema("surname").value("test").build());
+        userTO.getPlainAttrs().add(new 
AttrTO.Builder().schema("fullname").value("test").build());
+        userTO.getPlainAttrs().add(new 
AttrTO.Builder().schema("userId").value("[email protected]").build());
+
+        User user = entityFactory.newEntity(User.class);
+
+        dataBinder.create(user, userTO, true);
+        user = userDAO.save(user);
+
+        userKey = user.getKey();
+    }
+
+    @Test
+    public void membershipWithAttrNotAllowed() {
+        UserPatch patch = new UserPatch();
+        patch.setKey(userKey);
+
+        // add 'obscure' to user (no membership): works because 'obscure' is 
from 'other', default class for USER
+        patch.getPlainAttrs().add(new AttrPatch.Builder().
+                attrTO(new 
AttrTO.Builder().schema("obscure").value("testvalue").build()).build());
+
+        // add 'obscure' to user (via 'artDirector' membership): does not work 
because 'obscure' is from 'other'
+        // but 'artDirector' defines no type extension
+        MembershipPatch mp = new 
MembershipPatch.Builder().group("ece66293-8f31-4a84-8e8d-23da36e70846").build();
+        mp.getPlainAttrs().add(new 
AttrTO.Builder().schema("obscure").value("testvalue2").build());
+        patch.getMemberships().add(mp);
+
+        assertThrows(InvalidEntityException.class, () -> 
dataBinder.update(userDAO.find(patch.getKey()), patch));
+    }
+
+    @Test
+    public void membershipWithAttr() {
+        UserPatch patch = new UserPatch();
+        patch.setKey(userKey);
+
+        // add 'obscure' (no membership): works because 'obscure' is from 
'other', default class for USER
+        patch.getPlainAttrs().add(new AttrPatch.Builder().
+                attrTO(new 
AttrTO.Builder().schema("obscure").value("testvalue").build()).build());
+
+        // add 'obscure' (via 'additional' membership): that group defines 
type extension with classes 'other' and 'csv'
+        MembershipPatch mp = new 
MembershipPatch.Builder().group("034740a9-fa10-453b-af37-dc7897e98fb1").build();
+        mp.getPlainAttrs().add(new 
AttrTO.Builder().schema("obscure").value("testvalue2").build());
+        patch.getMemberships().add(mp);
+
+        dataBinder.update(userDAO.find(patch.getKey()), patch);
+
+        User user = userDAO.find(patch.getKey());
+        UMembership newM = 
user.getMembership("034740a9-fa10-453b-af37-dc7897e98fb1").get();
+        assertEquals(1, user.getPlainAttrs(newM).size());
+
+        assertNull(user.getPlainAttr("obscure").get().getMembership());
+        assertEquals(2, user.getPlainAttrs("obscure").size());
+        
assertTrue(user.getPlainAttrs("obscure").contains(user.getPlainAttr("obscure").get()));
+        assertTrue(user.getPlainAttrs("obscure").stream().anyMatch(a -> 
a.getMembership() == null));
+        assertTrue(user.getPlainAttrs("obscure").stream().anyMatch(a -> 
newM.equals(a.getMembership())));
+    }
+}
diff --git 
a/core/spring/src/main/java/org/apache/syncope/core/spring/policy/DefaultPasswordRule.java
 
b/core/spring/src/main/java/org/apache/syncope/core/spring/policy/DefaultPasswordRule.java
index cd8d60fc4c..78e5a153de 100644
--- 
a/core/spring/src/main/java/org/apache/syncope/core/spring/policy/DefaultPasswordRule.java
+++ 
b/core/spring/src/main/java/org/apache/syncope/core/spring/policy/DefaultPasswordRule.java
@@ -177,8 +177,8 @@ public class DefaultPasswordRule implements PasswordRule {
 
     @Transactional(readOnly = true)
     @Override
-    public void enforce(final User user) {
-        if (user.getPassword() != null && user.getClearPassword() != null) {
+    public void enforce(final User user, final String clearPassword) {
+        if (clearPassword != null) {
             Set<String> wordsNotPermitted = new 
HashSet<>(conf.getWordsNotPermitted());
             wordsNotPermitted.addAll(
                     conf.getSchemasNotPermitted().stream().
@@ -189,7 +189,7 @@ public class DefaultPasswordRule implements PasswordRule {
                             flatMap(Collection::stream).
                             collect(Collectors.toSet()));
 
-            enforce(user.getClearPassword(), user.getUsername(), 
wordsNotPermitted);
+            enforce(clearPassword, user.getUsername(), wordsNotPermitted);
         }
     }
 
diff --git 
a/core/spring/src/main/java/org/apache/syncope/core/spring/policy/HaveIBeenPwnedPasswordRule.java
 
b/core/spring/src/main/java/org/apache/syncope/core/spring/policy/HaveIBeenPwnedPasswordRule.java
index a843b0cce5..a3d84aeeb6 100644
--- 
a/core/spring/src/main/java/org/apache/syncope/core/spring/policy/HaveIBeenPwnedPasswordRule.java
+++ 
b/core/spring/src/main/java/org/apache/syncope/core/spring/policy/HaveIBeenPwnedPasswordRule.java
@@ -22,6 +22,7 @@ import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
+import java.util.Optional;
 import java.util.stream.Stream;
 import javax.crypto.BadPaddingException;
 import javax.crypto.IllegalBlockSizeException;
@@ -69,9 +70,9 @@ public class HaveIBeenPwnedPasswordRule implements 
PasswordRule {
         }
     }
 
-    protected void enforce(final String clear) {
+    protected void enforce(final String clearPassword) {
         try {
-            String sha1 = ENCRYPTOR.encode(clear, CipherAlgorithm.SHA1);
+            String sha1 = ENCRYPTOR.encode(clearPassword, 
CipherAlgorithm.SHA1);
 
             HttpHeaders headers = new HttpHeaders();
             headers.set(HttpHeaders.USER_AGENT, "Apache Syncope");
@@ -98,27 +99,25 @@ public class HaveIBeenPwnedPasswordRule implements 
PasswordRule {
 
     @Transactional(readOnly = true)
     @Override
-    public void enforce(final User user) {
-        if (user.getPassword() != null && user.getClearPassword() != null) {
-            enforce(user.getClearPassword());
-        }
+    public void enforce(final User user, final String clearPassword) {
+        Optional.ofNullable(clearPassword).ifPresent(this::enforce);
     }
 
     @Transactional(readOnly = true)
     @Override
     public void enforce(final LinkedAccount account) {
         if (account.getPassword() != null) {
-            String clear = null;
+            String clearPassword = null;
             if (account.canDecodeSecrets()) {
                 try {
-                    clear = ENCRYPTOR.decode(account.getPassword(), 
account.getCipherAlgorithm());
+                    clearPassword = ENCRYPTOR.decode(account.getPassword(), 
account.getCipherAlgorithm());
                 } catch (Exception e) {
                     LOG.error("Could not decode password for {}", account, e);
                 }
             }
 
-            if (clear != null) {
-                enforce(clear);
+            if (clearPassword != null) {
+                enforce(clearPassword);
             }
         }
     }
diff --git 
a/core/spring/src/test/java/org/apache/syncope/core/spring/security/TestPasswordRule.java
 
b/core/spring/src/test/java/org/apache/syncope/core/spring/security/TestPasswordRule.java
index 41f9fbcaaf..7e62cf80f7 100644
--- 
a/core/spring/src/test/java/org/apache/syncope/core/spring/security/TestPasswordRule.java
+++ 
b/core/spring/src/test/java/org/apache/syncope/core/spring/security/TestPasswordRule.java
@@ -46,7 +46,7 @@ public class TestPasswordRule implements PasswordRule {
     }
 
     @Override
-    public void enforce(final User user) {
+    public void enforce(final User user, final String clearPassword) {
         // nothing to do
     }
 
diff --git a/core/workflow-java/pom.xml b/core/workflow-java/pom.xml
index 416c2e11f8..ff5b3e37d7 100644
--- a/core/workflow-java/pom.xml
+++ b/core/workflow-java/pom.xml
@@ -57,6 +57,45 @@ under the License.
       <artifactId>syncope-core-spring</artifactId>
       <version>${project.version}</version>
     </dependency>    
+
+    <!-- TEST -->
+    <dependency>
+      <groupId>org.apache.syncope.core</groupId>
+      <artifactId>syncope-core-persistence-jpa</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-simple</artifactId>
+      <version>${slf4j.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.h2database</groupId>
+      <artifactId>h2</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-test</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-junit-jupiter</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
@@ -66,5 +105,29 @@ under the License.
         <artifactId>maven-checkstyle-plugin</artifactId>
       </plugin>
     </plugins>
+
+    <resources>
+      <resource>
+        <directory>${basedir}/src/main/resources</directory>
+        <filtering>true</filtering>
+      </resource>
+    </resources>
+    <testResources>
+      <testResource>
+        <directory>${basedir}/src/test/resources</directory>
+        <filtering>true</filtering>
+      </testResource>
+      <testResource>
+        <directory>${basedir}/../persistence-jpa/src/main/resources</directory>
+        <includes>
+          <include>persistence.properties</include>
+        </includes>
+        <filtering>true</filtering>
+      </testResource>
+      <testResource>
+        <directory>${basedir}/../persistence-jpa/src/test/resources</directory>
+        <filtering>true</filtering>
+      </testResource>
+    </testResources>
   </build>
 </project>
diff --git 
a/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/AbstractUserWorkflowAdapter.java
 
b/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/AbstractUserWorkflowAdapter.java
index 9cf8d06647..eb7672e247 100644
--- 
a/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/AbstractUserWorkflowAdapter.java
+++ 
b/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/AbstractUserWorkflowAdapter.java
@@ -19,18 +19,43 @@
 package org.apache.syncope.core.workflow.java;
 
 import java.util.Collections;
+import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import javax.annotation.Resource;
 import org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.common.lib.patch.PasswordPatch;
 import org.apache.syncope.common.lib.patch.UserPatch;
 import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.lib.types.StandardEntitlement;
+import org.apache.syncope.common.lib.types.EntityViolationType;
+import org.apache.syncope.common.lib.types.ResourceOperation;
+import 
org.apache.syncope.core.persistence.api.attrvalue.validation.InvalidEntityException;
+import org.apache.syncope.core.persistence.api.dao.AccountRule;
+import org.apache.syncope.core.persistence.api.dao.PasswordRule;
+import org.apache.syncope.core.persistence.api.dao.RealmDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.entity.Entity;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.Implementation;
+import org.apache.syncope.core.persistence.api.entity.Realm;
+import org.apache.syncope.core.persistence.api.entity.policy.AccountPolicy;
+import org.apache.syncope.core.persistence.api.entity.policy.PasswordPolicy;
+import 
org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
 import org.apache.syncope.core.persistence.api.entity.user.User;
+import org.apache.syncope.core.provisioning.api.PropagationByResource;
 import org.apache.syncope.core.provisioning.api.UserWorkflowResult;
 import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
+import org.apache.syncope.core.spring.implementation.ImplementationManager;
+import org.apache.syncope.core.spring.policy.AccountPolicyException;
+import org.apache.syncope.core.spring.policy.PasswordPolicyException;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
+import org.apache.syncope.core.spring.security.Encryptor;
 import org.apache.syncope.core.workflow.api.UserWorkflowAdapter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -49,14 +74,218 @@ public abstract class AbstractUserWorkflowAdapter 
implements UserWorkflowAdapter
     @Autowired
     protected UserDAO userDAO;
 
+    @Autowired
+    protected RealmDAO realmDAO;
+
     @Autowired
     protected EntityFactory entityFactory;
 
+    @Resource(name = "adminUser")
+    protected String adminUser;
+
+    @Resource(name = "anonymousUser")
+    protected String anonymousUser;
+
+    protected final Map<String, AccountRule> perContextAccountRules = new 
ConcurrentHashMap<>();
+
+    protected final Map<String, PasswordRule> perContextPasswordRules = new 
ConcurrentHashMap<>();
+
     @Override
     public String getPrefix() {
         return null;
     }
 
+    protected List<AccountPolicy> getAccountPolicies(final User user) {
+        List<AccountPolicy> policies = new ArrayList<>();
+
+        // add resource policies
+        userDAO.findAllResources(user).stream().
+                map(ExternalResource::getAccountPolicy).
+                filter(Objects::nonNull).
+                forEach(policies::add);
+
+        // add realm policies
+        realmDAO.findAncestors(user.getRealm()).stream().
+                map(Realm::getAccountPolicy).
+                filter(Objects::nonNull).
+                forEach(policies::add);
+
+        return policies;
+    }
+
+    protected List<AccountRule> getAccountRules(final AccountPolicy policy) {
+        List<AccountRule> result = new ArrayList<>();
+
+        for (Implementation impl : policy.getRules()) {
+            try {
+                ImplementationManager.buildAccountRule(
+                        impl,
+                        () -> perContextAccountRules.get(impl.getKey()),
+                        instance -> perContextAccountRules.put(impl.getKey(), 
instance)).
+                        ifPresent(result::add);
+            } catch (Exception e) {
+                LOG.warn("While building {}", impl, e);
+            }
+        }
+
+        return result;
+    }
+
+    protected List<PasswordPolicy> getPasswordPolicies(final User user) {
+        List<PasswordPolicy> policies = new ArrayList<>();
+
+        // add resource policies
+        userDAO.findAllResources(user).
+                forEach(resource -> 
Optional.ofNullable(resource.getPasswordPolicy()).
+                filter(p -> !policies.contains(p)).
+                ifPresent(policies::add));
+
+        // add realm policies
+        realmDAO.findAncestors(user.getRealm()).
+                forEach(realm -> 
Optional.ofNullable(realm.getPasswordPolicy()).
+                filter(p -> !policies.contains(p)).
+                ifPresent(policies::add));
+
+        return policies;
+    }
+
+    protected List<PasswordRule> getPasswordRules(final PasswordPolicy policy) 
{
+        List<PasswordRule> result = new ArrayList<>();
+
+        for (Implementation impl : policy.getRules()) {
+            try {
+                ImplementationManager.buildPasswordRule(
+                        impl,
+                        () -> perContextPasswordRules.get(impl.getKey()),
+                        instance -> perContextPasswordRules.put(impl.getKey(), 
instance)).
+                        ifPresent(result::add);
+            } catch (Exception e) {
+                LOG.warn("While building {}", impl, e);
+            }
+        }
+
+        return result;
+    }
+
+    protected Pair<Boolean, Boolean> enforcePolicies(
+            final User user,
+            final boolean disablePwdPolicyCheck,
+            final String clearPassword) {
+
+        if (!disablePwdPolicyCheck) {
+            // ------------------------------
+            // Verify password policies
+            // ------------------------------
+            LOG.debug("Password Policy enforcement");
+
+            try {
+                int maxPPSpecHistory = 0;
+                for (PasswordPolicy policy : getPasswordPolicies(user)) {
+                    if (clearPassword == null && 
!policy.isAllowNullPassword()) {
+                        throw new PasswordPolicyException("Password 
mandatory");
+                    }
+
+                    getPasswordRules(policy).forEach(rule -> {
+                        rule.enforce(user, clearPassword);
+
+                        user.getLinkedAccounts().stream().
+                                filter(account -> account.getPassword() != 
null).
+                                forEach(rule::enforce);
+                    });
+
+                    boolean matching = false;
+                    if (policy.getHistoryLength() > 0) {
+                        List<String> pwdHistory = user.getPasswordHistory();
+                        matching = 
pwdHistory.subList(policy.getHistoryLength() >= pwdHistory.size()
+                                ? 0
+                                : pwdHistory.size() - 
policy.getHistoryLength(), pwdHistory.size()).stream().
+                                map(old -> Encryptor.getInstance().verify(
+                                clearPassword, user.getCipherAlgorithm(), 
old)).
+                                reduce(matching, (accumulator, item) -> 
accumulator | item);
+                    }
+                    if (matching) {
+                        throw new PasswordPolicyException("Password value was 
used in the past: not allowed");
+                    }
+
+                    if (policy.getHistoryLength() > maxPPSpecHistory) {
+                        maxPPSpecHistory = policy.getHistoryLength();
+                    }
+                }
+
+                // update user's password history with encrypted password
+                if (maxPPSpecHistory > 0
+                        && user.getPassword() != null
+                        && 
!user.getPasswordHistory().contains(user.getPassword())) {
+
+                    user.getPasswordHistory().add(user.getPassword());
+                }
+                // keep only the last maxPPSpecHistory items in user's 
password history
+                if (maxPPSpecHistory < user.getPasswordHistory().size()) {
+                    for (int i = 0; i < user.getPasswordHistory().size() - 
maxPPSpecHistory; i++) {
+                        user.getPasswordHistory().remove(i);
+                    }
+                }
+            } catch (InvalidEntityException e) {
+                throw e;
+            } catch (Exception e) {
+                LOG.error("Invalid password for {}", user, e);
+                throw new InvalidEntityException(User.class, 
EntityViolationType.InvalidPassword, e.getMessage());
+            }
+        }
+
+        // ------------------------------
+        // Verify account policies
+        // ------------------------------
+        LOG.debug("Account Policy enforcement");
+
+        boolean suspend = false;
+        boolean propagateSuspension = false;
+        try {
+            if (user.getUsername() == null) {
+                throw new AccountPolicyException("Null username");
+            }
+
+            if (adminUser.equals(user.getUsername()) || 
anonymousUser.equals(user.getUsername())) {
+                throw new AccountPolicyException("Not allowed: " + 
user.getUsername());
+            }
+
+            List<AccountPolicy> accountPolicies = getAccountPolicies(user);
+            if (accountPolicies.isEmpty()) {
+                if (!Entity.ID_PATTERN.matcher(user.getUsername()).matches()) {
+                    throw new AccountPolicyException("Character(s) not 
allowed: " + user.getUsername());
+                }
+                user.getLinkedAccounts().stream().
+                        filter(account -> account.getUsername() != null).
+                        forEach(account -> {
+                            if 
(!Entity.ID_PATTERN.matcher(account.getUsername()).matches()) {
+                                throw new AccountPolicyException("Character(s) 
not allowed: " + account.getUsername());
+                            }
+                        });
+            } else {
+                for (AccountPolicy policy : accountPolicies) {
+                    getAccountRules(policy).forEach(rule -> {
+                        rule.enforce(user);
+
+                        user.getLinkedAccounts().stream().
+                                filter(account -> account.getUsername() != 
null).
+                                forEach(rule::enforce);
+                    });
+
+                    suspend |= user.getFailedLogins() != null && 
policy.getMaxAuthenticationAttempts() > 0
+                            && user.getFailedLogins() > 
policy.getMaxAuthenticationAttempts() && !user.isSuspended();
+                    propagateSuspension |= policy.isPropagateSuspension();
+                }
+            }
+        } catch (InvalidEntityException e) {
+            throw e;
+        } catch (Exception e) {
+            LOG.error("Invalid username for {}", user, e);
+            throw new InvalidEntityException(User.class, 
EntityViolationType.InvalidUsername, e.getMessage());
+        }
+
+        return Pair.of(suspend, propagateSuspension);
+    }
+
     @Override
     public UserWorkflowResult<Pair<String, Boolean>> create(final UserTO 
userTO, final boolean storePassword) {
         return create(userTO, false, null, storePassword);
@@ -72,7 +301,15 @@ public abstract class AbstractUserWorkflowAdapter 
implements UserWorkflowAdapter
             final Boolean enabled,
             final boolean storePassword) {
 
-        return doCreate(userTO, disablePwdPolicyCheck, enabled, storePassword);
+        UserWorkflowResult<Pair<String, Boolean>> result =
+                doCreate(userTO, disablePwdPolicyCheck, enabled, 
storePassword);
+
+        // enforce password and account policies
+        User user = userDAO.find(result.getResult().getKey());
+        enforcePolicies(user, disablePwdPolicyCheck, disablePwdPolicyCheck ? 
null : userTO.getPassword());
+        userDAO.save(user);
+
+        return result;
     }
 
     protected abstract UserWorkflowResult<String> doActivate(User user, String 
token);
@@ -86,9 +323,36 @@ public abstract class AbstractUserWorkflowAdapter 
implements UserWorkflowAdapter
 
     @Override
     public UserWorkflowResult<Pair<UserPatch, Boolean>> update(final UserPatch 
userPatch) {
-        UserWorkflowResult<Pair<UserPatch, Boolean>> result = 
doUpdate(userDAO.authFind(userPatch.getKey()), userPatch);
-
         User user = userDAO.find(userPatch.getKey());
+
+        UserWorkflowResult<Pair<UserPatch, Boolean>> result;
+        // skip actual workflow operations in case only password change on 
resources was requested
+        if (userPatch.isEmptyButPassword() && 
!userPatch.getPassword().isOnSyncope()) {
+            PropagationByResource<String> propByRes = new 
PropagationByResource<>();
+            userDAO.findAllResources(user).stream().
+                    filter(resource -> 
userPatch.getPassword().getResources().contains(resource.getKey())).
+                    forEach(resource -> 
propByRes.add(ResourceOperation.UPDATE, resource.getKey()));
+
+            PropagationByResource<Pair<String, String>> propByLinkedAccount = 
new PropagationByResource<>();
+            user.getLinkedAccounts().stream().
+                    filter(account -> 
userPatch.getPassword().getResources().contains(account.getResource().getKey())).
+                    forEach(account -> propByLinkedAccount.add(
+                    ResourceOperation.UPDATE,
+                    Pair.of(account.getResource().getKey(), 
account.getConnObjectKeyValue())));
+
+            result = new UserWorkflowResult<>(
+                    Pair.of(userPatch, !user.isSuspended()), propByRes, 
propByLinkedAccount, "update");
+        } else {
+            result = doUpdate(userDAO.authFind(userPatch.getKey()), userPatch);
+        }
+
+        // enforce password and account policies
+        enforcePolicies(
+                user,
+                false,
+                
Optional.ofNullable(userPatch.getPassword()).map(PasswordPatch::getValue).orElse(null));
+        user = userDAO.save(user);
+
         if (!AuthContextUtils.getUsername().equals(user.getUsername())) {
             // ensure that requester's administration rights are still valid
             Set<String> authRealms = new HashSet<>();
@@ -124,7 +388,7 @@ public abstract class AbstractUserWorkflowAdapter 
implements UserWorkflowAdapter
 
         Pair<UserWorkflowResult<String>, Boolean> result = null;
 
-        Pair<Boolean, Boolean> enforce = userDAO.enforcePolicies(user);
+        Pair<Boolean, Boolean> enforce = enforcePolicies(user, true, null);
         if (enforce.getKey()) {
             LOG.debug("User {} {} is over the max failed logins", 
user.getKey(), user.getUsername());
 
@@ -169,7 +433,13 @@ public abstract class AbstractUserWorkflowAdapter 
implements UserWorkflowAdapter
     public UserWorkflowResult<Pair<UserPatch, Boolean>> confirmPasswordReset(
             final String key, final String token, final String password) {
 
-        return doConfirmPasswordReset(userDAO.authFind(key), token, password);
+        User user = userDAO.authFind(key);
+
+        // enforce password and account policies
+        enforcePolicies(user, false, password);
+        user = userDAO.save(user);
+
+        return doConfirmPasswordReset(user, token, password);
     }
 
     protected abstract void doDelete(User user);
diff --git 
a/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/DefaultUserWorkflowAdapter.java
 
b/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/DefaultUserWorkflowAdapter.java
index 8c6353c29c..be26d7550e 100644
--- 
a/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/DefaultUserWorkflowAdapter.java
+++ 
b/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/DefaultUserWorkflowAdapter.java
@@ -55,11 +55,6 @@ public class DefaultUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter {
         User user = entityFactory.newEntity(User.class);
         dataBinder.create(user, userTO, storePassword);
 
-        // this will make UserValidator not to consider password policies at 
all
-        if (disablePwdPolicyCheck) {
-            user.removeClearPassword();
-        }
-
         String status;
         boolean propagateEnable;
         if (enabled == null) {
diff --git 
a/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/DefaultUserWorkflowAdapterTest.java
 
b/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/DefaultUserWorkflowAdapterTest.java
new file mode 100644
index 0000000000..a23034133e
--- /dev/null
+++ 
b/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/DefaultUserWorkflowAdapterTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.workflow.java;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.lib.types.StandardEntitlement;
+import 
org.apache.syncope.core.persistence.api.attrvalue.validation.InvalidEntityException;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.entity.user.User;
+import org.apache.syncope.core.provisioning.api.UserWorkflowResult;
+import org.apache.syncope.core.spring.security.SyncopeAuthenticationDetails;
+import org.apache.syncope.core.spring.security.SyncopeGrantedAuthority;
+import org.apache.syncope.core.workflow.api.UserWorkflowAdapter;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import 
org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+import org.springframework.transaction.annotation.Transactional;
+
+@SpringJUnitConfig(classes = WorkflowTestContext.class)
+@Transactional("Master")
+public class DefaultUserWorkflowAdapterTest {
+
+    @BeforeAll
+    public static void setAuthContext() {
+        List<GrantedAuthority> authorities = 
StandardEntitlement.values().stream().
+                map(entitlement -> new SyncopeGrantedAuthority(entitlement, 
SyncopeConstants.ROOT_REALM)).
+                collect(Collectors.toList());
+
+        UsernamePasswordAuthenticationToken auth = new 
UsernamePasswordAuthenticationToken(
+                new org.springframework.security.core.userdetails.User(
+                        "admin", "FAKE_PASSWORD", authorities), 
"FAKE_PASSWORD", authorities);
+        auth.setDetails(new 
SyncopeAuthenticationDetails(SyncopeConstants.MASTER_DOMAIN, null));
+        SecurityContextHolder.getContext().setAuthentication(auth);
+    }
+
+    @AfterAll
+    public static void unsetAuthContext() {
+        SecurityContextHolder.getContext().setAuthentication(null);
+    }
+
+    @Autowired
+    private UserWorkflowAdapter uwfAdapter;
+
+    @Autowired
+    private UserDAO userDAO;
+
+    @Test
+    public void createInvalidPassword() {
+        UserTO userTO = new UserTO();
+        userTO.setUsername(UUID.randomUUID().toString());
+        userTO.setRealm("/even/two");
+        userTO.setPassword("pass");
+
+        assertThrows(InvalidEntityException.class, () -> 
uwfAdapter.create(userTO, true));
+    }
+
+    @Test
+    public void createInvalidUsername() {
+        UserTO userTO = new UserTO();
+        userTO.setUsername("username!");
+        userTO.setRealm("/even/two");
+        userTO.setPassword("password123");
+
+        assertThrows(InvalidEntityException.class, () -> 
uwfAdapter.create(userTO, true));
+    }
+
+    @Test
+    public void passwordHistory() {
+        UserTO userTO = new UserTO();
+        userTO.setUsername(UUID.randomUUID().toString());
+        userTO.setRealm("/even/two");
+        userTO.setPassword("password123");
+
+        UserWorkflowResult<Pair<String, Boolean>> result = 
uwfAdapter.create(userTO, true);
+
+        User user = userDAO.find(result.getResult().getLeft());
+        assertNotNull(user);
+        assertEquals(1, user.getPasswordHistory().size());
+    }
+}
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/implementations/MyPasswordRule.groovy
 
b/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/DummyConnectorRegistry.java
similarity index 60%
copy from 
client/console/src/main/resources/org/apache/syncope/client/console/implementations/MyPasswordRule.groovy
copy to 
core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/DummyConnectorRegistry.java
index 0bb15dbed0..3e4e48c52d 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/implementations/MyPasswordRule.groovy
+++ 
b/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/DummyConnectorRegistry.java
@@ -16,17 +16,19 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import groovy.transform.CompileStatic
-import org.apache.syncope.core.persistence.api.dao.PasswordRule
-import org.apache.syncope.core.persistence.api.entity.user.LinkedAccount
-import org.apache.syncope.core.persistence.api.entity.user.User
+package org.apache.syncope.core.workflow.java;
 
-@CompileStatic
-class MyPasswordRule implements PasswordRule {
-  
-  void enforce(User user) {
-  }
+import org.apache.syncope.core.persistence.api.dao.NotFoundException;
+import 
org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
+import org.apache.syncope.core.provisioning.api.ConnectorRegistry;
 
-  void enforce(LinkedAccount account) {
-  }
+public class DummyConnectorRegistry implements ConnectorRegistry {
+
+    @Override
+    public void registerConnector(final ExternalResource resource) throws 
NotFoundException {
+    }
+
+    @Override
+    public void unregisterConnector(final String id) {
+    }
 }
diff --git 
a/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/DummyImplementationLookup.java
 
b/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/DummyImplementationLookup.java
new file mode 100644
index 0000000000..83e6b64566
--- /dev/null
+++ 
b/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/DummyImplementationLookup.java
@@ -0,0 +1,101 @@
+/*
+ * 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.workflow.java;
+
+import java.util.Collections;
+import java.util.Set;
+import org.apache.syncope.common.lib.policy.AccountRuleConf;
+import org.apache.syncope.common.lib.policy.PasswordRuleConf;
+import org.apache.syncope.common.lib.policy.PullCorrelationRuleConf;
+import org.apache.syncope.common.lib.policy.PushCorrelationRuleConf;
+import org.apache.syncope.common.lib.report.ReportletConf;
+import org.apache.syncope.common.lib.types.ImplementationType;
+import org.apache.syncope.core.persistence.api.ImplementationLookup;
+import org.apache.syncope.core.persistence.api.dao.AccountRule;
+import org.apache.syncope.core.persistence.api.dao.PasswordRule;
+import org.apache.syncope.core.persistence.api.dao.PullCorrelationRule;
+import org.apache.syncope.core.persistence.api.dao.PushCorrelationRule;
+import org.apache.syncope.core.persistence.api.dao.Reportlet;
+import org.apache.syncope.core.persistence.jpa.dao.DefaultPullCorrelationRule;
+import org.apache.syncope.core.persistence.jpa.dao.DefaultPushCorrelationRule;
+import org.apache.syncope.core.spring.policy.DefaultAccountRule;
+import org.apache.syncope.core.spring.policy.DefaultPasswordRule;
+
+public class DummyImplementationLookup implements ImplementationLookup {
+
+    @Override
+    public Integer getPriority() {
+        return -1;
+    }
+
+    @Override
+    public void load() {
+        // do nothing
+    }
+
+    @Override
+    public Set<String> getClassNames(final ImplementationType type) {
+        return Collections.emptySet();
+    }
+
+    @Override
+    public Set<Class<?>> getJWTSSOProviderClasses() {
+        return Collections.emptySet();
+    }
+
+    @Override
+    public Class<Reportlet> getReportletClass(
+            final Class<? extends ReportletConf> reportletConfClass) {
+
+        return null;
+    }
+
+    @Override
+    public Class<? extends AccountRule> getAccountRuleClass(
+            final Class<? extends AccountRuleConf> accountRuleConfClass) {
+
+        return DefaultAccountRule.class;
+    }
+
+    @Override
+    public Class<? extends PasswordRule> getPasswordRuleClass(
+            final Class<? extends PasswordRuleConf> passwordRuleConfClass) {
+
+        return DefaultPasswordRule.class;
+    }
+
+    @Override
+    public Class<? extends PullCorrelationRule> getPullCorrelationRuleClass(
+            final Class<? extends PullCorrelationRuleConf> 
pullCorrelationRuleConfClass) {
+
+        return DefaultPullCorrelationRule.class;
+    }
+
+    @Override
+    public Class<? extends PushCorrelationRule> getPushCorrelationRuleClass(
+            final Class<? extends PushCorrelationRuleConf> 
pushCorrelationRuleConfClass) {
+
+        return DefaultPushCorrelationRule.class;
+    }
+
+    @Override
+    public Set<Class<?>> getAuditAppenderClasses() {
+        return Collections.emptySet();
+    }
+}
diff --git 
a/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/TestInitializer.java
 
b/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/TestInitializer.java
new file mode 100644
index 0000000000..6e26159d85
--- /dev/null
+++ 
b/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/TestInitializer.java
@@ -0,0 +1,49 @@
+/*
+ * 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.workflow.java;
+
+import org.apache.syncope.core.persistence.api.content.ContentLoader;
+import org.apache.syncope.core.spring.ApplicationContextProvider;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.ConfigurableApplicationContext;
+
+public class TestInitializer implements ApplicationContextAware, 
InitializingBean {
+
+    @Autowired
+    private ContentLoader contentLoader;
+
+    private ConfigurableApplicationContext ctx;
+
+    @Override
+    public void setApplicationContext(final ApplicationContext ctx) throws 
BeansException {
+        this.ctx = (ConfigurableApplicationContext) ctx;
+    }
+
+    @Override
+    public void afterPropertiesSet() throws Exception {
+        ApplicationContextProvider.setBeanFactory((DefaultListableBeanFactory) 
ctx.getBeanFactory());
+
+        contentLoader.load();
+    }
+}
diff --git 
a/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/WorkflowTestContext.java
 
b/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/WorkflowTestContext.java
new file mode 100644
index 0000000000..a7f6ff1234
--- /dev/null
+++ 
b/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/WorkflowTestContext.java
@@ -0,0 +1,94 @@
+/*
+ * 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.workflow.java;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+
+import java.util.Date;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.lib.types.CipherAlgorithm;
+import org.apache.syncope.core.persistence.api.ImplementationLookup;
+import org.apache.syncope.core.persistence.api.dao.RealmDAO;
+import org.apache.syncope.core.persistence.api.entity.user.User;
+import org.apache.syncope.core.provisioning.api.ConnectorRegistry;
+import org.apache.syncope.core.provisioning.api.data.AnyObjectDataBinder;
+import org.apache.syncope.core.provisioning.api.data.GroupDataBinder;
+import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.ImportResource;
+
+@Configuration(proxyBeanMethods = false)
+@ImportResource({
+    "classpath:persistenceTest.xml",
+    "classpath:workflowContext.xml",
+    "classpath:workflowTest.xml" })
+public class WorkflowTestContext {
+
+    @Bean
+    public UserDataBinder userDataBinder(final RealmDAO realmDAO) {
+        UserDataBinder dataBinder = mock(UserDataBinder.class);
+
+        doAnswer(ic -> {
+            User user = ic.getArgument(0);
+            UserTO userTO = ic.getArgument(1);
+
+            user.setUsername(userTO.getUsername());
+            user.setRealm(realmDAO.findByFullPath(userTO.getRealm()));
+            user.setCreator("admin");
+            user.setCreationDate(new Date());
+            user.setCipherAlgorithm(CipherAlgorithm.SHA256);
+            user.setPassword(userTO.getPassword());
+
+            return null;
+        }).when(dataBinder).create(any(User.class), any(UserTO.class), 
anyBoolean());
+
+        return dataBinder;
+    }
+
+    @Bean
+    public GroupDataBinder groupDataBinder() {
+        GroupDataBinder dataBinder = mock(GroupDataBinder.class);
+        return dataBinder;
+    }
+
+    @Bean
+    public AnyObjectDataBinder anyObjectDataBinder() {
+        AnyObjectDataBinder dataBinder = mock(AnyObjectDataBinder.class);
+        return dataBinder;
+    }
+
+    @Bean
+    public ConnectorRegistry connectorRegistry() {
+        return new DummyConnectorRegistry();
+    }
+
+    @Bean
+    public TestInitializer testInitializer() {
+        return new TestInitializer();
+    }
+
+    @Bean
+    public ImplementationLookup implementationLookup() {
+        return new DummyImplementationLookup();
+    }
+}
diff --git 
a/core/workflow-java/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
 
b/core/workflow-java/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000000..5895d20600
--- /dev/null
+++ 
b/core/workflow-java/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1,18 @@
+# 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.
+
+mock-maker-inline
diff --git a/core/workflow-java/src/test/resources/workflowTest.xml 
b/core/workflow-java/src/test/resources/workflowTest.xml
new file mode 100644
index 0000000000..188b23f641
--- /dev/null
+++ b/core/workflow-java/src/test/resources/workflowTest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<beans xmlns="http://www.springframework.org/schema/beans";
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+       xsi:schemaLocation="http://www.springframework.org/schema/beans
+                           
http://www.springframework.org/schema/beans/spring-beans.xsd";>
+    
+  <bean 
class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
+    <property name="locations">
+      <list>
+        <value>classpath:persistence.properties</value>
+        <value>classpath:domains/*.properties</value>
+        <value>classpath:security.properties</value>
+        <value>classpath:connid.properties</value>
+        <value>classpath:mail.properties</value>
+        <value>classpath:workflow.properties</value>
+      </list>
+    </property>
+    <property name="ignoreResourceNotFound" value="true"/>
+    <property name="ignoreUnresolvablePlaceholders" value="true"/>
+  </bean>
+</beans>
diff --git 
a/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/impl/FlowableUserWorkflowAdapter.java
 
b/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/impl/FlowableUserWorkflowAdapter.java
index 00fee3016f..8fb9779255 100644
--- 
a/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/impl/FlowableUserWorkflowAdapter.java
+++ 
b/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/impl/FlowableUserWorkflowAdapter.java
@@ -34,11 +34,11 @@ import 
org.apache.syncope.core.provisioning.api.PropagationByResource;
 import org.apache.syncope.common.lib.types.ResourceOperation;
 import org.apache.syncope.core.flowable.api.UserRequestHandler;
 import org.apache.syncope.core.flowable.api.WorkflowTaskManager;
-import org.apache.syncope.core.spring.security.AuthContextUtils;
+import org.apache.syncope.core.flowable.support.DomainProcessEngine;
 import org.apache.syncope.core.persistence.api.entity.user.User;
 import org.apache.syncope.core.provisioning.api.UserWorkflowResult;
-import org.apache.syncope.core.flowable.support.DomainProcessEngine;
 import org.apache.syncope.core.provisioning.api.event.AnyLifecycleEvent;
+import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.apache.syncope.core.workflow.api.WorkflowException;
 import org.apache.syncope.core.workflow.java.AbstractUserWorkflowAdapter;
 import org.flowable.bpmn.model.FlowElement;
@@ -128,11 +128,6 @@ public class FlowableUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter imp
             user.setSuspended(!updatedEnabled);
         }
 
-        // this will make UserValidator not to consider password policies at 
all
-        if (disablePwdPolicyCheck) {
-            user.removeClearPassword();
-        }
-
         FlowableRuntimeUtils.updateStatus(engine, 
procInst.getProcessInstanceId(), user);
         User created = userDAO.save(user);
 
diff --git 
a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/TestPasswordRule.java
 
b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/TestPasswordRule.java
index 2a3eb71df5..88bd4a212d 100644
--- 
a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/TestPasswordRule.java
+++ 
b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/TestPasswordRule.java
@@ -55,8 +55,8 @@ public class TestPasswordRule implements PasswordRule {
 
     @Transactional(readOnly = true)
     @Override
-    public void enforce(final User user) {
-        if (user.getClearPassword() != null && 
!user.getClearPassword().endsWith(conf.getMustEndWith())) {
+    public void enforce(final User user, final String clearPassword) {
+        if (clearPassword != null && 
!clearPassword.endsWith(conf.getMustEndWith())) {
             throw new PasswordPolicyException("Password not ending with " + 
conf.getMustEndWith());
         }
     }
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java
index 48faca1f85..32fc427889 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java
@@ -1215,7 +1215,7 @@ public class UserITCase extends AbstractITCase {
         }
 
         AssociationPatch associationPatch = new 
AssociationPatch.Builder().key(actual.getKey()).
-                
value("password").action(ResourceAssociationAction.ASSIGN).resource(RESOURCE_NAME_CSV).build();
+                
value("password123").action(ResourceAssociationAction.ASSIGN).resource(RESOURCE_NAME_CSV).build();
 
         
assertNotNull(parseBatchResponse(userService.associate(associationPatch)));
 
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 8d9d12b21b..a11ab65597 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
@@ -35,6 +35,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import java.util.UUID;
 import javax.naming.NamingException;
 import javax.sql.DataSource;
 import javax.ws.rs.core.GenericType;
@@ -489,7 +490,7 @@ public class UserIssuesITCase extends AbstractITCase {
         // 2. request to change password only on testdb (no Syncope, no 
testdb2)
         UserPatch userPatch = new UserPatch();
         userPatch.setKey(userTO.getKey());
-        userPatch.setPassword(new 
PasswordPatch.Builder().value(getUUIDString()).onSyncope(false).
+        userPatch.setPassword(new 
PasswordPatch.Builder().value(UUID.randomUUID().toString()).onSyncope(false).
                 resource(RESOURCE_NAME_TESTDB).build());
 
         ProvisioningResult<UserTO> result = updateUser(userPatch);
@@ -1543,9 +1544,8 @@ public class UserIssuesITCase extends AbstractITCase {
                 .value("Other")
                 .build());
 
-        for (int i = 0; i < 2; i++) {
-            updateUser(userPatch);
-        }
+        updateUser(userPatch);
+        updateUser(userPatch);
 
         // 2. remove resources, auxiliary classes and roles
         userPatch.getResources().clear();
@@ -1570,4 +1570,24 @@ public class UserIssuesITCase extends AbstractITCase {
         assertFalse(userTO.getAuxClasses().contains("csv"), "Should not 
contain removed auxiliary classes");
         assertFalse(userTO.getRoles().contains("Other"), "Should not contain 
removed roles");
     }
+
+    @Test
+    public void issueSYNCOPE1750() {
+        UserTO userTO = UserITCase.getUniqueSampleTO("[email protected]");
+        userTO.getResources().add(RESOURCE_NAME_NOPROPAGATION);
+        userTO = createUser(userTO).getEntity();
+
+        UserPatch patch = new UserPatch();
+        patch.setKey(userTO.getKey());
+        patch.setPassword(new PasswordPatch.Builder().
+                
onSyncope(false).resource(RESOURCE_NAME_NOPROPAGATION).value("short").build());
+
+        try {
+            userService.update(patch);
+            fail();
+        } catch (SyncopeClientException e) {
+            assertEquals(ClientExceptionType.InvalidUser, e.getType());
+            assertTrue(e.getMessage().contains("InvalidPassword: Password too 
short"));
+        }
+    }
 }
diff --git a/pom.xml b/pom.xml
index fad34dc0fa..497977503b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2619,12 +2619,11 @@ under the License.
               <dependency>
                 <groupId>org.asciidoctor</groupId>
                 <artifactId>asciidoctorj-pdf</artifactId>
-                <version>2.3.4</version>
+                <version>2.3.6</version>
               </dependency>
             </dependencies>
             <configuration>
               <doctype>book</doctype>
-              <sourceHighlighter>highlightjs</sourceHighlighter>
               <attributes>
                 <docVersion>${project.version}</docVersion>
                 <snapshotOrRelease>${snapshotOrRelease}</snapshotOrRelease>


Reply via email to