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   */
20  package org.apache.directory.server.core.kerberos;
21  
22  
23  import org.apache.directory.server.core.entry.ServerAttribute;
24  import org.apache.directory.server.core.entry.ServerBinaryValue;
25  import org.apache.directory.server.core.entry.ServerEntry;
26  import org.apache.directory.server.core.entry.ServerStringValue;
27  import org.apache.directory.server.core.interceptor.BaseInterceptor;
28  import org.apache.directory.server.core.interceptor.Interceptor;
29  import org.apache.directory.server.core.interceptor.NextInterceptor;
30  import org.apache.directory.server.core.interceptor.context.AddOperationContext;
31  import org.apache.directory.server.core.interceptor.context.ModifyOperationContext;
32  import org.apache.directory.shared.ldap.constants.SchemaConstants;
33  import org.apache.directory.shared.ldap.entry.Modification;
34  import org.apache.directory.shared.ldap.entry.Value;
35  import org.apache.directory.shared.ldap.name.LdapDN;
36  import org.apache.directory.shared.ldap.util.StringTools;
37  import org.slf4j.Logger;
38  import org.slf4j.LoggerFactory;
39  
40  import java.util.ArrayList;
41  import java.util.List;
42  
43  
44  /**
45   * An {@link Interceptor} that enforces password policy for users.  Add or modify operations
46   * on the 'userPassword' attribute are checked against a password policy.  The password is
47   * rejected if it does not pass the password policy checks.  The password MUST be passed to
48   * the core as plaintext.
49   * 
50   * @org.apache.xbean.XBean
51   *
52   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
53   * @version $Rev$, $Date$
54   */
55  public class PasswordPolicyInterceptor extends BaseInterceptor
56  {
57      /** The log for this class. */
58      private static final Logger log = LoggerFactory.getLogger( PasswordPolicyInterceptor.class );
59  
60      /** The service name. */
61      public static final String NAME = "passwordPolicyService";
62  
63  
64      /**
65       * Check added attributes for a 'userPassword'.  If a 'userPassword' is found, apply any
66       * password policy checks.
67       */
68      public void add( NextInterceptor next, AddOperationContext addContext ) throws Exception
69      {
70          LdapDN normName = addContext.getDn();
71  
72          ServerEntry entry = addContext.getEntry();
73  
74          log.debug( "Adding the entry '{}' for DN '{}'.", entry, normName.getUpName() );
75  
76          if ( entry.get( SchemaConstants.USER_PASSWORD_AT ) != null )
77          {
78              String username = null;
79  
80              ServerBinaryValue userPassword = (ServerBinaryValue)entry.get( SchemaConstants.USER_PASSWORD_AT ).get();
81  
82              // The password is stored in a non H/R attribute, but it's a String
83              String strUserPassword = StringTools.utf8ToString( userPassword.get() );
84  
85              if ( log.isDebugEnabled() )
86              {
87                  StringBuffer sb = new StringBuffer();
88                  sb.append( "'" + strUserPassword + "' ( " );
89                  sb.append( userPassword );
90                  sb.append( " )" );
91                  log.debug( "Adding Attribute id : 'userPassword',  Values : [ {} ]", sb.toString() );
92              }
93  
94              if ( entry.get( SchemaConstants.CN_AT ) != null )
95              {
96                  ServerStringValue attr = (ServerStringValue)entry.get( SchemaConstants.CN_AT ).get();
97                  username = attr.get();
98              }
99  
100             // If userPassword fails checks, throw new NamingException.
101             check( username, strUserPassword );
102         }
103 
104         next.add( addContext );
105     }
106 
107 
108     /**
109      * Check modification items for a 'userPassword'.  If a 'userPassword' is found, apply any
110      * password policy checks.
111      */
112     public void modify( NextInterceptor next, ModifyOperationContext modContext ) throws Exception
113     {
114         LdapDN name = modContext.getDn();
115 
116         List<Modification> mods = modContext.getModItems();
117 
118         String operation = null;
119 
120         for ( Modification mod:mods )
121         {
122             if ( log.isDebugEnabled() )
123             {
124                 switch ( mod.getOperation() )
125                 {
126                     case ADD_ATTRIBUTE:
127                         operation = "Adding";
128                         break;
129                         
130                     case REMOVE_ATTRIBUTE:
131                         operation = "Removing";
132                         break;
133                         
134                     case REPLACE_ATTRIBUTE:
135                         operation = "Replacing";
136                         break;
137                 }
138             }
139 
140             ServerAttribute attr = (ServerAttribute)mod.getAttribute();
141 
142             if ( attr.instanceOf( SchemaConstants.USER_PASSWORD_AT ) )
143             {
144                 Value<?> userPassword = attr.get();
145                 String pwd = "";
146 
147                 if ( userPassword != null )
148                 {
149                     if ( userPassword instanceof ServerStringValue )
150                     {
151                         log.debug( "{} Attribute id : 'userPassword',  Values : [ '{}' ]", operation, attr );
152                         pwd = ((ServerStringValue)userPassword).get();
153                     }
154                     else if ( userPassword instanceof ServerBinaryValue )
155                     {
156                         ServerBinaryValue password = (ServerBinaryValue)userPassword.get();
157                         
158                         String string = "";
159                         
160                         if ( password != null )
161                         {
162                             string = StringTools.utf8ToString( password.get() );
163                         }
164 
165                         if ( log.isDebugEnabled() )
166                         {
167                             StringBuffer sb = new StringBuffer();
168                             sb.append( "'" + string + "' ( " );
169                             sb.append( StringTools.dumpBytes( password.get() ).trim() );
170                             sb.append( " )" );
171                             log.debug( "{} Attribute id : 'userPassword',  Values : [ {} ]", operation, sb.toString() );
172                         }
173 
174                         pwd = string;
175                     }
176 
177                     // if userPassword fails checks, throw new NamingException.
178                     check( name.getUpName(), pwd );
179                 }
180             }
181 
182             if ( log.isDebugEnabled() )
183             {
184                 log.debug( operation + " for entry '" + name.getUpName() + "' the attribute " + mod.getAttribute() );
185             }
186         }
187 
188         next.modify( modContext );
189     }
190 
191 
192     void check( String username, String password ) throws Exception
193     {
194         int passwordLength = 6;
195         int categoryCount = 2;
196         int tokenSize = 3;
197 
198         if ( !isValid( username, password, passwordLength, categoryCount, tokenSize ) )
199         {
200             String explanation = buildErrorMessage( username, password, passwordLength, categoryCount, tokenSize );
201             log.error( explanation );
202 
203             throw new Exception( explanation );
204         }
205     }
206 
207 
208     /**
209      * Tests that:
210      * The password is at least six characters long.
211      * The password contains a mix of characters.
212      * The password does not contain three letter (or more) tokens from the user's account name.
213      */
214     boolean isValid( String username, String password, int passwordLength, int categoryCount, int tokenSize )
215     {
216         return isValidPasswordLength( password, passwordLength ) && isValidCategoryCount( password, categoryCount )
217             && isValidUsernameSubstring( username, password, tokenSize );
218     }
219 
220 
221     /**
222      * The password is at least six characters long.
223      */
224     boolean isValidPasswordLength( String password, int passwordLength )
225     {
226         return password.length() >= passwordLength;
227     }
228 
229 
230     /**
231      * The password contains characters from at least three of the following four categories:
232      * English uppercase characters (A - Z)
233      * English lowercase characters (a - z)
234      * Base 10 digits (0 - 9)
235      * Any non-alphanumeric character (for example: !, $, #, or %)
236      */
237     boolean isValidCategoryCount( String password, int categoryCount )
238     {
239         int uppercase = 0;
240         int lowercase = 0;
241         int digit = 0;
242         int nonAlphaNumeric = 0;
243 
244         char[] characters = password.toCharArray();
245 
246         for ( char character:characters )
247         {
248             if ( Character.isLowerCase( character ) )
249             {
250                 lowercase = 1;
251             }
252             else
253             {
254                 if ( Character.isUpperCase( character ) )
255                 {
256                     uppercase = 1;
257                 }
258                 else
259                 {
260                     if ( Character.isDigit( character ) )
261                     {
262                         digit = 1;
263                     }
264                     else
265                     {
266                         if ( !Character.isLetterOrDigit( character ) )
267                         {
268                             nonAlphaNumeric = 1;
269                         }
270                     }
271                 }
272             }
273         }
274 
275         return ( uppercase + lowercase + digit + nonAlphaNumeric ) >= categoryCount;
276     }
277 
278 
279     /**
280      * The password does not contain three letter (or more) tokens from the user's account name.
281      * 
282      * If the account name is less than three characters long, this check is not performed
283      * because the rate at which passwords would be rejected is too high. For each token that is
284      * three or more characters long, that token is searched for in the password; if it is present,
285      * the password change is rejected. For example, the name "First M. Last" would be split into
286      * three tokens: "First", "M", and "Last". Because the second token is only one character long,
287      * it would be ignored. Therefore, this user could not have a password that included either
288      * "first" or "last" as a substring anywhere in the password. All of these checks are
289      * case-insensitive.
290      */
291     boolean isValidUsernameSubstring( String username, String password, int tokenSize )
292     {
293         String[] tokens = username.split( "[^a-zA-Z]" );
294 
295         for ( int ii = 0; ii < tokens.length; ii++ )
296         {
297             if ( tokens[ii].length() >= tokenSize )
298             {
299                 if ( password.matches( "(?i).*" + tokens[ii] + ".*" ) )
300                 {
301                     return false;
302                 }
303             }
304         }
305 
306         return true;
307     }
308 
309 
310     private String buildErrorMessage( String username, String password, int passwordLength, int categoryCount,
311         int tokenSize )
312     {
313         List<String> violations = new ArrayList<String>();
314 
315         if ( !isValidPasswordLength( password, passwordLength ) )
316         {
317             violations.add( "length too short" );
318         }
319 
320         if ( !isValidCategoryCount( password, categoryCount ) )
321         {
322             violations.add( "insufficient character mix" );
323         }
324 
325         if ( !isValidUsernameSubstring( username, password, tokenSize ) )
326         {
327             violations.add( "contains portions of username" );
328         }
329 
330         StringBuffer sb = new StringBuffer( "Password violates policy:  " );
331 
332         boolean isFirst = true;
333 
334         for ( String violation : violations )
335         {
336             if ( isFirst )
337             {
338                 isFirst = false;
339             }
340             else
341             {
342                 sb.append( ", " );
343             }
344 
345             sb.append( violation );
346         }
347 
348         return sb.toString();
349     }
350 }