GUACAMOLE-36: Merge password policy support.

This commit is contained in:
James Muehlner
2017-01-03 20:41:46 -08:00
23 changed files with 940 additions and 43 deletions

View File

@@ -63,6 +63,7 @@ import org.apache.guacamole.auth.jdbc.connection.ConnectionParameterMapper;
import org.apache.guacamole.auth.jdbc.permission.SharingProfilePermissionMapper; import org.apache.guacamole.auth.jdbc.permission.SharingProfilePermissionMapper;
import org.apache.guacamole.auth.jdbc.permission.SharingProfilePermissionService; import org.apache.guacamole.auth.jdbc.permission.SharingProfilePermissionService;
import org.apache.guacamole.auth.jdbc.permission.SharingProfilePermissionSet; import org.apache.guacamole.auth.jdbc.permission.SharingProfilePermissionSet;
import org.apache.guacamole.auth.jdbc.security.PasswordPolicyService;
import org.apache.guacamole.auth.jdbc.sharing.ConnectionSharingService; import org.apache.guacamole.auth.jdbc.sharing.ConnectionSharingService;
import org.apache.guacamole.auth.jdbc.sharing.HashSharedConnectionMap; import org.apache.guacamole.auth.jdbc.sharing.HashSharedConnectionMap;
import org.apache.guacamole.auth.jdbc.sharing.SecureRandomShareKeyGenerator; import org.apache.guacamole.auth.jdbc.sharing.SecureRandomShareKeyGenerator;
@@ -159,6 +160,7 @@ public class JDBCAuthenticationProviderModule extends MyBatisModule {
bind(ConnectionService.class); bind(ConnectionService.class);
bind(GuacamoleTunnelService.class).to(RestrictedGuacamoleTunnelService.class); bind(GuacamoleTunnelService.class).to(RestrictedGuacamoleTunnelService.class);
bind(PasswordEncryptionService.class).to(SHA256PasswordEncryptionService.class); bind(PasswordEncryptionService.class).to(SHA256PasswordEncryptionService.class);
bind(PasswordPolicyService.class);
bind(SaltService.class).to(SecureRandomSaltService.class); bind(SaltService.class).to(SecureRandomSaltService.class);
bind(SharedConnectionMap.class).to(HashSharedConnectionMap.class).in(Scopes.SINGLETON); bind(SharedConnectionMap.class).to(HashSharedConnectionMap.class).in(Scopes.SINGLETON);
bind(ShareKeyGenerator.class).to(SecureRandomShareKeyGenerator.class).in(Scopes.SINGLETON); bind(ShareKeyGenerator.class).to(SecureRandomShareKeyGenerator.class).in(Scopes.SINGLETON);

View File

@@ -21,6 +21,7 @@ package org.apache.guacamole.auth.jdbc;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.LocalEnvironment; import org.apache.guacamole.environment.LocalEnvironment;
import org.apache.guacamole.auth.jdbc.security.PasswordPolicy;
/** /**
* A JDBC-specific implementation of Environment that defines generic properties * A JDBC-specific implementation of Environment that defines generic properties
@@ -128,4 +129,14 @@ public abstract class JDBCEnvironment extends LocalEnvironment {
public abstract int getDefaultMaxGroupConnectionsPerUser() public abstract int getDefaultMaxGroupConnectionsPerUser()
throws GuacamoleException; throws GuacamoleException;
/**
* Returns the policy which applies to newly-set passwords. Passwords which
* apply to Guacamole user accounts will be required to conform to this
* policy.
*
* @return
* The password policy which applies to Guacamole user accounts.
*/
public abstract PasswordPolicy getPasswordPolicy();
} }

View File

@@ -172,9 +172,9 @@ public abstract class ModeledChildDirectoryObjectService<InternalType extends Mo
@Override @Override
protected void beforeCreate(ModeledAuthenticatedUser user, protected void beforeCreate(ModeledAuthenticatedUser user,
ModelType model) throws GuacamoleException { ExternalType object, ModelType model) throws GuacamoleException {
super.beforeCreate(user, model); super.beforeCreate(user, object, model);
// Validate that we can update all applicable parents // Validate that we can update all applicable parents
if (!canUpdateModifiedParents(user, null, model)) if (!canUpdateModifiedParents(user, null, model))
@@ -184,9 +184,9 @@ public abstract class ModeledChildDirectoryObjectService<InternalType extends Mo
@Override @Override
protected void beforeUpdate(ModeledAuthenticatedUser user, protected void beforeUpdate(ModeledAuthenticatedUser user,
ModelType model) throws GuacamoleException { InternalType object, ModelType model) throws GuacamoleException {
super.beforeUpdate(user, model); super.beforeUpdate(user, object, model);
// Validate that we can update all applicable parents // Validate that we can update all applicable parents
if (!canUpdateModifiedParents(user, model.getIdentifier(), model)) if (!canUpdateModifiedParents(user, model.getIdentifier(), model))

View File

@@ -222,6 +222,9 @@ public abstract class ModeledDirectoryObjectService<InternalType extends Modeled
* @param user * @param user
* The user creating the object. * The user creating the object.
* *
* @param object
* The object being created.
*
* @param model * @param model
* The model of the object being created. * The model of the object being created.
* *
@@ -230,7 +233,7 @@ public abstract class ModeledDirectoryObjectService<InternalType extends Modeled
* object. * object.
*/ */
protected void beforeCreate(ModeledAuthenticatedUser user, protected void beforeCreate(ModeledAuthenticatedUser user,
ModelType model ) throws GuacamoleException { ExternalType object, ModelType model) throws GuacamoleException {
// Verify permission to create objects // Verify permission to create objects
if (!user.getUser().isAdministrator() && !hasCreatePermission(user)) if (!user.getUser().isAdministrator() && !hasCreatePermission(user))
@@ -247,6 +250,9 @@ public abstract class ModeledDirectoryObjectService<InternalType extends Modeled
* @param user * @param user
* The user updating the existing object. * The user updating the existing object.
* *
* @param object
* The object being updated.
*
* @param model * @param model
* The model of the object being updated. * The model of the object being updated.
* *
@@ -255,7 +261,7 @@ public abstract class ModeledDirectoryObjectService<InternalType extends Modeled
* object. * object.
*/ */
protected void beforeUpdate(ModeledAuthenticatedUser user, protected void beforeUpdate(ModeledAuthenticatedUser user,
ModelType model) throws GuacamoleException { InternalType object, ModelType model) throws GuacamoleException {
// By default, do nothing. // By default, do nothing.
if (!hasObjectPermission(user, model.getIdentifier(), ObjectPermission.Type.UPDATE)) if (!hasObjectPermission(user, model.getIdentifier(), ObjectPermission.Type.UPDATE))
@@ -436,7 +442,7 @@ public abstract class ModeledDirectoryObjectService<InternalType extends Modeled
throws GuacamoleException { throws GuacamoleException {
ModelType model = getModelInstance(user, object); ModelType model = getModelInstance(user, object);
beforeCreate(user, model); beforeCreate(user, object, model);
// Create object // Create object
getObjectMapper().insert(model); getObjectMapper().insert(model);
@@ -467,7 +473,7 @@ public abstract class ModeledDirectoryObjectService<InternalType extends Modeled
throws GuacamoleException { throws GuacamoleException {
ModelType model = object.getModel(); ModelType model = object.getModel();
beforeUpdate(user, model); beforeUpdate(user, object, model);
// Update object // Update object
getObjectMapper().update(model); getObjectMapper().update(model);

View File

@@ -156,9 +156,10 @@ public class ConnectionService extends ModeledChildDirectoryObjectService<Modele
@Override @Override
protected void beforeCreate(ModeledAuthenticatedUser user, protected void beforeCreate(ModeledAuthenticatedUser user,
ConnectionModel model) throws GuacamoleException { Connection object, ConnectionModel model)
throws GuacamoleException {
super.beforeCreate(user, model); super.beforeCreate(user, object, model);
// Name must not be blank // Name must not be blank
if (model.getName() == null || model.getName().trim().isEmpty()) if (model.getName() == null || model.getName().trim().isEmpty())
@@ -173,10 +174,11 @@ public class ConnectionService extends ModeledChildDirectoryObjectService<Modele
@Override @Override
protected void beforeUpdate(ModeledAuthenticatedUser user, protected void beforeUpdate(ModeledAuthenticatedUser user,
ConnectionModel model) throws GuacamoleException { ModeledConnection object, ConnectionModel model)
throws GuacamoleException {
super.beforeUpdate(user, object, model);
super.beforeUpdate(user, model);
// Name must not be blank // Name must not be blank
if (model.getName() == null || model.getName().trim().isEmpty()) if (model.getName() == null || model.getName().trim().isEmpty())
throw new GuacamoleClientException("Connection names must not be blank."); throw new GuacamoleClientException("Connection names must not be blank.");

View File

@@ -139,9 +139,10 @@ public class ConnectionGroupService extends ModeledChildDirectoryObjectService<M
@Override @Override
protected void beforeCreate(ModeledAuthenticatedUser user, protected void beforeCreate(ModeledAuthenticatedUser user,
ConnectionGroupModel model) throws GuacamoleException { ConnectionGroup object, ConnectionGroupModel model)
throws GuacamoleException {
super.beforeCreate(user, model); super.beforeCreate(user, object, model);
// Name must not be blank // Name must not be blank
if (model.getName() == null || model.getName().trim().isEmpty()) if (model.getName() == null || model.getName().trim().isEmpty())
@@ -156,9 +157,10 @@ public class ConnectionGroupService extends ModeledChildDirectoryObjectService<M
@Override @Override
protected void beforeUpdate(ModeledAuthenticatedUser user, protected void beforeUpdate(ModeledAuthenticatedUser user,
ConnectionGroupModel model) throws GuacamoleException { ModeledConnectionGroup object, ConnectionGroupModel model)
throws GuacamoleException {
super.beforeUpdate(user, model); super.beforeUpdate(user, object, model);
// Name must not be blank // Name must not be blank
if (model.getName() == null || model.getName().trim().isEmpty()) if (model.getName() == null || model.getName().trim().isEmpty())

View File

@@ -0,0 +1,42 @@
/*
* 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.guacamole.auth.jdbc.security;
/**
* Thrown when an attempt is made to set a user's password to a string which
* contains their own username, in violation of the defined password policy.
*
* @author Michael Jumper
*/
public class PasswordContainsUsernameException extends PasswordPolicyException {
/**
* Creates a new PasswordContainsUsernameException with the given
* human-readable message. The translatable message is already defined.
*
* @param message
* A human-readable message describing the password policy violation
* that occurred.
*/
public PasswordContainsUsernameException(String message) {
super(message, "PASSWORD_POLICY.ERROR_CONTAINS_USERNAME");
}
}

View File

@@ -0,0 +1,52 @@
/*
* 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.guacamole.auth.jdbc.security;
import java.util.Collections;
import org.apache.guacamole.language.TranslatableMessage;
/**
* Thrown when an attempt is made to set a user's password to a string which is
* too short, in violation of the defined password policy.
*
* @author Michael Jumper
*/
public class PasswordMinimumLengthException extends PasswordPolicyException {
/**
* Creates a new PasswordMinimumLengthException with the given
* human-readable message. The translatable message is already defined.
*
* @param message
* A human-readable message describing the password policy violation
* that occurred.
*
* @param length
* The minimum length that passwords must have to avoid violating
* policy, in characters.
*/
public PasswordMinimumLengthException(String message, int length) {
super(message, new TranslatableMessage(
"PASSWORD_POLICY.ERROR_TOO_SHORT",
Collections.singletonMap("LENGTH", length)
));
}
}

View File

@@ -0,0 +1,103 @@
/*
* 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.guacamole.auth.jdbc.security;
import org.apache.guacamole.GuacamoleException;
/**
* A set of restrictions which define the level of complexity required for
* the passwords of Guacamole user accounts.
*
* @author Michael Jumper
*/
public interface PasswordPolicy {
/**
* Returns the minimum length of new passwords, in characters. Passwords
* which are shorter than this length cannot be used.
*
* @return
* The minimum number of characters required for new passwords.
*
* @throws GuacamoleException
* If the minimum password length cannot be parsed from
* guacamole.properties.
*/
int getMinimumLength() throws GuacamoleException;
/**
* Returns whether both uppercase and lowercase characters must be present
* in new passwords. If true, passwords which do not have at least one
* uppercase letter and one lowercase letter cannot be used.
*
* @return
* true if both uppercase and lowercase characters must be present in
* new passwords, false otherwise.
*
* @throws GuacamoleException
* If the multiple case requirement cannot be parsed from
* guacamole.properties.
*/
boolean isMultipleCaseRequired() throws GuacamoleException;
/**
* Returns whether numeric characters (digits) must be present in new
* passwords. If true, passwords which do not have at least one numeric
* character cannot be used.
*
* @return
* true if numeric characters must be present in new passwords,
* false otherwise.
*
* @throws GuacamoleException
* If the numeric character requirement cannot be parsed from
* guacamole.properties.
*/
boolean isNumericRequired() throws GuacamoleException;
/**
* Returns whether non-alphanumeric characters (symbols) must be present in
* new passwords. If true, passwords which do not have at least one
* non-alphanumeric character cannot be used.
*
* @return
* true if non-alphanumeric characters must be present in new passwords,
* false otherwise.
*
* @throws GuacamoleException
* If the non-alphanumeric character requirement cannot be parsed from
* guacamole.properties.
*/
boolean isNonAlphanumericRequired() throws GuacamoleException;
/**
* Returns whether new passwords must not contain the user's own username.
*
* @return
* true if new passwords must not contain the user's own username,
* false otherwise.
*
* @throws GuacamoleException
* If the username password restriction cannot be parsed from
* guacamole.properties.
*/
boolean isUsernameProhibited() throws GuacamoleException;
}

View File

@@ -0,0 +1,86 @@
/*
* 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.guacamole.auth.jdbc.security;
import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.language.Translatable;
import org.apache.guacamole.language.TranslatableMessage;
/**
* Thrown when an attempt to change a user's password fails due to a violation
* of password complexity policies.
*
* @author Michael Jumper
*/
public class PasswordPolicyException extends GuacamoleClientException
implements Translatable {
/**
* A translatable message which, after being passed through the translation
* system, describes the policy violation that occurred.
*/
private final TranslatableMessage translatableMessage;
/**
* Creates a new PasswordPolicyException with the given human-readable
* message (which will not be passed through the translation system) and
* translation string key(which WILL be passed through the translation
* system), both of which should describe the policy violation that
* occurred.
*
* @param message
* A human-readable message describing the policy violation that
* occurred.
*
* @param translationKey
* The key of a translation string known to the translation system
* which describes the policy violation that occurred.
*/
public PasswordPolicyException(String message, String translationKey) {
super(message);
this.translatableMessage = new TranslatableMessage(translationKey);
}
/**
* Creates a new PasswordPolicyException with the given human-readable
* message (which will not be passed through the translation system) and
* translatable message (which WILL be passed through the translation
* system), both of which should describe the policy violation that
* occurred.
*
* @param message
* A human-readable message describing the policy violation that
* occurred.
*
* @param translatableMessage
* A translatable message which, after being passed through the
* translation system, describes the policy violation that occurred.
*/
public PasswordPolicyException(String message, TranslatableMessage translatableMessage) {
super(message);
this.translatableMessage = translatableMessage;
}
@Override
public TranslatableMessage getTranslatableMessage() {
return translatableMessage;
}
}

View File

@@ -0,0 +1,149 @@
/*
* 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.guacamole.auth.jdbc.security;
import com.google.inject.Inject;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.jdbc.JDBCEnvironment;
/**
* Service which verifies compliance with the password policy configured via
* guacamole.properties.
*
* @author Michael Jumper
*/
public class PasswordPolicyService {
/**
* The Guacamole server environment.
*/
@Inject
private JDBCEnvironment environment;
/**
* Regular expression which matches only if the string contains at least one
* lowercase character.
*/
private final Pattern CONTAINS_LOWERCASE = Pattern.compile("\\p{javaLowerCase}");
/**
* Regular expression which matches only if the string contains at least one
* uppercase character.
*/
private final Pattern CONTAINS_UPPERCASE = Pattern.compile("\\p{javaUpperCase}");
/**
* Regular expression which matches only if the string contains at least one
* numeric character.
*/
private final Pattern CONTAINS_DIGIT = Pattern.compile("\\p{Digit}");
/**
* Regular expression which matches only if the string contains at least one
* non-alphanumeric character.
*/
private final Pattern CONTAINS_NON_ALPHANUMERIC =
Pattern.compile("[^\\p{javaLowerCase}\\p{javaUpperCase}\\p{Digit}]");
/**
* Returns whether the given string matches all of the provided regular
* expressions.
*
* @param str
* The string to test against all provided regular expressions.
*
* @param patterns
* The regular expressions to match against the given string.
*
* @return
* true if the given string matches all provided regular expressions,
* false otherwise.
*/
private boolean matches(String str, Pattern... patterns) {
// Check given string against all provided patterns
for (Pattern pattern : patterns) {
// Fail overall test if any pattern fails to match
Matcher matcher = pattern.matcher(str);
if (!matcher.find())
return false;
}
// All provided patterns matched
return true;
}
/**
* Verifies that the given new password complies with the password policy
* configured within guacamole.properties, throwing a GuacamoleException if
* the policy is violated in any way.
*
* @param username
* The username of the user whose password is being changed.
*
* @param password
* The proposed new password.
*
* @throws GuacamoleException
* If the password policy cannot be parsed, or if the proposed password
* violates the password policy.
*/
public void verifyPassword(String username, String password)
throws GuacamoleException {
// Retrieve password policy from environment
PasswordPolicy policy = environment.getPasswordPolicy();
// Enforce minimum password length
if (password.length() < policy.getMinimumLength())
throw new PasswordMinimumLengthException(
"Password does not meet minimum length requirements.",
policy.getMinimumLength());
// Disallow passwords containing the username
if (policy.isUsernameProhibited() && password.toLowerCase().contains(username.toLowerCase()))
throw new PasswordContainsUsernameException(
"Password must not contain username.");
// Require both uppercase and lowercase characters
if (policy.isMultipleCaseRequired() && !matches(password, CONTAINS_LOWERCASE, CONTAINS_UPPERCASE))
throw new PasswordRequiresMultipleCaseException(
"Password must contain both uppercase and lowercase.");
// Require digits
if (policy.isNumericRequired() && !matches(password, CONTAINS_DIGIT))
throw new PasswordRequiresDigitException(
"Passwords must contain at least one digit.");
// Require non-alphanumeric symbols
if (policy.isNonAlphanumericRequired() && !matches(password, CONTAINS_NON_ALPHANUMERIC))
throw new PasswordRequiresSymbolException(
"Passwords must contain at least one non-alphanumeric character.");
// Password passes all defined restrictions
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.guacamole.auth.jdbc.security;
/**
* Thrown when an attempt is made to set a user's password to a string which
* contains no numeric characters (digits), in violation of the defined password
* policy.
*
* @author Michael Jumper
*/
public class PasswordRequiresDigitException extends PasswordPolicyException {
/**
* Creates a new PasswordRequiresDigitException with the given
* human-readable message. The translatable message is already defined.
*
* @param message
* A human-readable message describing the password policy violation
* that occurred.
*/
public PasswordRequiresDigitException(String message) {
super(message, "PASSWORD_POLICY.ERROR_REQUIRES_DIGIT");
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.guacamole.auth.jdbc.security;
/**
* Thrown when an attempt is made to set a user's password to a string which
* does not contain both uppercase and lowercase characters, in violation of the
* defined password policy.
*
* @author Michael Jumper
*/
public class PasswordRequiresMultipleCaseException extends PasswordPolicyException {
/**
* Creates a new PasswordRequiresMultipleCaseException with the given
* human-readable message. The translatable message is already defined.
*
* @param message
* A human-readable message describing the password policy violation
* that occurred.
*/
public PasswordRequiresMultipleCaseException(String message) {
super(message, "PASSWORD_POLICY.ERROR_REQUIRES_MULTIPLE_CASE");
}
}

View File

@@ -0,0 +1,43 @@
/*
* 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.guacamole.auth.jdbc.security;
/**
* Thrown when an attempt is made to set a user's password to a string which
* contains no non-alphanumeric characters (symbols), in violation of the
* defined password policy.
*
* @author Michael Jumper
*/
public class PasswordRequiresSymbolException extends PasswordPolicyException {
/**
* Creates a new PasswordRequiresSymbolException with the given
* human-readable message. The translatable message is already defined.
*
* @param message
* A human-readable message describing the password policy violation
* that occurred.
*/
public PasswordRequiresSymbolException(String message) {
super(message, "PASSWORD_POLICY.ERROR_REQUIRES_NON_ALNUM");
}
}

View File

@@ -139,9 +139,10 @@ public class SharingProfileService
@Override @Override
protected void beforeCreate(ModeledAuthenticatedUser user, protected void beforeCreate(ModeledAuthenticatedUser user,
SharingProfileModel model) throws GuacamoleException { SharingProfile object, SharingProfileModel model)
throws GuacamoleException {
super.beforeCreate(user, model); super.beforeCreate(user, object, model);
// Name must not be blank // Name must not be blank
if (model.getName() == null || model.getName().trim().isEmpty()) if (model.getName() == null || model.getName().trim().isEmpty())
@@ -156,9 +157,10 @@ public class SharingProfileService
@Override @Override
protected void beforeUpdate(ModeledAuthenticatedUser user, protected void beforeUpdate(ModeledAuthenticatedUser user,
SharingProfileModel model) throws GuacamoleException { ModeledSharingProfile object, SharingProfileModel model)
throws GuacamoleException {
super.beforeUpdate(user, model); super.beforeUpdate(user, object, model);
// Name must not be blank // Name must not be blank
if (model.getName() == null || model.getName().trim().isEmpty()) if (model.getName() == null || model.getName().trim().isEmpty())

View File

@@ -206,10 +206,10 @@ public class ModeledUser extends ModeledDirectoryObject<UserModel> implements Us
// Store plaintext password internally // Store plaintext password internally
this.password = password; this.password = password;
// If no password provided, clear password salt and hash // If no password provided, set random password
if (password == null) { if (password == null) {
userModel.setPasswordSalt(null); userModel.setPasswordSalt(saltService.generateSalt());
userModel.setPasswordHash(null); userModel.setPasswordHash(saltService.generateSalt());
} }
// Otherwise generate new salt and hash given password using newly-generated salt // Otherwise generate new salt and hash given password using newly-generated salt

View File

@@ -35,6 +35,7 @@ import org.apache.guacamole.auth.jdbc.permission.ObjectPermissionMapper;
import org.apache.guacamole.auth.jdbc.permission.ObjectPermissionModel; import org.apache.guacamole.auth.jdbc.permission.ObjectPermissionModel;
import org.apache.guacamole.auth.jdbc.permission.UserPermissionMapper; import org.apache.guacamole.auth.jdbc.permission.UserPermissionMapper;
import org.apache.guacamole.auth.jdbc.security.PasswordEncryptionService; import org.apache.guacamole.auth.jdbc.security.PasswordEncryptionService;
import org.apache.guacamole.auth.jdbc.security.PasswordPolicyService;
import org.apache.guacamole.form.Field; import org.apache.guacamole.form.Field;
import org.apache.guacamole.form.PasswordField; import org.apache.guacamole.form.PasswordField;
import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticatedUser;
@@ -130,6 +131,12 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
@Inject @Inject
private PasswordEncryptionService encryptionService; private PasswordEncryptionService encryptionService;
/**
* Service for enforcing password complexity policies.
*/
@Inject
private PasswordPolicyService passwordPolicyService;
@Override @Override
protected ModeledDirectoryObjectMapper<UserModel> getObjectMapper() { protected ModeledDirectoryObjectMapper<UserModel> getObjectMapper() {
return userMapper; return userMapper;
@@ -185,10 +192,10 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
} }
@Override @Override
protected void beforeCreate(ModeledAuthenticatedUser user, UserModel model) protected void beforeCreate(ModeledAuthenticatedUser user, User object,
throws GuacamoleException { UserModel model) throws GuacamoleException {
super.beforeCreate(user, model); super.beforeCreate(user, object, model);
// Username must not be blank // Username must not be blank
if (model.getIdentifier() == null || model.getIdentifier().trim().isEmpty()) if (model.getIdentifier() == null || model.getIdentifier().trim().isEmpty())
@@ -199,13 +206,17 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
if (!existing.isEmpty()) if (!existing.isEmpty())
throw new GuacamoleClientException("User \"" + model.getIdentifier() + "\" already exists."); throw new GuacamoleClientException("User \"" + model.getIdentifier() + "\" already exists.");
// Verify new password does not violate defined policies (if specified)
if (object.getPassword() != null)
passwordPolicyService.verifyPassword(object.getIdentifier(), object.getPassword());
} }
@Override @Override
protected void beforeUpdate(ModeledAuthenticatedUser user, protected void beforeUpdate(ModeledAuthenticatedUser user,
UserModel model) throws GuacamoleException { ModeledUser object, UserModel model) throws GuacamoleException {
super.beforeUpdate(user, model); super.beforeUpdate(user, object, model);
// Username must not be blank // Username must not be blank
if (model.getIdentifier() == null || model.getIdentifier().trim().isEmpty()) if (model.getIdentifier() == null || model.getIdentifier().trim().isEmpty())
@@ -220,7 +231,11 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
throw new GuacamoleClientException("User \"" + model.getIdentifier() + "\" already exists."); throw new GuacamoleClientException("User \"" + model.getIdentifier() + "\" already exists.");
} }
// Verify new password does not violate defined policies (if specified)
if (object.getPassword() != null)
passwordPolicyService.verifyPassword(object.getIdentifier(), object.getPassword());
} }
@Override @Override
@@ -412,6 +427,9 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
if (!newPassword.equals(confirmNewPassword)) if (!newPassword.equals(confirmNewPassword))
throw new GuacamoleClientException("LOGIN.ERROR_PASSWORD_MISMATCH"); throw new GuacamoleClientException("LOGIN.ERROR_PASSWORD_MISMATCH");
// Verify new password does not violate defined policies
passwordPolicyService.verifyPassword(username, newPassword);
// Change password and reset expiration flag // Change password and reset expiration flag
userModel.setExpired(false); userModel.setExpired(false);
user.setPassword(newPassword); user.setPassword(newPassword);

View File

@@ -54,6 +54,16 @@
"INFO_SHARED_BY" : "Shared by {USERNAME}" "INFO_SHARED_BY" : "Shared by {USERNAME}"
}, },
"PASSWORD_POLICY" : {
"ERROR_CONTAINS_USERNAME" : "Passwords may not contain the username.",
"ERROR_REQUIRES_DIGIT" : "Passwords must contain at least one digit.",
"ERROR_REQUIRES_MULTIPLE_CASE" : "Passwords must contain both uppercase and lowercase characters.",
"ERROR_REQUIRES_NON_ALNUM" : "Passwords must contain at least one symbol.",
"ERROR_TOO_SHORT" : "Passwords must be at least {LENGTH} {LENGTH, plural, one{character} other{characters}} long."
},
"USER_ATTRIBUTES" : { "USER_ATTRIBUTES" : {
"FIELD_HEADER_DISABLED" : "Login disabled:", "FIELD_HEADER_DISABLED" : "Login disabled:",

View File

@@ -23,6 +23,7 @@ import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.jdbc.JDBCEnvironment; import org.apache.guacamole.auth.jdbc.JDBCEnvironment;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.apache.guacamole.auth.jdbc.security.PasswordPolicy;
/** /**
* A MySQL-specific implementation of JDBCEnvironment provides database * A MySQL-specific implementation of JDBCEnvironment provides database
@@ -220,6 +221,11 @@ public class MySQLEnvironment extends JDBCEnvironment {
); );
} }
@Override
public PasswordPolicy getPasswordPolicy() {
return new MySQLPasswordPolicy(this);
}
/** /**
* Returns the hostname of the MySQL server hosting the Guacamole * Returns the hostname of the MySQL server hosting the Guacamole
* authentication tables. If unspecified, this will be "localhost". * authentication tables. If unspecified, this will be "localhost".

View File

@@ -0,0 +1,142 @@
/*
* 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.guacamole.auth.mysql;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.jdbc.JDBCEnvironment;
import org.apache.guacamole.auth.jdbc.security.PasswordPolicy;
import org.apache.guacamole.properties.BooleanGuacamoleProperty;
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
/**
* PasswordPolicy implementation which reads the details of the policy from
* MySQL-specific properties in guacamole.properties.
*
* @author Michael Jumper
*/
public class MySQLPasswordPolicy implements PasswordPolicy {
/**
* The property which specifies the minimum length required of all user
* passwords. By default, this will be zero.
*/
private static final IntegerGuacamoleProperty MIN_LENGTH =
new IntegerGuacamoleProperty() {
@Override
public String getName() { return "mysql-user-password-min-length"; }
};
/**
* The property which specifies whether all user passwords must have at
* least one lowercase character and one uppercase character. By default,
* no such restriction is imposed.
*/
private static final BooleanGuacamoleProperty REQUIRE_MULTIPLE_CASE =
new BooleanGuacamoleProperty() {
@Override
public String getName() { return "mysql-user-password-require-multiple-case"; }
};
/**
* The property which specifies whether all user passwords must have at
* least one numeric character (digit). By default, no such restriction is
* imposed.
*/
private static final BooleanGuacamoleProperty REQUIRE_DIGIT =
new BooleanGuacamoleProperty() {
@Override
public String getName() { return "mysql-user-password-require-digit"; }
};
/**
* The property which specifies whether all user passwords must have at
* least one non-alphanumeric character (symbol). By default, no such
* restriction is imposed.
*/
private static final BooleanGuacamoleProperty REQUIRE_SYMBOL =
new BooleanGuacamoleProperty() {
@Override
public String getName() { return "mysql-user-password-require-symbol"; }
};
/**
* The property which specifies whether users are prohibited from including
* their own username in their password. By default, no such restriction is
* imposed.
*/
private static final BooleanGuacamoleProperty PROHIBIT_USERNAME =
new BooleanGuacamoleProperty() {
@Override
public String getName() { return "mysql-user-password-prohibit-username"; }
};
/**
* The Guacamole server environment.
*/
private final JDBCEnvironment environment;
/**
* Creates a new MySQLPasswordPolicy which reads the details of the policy
* from the properties exposed by the given environment.
*
* @param environment
* The environment from which password policy properties should be
* read.
*/
public MySQLPasswordPolicy(JDBCEnvironment environment) {
this.environment = environment;
}
@Override
public int getMinimumLength() throws GuacamoleException {
return environment.getProperty(MIN_LENGTH, 0);
}
@Override
public boolean isMultipleCaseRequired() throws GuacamoleException {
return environment.getProperty(REQUIRE_MULTIPLE_CASE, false);
}
@Override
public boolean isNumericRequired() throws GuacamoleException {
return environment.getProperty(REQUIRE_DIGIT, false);
}
@Override
public boolean isNonAlphanumericRequired() throws GuacamoleException {
return environment.getProperty(REQUIRE_SYMBOL, false);
}
@Override
public boolean isUsernameProhibited() throws GuacamoleException {
return environment.getProperty(PROHIBIT_USERNAME, false);
}
}

View File

@@ -23,6 +23,7 @@ import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.jdbc.JDBCEnvironment; import org.apache.guacamole.auth.jdbc.JDBCEnvironment;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.apache.guacamole.auth.jdbc.security.PasswordPolicy;
/** /**
* A PostgreSQL-specific implementation of JDBCEnvironment provides database * A PostgreSQL-specific implementation of JDBCEnvironment provides database
@@ -219,6 +220,11 @@ public class PostgreSQLEnvironment extends JDBCEnvironment {
); );
} }
@Override
public PasswordPolicy getPasswordPolicy() {
return new PostgreSQLPasswordPolicy(this);
}
/** /**
* Returns the hostname of the PostgreSQL server hosting the Guacamole * Returns the hostname of the PostgreSQL server hosting the Guacamole
* authentication tables. If unspecified, this will be "localhost". * authentication tables. If unspecified, this will be "localhost".

View File

@@ -0,0 +1,142 @@
/*
* 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.guacamole.auth.postgresql;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.jdbc.JDBCEnvironment;
import org.apache.guacamole.auth.jdbc.security.PasswordPolicy;
import org.apache.guacamole.properties.BooleanGuacamoleProperty;
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
/**
* PasswordPolicy implementation which reads the details of the policy from
* PostgreSQL-specific properties in guacamole.properties.
*
* @author Michael Jumper
*/
public class PostgreSQLPasswordPolicy implements PasswordPolicy {
/**
* The property which specifies the minimum length required of all user
* passwords. By default, this will be zero.
*/
private static final IntegerGuacamoleProperty MIN_LENGTH =
new IntegerGuacamoleProperty() {
@Override
public String getName() { return "postgresql-user-password-min-length"; }
};
/**
* The property which specifies whether all user passwords must have at
* least one lowercase character and one uppercase character. By default,
* no such restriction is imposed.
*/
private static final BooleanGuacamoleProperty REQUIRE_MULTIPLE_CASE =
new BooleanGuacamoleProperty() {
@Override
public String getName() { return "postgresql-user-password-require-multiple-case"; }
};
/**
* The property which specifies whether all user passwords must have at
* least one numeric character (digit). By default, no such restriction is
* imposed.
*/
private static final BooleanGuacamoleProperty REQUIRE_DIGIT =
new BooleanGuacamoleProperty() {
@Override
public String getName() { return "postgresql-user-password-require-digit"; }
};
/**
* The property which specifies whether all user passwords must have at
* least one non-alphanumeric character (symbol). By default, no such
* restriction is imposed.
*/
private static final BooleanGuacamoleProperty REQUIRE_SYMBOL =
new BooleanGuacamoleProperty() {
@Override
public String getName() { return "postgresql-user-password-require-symbol"; }
};
/**
* The property which specifies whether users are prohibited from including
* their own username in their password. By default, no such restriction is
* imposed.
*/
private static final BooleanGuacamoleProperty PROHIBIT_USERNAME =
new BooleanGuacamoleProperty() {
@Override
public String getName() { return "postgresql-user-password-prohibit-username"; }
};
/**
* The Guacamole server environment.
*/
private final JDBCEnvironment environment;
/**
* Creates a new PostgreSQLPasswordPolicy which reads the details of the
* policy from the properties exposed by the given environment.
*
* @param environment
* The environment from which password policy properties should be
* read.
*/
public PostgreSQLPasswordPolicy(JDBCEnvironment environment) {
this.environment = environment;
}
@Override
public int getMinimumLength() throws GuacamoleException {
return environment.getProperty(MIN_LENGTH, 0);
}
@Override
public boolean isMultipleCaseRequired() throws GuacamoleException {
return environment.getProperty(REQUIRE_MULTIPLE_CASE, false);
}
@Override
public boolean isNumericRequired() throws GuacamoleException {
return environment.getProperty(REQUIRE_DIGIT, false);
}
@Override
public boolean isNonAlphanumericRequired() throws GuacamoleException {
return environment.getProperty(REQUIRE_SYMBOL, false);
}
@Override
public boolean isUsernameProhibited() throws GuacamoleException {
return environment.getProperty(PROHIBIT_USERNAME, false);
}
}

View File

@@ -21,11 +21,9 @@ package org.apache.guacamole.rest.user;
import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject; import com.google.inject.assistedinject.AssistedInject;
import java.util.UUID;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.Produces; import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.User; import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.Directory;
import org.apache.guacamole.net.auth.UserContext; import org.apache.guacamole.net.auth.UserContext;
@@ -69,15 +67,4 @@ public class UserDirectoryResource extends DirectoryResource<User, APIUser> {
super(userContext, directory, translator, resourceFactory); super(userContext, directory, translator, resourceFactory);
} }
@Override
public APIUser createObject(APIUser object) throws GuacamoleException {
// Randomly set the password if it wasn't provided
if (object.getPassword() == null)
object.setPassword(UUID.randomUUID().toString());
return super.createObject(object);
}
} }