blob: 50b9e4295166024d93431fac35f198b59548aa04 [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.guacamole.auth.jdbc.user;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.auth.jdbc.base.ModeledDirectoryObjectMapper;
import org.apache.guacamole.auth.jdbc.base.ModeledDirectoryObjectService;
import org.apache.guacamole.GuacamoleClientException;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleSecurityException;
import org.apache.guacamole.GuacamoleUnsupportedException;
import org.apache.guacamole.auth.jdbc.base.ActivityRecordModel;
import org.apache.guacamole.auth.jdbc.base.ActivityRecordSearchTerm;
import org.apache.guacamole.auth.jdbc.base.ActivityRecordSortPredicate;
import org.apache.guacamole.auth.jdbc.base.EntityMapper;
import org.apache.guacamole.auth.jdbc.base.ModeledActivityRecord;
import org.apache.guacamole.auth.jdbc.permission.ObjectPermissionMapper;
import org.apache.guacamole.auth.jdbc.permission.ObjectPermissionModel;
import org.apache.guacamole.auth.jdbc.permission.UserPermissionMapper;
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.PasswordField;
import org.apache.guacamole.language.TranslatableGuacamoleClientException;
import org.apache.guacamole.language.TranslatableGuacamoleInsufficientCredentialsException;
import org.apache.guacamole.net.auth.ActivityRecord;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
import org.apache.guacamole.net.auth.permission.ObjectPermission;
import org.apache.guacamole.net.auth.permission.ObjectPermissionSet;
import org.apache.guacamole.net.auth.permission.SystemPermission;
import org.apache.guacamole.net.auth.permission.SystemPermissionSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Service which provides convenience methods for creating, retrieving, and
* manipulating users.
*/
public class UserService extends ModeledDirectoryObjectService<ModeledUser, User, UserModel> {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
/**
* All user permissions which are implicitly granted to the new user upon
* creation.
*/
private static final ObjectPermission.Type[] IMPLICIT_USER_PERMISSIONS = {
ObjectPermission.Type.READ
};
/**
* The name of the HTTP password parameter to expect if the user is
* changing their expired password upon login.
*/
private static final String NEW_PASSWORD_PARAMETER = "new-password";
/**
* The password field to provide the user when their password is expired
* and must be changed.
*/
private static final Field NEW_PASSWORD = new PasswordField(NEW_PASSWORD_PARAMETER);
/**
* The name of the HTTP password confirmation parameter to expect if the
* user is changing their expired password upon login.
*/
private static final String CONFIRM_NEW_PASSWORD_PARAMETER = "confirm-new-password";
/**
* The password confirmation field to provide the user when their password
* is expired and must be changed.
*/
private static final Field CONFIRM_NEW_PASSWORD = new PasswordField(CONFIRM_NEW_PASSWORD_PARAMETER);
/**
* Information describing the expected credentials if a user's password is
* expired. If a user's password is expired, it must be changed during the
* login process.
*/
private static final CredentialsInfo EXPIRED_PASSWORD = new CredentialsInfo(Arrays.asList(
CredentialsInfo.USERNAME,
CredentialsInfo.PASSWORD,
NEW_PASSWORD,
CONFIRM_NEW_PASSWORD
));
/**
* Mapper for creating/deleting entities.
*/
@Inject
private EntityMapper entityMapper;
/**
* Mapper for accessing users.
*/
@Inject
private UserMapper userMapper;
/**
* Mapper for manipulating user permissions.
*/
@Inject
private UserPermissionMapper userPermissionMapper;
/**
* Mapper for accessing user login history.
*/
@Inject
private UserRecordMapper userRecordMapper;
/**
* Provider for creating users.
*/
@Inject
private Provider<ModeledUser> userProvider;
/**
* Service for hashing passwords.
*/
@Inject
private PasswordEncryptionService encryptionService;
/**
* Service for enforcing password complexity policies.
*/
@Inject
private PasswordPolicyService passwordPolicyService;
@Override
protected ModeledDirectoryObjectMapper<UserModel> getObjectMapper() {
return userMapper;
}
@Override
protected ObjectPermissionMapper getPermissionMapper() {
return userPermissionMapper;
}
@Override
protected ModeledUser getObjectInstance(ModeledAuthenticatedUser currentUser,
UserModel model) throws GuacamoleException {
boolean exposeRestrictedAttributes;
// Expose restricted attributes if the user does not yet exist
if (model.getObjectID() == null)
exposeRestrictedAttributes = true;
// Otherwise, if the user permissions are available, expose restricted
// attributes only if the user has ADMINISTER permission
else if (currentUser != null)
exposeRestrictedAttributes = hasObjectPermission(currentUser,
model.getIdentifier(), ObjectPermission.Type.ADMINISTER);
// If user permissions are not available, do not expose anything
else
exposeRestrictedAttributes = false;
// Produce ModeledUser exposing only those attributes for which the
// current user has permission
ModeledUser user = userProvider.get();
user.init(currentUser, model, exposeRestrictedAttributes);
return user;
}
@Override
protected UserModel getModelInstance(ModeledAuthenticatedUser currentUser,
final User object) throws GuacamoleException {
// Create new ModeledUser backed by blank model
UserModel model = new UserModel();
ModeledUser user = getObjectInstance(currentUser, model);
// Set model contents through ModeledUser, copying the provided user
user.setIdentifier(object.getIdentifier());
user.setPassword(object.getPassword());
user.setAttributes(object.getAttributes());
return model;
}
@Override
protected boolean hasCreatePermission(ModeledAuthenticatedUser user)
throws GuacamoleException {
// Return whether user has explicit user creation permission
SystemPermissionSet permissionSet = user.getUser().getEffectivePermissions().getSystemPermissions();
return permissionSet.hasPermission(SystemPermission.Type.CREATE_USER);
}
@Override
protected ObjectPermissionSet getEffectivePermissionSet(ModeledAuthenticatedUser user)
throws GuacamoleException {
// Return permissions related to users
return user.getUser().getEffectivePermissions().getUserPermissions();
}
@Override
protected void beforeCreate(ModeledAuthenticatedUser user, User object,
UserModel model) throws GuacamoleException {
super.beforeCreate(user, object, model);
// Username must not be blank
if (model.getIdentifier() == null || model.getIdentifier().trim().isEmpty())
throw new GuacamoleClientException("The username must not be blank.");
// Do not create duplicate users
Collection<UserModel> existing = userMapper.select(Collections.singleton(model.getIdentifier()));
if (!existing.isEmpty())
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());
// Create base entity object, implicitly populating underlying entity ID
entityMapper.insert(model);
}
@Override
protected void beforeUpdate(ModeledAuthenticatedUser user,
ModeledUser object, UserModel model) throws GuacamoleException {
super.beforeUpdate(user, object, model);
// Username must not be blank
if (model.getIdentifier() == null || model.getIdentifier().trim().isEmpty())
throw new GuacamoleClientException("The username must not be blank.");
// Check whether such a user is already present
UserModel existing = userMapper.selectOne(model.getIdentifier());
if (existing != null) {
// Do not rename to existing user
if (!existing.getObjectID().equals(model.getObjectID()))
throw new GuacamoleClientException("User \"" + model.getIdentifier() + "\" already exists.");
}
// Verify new password does not violate defined policies (if specified)
if (object.getPassword() != null) {
// Enforce password age only for non-privileged users
if (!user.isPrivileged())
passwordPolicyService.verifyPasswordAge(object);
// Always verify password complexity
passwordPolicyService.verifyPassword(object.getIdentifier(), object.getPassword());
// Store previous password in history
passwordPolicyService.recordPassword(object);
}
}
@Override
protected Collection<ObjectPermissionModel>
getImplicitPermissions(ModeledAuthenticatedUser user, UserModel model) {
// Get original set of implicit permissions and make a copy
Collection<ObjectPermissionModel> implicitPermissions =
new ArrayList<>(super.getImplicitPermissions(user, model));
// Grant implicit permissions to the new user
for (ObjectPermission.Type permissionType : IMPLICIT_USER_PERMISSIONS) {
ObjectPermissionModel permissionModel = new ObjectPermissionModel();
permissionModel.setEntityID(model.getEntityID());
permissionModel.setType(permissionType);
permissionModel.setObjectIdentifier(model.getIdentifier());
// Add new permission to implicit permission set
implicitPermissions.add(permissionModel);
}
return Collections.unmodifiableCollection(implicitPermissions);
}
@Override
protected void beforeDelete(ModeledAuthenticatedUser user, String identifier) throws GuacamoleException {
super.beforeDelete(user, identifier);
// Do not allow users to delete themselves
if (identifier.equals(user.getUser().getIdentifier()))
throw new GuacamoleUnsupportedException("Deleting your own user is not allowed.");
}
@Override
protected boolean isValidIdentifier(String identifier) {
// All strings are valid user identifiers
return true;
}
/**
* Retrieves the user corresponding to the given credentials from the
* database. Note that this function will not enforce any additional
* account restrictions, including explicitly disabled accounts,
* scheduling, and password expiration. It is the responsibility of the
* caller to enforce such restrictions, if desired.
*
* @param authenticationProvider
* The AuthenticationProvider on behalf of which the user is being
* retrieved.
*
* @param credentials
* The credentials to use when locating the user.
*
* @return
* An AuthenticatedUser containing the existing ModeledUser object if
* the credentials given are valid, null otherwise.
*
* @throws GuacamoleException
* If the provided credentials to not conform to expectations.
*/
public ModeledAuthenticatedUser retrieveAuthenticatedUser(AuthenticationProvider authenticationProvider,
Credentials credentials) throws GuacamoleException {
// Get username and password
String username = credentials.getUsername();
String password = credentials.getPassword();
// Retrieve corresponding user model, if such a user exists
UserModel userModel = userMapper.selectOne(username);
if (userModel == null)
return null;
// Verify provided password is correct
byte[] hash = encryptionService.createPasswordHash(password, userModel.getPasswordSalt());
if (!Arrays.equals(hash, userModel.getPasswordHash()))
return null;
// Create corresponding user object, set up cyclic reference
ModeledUser user = getObjectInstance(null, userModel);
user.setCurrentUser(new ModeledAuthenticatedUser(authenticationProvider, user, credentials));
// Return now-authenticated user
return user.getCurrentUser();
}
/**
* Retrieves the user corresponding to the given AuthenticatedUser from the
* database.
*
* @param authenticationProvider
* The AuthenticationProvider on behalf of which the user is being
* retrieved.
*
* @param authenticatedUser
* The AuthenticatedUser to retrieve the corresponding ModeledUser of.
*
* @return
* The ModeledUser which corresponds to the given AuthenticatedUser, or
* null if no such user exists.
*
* @throws GuacamoleException
* If a ModeledUser object for the user corresponding to the given
* AuthenticatedUser cannot be created.
*/
public ModeledUser retrieveUser(AuthenticationProvider authenticationProvider,
AuthenticatedUser authenticatedUser) throws GuacamoleException {
// If we already queried this user, return that rather than querying again
if (authenticatedUser instanceof ModeledAuthenticatedUser)
return ((ModeledAuthenticatedUser) authenticatedUser).getUser();
// Retrieve corresponding user model, if such a user exists
UserModel userModel = userMapper.selectOne(authenticatedUser.getIdentifier());
if (userModel == null)
return null;
// Create corresponding user object, set up cyclic reference
ModeledUser user = getObjectInstance(null, userModel);
user.setCurrentUser(new ModeledAuthenticatedUser(authenticatedUser,
authenticationProvider, user));
// Return already-authenticated user
return user;
}
/**
* Generates an empty (skeleton) user corresponding to the given
* AuthenticatedUser. The user will not be stored in the database, and
* will only be available in-memory during the time the session is
* active.
*
* @param authenticationProvider
* The AuthenticationProvider on behalf of which the user is being
* retrieved.
*
* @param authenticatedUser
* The AuthenticatedUser to generate the skeleton account for.
*
* @return
* The empty ModeledUser which corresponds to the given
* AuthenticatedUser.
*
* @throws GuacamoleException
* If a ModeledUser object for the user corresponding to the given
* AuthenticatedUser cannot be created.
*/
public ModeledUser retrieveSkeletonUser(AuthenticationProvider authenticationProvider,
AuthenticatedUser authenticatedUser) throws GuacamoleException {
// Set up an empty user model
ModeledUser user = getObjectInstance(null,
new UserModel(authenticatedUser.getIdentifier()));
// Create user object, and configure cyclic reference
user.setCurrentUser(new ModeledAuthenticatedUser(authenticatedUser,
authenticationProvider, user));
// Return the new user.
return user;
}
/**
* Resets the password of the given user to the new password specified via
* the "new-password" and "confirm-new-password" parameters from the
* provided credentials. If these parameters are missing or invalid,
* additional credentials will be requested.
*
* @param user
* The user whose password should be reset.
*
* @param credentials
* The credentials from which the parameters required for password
* reset should be retrieved.
*
* @throws GuacamoleException
* If the password reset parameters within the given credentials are
* invalid or missing.
*/
public void resetExpiredPassword(ModeledUser user, Credentials credentials)
throws GuacamoleException {
UserModel userModel = user.getModel();
// Get username
String username = user.getIdentifier();
// Pull new password from HTTP request
HttpServletRequest request = credentials.getRequest();
String newPassword = request.getParameter(NEW_PASSWORD_PARAMETER);
String confirmNewPassword = request.getParameter(CONFIRM_NEW_PASSWORD_PARAMETER);
// Require new password if account is expired
if (newPassword == null || confirmNewPassword == null) {
logger.info("The password of user \"{}\" has expired and must be reset.", username);
throw new TranslatableGuacamoleInsufficientCredentialsException("Password has expired",
"LOGIN.INFO_PASSWORD_EXPIRED", EXPIRED_PASSWORD);
}
// New password must be different from old password
if (newPassword.equals(credentials.getPassword()))
throw new TranslatableGuacamoleClientException("New passwords may "
+ "not be identical to the current password if password "
+ "reset is required.", "LOGIN.ERROR_PASSWORD_SAME");
// New password must not be blank
if (newPassword.isEmpty())
throw new TranslatableGuacamoleClientException("Passwords may not "
+ "be blank.", "LOGIN.ERROR_PASSWORD_BLANK");
// Confirm that the password was entered correctly twice
if (!newPassword.equals(confirmNewPassword))
throw new TranslatableGuacamoleClientException("New password does "
+ "not match.", "LOGIN.ERROR_PASSWORD_MISMATCH");
// Verify new password does not violate defined policies
passwordPolicyService.verifyPassword(username, newPassword);
// Change password and reset expiration flag
userModel.setExpired(false);
user.setPassword(newPassword);
userMapper.update(userModel);
logger.info("Expired password of user \"{}\" has been reset.", username);
}
/**
* Returns a ActivityRecord object which is backed by the given model.
*
* @param model
* The model object to use to back the returned connection record
* object.
*
* @return
* A connection record object which is backed by the given model.
*/
protected ActivityRecord getObjectInstance(ActivityRecordModel model) {
return new ModeledActivityRecord(model);
}
/**
* Returns a list of ActivityRecord objects which are backed by the
* models in the given list.
*
* @param models
* The model objects to use to back the activity record objects
* within the returned list.
*
* @return
* A list of activity record objects which are backed by the models
* in the given list.
*/
protected List<ActivityRecord> getObjectInstances(List<ActivityRecordModel> models) {
// Create new list of records by manually converting each model
List<ActivityRecord> objects = new ArrayList<ActivityRecord>(models.size());
for (ActivityRecordModel model : models)
objects.add(getObjectInstance(model));
return objects;
}
/**
* Retrieves the login history of the given user, including any active
* sessions.
*
* @param authenticatedUser
* The user retrieving the login history.
*
* @param user
* The user whose history is being retrieved.
*
* @return
* The login history of the given user, including any active sessions.
*
* @throws GuacamoleException
* If permission to read the login history is denied.
*/
public List<ActivityRecord> retrieveHistory(ModeledAuthenticatedUser authenticatedUser,
ModeledUser user) throws GuacamoleException {
String username = user.getIdentifier();
// Retrieve history only if READ permission is granted
if (hasObjectPermission(authenticatedUser, username, ObjectPermission.Type.READ))
return getObjectInstances(userRecordMapper.select(username));
// The user does not have permission to read the history
throw new GuacamoleSecurityException("Permission denied.");
}
/**
* Retrieves user login history records matching the given criteria.
* Retrieves up to <code>limit</code> user history records matching the
* given terms and sorted by the given predicates. Only history records
* associated with data that the given user can read are returned.
*
* @param user
* The user retrieving the login history.
*
* @param requiredContents
* The search terms that must be contained somewhere within each of the
* returned records.
*
* @param sortPredicates
* A list of predicates to sort the returned records by, in order of
* priority.
*
* @param limit
* The maximum number of records that should be returned.
*
* @return
* The login history of the given user, including any active sessions.
*
* @throws GuacamoleException
* If permission to read the user login history is denied.
*/
public List<ActivityRecord> retrieveHistory(ModeledAuthenticatedUser user,
Collection<ActivityRecordSearchTerm> requiredContents,
List<ActivityRecordSortPredicate> sortPredicates, int limit)
throws GuacamoleException {
List<ActivityRecordModel> searchResults;
// Bypass permission checks if the user is privileged
if (user.isPrivileged())
searchResults = userRecordMapper.search(requiredContents,
sortPredicates, limit);
// Otherwise only return explicitly readable history records
else
searchResults = userRecordMapper.searchReadable(user.getUser().getModel(),
requiredContents, sortPredicates, limit, user.getEffectiveUserGroups());
return getObjectInstances(searchResults);
}
}