Merge staging/1.2.0 changes back to master.

This commit is contained in:
Virtually Nick
2020-06-18 11:38:34 -04:00
21 changed files with 216 additions and 58 deletions

View File

@@ -139,6 +139,7 @@ public class JDBCAuthenticationProviderService implements AuthenticationProvider
// Initialize the UserContext with the user account and return it. // Initialize the UserContext with the user account and return it.
context.init(user.getCurrentUser()); context.init(user.getCurrentUser());
context.recordUserLogin();
return context; return context;
} }

View File

@@ -82,22 +82,24 @@ public class ActiveConnectionPermissionService
// Retrieve permissions only if allowed // Retrieve permissions only if allowed
if (canReadPermissions(user, targetEntity)) { if (canReadPermissions(user, targetEntity)) {
// Only administrators may access active connections // Privileged accounts (such as administrators or UserContexts
boolean isAdmin = targetEntity.isAdministrator(); // returned by getPrivileged()) may always access active connections
boolean isPrivileged = targetEntity.isPrivileged();
// Get all active connections // Get all active connections
Collection<ActiveConnectionRecord> records = tunnelService.getActiveConnections(user); Collection<ActiveConnectionRecord> records = tunnelService.getActiveConnections(user);
// We have READ, and possibly DELETE, on all active connections // We have READ, and possibly DELETE, on all active connections
Set<ObjectPermission> permissions = new HashSet<ObjectPermission>(); Set<ObjectPermission> permissions = new HashSet<>();
for (ActiveConnectionRecord record : records) { for (ActiveConnectionRecord record : records) {
// Add implicit READ // Add implicit READ
String identifier = record.getUUID().toString(); String identifier = record.getUUID().toString();
permissions.add(new ObjectPermission(ObjectPermission.Type.READ, identifier)); permissions.add(new ObjectPermission(ObjectPermission.Type.READ, identifier));
// If we're an admin, or the connection is ours, then we can DELETE // If the target user is privileged, or the connection belongs
if (isAdmin || targetEntity.isUser(record.getUsername())) // to the target user, then they can DELETE
if (isPrivileged || targetEntity.isUser(record.getUsername()))
permissions.add(new ObjectPermission(ObjectPermission.Type.DELETE, identifier)); permissions.add(new ObjectPermission(ObjectPermission.Type.DELETE, identifier));
} }

View File

@@ -81,7 +81,7 @@ public class ActiveConnectionService
Collection<String> identifiers) throws GuacamoleException { Collection<String> identifiers) throws GuacamoleException {
String username = user.getIdentifier(); String username = user.getIdentifier();
boolean isAdmin = user.getUser().isAdministrator(); boolean isPrivileged = user.isPrivileged();
Set<String> identifierSet = new HashSet<String>(identifiers); Set<String> identifierSet = new HashSet<String>(identifiers);
// Retrieve all visible connections (permissions enforced by tunnel service) // Retrieve all visible connections (permissions enforced by tunnel service)
@@ -95,7 +95,7 @@ public class ActiveConnectionService
// be able to connect to (join) the active connection if they are // be able to connect to (join) the active connection if they are
// the user that started the connection OR the user is an admin // the user that started the connection OR the user is an admin
boolean hasPrivilegedAccess = boolean hasPrivilegedAccess =
isAdmin || username.equals(record.getUsername()); isPrivileged || username.equals(record.getUsername());
// Add connection if within requested identifiers // Add connection if within requested identifiers
if (identifierSet.contains(record.getUUID().toString())) { if (identifierSet.contains(record.getUUID().toString())) {
@@ -211,7 +211,7 @@ public class ActiveConnectionService
ObjectPermissionSet permissionSet = getPermissionSet(user); ObjectPermissionSet permissionSet = getPermissionSet(user);
return user.getUser().isAdministrator() return user.isPrivileged()
|| permissionSet.hasPermission(type, identifier); || permissionSet.hasPermission(type, identifier);
} }

View File

@@ -148,8 +148,8 @@ public abstract class ModeledChildDirectoryObjectService<InternalType extends Mo
protected boolean canUpdateModifiedParents(ModeledAuthenticatedUser user, protected boolean canUpdateModifiedParents(ModeledAuthenticatedUser user,
String identifier, ModelType model) throws GuacamoleException { String identifier, ModelType model) throws GuacamoleException {
// If user is an administrator, no need to check // If user is privileged, no need to check
if (user.getUser().isAdministrator()) if (user.isPrivileged())
return true; return true;
// Verify that we have permission to modify any modified parents // Verify that we have permission to modify any modified parents

View File

@@ -171,7 +171,7 @@ public abstract class ModeledDirectoryObjectService<InternalType extends Modeled
ObjectPermissionSet permissionSet = getEffectivePermissionSet(user); ObjectPermissionSet permissionSet = getEffectivePermissionSet(user);
// Return whether permission is granted // Return whether permission is granted
return user.getUser().isAdministrator() return user.isPrivileged()
|| permissionSet.hasPermission(type, identifier); || permissionSet.hasPermission(type, identifier);
} }
@@ -248,7 +248,7 @@ public abstract class ModeledDirectoryObjectService<InternalType extends Modeled
ExternalType object, 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.isPrivileged() && !hasCreatePermission(user))
throw new GuacamoleSecurityException("Permission denied."); throw new GuacamoleSecurityException("Permission denied.");
} }
@@ -395,8 +395,8 @@ public abstract class ModeledDirectoryObjectService<InternalType extends Modeled
Collection<ModelType> objects; Collection<ModelType> objects;
// Bypass permission checks if the user is a system admin // Bypass permission checks if the user is privileged
if (user.getUser().isAdministrator()) if (user.isPrivileged())
objects = getObjectMapper().select(identifiers); objects = getObjectMapper().select(identifiers);
// Otherwise only return explicitly readable identifiers // Otherwise only return explicitly readable identifiers
@@ -507,8 +507,8 @@ public abstract class ModeledDirectoryObjectService<InternalType extends Modeled
public Set<String> getIdentifiers(ModeledAuthenticatedUser user) public Set<String> getIdentifiers(ModeledAuthenticatedUser user)
throws GuacamoleException { throws GuacamoleException {
// Bypass permission checks if the user is a system admin // Bypass permission checks if the user is privileged
if (user.getUser().isAdministrator()) if (user.isPrivileged())
return getObjectMapper().selectIdentifiers(); return getObjectMapper().selectIdentifiers();
// Otherwise only return explicitly readable identifiers // Otherwise only return explicitly readable identifiers

View File

@@ -132,18 +132,20 @@ public abstract class ModeledPermissions<ModelType extends EntityModel>
} }
/** /**
* Returns whether this entity is a system administrator, and thus is not * Returns whether this entity is effectively unrestricted by permissions,
* restricted by permissions, taking into account permission inheritance * such as a system administrator or an internal user operating via a
* via user groups. * privileged UserContext. Permission inheritance via user groups is taken
* into account.
* *
* @return * @return
* true if this entity is a system administrator, false otherwise. * true if this entity should be unrestricted by permissions, false
* otherwise.
* *
* @throws GuacamoleException * @throws GuacamoleException
* If an error occurs while determining the entity's system administrator * If an error occurs while determining whether permission restrictions
* status. * apply to the entity.
*/ */
public boolean isAdministrator() throws GuacamoleException { public boolean isPrivileged() throws GuacamoleException {
SystemPermissionSet systemPermissionSet = getEffective().getSystemPermissions(); SystemPermissionSet systemPermissionSet = getEffective().getSystemPermissions();
return systemPermissionSet.hasPermission(SystemPermission.Type.ADMINISTER); return systemPermissionSet.hasPermission(SystemPermission.Type.ADMINISTER);
} }

View File

@@ -140,8 +140,9 @@ public abstract class RelatedObjectSet<ParentObjectType extends ModeledDirectory
private boolean canAlterRelation(Collection<String> identifiers) private boolean canAlterRelation(Collection<String> identifiers)
throws GuacamoleException { throws GuacamoleException {
// System administrators may alter any relations // Privileged users (such as system administrators) may alter any
if (getCurrentUser().getUser().isAdministrator()) // relations
if (getCurrentUser().isPrivileged())
return true; return true;
// Non-admin users require UPDATE permission on the parent object ... // Non-admin users require UPDATE permission on the parent object ...
@@ -162,9 +163,9 @@ public abstract class RelatedObjectSet<ParentObjectType extends ModeledDirectory
@Override @Override
public Set<String> getObjects() throws GuacamoleException { public Set<String> getObjects() throws GuacamoleException {
// Bypass permission checks if the user is a system admin // Bypass permission checks if the user is a privileged
ModeledAuthenticatedUser user = getCurrentUser(); ModeledAuthenticatedUser user = getCurrentUser();
if (user.getUser().isAdministrator()) if (user.isPrivileged())
return getObjectRelationMapper().selectChildIdentifiers(parent.getModel()); return getObjectRelationMapper().selectChildIdentifiers(parent.getModel());
// Otherwise only return explicitly readable identifiers // Otherwise only return explicitly readable identifiers

View File

@@ -297,8 +297,8 @@ public class ConnectionService extends ModeledChildDirectoryObjectService<Modele
String identifier) String identifier)
throws GuacamoleException { throws GuacamoleException {
// Bypass permission checks if the user is a system admin // Bypass permission checks if the user is privileged
if (user.getUser().isAdministrator()) if (user.isPrivileged())
return connectionMapper.selectIdentifiersWithin(identifier); return connectionMapper.selectIdentifiersWithin(identifier);
// Otherwise only return explicitly readable identifiers // Otherwise only return explicitly readable identifiers
@@ -470,8 +470,8 @@ public class ConnectionService extends ModeledChildDirectoryObjectService<Modele
List<ConnectionRecordModel> searchResults; List<ConnectionRecordModel> searchResults;
// Bypass permission checks if the user is a system admin // Bypass permission checks if the user is privileged
if (user.getUser().isAdministrator()) if (user.isPrivileged())
searchResults = connectionRecordMapper.search(requiredContents, searchResults = connectionRecordMapper.search(requiredContents,
sortPredicates, limit); sortPredicates, limit);

View File

@@ -218,8 +218,8 @@ public class ConnectionGroupService extends ModeledChildDirectoryObjectService<M
String identifier) String identifier)
throws GuacamoleException { throws GuacamoleException {
// Bypass permission checks if the user is a system admin // Bypass permission checks if the user is privileged
if (user.getUser().isAdministrator()) if (user.isPrivileged())
return connectionGroupMapper.selectIdentifiersWithin(identifier); return connectionGroupMapper.selectIdentifiersWithin(identifier);
// Otherwise only return explicitly readable identifiers // Otherwise only return explicitly readable identifiers

View File

@@ -104,8 +104,8 @@ public abstract class AbstractPermissionService<PermissionSetType extends Permis
if (targetEntity.isUser(user.getUser().getIdentifier())) if (targetEntity.isUser(user.getUser().getIdentifier()))
return true; return true;
// A system adminstrator can do anything // Privileged users (such as system administrators) may do anything
if (user.getUser().isAdministrator()) if (user.isPrivileged())
return true; return true;
// Can read permissions on target entity if explicit READ is granted // Can read permissions on target entity if explicit READ is granted

View File

@@ -95,8 +95,8 @@ public abstract class ModeledObjectPermissionService
Collection<ObjectPermission> permissions) Collection<ObjectPermission> permissions)
throws GuacamoleException { throws GuacamoleException {
// A system adminstrator can do anything // Privileged users (such as system administrators) may do anything
if (user.getUser().isAdministrator()) if (user.isPrivileged())
return true; return true;
// Verify user has update permission on the target entity // Verify user has update permission on the target entity
@@ -187,8 +187,8 @@ public abstract class ModeledObjectPermissionService
if (identifiers.isEmpty()) if (identifiers.isEmpty())
return identifiers; return identifiers;
// If user is an admin, everything is accessible // Privileged users (such as system administrators) may access everything
if (user.getUser().isAdministrator()) if (user.isPrivileged())
return identifiers; return identifiers;
// Otherwise, return explicitly-retrievable identifiers only if allowed // Otherwise, return explicitly-retrievable identifiers only if allowed

View File

@@ -94,8 +94,9 @@ public class SystemPermissionService
ModeledPermissions<? extends EntityModel> targetEntity, ModeledPermissions<? extends EntityModel> targetEntity,
Collection<SystemPermission> permissions) throws GuacamoleException { Collection<SystemPermission> permissions) throws GuacamoleException {
// Only an admin can create system permissions // Only privileged users (such as system administrators) can create
if (user.getUser().isAdministrator()) { // system permissions
if (user.isPrivileged()) {
Collection<SystemPermissionModel> models = getModelInstances(targetEntity, permissions); Collection<SystemPermissionModel> models = getModelInstances(targetEntity, permissions);
systemPermissionMapper.insert(models); systemPermissionMapper.insert(models);
return; return;
@@ -111,8 +112,9 @@ public class SystemPermissionService
ModeledPermissions<? extends EntityModel> targetEntity, ModeledPermissions<? extends EntityModel> targetEntity,
Collection<SystemPermission> permissions) throws GuacamoleException { Collection<SystemPermission> permissions) throws GuacamoleException {
// Only an admin can delete system permissions // Only privileged users (such as system administrators) can delete
if (user.getUser().isAdministrator()) { // system permissions
if (user.isPrivileged()) {
// Do not allow users to remove their own admin powers // Do not allow users to remove their own admin powers
if (user.getUser().getIdentifier().equals(targetEntity.getIdentifier())) if (user.getUser().getIdentifier().equals(targetEntity.getIdentifier()))

View File

@@ -628,8 +628,9 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
if (records.isEmpty()) if (records.isEmpty())
return Collections.<ActiveConnectionRecord>emptyList(); return Collections.<ActiveConnectionRecord>emptyList();
// A system administrator can view all connections; no need to filter // Privileged users (such as system administrators) can view all
if (user.getUser().isAdministrator()) // connections; no need to filter
if (user.isPrivileged())
return records; return records;
// Build set of all connection identifiers associated with active tunnels // Build set of all connection identifiers associated with active tunnels

View File

@@ -23,6 +23,7 @@ import com.google.common.collect.Sets;
import java.util.Collections; import java.util.Collections;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials; import org.apache.guacamole.net.auth.Credentials;
@@ -176,4 +177,22 @@ public class ModeledAuthenticatedUser extends RemoteAuthenticatedUser {
super.getEffectiveUserGroups()); super.getEffectiveUserGroups());
} }
/**
* Returns whether this user is effectively unrestricted by permissions,
* such as a system administrator or an internal user operating via a
* privileged UserContext. Permission inheritance via user groups is taken
* into account.
*
* @return
* true if this user should be unrestricted by permissions, false
* otherwise.
*
* @throws GuacamoleException
* If an error occurs while determining whether permission restrictions
* apply to the user.
*/
public boolean isPrivileged() throws GuacamoleException {
return getUser().isPrivileged();
}
} }

View File

@@ -48,6 +48,7 @@ import org.apache.guacamole.net.auth.ConnectionGroup;
import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.Directory;
import org.apache.guacamole.net.auth.SharingProfile; import org.apache.guacamole.net.auth.SharingProfile;
import org.apache.guacamole.net.auth.User; import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.auth.UserGroup; import org.apache.guacamole.net.auth.UserGroup;
/** /**
@@ -117,6 +118,12 @@ public class ModeledUserContext extends RestrictedObject
@Inject @Inject
private Provider<UserRecordSet> userRecordSetProvider; private Provider<UserRecordSet> userRecordSetProvider;
/**
* Provider for retrieving UserContext instances.
*/
@Inject
private Provider<ModeledUserContext> userContextProvider;
/** /**
* Mapper for user login records. * Mapper for user login records.
*/ */
@@ -124,7 +131,10 @@ public class ModeledUserContext extends RestrictedObject
private UserRecordMapper userRecordMapper; private UserRecordMapper userRecordMapper;
/** /**
* The activity record associated with this user's Guacamole session. * The activity record associated with this user's Guacamole session. If
* this user's session will not have an associated activity record, such as
* a temporary privileged session created via getPrivileged(), this will be
* null.
*/ */
private ActivityRecordModel userRecord; private ActivityRecordModel userRecord;
@@ -141,17 +151,42 @@ public class ModeledUserContext extends RestrictedObject
sharingProfileDirectory.init(currentUser); sharingProfileDirectory.init(currentUser);
activeConnectionDirectory.init(currentUser); activeConnectionDirectory.init(currentUser);
}
/**
* Records that the user associated with this UserContext has logged in,
* creating a partial activity record. The resulting activity record will
* contain a start date only, with the end date being automatically
* populated when this UserContext is invalidated. If this function is
* invoked more than once for the same UserContext, only the first
* invocation has any effect. If this function is never invoked, no
* activity record will be recorded, including when this UserContext is
* invalidated.
*/
public void recordUserLogin() {
// Do nothing if invoked multiple times
if (userRecord != null)
return;
// Create login record for user // Create login record for user
userRecord = new ActivityRecordModel(); userRecord = new ActivityRecordModel();
userRecord.setUsername(currentUser.getIdentifier()); userRecord.setUsername(getCurrentUser().getIdentifier());
userRecord.setStartDate(new Date()); userRecord.setStartDate(new Date());
userRecord.setRemoteHost(currentUser.getCredentials().getRemoteAddress()); userRecord.setRemoteHost(getCurrentUser().getCredentials().getRemoteAddress());
// Insert record representing login // Insert record representing login
userRecordMapper.insert(userRecord); userRecordMapper.insert(userRecord);
} }
@Override
public UserContext getPrivileged() {
ModeledUserContext context = userContextProvider.get();
context.init(new PrivilegedModeledAuthenticatedUser(getCurrentUser()));
return context;
}
@Override @Override
public User self() { public User self() {
return getCurrentUser().getUser(); return getCurrentUser().getUser();
@@ -253,9 +288,11 @@ public class ModeledUserContext extends RestrictedObject
@Override @Override
public void invalidate() { public void invalidate() {
// Record logout time // Record logout time only if login time was recorded
userRecord.setEndDate(new Date()); if (userRecord != null) {
userRecordMapper.update(userRecord); userRecord.setEndDate(new Date());
userRecordMapper.update(userRecord);
}
} }

View File

@@ -0,0 +1,50 @@
/*
* 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 org.apache.guacamole.GuacamoleException;
/**
* A ModeledAuthenticatedUser which is always privileged, returning true for
* every call to isPrivileged().
*/
public class PrivilegedModeledAuthenticatedUser extends ModeledAuthenticatedUser {
/**
* Creates a new PrivilegedModeledAuthenticatedUser which shares the same
* user identity as the given ModeledAuthenticatedUser. Regardless of the
* privileges explicitly granted to the given user, the resulting
* PrivilegedModeledAuthenticatedUser will always assert that it is
* privileged.
*
* @param authenticatedUser
* The ModeledAuthenticatedUser that declares the identity of the user
* in question.
*/
public PrivilegedModeledAuthenticatedUser(ModeledAuthenticatedUser authenticatedUser){
super(authenticatedUser, authenticatedUser.getModelAuthenticationProvider(), authenticatedUser.getUser());
}
@Override
public boolean isPrivileged() throws GuacamoleException {
return true;
}
}

View File

@@ -278,8 +278,8 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
// Verify new password does not violate defined policies (if specified) // Verify new password does not violate defined policies (if specified)
if (object.getPassword() != null) { if (object.getPassword() != null) {
// Enforce password age only for non-adminstrators // Enforce password age only for non-privileged users
if (!user.getUser().isAdministrator()) if (!user.isPrivileged())
passwordPolicyService.verifyPasswordAge(object); passwordPolicyService.verifyPasswordAge(object);
// Always verify password complexity // Always verify password complexity
@@ -626,8 +626,8 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
List<ActivityRecordModel> searchResults; List<ActivityRecordModel> searchResults;
// Bypass permission checks if the user is a system admin // Bypass permission checks if the user is privileged
if (user.getUser().isAdministrator()) if (user.isPrivileged())
searchResults = userRecordMapper.search(requiredContents, searchResults = userRecordMapper.search(requiredContents,
sortPredicates, limit); sortPredicates, limit);

View File

@@ -181,12 +181,13 @@ public class UserVerificationService {
// Update user object // Update user object
try { try {
context.getUserDirectory().update(self); context.getPrivileged().getUserDirectory().update(self);
} }
catch (GuacamoleSecurityException e) { catch (GuacamoleSecurityException e) {
logger.info("User \"{}\" cannot store their TOTP key as they " logger.info("User \"{}\" cannot store their TOTP key as they "
+ "lack permission to update their own account. TOTP " + "lack permission to update their own account and the "
+ "will be disabled for this user.", + "TOTP extension was unable to obtain privileged access. "
+ "TOTP will be disabled for this user.",
self.getIdentifier()); self.getIdentifier());
logger.debug("Permission denied to set TOTP key of user " logger.debug("Permission denied to set TOTP key of user "
+ "account.", e); + "account.", e);

View File

@@ -254,4 +254,16 @@ public abstract class AbstractUserContext implements UserContext {
public void invalidate() { public void invalidate() {
} }
/**
* {@inheritDoc}
*
* <p>This implementation simply returns <code>this</code>. Implementations
* that wish to provide additional privileges to extensions requesting
* privileged access should override this function.
*/
@Override
public UserContext getPrivileged() {
return this;
}
} }

View File

@@ -152,4 +152,9 @@ public class DelegatingUserContext implements UserContext {
userContext.invalidate(); userContext.invalidate();
} }
@Override
public UserContext getPrivileged() {
return userContext.getPrivileged();
}
} }

View File

@@ -262,4 +262,29 @@ public interface UserContext {
*/ */
void invalidate(); void invalidate();
/**
* Returns a user context which provides privileged access. Unlike the
* original user context, which is required to enforce its own permissions
* and act only within the rights of the associated user, the user context
* returned by this function MAY ignore the restrictions that otherwise
* limit the current user's access.
*
* <p>This function is intended to allow extensions which decorate other
* extensions to act independently of the restrictions that affect the
* current user. This function will only be invoked by extensions and
* WILL NOT be invoked directly by the web application. Implementations of
* this function MAY still enforce access restrictions, particularly if
* they do not want to grant full, unrestricted access to other extensions.
*
* <p>A default implementation which simply returns <code>this</code> is
* provided for compatibility with Apache Guacamole 1.1.0 and older.
*
* @return
* A user context instance which MAY ignore some or all restrictions
* which otherwise limit the current user's access.
*/
default UserContext getPrivileged() {
return this;
}
} }