Skip to content

Commit bd3270c

Browse files
committed
Container scoping for NAb including automation
1 parent 3fbde03 commit bd3270c

1 file changed

Lines changed: 110 additions & 0 deletions

File tree

src/org/labkey/test/tests/nab/NabAssayTest.java

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,15 @@
1616

1717
package org.labkey.test.tests.nab;
1818

19+
import org.jetbrains.annotations.Nullable;
1920
import org.junit.BeforeClass;
2021
import org.junit.Test;
2122
import org.junit.experimental.categories.Category;
23+
import org.labkey.remoteapi.CommandException;
24+
import org.labkey.remoteapi.query.ContainerFilter;
25+
import org.labkey.remoteapi.query.Filter;
26+
import org.labkey.remoteapi.query.SelectRowsCommand;
27+
import org.labkey.remoteapi.query.SelectRowsResponse;
2228
import org.labkey.test.BaseWebDriverTest;
2329
import org.labkey.test.Locator;
2430
import org.labkey.test.Locators;
@@ -37,23 +43,30 @@
3743
import org.labkey.test.pages.query.NewQueryPage;
3844
import org.labkey.test.pages.query.SourceQueryPage;
3945
import org.labkey.test.tests.AbstractAssayTest;
46+
import org.labkey.test.util.APIAssayHelper;
47+
import org.labkey.test.util.ApiPermissionsHelper;
4048
import org.labkey.test.util.AssayImportOptions;
4149
import org.labkey.test.util.AssayImporter;
4250
import org.labkey.test.util.DataRegionTable;
4351
import org.labkey.test.util.DilutionAssayHelper;
4452
import org.labkey.test.util.LogMethod;
53+
import org.labkey.test.util.PermissionsHelper;
4554
import org.labkey.test.util.PortalHelper;
4655
import org.labkey.test.util.QCAssayScriptHelper;
56+
import org.labkey.test.util.SimpleHttpRequest;
57+
import org.labkey.test.util.SimpleHttpResponse;
4758
import org.labkey.test.util.TestLogger;
4859
import org.labkey.test.util.WikiHelper;
4960
import org.openqa.selenium.WebDriverException;
5061
import org.openqa.selenium.WebElement;
5162
import org.openqa.selenium.support.ui.ExpectedConditions;
5263

5364
import java.io.File;
65+
import java.io.IOException;
5466
import java.util.ArrayList;
5567
import java.util.Arrays;
5668
import java.util.List;
69+
import java.util.Map;
5770

5871
import static org.junit.Assert.assertEquals;
5972
import static org.junit.Assert.assertFalse;
@@ -73,6 +86,10 @@ public class NabAssayTest extends AbstractAssayTest
7386
protected final static String TEST_ASSAY_USR_NAB_READER = "[email protected]";
7487
private final static String TEST_ASSAY_GRP_NAB_READER = "Nab Dataset Reader";
7588

89+
// Container-scoping fixtures (GitHub Issue #1892, NAB-1/2/8/9): a "bystander" folder and a user privileged only there.
90+
private final static String TEST_ASSAY_FLDR_NAB_SCOPE = "NabScopeBystanderFolder";
91+
private final static String TEST_ASSAY_USR_NAB_SCOPE = "[email protected]";
92+
7693
private static final String NAB_FILENAME2 = "m0902053;3999.xls";
7794
protected final File TEST_ASSAY_NAB_FILE1 = TestFileUtils.getSampleData("Nab/m0902051;3997.xls");
7895
protected final File TEST_ASSAY_NAB_FILE2 = TestFileUtils.getSampleData("Nab/" + NAB_FILENAME2);
@@ -171,6 +188,8 @@ protected void doCleanup(boolean afterTest) throws TestTimeoutException
171188
{
172189
super.doCleanup(afterTest);
173190

191+
_userHelper.deleteUsers(false, TEST_ASSAY_USR_NAB_SCOPE);
192+
174193
try
175194
{
176195
new QCAssayScriptHelper(this).deleteEngine();
@@ -383,6 +402,9 @@ public void runUITests()
383402
startSystemMaintenance("Database");
384403
waitForSystemMaintenanceCompletion();
385404

405+
// Verify cross-container access control for the run/specimen-resolving actions (NAB-1/2/8/9) while the imported runs are still present.
406+
verifyContainerScopedAccessControl();
407+
386408
// Return to the run list
387409
navigateToFolder(getProjectName(), TEST_ASSAY_FLDR_NAB);
388410
clickAndWait(Locator.linkWithText(TEST_ASSAY_NAB));
@@ -489,6 +511,94 @@ public void runUITests()
489511
runNabQCTest();
490512
}
491513

514+
/**
515+
* GitHub Issue #1892: Selenium coverage for the NAb container-scoping fixes (NAB-1, NAB-2, NAB-8, NAB-9). Each of these
516+
* actions resolves a run (by global rowId) or a NAb specimen object id (resolved to its run by a global, cross-container
517+
* lookup) without an intrinsic container check. A user privileged only in a bystander folder must not be able to reach a
518+
* run living in the (foreign) assay folder by pointing one of these actions at its row/object id while scoping the request
519+
* to the bystander folder. We capture the ids as the admin, then issue the requests as an impersonated bystander Editor.
520+
*/
521+
@LogMethod
522+
private void verifyContainerScopedAccessControl()
523+
{
524+
// Capture a protocol id, a run rowId, and a NAb specimen object id from the (foreign) assay folder — done as the admin, before impersonating.
525+
int protocolId = ((APIAssayHelper) _assayHelper).getIdFromAssayName(TEST_ASSAY_NAB, "/" + getProjectName());
526+
int runId = firstRowId("Runs", null);
527+
int objectId = firstRowId("Data", null);
528+
assertTrue("Expected an imported NAb run and specimen to scope against", runId > 0 && objectId > 0);
529+
530+
// A user who is an Editor (read + delete) in the bystander folder only — no access to the assay folder where the run lives.
531+
_containerHelper.createSubfolder(getProjectName(), TEST_ASSAY_FLDR_NAB_SCOPE);
532+
String bystanderPath = getProjectName() + "/" + TEST_ASSAY_FLDR_NAB_SCOPE;
533+
_userHelper.createUser(TEST_ASSAY_USR_NAB_SCOPE);
534+
new ApiPermissionsHelper(this).addMemberToRole(TEST_ASSAY_USR_NAB_SCOPE, "Editor", PermissionsHelper.MemberType.user, "/" + bystanderPath);
535+
536+
impersonate(TEST_ASSAY_USR_NAB_SCOPE);
537+
try
538+
{
539+
// NAB-2: DownloadDatafileAction resolves the run by global rowId.
540+
assertForeignContainerRejected("downloadDatafile (NAB-2)",
541+
WebTestHelper.buildURL("nabassay", bystanderPath, "downloadDatafile", Map.of("rowId", String.valueOf(runId))), "GET");
542+
543+
// NAB-8: NabMultiGraphAction -> MultiGraphAction.getView resolves the object ids to runs.
544+
assertForeignContainerRejected("nabMultiGraph (NAB-8)",
545+
WebTestHelper.buildURL("nabassay", bystanderPath, "nabMultiGraph", Map.of("protocolId", String.valueOf(protocolId), "id", String.valueOf(objectId))), "GET");
546+
547+
// NAB-9: NabGraphSelectedAction -> GraphSelectedAction.getView resolves the object ids to runs.
548+
assertForeignContainerRejected("nabGraphSelected (NAB-9)",
549+
WebTestHelper.buildURL("nabassay", bystanderPath, "nabGraphSelected", Map.of("protocolId", String.valueOf(protocolId), "id", String.valueOf(objectId))), "GET");
550+
551+
// NAB-1: DeleteRunAction resolves the run by global rowId; this action is a POST.
552+
assertForeignContainerRejected("deleteRun (NAB-1)",
553+
WebTestHelper.buildURL("nabassay", bystanderPath, "deleteRun", Map.of("rowId", String.valueOf(runId))), "POST");
554+
}
555+
finally
556+
{
557+
stopImpersonating();
558+
}
559+
560+
// The run must survive the rejected cross-container delete attempt.
561+
assertEquals("Foreign-container delete must not remove the run", runId, firstRowId("Runs", List.of(new Filter("RowId", runId))));
562+
}
563+
564+
/** Fetch the RowId of the first row of an assay.NAb query across the project's subfolders, as the admin. Returns -1 if none. */
565+
private int firstRowId(String queryName, @Nullable List<Filter> filters)
566+
{
567+
SelectRowsCommand command = new SelectRowsCommand("assay.NAb." + TEST_ASSAY_NAB, queryName);
568+
command.setColumns(List.of("RowId"));
569+
command.setContainerFilter(ContainerFilter.CurrentAndSubfolders);
570+
if (filters != null)
571+
command.setFilters(filters);
572+
try
573+
{
574+
SelectRowsResponse response = command.execute(createDefaultConnection(), "/" + getProjectName());
575+
return response.getRows().isEmpty() ? -1 : ((Number) response.getRows().get(0).get("RowId")).intValue();
576+
}
577+
catch (IOException | CommandException e)
578+
{
579+
throw new RuntimeException(e);
580+
}
581+
}
582+
583+
private void assertForeignContainerRejected(String description, String url, String requestMethod)
584+
{
585+
SimpleHttpRequest request = new SimpleHttpRequest(url, requestMethod);
586+
request.copySession(getDriver()); // execute as the impersonated bystander user (carries CSRF token for the POST)
587+
request.clearLogin(); // rely solely on the impersonated session, not admin basic-auth
588+
SimpleHttpResponse response;
589+
try
590+
{
591+
response = request.getResponse();
592+
}
593+
catch (IOException e)
594+
{
595+
throw new RuntimeException(e);
596+
}
597+
assertEquals("Foreign-container request should be rejected with 404: " + description, 404, response.getResponseCode());
598+
assertTrue("Foreign-container rejection for " + description + " should report the resource does not exist, was: " + response.getResponseBody(),
599+
response.getResponseBody().contains("exist"));
600+
}
601+
492602
//Issue 17050: UnsupportedOperationException from org.labkey.nab.query.NabProtocolSchema$NabResultsQueryView.createDataView
493603
private void directBrowserQueryTest()
494604
{

0 commit comments

Comments
 (0)