View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.syncope.core.spring.policy;
20  
21  import java.io.InputStream;
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.Objects;
27  import java.util.Properties;
28  import java.util.Set;
29  import java.util.stream.Collectors;
30  import org.apache.commons.lang3.ArrayUtils;
31  import org.apache.commons.lang3.StringUtils;
32  import org.apache.syncope.common.lib.policy.DefaultPasswordRuleConf;
33  import org.apache.syncope.common.lib.policy.PasswordRuleConf;
34  import org.apache.syncope.core.persistence.api.entity.PlainAttr;
35  import org.apache.syncope.core.persistence.api.entity.user.LinkedAccount;
36  import org.apache.syncope.core.persistence.api.entity.user.User;
37  import org.apache.syncope.core.provisioning.api.rules.PasswordRule;
38  import org.apache.syncope.core.provisioning.api.rules.PasswordRuleConfClass;
39  import org.apache.syncope.core.spring.security.Encryptor;
40  import org.passay.CharacterData;
41  import org.passay.CharacterRule;
42  import org.passay.EnglishCharacterData;
43  import org.passay.IllegalCharacterRule;
44  import org.passay.LengthRule;
45  import org.passay.PasswordData;
46  import org.passay.PasswordValidator;
47  import org.passay.PropertiesMessageResolver;
48  import org.passay.RepeatCharactersRule;
49  import org.passay.Rule;
50  import org.passay.RuleResult;
51  import org.passay.UsernameRule;
52  import org.slf4j.Logger;
53  import org.slf4j.LoggerFactory;
54  import org.springframework.transaction.annotation.Transactional;
55  import org.springframework.util.CollectionUtils;
56  
57  @PasswordRuleConfClass(DefaultPasswordRuleConf.class)
58  public class DefaultPasswordRule implements PasswordRule {
59  
60      protected static final Logger LOG = LoggerFactory.getLogger(DefaultPasswordRule.class);
61  
62      protected static final Encryptor ENCRYPTOR = Encryptor.getInstance();
63  
64      public static List<Rule> conf2Rules(final DefaultPasswordRuleConf conf) {
65          List<Rule> rules = new ArrayList<>();
66  
67          LengthRule lengthRule = new LengthRule();
68          if (conf.getMinLength() > 0) {
69              lengthRule.setMinimumLength(conf.getMinLength());
70          }
71          if (conf.getMaxLength() > 0) {
72              lengthRule.setMaximumLength(conf.getMaxLength());
73          }
74          rules.add(lengthRule);
75  
76          if (conf.getAlphabetical() > 0) {
77              rules.add(new CharacterRule(EnglishCharacterData.Alphabetical, conf.getAlphabetical()));
78          }
79  
80          if (conf.getUppercase() > 0) {
81              rules.add(new CharacterRule(EnglishCharacterData.UpperCase, conf.getUppercase()));
82          }
83  
84          if (conf.getLowercase() > 0) {
85              rules.add(new CharacterRule(EnglishCharacterData.LowerCase, conf.getLowercase()));
86          }
87  
88          if (conf.getDigit() > 0) {
89              rules.add(new CharacterRule(EnglishCharacterData.Digit, conf.getDigit()));
90          }
91  
92          if (conf.getSpecial() > 0) {
93              rules.add(new CharacterRule(new CharacterData() {
94  
95                  @Override
96                  public String getErrorCode() {
97                      return "INSUFFICIENT_SPECIAL";
98                  }
99  
100                 @Override
101                 public String getCharacters() {
102                     return new String(ArrayUtils.toPrimitive(conf.getSpecialChars().toArray(Character[]::new)));
103                 }
104             }, conf.getSpecial()));
105         }
106 
107         if (!conf.getIllegalChars().isEmpty()) {
108             rules.add(new IllegalCharacterRule(
109                     ArrayUtils.toPrimitive(conf.getIllegalChars().toArray(Character[]::new))));
110         }
111 
112         if (conf.getRepeatSame() > 0) {
113             rules.add(new RepeatCharactersRule(conf.getRepeatSame()));
114         }
115 
116         if (!conf.isUsernameAllowed()) {
117             rules.add(new UsernameRule(true, true));
118         }
119 
120         return rules;
121     }
122 
123     protected DefaultPasswordRuleConf conf;
124 
125     protected PasswordValidator passwordValidator;
126 
127     @Override
128     public PasswordRuleConf getConf() {
129         return conf;
130     }
131 
132     @Override
133     public void setConf(final PasswordRuleConf conf) {
134         if (conf instanceof DefaultPasswordRuleConf) {
135             this.conf = (DefaultPasswordRuleConf) conf;
136 
137             Properties passay = new Properties();
138             try (InputStream in = getClass().getResourceAsStream("/passay.properties")) {
139                 passay.load(in);
140                 passwordValidator = new PasswordValidator(new PropertiesMessageResolver(passay), conf2Rules(this.conf));
141             } catch (Exception e) {
142                 throw new IllegalStateException("Could not initialize Passay", e);
143             }
144         } else {
145             throw new IllegalArgumentException(
146                     DefaultPasswordRuleConf.class.getName() + " expected, got " + conf.getClass().getName());
147         }
148     }
149 
150     protected void enforce(final String clear, final String username, final Set<String> wordsNotPermitted) {
151         RuleResult result = passwordValidator.validate(
152                 username == null ? new PasswordData(clear) : new PasswordData(username, clear));
153         if (!result.isValid()) {
154             throw new PasswordPolicyException(passwordValidator.getMessages(result).
155                     stream().collect(Collectors.joining(",")));
156         }
157 
158         // check words not permitted
159         wordsNotPermitted.stream().
160                 filter(word -> StringUtils.containsIgnoreCase(clear, word)).findFirst().
161                 ifPresent(word -> {
162                     throw new PasswordPolicyException("Used word(s) not permitted");
163                 });
164     }
165 
166     @Override
167     public void enforce(final String username, final String clearPassword) {
168         if (clearPassword != null) {
169             Set<String> wordsNotPermitted = new HashSet<>(conf.getWordsNotPermitted());
170             enforce(clearPassword, username, wordsNotPermitted);
171         }
172     }
173 
174     @Transactional(readOnly = true)
175     @Override
176     public void enforce(final User user, final String clearPassword) {
177         if (clearPassword != null) {
178             Set<String> wordsNotPermitted = new HashSet<>(conf.getWordsNotPermitted());
179             wordsNotPermitted.addAll(
180                     conf.getSchemasNotPermitted().stream().
181                             map(schema -> user.getPlainAttr(schema).
182                             map(PlainAttr::getValuesAsStrings).orElse(null)).
183                             filter(Objects::nonNull).
184                             filter(values -> !CollectionUtils.isEmpty(values)).
185                             flatMap(Collection::stream).
186                             collect(Collectors.toSet()));
187 
188             enforce(clearPassword, user.getUsername(), wordsNotPermitted);
189         }
190     }
191 
192     @Transactional(readOnly = true)
193     @Override
194     public void enforce(final LinkedAccount account) {
195         conf.getWordsNotPermitted().addAll(
196                 conf.getSchemasNotPermitted().stream().
197                         map(schema -> account.getPlainAttr(schema).
198                         map(PlainAttr::getValuesAsStrings).orElse(null)).
199                         filter(Objects::nonNull).
200                         filter(values -> !CollectionUtils.isEmpty(values)).
201                         flatMap(Collection::stream).
202                         collect(Collectors.toList()));
203 
204         if (account.getPassword() != null) {
205             String clear = null;
206             if (account.canDecodeSecrets()) {
207                 try {
208                     clear = ENCRYPTOR.decode(account.getPassword(), account.getCipherAlgorithm());
209                 } catch (Exception e) {
210                     LOG.error("Could not decode password for {}", account, e);
211                 }
212             }
213 
214             if (clear != null) {
215                 Set<String> wordsNotPermitted = new HashSet<>(conf.getWordsNotPermitted());
216                 wordsNotPermitted.addAll(
217                         conf.getSchemasNotPermitted().stream().
218                                 map(schema -> account.getPlainAttr(schema).
219                                 map(PlainAttr::getValuesAsStrings).orElse(null)).
220                                 filter(Objects::nonNull).
221                                 filter(values -> !CollectionUtils.isEmpty(values)).
222                                 flatMap(Collection::stream).
223                                 collect(Collectors.toSet()));
224 
225                 enforce(clear, account.getUsername(), wordsNotPermitted);
226             }
227         }
228     }
229 }