Skip to content

Commit 64f6763

Browse files
committed
Merge remote-tracking branch 'origin/release26.7-SNAPSHOT' into 26.7_fb_sql_execute_refactor
2 parents 88e920e + 7d8a2af commit 64f6763

38 files changed

Lines changed: 457 additions & 102 deletions

api/src/org/labkey/api/admin/AdminBean.java

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import org.labkey.api.data.ContainerManager;
2525
import org.labkey.api.data.CoreSchema;
2626
import org.labkey.api.data.DbScope;
27-
import org.labkey.api.module.Module;
2827
import org.labkey.api.module.ModuleLoader;
2928
import org.labkey.api.security.UserManager;
3029
import org.labkey.api.settings.AppProps;
@@ -43,7 +42,6 @@
4342
import java.util.ArrayList;
4443
import java.util.Arrays;
4544
import java.util.Collections;
46-
import java.util.Comparator;
4745
import java.util.List;
4846
import java.util.Map;
4947
import java.util.TreeMap;
@@ -82,7 +80,6 @@ public static class RecentUser
8280
public static final String sessionTimeout = Formats.commaf0.format(ModuleLoader.getServletContext().getSessionTimeout());
8381
public static final String buildTime = ModuleLoader.getInstance().getCoreModule().getBuildTime();
8482
public static final String serverStartupTime = DateUtil.formatDateTime(ContainerManager.getRoot());
85-
public static final List<Module> modules;
8683

8784
public static String asserts = "disabled";
8885

@@ -116,9 +113,6 @@ public static class RecentUser
116113

117114
//noinspection ConstantConditions,AssertWithSideEffects
118115
assert null != (asserts = "enabled");
119-
120-
modules = new ArrayList<>(ModuleLoader.getInstance().getModules());
121-
modules.sort(Comparator.comparing(Module::getName, String.CASE_INSENSITIVE_ORDER));
122116
}
123117

124118
private static @Nullable String getValue(Field field)

api/src/org/labkey/api/data/dialect/BasePostgreSqlDialect.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -924,6 +924,19 @@ public boolean supportsNativeGreatestAndLeast()
924924
return true;
925925
}
926926

927+
@Override
928+
public boolean supportsIsNumeric()
929+
{
930+
return true;
931+
}
932+
933+
@Override
934+
public SQLFragment isNumericExpr(SQLFragment expression)
935+
{
936+
return new SQLFragment("(CASE WHEN CAST((").append(expression)
937+
.append(") AS TEXT) ~ '^[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)$' THEN 1 ELSE 0 END)");
938+
}
939+
927940
private class PostgreSqlColumnMetaDataReader extends ColumnMetaDataReader
928941
{
929942
private final TableInfo _table;

api/src/org/labkey/api/data/dialect/SqlDialect.java

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -863,15 +863,25 @@ public SQLFragment getNumericCast(SQLFragment expression)
863863
* @param arguments Arguments passed from the LK SQL
864864
* @return the dialect equivalent SQLFragrment
865865
*/
866-
public SQLFragment getGreatestAndLeastSQL(String method, SQLFragment... arguments)
867-
{
868-
throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement");
869-
}
870-
871-
public void handleCreateDatabaseException(SQLException e) throws ServletException
872-
{
873-
throw(new ServletException("Can't create database", e));
874-
}
866+
public SQLFragment getGreatestAndLeastSQL(String method, SQLFragment... arguments)
867+
{
868+
throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement");
869+
}
870+
871+
public boolean supportsIsNumeric()
872+
{
873+
return false;
874+
}
875+
876+
public SQLFragment isNumericExpr(SQLFragment expression)
877+
{
878+
throw new UnsupportedOperationException(getClass().getSimpleName() + " does not implement");
879+
}
880+
881+
public void handleCreateDatabaseException(SQLException e) throws ServletException
882+
{
883+
throw(new ServletException("Can't create database", e));
884+
}
875885

876886
/**
877887
* Wrap one or more INSERT statements to allow explicit specification

api/src/org/labkey/api/moduleeditor/api/ModuleEditorService.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,11 @@ default File getFileForModuleResource(Module module, Path path)
8181
return null;
8282
return FileUtil.appendPath(resources, path);
8383
}
84+
85+
// used by ModuleResourceProvider@AllModuleResourcesRoot() (_webdav/@modules) which wants to soo
86+
// the whole module source directory (e.g. when using SourcePath)
87+
default File getModuleRoot(Module module, @Nullable List<String> messages)
88+
{
89+
return null;
90+
}
8491
}

api/src/org/labkey/api/security/ApiKeyManager.java

Lines changed: 98 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.jetbrains.annotations.Nullable;
2323
import org.junit.Assert;
2424
import org.junit.Test;
25+
import org.labkey.api.data.ContainerManager;
2526
import org.labkey.api.data.CoreSchema;
2627
import org.labkey.api.data.DbScope;
2728
import org.labkey.api.data.DbScope.Transaction;
@@ -39,6 +40,15 @@
3940
import org.labkey.api.query.FieldKey;
4041
import org.labkey.api.security.UserManager.SessionHandler;
4142
import org.labkey.api.security.ValidEmail.InvalidEmailException;
43+
import org.labkey.api.security.permissions.AdminPermission;
44+
import org.labkey.api.security.permissions.DeletePermission;
45+
import org.labkey.api.security.permissions.InsertPermission;
46+
import org.labkey.api.security.permissions.ReadPermission;
47+
import org.labkey.api.security.permissions.UpdatePermission;
48+
import org.labkey.api.security.roles.EditorRole;
49+
import org.labkey.api.security.roles.ReaderRole;
50+
import org.labkey.api.security.roles.Role;
51+
import org.labkey.api.security.roles.RoleManager;
4252
import org.labkey.api.settings.AppProps;
4353
import org.labkey.api.settings.LenientStartupPropertyHandler;
4454
import org.labkey.api.settings.StartupProperty;
@@ -60,6 +70,7 @@
6070
import java.util.Map;
6171
import java.util.Objects;
6272
import java.util.Set;
73+
import java.util.stream.Stream;
6374

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

@@ -85,9 +96,9 @@ private ApiKeyManager()
8596
* @param user User to be associated with the new API key.
8697
* @return An API key that expires after the admin-configured duration
8798
*/
88-
public @NotNull String createKey(@NotNull User user, @Nullable String description)
99+
public @NotNull String createKey(@NotNull User user, @Nullable String description, @Nullable Class<? extends Role> restrictionRole)
89100
{
90-
return createKey(user, AppProps.getInstance().getApiKeyExpirationSeconds(), description);
101+
return createKey(user, AppProps.getInstance().getApiKeyExpirationSeconds(), description, restrictionRole);
91102
}
92103

93104
/**
@@ -97,6 +108,18 @@ private ApiKeyManager()
97108
* @return An API key that expires after the specified number of seconds
98109
*/
99110
public @NotNull String createKey(@NotNull User user, int expirationSeconds, @Nullable String description)
111+
{
112+
return createKey(user, expirationSeconds, description, null);
113+
}
114+
115+
/**
116+
* Create an API key associated with a user and persist it in the database.
117+
* @param user User to be associated with the new API key.
118+
* @param expirationSeconds Number of seconds until expiration. -1 means no expiration.
119+
* @param restrictionRole Role class that limits this API key's permissions. null means no restrictions.
120+
* @return An API key that expires after the specified number of seconds
121+
*/
122+
public @NotNull String createKey(@NotNull User user, int expirationSeconds, @Nullable String description, @Nullable Class<? extends Role> restrictionRole)
100123
{
101124
if (user.isGuest())
102125
throw new IllegalStateException("Can't create an API key for a guest");
@@ -120,6 +143,9 @@ private ApiKeyManager()
120143
if (description != null)
121144
map.put("Description", StringUtils.abbreviate(description.trim(), 256));
122145

146+
if (restrictionRole != null)
147+
map.put("RestrictionRole", restrictionRole.getName());
148+
123149
try (Transaction t = CoreSchema.getInstance().getScope().beginTransaction(TRANSACTION_KIND))
124150
{
125151
Table.insert(user, CoreSchema.getInstance().getTableAPIKeys(), map);
@@ -183,17 +209,35 @@ public void updateLastUsed(String apikey)
183209

184210
try (Transaction t = scope.beginTransaction(TRANSACTION_KIND))
185211
{
186-
SQLFragment sql = new SQLFragment("UPDATE " + CoreSchema.getInstance().getTableAPIKeys() + " SET LastUsed = ? WHERE Crypt = ?", new Date(), crypt(apikey));
212+
SQLFragment sql = new SQLFragment("UPDATE ")
213+
.append(CoreSchema.getInstance().getTableAPIKeys())
214+
.append(" SET LastUsed = ? WHERE Crypt = ?")
215+
.add(new Date())
216+
.add(crypt(apikey));
187217
new SqlExecutor(scope).execute(sql);
188218
t.commit();
189219
}
190220
}
191221

192-
public record ApiKeyAuthentication(int createdBy, int rowId)
222+
public record ApiKeyAuthentication(int createdBy, int rowId, @Nullable Class<? extends Role> restrictionRole)
193223
{
194-
public User getUser()
224+
public @Nullable User getUser()
195225
{
196-
return UserManager.getUser(createdBy());
226+
User user = UserManager.getUser(createdBy());
227+
if (restrictionRole != null)
228+
{
229+
Role role = RoleManager.getRole(restrictionRole);
230+
if (role == null)
231+
{
232+
LOG.error("API key for {} specifies a restriction role {} that was not found", user, restrictionRole.getName());
233+
user = null;
234+
}
235+
else
236+
{
237+
user = new PermissionsRestrictedUser(user, role.getPermissions());
238+
}
239+
}
240+
return user;
197241
}
198242
}
199243

@@ -212,7 +256,7 @@ public User getUser()
212256

213257
try (Transaction t = CoreSchema.getInstance().getScope().beginTransaction(TRANSACTION_KIND))
214258
{
215-
ret = new TableSelector(CoreSchema.getInstance().getTableAPIKeys(), Set.of("CreatedBy", "RowId"), filter, null).getObject(ApiKeyAuthentication.class);
259+
ret = new TableSelector(CoreSchema.getInstance().getTableAPIKeys(), Set.of("CreatedBy", "RowId", "RestrictionRole"), filter, null).getObject(ApiKeyAuthentication.class);
216260
t.commit();
217261
}
218262

@@ -327,6 +371,53 @@ public void testTransaction()
327371
ApiKeyManager.get().deleteKey(apikey);
328372
assertNull(ApiKeyManager.get().authenticateFromApiKey(apikey));
329373
}
374+
375+
private record UserAndKey(User user, String apiKey){}
376+
377+
@Test
378+
public void testRoleRestrictions()
379+
{
380+
User admin = TestContext.get().getUser();
381+
UserAndKey readerUAK = createApiKeyAndRetrieveUser(admin, ReaderRole.class);
382+
User reader = readerUAK.user();
383+
UserAndKey editorUAK = createApiKeyAndRetrieveUser(admin, EditorRole.class);
384+
User editor = editorUAK.user();
385+
386+
ContainerManager.getAllChildren(ContainerManager.getRoot(), admin, AdminPermission.class).stream()
387+
.limit(5)
388+
.forEach(child -> {
389+
assertTrue(child.hasPermission(admin, AdminPermission.class));
390+
assertFalse(child.hasPermission(editor, AdminPermission.class));
391+
assertFalse(child.hasPermission(reader, AdminPermission.class));
392+
Stream.of(DeletePermission.class, UpdatePermission.class, InsertPermission.class)
393+
.forEach(perm -> {
394+
assertTrue(child.hasPermission(admin, perm));
395+
assertTrue(child.hasPermission(editor, perm));
396+
assertFalse(child.hasPermission(reader, perm));
397+
});
398+
assertTrue(child.hasPermission(admin, ReadPermission.class));
399+
assertTrue(child.hasPermission(editor, ReadPermission.class));
400+
assertTrue(child.hasPermission(reader, ReadPermission.class));
401+
});
402+
403+
ApiKeyManager.get().deleteKey(readerUAK.apiKey());
404+
ApiKeyManager.get().deleteKey(editorUAK.apiKey());
405+
assertNull(ApiKeyManager.get().authenticateFromApiKey(readerUAK.apiKey()));
406+
assertNull(ApiKeyManager.get().authenticateFromApiKey(editorUAK.apiKey()));
407+
}
408+
409+
private UserAndKey createApiKeyAndRetrieveUser(User user, Class<? extends Role> restrictionRole)
410+
{
411+
String apiKey = ApiKeyManager.get().createKey(user, 10, "Created by ApiKeyManager.TestCase", restrictionRole);
412+
ApiKeyAuthentication auth = ApiKeyManager.get().authenticateFromApiKey(apiKey);
413+
assertNotNull(auth);
414+
User restrictedUser = auth.getUser();
415+
assertNotNull(restrictedUser);
416+
assertEquals(user.getUserId(), restrictedUser.getUserId());
417+
assertTrue(restrictedUser instanceof PermissionsRestrictedUser);
418+
419+
return new UserAndKey(restrictedUser, apiKey);
420+
}
330421
}
331422

332423
public static class ApiKeyMaintenanceTask implements MaintenanceTask

api/src/org/labkey/api/security/ClonedUser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ protected ClonedUser(String email, int userId, String displayName, String firstN
4545
setPhone(phone);
4646
setLastActivity(lastActivity);
4747

48-
setImpersonationContext(ctx);
48+
setPermissionsContext(ctx);
4949
}
5050

5151
// Map a stream of role classes to a set of roles

api/src/org/labkey/api/security/ElevatedUser.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.labkey.api.security;
1717

18+
import com.google.common.collect.Streams;
1819
import org.labkey.api.audit.permissions.CanSeeAuditLogPermission;
1920
import org.labkey.api.data.Container;
2021
import org.labkey.api.security.permissions.Permission;
@@ -26,6 +27,7 @@
2627
import java.util.Collection;
2728
import java.util.Set;
2829
import java.util.stream.Collectors;
30+
import java.util.stream.Stream;
2931

3032
/**
3133
* A wrapped user that possesses all the security properties (groups, roles, impersonation status, etc.) of the
@@ -35,9 +37,26 @@
3537
*/
3638
public class ElevatedUser extends ClonedUser
3739
{
40+
private static class ElevatedUserContext extends WrappedPermissionsContext
41+
{
42+
private final Set<Role> _additionalRoles;
43+
44+
private ElevatedUserContext(PermissionsContext delegate, Set<Role> additionalRoles)
45+
{
46+
super(delegate);
47+
_additionalRoles = additionalRoles;
48+
}
49+
50+
@Override
51+
public Stream<Role> getAssignedRoles(User user, SecurableResource resource)
52+
{
53+
return Streams.concat(_additionalRoles.stream(), super.getAssignedRoles(user, resource));
54+
}
55+
}
56+
3857
private ElevatedUser(User user, Set<Role> rolesToAdd)
3958
{
40-
super(user, new WrappedPermissionsContext(user.getPermissionsContext(), rolesToAdd));
59+
super(user, new ElevatedUserContext(user.getPermissionsContext(), rolesToAdd));
4160
}
4261

4362
private ElevatedUser(User user, PermissionsContext ctx)

api/src/org/labkey/api/security/LimitedUser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ private LimitedUser(
9797
@JsonProperty("_lastLogin") Date lastLogin,
9898
@JsonProperty("_phone") String phone,
9999
@JsonProperty("_lastActivity") Date lastActivity,
100-
@JsonProperty("_impersonationContext") PermissionsContext ctx
100+
@JsonProperty("_permissionsContext") PermissionsContext ctx
101101
)
102102
{
103103
super(name, userId, displayName, firstName, lastName, active, lastLogin, phone, lastActivity, ctx);

api/src/org/labkey/api/security/PermissionsContext.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.labkey.api.security;
1717

1818
import com.google.common.collect.Streams;
19+
import jakarta.servlet.http.HttpSession;
1920
import org.jetbrains.annotations.Nullable;
2021
import org.labkey.api.data.Container;
2122
import org.labkey.api.data.ContainerManager;
@@ -87,4 +88,9 @@ default Stream<Class<? extends Permission>> filterPermissions(Stream<Class<? ext
8788
{
8889
return perms;
8990
}
91+
92+
/** PermissionsContext can add session attributes alongside the user ID and attributes */
93+
default void modifySession(HttpSession session)
94+
{
95+
}
9096
}

0 commit comments

Comments
 (0)