Index: kernel-impl/src/main/java/org/sakaiproject/user/impl/BaseUserDirectoryService.java =================================================================== --- kernel-impl/src/main/java/org/sakaiproject/user/impl/BaseUserDirectoryService.java (revision 129727) +++ kernel-impl/src/main/java/org/sakaiproject/user/impl/BaseUserDirectoryService.java (working copy) @@ -67,6 +67,7 @@ import org.sakaiproject.user.api.DisplayAdvisorUDP; import org.sakaiproject.user.api.DisplaySortAdvisorUPD; import org.sakaiproject.user.api.ExternalUserSearchUDP; +import org.sakaiproject.user.api.PasswordPolicyProvider; import org.sakaiproject.user.api.User; import org.sakaiproject.user.api.UserAlreadyDefinedException; import org.sakaiproject.user.api.UserDirectoryProvider; @@ -130,6 +131,12 @@ /** Collaborator for doing passwords. */ protected PasswordService m_pwdService = null; + + /** For validating passwords */ + protected PasswordPolicyProvider m_passwordPolicyProvider = null; + + /** Component ID used to find the password policy provider */ + protected String m_passwordPolicyProviderName = PasswordPolicyProvider.class.getName(); /********************************************************************************************************************************************************************************************************************************************************** * Abstractions, etc. @@ -140,6 +147,33 @@ */ protected abstract Storage newStorage(); + /* (non-Javadoc) + * @see org.sakaiproject.user.api.UserDirectoryService#getPasswordPolicy() + */ + public PasswordPolicyProvider getPasswordPolicy() { + // https://jira.sakaiproject.org/browse/KNL-1123 + // If the password policy object is not null, return it to the caller + if ( m_passwordPolicyProvider == null ) { + // Otherwise, try to get the (default) password policy object before returning it + // Try getting it by the configured name + if ( m_passwordPolicyProviderName != null ) { + m_passwordPolicyProvider = (PasswordPolicyProvider) ComponentManager.get( m_passwordPolicyProviderName ); + } + // Try getting the default impl via ComponentManager + if ( m_passwordPolicyProvider == null ) { + m_passwordPolicyProvider = (PasswordPolicyProvider) ComponentManager.get(PasswordPolicyProvider.class); + } + // If all else failed, manually instantiate default implementation + if ( m_passwordPolicyProvider == null ) { + m_passwordPolicyProvider = new PasswordPolicyProviderDefaultImpl(serverConfigurationService()); + } + } + if ( m_passwordPolicyProvider == null ) { + throw new IllegalStateException("unable to find the password policy provider by name: "+m_passwordPolicyProviderName); + } + return m_passwordPolicyProvider; + } + /** * Access the partial URL that forms the root of resource URLs. * @@ -380,6 +414,14 @@ m_contextualUserDisplayService = contextualUserDisplayService; } + public void setPasswordPolicyProvider( PasswordPolicyProvider passwordPolicyProvider ) { + m_passwordPolicyProvider = passwordPolicyProvider; + } + + public void setPasswordPolicyProviderName( String passwordPolicyProviderName ) { + m_passwordPolicyProviderName = StringUtils.trimToNull( passwordPolicyProviderName ); + } + /** The # seconds to cache gets. 0 disables the cache. */ protected int m_cacheSeconds = 0; @@ -572,6 +614,16 @@ m_pwdService = new PasswordService(); } + m_passwordPolicyProviderName = serverConfigurationService().getString(PasswordPolicyProvider.SAK_PROP_PROVIDER_NAME, PasswordPolicyProvider.class.getName()); + if (StringUtils.isEmpty(m_passwordPolicyProviderName)) { + m_passwordPolicyProviderName = PasswordPolicyProvider.class.getName(); + M_log.warn("init(): Empty name for passwordPolicyProvider: Using the default name instead: "+m_passwordPolicyProviderName); + } + if (m_passwordPolicyProvider == null) { + m_passwordPolicyProvider = getPasswordPolicy(); // this will load the PasswordPolicy provider bean or instantiate the default + } + M_log.info("init(): PasswordPolicyProvider ("+m_passwordPolicyProviderName+"): " + ((m_passwordPolicyProvider == null) ? "none" : m_passwordPolicyProvider.getClass().getName())); + M_log.info("init(): provider: " + ((m_provider == null) ? "none" : m_provider.getClass().getName()) + " separateIdEid: " + m_separateIdEid); } @@ -590,6 +642,7 @@ m_storage = null; m_provider = null; m_anon = null; + m_passwordPolicyProvider = null; M_log.info("destroy()"); } Index: kernel-impl/src/main/java/org/sakaiproject/user/impl/PasswordPolicyProviderDefaultImpl.java =================================================================== --- kernel-impl/src/main/java/org/sakaiproject/user/impl/PasswordPolicyProviderDefaultImpl.java (revision 0) +++ kernel-impl/src/main/java/org/sakaiproject/user/impl/PasswordPolicyProviderDefaultImpl.java (revision 0) @@ -0,0 +1,175 @@ +/********************************************************************************** + * $URL$ + * $Id$ + *********************************************************************************** + * + * Copyright (c) 2005, 2006, 2008, 2009, 2010 Sakai Foundation + * + * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.user.impl; + +import java.util.Arrays; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.sakaiproject.component.cover.ComponentManager; +import org.sakaiproject.component.api.ServerConfigurationService; +import org.sakaiproject.user.api.PasswordPolicyProvider; + +/** + * This is the default implementation of the Password policy provider. + * + * @author bjones86 - OWL-831/RES-54 + * https://jira.sakaiproject.org/browse/KNL-1123 + */ +public class PasswordPolicyProviderDefaultImpl implements PasswordPolicyProvider { + /** Our log (commons). */ + private static Log logger = LogFactory.getLog(PasswordPolicyProviderDefaultImpl.class); + + /** ServerConfigurationService */ + private ServerConfigurationService serverConfigurationService; + public void setServerConfigurationService(ServerConfigurationService serverConfigurationService) { + this.serverConfigurationService = serverConfigurationService; + } + + /** value for minimum password entropy */ + private int minEntropy = DEFAULT_MIN_ENTROPY; + + /** value for maximum password sequence length */ + private int maxSequenceLength = DEFAULT_MAX_SEQ_LENGTH; + + /** array of all lower case characters (used for calculating password entropy) */ + private static final char[] CHARS_LOWER = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' }; + + /** array of all upper case characters (used for calculating password entropy) */ + private static final char[] CHARS_UPPER = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' }; + + /** array of all digit characters (used for calculating password entropy) */ + private static final char[] CHARS_DIGIT = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; + + /** array of all special characters (used for calculating password entropy) */ + private static final char[] CHARS_SPECIAL = { '!', '$', '*', '+', '-', '.', '=', '?', '@', '^', '_', '|', '~' }; + + /** + * Default zero-arg constructor + * DO NOT USE + */ + PasswordPolicyProviderDefaultImpl() { + this(null); + } + + /** + * @param serverConfigurationService + */ + public PasswordPolicyProviderDefaultImpl(ServerConfigurationService serverConfigurationService) { + this.serverConfigurationService = serverConfigurationService; + init(); + } + + + /** + * Initialization method (Spring) + */ + public void init() { + // Get the values from sakai.properties + if (serverConfigurationService == null) { + serverConfigurationService = (ServerConfigurationService) ComponentManager.get(org.sakaiproject.component.api.ServerConfigurationService.class); + } + if (serverConfigurationService != null) { + minEntropy = serverConfigurationService.getInt(SAK_PROP_MIN_PASSWORD_ENTROPY, minEntropy); + maxSequenceLength = serverConfigurationService.getInt(SAK_PROP_MAX_PASSWORD_SEQ_LENGTH, maxSequenceLength); + } + logger.info("PasswordPolicyProviderDefaultImpl.init(): minEntropy="+minEntropy+", maxSequenceLength="+maxSequenceLength); + } + + /** + * Destroy method (Spring) + */ + public void destroy() { + if (logger.isDebugEnabled()) + logger.debug("PasswordPolicyProviderDefaultImpl.destroy()"); + } + + /** + * {@inheritDoc} + */ + public boolean validatePassword(String password, String userDisplayID) { + if (logger.isDebugEnabled()) + logger.debug("PasswordPolicyProviderDefaultImpl.validatePassword( " + password + " )"); + + // If the password is null, it's invalid + if (password == null) { + return false; // SHORT CIRCUIT + } + + // If the password contains X number of characters from their display ID, it's invalid + // (where X is the maximum password sequence length defined in sakai.properties) + if (userDisplayID != null) { + int length = userDisplayID.length(); + for (int i = 0; i < length - (maxSequenceLength - 1); i++) { + String sub = userDisplayID.substring(i, i + maxSequenceLength); + if (password.indexOf(sub) > -1) { + return false; // SHORT CIRCUIT + } + } + } + + // Count the number of character sets used in the password + int characterSets = 0; + characterSets += isCharacterSetPresentInPassword(CHARS_LOWER, password); + characterSets += isCharacterSetPresentInPassword(CHARS_UPPER, password); + characterSets += isCharacterSetPresentInPassword(CHARS_DIGIT, password); + characterSets += isCharacterSetPresentInPassword(CHARS_SPECIAL, password); + + // Calculate and verify the password strength + int strength = password.length() * characterSets; + if (strength < minEntropy) { + return false; // SHORT CIRCUIT + } + + // The password has passed all requirements, therefore the password is valid + return true; + } + + /** + * Determine if the given character set is present in the given password string. + * + * @param characterSet + * the set of characters to check for + * @param password + * the password to search for the charachter set in + * @return 1 if the character set is present in the password, 0 otherwise + */ + private int isCharacterSetPresentInPassword(char[] characterSet, String password) { + for (int i = 0; i < password.length(); i++) { + if (Arrays.binarySearch(characterSet, password.charAt(i)) >= 0) { + return 1; // SHORT CIRCUIT + } + } + return 0; + } + + /** + * {@inheritDoc} + */ + public String getClientValidatePasswordFunction() { + if (logger.isDebugEnabled()) + logger.debug("PasswordPolicyProviderDefaultImpl.getClientValidatePasswordFunction()"); + String javaScript = ""; + return javaScript; + } + +} Property changes on: kernel-impl/src/main/java/org/sakaiproject/user/impl/PasswordPolicyProviderDefaultImpl.java ___________________________________________________________________ Added: svn:keywords + Date Revision Author HeadURL Id Added: svn:eol-style + native Index: api/src/main/java/org/sakaiproject/user/api/PasswordPolicyProvider.java =================================================================== --- api/src/main/java/org/sakaiproject/user/api/PasswordPolicyProvider.java (revision 0) +++ api/src/main/java/org/sakaiproject/user/api/PasswordPolicyProvider.java (revision 0) @@ -0,0 +1,64 @@ +/********************************************************************************** + * $URL$ + * $Id$ + *********************************************************************************** + * + * Copyright (c) 2005, 2006, 2008, 2009, 2010 Sakai Foundation + * + * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.user.api; + +/** + * This interface provides the method stubs needed for any password policy object. + * All password policy implementations need to implement this interface. + * + * https://jira.sakaiproject.org/browse/KNL-1123 + */ +public interface PasswordPolicyProvider { + + /** value for minimum password entropy */ + public static final int DEFAULT_MIN_ENTROPY = 16; + + /** value for maximum password sequence length */ + public static final int DEFAULT_MAX_SEQ_LENGTH = 3; + + /** sakai.property for minimum password entropy */ + public static final String SAK_PROP_MIN_PASSWORD_ENTROPY = "user.password.minimum.entropy"; + + /** sakai.property for maximum password sequence length */ + public static final String SAK_PROP_MAX_PASSWORD_SEQ_LENGTH = "user.password.maximum.sequence.length"; + + /** sakai.property for minimum password entropy */ + public static final String SAK_PROP_PROVIDER_NAME = "user.password.policy.provider.name"; + + /** + * This function returns a boolean value of true/false, depending on if the given password meets the validation criteria. + * + * Based on verifyPasswordStrength() in http://grepcode.com/file/repo1.maven.org/maven2/org.owasp.esapi/esapi/2.0_rc10/org/owasp/esapi/reference/FileBasedAuthenticator.java + * + * @param password the password to be validated + * @param userDisplayID the user's login ID + * @return true/false (password is valid/invalid) + */ + public boolean validatePassword(String password, String userDisplayID); + + /** + * This method is called to retrieve the JavaScript validation function to be used client-side. + * + * @return the JavaScript function to be used client-side + */ + public String getClientValidatePasswordFunction(); +} Property changes on: api/src/main/java/org/sakaiproject/user/api/PasswordPolicyProvider.java ___________________________________________________________________ Added: svn:keywords + Date Revision Author HeadURL Id Added: svn:eol-style + native Index: api/src/main/java/org/sakaiproject/user/api/UserDirectoryService.java =================================================================== --- api/src/main/java/org/sakaiproject/user/api/UserDirectoryService.java (revision 129727) +++ api/src/main/java/org/sakaiproject/user/api/UserDirectoryService.java (working copy) @@ -72,6 +72,14 @@ /** User eid for the admin user. */ static final String ADMIN_EID = "admin"; + + /** + * This function returns the PasswordPolicyProvider object (which defines the password policies for this installation) + * + * @return a PasswordPolicyProvider object + * @throws IllegalStateException if the system does not have a PasswordPolicyProvider + */ + public PasswordPolicyProvider getPasswordPolicy(); /** * Add a new user to the directory. Must commitEdit() to make official, or cancelEdit() when done! Id is auto-generated.