From 8bf8fe9687935a4510536de1e0184a145def022a Mon Sep 17 00:00:00 2001
From: Cory Nathe
Date: Tue, 25 Nov 2025 15:42:21 -0600
Subject: [PATCH 01/12] GitHub Issue #443: AbstractAssayTsvDataHandler to
lookup field values by titleColumn before primaryKey in checkData (#7226)
- similar to LookupResolutionType.alternateThenPrimaryKey, we want to check if the string value remaps using alternate keys (titleColumn) first
---
.../assay/AbstractAssayTsvDataHandler.java | 12 +++++++---
.../labkey/test/tests/study/AssayTest.java | 23 ++++++++++++++++++-
2 files changed, 31 insertions(+), 4 deletions(-)
diff --git a/api/src/org/labkey/api/assay/AbstractAssayTsvDataHandler.java b/api/src/org/labkey/api/assay/AbstractAssayTsvDataHandler.java
index 9373ee83ee9..6968e221d64 100644
--- a/api/src/org/labkey/api/assay/AbstractAssayTsvDataHandler.java
+++ b/api/src/org/labkey/api/assay/AbstractAssayTsvDataHandler.java
@@ -706,7 +706,8 @@ private DataIterator checkData(
DomainProperty wellLocationPropFinder = null;
DomainProperty wellLsidPropFinder = null;
- RemapCache cache = new RemapCache();
+ RemapCache cacheWithoutPkLookup = new RemapCache();
+ RemapCache cacheWithPkLookup = new RemapCache();
Map remappableLookup = new HashMap<>();
Map materialCache = new LongHashMap<>();
Map> plateWellCache = new LongHashMap<>();
@@ -879,7 +880,12 @@ else if (entry.getKey().equalsIgnoreCase(ProvenanceService.PROVENANCE_INPUT_PROP
{
String s = o instanceof String ? (String) o : o.toString();
TableInfo lookupTable = remappableLookup.get(pd);
- Object remapped = cache.remap(lookupTable, s, true);
+
+ // GitHub Issue #443: similar to LookupResolutionType.alternateThenPrimaryKey, we want to check if the string value remaps using alternate keys (titleColumn) first
+ Object remapped = cacheWithoutPkLookup.remap(lookupTable, s, false);
+ if (remapped == null)
+ remapped = cacheWithPkLookup.remap(lookupTable, s, true);
+
if (remapped == null)
{
if (SAMPLE_CONCEPT_URI.equals(pd.getConceptURI()))
@@ -1026,7 +1032,7 @@ else if (o instanceof MvFieldWrapper mvWrapper)
try
{
if (material == null)
- material = exp.findExpMaterial(lookupContainer, user, materialName, byNameSS, cache, materialCache);
+ material = exp.findExpMaterial(lookupContainer, user, materialName, byNameSS, cacheWithoutPkLookup, materialCache);
}
catch (ValidationException ve)
{
diff --git a/study/test/src/org/labkey/test/tests/study/AssayTest.java b/study/test/src/org/labkey/test/tests/study/AssayTest.java
index 5812f79bedf..b455a46c0a7 100644
--- a/study/test/src/org/labkey/test/tests/study/AssayTest.java
+++ b/study/test/src/org/labkey/test/tests/study/AssayTest.java
@@ -1036,7 +1036,10 @@ public void testAssayLookupValidatorConversion()
_listHelper.bulkImportData(TestDataUtils.tsvStringFromRowMaps(List.of(
Map.of(valueField.getName(), "One"),
Map.of(valueField.getName(), "Two"),
- Map.of(valueField.getName(), "123")
+ Map.of(valueField.getName(), "123"),
+ // GitHub Issue #443: value is the primary key for another row
+ Map.of(valueField.getName(), "5"), // pk = 4
+ Map.of(valueField.getName(), "6") // pk = 5
), List.of(valueField.getName()), true));
log("Create an assay with a results lookup field to the list, with lookup validator set");
@@ -1060,6 +1063,24 @@ public void testAssayLookupValidatorConversion()
.setLookupValidatorEnabled(false);
designerPage.clickFinish();
verifyAssayImportForLookupValidator(ISSUE_53625_ASSAY, lookupField, "RunWithoutLookupValidator", false);
+
+ log("GitHub Issue #443: Verify that importing a value that is also a primary key maps to the titleColumn value");
+ verifyAssayImportForPKValueThatIsTitleColumn(ISSUE_53625_ASSAY, lookupField, "RunWithPKandTitleColumn");
+ }
+
+ private void verifyAssayImportForPKValueThatIsTitleColumn(String assayName, FieldInfo lookupField, String runName)
+ {
+ String runDataStr = TestDataUtils.tsvStringFromRowMaps(List.of(
+ Map.of(lookupField.getName(), "4"), // pk 4, value 5
+ Map.of(lookupField.getName(), "5"), // pk 4, value 5
+ Map.of(lookupField.getName(), "6")), // pk 5, value 6
+ List.of(lookupField.getName()), true
+ );
+ importAssayData(assayName, runName, runDataStr);
+ clickAndWait(Locator.linkWithText(runName));
+ DataRegionTable dataTable = new DataRegionTable("Data", getDriver());
+ checker().verifyEquals("Incorrect number of results shown.", 3, dataTable.getDataRowCount());
+ checker().fatal().verifyEquals("Lookup values not as expected.", List.of("5", "5", "6"), dataTable.getColumnDataAsText(lookupField.getLabel()));
}
private void verifyAssayImportForLookupValidator(String assayName, FieldInfo lookupField, String runName, boolean validatorOn)
From d2edbe9cb40a9821f3fcc3cd2fc97dce85424699 Mon Sep 17 00:00:00 2001
From: Adam Rauch
Date: Tue, 25 Nov 2025 18:00:04 -0800
Subject: [PATCH 02/12] Standardize IN clause generation (#7221)
---
.../attachments/LookAndFeelResourceType.java | 1 +
api/src/org/labkey/api/data/SimpleFilter.java | 35 ++++
.../api/data/TempTableInClauseGenerator.java | 2 +
.../data/dialect/BasePostgreSqlDialect.java | 35 ----
.../data/dialect/DatabaseMaintenanceTask.java | 12 +-
.../api/data/dialect/SimpleSqlDialect.java | 2 +-
.../labkey/api/data/dialect/SqlDialect.java | 32 +++-
.../migration/DatabaseMigrationService.java | 2 +-
.../DefaultMigrationSchemaHandler.java | 8 +-
api/src/org/labkey/api/util/JobRunner.java | 37 ++--
.../core/CoreMigrationSchemaHandler.java | 5 +-
core/src/org/labkey/core/CoreModule.java | 6 +-
.../core/dialect/PostgreSql92Dialect.java | 37 +++-
.../core/dialect/PostgreSqlInClauseTest.java | 59 ------
.../ExperimentMigrationSchemaHandler.java | 171 +++++++++++-------
.../labkey/experiment/ExperimentModule.java | 24 +--
16 files changed, 250 insertions(+), 218 deletions(-)
delete mode 100644 core/src/org/labkey/core/dialect/PostgreSqlInClauseTest.java
diff --git a/api/src/org/labkey/api/attachments/LookAndFeelResourceType.java b/api/src/org/labkey/api/attachments/LookAndFeelResourceType.java
index 0cf38f92686..4495a645f8d 100644
--- a/api/src/org/labkey/api/attachments/LookAndFeelResourceType.java
+++ b/api/src/org/labkey/api/attachments/LookAndFeelResourceType.java
@@ -41,6 +41,7 @@ private LookAndFeelResourceType()
@Override
public void addWhereSql(SQLFragment sql, String parentColumn, String documentNameColumn)
{
+ // Keep in sync with CoreMigrationSchemaHandler.copyAttachments()
sql.append(parentColumn).append(" IN (SELECT EntityId FROM ").append(CoreSchema.getInstance().getTableInfoContainers(), "c").append(") AND (");
sql.append(documentNameColumn).append(" IN (?, ?) OR ");
sql.add(AttachmentCache.FAVICON_FILE_NAME);
diff --git a/api/src/org/labkey/api/data/SimpleFilter.java b/api/src/org/labkey/api/data/SimpleFilter.java
index 8dff53f20ff..5393bbe41aa 100644
--- a/api/src/org/labkey/api/data/SimpleFilter.java
+++ b/api/src/org/labkey/api/data/SimpleFilter.java
@@ -1865,6 +1865,41 @@ private int howManyLessThan(List userIdsDesc, int max)
}
return howMany;
}
+
+ /**
+ * This used to be a PostgreSQL-specific test, but it should run and pass on SQL Server as well. It's largely
+ * redundant with testLargeInClause() above, but causes no harm.
+ */
+ @Test
+ public void testTempTableInClause()
+ {
+ DbSchema core = CoreSchema.getInstance().getSchema();
+ SqlDialect d = core.getSqlDialect();
+
+ Collection allUserIds = new TableSelector(CoreSchema.getInstance().getTableInfoUsersData(), Collections.singleton("UserId")).getCollection(Integer.class);
+ SQLFragment shortSql = new SQLFragment("SELECT * FROM core.UsersData WHERE UserId");
+ d.appendInClauseSql(shortSql, allUserIds);
+ assertEquals(allUserIds.size(), new SqlSelector(core, shortSql).getRowCount());
+
+ ArrayList
-
+
- | Schema |
- Query |
- View Name |
- Flags |
- Owner |
- Created |
- Created By |
- Modified |
- Modified By |
+ Schema |
+ Query |
+ View Name |
+ Flags |
+ Owner |
+ Created |
+ Created By |
+ Modified |
+ Modified By |
+ |
<% if (getViewContext().hasPermission(UpdatePermission.class))
{
+ int count = 1;
for (CstmView view : views)
{
+ count++;
List flags = new ArrayList<>();
if (view.getCustomViewId() == 0)
flags.add("session");
@@ -133,7 +138,7 @@
if (mgr.isSnapshot(view.getFlags()))
flags.add("shapshot");
%>
-
+
| <%=h(view.getSchema())%>
|
<%=h(view.getQueryName())%>
From e1da7bd1f13c265e41e31999ec9e88925d83d04b Mon Sep 17 00:00:00 2001
From: Nick Kerr
Date: Mon, 15 Dec 2025 17:47:12 -0800
Subject: [PATCH 12/12] Additional action logging (#7270)
---
.../api/action/PermissionCheckableAction.java | 17 ++++++++++++++++-
api/src/org/labkey/api/data/Container.java | 13 +++++++++++--
2 files changed, 27 insertions(+), 3 deletions(-)
diff --git a/api/src/org/labkey/api/action/PermissionCheckableAction.java b/api/src/org/labkey/api/action/PermissionCheckableAction.java
index 5795a2e2693..984aa69a8a4 100644
--- a/api/src/org/labkey/api/action/PermissionCheckableAction.java
+++ b/api/src/org/labkey/api/action/PermissionCheckableAction.java
@@ -16,6 +16,7 @@
package org.labkey.api.action;
import jakarta.servlet.http.HttpServletResponse;
+import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import org.labkey.api.data.Container;
import org.labkey.api.module.IgnoresForbiddenProjectCheck;
@@ -39,6 +40,7 @@
import org.labkey.api.security.roles.RoleManager;
import org.labkey.api.util.ConfigurationException;
import org.labkey.api.util.HttpUtil;
+import org.labkey.api.util.logging.LogHelper;
import org.labkey.api.view.BadRequestException;
import org.labkey.api.view.NotFoundException;
import org.labkey.api.view.RedirectException;
@@ -55,6 +57,7 @@
public abstract class PermissionCheckableAction implements Controller, PermissionCheckable, HasViewContext
{
+ private static final Logger LOG = LogHelper.getLogger(PermissionCheckableAction.class, "Permission checks for actions");
private static final HttpUtil.Method[] arrayGetPost = new HttpUtil.Method[] {Method.GET, Method.POST};
private ViewContext _context = null;
UnauthorizedException.Type _unauthorizedType = UnauthorizedException.Type.redirectToLogin;
@@ -148,6 +151,8 @@ private void _checkActionPermissions(Set contextualRoles) throws Unauthori
Container c = context.getContainer();
User user = context.getUser();
Class extends Controller> actionClass = getClass();
+ if (LOG.isDebugEnabled())
+ LOG.debug(actionClass.getName() + ": checking permissions for user " + (user == null ? "" : user.getName() + " (impersonated=" + user.isImpersonated() + ")"));
if (!actionClass.isAnnotationPresent(IgnoresForbiddenProjectCheck.class))
c.throwIfForbiddenProject(user);
@@ -159,18 +164,22 @@ private void _checkActionPermissions(Set contextualRoles) throws Unauthori
methodsAllowed = methodsAllowedAnnotation.value();
if (Arrays.stream(methodsAllowed).noneMatch(s -> s.equals(method)))
{
- throw new BadRequestException("Method Not Allowed: " + method, null, HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+ String msg = "Method Not Allowed: " + method;
+ LOG.debug(msg);
+ throw new BadRequestException(msg, null, HttpServletResponse.SC_METHOD_NOT_ALLOWED);
}
boolean requiresSiteAdmin = actionClass.isAnnotationPresent(RequiresSiteAdmin.class);
if (requiresSiteAdmin && !user.hasSiteAdminPermission())
{
+ LOG.debug(actionClass.getName() + ": action requires site admin permissions");
throw new UnauthorizedException();
}
boolean requiresLogin = actionClass.isAnnotationPresent(RequiresLogin.class);
if (requiresLogin && user.isGuest())
{
+ LOG.debug(actionClass.getName() + ": action requires login (non-guest)");
throw new UnauthorizedException();
}
@@ -214,7 +223,10 @@ private void _checkActionPermissions(Set contextualRoles) throws Unauthori
// Must have all permissions in permissionsRequired
if (!SecurityManager.hasAllPermissions(this.getClass().getName()+"_checkActionPermissions",
c, user, permissionsRequired, contextualRoles))
+ {
+ LOG.debug(actionClass.getName() + ": action requires all permissions: " + permissionsRequired);
throw new UnauthorizedException();
+ }
CSRF.Method csrfCheck = actionClass.isAnnotationPresent(CSRF.class) ? actionClass.getAnnotation(CSRF.class).value() : CSRF.Method.POST;
csrfCheck.validate(context);
@@ -228,7 +240,10 @@ private void _checkActionPermissions(Set contextualRoles) throws Unauthori
Collections.addAll(permissionsAnyOf, requiresAnyOf.value());
if (!SecurityManager.hasAnyPermissions(this.getClass().getName() + "_checkActionPermissions",
c, user, permissionsAnyOf, contextualRoles))
+ {
+ LOG.debug(actionClass.getName() + ": action requires any permissions: " + permissionsAnyOf);
throw new UnauthorizedException();
+ }
}
boolean requiresNoPermission = actionClass.isAnnotationPresent(RequiresNoPermission.class);
diff --git a/api/src/org/labkey/api/data/Container.java b/api/src/org/labkey/api/data/Container.java
index c55c45cd561..d8922b085fc 100644
--- a/api/src/org/labkey/api/data/Container.java
+++ b/api/src/org/labkey/api/data/Container.java
@@ -41,6 +41,7 @@
import org.labkey.api.query.QueryService;
import org.labkey.api.security.HasPermission;
import org.labkey.api.security.SecurableResource;
+import org.labkey.api.security.SecurityLogger;
import org.labkey.api.security.SecurityManager;
import org.labkey.api.security.SecurityPolicy;
import org.labkey.api.security.SecurityPolicyManager;
@@ -549,7 +550,11 @@ private boolean handleForbiddenProject(User user, Set contextualRoles, boo
if (null != impersonationProject && !impersonationProject.equals(currentProject))
{
if (shouldThrow)
- throw new ForbiddenProjectException("You are not allowed to access this folder while impersonating within a different project.");
+ {
+ String msg = "You are not allowed to access this folder while impersonating within a different project.";
+ SecurityLogger.log(msg, user, null, null);
+ throw new ForbiddenProjectException(msg);
+ }
return true;
}
@@ -562,7 +567,11 @@ private boolean handleForbiddenProject(User user, Set contextualRoles, boo
if (lockState.isLocked() && ContainerManager.LOCKED_PROJECT_HANDLER.isForbidden(currentProject, user, contextualRoles, lockState))
{
if (shouldThrow)
- throw new ForbiddenProjectException("You are not allowed to access this folder; it is " + lockState.getDescription() + ".");
+ {
+ String msg = "You are not allowed to access this folder; it is " + lockState.getDescription() + ".";
+ SecurityLogger.log(msg, user, null, null);
+ throw new ForbiddenProjectException(msg);
+ }
return true;
}
|