From 0b0d2c034b11ccad66390e781fa3f743e3ca36ff Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 30 Jun 2026 09:49:44 -0700 Subject: [PATCH 1/7] Role-restricted API keys --- .../labkey/api/security/ApiKeyManager.java | 9 ++++- .../api/security/AuthenticationProvider.java | 19 ++++++++++ .../org/labkey/api/security/ClonedUser.java | 2 +- .../org/labkey/api/security/ElevatedUser.java | 21 +++++++++- .../org/labkey/api/security/LimitedUser.java | 2 +- .../api/security/RoleRestrictedUser.java | 38 +++++++++++++++++++ api/src/org/labkey/api/security/RoleSet.java | 2 +- .../labkey/api/security/SecurityManager.java | 16 ++++++-- api/src/org/labkey/api/security/User.java | 4 +- .../security/WrappedPermissionsContext.java | 17 ++------- .../GroupImpersonationContextFactory.java | 3 +- .../ImpersonationContextFactory.java | 7 ++-- .../RoleImpersonationContextFactory.java | 3 +- .../UserImpersonationContextFactory.java | 3 +- .../login/DbLoginAuthenticationProvider.java | 7 +++- 15 files changed, 116 insertions(+), 37 deletions(-) create mode 100644 api/src/org/labkey/api/security/RoleRestrictedUser.java diff --git a/api/src/org/labkey/api/security/ApiKeyManager.java b/api/src/org/labkey/api/security/ApiKeyManager.java index 69684001985..70998d8b24d 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; @@ -183,13 +184,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 roleRestrictionClass) { public User getUser() { diff --git a/api/src/org/labkey/api/security/AuthenticationProvider.java b/api/src/org/labkey/api/security/AuthenticationProvider.java index b293a5ddf02..b4c6c20a4ac 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 _roleRestriction; // 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 role restriction class, if present + */ + public @Nullable Class getRoleRestriction() + { + return _roleRestriction; + } + + /** + * Set a role that restricts this user's permissions + */ + public AuthenticationResponse setRoleRestriction(Class clazz) + { + _roleRestriction = 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..010d4003865 --- /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 that 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 clazz) + { + super(ctx); + _role = RoleManager.getRole(clazz); + } + + @Override + public Stream> filterPermissions(Stream> perms) + { + Set> rolePermissions = _role.getPermissions(); + return super.filterPermissions(perms).filter(rolePermissions::contains); + } + } + + public RoleRestrictedUser(User user, Class clazz) + { + super(user, new RoleRestrictedPermissionsContext(user.getPermissionsContext(), clazz)); + } +} 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..24ff3dcd6f1 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 ROLE_RESTRICTION_KEY = RoleRestrictedUser.class.getName() + "$RoleRestrictionKey"; 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 roleRestrictionClass = (Class) session.getAttribute(ROLE_RESTRICTION_KEY); + if (roleRestrictionClass != null) + sessionUser = new RoleRestrictedUser(sessionUser, roleRestrictionClass); } } @@ -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.getRoleRestriction() != null) + newSession.setAttribute(ROLE_RESTRICTION_KEY, response.getRoleRestriction()); } 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/src/org/labkey/core/login/DbLoginAuthenticationProvider.java b/core/src/org/labkey/core/login/DbLoginAuthenticationProvider.java index f9f9283cf48..68fada54bf6 100644 --- a/core/src/org/labkey/core/login/DbLoginAuthenticationProvider.java +++ b/core/src/org/labkey/core/login/DbLoginAuthenticationProvider.java @@ -36,6 +36,7 @@ import org.labkey.api.security.UserManager; import org.labkey.api.security.ValidEmail; 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.StandardStartupPropertyHandler; import org.labkey.api.settings.StartupPropertyEntry; @@ -122,7 +123,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())) + .setRoleRestriction(auth.roleRestrictionClass()); // Update core.ApiKeys.LastUsed (throttled) API_KEY_LAST_USED_THROTTLE.execute(password); @@ -184,7 +186,8 @@ public boolean isFicamApproved() } } - return AuthenticationResponse.success(configuration, user); + Class roleRestriction = null; // TODO: Temp for testing - set a breakpoint and assign a value to test + return AuthenticationResponse.success(configuration, user).setRoleRestriction(roleRestriction); } } From 0186b8c41c2b5c2d2123efbb4ceb6d8b901620e9 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 30 Jun 2026 16:18:23 -0700 Subject: [PATCH 2/7] Use custom API action to convey a relevant subset of roles --- .../labkey/api/security/ApiKeyManager.java | 19 +++++++- core/package-lock.json | 8 ++-- core/package.json | 2 +- .../core/security/SecurityController.java | 47 ++++++++++++++++++- 4 files changed, 68 insertions(+), 8 deletions(-) diff --git a/api/src/org/labkey/api/security/ApiKeyManager.java b/api/src/org/labkey/api/security/ApiKeyManager.java index 70998d8b24d..b1951d100a7 100644 --- a/api/src/org/labkey/api/security/ApiKeyManager.java +++ b/api/src/org/labkey/api/security/ApiKeyManager.java @@ -86,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 restrictedRole) { - return createKey(user, AppProps.getInstance().getApiKeyExpirationSeconds(), description); + return createKey(user, AppProps.getInstance().getApiKeyExpirationSeconds(), description, restrictedRole); } /** @@ -98,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 restrictedRole 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 restrictedRole) { if (user.isGuest()) throw new IllegalStateException("Can't create an API key for a guest"); @@ -121,6 +133,9 @@ private ApiKeyManager() if (description != null) map.put("Description", StringUtils.abbreviate(description.trim(), 256)); + if (restrictedRole != null) + map.put("RestrictedRole", restrictedRole.getName()); + try (Transaction t = CoreSchema.getInstance().getScope().beginTransaction(TRANSACTION_KIND)) { Table.insert(user, CoreSchema.getInstance().getTableAPIKeys(), map); diff --git a/core/package-lock.json b/core/package-lock.json index 3bab6585671..2fe922e0d6b 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.1", "@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.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.45.3-fb-role-restricted-api-keys.1.tgz", + "integrity": "sha512-ECxFtC8Jcf/XbzD0PmH4FqRGaWKDsTdcWx6Ey3Ni7h1PmdQ+blkTmwzQdp7PgN6ZpNGXx8kiiExlJszHsvN6hQ==", "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..f1a25573eeb 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.1", "@labkey/themes": "1.9.4" }, "devDependencies": { diff --git a/core/src/org/labkey/core/security/SecurityController.java b/core/src/org/labkey/core/security/SecurityController.java index 023e4c30eab..68a7d7c38c1 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 RestrictedRole(Class roleClass) + { + String displayName() + { + return RoleManager.getRole(roleClass).getDisplayName(); + } + } + + private static final Map RESTRICTED_ROLE_MAP = Stream.of( + ReaderRole.class, + AuthorRole.class, + EditorWithoutDeleteRole.class, + EditorRole.class + ).collect(LabKeyCollectors.toLinkedMap(Class::getName, RestrictedRole::new)); + + @RequiresLogin + public static class GetApiKeyRolesAction extends ReadOnlyApiAction + { + @Override + public Object execute(Object o, BindException errors) throws Exception + { + return RESTRICTED_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()); + RestrictedRole rr = RESTRICTED_ROLE_MAP.get(form.getRole()); + apiKey = ApiKeyManager.get().createKey(getUser(), form.getDescription(), rr != null ? rr.roleClass() : null); break; case "session": if (!AppProps.getInstance().isAllowSessionKeys()) From c326bad9f33ee4a6e74d72ce78f756923973689d Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Tue, 30 Jun 2026 16:49:13 -0700 Subject: [PATCH 3/7] Bump labkey-ui-components version --- core/package-lock.json | 8 ++++---- core/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/package-lock.json b/core/package-lock.json index 2fe922e0d6b..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.3-fb-role-restricted-api-keys.1", + "@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.3-fb-role-restricted-api-keys.1", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/components/-/@labkey/components-7.45.3-fb-role-restricted-api-keys.1.tgz", - "integrity": "sha512-ECxFtC8Jcf/XbzD0PmH4FqRGaWKDsTdcWx6Ey3Ni7h1PmdQ+blkTmwzQdp7PgN6ZpNGXx8kiiExlJszHsvN6hQ==", + "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 f1a25573eeb..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.3-fb-role-restricted-api-keys.1", + "@labkey/components": "7.45.3-fb-role-restricted-api-keys.2", "@labkey/themes": "1.9.4" }, "devDependencies": { From 93368a8b19ff99f2126196dd247df09d2cbe3745 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Wed, 1 Jul 2026 09:53:37 -0700 Subject: [PATCH 4/7] Terminology: restriction role --- .../labkey/api/security/ApiKeyManager.java | 14 ++++---- .../api/security/AuthenticationProvider.java | 12 +++---- .../api/security/RoleRestrictedUser.java | 10 +++--- .../labkey/api/security/SecurityManager.java | 12 +++---- .../login/DbLoginAuthenticationProvider.java | 6 ++-- .../core/security/SecurityController.java | 36 +++++++++---------- 6 files changed, 44 insertions(+), 46 deletions(-) diff --git a/api/src/org/labkey/api/security/ApiKeyManager.java b/api/src/org/labkey/api/security/ApiKeyManager.java index b1951d100a7..1a78b353554 100644 --- a/api/src/org/labkey/api/security/ApiKeyManager.java +++ b/api/src/org/labkey/api/security/ApiKeyManager.java @@ -86,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, @Nullable Class restrictedRole) + public @NotNull String createKey(@NotNull User user, @Nullable String description, @Nullable Class restrictionRole) { - return createKey(user, AppProps.getInstance().getApiKeyExpirationSeconds(), description, restrictedRole); + return createKey(user, AppProps.getInstance().getApiKeyExpirationSeconds(), description, restrictionRole); } /** @@ -106,10 +106,10 @@ private ApiKeyManager() * 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 restrictedRole Role class that limits this API key's permissions. null means no restrictions. + * @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 restrictedRole) + 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"); @@ -133,8 +133,8 @@ private ApiKeyManager() if (description != null) map.put("Description", StringUtils.abbreviate(description.trim(), 256)); - if (restrictedRole != null) - map.put("RestrictedRole", restrictedRole.getName()); + if (restrictionRole != null) + map.put("RestrictionRole", restrictionRole.getName()); try (Transaction t = CoreSchema.getInstance().getScope().beginTransaction(TRANSACTION_KIND)) { @@ -209,7 +209,7 @@ public void updateLastUsed(String apikey) } } - public record ApiKeyAuthentication(int createdBy, int rowId, @Nullable Class roleRestrictionClass) + public record ApiKeyAuthentication(int createdBy, int rowId, @Nullable Class restrictionRole) { public User getUser() { diff --git a/api/src/org/labkey/api/security/AuthenticationProvider.java b/api/src/org/labkey/api/security/AuthenticationProvider.java index b4c6c20a4ac..64f8a294d86 100644 --- a/api/src/org/labkey/api/security/AuthenticationProvider.java +++ b/api/src/org/labkey/api/security/AuthenticationProvider.java @@ -307,7 +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 _roleRestriction; // Limit user's permissions to those of this role + 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 @@ -430,19 +430,19 @@ public AuthenticationResponse setAuthenticationProperties(Map ma } /** - * Get the role restriction class, if present + * Get the restriction role class, if present */ - public @Nullable Class getRoleRestriction() + public @Nullable Class getRestrictionRole() { - return _roleRestriction; + return _restrictionRole; } /** * Set a role that restricts this user's permissions */ - public AuthenticationResponse setRoleRestriction(Class clazz) + public AuthenticationResponse setRestrictionRole(Class clazz) { - _roleRestriction = clazz; + _restrictionRole = clazz; return this; } diff --git a/api/src/org/labkey/api/security/RoleRestrictedUser.java b/api/src/org/labkey/api/security/RoleRestrictedUser.java index 010d4003865..8c971abee00 100644 --- a/api/src/org/labkey/api/security/RoleRestrictedUser.java +++ b/api/src/org/labkey/api/security/RoleRestrictedUser.java @@ -8,7 +8,7 @@ import java.util.stream.Stream; /** - * A cloned user that has the user's permissions in all that user's containers, but always restricted to the supplied + * 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 @@ -17,10 +17,10 @@ private static class RoleRestrictedPermissionsContext extends WrappedPermissions { private final Role _role; - private RoleRestrictedPermissionsContext(PermissionsContext ctx, Class clazz) + private RoleRestrictedPermissionsContext(PermissionsContext ctx, Class restrictionRole) { super(ctx); - _role = RoleManager.getRole(clazz); + _role = RoleManager.getRole(restrictionRole); } @Override @@ -31,8 +31,8 @@ public Stream> filterPermissions(Stream clazz) + public RoleRestrictedUser(User user, Class restrictionRole) { - super(user, new RoleRestrictedPermissionsContext(user.getPermissionsContext(), clazz)); + super(user, new RoleRestrictedPermissionsContext(user.getPermissionsContext(), restrictionRole)); } } diff --git a/api/src/org/labkey/api/security/SecurityManager.java b/api/src/org/labkey/api/security/SecurityManager.java index 24ff3dcd6f1..6b7b22877f7 100644 --- a/api/src/org/labkey/api/security/SecurityManager.java +++ b/api/src/org/labkey/api/security/SecurityManager.java @@ -177,7 +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 ROLE_RESTRICTION_KEY = RoleRestrictedUser.class.getName() + "$RoleRestrictionKey"; + 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"; @@ -522,9 +522,9 @@ public static User getSessionUser(HandshakeRequest request) } //noinspection unchecked - Class roleRestrictionClass = (Class) session.getAttribute(ROLE_RESTRICTION_KEY); - if (roleRestrictionClass != null) - sessionUser = new RoleRestrictedUser(sessionUser, roleRestrictionClass); + Class restrictionRole = (Class) session.getAttribute(RESTRICTION_ROLE_KEY); + if (restrictionRole != null) + sessionUser = new RoleRestrictedUser(sessionUser, restrictionRole); } } @@ -822,8 +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.getRoleRestriction() != null) - newSession.setAttribute(ROLE_RESTRICTION_KEY, response.getRoleRestriction()); + if (response.getRestrictionRole() != null) + newSession.setAttribute(RESTRICTION_ROLE_KEY, response.getRestrictionRole()); } return newSession; diff --git a/core/src/org/labkey/core/login/DbLoginAuthenticationProvider.java b/core/src/org/labkey/core/login/DbLoginAuthenticationProvider.java index 68fada54bf6..2f02beab4c9 100644 --- a/core/src/org/labkey/core/login/DbLoginAuthenticationProvider.java +++ b/core/src/org/labkey/core/login/DbLoginAuthenticationProvider.java @@ -36,7 +36,6 @@ import org.labkey.api.security.UserManager; import org.labkey.api.security.ValidEmail; 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.StandardStartupPropertyHandler; import org.labkey.api.settings.StartupPropertyEntry; @@ -124,7 +123,7 @@ public boolean isFicamApproved() ret = AuthenticationResponse.success(configuration, auth.getUser()).setSuccessDetails(UserManager.UserAuditEvent.API_KEY) .setRequireSecondary(false) .setAuthenticationProperties(Map.of(ApiKeyManager.API_KEY_ROW_ID, auth.rowId())) - .setRoleRestriction(auth.roleRestrictionClass()); + .setRestrictionRole(auth.restrictionRole()); // Update core.ApiKeys.LastUsed (throttled) API_KEY_LAST_USED_THROTTLE.execute(password); @@ -186,8 +185,7 @@ public boolean isFicamApproved() } } - Class roleRestriction = null; // TODO: Temp for testing - set a breakpoint and assign a value to test - return AuthenticationResponse.success(configuration, user).setRoleRestriction(roleRestriction); + return AuthenticationResponse.success(configuration, user); } } diff --git a/core/src/org/labkey/core/security/SecurityController.java b/core/src/org/labkey/core/security/SecurityController.java index 68a7d7c38c1..0058cc966a4 100644 --- a/core/src/org/labkey/core/security/SecurityController.java +++ b/core/src/org/labkey/core/security/SecurityController.java @@ -2294,7 +2294,7 @@ public void addNavTrail(NavTree root) } } - record RestrictedRole(Class roleClass) + record RestrictionRole(Class roleClass) { String displayName() { @@ -2302,12 +2302,12 @@ String displayName() } } - private static final Map RESTRICTED_ROLE_MAP = Stream.of( + private static final Map RESTRICTION_ROLE_MAP = Stream.of( ReaderRole.class, AuthorRole.class, EditorWithoutDeleteRole.class, EditorRole.class - ).collect(LabKeyCollectors.toLinkedMap(Class::getName, RestrictedRole::new)); + ).collect(LabKeyCollectors.toLinkedMap(Class::getName, RestrictionRole::new)); @RequiresLogin public static class GetApiKeyRolesAction extends ReadOnlyApiAction @@ -2315,7 +2315,7 @@ public static class GetApiKeyRolesAction extends ReadOnlyApiAction @Override public Object execute(Object o, BindException errors) throws Exception { - return RESTRICTED_ROLE_MAP.entrySet().stream() + return RESTRICTION_ROLE_MAP.entrySet().stream() .map(e -> new JSONObject(Map.of("uniqueName", e.getKey(), "displayName", e.getValue().displayName()))) .collect(LabKeyCollectors.toJSONArray()); } @@ -2374,7 +2374,7 @@ public Object execute(CreateApiKeyForm form, BindException errors) if (!AppProps.getInstance().isAllowApiKeys()) throw new NotFoundException("Creation of API keys is disabled"); - RestrictedRole rr = RESTRICTED_ROLE_MAP.get(form.getRole()); + RestrictionRole rr = RESTRICTION_ROLE_MAP.get(form.getRole()); apiKey = ApiKeyManager.get().createKey(getUser(), form.getDescription(), rr != null ? rr.roleClass() : null); break; case "session": @@ -2410,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() ); } From 6e9eed85e7a2dff49623f79891efb7d476a5e4b3 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Wed, 1 Jul 2026 10:14:44 -0700 Subject: [PATCH 5/7] Read the role column --- api/src/org/labkey/api/security/ApiKeyManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/org/labkey/api/security/ApiKeyManager.java b/api/src/org/labkey/api/security/ApiKeyManager.java index 1a78b353554..c38d24e166d 100644 --- a/api/src/org/labkey/api/security/ApiKeyManager.java +++ b/api/src/org/labkey/api/security/ApiKeyManager.java @@ -232,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(); } From fc96d71a768770d2c298d14caf776bbe224d3506 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Wed, 1 Jul 2026 10:53:22 -0700 Subject: [PATCH 6/7] Add the new column to query tables and core.xml --- core/resources/schemas/core.xml | 1 + core/src/org/labkey/core/query/ApiKeysTableInfo.java | 1 + 2 files changed, 2 insertions(+) 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/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) { From 10af8ca58f2b7b7ec57e60baf1e165ea7e28fc41 Mon Sep 17 00:00:00 2001 From: Adam Rauch Date: Wed, 1 Jul 2026 15:32:23 -0700 Subject: [PATCH 7/7] Add core.ApiKeys.RestrictionRole --- core/module.properties | 2 +- .../schemas/dbscripts/postgresql/core-26.006-26.007.sql | 1 + .../schemas/dbscripts/sqlserver/core-26.006-26.007.sql | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 core/resources/schemas/dbscripts/postgresql/core-26.006-26.007.sql create mode 100644 core/resources/schemas/dbscripts/sqlserver/core-26.006-26.007.sql 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/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