| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * |
| */ |
| package org.apache.directory.server.core.kerberos; |
| |
| |
| import org.apache.directory.server.core.entry.ServerAttribute; |
| import org.apache.directory.server.core.entry.ServerBinaryValue; |
| import org.apache.directory.server.core.entry.ServerEntry; |
| import org.apache.directory.server.core.entry.ServerStringValue; |
| import org.apache.directory.server.core.interceptor.BaseInterceptor; |
| import org.apache.directory.server.core.interceptor.Interceptor; |
| import org.apache.directory.server.core.interceptor.NextInterceptor; |
| import org.apache.directory.server.core.interceptor.context.AddOperationContext; |
| import org.apache.directory.server.core.interceptor.context.ModifyOperationContext; |
| import org.apache.directory.shared.ldap.constants.SchemaConstants; |
| import org.apache.directory.shared.ldap.entry.Modification; |
| import org.apache.directory.shared.ldap.entry.Value; |
| import org.apache.directory.shared.ldap.name.LdapDN; |
| import org.apache.directory.shared.ldap.util.StringTools; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import javax.naming.NamingException; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| |
| /** |
| * An {@link Interceptor} that enforces password policy for users. Add or modify operations |
| * on the 'userPassword' attribute are checked against a password policy. The password is |
| * rejected if it does not pass the password policy checks. The password MUST be passed to |
| * the core as plaintext. |
| * |
| * @org.apache.xbean.XBean |
| * |
| * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> |
| * @version $Rev$, $Date$ |
| */ |
| public class PasswordPolicyInterceptor extends BaseInterceptor |
| { |
| /** The log for this class. */ |
| private static final Logger log = LoggerFactory.getLogger( PasswordPolicyInterceptor.class ); |
| |
| /** The service name. */ |
| public static final String NAME = "passwordPolicyService"; |
| |
| |
| /** |
| * Check added attributes for a 'userPassword'. If a 'userPassword' is found, apply any |
| * password policy checks. |
| */ |
| public void add( NextInterceptor next, AddOperationContext addContext ) throws NamingException |
| { |
| LdapDN normName = addContext.getDn(); |
| |
| ServerEntry entry = addContext.getEntry(); |
| |
| log.debug( "Adding the entry '{}' for DN '{}'.", entry, normName.getUpName() ); |
| |
| if ( entry.get( SchemaConstants.USER_PASSWORD_AT ) != null ) |
| { |
| String username = null; |
| |
| ServerBinaryValue userPassword = (ServerBinaryValue)entry.get( SchemaConstants.USER_PASSWORD_AT ).get(); |
| |
| // The password is stored in a non H/R attribute, but it's a String |
| String strUserPassword = StringTools.utf8ToString( userPassword.get() ); |
| |
| if ( log.isDebugEnabled() ) |
| { |
| StringBuffer sb = new StringBuffer(); |
| sb.append( "'" + strUserPassword + "' ( " ); |
| sb.append( userPassword ); |
| sb.append( " )" ); |
| log.debug( "Adding Attribute id : 'userPassword', Values : [ {} ]", sb.toString() ); |
| } |
| |
| if ( entry.get( SchemaConstants.CN_AT ) != null ) |
| { |
| ServerStringValue attr = (ServerStringValue)entry.get( SchemaConstants.CN_AT ).get(); |
| username = attr.get(); |
| } |
| |
| // If userPassword fails checks, throw new NamingException. |
| check( username, strUserPassword ); |
| } |
| |
| next.add( addContext ); |
| } |
| |
| |
| /** |
| * Check modification items for a 'userPassword'. If a 'userPassword' is found, apply any |
| * password policy checks. |
| */ |
| public void modify( NextInterceptor next, ModifyOperationContext modContext ) throws NamingException |
| { |
| LdapDN name = modContext.getDn(); |
| |
| List<Modification> mods = modContext.getModItems(); |
| |
| String operation = null; |
| |
| for ( Modification mod:mods ) |
| { |
| if ( log.isDebugEnabled() ) |
| { |
| switch ( mod.getOperation() ) |
| { |
| case ADD_ATTRIBUTE: |
| operation = "Adding"; |
| break; |
| |
| case REMOVE_ATTRIBUTE: |
| operation = "Removing"; |
| break; |
| |
| case REPLACE_ATTRIBUTE: |
| operation = "Replacing"; |
| break; |
| } |
| } |
| |
| ServerAttribute attr = (ServerAttribute)mod.getAttribute(); |
| |
| if ( attr.instanceOf( SchemaConstants.USER_PASSWORD_AT ) ) |
| { |
| Value<?> userPassword = attr.get(); |
| String pwd = ""; |
| |
| if ( userPassword != null ) |
| { |
| if ( userPassword instanceof ServerStringValue ) |
| { |
| log.debug( "{} Attribute id : 'userPassword', Values : [ '{}' ]", operation, attr ); |
| pwd = ((ServerStringValue)userPassword).get(); |
| } |
| else if ( userPassword instanceof ServerBinaryValue ) |
| { |
| ServerBinaryValue password = (ServerBinaryValue)userPassword.get(); |
| |
| String string = ""; |
| |
| if ( password != null ) |
| { |
| string = StringTools.utf8ToString( password.get() ); |
| } |
| |
| if ( log.isDebugEnabled() ) |
| { |
| StringBuffer sb = new StringBuffer(); |
| sb.append( "'" + string + "' ( " ); |
| sb.append( StringTools.dumpBytes( password.get() ).trim() ); |
| sb.append( " )" ); |
| log.debug( "{} Attribute id : 'userPassword', Values : [ {} ]", operation, sb.toString() ); |
| } |
| |
| pwd = string; |
| } |
| |
| // if userPassword fails checks, throw new NamingException. |
| check( name.getUpName(), pwd ); |
| } |
| } |
| |
| if ( log.isDebugEnabled() ) |
| { |
| log.debug( operation + " for entry '" + name.getUpName() + "' the attribute " + mod.getAttribute() ); |
| } |
| } |
| |
| next.modify( modContext ); |
| } |
| |
| |
| void check( String username, String password ) throws NamingException |
| { |
| int passwordLength = 6; |
| int categoryCount = 2; |
| int tokenSize = 3; |
| |
| if ( !isValid( username, password, passwordLength, categoryCount, tokenSize ) ) |
| { |
| String explanation = buildErrorMessage( username, password, passwordLength, categoryCount, tokenSize ); |
| log.error( explanation ); |
| |
| throw new NamingException( explanation ); |
| } |
| } |
| |
| |
| /** |
| * Tests that: |
| * The password is at least six characters long. |
| * The password contains a mix of characters. |
| * The password does not contain three letter (or more) tokens from the user's account name. |
| */ |
| boolean isValid( String username, String password, int passwordLength, int categoryCount, int tokenSize ) |
| { |
| return isValidPasswordLength( password, passwordLength ) && isValidCategoryCount( password, categoryCount ) |
| && isValidUsernameSubstring( username, password, tokenSize ); |
| } |
| |
| |
| /** |
| * The password is at least six characters long. |
| */ |
| boolean isValidPasswordLength( String password, int passwordLength ) |
| { |
| return password.length() >= passwordLength; |
| } |
| |
| |
| /** |
| * The password contains characters from at least three of the following four categories: |
| * English uppercase characters (A - Z) |
| * English lowercase characters (a - z) |
| * Base 10 digits (0 - 9) |
| * Any non-alphanumeric character (for example: !, $, #, or %) |
| */ |
| boolean isValidCategoryCount( String password, int categoryCount ) |
| { |
| int uppercase = 0; |
| int lowercase = 0; |
| int digit = 0; |
| int nonAlphaNumeric = 0; |
| |
| char[] characters = password.toCharArray(); |
| |
| for ( char character:characters ) |
| { |
| if ( Character.isLowerCase( character ) ) |
| { |
| lowercase = 1; |
| } |
| else |
| { |
| if ( Character.isUpperCase( character ) ) |
| { |
| uppercase = 1; |
| } |
| else |
| { |
| if ( Character.isDigit( character ) ) |
| { |
| digit = 1; |
| } |
| else |
| { |
| if ( !Character.isLetterOrDigit( character ) ) |
| { |
| nonAlphaNumeric = 1; |
| } |
| } |
| } |
| } |
| } |
| |
| return ( uppercase + lowercase + digit + nonAlphaNumeric ) >= categoryCount; |
| } |
| |
| |
| /** |
| * The password does not contain three letter (or more) tokens from the user's account name. |
| * |
| * If the account name is less than three characters long, this check is not performed |
| * because the rate at which passwords would be rejected is too high. For each token that is |
| * three or more characters long, that token is searched for in the password; if it is present, |
| * the password change is rejected. For example, the name "First M. Last" would be split into |
| * three tokens: "First", "M", and "Last". Because the second token is only one character long, |
| * it would be ignored. Therefore, this user could not have a password that included either |
| * "first" or "last" as a substring anywhere in the password. All of these checks are |
| * case-insensitive. |
| */ |
| boolean isValidUsernameSubstring( String username, String password, int tokenSize ) |
| { |
| String[] tokens = username.split( "[^a-zA-Z]" ); |
| |
| for ( int ii = 0; ii < tokens.length; ii++ ) |
| { |
| if ( tokens[ii].length() >= tokenSize ) |
| { |
| if ( password.matches( "(?i).*" + tokens[ii] + ".*" ) ) |
| { |
| return false; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| |
| private String buildErrorMessage( String username, String password, int passwordLength, int categoryCount, |
| int tokenSize ) |
| { |
| List<String> violations = new ArrayList<String>(); |
| |
| if ( !isValidPasswordLength( password, passwordLength ) ) |
| { |
| violations.add( "length too short" ); |
| } |
| |
| if ( !isValidCategoryCount( password, categoryCount ) ) |
| { |
| violations.add( "insufficient character mix" ); |
| } |
| |
| if ( !isValidUsernameSubstring( username, password, tokenSize ) ) |
| { |
| violations.add( "contains portions of username" ); |
| } |
| |
| StringBuffer sb = new StringBuffer( "Password violates policy: " ); |
| |
| boolean isFirst = true; |
| |
| for ( String violation : violations ) |
| { |
| if ( isFirst ) |
| { |
| isFirst = false; |
| } |
| else |
| { |
| sb.append( ", " ); |
| } |
| |
| sb.append( violation ); |
| } |
| |
| return sb.toString(); |
| } |
| } |