This is an automated email from the ASF dual-hosted git repository.
ilgrosso pushed a commit to branch 4_1_X
in repository https://gitbox.apache.org/repos/asf/syncope.git
The following commit(s) were added to refs/heads/4_1_X by this push:
new b09d0a7076 [SYNCOPE-1979] check also for not permitted words and
schemas as substrings in default password rule (#1432)
b09d0a7076 is described below
commit b09d0a707643d42c7b907b01158d909ed658ce9c
Author: Andrea Patricelli <[email protected]>
AuthorDate: Wed Jun 24 14:43:07 2026 +0200
[SYNCOPE-1979] check also for not permitted words and schemas as substrings
in default password rule (#1432)
---
.../common/lib/policy/DefaultPasswordRuleConf.java | 35 +++++++++++++++++++--
.../core/persistence/neo4j/PersistenceContext.java | 6 ++--
.../neo4j/dao/repo/ImplementationRepoExtImpl.java | 34 ++++++++++++++++++++
.../core/spring/policy/DefaultPasswordRule.java | 13 ++++++--
.../core/spring/security/PasswordGenerator.java | 13 ++++++--
.../org/apache/syncope/fit/core/PolicyITCase.java | 36 ++++++++++++++++++++++
6 files changed, 126 insertions(+), 11 deletions(-)
diff --git
a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/policy/DefaultPasswordRuleConf.java
b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/policy/DefaultPasswordRuleConf.java
index e38c3bcb30..0732955b78 100644
---
a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/policy/DefaultPasswordRuleConf.java
+++
b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/policy/DefaultPasswordRuleConf.java
@@ -54,17 +54,22 @@ public class DefaultPasswordRuleConf extends
AbstractPasswordRuleConf {
private boolean usernameAllowed;
/**
- * Substrings not permitted.
+ * Words not permitted.
*/
private final List<String> wordsNotPermitted = new ArrayList<>();
/**
* User attribute values not permitted.
*/
- @Schema(anyTypeKind = AnyTypeKind.USER,
- type = { SchemaType.PLAIN, SchemaType.DERIVED })
+ @Schema(anyTypeKind = AnyTypeKind.USER, type = { SchemaType.PLAIN,
SchemaType.DERIVED })
private final List<String> schemasNotPermitted = new ArrayList<>();
+ private boolean notPermittedCaseSensitive;
+
+ private boolean notPermittedAsSubstrings;
+
+ private boolean notPermittedBackwards;
+
public int getMaxLength() {
return maxLength;
}
@@ -152,4 +157,28 @@ public class DefaultPasswordRuleConf extends
AbstractPasswordRuleConf {
public List<String> getSchemasNotPermitted() {
return schemasNotPermitted;
}
+
+ public boolean isNotPermittedCaseSensitive() {
+ return notPermittedCaseSensitive;
+ }
+
+ public void setNotPermittedCaseSensitive(final boolean
notPermittedCaseSensitive) {
+ this.notPermittedCaseSensitive = notPermittedCaseSensitive;
+ }
+
+ public boolean isNotPermittedAsSubstrings() {
+ return notPermittedAsSubstrings;
+ }
+
+ public void setNotPermittedAsSubstrings(final boolean
notPermittedAsSubstrings) {
+ this.notPermittedAsSubstrings = notPermittedAsSubstrings;
+ }
+
+ public boolean isNotPermittedBackwards() {
+ return notPermittedBackwards;
+ }
+
+ public void setNotPermittedBackwards(final boolean notPermittedBackwards) {
+ this.notPermittedBackwards = notPermittedBackwards;
+ }
}
diff --git
a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/PersistenceContext.java
b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/PersistenceContext.java
index 4f95ba6705..0ab68d4c4b 100644
---
a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/PersistenceContext.java
+++
b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/PersistenceContext.java
@@ -944,14 +944,16 @@ public class PersistenceContext {
@Bean
public ImplementationRepoExt implementationRepoExt(
final ExternalResourceDAO resourceDAO,
+ final PolicyDAO policyDAO,
+ final RealmDAO realmDAO,
final EntityCacheDAO entityCacheDAO,
final Neo4jTemplate neo4jTemplate,
final Neo4jClient neo4jClient,
final NodeValidator nodeValidator,
final Cache<EntityCacheKey, Neo4jImplementation>
implementationCache) {
- return new ImplementationRepoExtImpl(
- resourceDAO, entityCacheDAO, neo4jTemplate, neo4jClient,
nodeValidator, implementationCache);
+ return new ImplementationRepoExtImpl(resourceDAO, policyDAO, realmDAO,
entityCacheDAO, neo4jTemplate,
+ neo4jClient, nodeValidator, implementationCache);
}
@ConditionalOnMissingBean
diff --git
a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/ImplementationRepoExtImpl.java
b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/ImplementationRepoExtImpl.java
index 2d81bd0076..417579becf 100644
---
a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/ImplementationRepoExtImpl.java
+++
b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/ImplementationRepoExtImpl.java
@@ -26,11 +26,18 @@ import javax.cache.Cache;
import org.apache.commons.lang3.StringUtils;
import org.apache.syncope.core.persistence.api.dao.EntityCacheDAO;
import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
+import org.apache.syncope.core.persistence.api.dao.PolicyDAO;
+import org.apache.syncope.core.persistence.api.dao.RealmDAO;
import org.apache.syncope.core.persistence.api.entity.Implementation;
import org.apache.syncope.core.persistence.neo4j.dao.AbstractDAO;
import org.apache.syncope.core.persistence.neo4j.entity.EntityCacheKey;
import org.apache.syncope.core.persistence.neo4j.entity.Neo4jExternalResource;
import org.apache.syncope.core.persistence.neo4j.entity.Neo4jImplementation;
+import org.apache.syncope.core.persistence.neo4j.entity.Neo4jRealm;
+import
org.apache.syncope.core.persistence.neo4j.entity.policy.Neo4jAccountPolicy;
+import
org.apache.syncope.core.persistence.neo4j.entity.policy.Neo4jInboundPolicy;
+import
org.apache.syncope.core.persistence.neo4j.entity.policy.Neo4jPasswordPolicy;
+import org.apache.syncope.core.persistence.neo4j.entity.policy.Neo4jPushPolicy;
import org.apache.syncope.core.persistence.neo4j.spring.NodeValidator;
import org.apache.syncope.core.spring.implementation.ImplementationManager;
import org.springframework.data.neo4j.core.Neo4jClient;
@@ -41,6 +48,10 @@ public class ImplementationRepoExtImpl extends AbstractDAO
implements Implementa
protected final ExternalResourceDAO resourceDAO;
+ protected final PolicyDAO policyDAO;
+
+ protected final RealmDAO realmDAO;
+
protected final EntityCacheDAO entityCacheDAO;
protected final NodeValidator nodeValidator;
@@ -49,6 +60,8 @@ public class ImplementationRepoExtImpl extends AbstractDAO
implements Implementa
public ImplementationRepoExtImpl(
final ExternalResourceDAO resourceDAO,
+ final PolicyDAO policyDAO,
+ final RealmDAO realmDAO,
final EntityCacheDAO entityCacheDAO,
final Neo4jTemplate neo4jTemplate,
final Neo4jClient neo4jClient,
@@ -57,6 +70,8 @@ public class ImplementationRepoExtImpl extends AbstractDAO
implements Implementa
super(neo4jTemplate, neo4jClient);
this.resourceDAO = resourceDAO;
+ this.policyDAO = policyDAO;
+ this.realmDAO = realmDAO;
this.entityCacheDAO = entityCacheDAO;
this.nodeValidator = nodeValidator;
this.cache = cache;
@@ -107,6 +122,25 @@ public class ImplementationRepoExtImpl extends AbstractDAO
implements Implementa
resourceDAO.findByProvisionSorter(saved).
forEach(resource ->
entityCacheDAO.evict(Neo4jExternalResource.class, resource.getKey()));
+ policyDAO.findByAccountRule(saved).forEach(policy -> {
+ entityCacheDAO.evict(Neo4jAccountPolicy.class, policy.getKey());
+ realmDAO.findByPolicy(policy).forEach(realm ->
entityCacheDAO.evict(Neo4jRealm.class, realm.getKey()));
+ });
+ policyDAO.findByInboundCorrelationRule(saved).forEach(policy -> {
+ entityCacheDAO.evict(Neo4jInboundPolicy.class, policy.getKey());
+ resourceDAO.findByPolicy(policy)
+ .forEach(resource ->
entityCacheDAO.evict(Neo4jExternalResource.class, resource.getKey()));
+ });
+ policyDAO.findByPasswordRule(saved).forEach(policy -> {
+ entityCacheDAO.evict(Neo4jPasswordPolicy.class, policy.getKey());
+ realmDAO.findByPolicy(policy).forEach(realm ->
entityCacheDAO.evict(Neo4jRealm.class, realm.getKey()));
+ });
+ policyDAO.findByPushCorrelationRule(saved).forEach(policy -> {
+ entityCacheDAO.evict(Neo4jPushPolicy.class, policy.getKey());
+ resourceDAO.findByPolicy(policy)
+ .forEach(resource ->
entityCacheDAO.evict(Neo4jExternalResource.class, resource.getKey()));
+ });
+
return saved;
}
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 e39b6abafc..fe9b1090b0 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
@@ -42,6 +42,7 @@ import org.passay.dictionary.ArrayWordList;
import org.passay.dictionary.WordListDictionary;
import org.passay.resolver.PropertiesMessageResolver;
import org.passay.rule.DictionaryRule;
+import org.passay.rule.DictionarySubstringRule;
import org.passay.rule.Rule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -89,9 +90,15 @@ public class DefaultPasswordRule implements PasswordRule {
protected void enforce(final String username, final String clearPassword,
final Collection<String> notPermitted) {
List<Rule> rules = PasswordGenerator.conf2Rules(conf);
if (!notPermitted.isEmpty()) {
- rules.add(new DictionaryRule(new WordListDictionary(new
ArrayWordList(
-
notPermitted.stream().distinct().sorted(Comparator.naturalOrder()).toArray(String[]::new),
true)),
- true));
+ WordListDictionary wld = new WordListDictionary(new
ArrayWordList(notPermitted.stream()
+ .distinct()
+ .sorted(conf.isNotPermittedCaseSensitive()
+ ? Comparator.naturalOrder()
+ : String.CASE_INSENSITIVE_ORDER)
+ .toArray(String[]::new),
conf.isNotPermittedCaseSensitive()));
+ rules.add(conf.isNotPermittedAsSubstrings()
+ ? new DictionarySubstringRule(wld,
conf.isNotPermittedBackwards())
+ : new DictionaryRule(wld, conf.isNotPermittedBackwards()));
}
PasswordValidator passwordValidator = new
DefaultPasswordValidator(messageResolver, rules);
diff --git
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/PasswordGenerator.java
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/PasswordGenerator.java
index c57515b1a5..260bbc3af2 100644
---
a/core/spring/src/main/java/org/apache/syncope/core/spring/security/PasswordGenerator.java
+++
b/core/spring/src/main/java/org/apache/syncope/core/spring/security/PasswordGenerator.java
@@ -33,6 +33,7 @@ import org.passay.dictionary.ArrayWordList;
import org.passay.dictionary.WordListDictionary;
import org.passay.rule.CharacterRule;
import org.passay.rule.DictionaryRule;
+import org.passay.rule.DictionarySubstringRule;
import org.passay.rule.IllegalCharacterRule;
import org.passay.rule.LengthRule;
import org.passay.rule.RepeatCharactersRule;
@@ -95,9 +96,15 @@ public interface PasswordGenerator {
}
if (!conf.getWordsNotPermitted().isEmpty()) {
- conf.getWordsNotPermitted().sort(Comparator.naturalOrder());
- rules.add(new DictionaryRule(new WordListDictionary(
- new
ArrayWordList(conf.getWordsNotPermitted().toArray(String[]::new), true)),
true));
+ WordListDictionary wld = new WordListDictionary(
+ new ArrayWordList(conf.getWordsNotPermitted().stream().
+
distinct().sorted(conf.isNotPermittedCaseSensitive()
+ ? Comparator.naturalOrder()
+ :
String.CASE_INSENSITIVE_ORDER).toArray(String[]::new),
+ conf.isNotPermittedCaseSensitive()));
+ rules.add(conf.isNotPermittedAsSubstrings()
+ ? new DictionarySubstringRule(wld,
conf.isNotPermittedBackwards())
+ : new DictionaryRule(wld, conf.isNotPermittedBackwards()));
}
return rules;
diff --git
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PolicyITCase.java
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PolicyITCase.java
index 5bcfcef9c0..acb7b4253e 100644
---
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PolicyITCase.java
+++
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PolicyITCase.java
@@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@@ -47,6 +48,7 @@ import org.apache.syncope.common.lib.policy.PasswordPolicyTO;
import org.apache.syncope.common.lib.policy.PropagationPolicyTO;
import org.apache.syncope.common.lib.policy.PushPolicyTO;
import org.apache.syncope.common.lib.policy.TicketExpirationPolicyTO;
+import org.apache.syncope.common.lib.request.UserCR;
import org.apache.syncope.common.lib.to.ImplementationTO;
import org.apache.syncope.common.lib.to.ResourceTO;
import org.apache.syncope.common.lib.types.AnyTypeKind;
@@ -482,4 +484,38 @@ public class PolicyITCase extends AbstractITCase {
RESOURCE_SERVICE.update(ldap);
}
}
+
+ @Test
+ public void issueSYNCOPE1979() {
+ // 1. Set a new password policy with not permitted schemas and not
permitted words
+ ImplementationTO originalRule =
+
IMPLEMENTATION_SERVICE.read(IdRepoImplementationType.PASSWORD_RULE,
"DefaultPasswordRuleConf2");
+ DefaultPasswordRuleConf defaultPasswordRuleConf =
+ POJOHelper.deserialize(originalRule.getBody(),
DefaultPasswordRuleConf.class);
+ defaultPasswordRuleConf.getSchemasNotPermitted().add("firstname");
+ defaultPasswordRuleConf.getSchemasNotPermitted().add("surname");
+ defaultPasswordRuleConf.getSchemasNotPermitted().add("changePwdDate");
+ defaultPasswordRuleConf.setNotPermittedAsSubstrings(true);
+ originalRule.setBody(POJOHelper.serialize(defaultPasswordRuleConf));
+ IMPLEMENTATION_SERVICE.update(originalRule);
+ try {
+ UserCR userCR =
UserITCase.getUniqueSample("[email protected]");
+ // 1. set password with not permitted word inside
+ SyncopeClientException sce =
assertThrows(SyncopeClientException.class, () -> {
+ userCR.setPassword("Notpermitted12345!");
+ createUser(userCR);
+ });
+
assertTrue(sce.getElements().iterator().next().startsWith("InvalidPassword"));
+
+ // 2. set password with not permitted schema inside
+ sce = assertThrows(SyncopeClientException.class, () -> {
+
userCR.setPassword(userCR.getPlainAttr("firstname").get().getValues().getFirst()
+ "12345!");
+ createUser(userCR);
+ });
+
assertTrue(sce.getElements().iterator().next().startsWith("InvalidPassword"));
+ } finally {
+ // restore old password policy
+ IMPLEMENTATION_SERVICE.update(originalRule);
+ }
+ }
}