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