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 e38c3bcb306..0732955b78e 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 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 schemasNotPermitted = new ArrayList<>(); + private boolean notPermittedCaseSensitive; + + private boolean notPermittedAsSubstrings; + + private boolean notPermittedBackwards; + public int getMaxLength() { return maxLength; } @@ -152,4 +157,28 @@ public List getWordsNotPermitted() { public List 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 4f95ba67055..0ab68d4c4b3 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 Cache implementationCache(final Cach @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 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 2d81bd0076c..417579becff 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 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 ImplementationRepoExtImpl( 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 Implementation save(final Implementation implementation) { 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 e39b6abafce..fe9b1090b0b 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.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 void setConf(final PasswordRuleConf conf) { protected void enforce(final String username, final String clearPassword, final Collection notPermitted) { List 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 c57515b1a54..260bbc3af27 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.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 String getCharacters() { } 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 5bcfcef9c0d..acb7b4253ea 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.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.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 void issueSYNCOPE682() { 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("syncope1979@syncope.apache.org"); + // 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); + } + } }