blob: 870a7aba623f0d220a001f573e96903dd9e7dc66 [file] [log] [blame]
/*
* 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();
}
}