Skip to content
Open
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
105 changes: 98 additions & 7 deletions api/src/org/labkey/api/security/ApiKeyManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.jetbrains.annotations.Nullable;
import org.junit.Assert;
import org.junit.Test;
import org.labkey.api.data.ContainerManager;
import org.labkey.api.data.CoreSchema;
import org.labkey.api.data.DbScope;
import org.labkey.api.data.DbScope.Transaction;
Expand All @@ -39,6 +40,15 @@
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.permissions.AdminPermission;
import org.labkey.api.security.permissions.DeletePermission;
import org.labkey.api.security.permissions.InsertPermission;
import org.labkey.api.security.permissions.ReadPermission;
import org.labkey.api.security.permissions.UpdatePermission;
import org.labkey.api.security.roles.EditorRole;
import org.labkey.api.security.roles.ReaderRole;
import org.labkey.api.security.roles.Role;
import org.labkey.api.security.roles.RoleManager;
import org.labkey.api.settings.AppProps;
import org.labkey.api.settings.LenientStartupPropertyHandler;
import org.labkey.api.settings.StartupProperty;
Expand All @@ -60,6 +70,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;

import static org.labkey.api.util.IntegerUtils.asInteger;

Expand All @@ -85,9 +96,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 +108,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 +143,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,17 +209,35 @@ 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()
public @Nullable User getUser()
{
return UserManager.getUser(createdBy());
User user = UserManager.getUser(createdBy());
if (restrictionRole != null)
{
Role role = RoleManager.getRole(restrictionRole);
if (role == null)
{
LOG.error("API key for {} specifies a restriction role {} that was not found", user, restrictionRole.getName());
user = null;
}
else
{
user = new PermissionsRestrictedUser(user, role.getPermissions());
}
}
return user;
}
}

Expand All @@ -212,7 +256,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 Expand Up @@ -327,6 +371,53 @@ public void testTransaction()
ApiKeyManager.get().deleteKey(apikey);
assertNull(ApiKeyManager.get().authenticateFromApiKey(apikey));
}

private record UserAndKey(User user, String apiKey){}

@Test
public void testRoleRestrictions()
{
User admin = TestContext.get().getUser();
UserAndKey readerUAK = createApiKeyAndRetrieveUser(admin, ReaderRole.class);
User reader = readerUAK.user();
UserAndKey editorUAK = createApiKeyAndRetrieveUser(admin, EditorRole.class);
User editor = editorUAK.user();

ContainerManager.getAllChildren(ContainerManager.getRoot(), admin, AdminPermission.class).stream()
.limit(5)
.forEach(child -> {
assertTrue(child.hasPermission(admin, AdminPermission.class));
assertFalse(child.hasPermission(editor, AdminPermission.class));
assertFalse(child.hasPermission(reader, AdminPermission.class));
Stream.of(DeletePermission.class, UpdatePermission.class, InsertPermission.class)
.forEach(perm -> {
assertTrue(child.hasPermission(admin, perm));
assertTrue(child.hasPermission(editor, perm));
assertFalse(child.hasPermission(reader, perm));
});
assertTrue(child.hasPermission(admin, ReadPermission.class));
assertTrue(child.hasPermission(editor, ReadPermission.class));
assertTrue(child.hasPermission(reader, ReadPermission.class));
});

ApiKeyManager.get().deleteKey(readerUAK.apiKey());
ApiKeyManager.get().deleteKey(editorUAK.apiKey());
assertNull(ApiKeyManager.get().authenticateFromApiKey(readerUAK.apiKey()));
assertNull(ApiKeyManager.get().authenticateFromApiKey(editorUAK.apiKey()));
}

private UserAndKey createApiKeyAndRetrieveUser(User user, Class<? extends Role> restrictionRole)
{
String apiKey = ApiKeyManager.get().createKey(user, 10, "Created by ApiKeyManager.TestCase", restrictionRole);
ApiKeyAuthentication auth = ApiKeyManager.get().authenticateFromApiKey(apiKey);
assertNotNull(auth);
User restrictedUser = auth.getUser();
assertNotNull(restrictedUser);
assertEquals(user.getUserId(), restrictedUser.getUserId());
assertTrue(restrictedUser instanceof PermissionsRestrictedUser);

return new UserAndKey(restrictedUser, apiKey);
}
}

public static class ApiKeyMaintenanceTask implements MaintenanceTask
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
6 changes: 6 additions & 0 deletions api/src/org/labkey/api/security/PermissionsContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package org.labkey.api.security;

import com.google.common.collect.Streams;
import jakarta.servlet.http.HttpSession;
import org.jetbrains.annotations.Nullable;
import org.labkey.api.data.Container;
import org.labkey.api.data.ContainerManager;
Expand Down Expand Up @@ -87,4 +88,9 @@ default Stream<Class<? extends Permission>> filterPermissions(Stream<Class<? ext
{
return perms;
}

/** PermissionsContext can add session attributes alongside the user ID and attributes */
default void modifySession(HttpSession session)
{
}
}
46 changes: 46 additions & 0 deletions api/src/org/labkey/api/security/PermissionsRestrictedUser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.labkey.api.security;

import jakarta.servlet.http.HttpSession;
import org.jetbrains.annotations.NotNull;
import org.labkey.api.security.permissions.Permission;

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
* permissions. This is useful for creating read-only API keys, editor-only API keys, etc.
*/
public class PermissionsRestrictedUser extends ClonedUser
{
public static final String ALLOWED_PERMISSIONS_KEY = PermissionsRestrictedUser.class.getName() + "$AllowedPermissionsKey";

private static class RoleRestrictedPermissionsContext extends WrappedPermissionsContext
{
private final Set<Class<? extends Permission>> _allowedPermissions;

private RoleRestrictedPermissionsContext(PermissionsContext ctx, Set<Class<? extends Permission>> allowedPermissions)
{
super(ctx);
_allowedPermissions = allowedPermissions;
}

@Override
public Stream<Class<? extends Permission>> filterPermissions(Stream<Class<? extends Permission>> perms)
{
return super.filterPermissions(perms).filter(_allowedPermissions::contains);
}

@Override
public void modifySession(HttpSession session)
{
super.modifySession(session);
session.setAttribute(ALLOWED_PERMISSIONS_KEY, _allowedPermissions);
}
}

public PermissionsRestrictedUser(User user, @NotNull Set<Class<? extends Permission>> allowedPermissions)
{
super(user, new RoleRestrictedPermissionsContext(user.getPermissionsContext(), allowedPermissions));
}
}
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
15 changes: 11 additions & 4 deletions api/src/org/labkey/api/security/SecurityManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@
import java.util.stream.Stream;

import static org.labkey.api.action.SpringActionController.ERROR_MSG;
import static org.labkey.api.security.PermissionsRestrictedUser.ALLOWED_PERMISSIONS_KEY;
import static org.labkey.api.util.IntegerUtils.asInteger;

/**
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
Set<Class<? extends Permission>> allowedPermissions = (Set<Class<? extends Permission>>) session.getAttribute(ALLOWED_PERMISSIONS_KEY);
if (allowedPermissions != null)
sessionUser = new PermissionsRestrictedUser(sessionUser, allowedPermissions);
}
}

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,7 @@ 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());
user.getPermissionsContext().modifySession(newSession);
}

return newSession;
Expand Down Expand Up @@ -1121,7 +1128,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 +3350,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
Loading