diff --git a/api/src/org/labkey/api/security/ApiKeyManager.java b/api/src/org/labkey/api/security/ApiKeyManager.java index 69684001985..c38d24e166d 100644 --- a/api/src/org/labkey/api/security/ApiKeyManager.java +++ b/api/src/org/labkey/api/security/ApiKeyManager.java @@ -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; @@ -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 restrictionRole) { - return createKey(user, AppProps.getInstance().getApiKeyExpirationSeconds(), description); + return createKey(user, AppProps.getInstance().getApiKeyExpirationSeconds(), description, restrictionRole); } /** @@ -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 restrictionRole) { if (user.isGuest()) throw new IllegalStateException("Can't create an API key for a guest"); @@ -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); @@ -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 restrictionRole) { public User getUser() { @@ -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(); } diff --git a/api/src/org/labkey/api/security/AuthenticationProvider.java b/api/src/org/labkey/api/security/AuthenticationProvider.java index b293a5ddf02..64f8a294d86 100644 --- a/api/src/org/labkey/api/security/AuthenticationProvider.java +++ b/api/src/org/labkey/api/security/AuthenticationProvider.java @@ -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; @@ -306,6 +307,7 @@ class AuthenticationResponse private @Nullable ActionURL _redirectURL = null; private @NotNull Map _userAttributeMap = Collections.emptyMap(); // A case-insensitive map of attribute names and values associated with the user private @NotNull Map _authenticationProperties = Collections.emptyMap(); + private @Nullable Class _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 @@ -427,6 +429,23 @@ public AuthenticationResponse setAuthenticationProperties(Map ma return this; } + /** + * Get the restriction role class, if present + */ + public @Nullable Class getRestrictionRole() + { + return _restrictionRole; + } + + /** + * Set a role that restricts this user's permissions + */ + public AuthenticationResponse setRestrictionRole(Class clazz) + { + _restrictionRole = clazz; + return this; + } + public @Nullable String getSuccessDetails() { return _successDetails; diff --git a/api/src/org/labkey/api/security/ClonedUser.java b/api/src/org/labkey/api/security/ClonedUser.java index b5870e4bf36..591d3511db3 100644 --- a/api/src/org/labkey/api/security/ClonedUser.java +++ b/api/src/org/labkey/api/security/ClonedUser.java @@ -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 diff --git a/api/src/org/labkey/api/security/ElevatedUser.java b/api/src/org/labkey/api/security/ElevatedUser.java index c65bd223d75..88915061029 100644 --- a/api/src/org/labkey/api/security/ElevatedUser.java +++ b/api/src/org/labkey/api/security/ElevatedUser.java @@ -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; @@ -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 @@ -35,9 +37,26 @@ */ public class ElevatedUser extends ClonedUser { + private static class ElevatedUserContext extends WrappedPermissionsContext + { + private final Set _additionalRoles; + + private ElevatedUserContext(PermissionsContext delegate, Set additionalRoles) + { + super(delegate); + _additionalRoles = additionalRoles; + } + + @Override + public Stream getAssignedRoles(User user, SecurableResource resource) + { + return Streams.concat(_additionalRoles.stream(), super.getAssignedRoles(user, resource)); + } + } + private ElevatedUser(User user, Set rolesToAdd) { - super(user, new WrappedPermissionsContext(user.getPermissionsContext(), rolesToAdd)); + super(user, new ElevatedUserContext(user.getPermissionsContext(), rolesToAdd)); } private ElevatedUser(User user, PermissionsContext ctx) diff --git a/api/src/org/labkey/api/security/LimitedUser.java b/api/src/org/labkey/api/security/LimitedUser.java index cf618ce638c..0febeaf697e 100644 --- a/api/src/org/labkey/api/security/LimitedUser.java +++ b/api/src/org/labkey/api/security/LimitedUser.java @@ -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); diff --git a/api/src/org/labkey/api/security/RoleRestrictedUser.java b/api/src/org/labkey/api/security/RoleRestrictedUser.java new file mode 100644 index 00000000000..8c971abee00 --- /dev/null +++ b/api/src/org/labkey/api/security/RoleRestrictedUser.java @@ -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 restrictionRole) + { + super(ctx); + _role = RoleManager.getRole(restrictionRole); + } + + @Override + public Stream> filterPermissions(Stream> perms) + { + Set> rolePermissions = _role.getPermissions(); + return super.filterPermissions(perms).filter(rolePermissions::contains); + } + } + + public RoleRestrictedUser(User user, Class restrictionRole) + { + super(user, new RoleRestrictedPermissionsContext(user.getPermissionsContext(), restrictionRole)); + } +} diff --git a/api/src/org/labkey/api/security/RoleSet.java b/api/src/org/labkey/api/security/RoleSet.java index 58276a200ef..80fac9341b6 100644 --- a/api/src/org/labkey/api/security/RoleSet.java +++ b/api/src/org/labkey/api/security/RoleSet.java @@ -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())); diff --git a/api/src/org/labkey/api/security/SecurityManager.java b/api/src/org/labkey/api/security/SecurityManager.java index 84059487c84..6b7b22877f7 100644 --- a/api/src/org/labkey/api/security/SecurityManager.java +++ b/api/src/org/labkey/api/security/SecurityManager.java @@ -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"; @@ -517,8 +518,13 @@ public static User getSessionUser(HandshakeRequest request) if (null != factory) { - sessionUser.setImpersonationContext(factory.getImpersonationContext()); + sessionUser.setPermissionsContext(factory.getImpersonationContext()); } + + //noinspection unchecked + Class restrictionRole = (Class) session.getAttribute(RESTRICTION_ROLE_KEY); + if (restrictionRole != null) + sessionUser = new RoleRestrictedUser(sessionUser, restrictionRole); } } @@ -586,7 +592,7 @@ public static Pair 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); @@ -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; @@ -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 != ?"); @@ -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; diff --git a/api/src/org/labkey/api/security/User.java b/api/src/org/labkey/api/security/User.java index 32d8cfbd45d..394112bebd2 100644 --- a/api/src/org/labkey/api/security/User.java +++ b/api/src/org/labkey/api/security/User.java @@ -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; @@ -409,7 +409,7 @@ public void setLastActivity(Date lastActivity) _lastActivity = lastActivity; } - void setImpersonationContext(PermissionsContext permissionsContext) + void setPermissionsContext(PermissionsContext permissionsContext) { _permissionsContext = permissionsContext; } diff --git a/api/src/org/labkey/api/security/WrappedPermissionsContext.java b/api/src/org/labkey/api/security/WrappedPermissionsContext.java index a33506a12af..ea37a76f9c0 100644 --- a/api/src/org/labkey/api/security/WrappedPermissionsContext.java +++ b/api/src/org/labkey/api/security/WrappedPermissionsContext.java @@ -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; @@ -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 _additionalRoles; - public WrappedPermissionsContext(PermissionsContext delegate, Set additionalRoles) + public WrappedPermissionsContext(PermissionsContext delegate) { _delegate = delegate; - _additionalRoles = additionalRoles; - } - - public WrappedPermissionsContext(PermissionsContext delegate, Role additionalRole) - { - this(delegate, Set.of(additionalRole)); } @Override @@ -86,7 +77,7 @@ public PrincipalArray getGroups(User user) @Override public Stream getAssignedRoles(User user, SecurableResource resource) { - return Streams.concat(_additionalRoles.stream(), _delegate.getAssignedRoles(user, resource)); + return _delegate.getAssignedRoles(user, resource); } @Override diff --git a/api/src/org/labkey/api/security/impersonation/GroupImpersonationContextFactory.java b/api/src/org/labkey/api/security/impersonation/GroupImpersonationContextFactory.java index 47d4dd7e19e..20b0b086977 100644 --- a/api/src/org/labkey/api/security/impersonation/GroupImpersonationContextFactory.java +++ b/api/src/org/labkey/api/security/impersonation/GroupImpersonationContextFactory.java @@ -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; @@ -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); diff --git a/api/src/org/labkey/api/security/impersonation/ImpersonationContextFactory.java b/api/src/org/labkey/api/security/impersonation/ImpersonationContextFactory.java index 02a9159a24c..14bf54210bd 100644 --- a/api/src/org/labkey/api/security/impersonation/ImpersonationContextFactory.java +++ b/api/src/org/labkey/api/security/impersonation/ImpersonationContextFactory.java @@ -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(); diff --git a/api/src/org/labkey/api/security/impersonation/RoleImpersonationContextFactory.java b/api/src/org/labkey/api/security/impersonation/RoleImpersonationContextFactory.java index cc50b6b4945..7ef34f13dbf 100644 --- a/api/src/org/labkey/api/security/impersonation/RoleImpersonationContextFactory.java +++ b/api/src/org/labkey/api/security/impersonation/RoleImpersonationContextFactory.java @@ -24,7 +24,6 @@ import org.labkey.api.audit.AuditLogService; import org.labkey.api.data.Container; import org.labkey.api.data.ContainerManager; -import org.labkey.api.security.PermissionsContext; import org.labkey.api.security.PrincipalArray; import org.labkey.api.security.RoleSet; import org.labkey.api.security.SecurableResource; @@ -101,7 +100,7 @@ public RoleImpersonationContextFactory(@Nullable Container project, User adminUs } @Override - public PermissionsContext getImpersonationContext() + public AbstractImpersonationContext getImpersonationContext() { Container project = (null != _projectId ? ContainerManager.getForId(_projectId) : null); diff --git a/api/src/org/labkey/api/security/impersonation/UserImpersonationContextFactory.java b/api/src/org/labkey/api/security/impersonation/UserImpersonationContextFactory.java index 5b2b41fc610..d80e81d45ea 100644 --- a/api/src/org/labkey/api/security/impersonation/UserImpersonationContextFactory.java +++ b/api/src/org/labkey/api/security/impersonation/UserImpersonationContextFactory.java @@ -24,7 +24,6 @@ import org.labkey.api.data.Container; import org.labkey.api.data.ContainerManager; import org.labkey.api.security.GroupManager; -import org.labkey.api.security.PermissionsContext; import org.labkey.api.security.PrincipalArray; import org.labkey.api.security.SecurableResource; import org.labkey.api.security.SecurityManager; @@ -84,7 +83,7 @@ public User getAdminUser() } @Override - public PermissionsContext getImpersonationContext() + public AbstractImpersonationContext getImpersonationContext() { Container project = (null != _projectId ? ContainerManager.getForId(_projectId) : null); diff --git a/core/module.properties b/core/module.properties index 0be23cf2635..0731d77b149 100644 --- a/core/module.properties +++ b/core/module.properties @@ -1,6 +1,6 @@ Name: Core ModuleClass: org.labkey.core.CoreModule -SchemaVersion: 26.006 +SchemaVersion: 26.007 Label: Administration and Essential Services Description: The Core module provides central services such as login, \ security, administration, folder management, user management, \ diff --git a/core/package-lock.json b/core/package-lock.json index 3bab6585671..efff98f45bd 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -8,7 +8,7 @@ "name": "labkey-core", "version": "0.0.0", "dependencies": { - "@labkey/components": "7.45.2", + "@labkey/components": "7.45.3-fb-role-restricted-api-keys.2", "@labkey/themes": "1.9.4" }, "devDependencies": { @@ -3789,9 +3789,9 @@ } }, "node_modules/@labkey/components": { - "version": "7.45.2", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.45.2.tgz", - "integrity": "sha512-o/U7wotLaaoqXFhUFTxzgGeU1OVOwq16PBzCF/Ti/+Z8WW3OxAsAUCK/YqQMLkGu5sgH+ZOHoxa6tO3+NjipBw==", + "version": "7.45.3-fb-role-restricted-api-keys.2", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.45.3-fb-role-restricted-api-keys.2.tgz", + "integrity": "sha512-Y63kErfjETWYIm4aezV4wXsLR1OS3mozgTBl9uztyw8NE5sHtMu5udaaz583rqSmBBXjFizComwt4bgo1Q5h1A==", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/core/package.json b/core/package.json index 48eae14a889..b7877f2b704 100644 --- a/core/package.json +++ b/core/package.json @@ -20,7 +20,7 @@ "lint-branch-fix": "node lint.diff.mjs --currentBranch --fix" }, "dependencies": { - "@labkey/components": "7.45.2", + "@labkey/components": "7.45.3-fb-role-restricted-api-keys.2", "@labkey/themes": "1.9.4" }, "devDependencies": { diff --git a/core/resources/schemas/core.xml b/core/resources/schemas/core.xml index df2a6ef7db3..917d39fffb4 100644 --- a/core/resources/schemas/core.xml +++ b/core/resources/schemas/core.xml @@ -705,6 +705,7 @@ + diff --git a/core/resources/schemas/dbscripts/postgresql/core-26.006-26.007.sql b/core/resources/schemas/dbscripts/postgresql/core-26.006-26.007.sql new file mode 100644 index 00000000000..e761ac28830 --- /dev/null +++ b/core/resources/schemas/dbscripts/postgresql/core-26.006-26.007.sql @@ -0,0 +1 @@ +ALTER TABLE core.ApiKeys ADD RestrictionRole VARCHAR(256); \ No newline at end of file diff --git a/core/resources/schemas/dbscripts/sqlserver/core-26.006-26.007.sql b/core/resources/schemas/dbscripts/sqlserver/core-26.006-26.007.sql new file mode 100644 index 00000000000..1e2671c861b --- /dev/null +++ b/core/resources/schemas/dbscripts/sqlserver/core-26.006-26.007.sql @@ -0,0 +1 @@ +ALTER TABLE core.ApiKeys ADD RestrictionRole NVARCHAR(256); \ No newline at end of file diff --git a/core/src/org/labkey/core/login/DbLoginAuthenticationProvider.java b/core/src/org/labkey/core/login/DbLoginAuthenticationProvider.java index f9f9283cf48..2f02beab4c9 100644 --- a/core/src/org/labkey/core/login/DbLoginAuthenticationProvider.java +++ b/core/src/org/labkey/core/login/DbLoginAuthenticationProvider.java @@ -122,7 +122,8 @@ public boolean isFicamApproved() // API keys are exempt from secondary authentication, Issue 48764 ret = AuthenticationResponse.success(configuration, auth.getUser()).setSuccessDetails(UserManager.UserAuditEvent.API_KEY) .setRequireSecondary(false) - .setAuthenticationProperties(Map.of(ApiKeyManager.API_KEY_ROW_ID, auth.rowId())); + .setAuthenticationProperties(Map.of(ApiKeyManager.API_KEY_ROW_ID, auth.rowId())) + .setRestrictionRole(auth.restrictionRole()); // Update core.ApiKeys.LastUsed (throttled) API_KEY_LAST_USED_THROTTLE.execute(password); diff --git a/core/src/org/labkey/core/query/ApiKeysTableInfo.java b/core/src/org/labkey/core/query/ApiKeysTableInfo.java index bbdc93a622e..e47848d189f 100644 --- a/core/src/org/labkey/core/query/ApiKeysTableInfo.java +++ b/core/src/org/labkey/core/query/ApiKeysTableInfo.java @@ -44,6 +44,7 @@ public ApiKeysTableInfo(@NotNull CoreQuerySchema schema, boolean filterToCurrent addWrapColumn(getRealTable().getColumn("Expiration")); addWrapColumn(getRealTable().getColumn("LastUsed")); addWrapColumn(getRealTable().getColumn("Description")); + addWrapColumn(getRealTable().getColumn("RestrictionRole")); if (filterToCurrentUser) { diff --git a/core/src/org/labkey/core/security/SecurityController.java b/core/src/org/labkey/core/security/SecurityController.java index 023e4c30eab..0058cc966a4 100644 --- a/core/src/org/labkey/core/security/SecurityController.java +++ b/core/src/org/labkey/core/security/SecurityController.java @@ -47,6 +47,7 @@ import org.labkey.api.audit.provider.GroupAuditProvider; import org.labkey.api.audit.provider.GroupAuditProvider.GroupAuditEvent; import org.labkey.api.collections.CaseInsensitiveHashMap; +import org.labkey.api.collections.LabKeyCollectors; import org.labkey.api.compliance.ComplianceService; import org.labkey.api.data.ColumnInfo; import org.labkey.api.data.Container; @@ -115,9 +116,13 @@ import org.labkey.api.security.permissions.UpdateUserPermission; import org.labkey.api.security.permissions.UserManagementPermission; import org.labkey.api.security.roles.ApplicationAdminRole; +import org.labkey.api.security.roles.AuthorRole; +import org.labkey.api.security.roles.EditorRole; +import org.labkey.api.security.roles.EditorWithoutDeleteRole; import org.labkey.api.security.roles.FolderAdminRole; import org.labkey.api.security.roles.NoPermissionsRole; import org.labkey.api.security.roles.ProjectAdminRole; +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.security.roles.SiteAdminRole; @@ -164,6 +169,7 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.apache.commons.lang3.StringUtils.trimToNull; @@ -2288,10 +2294,38 @@ public void addNavTrail(NavTree root) } } + record RestrictionRole(Class roleClass) + { + String displayName() + { + return RoleManager.getRole(roleClass).getDisplayName(); + } + } + + private static final Map RESTRICTION_ROLE_MAP = Stream.of( + ReaderRole.class, + AuthorRole.class, + EditorWithoutDeleteRole.class, + EditorRole.class + ).collect(LabKeyCollectors.toLinkedMap(Class::getName, RestrictionRole::new)); + + @RequiresLogin + public static class GetApiKeyRolesAction extends ReadOnlyApiAction + { + @Override + public Object execute(Object o, BindException errors) throws Exception + { + return RESTRICTION_ROLE_MAP.entrySet().stream() + .map(e -> new JSONObject(Map.of("uniqueName", e.getKey(), "displayName", e.getValue().displayName()))) + .collect(LabKeyCollectors.toJSONArray()); + } + } + public static class CreateApiKeyForm { private String _type; private String _description; + private String _role; public String getType() { @@ -2314,6 +2348,16 @@ public void setDescription(String description) { _description = description; } + + public String getRole() + { + return _role; + } + + public void setRole(String role) + { + _role = role; + } } @RequiresLogin @@ -2330,7 +2374,8 @@ public Object execute(CreateApiKeyForm form, BindException errors) if (!AppProps.getInstance().isAllowApiKeys()) throw new NotFoundException("Creation of API keys is disabled"); - apiKey = ApiKeyManager.get().createKey(getUser(), form.getDescription()); + RestrictionRole rr = RESTRICTION_ROLE_MAP.get(form.getRole()); + apiKey = ApiKeyManager.get().createKey(getUser(), form.getDescription(), rr != null ? rr.roleClass() : null); break; case "session": if (!AppProps.getInstance().isAllowSessionKeys()) @@ -2365,29 +2410,29 @@ public void testActionPermissions() // @RequiresPermission(ReadPermission.class) assertForReadPermission(user, false, - new CompleteUserReadAction() + new CompleteUserReadAction() ); // @RequiresPermission(AdminPermission.class) assertForAdminPermission(user, - new PermissionsAction(), - new StandardDeleteGroupAction(), + new PermissionsAction(), + new StandardDeleteGroupAction(), controller.new GroupAction(), - new CompleteMemberAction(), - new CompleteUserAction(), - new GroupExportAction(), - new GroupPermissionAction(), - new UpdatePermissionsAction(), - new ShowRegistrationEmailAction(), - new GroupDiagramAction(), - new FolderAccessAction() + new CompleteMemberAction(), + new CompleteUserAction(), + new GroupExportAction(), + new GroupPermissionAction(), + new UpdatePermissionsAction(), + new ShowRegistrationEmailAction(), + new GroupDiagramAction(), + new FolderAccessAction() ); // @RequiresPermission(UserManagementPermission.class) assertForUserPermissions(user, controller.new AddUsersAction(), - new ShowResetEmailAction(), - new AdminResetPasswordAction() + new ShowResetEmailAction(), + new AdminResetPasswordAction() ); }