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>