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.
context.init(user.getCurrentUser());
context.recordUserLogin();
return context;
}

View File

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

View File

@@ -81,7 +81,7 @@ public class ActiveConnectionService
Collection<String> identifiers) throws GuacamoleException {
String username = user.getIdentifier();
boolean isAdmin = user.getUser().isAdministrator();
boolean isPrivileged = user.isPrivileged();
Set<String> identifierSet = new HashSet<String>(identifiers);
// 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
// the user that started the connection OR the user is an admin
boolean hasPrivilegedAccess =
isAdmin || username.equals(record.getUsername());
isPrivileged || username.equals(record.getUsername());
// Add connection if within requested identifiers
if (identifierSet.contains(record.getUUID().toString())) {
@@ -211,7 +211,7 @@ public class ActiveConnectionService
ObjectPermissionSet permissionSet = getPermissionSet(user);
return user.getUser().isAdministrator()
return user.isPrivileged()
|| permissionSet.hasPermission(type, identifier);
}

View File

@@ -148,8 +148,8 @@ public abstract class ModeledChildDirectoryObjectService<InternalType extends Mo
protected boolean canUpdateModifiedParents(ModeledAuthenticatedUser user,
String identifier, ModelType model) throws GuacamoleException {
// If user is an administrator, no need to check
if (user.getUser().isAdministrator())
// If user is privileged, no need to check
if (user.isPrivileged())
return true;
// 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);
// Return whether permission is granted
return user.getUser().isAdministrator()
return user.isPrivileged()
|| permissionSet.hasPermission(type, identifier);
}
@@ -248,7 +248,7 @@ public abstract class ModeledDirectoryObjectService<InternalType extends Modeled
ExternalType object, ModelType model) throws GuacamoleException {
// Verify permission to create objects
if (!user.getUser().isAdministrator() && !hasCreatePermission(user))
if (!user.isPrivileged() && !hasCreatePermission(user))
throw new GuacamoleSecurityException("Permission denied.");
}
@@ -395,8 +395,8 @@ public abstract class ModeledDirectoryObjectService<InternalType extends Modeled
Collection<ModelType> objects;
// Bypass permission checks if the user is a system admin
if (user.getUser().isAdministrator())
// Bypass permission checks if the user is privileged
if (user.isPrivileged())
objects = getObjectMapper().select(identifiers);
// Otherwise only return explicitly readable identifiers
@@ -507,8 +507,8 @@ public abstract class ModeledDirectoryObjectService<InternalType extends Modeled
public Set<String> getIdentifiers(ModeledAuthenticatedUser user)
throws GuacamoleException {
// Bypass permission checks if the user is a system admin
if (user.getUser().isAdministrator())
// Bypass permission checks if the user is privileged
if (user.isPrivileged())
return getObjectMapper().selectIdentifiers();
// 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
* restricted by permissions, taking into account permission inheritance
* via user groups.
* Returns whether this entity 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 entity is a system administrator, false otherwise.
* true if this entity should be unrestricted by permissions, false
* otherwise.
*
* @throws GuacamoleException
* If an error occurs while determining the entity's system administrator
* status.
* If an error occurs while determining whether permission restrictions
* apply to the entity.
*/
public boolean isAdministrator() throws GuacamoleException {
public boolean isPrivileged() throws GuacamoleException {
SystemPermissionSet systemPermissionSet = getEffective().getSystemPermissions();
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)
throws GuacamoleException {
// System administrators may alter any relations
if (getCurrentUser().getUser().isAdministrator())
// Privileged users (such as system administrators) may alter any
// relations
if (getCurrentUser().isPrivileged())
return true;
// Non-admin users require UPDATE permission on the parent object ...
@@ -162,9 +163,9 @@ public abstract class RelatedObjectSet<ParentObjectType extends ModeledDirectory
@Override
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();
if (user.getUser().isAdministrator())
if (user.isPrivileged())
return getObjectRelationMapper().selectChildIdentifiers(parent.getModel());
// Otherwise only return explicitly readable identifiers

View File

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

View File

@@ -218,8 +218,8 @@ public class ConnectionGroupService extends ModeledChildDirectoryObjectService<M
String identifier)
throws GuacamoleException {
// Bypass permission checks if the user is a system admin
if (user.getUser().isAdministrator())
// Bypass permission checks if the user is privileged
if (user.isPrivileged())
return connectionGroupMapper.selectIdentifiersWithin(identifier);
// 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()))
return true;
// A system adminstrator can do anything
if (user.getUser().isAdministrator())
// Privileged users (such as system administrators) may do anything
if (user.isPrivileged())
return true;
// Can read permissions on target entity if explicit READ is granted

View File

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

View File

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

View File

@@ -628,8 +628,9 @@ public abstract class AbstractGuacamoleTunnelService implements GuacamoleTunnelS
if (records.isEmpty())
return Collections.<ActiveConnectionRecord>emptyList();
// A system administrator can view all connections; no need to filter
if (user.getUser().isAdministrator())
// Privileged users (such as system administrators) can view all
// connections; no need to filter
if (user.isPrivileged())
return records;
// 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.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials;
@@ -176,4 +177,22 @@ public class ModeledAuthenticatedUser extends RemoteAuthenticatedUser {
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.SharingProfile;
import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.auth.UserGroup;
/**
@@ -117,6 +118,12 @@ public class ModeledUserContext extends RestrictedObject
@Inject
private Provider<UserRecordSet> userRecordSetProvider;
/**
* Provider for retrieving UserContext instances.
*/
@Inject
private Provider<ModeledUserContext> userContextProvider;
/**
* Mapper for user login records.
*/
@@ -124,7 +131,10 @@ public class ModeledUserContext extends RestrictedObject
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;
@@ -141,17 +151,42 @@ public class ModeledUserContext extends RestrictedObject
sharingProfileDirectory.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
userRecord = new ActivityRecordModel();
userRecord.setUsername(currentUser.getIdentifier());
userRecord.setUsername(getCurrentUser().getIdentifier());
userRecord.setStartDate(new Date());
userRecord.setRemoteHost(currentUser.getCredentials().getRemoteAddress());
userRecord.setRemoteHost(getCurrentUser().getCredentials().getRemoteAddress());
// Insert record representing login
userRecordMapper.insert(userRecord);
}
@Override
public UserContext getPrivileged() {
ModeledUserContext context = userContextProvider.get();
context.init(new PrivilegedModeledAuthenticatedUser(getCurrentUser()));
return context;
}
@Override
public User self() {
return getCurrentUser().getUser();
@@ -253,9 +288,11 @@ public class ModeledUserContext extends RestrictedObject
@Override
public void invalidate() {
// Record logout time
userRecord.setEndDate(new Date());
userRecordMapper.update(userRecord);
// Record logout time only if login time was recorded
if (userRecord != null) {
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)
if (object.getPassword() != null) {
// Enforce password age only for non-adminstrators
if (!user.getUser().isAdministrator())
// Enforce password age only for non-privileged users
if (!user.isPrivileged())
passwordPolicyService.verifyPasswordAge(object);
// Always verify password complexity
@@ -626,8 +626,8 @@ public class UserService extends ModeledDirectoryObjectService<ModeledUser, User
List<ActivityRecordModel> searchResults;
// Bypass permission checks if the user is a system admin
if (user.getUser().isAdministrator())
// Bypass permission checks if the user is privileged
if (user.isPrivileged())
searchResults = userRecordMapper.search(requiredContents,
sortPredicates, limit);

View File

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

View File

@@ -254,4 +254,16 @@ public abstract class AbstractUserContext implements UserContext {
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();
}
@Override
public UserContext getPrivileged() {
return userContext.getPrivileged();
}
}

View File

@@ -262,4 +262,29 @@ public interface UserContext {
*/
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;
}
}