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

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


The following commit(s) were added to refs/heads/3_0_X by this push:
     new 48c4f70a6e [SYNCOPE-1759] New REST endpoint: /users/self/compliance 
(#462)
48c4f70a6e is described below

commit 48c4f70a6e4de1aac71295db6c82ba0d288abef9
Author: Francesco Chicchiriccò <[email protected]>
AuthorDate: Fri May 19 13:27:12 2023 +0200

    [SYNCOPE-1759] New REST endpoint: /users/self/compliance (#462)
---
 .../client/console/rest/UserSelfRestClient.java    |   4 +-
 .../console/implementations/MyAccountRule.groovy   |   3 +
 .../console/implementations/MyPasswordRule.groovy  |   3 +
 .../client/enduser/rest/UserSelfRestClient.java    |   3 +-
 .../syncope/common/lib/request/PasswordPatch.java  |   1 -
 .../common/rest/api/beans/ComplianceQuery.java     | 155 +++++++++++++++++++++
 .../common/rest/api/service/UserSelfService.java   |  21 ++-
 .../syncope/core/logic/IdRepoLogicContext.java     |   9 +-
 .../org/apache/syncope/core/logic/UserLogic.java   | 102 +++++++++++---
 .../core/rest/cxf/service/UserSelfServiceImpl.java |   9 +-
 .../core/provisioning/api/rules/AccountRule.java   |   2 +
 .../core/provisioning/api/rules/PasswordRule.java  |   2 +
 .../rules/{PasswordRule.java => RuleEnforcer.java} |  25 ++--
 .../core/spring/policy/DefaultAccountRule.java     |   6 +
 .../core/spring/policy/DefaultPasswordRule.java    |   8 ++
 .../core/spring/policy/DefaultRuleEnforcer.java    | 136 ++++++++++++++++++
 .../spring/policy/HaveIBeenPwnedPasswordRule.java  |   5 +
 .../core/spring/security/SecurityContext.java      |   9 ++
 .../core/spring/security/TestPasswordRule.java     |   5 +
 .../workflow/java/AbstractUserWorkflowAdapter.java | 102 ++------------
 .../workflow/java/DefaultUserWorkflowAdapter.java  |   4 +-
 .../core/workflow/java/WorkflowContext.java        |   3 +
 .../core/flowable/FlowableWorkflowContext.java     |   3 +
 .../flowable/impl/FlowableUserWorkflowAdapter.java |   4 +-
 .../fit/core/reference/TestAccountRule.java        |  11 +-
 .../fit/core/reference/TestPasswordRule.java       |   7 +
 .../org/apache/syncope/fit/core/UserITCase.java    |  26 ++++
 .../apache/syncope/fit/core/UserSelfITCase.java    |   3 +-
 28 files changed, 535 insertions(+), 136 deletions(-)

diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/UserSelfRestClient.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/UserSelfRestClient.java
index 0d8f3b6aa1..fa18cf51ba 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/UserSelfRestClient.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/rest/UserSelfRestClient.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.client.console.rest;
 
+import org.apache.syncope.common.lib.request.PasswordPatch;
 import org.apache.syncope.common.rest.api.service.UserSelfService;
 
 public class UserSelfRestClient extends BaseRestClient {
@@ -25,7 +26,6 @@ public class UserSelfRestClient extends BaseRestClient {
     private static final long serialVersionUID = 100731599744900931L;
 
     public static void changePassword(final String password) {
-        getService(UserSelfService.class).mustChangePassword(password);
+        getService(UserSelfService.class).mustChangePassword(new 
PasswordPatch.Builder().value(password).build());
     }
-
 }
diff --git 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyAccountRule.groovy
 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyAccountRule.groovy
index 8926d21273..1826559aa4 100644
--- 
a/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyAccountRule.groovy
+++ 
b/client/idrepo/console/src/main/resources/org/apache/syncope/client/console/implementations/MyAccountRule.groovy
@@ -24,6 +24,9 @@ import 
org.apache.syncope.core.persistence.api.entity.user.User
 @CompileStatic
 class MyAccountRule implements AccountRule {
   
+  void enforce(String username) {
+  }
+
   void enforce(User user) {
   }
 
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 03a1595a9a..a54f8ceb3c 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,6 +24,9 @@ import 
org.apache.syncope.core.persistence.api.entity.user.User
 @CompileStatic
 class MyPasswordRule implements PasswordRule {
   
+  void enforce(String username, String clearPassword) {
+  }
+
   void enforce(User user, String clearPassword) {
   }
 
diff --git 
a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/UserSelfRestClient.java
 
b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/UserSelfRestClient.java
index 14c78f6fd9..559b9212e0 100644
--- 
a/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/UserSelfRestClient.java
+++ 
b/client/idrepo/enduser/src/main/java/org/apache/syncope/client/enduser/rest/UserSelfRestClient.java
@@ -19,6 +19,7 @@
 package org.apache.syncope.client.enduser.rest;
 
 import javax.ws.rs.core.GenericType;
+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.to.ProvisioningResult;
@@ -30,7 +31,7 @@ public class UserSelfRestClient extends BaseRestClient {
     private static final long serialVersionUID = -1575748964398293968L;
 
     public static void mustChangePassword(final String password) {
-        getService(UserSelfService.class).mustChangePassword(password);
+        getService(UserSelfService.class).mustChangePassword(new 
PasswordPatch.Builder().value(password).build());
     }
 
     public static void requestPasswordReset(final String username, final 
String securityAnswer) {
diff --git 
a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/request/PasswordPatch.java
 
b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/request/PasswordPatch.java
index c681ede524..6202c05ec1 100644
--- 
a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/request/PasswordPatch.java
+++ 
b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/request/PasswordPatch.java
@@ -60,7 +60,6 @@ public class PasswordPatch extends StringReplacePatchItem {
             }
             return this;
         }
-
     }
 
     /**
diff --git 
a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/ComplianceQuery.java
 
b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/ComplianceQuery.java
new file mode 100644
index 0000000000..15bf953482
--- /dev/null
+++ 
b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/ComplianceQuery.java
@@ -0,0 +1,155 @@
+/*
+ * 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.common.rest.api.beans;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+
+public class ComplianceQuery implements Serializable {
+
+    private static final long serialVersionUID = -7324275079761880426L;
+
+    public static class Builder {
+
+        private final ComplianceQuery instance = new ComplianceQuery();
+
+        public Builder username(final String username) {
+            instance.setUsername(username);
+            return this;
+        }
+
+        public Builder password(final String password) {
+            instance.setPassword(password);
+            return this;
+        }
+
+        public Builder realm(final String realm) {
+            instance.setRealm(realm);
+            return this;
+        }
+
+        public ComplianceQuery build() {
+            return instance;
+        }
+
+        public Builder resource(final String resource) {
+            if (resource != null) {
+                instance.getResources().add(resource);
+            }
+            return this;
+        }
+
+        public Builder resources(final String... resources) {
+            instance.getResources().addAll(List.of(resources));
+            return this;
+        }
+
+        public Builder resources(final Collection<String> resources) {
+            if (resources != null) {
+                instance.getResources().addAll(resources);
+            }
+            return this;
+        }
+    }
+
+    private String username;
+
+    private String password;
+
+    private String realm;
+
+    private Set<String> resources = new HashSet<>();
+
+    public String getUsername() {
+        return username;
+    }
+
+    public void setUsername(final String username) {
+        this.username = username;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(final String password) {
+        this.password = password;
+    }
+
+    public String getRealm() {
+        return realm;
+    }
+
+    public void setRealm(final String realm) {
+        this.realm = realm;
+    }
+
+    public Set<String> getResources() {
+        return resources;
+    }
+
+    public void setResources(final Set<String> resources) {
+        this.resources = resources;
+    }
+
+    @JsonIgnore
+    public boolean isEmpty() {
+        if (StringUtils.isBlank(username) && StringUtils.isBlank(password)) {
+            return true;
+        }
+        return StringUtils.isEmpty(realm) && resources.isEmpty();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        ComplianceQuery other = (ComplianceQuery) obj;
+        return new EqualsBuilder().
+                append(username, other.username).
+                append(password, other.password).
+                append(realm, other.realm).
+                append(resources, other.resources).
+                build();
+    }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder().
+                append(username).
+                append(password).
+                append(realm).
+                append(resources).
+                build();
+    }
+}
diff --git 
a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/UserSelfService.java
 
b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/UserSelfService.java
index 8dc4e5448a..a3312b1c69 100644
--- 
a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/UserSelfService.java
+++ 
b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/UserSelfService.java
@@ -41,12 +41,14 @@ import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+import org.apache.syncope.common.lib.request.PasswordPatch;
 import org.apache.syncope.common.lib.request.StatusR;
 import org.apache.syncope.common.lib.request.UserCR;
 import org.apache.syncope.common.lib.request.UserUR;
 import org.apache.syncope.common.lib.to.ProvisioningResult;
 import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.beans.ComplianceQuery;
 
 /**
  * REST operations for user self-management.
@@ -236,7 +238,24 @@ public interface UserSelfService extends JAXRSService {
     @POST
     @Path("mustChangePassword")
     @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML })
-    Response mustChangePassword(String password);
+    @Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML })
+    Response mustChangePassword(@NotNull PasswordPatch password);
+
+    /**
+     * Checks compliance of the given username and / or password with 
applicable policies.
+     * 
+     * @param query compliance query
+     */
+    @ApiResponses(
+            @ApiResponse(responseCode = "204", description = "Operation was 
successful"))
+    @Operation(security = {
+        @SecurityRequirement(name = "BasicAuthentication"),
+        @SecurityRequirement(name = "Bearer") })
+    @POST
+    @Path("compliance")
+    @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML })
+    @Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, 
MediaType.APPLICATION_XML })
+    void compliance(@NotNull ComplianceQuery query);
 
     /**
      * Provides answer for the security question configured for user matching 
the given username, if any.
diff --git 
a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/IdRepoLogicContext.java
 
b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/IdRepoLogicContext.java
index 9dc3d19977..dcc594a2a7 100644
--- 
a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/IdRepoLogicContext.java
+++ 
b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/IdRepoLogicContext.java
@@ -103,6 +103,7 @@ import 
org.apache.syncope.core.provisioning.api.notification.NotificationJobDele
 import 
org.apache.syncope.core.provisioning.api.notification.NotificationManager;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationManager;
 import 
org.apache.syncope.core.provisioning.api.propagation.PropagationTaskExecutor;
+import org.apache.syncope.core.provisioning.api.rules.RuleEnforcer;
 import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
 import org.apache.syncope.core.spring.security.SecurityProperties;
 import org.apache.syncope.core.workflow.api.AnyObjectWorkflowAdapter;
@@ -546,11 +547,13 @@ public class IdRepoLogicContext {
             final UserDAO userDAO,
             final GroupDAO groupDAO,
             final AnySearchDAO anySearchDAO,
+            final ExternalResourceDAO resourceDAO,
             final AccessTokenDAO accessTokenDAO,
             final DelegationDAO delegationDAO,
             final ConfParamOps confParamOps,
             final UserProvisioningManager provisioningManager,
-            final SyncopeLogic syncopeLogic) {
+            final SyncopeLogic syncopeLogic,
+            final RuleEnforcer ruleEnforcer) {
 
         return new UserLogic(
                 realmDAO,
@@ -559,11 +562,13 @@ public class IdRepoLogicContext {
                 userDAO,
                 groupDAO,
                 anySearchDAO,
+                resourceDAO,
                 accessTokenDAO,
                 delegationDAO,
                 confParamOps,
                 binder,
                 provisioningManager,
-                syncopeLogic);
+                syncopeLogic,
+                ruleEnforcer);
     }
 }
diff --git 
a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java 
b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java
index 023d5e21fc..2d02fc9fbc 100644
--- 
a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java
+++ 
b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java
@@ -26,6 +26,7 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.commons.lang3.tuple.Triple;
 import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
@@ -43,13 +44,17 @@ import org.apache.syncope.common.lib.to.ProvisioningResult;
 import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
+import org.apache.syncope.common.lib.types.EntityViolationType;
 import org.apache.syncope.common.lib.types.IdRepoEntitlement;
 import org.apache.syncope.common.lib.types.PatchOperation;
+import org.apache.syncope.common.rest.api.beans.ComplianceQuery;
 import org.apache.syncope.core.logic.api.LogicActions;
+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.AnySearchDAO;
 import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
 import org.apache.syncope.core.persistence.api.dao.DelegationDAO;
+import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.NotFoundException;
 import org.apache.syncope.core.persistence.api.dao.RealmDAO;
@@ -57,14 +62,21 @@ import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
 import org.apache.syncope.core.persistence.api.entity.AccessToken;
+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.Realm;
 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.User;
 import org.apache.syncope.core.provisioning.api.UserProvisioningManager;
 import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
+import org.apache.syncope.core.provisioning.api.rules.RuleEnforcer;
 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 import org.apache.syncope.core.provisioning.api.utils.RealmUtils;
 import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
+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.springframework.security.access.prepost.PreAuthorize;
@@ -82,6 +94,8 @@ public class UserLogic extends AbstractAnyLogic<UserTO, 
UserCR, UserUR> {
 
     protected final AnySearchDAO searchDAO;
 
+    protected final ExternalResourceDAO resourceDAO;
+
     protected final AccessTokenDAO accessTokenDAO;
 
     protected final DelegationDAO delegationDAO;
@@ -94,6 +108,8 @@ public class UserLogic extends AbstractAnyLogic<UserTO, 
UserCR, UserUR> {
 
     protected final SyncopeLogic syncopeLogic;
 
+    protected final RuleEnforcer ruleEnforcer;
+
     public UserLogic(
             final RealmDAO realmDAO,
             final AnyTypeDAO anyTypeDAO,
@@ -101,24 +117,28 @@ public class UserLogic extends AbstractAnyLogic<UserTO, 
UserCR, UserUR> {
             final UserDAO userDAO,
             final GroupDAO groupDAO,
             final AnySearchDAO searchDAO,
+            final ExternalResourceDAO resourceDAO,
             final AccessTokenDAO accessTokenDAO,
             final DelegationDAO delegationDAO,
             final ConfParamOps confParamOps,
             final UserDataBinder binder,
             final UserProvisioningManager provisioningManager,
-            final SyncopeLogic syncopeLogic) {
+            final SyncopeLogic syncopeLogic,
+            final RuleEnforcer ruleEnforcer) {
 
         super(realmDAO, anyTypeDAO, templateUtils);
 
         this.userDAO = userDAO;
         this.groupDAO = groupDAO;
         this.searchDAO = searchDAO;
+        this.resourceDAO = resourceDAO;
         this.accessTokenDAO = accessTokenDAO;
         this.delegationDAO = delegationDAO;
         this.confParamOps = confParamOps;
         this.binder = binder;
         this.provisioningManager = provisioningManager;
         this.syncopeLogic = syncopeLogic;
+        this.ruleEnforcer = ruleEnforcer;
     }
 
     @PreAuthorize("isAuthenticated() and not(hasRole('" + 
IdRepoEntitlement.MUST_CHANGE_PASSWORD + "'))")
@@ -337,15 +357,17 @@ public class UserLogic extends AbstractAnyLogic<UserTO, 
UserCR, UserUR> {
     }
 
     @PreAuthorize("hasRole('" + IdRepoEntitlement.MUST_CHANGE_PASSWORD + "')")
-    public ProvisioningResult<UserTO> mustChangePassword(final String 
password, final boolean nullPriorityAsync) {
+    public ProvisioningResult<UserTO> mustChangePassword(
+            final PasswordPatch password, final boolean nullPriorityAsync) {
+
         UserTO userTO = binder.getAuthenticatedUserTO();
 
+        password.setOnSyncope(true);
+        password.getResources().clear();
+        
password.getResources().addAll(userDAO.findAllResourceKeys(userTO.getKey()));
+
         UserUR userUR = new UserUR.Builder(userTO.getKey()).
-                password(new PasswordPatch.Builder().
-                        value(password).
-                        onSyncope(true).
-                        
resources(userDAO.findAllResourceKeys(userTO.getKey())).
-                        build()).
+                password(password).
                 mustChangePassword(new 
BooleanReplacePatchItem.Builder().value(false).build()).
                 build();
         ProvisioningResult<UserTO> result = selfUpdate(userUR, 
nullPriorityAsync);
@@ -357,16 +379,61 @@ public class UserLogic extends AbstractAnyLogic<UserTO, 
UserCR, UserUR> {
     }
 
     @PreAuthorize("isAnonymous() or hasRole('" + IdRepoEntitlement.ANONYMOUS + 
"')")
-    @Transactional
-    public void requestPasswordReset(final String username, final String 
securityAnswer) {
-        if (username == null) {
-            throw new NotFoundException("Null username");
+    @Transactional(readOnly = true)
+    public void compliance(final ComplianceQuery query) {
+        SyncopeClientException sce = 
SyncopeClientException.build(ClientExceptionType.RESTValidation);
+
+        if (query.isEmpty()) {
+            sce.getElements().add("Nothing to check");
+            throw sce;
+        }
+
+        Realm realm = null;
+        if (StringUtils.isNotBlank(query.getRealm())) {
+            realm = 
Optional.ofNullable(realmDAO.findByFullPath(query.getRealm())).
+                    orElseThrow(() -> new NotFoundException("Realm " + 
query.getRealm()));
+        }
+        Set<ExternalResource> resources = query.getResources().stream().
+                
map(resourceDAO::find).filter(Objects::nonNull).collect(Collectors.toSet());
+        if (realm == null && resources.isEmpty()) {
+            sce.getElements().add("Nothing to check");
+            throw sce;
+        }
+
+        if (StringUtils.isNotBlank(query.getUsername())) {
+            List<AccountPolicy> accountPolicies = 
ruleEnforcer.getAccountPolicies(realm, resources);
+            try {
+                if (accountPolicies.isEmpty()) {
+                    if 
(!Entity.ID_PATTERN.matcher(query.getUsername()).matches()) {
+                        throw new AccountPolicyException("Character(s) not 
allowed: " + query.getUsername());
+                    }
+                } else {
+                    for (AccountPolicy policy : accountPolicies) {
+                        ruleEnforcer.getAccountRules(policy).forEach(rule -> 
rule.enforce(query.getUsername()));
+                    }
+                }
+            } catch (AccountPolicyException e) {
+                throw new InvalidEntityException(User.class, 
EntityViolationType.InvalidUsername, e.getMessage());
+            }
         }
 
-        User user = userDAO.findByUsername(username);
-        if (user == null) {
-            throw new NotFoundException("User " + username);
+        if (StringUtils.isNotBlank(query.getPassword())) {
+            try {
+                for (PasswordPolicy policy : 
ruleEnforcer.getPasswordPolicies(realm, resources)) {
+                    ruleEnforcer.getPasswordRules(policy).
+                            forEach(rule -> rule.enforce(query.getUsername(), 
query.getPassword()));
+                }
+            } catch (PasswordPolicyException e) {
+                throw new InvalidEntityException(User.class, 
EntityViolationType.InvalidPassword, e.getMessage());
+            }
         }
+    }
+
+    @PreAuthorize("isAnonymous() or hasRole('" + IdRepoEntitlement.ANONYMOUS + 
"')")
+    @Transactional
+    public void requestPasswordReset(final String username, final String 
securityAnswer) {
+        User user = Optional.ofNullable(userDAO.findByUsername(username)).
+                orElseThrow(() -> new NotFoundException("User " + username));
 
         if (syncopeLogic.isPwdResetRequiringSecurityQuestions()
                 && (securityAnswer == null || !Encryptor.getInstance().
@@ -381,10 +448,9 @@ public class UserLogic extends AbstractAnyLogic<UserTO, 
UserCR, UserUR> {
     @PreAuthorize("isAnonymous() or hasRole('" + IdRepoEntitlement.ANONYMOUS + 
"')")
     @Transactional
     public void confirmPasswordReset(final String token, final String 
password) {
-        User user = userDAO.findByToken(token);
-        if (user == null) {
-            throw new NotFoundException("User with token " + token);
-        }
+        User user = Optional.ofNullable(userDAO.findByToken(token)).
+                orElseThrow(() -> new NotFoundException("User with token " + 
token));
+
         provisioningManager.confirmPasswordReset(
                 user.getKey(), token, password, 
AuthContextUtils.getUsername(), REST_CONTEXT);
     }
diff --git 
a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserSelfServiceImpl.java
 
b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserSelfServiceImpl.java
index 61b0f9bb16..76a281f2d4 100644
--- 
a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserSelfServiceImpl.java
+++ 
b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/UserSelfServiceImpl.java
@@ -22,6 +22,7 @@ import javax.ws.rs.core.Response;
 import org.apache.commons.lang3.tuple.Triple;
 import org.apache.syncope.common.lib.AnyOperations;
 import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.request.PasswordPatch;
 import org.apache.syncope.common.lib.request.StatusR;
 import org.apache.syncope.common.lib.request.UserCR;
 import org.apache.syncope.common.lib.request.UserUR;
@@ -29,6 +30,7 @@ import org.apache.syncope.common.lib.to.ProvisioningResult;
 import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.beans.ComplianceQuery;
 import org.apache.syncope.common.rest.api.service.UserSelfService;
 import org.apache.syncope.core.logic.SyncopeLogic;
 import org.apache.syncope.core.logic.UserLogic;
@@ -94,11 +96,16 @@ public class UserSelfServiceImpl extends AbstractService 
implements UserSelfServ
     }
 
     @Override
-    public Response mustChangePassword(final String password) {
+    public Response mustChangePassword(final PasswordPatch password) {
         ProvisioningResult<UserTO> updated = 
logic.mustChangePassword(password, isNullPriorityAsync());
         return modificationResponse(updated);
     }
 
+    @Override
+    public void compliance(final ComplianceQuery query) {
+        logic.compliance(query);
+    }
+
     @Override
     public void requestPasswordReset(final String username, final String 
securityAnswer) {
         if (!syncopeLogic.isPwdResetAllowed()) {
diff --git 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/rules/AccountRule.java
 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/rules/AccountRule.java
index 4dfb32562e..24721d1b0b 100644
--- 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/rules/AccountRule.java
+++ 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/rules/AccountRule.java
@@ -30,6 +30,8 @@ public interface AccountRule {
     default void setConf(AccountRuleConf conf) {
     }
 
+    void enforce(String username);
+
     void enforce(User user);
 
     void enforce(LinkedAccount accout);
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 19ef16e79c..763243e462 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,6 +34,8 @@ public interface PasswordRule {
     default void setConf(PasswordRuleConf conf) {
     }
 
+    void enforce(String username, String clearPassword);
+
     void enforce(User user, String clearPassword);
 
     void enforce(LinkedAccount account);
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/RuleEnforcer.java
similarity index 55%
copy from 
core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/rules/PasswordRule.java
copy to 
core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/rules/RuleEnforcer.java
index 19ef16e79c..e9a98ec444 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/RuleEnforcer.java
@@ -18,23 +18,20 @@
  */
 package org.apache.syncope.core.provisioning.api.rules;
 
-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.Collection;
+import java.util.List;
+import org.apache.syncope.core.persistence.api.entity.ExternalResource;
+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;
 
-/**
- * Interface for enforcing a given password rule to user.
- */
-public interface PasswordRule {
+public interface RuleEnforcer {
 
-    default PasswordRuleConf getConf() {
-        return null;
-    }
+    List<AccountPolicy> getAccountPolicies(Realm realm, 
Collection<ExternalResource> resources);
 
-    default void setConf(PasswordRuleConf conf) {
-    }
+    List<AccountRule> getAccountRules(AccountPolicy policy);
 
-    void enforce(User user, String clearPassword);
+    List<PasswordPolicy> getPasswordPolicies(Realm realm, 
Collection<ExternalResource> resources);
 
-    void enforce(LinkedAccount account);
+    List<PasswordRule> getPasswordRules(PasswordPolicy policy);
 }
diff --git 
a/core/spring/src/main/java/org/apache/syncope/core/spring/policy/DefaultAccountRule.java
 
b/core/spring/src/main/java/org/apache/syncope/core/spring/policy/DefaultAccountRule.java
index dc0bf2c995..0cb4b1e7b9 100644
--- 
a/core/spring/src/main/java/org/apache/syncope/core/spring/policy/DefaultAccountRule.java
+++ 
b/core/spring/src/main/java/org/apache/syncope/core/spring/policy/DefaultAccountRule.java
@@ -98,6 +98,12 @@ public class DefaultAccountRule implements AccountRule {
                 });
     }
 
+    @Override
+    public void enforce(final String username) {
+        Set<String> wordsNotPermitted = new 
HashSet<>(conf.getWordsNotPermitted());
+        enforce(username, wordsNotPermitted);
+    }
+
     @Transactional(readOnly = true)
     @Override
     public void enforce(final User user) {
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 ae63f4df25..e9a63c2af7 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
@@ -163,6 +163,14 @@ public class DefaultPasswordRule implements PasswordRule {
                 });
     }
 
+    @Override
+    public void enforce(final String username, final String clearPassword) {
+        if (clearPassword != null) {
+            Set<String> wordsNotPermitted = new 
HashSet<>(conf.getWordsNotPermitted());
+            enforce(clearPassword, username, wordsNotPermitted);
+        }
+    }
+
     @Transactional(readOnly = true)
     @Override
     public void enforce(final User user, final String clearPassword) {
diff --git 
a/core/spring/src/main/java/org/apache/syncope/core/spring/policy/DefaultRuleEnforcer.java
 
b/core/spring/src/main/java/org/apache/syncope/core/spring/policy/DefaultRuleEnforcer.java
new file mode 100644
index 0000000000..4a8e925de5
--- /dev/null
+++ 
b/core/spring/src/main/java/org/apache/syncope/core/spring/policy/DefaultRuleEnforcer.java
@@ -0,0 +1,136 @@
+/*
+ * 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.spring.policy;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.syncope.core.persistence.api.dao.RealmDAO;
+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.provisioning.api.rules.AccountRule;
+import org.apache.syncope.core.provisioning.api.rules.PasswordRule;
+import org.apache.syncope.core.provisioning.api.rules.RuleEnforcer;
+import org.apache.syncope.core.spring.implementation.ImplementationManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.transaction.annotation.Transactional;
+
+public class DefaultRuleEnforcer implements RuleEnforcer {
+
+    protected static final Logger LOG = 
LoggerFactory.getLogger(RuleEnforcer.class);
+
+    protected final RealmDAO realmDAO;
+
+    protected final Map<String, AccountRule> perContextAccountRules = new 
ConcurrentHashMap<>();
+
+    protected final Map<String, PasswordRule> perContextPasswordRules = new 
ConcurrentHashMap<>();
+
+    public DefaultRuleEnforcer(final RealmDAO realmDAO) {
+        this.realmDAO = realmDAO;
+    }
+
+    @Transactional(readOnly = true)
+    @Override
+    public List<AccountPolicy> getAccountPolicies(final Realm realm, final 
Collection<ExternalResource> resources) {
+        List<AccountPolicy> policies = new ArrayList<>();
+
+        // add resource policies
+        resources.forEach(resource -> 
Optional.ofNullable(resource.getAccountPolicy()).
+                filter(p -> !policies.contains(p)).
+                ifPresent(policies::add));
+
+        // add realm policies
+        if (realm != null) {
+            realmDAO.findAncestors(realm).
+                    forEach(r -> Optional.ofNullable(r.getAccountPolicy()).
+                    filter(p -> !policies.contains(p)).
+                    ifPresent(policies::add));
+        }
+
+        return policies;
+    }
+
+    @Transactional(readOnly = true)
+    @Override
+    public 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;
+    }
+
+    @Transactional(readOnly = true)
+    @Override
+    public List<PasswordPolicy> getPasswordPolicies(final Realm realm, final 
Collection<ExternalResource> resources) {
+        List<PasswordPolicy> policies = new ArrayList<>();
+
+        // add resource policies
+        resources.forEach(resource -> 
Optional.ofNullable(resource.getPasswordPolicy()).
+                filter(p -> !policies.contains(p)).
+                ifPresent(policies::add));
+
+        // add realm policies
+        if (realm != null) {
+            realmDAO.findAncestors(realm).
+                    forEach(r -> Optional.ofNullable(r.getPasswordPolicy()).
+                    filter(p -> !policies.contains(p)).
+                    ifPresent(policies::add));
+        }
+
+        return policies;
+    }
+
+    @Transactional(readOnly = true)
+    @Override
+    public 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;
+    }
+}
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 e66eb215e2..11b71dec4b 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
@@ -97,6 +97,11 @@ public class HaveIBeenPwnedPasswordRule implements 
PasswordRule {
         }
     }
 
+    @Override
+    public void enforce(final String username, final String clearPassword) {
+        Optional.ofNullable(clearPassword).ifPresent(this::enforce);
+    }
+
     @Transactional(readOnly = true)
     @Override
     public void enforce(final User user, final String clearPassword) {
diff --git 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityContext.java
 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityContext.java
index 486ab1612d..7a5624837a 100644
--- 
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityContext.java
+++ 
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/SecurityContext.java
@@ -24,7 +24,10 @@ import com.nimbusds.jose.KeyLengthException;
 import java.security.NoSuchAlgorithmException;
 import java.security.spec.InvalidKeySpecException;
 import org.apache.syncope.common.lib.types.CipherAlgorithm;
+import org.apache.syncope.core.persistence.api.dao.RealmDAO;
+import org.apache.syncope.core.provisioning.api.rules.RuleEnforcer;
 import org.apache.syncope.core.spring.ApplicationContextProvider;
+import org.apache.syncope.core.spring.policy.DefaultRuleEnforcer;
 import org.apache.syncope.core.spring.security.jws.AccessTokenJWSSigner;
 import org.apache.syncope.core.spring.security.jws.AccessTokenJWSVerifier;
 import org.slf4j.Logger;
@@ -112,6 +115,12 @@ public class SecurityContext {
         return new DefaultPasswordGenerator();
     }
 
+    @ConditionalOnMissingBean
+    @Bean
+    public RuleEnforcer ruleEnforcer(final RealmDAO realmDAO) {
+        return new DefaultRuleEnforcer(realmDAO);
+    }
+
     @Bean
     public GrantedAuthorityDefaults grantedAuthorityDefaults() {
         return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix
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 61448f0e0e..bbc33d1f4f 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
@@ -45,6 +45,11 @@ public class TestPasswordRule implements PasswordRule {
         }
     }
 
+    @Override
+    public void enforce(final String username, final String clearPassword) {
+        // nothing to do
+    }
+
     @Override
     public void enforce(final User user, final String clearPassword) {
         // nothing to do
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 129adb5ec2..f6e9e52f2c 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,14 +18,10 @@
  */
 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;
@@ -38,18 +34,13 @@ 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.provisioning.api.rules.RuleEnforcer;
 import org.apache.syncope.core.spring.policy.AccountPolicyException;
 import org.apache.syncope.core.spring.policy.PasswordPolicyException;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
@@ -76,22 +67,22 @@ public abstract class AbstractUserWorkflowAdapter extends 
AbstractWorkflowAdapte
 
     protected final SecurityProperties securityProperties;
 
-    protected final Map<String, AccountRule> perContextAccountRules = new 
ConcurrentHashMap<>();
-
-    protected final Map<String, PasswordRule> perContextPasswordRules = new 
ConcurrentHashMap<>();
+    protected final RuleEnforcer ruleEnforcer;
 
     public AbstractUserWorkflowAdapter(
             final UserDataBinder dataBinder,
             final UserDAO userDAO,
             final RealmDAO realmDAO,
             final EntityFactory entityFactory,
-            final SecurityProperties securityProperties) {
+            final SecurityProperties securityProperties,
+            final RuleEnforcer ruleEnforcer) {
 
         this.dataBinder = dataBinder;
         this.userDAO = userDAO;
         this.realmDAO = realmDAO;
         this.entityFactory = entityFactory;
         this.securityProperties = securityProperties;
+        this.ruleEnforcer = ruleEnforcer;
     }
 
     @Override
@@ -99,78 +90,6 @@ 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,
@@ -184,12 +103,14 @@ public abstract class AbstractUserWorkflowAdapter extends 
AbstractWorkflowAdapte
 
             try {
                 int maxPPSpecHistory = 0;
-                for (PasswordPolicy policy : getPasswordPolicies(user)) {
+                for (PasswordPolicy policy : ruleEnforcer.getPasswordPolicies(
+                        user.getRealm(), userDAO.findAllResources(user))) {
+
                     if (clearPassword == null && 
!policy.isAllowNullPassword()) {
                         throw new PasswordPolicyException("Password 
mandatory");
                     }
 
-                    getPasswordRules(policy).forEach(rule -> {
+                    ruleEnforcer.getPasswordRules(policy).forEach(rule -> {
                         rule.enforce(user, clearPassword);
 
                         user.getLinkedAccounts().stream().
@@ -253,7 +174,8 @@ public abstract class AbstractUserWorkflowAdapter extends 
AbstractWorkflowAdapte
                 throw new AccountPolicyException("Not allowed: " + 
user.getUsername());
             }
 
-            List<AccountPolicy> accountPolicies = getAccountPolicies(user);
+            List<AccountPolicy> accountPolicies =
+                    ruleEnforcer.getAccountPolicies(user.getRealm(), 
userDAO.findAllResources(user));
             if (accountPolicies.isEmpty()) {
                 if (!Entity.ID_PATTERN.matcher(user.getUsername()).matches()) {
                     throw new AccountPolicyException("Character(s) not 
allowed: " + user.getUsername());
@@ -267,7 +189,7 @@ public abstract class AbstractUserWorkflowAdapter extends 
AbstractWorkflowAdapte
                         });
             } else {
                 for (AccountPolicy policy : accountPolicies) {
-                    getAccountRules(policy).forEach(rule -> {
+                    ruleEnforcer.getAccountRules(policy).forEach(rule -> {
                         rule.enforce(user);
 
                         user.getLinkedAccounts().stream().
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 a616640263..00029ed236 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
@@ -32,6 +32,7 @@ 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.event.AnyLifecycleEvent;
+import org.apache.syncope.core.provisioning.api.rules.RuleEnforcer;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.apache.syncope.core.spring.security.SecurityProperties;
 import org.apache.syncope.core.workflow.api.WorkflowException;
@@ -53,10 +54,11 @@ public class DefaultUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter {
             final RealmDAO realmDAO,
             final EntityFactory entityFactory,
             final SecurityProperties securityProperties,
+            final RuleEnforcer ruleEnforcer,
             final ConfParamOps confParamOps,
             final ApplicationEventPublisher publisher) {
 
-        super(dataBinder, userDAO, realmDAO, entityFactory, 
securityProperties);
+        super(dataBinder, userDAO, realmDAO, entityFactory, 
securityProperties, ruleEnforcer);
         this.confParamOps = confParamOps;
         this.publisher = publisher;
     }
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 bd4de35dc7..7dd7a4ee8a 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
@@ -27,6 +27,7 @@ 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.provisioning.api.rules.RuleEnforcer;
 import org.apache.syncope.core.spring.security.SecurityProperties;
 import org.apache.syncope.core.workflow.api.AnyObjectWorkflowAdapter;
 import org.apache.syncope.core.workflow.api.GroupWorkflowAdapter;
@@ -47,6 +48,7 @@ public class WorkflowContext {
             final RealmDAO realmDAO,
             final EntityFactory entityFactory,
             final SecurityProperties securityProperties,
+            final RuleEnforcer ruleEnforcer,
             final ConfParamOps confParamOps,
             final ApplicationEventPublisher publisher) {
 
@@ -56,6 +58,7 @@ public class WorkflowContext {
                 realmDAO,
                 entityFactory,
                 securityProperties,
+                ruleEnforcer,
                 confParamOps,
                 publisher);
     }
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 4882119fe5..9d0335fb53 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
@@ -45,6 +45,7 @@ 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;
 import 
org.apache.syncope.core.provisioning.api.notification.NotificationManager;
+import org.apache.syncope.core.provisioning.api.rules.RuleEnforcer;
 import org.apache.syncope.core.spring.security.SecurityProperties;
 import org.apache.syncope.core.workflow.api.UserWorkflowAdapter;
 import org.flowable.common.engine.impl.AbstractEngineConfiguration;
@@ -173,6 +174,7 @@ public class FlowableWorkflowContext {
             final RealmDAO realmDAO,
             final EntityFactory entityFactory,
             final SecurityProperties securityProperties,
+            final RuleEnforcer ruleEnforcer,
             final DomainProcessEngine engine,
             final UserRequestHandler userRequestHandler,
             final ApplicationEventPublisher publisher) {
@@ -183,6 +185,7 @@ public class FlowableWorkflowContext {
                 realmDAO,
                 entityFactory,
                 securityProperties,
+                ruleEnforcer,
                 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 81777294ec..b97780db1e 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
@@ -43,6 +43,7 @@ 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.event.AnyLifecycleEvent;
+import org.apache.syncope.core.provisioning.api.rules.RuleEnforcer;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.apache.syncope.core.spring.security.SecurityProperties;
 import org.apache.syncope.core.workflow.api.WorkflowException;
@@ -72,11 +73,12 @@ public class FlowableUserWorkflowAdapter extends 
AbstractUserWorkflowAdapter imp
             final RealmDAO realmDAO,
             final EntityFactory entityFactory,
             final SecurityProperties securityProperties,
+            final RuleEnforcer ruleEnforcer,
             final DomainProcessEngine engine,
             final UserRequestHandler userRequestHandler,
             final ApplicationEventPublisher publisher) {
 
-        super(dataBinder, userDAO, realmDAO, entityFactory, 
securityProperties);
+        super(dataBinder, userDAO, realmDAO, entityFactory, 
securityProperties, ruleEnforcer);
         this.engine = engine;
         this.userRequestHandler = userRequestHandler;
         this.publisher = publisher;
diff --git 
a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/TestAccountRule.java
 
b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/TestAccountRule.java
index 30188323c2..4430b1fc03 100644
--- 
a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/TestAccountRule.java
+++ 
b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/TestAccountRule.java
@@ -41,14 +41,19 @@ public class TestAccountRule implements AccountRule {
         }
     }
 
-    @Transactional(readOnly = true)
     @Override
-    public void enforce(final User user) {
-        if (!user.getUsername().contains(conf.getMustContainSubstring())) {
+    public void enforce(final String username) {
+        if (!username.contains(conf.getMustContainSubstring())) {
             throw new AccountPolicyException("Username not containing " + 
conf.getMustContainSubstring());
         }
     }
 
+    @Transactional(readOnly = true)
+    @Override
+    public void enforce(final User user) {
+        enforce(user.getUsername());
+    }
+
     @Transactional(readOnly = true)
     @Override
     public void enforce(final LinkedAccount accout) {
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 866a28df8e..311c788ea9 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
@@ -53,6 +53,13 @@ public class TestPasswordRule implements PasswordRule {
         }
     }
 
+    @Override
+    public void enforce(final String username, final String clearPassword) {
+        if (clearPassword != null && 
!clearPassword.endsWith(conf.getMustEndWith())) {
+            throw new PasswordPolicyException("Password not ending with " + 
conf.getMustEndWith());
+        }
+    }
+
     @Transactional(readOnly = true)
     @Override
     public void enforce(final User user, final String clearPassword) {
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 80593f0f95..14b94ce4e6 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
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.fit.core;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -85,6 +86,7 @@ import org.apache.syncope.common.lib.types.TaskType;
 import org.apache.syncope.common.rest.api.RESTHeaders;
 import org.apache.syncope.common.rest.api.batch.BatchResponseItem;
 import org.apache.syncope.common.rest.api.beans.AnyQuery;
+import org.apache.syncope.common.rest.api.beans.ComplianceQuery;
 import org.apache.syncope.common.rest.api.beans.RealmQuery;
 import org.apache.syncope.common.rest.api.beans.TaskQuery;
 import org.apache.syncope.common.rest.api.service.ResourceService;
@@ -973,6 +975,15 @@ public class UserITCase extends AbstractITCase {
         try {
             UserCR userCR = 
getUniqueSample("[email protected]");
             userCR.setRealm(realm.getFullPath());
+
+            try {
+                ANONYMOUS_CLIENT.getService(UserSelfService.class).compliance(
+                        new 
ComplianceQuery.Builder().password(userCR.getPassword()).realm(userCR.getRealm()).build());
+            } catch (SyncopeClientException e) {
+                assertEquals(ClientExceptionType.InvalidUser, e.getType());
+                
assertTrue(e.getElements().iterator().next().startsWith("InvalidPassword"));
+            }
+
             try {
                 createUser(userCR);
                 fail("This should not happen");
@@ -981,7 +992,16 @@ public class UserITCase extends AbstractITCase {
                 
assertTrue(e.getElements().iterator().next().startsWith("InvalidPassword"));
             }
 
+            try {
+                ANONYMOUS_CLIENT.getService(UserSelfService.class).compliance(
+                        new 
ComplianceQuery.Builder().username(userCR.getUsername()).realm(userCR.getRealm()).build());
+            } catch (SyncopeClientException e) {
+                assertEquals(ClientExceptionType.InvalidUser, e.getType());
+                
assertTrue(e.getElements().iterator().next().startsWith("InvalidUsername"));
+            }
+
             userCR.setPassword(userCR.getPassword() + "XXX");
+
             try {
                 createUser(userCR);
                 fail("This should not happen");
@@ -991,6 +1011,12 @@ public class UserITCase extends AbstractITCase {
             }
 
             userCR.setUsername("YYY" + userCR.getUsername());
+
+            assertDoesNotThrow(() -> 
ANONYMOUS_CLIENT.getService(UserSelfService.class).compliance(
+                    new 
ComplianceQuery.Builder().password(userCR.getPassword()).realm(userCR.getRealm()).build()));
+            assertDoesNotThrow(() -> 
ANONYMOUS_CLIENT.getService(UserSelfService.class).compliance(
+                    new 
ComplianceQuery.Builder().username(userCR.getUsername()).realm(userCR.getRealm()).build()));
+
             UserTO userTO = createUser(userCR).getEntity();
             assertNotNull(userTO);
         } finally {
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserSelfITCase.java
 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserSelfITCase.java
index c4596e7dad..031df76845 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserSelfITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserSelfITCase.java
@@ -444,7 +444,8 @@ public class UserSelfITCase extends AbstractITCase {
         }
 
         // 3. change password
-        
vivaldiClient.getService(UserSelfService.class).mustChangePassword("password123");
+        vivaldiClient.getService(UserSelfService.class).
+                mustChangePassword(new 
PasswordPatch.Builder().value("password123").build());
 
         // 4. verify it worked
         Triple<Map<String, Set<String>>, List<String>, UserTO> self =

Reply via email to