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

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


The following commit(s) were added to refs/heads/master by this push:
     new 769916b322 [SYNCOPE-1979] check also for not permitted words and 
schemas as substrings in default password rule (#1432)
769916b322 is described below

commit 769916b322b4184734c8f8630ebdd914d2bf8d65
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 fe40fb6ebb..db2c7dff9c 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
@@ -982,14 +982,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 37b710656e..686f9508f6 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;
@@ -484,4 +486,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);
+        }
+    }
 }

Reply via email to