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

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


The following commit(s) were added to refs/heads/master by this push:
     new 0f5a1edabe [SYNCOPE-1750] Reactoring Account and Password policy 
enforcement (#437) (#439)
0f5a1edabe is described below

commit 0f5a1edabe583b6160fe63e3f3811bd8a4ab7e09
Author: Francesco Chicchiriccò <[email protected]>
AuthorDate: Wed Apr 12 06:45:49 2023 +0200

    [SYNCOPE-1750] Reactoring Account and Password policy enforcement (#437) 
(#439)
---
 .../console/implementations/MyPasswordRule.groovy  |   2 +-
 .../apache/syncope/common/lib/request/UserUR.java  |  17 +-
 .../syncope/core/persistence/api/dao/UserDAO.java  |   2 -
 .../core/persistence/api/entity/user/User.java     |   8 -
 .../persistence/jpa/JPAJSONPersistenceContext.java |   3 -
 .../core/persistence/jpa/dao/JPAJSONUserDAO.java   |  20 --
 .../core/persistence/jpa/PersistenceContext.java   |   2 -
 .../core/persistence/jpa/dao/JPAUserDAO.java       | 229 -----------------
 .../core/persistence/jpa/entity/user/JPAUser.java  |  39 ---
 .../core/persistence/jpa/inner/UserTest.java       |  42 +--
 .../core/persistence/jpa/outer/UserTest.java       |  80 ------
 .../core/provisioning/api/rules/PasswordRule.java  |   2 +-
 .../java/data/ResourceDataBinderTest.java          |  22 +-
 .../provisioning/java/data/UserDataBinderTest.java | 118 +++++++++
 .../core/spring/policy/DefaultPasswordRule.java    |   6 +-
 .../spring/policy/HaveIBeenPwnedPasswordRule.java  |  19 +-
 .../core/spring/security/TestPasswordRule.java     |   2 +-
 core/workflow-java/pom.xml                         |  62 +++++
 .../workflow/java/AbstractUserWorkflowAdapter.java | 282 ++++++++++++++++++++-
 .../workflow/java/DefaultUserWorkflowAdapter.java  |  11 +-
 .../core/workflow/java/WorkflowContext.java        |  13 +-
 .../java/DefaultUserWorkflowAdapterTest.java       | 109 ++++++++
 .../core/workflow/java/DummyConfParamOps.java}     |  29 ++-
 .../syncope/core/workflow/java/DummyDomainOps.java |  64 +++++
 .../workflow/java/DummyImplementationLookup.java   |  85 +++++++
 .../core/workflow/java/TestInitializer.java        |  68 +++++
 .../core/workflow/java/WorkflowTestContext.java    | 111 ++++++++
 .../core/flowable/FlowableWorkflowContext.java     |   5 +
 .../flowable/impl/FlowableUserWorkflowAdapter.java |  11 +-
 .../apache/syncope/core/logic/SCIMDataBinder.java  |   6 +
 .../fit/core/reference/TestPasswordRule.java       |   4 +-
 .../syncope/fit/core/AbstractTaskITCase.java       |   7 +-
 .../org/apache/syncope/fit/core/UserITCase.java    |   2 +-
 .../apache/syncope/fit/core/UserIssuesITCase.java  |  35 ++-
 pom.xml                                            |  11 +-
 35 files changed, 1019 insertions(+), 509 deletions(-)

diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyPasswordRule.groovy
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyPasswordRule.groovy
index 0bb15dbed0..03a1595a9a 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyPasswordRule.groovy
+++ 
b/client/idrepo/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/idrepo/lib/src/main/java/org/apache/syncope/common/lib/request/UserUR.java
 
b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/request/UserUR.java
index 6bda4c36a1..c0873e0b92 100644
--- 
a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/request/UserUR.java
+++ 
b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/request/UserUR.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.common.lib.request;
 
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import 
com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
 import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
@@ -208,14 +209,24 @@ public class UserUR extends AnyUR {
         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/UserDAO.java
 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/UserDAO.java
index d8827aeeee..b5c53af215 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 f704859953..352afd92a3 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();
-
     OffsetDateTime getChangePwdDate();
 
     void setChangePwdDate(OffsetDateTime changePwdDate);
@@ -59,10 +55,6 @@ public interface User extends Account, 
GroupableRelatable<User, UMembership, UPl
 
     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/JPAJSONPersistenceContext.java
 
b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/JPAJSONPersistenceContext.java
index 71be82c43b..d7d22ffc8f 100644
--- 
a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/JPAJSONPersistenceContext.java
+++ 
b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/JPAJSONPersistenceContext.java
@@ -31,7 +31,6 @@ import 
org.apache.syncope.core.persistence.api.dao.JPAJSONAnyDAO;
 import org.apache.syncope.core.persistence.api.dao.PlainAttrDAO;
 import org.apache.syncope.core.persistence.api.dao.PlainAttrValueDAO;
 import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
-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.AnyUtilsFactory;
@@ -128,7 +127,6 @@ public abstract class JPAJSONPersistenceContext {
             final @Lazy DynRealmDAO dynRealmDAO,
             final @Lazy RoleDAO roleDAO,
             final @Lazy AccessTokenDAO accessTokenDAO,
-            final @Lazy RealmDAO realmDAO,
             final @Lazy GroupDAO groupDAO,
             final @Lazy DelegationDAO delegationDAO,
             final @Lazy FIQLQueryDAO fiqlQueryDAO,
@@ -141,7 +139,6 @@ public abstract class JPAJSONPersistenceContext {
                 dynRealmDAO,
                 roleDAO,
                 accessTokenDAO,
-                realmDAO,
                 groupDAO,
                 delegationDAO,
                 fiqlQueryDAO,
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 9d61c68015..0e514159ae 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.dao.AccessTokenDAO;
 import org.apache.syncope.core.persistence.api.dao.DelegationDAO;
 import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO;
@@ -31,7 +30,6 @@ import 
org.apache.syncope.core.persistence.api.dao.FIQLQueryDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.JPAJSONAnyDAO;
 import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
-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.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.DerSchema;
@@ -40,7 +38,6 @@ import 
org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
 import org.apache.syncope.core.persistence.api.entity.PlainSchema;
 import org.apache.syncope.core.persistence.api.entity.user.User;
 import org.apache.syncope.core.persistence.jpa.entity.user.JPAJSONUser;
-import org.apache.syncope.core.persistence.jpa.entity.user.JPAUser;
 import org.apache.syncope.core.spring.security.SecurityProperties;
 
 public class JPAJSONUserDAO extends JPAUserDAO {
@@ -54,7 +51,6 @@ public class JPAJSONUserDAO extends JPAUserDAO {
             final DynRealmDAO dynRealmDAO,
             final RoleDAO roleDAO,
             final AccessTokenDAO accessTokenDAO,
-            final RealmDAO realmDAO,
             final GroupDAO groupDAO,
             final DelegationDAO delegationDAO,
             final FIQLQueryDAO fiqlQueryDAO,
@@ -67,7 +63,6 @@ public class JPAJSONUserDAO extends JPAUserDAO {
                 dynRealmDAO,
                 roleDAO,
                 accessTokenDAO,
-                realmDAO,
                 groupDAO,
                 delegationDAO,
                 fiqlQueryDAO,
@@ -106,24 +101,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/PersistenceContext.java
 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java
index bd6b6ad62a..f78917d180 100644
--- 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java
+++ 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java
@@ -708,7 +708,6 @@ public class PersistenceContext {
             final @Lazy DynRealmDAO dynRealmDAO,
             final RoleDAO roleDAO,
             final AccessTokenDAO accessTokenDAO,
-            final RealmDAO realmDAO,
             final @Lazy GroupDAO groupDAO,
             final DelegationDAO delegationDAO,
             final FIQLQueryDAO fiqlQueryDAO) {
@@ -720,7 +719,6 @@ public class PersistenceContext {
                 dynRealmDAO,
                 roleDAO,
                 accessTokenDAO,
-                realmDAO,
                 groupDAO,
                 delegationDAO,
                 fiqlQueryDAO,
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 88fa71bf08..3ca97362d7 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
@@ -19,7 +19,6 @@
 package org.apache.syncope.core.persistence.jpa.dao;
 
 import jakarta.persistence.NoResultException;
-import jakarta.persistence.PersistenceException;
 import jakarta.persistence.Query;
 import jakarta.persistence.TypedQuery;
 import java.time.OffsetDateTime;
@@ -31,13 +30,10 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.stream.Collectors;
 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.IdRepoEntitlement;
-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.DelegationDAO;
 import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO;
@@ -45,21 +41,16 @@ import 
org.apache.syncope.core.persistence.api.dao.DynRealmDAO;
 import org.apache.syncope.core.persistence.api.dao.FIQLQueryDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
-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.AnyUtils;
 import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
-import org.apache.syncope.core.persistence.api.entity.Entity;
 import org.apache.syncope.core.persistence.api.entity.ExternalResource;
-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.user.LinkedAccount;
 import org.apache.syncope.core.persistence.api.entity.user.SecurityQuestion;
 import org.apache.syncope.core.persistence.api.entity.user.UMembership;
@@ -67,15 +58,9 @@ import 
org.apache.syncope.core.persistence.api.entity.user.User;
 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.rules.AccountRule;
-import org.apache.syncope.core.provisioning.api.rules.PasswordRule;
 import org.apache.syncope.core.provisioning.api.utils.RealmUtils;
-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.DelegatedAdministrationException;
-import org.apache.syncope.core.spring.security.Encryptor;
 import org.apache.syncope.core.spring.security.SecurityProperties;
 import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
@@ -86,8 +71,6 @@ public class JPAUserDAO extends AbstractAnyDAO<User> 
implements UserDAO {
 
     protected final AccessTokenDAO accessTokenDAO;
 
-    protected final RealmDAO realmDAO;
-
     protected final GroupDAO groupDAO;
 
     protected final DelegationDAO delegationDAO;
@@ -96,10 +79,6 @@ public class JPAUserDAO extends AbstractAnyDAO<User> 
implements UserDAO {
 
     protected final SecurityProperties securityProperties;
 
-    protected final Map<String, AccountRule> perContextAccountRules = new 
ConcurrentHashMap<>();
-
-    protected final Map<String, PasswordRule> perContextPasswordRules = new 
ConcurrentHashMap<>();
-
     public JPAUserDAO(
             final AnyUtilsFactory anyUtilsFactory,
             final PlainSchemaDAO plainSchemaDAO,
@@ -107,7 +86,6 @@ public class JPAUserDAO extends AbstractAnyDAO<User> 
implements UserDAO {
             final DynRealmDAO dynRealmDAO,
             final RoleDAO roleDAO,
             final AccessTokenDAO accessTokenDAO,
-            final RealmDAO realmDAO,
             final GroupDAO groupDAO,
             final DelegationDAO delegationDAO,
             final FIQLQueryDAO fiqlQueryDAO,
@@ -116,7 +94,6 @@ public class JPAUserDAO extends AbstractAnyDAO<User> 
implements UserDAO {
         super(anyUtilsFactory, plainSchemaDAO, derSchemaDAO, dynRealmDAO);
         this.roleDAO = roleDAO;
         this.accessTokenDAO = accessTokenDAO;
-        this.realmDAO = realmDAO;
         this.groupDAO = groupDAO;
         this.delegationDAO = delegationDAO;
         this.fiqlQueryDAO = fiqlQueryDAO;
@@ -279,24 +256,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<>();
-
-        // add resource policies
-        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;
-    }
-
     @Override
     public List<User> findAll(final int page, final int itemsPerPage) {
         TypedQuery<User> query = entityManager().createQuery(
@@ -312,196 +271,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(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<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.getInstance().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.addToPasswordHistory(user.getPassword());
-            }
-            // keep only the last maxPPSpecHistory items in user's password 
history
-            if (maxPPSpecHistory < user.getPasswordHistory().size()) {
-                
user.removeOldestEntriesFromPasswordHistory(user.getPasswordHistory().size() - 
maxPPSpecHistory);
-            }
-        } 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 (securityProperties.getAdminUser().equals(user.getUsername())
-                    || 
securityProperties.getAnonymousUser().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 e462625d8e..84aa70bf1b 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
@@ -33,7 +33,6 @@ import jakarta.persistence.ManyToMany;
 import jakarta.persistence.ManyToOne;
 import jakarta.persistence.OneToMany;
 import jakarta.persistence.Table;
-import jakarta.persistence.Transient;
 import jakarta.persistence.UniqueConstraint;
 import jakarta.validation.Valid;
 import jakarta.validation.constraints.NotNull;
@@ -86,9 +85,6 @@ public class JPAUser
     @Column(nullable = true)
     protected String password;
 
-    @Transient
-    protected String clearPassword;
-
     @ManyToMany(fetch = FetchType.EAGER)
     @JoinTable(joinColumns =
             @JoinColumn(name = "user_id"),
@@ -179,9 +175,6 @@ public class JPAUser
     @Column(nullable = true)
     protected String securityAnswer;
 
-    @Transient
-    protected String clearSecurityAnswer;
-
     @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = 
"owner")
     @Valid
     protected List<JPALinkedAccount> linkedAccounts = new ArrayList<>();
@@ -223,24 +216,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);
@@ -248,8 +225,6 @@ 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(ConfParamOps.class).
@@ -436,22 +411,8 @@ 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(ConfParamOps.class).
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 bb9a7919cd..4d1467d22d 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
@@ -24,14 +24,12 @@ import static 
org.junit.jupiter.api.Assertions.assertNotNull;
 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 static org.junit.jupiter.api.Assertions.fail;
 
 import java.time.OffsetDateTime;
 import java.time.temporal.ChronoUnit;
 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 +199,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(OffsetDateTime.now());
-        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(OffsetDateTime.now());
-        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 +211,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()));
         assertTrue(actual.getLastChangeDate().truncatedTo(ChronoUnit.SECONDS).
                 
isEqual(userDAO.findLastChange(actual.getKey()).truncatedTo(ChronoUnit.SECONDS)));
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 46c35c87c1..d8d423e8a0 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,15 +23,12 @@ 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.time.OffsetDateTime;
 import java.util.List;
 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;
@@ -163,83 +160,6 @@ public class UserTest extends AbstractTest {
         assertEquals("8559d14d-58c2-46eb-a2d4-a7d35161e8f8", 
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(a -> 
a.getMembership() == null));
-        assertTrue(user.getPlainAttrs("obscure").stream().anyMatch(a -> 
newM.equals(a.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-api/src/main/java/org/apache/syncope/core/provisioning/api/rules/PasswordRule.java
 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/rules/PasswordRule.java
index 823a08e78f..19ef16e79c 100644
--- 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/rules/PasswordRule.java
+++ 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/rules/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/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 b1866fd0ac..70aa116e77 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
@@ -33,7 +33,6 @@ import org.apache.syncope.common.lib.to.ResourceTO;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.IdMEntitlement;
 import org.apache.syncope.common.lib.types.MappingPurpose;
-import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
 import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
 import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
 import org.apache.syncope.core.persistence.api.entity.ExternalResource;
@@ -55,18 +54,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 = IdMEntitlement.values().stream().
@@ -85,6 +72,15 @@ public class ResourceDataBinderTest extends AbstractTest {
         SecurityContextHolder.getContext().setAuthentication(null);
     }
 
+    @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..bebb7bdc27
--- /dev/null
+++ 
b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderTest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.Attr;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.request.AttrPatch;
+import org.apache.syncope.common.lib.request.MembershipUR;
+import org.apache.syncope.common.lib.request.UserUR;
+import org.apache.syncope.common.lib.types.IdRepoEntitlement;
+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.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.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 = 
IdRepoEntitlement.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;
+
+    @Test
+    public void membershipWithAttrNotAllowed() {
+        UserUR userUR = new 
UserUR.Builder("1417acbe-cbf6-4277-9372-e75e04f97000").build();
+
+        // add 'obscure' to user (no membership): works because 'obscure' is 
from 'other', default class for USER
+        userUR.getPlainAttrs().
+                add(new AttrPatch.Builder(new 
Attr.Builder("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
+        userUR.getMemberships().add(new 
MembershipUR.Builder("ece66293-8f31-4a84-8e8d-23da36e70846").
+                plainAttr(new 
Attr.Builder("obscure").value("testvalue2").build()).build());
+
+        assertThrows(InvalidEntityException.class, () -> 
dataBinder.update(userDAO.find(userUR.getKey()), userUR));
+    }
+
+    @Test
+    public void membershipWithAttr() {
+        UserUR userUR = new 
UserUR.Builder("1417acbe-cbf6-4277-9372-e75e04f97000").build();
+
+        // add 'obscure' (no membership): works because 'obscure' is from 
'other', default class for USER
+        userUR.getPlainAttrs().
+                add(new AttrPatch.Builder(new 
Attr.Builder("obscure").value("testvalue").build()).build());
+
+        // add 'obscure' (via 'additional' membership): that group defines 
type extension with classes 'other' and 'csv'
+        userUR.getMemberships().add(new 
MembershipUR.Builder("034740a9-fa10-453b-af37-dc7897e98fb1").
+                plainAttr(new 
Attr.Builder("obscure").value("testvalue2").build()).build());
+
+        dataBinder.update(userDAO.find(userUR.getKey()), userUR);
+
+        User user = userDAO.find(userUR.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 4182b35cc7..ae63f4df25 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
@@ -165,8 +165,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().
@@ -177,7 +177,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 d3149b9c8f..e66eb215e2 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 acbb44a201..61448f0e0e 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 5b27f9f3ac..976b8f2f88 100644
--- a/core/workflow-java/pom.xml
+++ b/core/workflow-java/pom.xml
@@ -57,6 +57,44 @@ under the License.
       <artifactId>syncope-core-spring</artifactId>
       <version>${project.version}</version>
     </dependency>    
+
+    <!-- TEST -->
+    <dependency>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-starter-validation</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <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>
+      <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>
   </dependencies>
 
   <build>
@@ -66,5 +104,29 @@ under the License.
         <artifactId>maven-checkstyle-plugin</artifactId>
       </plugin>
     </plugins>
+
+    <resources>
+      <resource>
+        <directory>src/main/resources</directory>
+        <filtering>true</filtering>
+      </resource>
+    </resources>
+    <testResources>
+      <testResource>
+        <directory>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 eb9e21eb1f..129adb5ec2 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
@@ -18,18 +18,43 @@
  */
 package org.apache.syncope.core.workflow.java;
 
+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 org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.common.lib.request.PasswordPatch;
 import org.apache.syncope.common.lib.request.UserCR;
 import org.apache.syncope.common.lib.request.UserUR;
+import org.apache.syncope.common.lib.types.EntityViolationType;
 import org.apache.syncope.common.lib.types.IdRepoEntitlement;
+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.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.ExternalResource;
+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.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.provisioning.api.rules.AccountRule;
+import org.apache.syncope.core.provisioning.api.rules.PasswordRule;
+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.spring.security.SecurityProperties;
 import org.apache.syncope.core.workflow.api.UserWorkflowAdapter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -45,16 +70,28 @@ public abstract class AbstractUserWorkflowAdapter extends 
AbstractWorkflowAdapte
 
     protected final UserDAO userDAO;
 
+    protected final RealmDAO realmDAO;
+
     protected final EntityFactory entityFactory;
 
+    protected final SecurityProperties securityProperties;
+
+    protected final Map<String, AccountRule> perContextAccountRules = new 
ConcurrentHashMap<>();
+
+    protected final Map<String, PasswordRule> perContextPasswordRules = new 
ConcurrentHashMap<>();
+
     public AbstractUserWorkflowAdapter(
             final UserDataBinder dataBinder,
             final UserDAO userDAO,
-            final EntityFactory entityFactory) {
+            final RealmDAO realmDAO,
+            final EntityFactory entityFactory,
+            final SecurityProperties securityProperties) {
 
         this.dataBinder = dataBinder;
         this.userDAO = userDAO;
+        this.realmDAO = realmDAO;
         this.entityFactory = entityFactory;
+        this.securityProperties = securityProperties;
     }
 
     @Override
@@ -62,6 +99,197 @@ public abstract class AbstractUserWorkflowAdapter extends 
AbstractWorkflowAdapte
         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.addToPasswordHistory(user.getPassword());
+                }
+                // keep only the last maxPPSpecHistory items in user's 
password history
+                if (maxPPSpecHistory < user.getPasswordHistory().size()) {
+                    
user.removeOldestEntriesFromPasswordHistory(user.getPasswordHistory().size() - 
maxPPSpecHistory);
+                }
+            } 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 (securityProperties.getAdminUser().equals(user.getUsername())
+                    || 
securityProperties.getAnonymousUser().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 UserCR userCR, final String creator, final String context) {
@@ -80,7 +308,15 @@ public abstract class AbstractUserWorkflowAdapter extends 
AbstractWorkflowAdapte
             final String creator,
             final String context) {
 
-        return doCreate(userCR, disablePwdPolicyCheck, enabled, creator, 
context);
+        UserWorkflowResult<Pair<String, Boolean>> result =
+                doCreate(userCR, disablePwdPolicyCheck, enabled, creator, 
context);
+
+        // enforce password and account policies
+        User user = userDAO.find(result.getResult().getKey());
+        enforcePolicies(user, disablePwdPolicyCheck, disablePwdPolicyCheck ? 
null : userCR.getPassword());
+        userDAO.save(user);
+
+        return result;
     }
 
     protected abstract UserWorkflowResult<String> doActivate(User user, String 
token, String updater, String context);
@@ -99,10 +335,36 @@ public abstract class AbstractUserWorkflowAdapter extends 
AbstractWorkflowAdapte
     public UserWorkflowResult<Pair<UserUR, Boolean>> update(
             final UserUR userUR, final String updater, final String context) {
 
-        UserWorkflowResult<Pair<UserUR, Boolean>> result = doUpdate(
-                userDAO.authFind(userUR.getKey()), userUR, updater, context);
-
         User user = userDAO.find(userUR.getKey());
+
+        UserWorkflowResult<Pair<UserUR, Boolean>> result;
+        // skip actual workflow operations in case only password change on 
resources was requested
+        if (userUR.isEmptyButPassword() && 
!userUR.getPassword().isOnSyncope()) {
+            PropagationByResource<String> propByRes = new 
PropagationByResource<>();
+            userDAO.findAllResources(user).stream().
+                    filter(resource -> 
userUR.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 -> 
userUR.getPassword().getResources().contains(account.getResource().getKey())).
+                    forEach(account -> propByLinkedAccount.add(
+                    ResourceOperation.UPDATE,
+                    Pair.of(account.getResource().getKey(), 
account.getConnObjectKeyValue())));
+
+            result = new UserWorkflowResult<>(
+                    Pair.of(userUR, !user.isSuspended()), propByRes, 
propByLinkedAccount, "update");
+        } else {
+            result = doUpdate(userDAO.authFind(userUR.getKey()), userUR, 
updater, context);
+        }
+
+        // enforce password and account policies
+        enforcePolicies(
+                user,
+                userUR.getPassword() == null,
+                
Optional.ofNullable(userUR.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<>();
@@ -140,7 +402,7 @@ public abstract class AbstractUserWorkflowAdapter extends 
AbstractWorkflowAdapte
 
         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());
 
@@ -185,7 +447,13 @@ public abstract class AbstractUserWorkflowAdapter extends 
AbstractWorkflowAdapte
     public UserWorkflowResult<Pair<UserUR, Boolean>> confirmPasswordReset(
             final String key, final String token, final String password, final 
String updater, final String context) {
 
-        return doConfirmPasswordReset(userDAO.authFind(key), token, password, 
updater, context);
+        User user = userDAO.authFind(key);
+
+        // enforce password and account policies
+        enforcePolicies(user, false, password);
+        user = userDAO.save(user);
+
+        return doConfirmPasswordReset(user, token, password, updater, context);
     }
 
     protected abstract void doDelete(User user, String eraser, String context);
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 4b612905b5..a616640263 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
@@ -24,6 +24,7 @@ import org.apache.syncope.common.lib.request.PasswordPatch;
 import org.apache.syncope.common.lib.request.UserCR;
 import org.apache.syncope.common.lib.request.UserUR;
 import org.apache.syncope.common.lib.types.ResourceOperation;
+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.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.user.User;
@@ -32,6 +33,7 @@ import 
org.apache.syncope.core.provisioning.api.UserWorkflowResult;
 import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
 import org.apache.syncope.core.provisioning.api.event.AnyLifecycleEvent;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
+import org.apache.syncope.core.spring.security.SecurityProperties;
 import org.apache.syncope.core.workflow.api.WorkflowException;
 import org.identityconnectors.framework.common.objects.SyncDeltaType;
 import org.springframework.context.ApplicationEventPublisher;
@@ -48,11 +50,13 @@ public class DefaultUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter {
     public DefaultUserWorkflowAdapter(
             final UserDataBinder dataBinder,
             final UserDAO userDAO,
+            final RealmDAO realmDAO,
             final EntityFactory entityFactory,
+            final SecurityProperties securityProperties,
             final ConfParamOps confParamOps,
             final ApplicationEventPublisher publisher) {
 
-        super(dataBinder, userDAO, entityFactory);
+        super(dataBinder, userDAO, realmDAO, entityFactory, 
securityProperties);
         this.confParamOps = confParamOps;
         this.publisher = publisher;
     }
@@ -68,11 +72,6 @@ public class DefaultUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter {
         User user = entityFactory.newEntity(User.class);
         dataBinder.create(user, userCR);
 
-        // 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/main/java/org/apache/syncope/core/workflow/java/WorkflowContext.java
 
b/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/WorkflowContext.java
index 3ce0018668..bd4de35dc7 100644
--- 
a/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/WorkflowContext.java
+++ 
b/core/workflow-java/src/main/java/org/apache/syncope/core/workflow/java/WorkflowContext.java
@@ -21,11 +21,13 @@ package org.apache.syncope.core.workflow.java;
 import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
+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.EntityFactory;
 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.apache.syncope.core.spring.security.SecurityProperties;
 import org.apache.syncope.core.workflow.api.AnyObjectWorkflowAdapter;
 import org.apache.syncope.core.workflow.api.GroupWorkflowAdapter;
 import org.apache.syncope.core.workflow.api.UserWorkflowAdapter;
@@ -42,11 +44,20 @@ public class WorkflowContext {
     public UserWorkflowAdapter uwfAdapter(
             final UserDataBinder userDataBinder,
             final UserDAO userDAO,
+            final RealmDAO realmDAO,
             final EntityFactory entityFactory,
+            final SecurityProperties securityProperties,
             final ConfParamOps confParamOps,
             final ApplicationEventPublisher publisher) {
 
-        return new DefaultUserWorkflowAdapter(userDataBinder, userDAO, 
entityFactory, confParamOps, publisher);
+        return new DefaultUserWorkflowAdapter(
+                userDataBinder,
+                userDAO,
+                realmDAO,
+                entityFactory,
+                securityProperties,
+                confParamOps,
+                publisher);
     }
 
     @ConditionalOnMissingBean
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..bbf990c376
--- /dev/null
+++ 
b/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/DefaultUserWorkflowAdapterTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.stream.Collectors;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.request.UserCR;
+import org.apache.syncope.common.lib.types.IdRepoEntitlement;
+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 = 
IdRepoEntitlement.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() {
+        UserCR userCR = new UserCR();
+        userCR.setUsername("username");
+        userCR.setRealm("/even/two");
+        userCR.setPassword("pass");
+
+        assertThrows(InvalidEntityException.class, () -> 
uwfAdapter.create(userCR, "admin", "test"));
+    }
+
+    @Test
+    public void createInvalidUsername() {
+        UserCR userCR = new UserCR();
+        userCR.setUsername("username!");
+        userCR.setRealm("/even/two");
+        userCR.setPassword("password123");
+
+        assertThrows(InvalidEntityException.class, () -> 
uwfAdapter.create(userCR, "admin", "test"));
+    }
+
+    @Test
+    public void passwordHistory() {
+        UserCR userCR = new UserCR();
+        userCR.setUsername("username");
+        userCR.setRealm("/even/two");
+        userCR.setPassword("password123");
+
+        UserWorkflowResult<Pair<String, Boolean>> result = 
uwfAdapter.create(userCR, "admin", "test");
+
+        User user = userDAO.find(result.getResult().getLeft());
+        assertNotNull(user);
+        assertEquals(1, user.getPasswordHistory().size());
+    }
+}
diff --git 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/rules/PasswordRule.java
 
b/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/DummyConfParamOps.java
similarity index 56%
copy from 
core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/rules/PasswordRule.java
copy to 
core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/DummyConfParamOps.java
index 823a08e78f..ce2e40bcd6 100644
--- 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/rules/PasswordRule.java
+++ 
b/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/DummyConfParamOps.java
@@ -16,25 +16,28 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.api.rules;
+package org.apache.syncope.core.workflow.java;
 
-import org.apache.syncope.common.lib.policy.PasswordRuleConf;
-import org.apache.syncope.core.persistence.api.entity.user.LinkedAccount;
-import org.apache.syncope.core.persistence.api.entity.user.User;
+import java.util.Map;
+import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
 
-/**
- * Interface for enforcing a given password rule to user.
- */
-public interface PasswordRule {
+public class DummyConfParamOps implements ConfParamOps {
 
-    default PasswordRuleConf getConf() {
-        return null;
+    @Override
+    public Map<String, Object> list(final String domain) {
+        return Map.of();
     }
 
-    default void setConf(PasswordRuleConf conf) {
+    @Override
+    public <T> T get(final String domain, final String key, final T 
defaultValue, final Class<T> reference) {
+        return defaultValue;
     }
 
-    void enforce(User user);
+    @Override
+    public <T> void set(final String domain, final String key, final T value) {
+    }
 
-    void enforce(LinkedAccount account);
+    @Override
+    public void remove(final String domain, final String key) {
+    }
 }
diff --git 
a/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/DummyDomainOps.java
 
b/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/DummyDomainOps.java
new file mode 100644
index 0000000000..759518b000
--- /dev/null
+++ 
b/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/DummyDomainOps.java
@@ -0,0 +1,64 @@
+/*
+ * 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.List;
+import org.apache.syncope.common.keymaster.client.api.DomainOps;
+import org.apache.syncope.common.keymaster.client.api.model.Domain;
+import org.apache.syncope.common.lib.types.CipherAlgorithm;
+import org.apache.syncope.core.persistence.api.DomainRegistry;
+
+public class DummyDomainOps implements DomainOps {
+
+    private final DomainRegistry domainRegistry;
+
+    public DummyDomainOps(final DomainRegistry domainRegistry) {
+        this.domainRegistry = domainRegistry;
+    }
+
+    @Override
+    public List<Domain> list() {
+        return List.of();
+    }
+
+    @Override
+    public Domain read(final String key) {
+        return new Domain.Builder(key).build();
+    }
+
+    @Override
+    public void create(final Domain domain) {
+        domainRegistry.register(domain);
+    }
+
+    @Override
+    public void changeAdminPassword(final String key, final String password, 
final CipherAlgorithm cipherAlgorithm) {
+        // nothing to do
+    }
+
+    @Override
+    public void adjustPoolSize(final String key, final int maxPoolSize, final 
int minIdle) {
+        // nothing to do
+    }
+
+    @Override
+    public void delete(final String key) {
+        // nothing to do
+    }
+}
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..fc82c8ef1e
--- /dev/null
+++ 
b/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/DummyImplementationLookup.java
@@ -0,0 +1,85 @@
+/*
+ * 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.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.ReportConf;
+import org.apache.syncope.core.provisioning.api.ImplementationLookup;
+import org.apache.syncope.core.provisioning.api.job.report.ReportJobDelegate;
+import org.apache.syncope.core.provisioning.api.rules.AccountRule;
+import org.apache.syncope.core.provisioning.api.rules.PasswordRule;
+import org.apache.syncope.core.provisioning.api.rules.PullCorrelationRule;
+import org.apache.syncope.core.provisioning.api.rules.PushCorrelationRule;
+import org.apache.syncope.core.spring.policy.DefaultAccountRule;
+import org.apache.syncope.core.spring.policy.DefaultPasswordRule;
+
+public class DummyImplementationLookup implements ImplementationLookup {
+
+    @Override
+    public int getOrder() {
+        return -1;
+    }
+
+    @Override
+    public Set<String> getClassNames(final String type) {
+        return Set.of();
+    }
+
+    @Override
+    public Set<Class<?>> getJWTSSOProviderClasses() {
+        return Set.of();
+    }
+
+    @Override
+    public Class<? extends ReportJobDelegate> getReportClass(final Class<? 
extends ReportConf> reportConfClass) {
+        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 null;
+    }
+
+    @Override
+    public Class<? extends PushCorrelationRule> getPushCorrelationRuleClass(
+            final Class<? extends PushCorrelationRuleConf> 
pushCorrelationRuleConfClass) {
+
+        return null;
+    }
+}
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..808c0bc232
--- /dev/null
+++ 
b/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/TestInitializer.java
@@ -0,0 +1,68 @@
+/*
+ * 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.common.lib.SyncopeConstants;
+import org.apache.syncope.core.persistence.api.DomainHolder;
+import org.apache.syncope.core.persistence.api.content.ContentLoader;
+import org.apache.syncope.core.persistence.jpa.StartupDomainLoader;
+import org.apache.syncope.core.spring.ApplicationContextProvider;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.context.ConfigurableApplicationContext;
+import 
org.springframework.transaction.support.TransactionSynchronizationManager;
+
+public class TestInitializer implements InitializingBean {
+
+    private final StartupDomainLoader domainLoader;
+
+    private final DomainHolder domainHolder;
+
+    private final ContentLoader contentLoader;
+
+    private final ConfigurableApplicationContext ctx;
+
+    public TestInitializer(
+            final StartupDomainLoader domainLoader,
+            final DomainHolder domainHolder,
+            final ContentLoader contentLoader,
+            final ConfigurableApplicationContext ctx) {
+
+        this.domainLoader = domainLoader;
+        this.domainHolder = domainHolder;
+        this.contentLoader = contentLoader;
+        this.ctx = ctx;
+    }
+
+    @Override
+    public void afterPropertiesSet() throws Exception {
+        ApplicationContextProvider.setApplicationContext(ctx);
+        ApplicationContextProvider.setBeanFactory((DefaultListableBeanFactory) 
ctx.getBeanFactory());
+
+        if (!TransactionSynchronizationManager.isSynchronizationActive()) {
+            TransactionSynchronizationManager.initSynchronization();
+        }
+
+        domainLoader.load();
+
+        contentLoader.load(
+                SyncopeConstants.MASTER_DOMAIN,
+                domainHolder.getDomains().get(SyncopeConstants.MASTER_DOMAIN));
+    }
+}
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..538c594027
--- /dev/null
+++ 
b/core/workflow-java/src/test/java/org/apache/syncope/core/workflow/java/WorkflowTestContext.java
@@ -0,0 +1,111 @@
+/*
+ * 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.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+
+import java.time.OffsetDateTime;
+import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
+import org.apache.syncope.common.keymaster.client.api.DomainOps;
+import org.apache.syncope.common.lib.request.UserCR;
+import org.apache.syncope.common.lib.types.CipherAlgorithm;
+import org.apache.syncope.core.persistence.api.DomainHolder;
+import org.apache.syncope.core.persistence.api.DomainRegistry;
+import org.apache.syncope.core.persistence.api.content.ContentLoader;
+import org.apache.syncope.core.persistence.api.dao.RealmDAO;
+import org.apache.syncope.core.persistence.api.entity.user.User;
+import org.apache.syncope.core.persistence.jpa.MasterDomain;
+import org.apache.syncope.core.persistence.jpa.PersistenceContext;
+import org.apache.syncope.core.persistence.jpa.StartupDomainLoader;
+import org.apache.syncope.core.provisioning.api.ImplementationLookup;
+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.apache.syncope.core.spring.security.SecurityContext;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.PropertySource;
+
+@PropertySource("classpath:core-test.properties")
+@Import({ SecurityContext.class, PersistenceContext.class, MasterDomain.class, 
WorkflowContext.class })
+@Configuration(proxyBeanMethods = false)
+public class WorkflowTestContext {
+
+    @Bean
+    public TestInitializer testInitializer(
+            final StartupDomainLoader domainLoader,
+            final DomainHolder domainHolder,
+            final ContentLoader contentLoader,
+            final ConfigurableApplicationContext ctx) {
+
+        return new TestInitializer(domainLoader, domainHolder, contentLoader, 
ctx);
+    }
+
+    @Bean
+    public UserDataBinder userDataBinder(final RealmDAO realmDAO) {
+        UserDataBinder dataBinder = mock(UserDataBinder.class);
+
+        doAnswer(ic -> {
+            User user = ic.getArgument(0);
+            UserCR userCR = ic.getArgument(1);
+
+            user.setUsername(userCR.getUsername());
+            user.setRealm(realmDAO.findByFullPath(userCR.getRealm()));
+            user.setCreator("admin");
+            user.setCreationDate(OffsetDateTime.now());
+            user.setCipherAlgorithm(CipherAlgorithm.SHA256);
+            user.setPassword(userCR.getPassword());
+
+            return null;
+        }).when(dataBinder).create(any(User.class), any(UserCR.class));
+
+        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 ImplementationLookup implementationLookup() {
+        return new DummyImplementationLookup();
+    }
+
+    @Bean
+    public ConfParamOps confParamOps() {
+        return new DummyConfParamOps();
+    }
+
+    @Bean
+    public DomainOps domainOps(final DomainRegistry domainRegistry) {
+        return new DummyDomainOps(domainRegistry);
+    }
+}
diff --git 
a/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/FlowableWorkflowContext.java
 
b/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/FlowableWorkflowContext.java
index 78de315243..4882119fe5 100644
--- 
a/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/FlowableWorkflowContext.java
+++ 
b/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/FlowableWorkflowContext.java
@@ -40,6 +40,7 @@ import org.apache.syncope.core.flowable.task.PasswordReset;
 import org.apache.syncope.core.flowable.task.Reactivate;
 import org.apache.syncope.core.flowable.task.Suspend;
 import org.apache.syncope.core.flowable.task.Update;
+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.EntityFactory;
 import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
@@ -169,7 +170,9 @@ public class FlowableWorkflowContext {
     public UserWorkflowAdapter uwfAdapter(
             final UserDataBinder userDataBinder,
             final UserDAO userDAO,
+            final RealmDAO realmDAO,
             final EntityFactory entityFactory,
+            final SecurityProperties securityProperties,
             final DomainProcessEngine engine,
             final UserRequestHandler userRequestHandler,
             final ApplicationEventPublisher publisher) {
@@ -177,7 +180,9 @@ public class FlowableWorkflowContext {
         return new FlowableUserWorkflowAdapter(
                 userDataBinder,
                 userDAO,
+                realmDAO,
                 entityFactory,
+                securityProperties,
                 engine,
                 userRequestHandler,
                 publisher);
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 5232b42c0a..81777294ec 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
@@ -35,6 +35,7 @@ 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.flowable.support.DomainProcessEngine;
+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.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.user.User;
@@ -43,6 +44,7 @@ import 
org.apache.syncope.core.provisioning.api.UserWorkflowResult;
 import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
 import org.apache.syncope.core.provisioning.api.event.AnyLifecycleEvent;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
+import org.apache.syncope.core.spring.security.SecurityProperties;
 import org.apache.syncope.core.workflow.api.WorkflowException;
 import org.apache.syncope.core.workflow.java.AbstractUserWorkflowAdapter;
 import org.flowable.bpmn.model.FlowElement;
@@ -67,12 +69,14 @@ public class FlowableUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter imp
     public FlowableUserWorkflowAdapter(
             final UserDataBinder dataBinder,
             final UserDAO userDAO,
+            final RealmDAO realmDAO,
             final EntityFactory entityFactory,
+            final SecurityProperties securityProperties,
             final DomainProcessEngine engine,
             final UserRequestHandler userRequestHandler,
             final ApplicationEventPublisher publisher) {
 
-        super(dataBinder, userDAO, entityFactory);
+        super(dataBinder, userDAO, realmDAO, entityFactory, 
securityProperties);
         this.engine = engine;
         this.userRequestHandler = userRequestHandler;
         this.publisher = publisher;
@@ -142,11 +146,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();
-        }
-
         metadata(user, creator, context);
         FlowableRuntimeUtils.updateStatus(engine, 
procInst.getProcessInstanceId(), user);
         User created = userDAO.save(user);
diff --git 
a/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/SCIMDataBinder.java
 
b/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/SCIMDataBinder.java
index f42e820f40..0e05439e1b 100644
--- 
a/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/SCIMDataBinder.java
+++ 
b/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/SCIMDataBinder.java
@@ -89,6 +89,12 @@ public class SCIMDataBinder {
 
     protected static final List<String> GROUP_SCHEMAS = 
List.of(Resource.Group.schema());
 
+    /**
+     * Translates the given SCIM filter into the equivalent JEXL expression.
+     *
+     * @param filter SCIM filter according to 
https://www.rfc-editor.org/rfc/rfc7644#section-3.4.2.2
+     * @return translated JEXL expression; see 
https://commons.apache.org/proper/commons-jexl/reference/syntax.html
+     * */
     public static String filter2JexlExpression(final String filter) {
         String jexlExpression = filter.
                 replace(" co ", " =~ ").
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 8c94ce70d7..866a28df8e 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/AbstractTaskITCase.java
 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AbstractTaskITCase.java
index 1d39e25235..bed307f121 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AbstractTaskITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/AbstractTaskITCase.java
@@ -24,6 +24,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -45,7 +46,6 @@ import org.apache.syncope.common.rest.api.beans.AnyQuery;
 import org.apache.syncope.common.rest.api.beans.ExecSpecs;
 import org.apache.syncope.common.rest.api.beans.TaskQuery;
 import org.apache.syncope.common.rest.api.service.TaskService;
-import 
org.apache.syncope.core.provisioning.java.job.notification.NotificationJob;
 import org.apache.syncope.fit.AbstractITCase;
 
 public abstract class AbstractTaskITCase extends AbstractITCase {
@@ -100,7 +100,7 @@ public abstract class AbstractTaskITCase extends 
AbstractITCase {
         AtomicReference<TaskTO> taskTO = new 
AtomicReference<>(taskService.read(type, taskKey, true));
         int preSyncSize = taskTO.get().getExecutions().size();
         ExecTO execution = taskService.execute(new 
ExecSpecs.Builder().key(taskKey).dryRun(dryRun).build());
-        assertEquals(initialStatus, execution.getStatus());
+        Optional.ofNullable(initialStatus).ifPresent(status -> 
assertEquals(status, execution.getStatus()));
         assertNotNull(execution.getExecutor());
 
         await().atMost(maxWaitSeconds, TimeUnit.SECONDS).pollInterval(1, 
TimeUnit.SECONDS).until(() -> {
@@ -125,8 +125,7 @@ public abstract class AbstractTaskITCase extends 
AbstractITCase {
     protected static ExecTO execNotificationTask(
             final TaskService taskService, final String taskKey, final int 
maxWaitSeconds) {
 
-        return execTask(taskService, TaskType.NOTIFICATION, taskKey,
-                NotificationJob.Status.SENT.name(), maxWaitSeconds, false);
+        return execTask(taskService, TaskType.NOTIFICATION, taskKey, null, 
maxWaitSeconds, false);
     }
 
     protected void execProvisioningTasks(
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 fa5d5bf61b..02b7bb043c 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
@@ -1188,7 +1188,7 @@ public class UserITCase extends AbstractITCase {
         }
 
         ResourceAR resourceAR = new ResourceAR.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(USER_SERVICE.associate(resourceAR)));
 
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 a6234941d3..f1b4ec73f2 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
@@ -27,6 +27,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 import static org.junit.jupiter.api.Assumptions.assumeFalse;
 
+import jakarta.ws.rs.HttpMethod;
 import jakarta.ws.rs.core.GenericType;
 import jakarta.ws.rs.core.MediaType;
 import jakarta.ws.rs.core.Response;
@@ -475,7 +476,7 @@ public class UserIssuesITCase extends AbstractITCase {
         // 2. request to change password only on testdb (no Syncope, no 
testdb2)
         UserUR userUR = new UserUR();
         userUR.setKey(userTO.getKey());
-        userUR.setPassword(new 
PasswordPatch.Builder().value(getUUIDString()).onSyncope(false).
+        userUR.setPassword(new 
PasswordPatch.Builder().value(UUID.randomUUID().toString()).onSyncope(false).
                 resource(RESOURCE_NAME_TESTDB).build());
 
         ProvisioningResult<UserTO> result = updateUser(userUR);
@@ -1528,9 +1529,8 @@ public class UserIssuesITCase extends AbstractITCase {
                 .value("Other")
                 .build());
 
-        for (int i = 0; i < 2; i++) {
-            updateUser(userUR);
-        }
+        updateUser(userUR);
+        updateUser(userUR);
 
         // 2. remove resources, auxiliary classes and roles
         userUR.getResources().clear();
@@ -1567,17 +1567,34 @@ public class UserIssuesITCase extends AbstractITCase {
                 accept(MediaType.APPLICATION_JSON_TYPE).
                 type(MediaType.APPLICATION_JSON_TYPE);
 
-        Response response = webClient.invoke("PATCH", 
JSON_MAPPER.writeValueAsString(req));
+        Response response = webClient.invoke(HttpMethod.PATCH, 
JSON_MAPPER.writeValueAsString(req));
         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
 
         // Key is mismatched in the path parameter and the request body.
         req.setKey(UUID.randomUUID().toString());
-        response = webClient.invoke("PATCH", 
JSON_MAPPER.writeValueAsString(req));
+        response = webClient.invoke(HttpMethod.PATCH, 
JSON_MAPPER.writeValueAsString(req));
         assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), 
response.getStatus());
-        
+
         // reading user by its username still works
-        userTO = USER_SERVICE.read(userTO.getKey());
-        userTO = USER_SERVICE.read(userTO.getUsername());
+        userTO = USER_SERVICE.read(req.getUsername().getValue());
         assertNotNull(userTO);
     }
+
+    @Test
+    public void issueSYNCOPE1750() {
+        UserCR userCR = UserITCase.getUniqueSample("[email protected]");
+        userCR.getResources().add(RESOURCE_NAME_NOPROPAGATION);
+        UserTO userTO = createUser(userCR).getEntity();
+
+        UserUR req = new UserUR.Builder(userTO.getKey()).password(new 
PasswordPatch.Builder().
+                
onSyncope(false).resource(RESOURCE_NAME_NOPROPAGATION).value("short").build()).build();
+
+        try {
+            USER_SERVICE.update(req);
+            fail();
+        } catch (SyncopeClientException e) {
+            assertEquals(ClientExceptionType.InvalidUser, e.getType());
+            assertTrue(e.getMessage().contains("InvalidPassword: Password must 
be 10 or more characters in length."));
+        }
+    }
 }
diff --git a/pom.xml b/pom.xml
index 600858cb0f..387de1761c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -956,12 +956,12 @@ under the License.
         <version>2.0.0</version>
         <exclusions>
           <exclusion>
-            <groupId>junit</groupId>
-            <artifactId>junit</artifactId>
+            <groupId>com.sun.mail</groupId>
+            <artifactId>jakarta.mail</artifactId>
           </exclusion>
           <exclusion>
-            <groupId>com.sun.activation</groupId>
-            <artifactId>jakarta.activation</artifactId>
+            <groupId>jakarta.activation</groupId>
+            <artifactId>jakarta.activation-api</artifactId>
           </exclusion>
         </exclusions>
       </dependency>
@@ -2139,12 +2139,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