Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions api/src/org/labkey/api/security/ApiKeyManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.labkey.api.query.FieldKey;
import org.labkey.api.security.UserManager.SessionHandler;
import org.labkey.api.security.ValidEmail.InvalidEmailException;
import org.labkey.api.security.roles.Role;
import org.labkey.api.settings.AppProps;
import org.labkey.api.settings.LenientStartupPropertyHandler;
import org.labkey.api.settings.StartupProperty;
Expand Down Expand Up @@ -85,9 +86,9 @@ private ApiKeyManager()
* @param user User to be associated with the new API key.
* @return An API key that expires after the admin-configured duration
*/
public @NotNull String createKey(@NotNull User user, @Nullable String description)
public @NotNull String createKey(@NotNull User user, @Nullable String description, @Nullable Class<? extends Role> restrictionRole)
{
return createKey(user, AppProps.getInstance().getApiKeyExpirationSeconds(), description);
return createKey(user, AppProps.getInstance().getApiKeyExpirationSeconds(), description, restrictionRole);
}

/**
Expand All @@ -97,6 +98,18 @@ private ApiKeyManager()
* @return An API key that expires after the specified number of seconds
*/
public @NotNull String createKey(@NotNull User user, int expirationSeconds, @Nullable String description)
{
return createKey(user, expirationSeconds, description, null);
}

/**
* Create an API key associated with a user and persist it in the database.
* @param user User to be associated with the new API key.
* @param expirationSeconds Number of seconds until expiration. -1 means no expiration.
* @param restrictionRole Role class that limits this API key's permissions. null means no restrictions.
* @return An API key that expires after the specified number of seconds
*/
public @NotNull String createKey(@NotNull User user, int expirationSeconds, @Nullable String description, @Nullable Class<? extends Role> restrictionRole)
{
if (user.isGuest())
throw new IllegalStateException("Can't create an API key for a guest");
Expand All @@ -120,6 +133,9 @@ private ApiKeyManager()
if (description != null)
map.put("Description", StringUtils.abbreviate(description.trim(), 256));

if (restrictionRole != null)
map.put("RestrictionRole", restrictionRole.getName());

try (Transaction t = CoreSchema.getInstance().getScope().beginTransaction(TRANSACTION_KIND))
{
Table.insert(user, CoreSchema.getInstance().getTableAPIKeys(), map);
Expand Down Expand Up @@ -183,13 +199,17 @@ public void updateLastUsed(String apikey)

try (Transaction t = scope.beginTransaction(TRANSACTION_KIND))
{
SQLFragment sql = new SQLFragment("UPDATE " + CoreSchema.getInstance().getTableAPIKeys() + " SET LastUsed = ? WHERE Crypt = ?", new Date(), crypt(apikey));
SQLFragment sql = new SQLFragment("UPDATE ")
.append(CoreSchema.getInstance().getTableAPIKeys())
.append(" SET LastUsed = ? WHERE Crypt = ?")
.add(new Date())
.add(crypt(apikey));
new SqlExecutor(scope).execute(sql);
t.commit();
}
}

public record ApiKeyAuthentication(int createdBy, int rowId)
public record ApiKeyAuthentication(int createdBy, int rowId, @Nullable Class<? extends Role> restrictionRole)
{
public User getUser()
{
Expand All @@ -212,7 +232,7 @@ public User getUser()

try (Transaction t = CoreSchema.getInstance().getScope().beginTransaction(TRANSACTION_KIND))
{
ret = new TableSelector(CoreSchema.getInstance().getTableAPIKeys(), Set.of("CreatedBy", "RowId"), filter, null).getObject(ApiKeyAuthentication.class);
ret = new TableSelector(CoreSchema.getInstance().getTableAPIKeys(), Set.of("CreatedBy", "RowId", "RestrictionRole"), filter, null).getObject(ApiKeyAuthentication.class);
t.commit();
}

Expand Down
19 changes: 19 additions & 0 deletions api/src/org/labkey/api/security/AuthenticationProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.labkey.api.security.AuthenticationManager.AuthenticationStatus;
import org.labkey.api.security.AuthenticationManager.AuthenticationValidator;
import org.labkey.api.security.ValidEmail.InvalidEmailException;
import org.labkey.api.security.roles.Role;
import org.labkey.api.settings.StandardStartupPropertyHandler;
import org.labkey.api.settings.StartupProperty;
import org.labkey.api.settings.StartupPropertyEntry;
Expand Down Expand Up @@ -306,6 +307,7 @@ class AuthenticationResponse
private @Nullable ActionURL _redirectURL = null;
private @NotNull Map<String, String> _userAttributeMap = Collections.emptyMap(); // A case-insensitive map of attribute names and values associated with the user
private @NotNull Map<String, Object> _authenticationProperties = Collections.emptyMap();
private @Nullable Class<? extends Role> _restrictionRole; // Limit user's permissions to those of this role
private boolean _requireSecondary = true; // Require secondary authentication
private boolean _reauth = false;
private @Nullable String _successDetails = null; // An optional string describing how successful authentication took place, which will
Expand Down Expand Up @@ -427,6 +429,23 @@ public AuthenticationResponse setAuthenticationProperties(Map<String, Object> ma
return this;
}

/**
* Get the restriction role class, if present
*/
public @Nullable Class<? extends Role> getRestrictionRole()
{
return _restrictionRole;
}

/**
* Set a role that restricts this user's permissions
*/
public AuthenticationResponse setRestrictionRole(Class<? extends Role> clazz)
{
_restrictionRole = clazz;
return this;
}

public @Nullable String getSuccessDetails()
{
return _successDetails;
Expand Down
2 changes: 1 addition & 1 deletion api/src/org/labkey/api/security/ClonedUser.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ protected ClonedUser(String email, int userId, String displayName, String firstN
setPhone(phone);
setLastActivity(lastActivity);

setImpersonationContext(ctx);
setPermissionsContext(ctx);
}

// Map a stream of role classes to a set of roles
Expand Down
21 changes: 20 additions & 1 deletion api/src/org/labkey/api/security/ElevatedUser.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package org.labkey.api.security;

import com.google.common.collect.Streams;
import org.labkey.api.audit.permissions.CanSeeAuditLogPermission;
import org.labkey.api.data.Container;
import org.labkey.api.security.permissions.Permission;
Expand All @@ -26,6 +27,7 @@
import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* A wrapped user that possesses all the security properties (groups, roles, impersonation status, etc.) of the
Expand All @@ -35,9 +37,26 @@
*/
public class ElevatedUser extends ClonedUser
{
private static class ElevatedUserContext extends WrappedPermissionsContext
{
private final Set<Role> _additionalRoles;

private ElevatedUserContext(PermissionsContext delegate, Set<Role> additionalRoles)
{
super(delegate);
_additionalRoles = additionalRoles;
}

@Override
public Stream<Role> getAssignedRoles(User user, SecurableResource resource)
{
return Streams.concat(_additionalRoles.stream(), super.getAssignedRoles(user, resource));
}
}

private ElevatedUser(User user, Set<Role> rolesToAdd)
{
super(user, new WrappedPermissionsContext(user.getPermissionsContext(), rolesToAdd));
super(user, new ElevatedUserContext(user.getPermissionsContext(), rolesToAdd));
}

private ElevatedUser(User user, PermissionsContext ctx)
Expand Down
2 changes: 1 addition & 1 deletion api/src/org/labkey/api/security/LimitedUser.java
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ private LimitedUser(
@JsonProperty("_lastLogin") Date lastLogin,
@JsonProperty("_phone") String phone,
@JsonProperty("_lastActivity") Date lastActivity,
@JsonProperty("_impersonationContext") PermissionsContext ctx
@JsonProperty("_permissionsContext") PermissionsContext ctx
)
{
super(name, userId, displayName, firstName, lastName, active, lastLogin, phone, lastActivity, ctx);
Expand Down
38 changes: 38 additions & 0 deletions api/src/org/labkey/api/security/RoleRestrictedUser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.labkey.api.security;

import org.labkey.api.security.permissions.Permission;
import org.labkey.api.security.roles.Role;
import org.labkey.api.security.roles.RoleManager;

import java.util.Set;
import java.util.stream.Stream;

/**
* A cloned user that has the user's permissions in all the user's containers, but always restricted to the supplied
* role. This is useful for creating read-only API keys, editor-only API keys, etc.
*/
public class RoleRestrictedUser extends ClonedUser
{
private static class RoleRestrictedPermissionsContext extends WrappedPermissionsContext
{
private final Role _role;

private RoleRestrictedPermissionsContext(PermissionsContext ctx, Class<? extends Role> restrictionRole)
{
super(ctx);
_role = RoleManager.getRole(restrictionRole);
}

@Override
public Stream<Class<? extends Permission>> filterPermissions(Stream<Class<? extends Permission>> perms)
{
Set<Class<? extends Permission>> rolePermissions = _role.getPermissions();
return super.filterPermissions(perms).filter(rolePermissions::contains);
}
}

public RoleRestrictedUser(User user, Class<? extends Role> restrictionRole)
{
super(user, new RoleRestrictedPermissionsContext(user.getPermissionsContext(), restrictionRole));
}
}
2 changes: 1 addition & 1 deletion api/src/org/labkey/api/security/RoleSet.java
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ private void testImpersonateRoles(User adminUser, @Nullable Container project, C
assertEquals(roles, reconstitutedRoleSet.getRoles());

RoleImpersonationContextFactory factory = new RoleImpersonationContextFactory(project, adminUser, roles, Collections.emptySet(), null);
impersonatingUser.setImpersonationContext(factory.getImpersonationContext());
impersonatingUser.setPermissionsContext(factory.getImpersonationContext());

if (null == project)
assertEquals(roles, impersonatingUser.getSiteRoles(ContainerManager.getRoot()).collect(Collectors.toSet()));
Expand Down
16 changes: 12 additions & 4 deletions api/src/org/labkey/api/security/SecurityManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ public class SecurityManager

public static final String USER_ID_KEY = User.class.getName() + "$userId";
private static final String IMPERSONATION_CONTEXT_FACTORY_KEY = User.class.getName() + "$ImpersonationContextFactoryKey";
private static final String RESTRICTION_ROLE_KEY = RoleRestrictedUser.class.getName() + "$RestrictionRoleKey";
private static final String AUTHENTICATION_VALIDATORS_KEY = SecurityManager.class.getName() + "$AuthenticationValidators";
private static final String AUTHENTICATION_METHOD = "SecurityManager.authenticationMethod";

Expand Down Expand Up @@ -517,8 +518,13 @@ public static User getSessionUser(HandshakeRequest request)

if (null != factory)
{
sessionUser.setImpersonationContext(factory.getImpersonationContext());
sessionUser.setPermissionsContext(factory.getImpersonationContext());
}

//noinspection unchecked
Class<? extends Role> restrictionRole = (Class<? extends Role>) session.getAttribute(RESTRICTION_ROLE_KEY);
if (restrictionRole != null)
sessionUser = new RoleRestrictedUser(sessionUser, restrictionRole);
}
}

Expand Down Expand Up @@ -586,7 +592,7 @@ public static Pair<User, HttpServletRequest> attemptAuthentication(HttpServletRe

if (!sessionUser.isImpersonated() && "true".equalsIgnoreCase(request.getHeader("LabKey-Disallow-Global-Roles")))
{
sessionUser.setImpersonationContext(DisallowPrivilegedRolesContext.get());
sessionUser.setPermissionsContext(DisallowPrivilegedRolesContext.get());
}

HttpSession session = request.getSession(false);
Expand Down Expand Up @@ -816,6 +822,8 @@ public static HttpSession setAuthenticatedUser(HttpServletRequest request, @Null
newSession.setAttribute(PRIMARY_AUTHENTICATION_CONFIGURATION, configuration.getRowId());
newSession.setAttribute(USER_ATTRIBUTES_KEY, response.getUserAttributeMap());
newSession.setAttribute(AUTHENTICATION_PROPERTIES, response.getAuthenticationProperties());
if (response.getRestrictionRole() != null)
newSession.setAttribute(RESTRICTION_ROLE_KEY, response.getRestrictionRole());
}

return newSession;
Expand Down Expand Up @@ -1121,7 +1129,7 @@ else if (email.getEmailAddress().indexOf("@") > 0)

// Issue 25813: if two users are being inserted at the same time through the addUser method above, we can get deadlock on SQLServer so we
// actively lock when doing this select to prevent it from promoting a lock up the chain.
SQLFragment select = new SQLFragment("SELECT UserId FROM " + core.getTableInfoUsersData());
SQLFragment select = new SQLFragment("SELECT UserId FROM ").append(core.getTableInfoUsersData());
if (core.getSchema().getScope().getSqlDialect().isSqlServer())
select.append(" WITH (UPDLOCK)");
select.append(" WHERE DisplayName = ? AND UserId != ?");
Expand Down Expand Up @@ -3343,7 +3351,7 @@ public void testReadOnlyImpersonate() throws InvalidEmailException, UserManageme
final User testUser = TestContext.get().getUser();
// this user is subsetted to only permit read permissions (see AllowedForReadOnlyUser)
User user = addUser(new ValidEmail("impersonate@test.net"), null, false).getUser();
user.setImpersonationContext(new ReadOnlyPermissionsContext());
user.setPermissionsContext(new ReadOnlyPermissionsContext());

Container testFolder = null;

Expand Down
4 changes: 2 additions & 2 deletions api/src/org/labkey/api/security/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
import org.labkey.api.security.permissions.AnalystPermission;
import org.labkey.api.security.permissions.ApplicationAdminPermission;
import org.labkey.api.security.permissions.BrowserDeveloperPermission;
import org.labkey.api.security.permissions.ImpersonatePermission;
import org.labkey.api.security.permissions.DeletePermission;
import org.labkey.api.security.permissions.ImpersonatePermission;
import org.labkey.api.security.permissions.ImpersonatePrivilegedSiteRolesPermission;
import org.labkey.api.security.permissions.InsertPermission;
import org.labkey.api.security.permissions.Permission;
Expand Down Expand Up @@ -409,7 +409,7 @@ public void setLastActivity(Date lastActivity)
_lastActivity = lastActivity;
}

void setImpersonationContext(PermissionsContext permissionsContext)
void setPermissionsContext(PermissionsContext permissionsContext)
{
_permissionsContext = permissionsContext;
}
Expand Down
17 changes: 4 additions & 13 deletions api/src/org/labkey/api/security/WrappedPermissionsContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package org.labkey.api.security;

import com.google.common.collect.Streams;
import org.jetbrains.annotations.Nullable;
import org.labkey.api.data.Container;
import org.labkey.api.security.impersonation.ImpersonationContextFactory;
Expand All @@ -24,26 +23,18 @@
import org.labkey.api.view.ActionURL;
import org.labkey.api.view.NavTree;

import java.util.Set;
import java.util.stream.Stream;

/**
* Do not use this class directly; use ElevatedUser instead.
* See subclasses ElevatedUser and RoleRestrictedUser
*/
public class WrappedPermissionsContext implements PermissionsContext
abstract class WrappedPermissionsContext implements PermissionsContext
{
private final PermissionsContext _delegate;
private final Set<Role> _additionalRoles;

public WrappedPermissionsContext(PermissionsContext delegate, Set<Role> additionalRoles)
public WrappedPermissionsContext(PermissionsContext delegate)
{
_delegate = delegate;
_additionalRoles = additionalRoles;
}

public WrappedPermissionsContext(PermissionsContext delegate, Role additionalRole)
{
this(delegate, Set.of(additionalRole));
}

@Override
Expand Down Expand Up @@ -86,7 +77,7 @@ public PrincipalArray getGroups(User user)
@Override
public Stream<Role> getAssignedRoles(User user, SecurableResource resource)
{
return Streams.concat(_additionalRoles.stream(), _delegate.getAssignedRoles(user, resource));
return _delegate.getAssignedRoles(user, resource);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import org.labkey.api.data.ContainerManager;
import org.labkey.api.security.Group;
import org.labkey.api.security.GroupMembershipCache;
import org.labkey.api.security.PermissionsContext;
import org.labkey.api.security.PrincipalArray;
import org.labkey.api.security.SecurableResource;
import org.labkey.api.security.SecurityManager;
Expand Down Expand Up @@ -77,7 +76,7 @@ public GroupImpersonationContextFactory(@Nullable Container project, User adminU
}

@Override
public PermissionsContext getImpersonationContext()
public AbstractImpersonationContext getImpersonationContext()
{
Container project = (null != _projectId ? ContainerManager.getForId(_projectId) : null);
Group group = SecurityManager.getGroup(_groupId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,16 @@
package org.labkey.api.security.impersonation;

import jakarta.servlet.http.HttpServletRequest;
import org.labkey.api.security.PermissionsContext;
import org.labkey.api.security.User;
import org.labkey.api.view.ViewContext;

import java.io.Serializable;

// We store implementations of this interface in session and construct ImpersonationContexts at each request. This
// protects us somewhat from user, container, etc. objects getting out-of-date.
// We store implementations of this interface in session and construct AbstractImpersonationContexts at each request.
// This protects us somewhat from user, container, etc. objects getting out-of-date.
public interface ImpersonationContextFactory extends Serializable
{
PermissionsContext getImpersonationContext();
AbstractImpersonationContext getImpersonationContext();
void startImpersonating(ViewContext context);
void stopImpersonating(HttpServletRequest request);
User getAdminUser();
Expand Down
Loading