Skip to content

Commit 127deae

Browse files
committed
Nab action container scoping
1 parent bd3270c commit 127deae

1 file changed

Lines changed: 131 additions & 9 deletions

File tree

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

Lines changed: 131 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@
1717
package org.labkey.test.tests.nab;
1818

1919
import org.jetbrains.annotations.Nullable;
20+
import org.json.JSONObject;
2021
import org.junit.BeforeClass;
2122
import org.junit.Test;
2223
import org.junit.experimental.categories.Category;
2324
import org.labkey.remoteapi.CommandException;
24-
import org.labkey.remoteapi.query.ContainerFilter;
25+
import org.labkey.remoteapi.SimplePostCommand;
2526
import org.labkey.remoteapi.query.Filter;
2627
import org.labkey.remoteapi.query.SelectRowsCommand;
2728
import org.labkey.remoteapi.query.SelectRowsResponse;
@@ -71,6 +72,7 @@
7172
import static org.junit.Assert.assertEquals;
7273
import static org.junit.Assert.assertFalse;
7374
import static org.junit.Assert.assertTrue;
75+
import static org.junit.Assert.fail;
7476

7577
@Category({Daily.class, Assays.class})
7678
@BaseWebDriverTest.ClassTimeout(minutes = 15)
@@ -86,7 +88,7 @@ public class NabAssayTest extends AbstractAssayTest
8688
protected final static String TEST_ASSAY_USR_NAB_READER = "[email protected]";
8789
private final static String TEST_ASSAY_GRP_NAB_READER = "Nab Dataset Reader";
8890

89-
// Container-scoping fixtures (GitHub Issue #1892, NAB-1/2/8/9): a "bystander" folder and a user privileged only there.
91+
// Container-scoping fixtures (GitHub Kanban #1892, NAB-1/2/8/9): a "bystander" folder and a user privileged only there.
9092
private final static String TEST_ASSAY_FLDR_NAB_SCOPE = "NabScopeBystanderFolder";
9193
private final static String TEST_ASSAY_USR_NAB_SCOPE = "[email protected]";
9294

@@ -366,6 +368,13 @@ public void runUITests()
366368
build()).doImport();
367369

368370
verifyRunDetails();
371+
372+
verifyCrossContainerExcludedWellsDenied();
373+
verifyCrossContainerQCControlInfoDenied();
374+
verifyCrossContainerQCDataDenied();
375+
verifyCrossContainerSaveQCControlInfoDenied();
376+
verifyCrossContainerGraphDenied();
377+
369378
// Test editing runs
370379
// Set the design to allow editing
371380
clickAndWait(Locator.linkWithText("View Runs"));
@@ -512,7 +521,7 @@ public void runUITests()
512521
}
513522

514523
/**
515-
* GitHub Issue #1892: Selenium coverage for the NAb container-scoping fixes (NAB-1, NAB-2, NAB-8, NAB-9). Each of these
524+
* GitHub Kanban #1892: Selenium coverage for the NAb container-scoping fixes (NAB-1, NAB-2, NAB-8, NAB-9). Each of these
516525
* actions resolves a run (by global rowId) or a NAb specimen object id (resolved to its run by a global, cross-container
517526
* lookup) without an intrinsic container check. A user privileged only in a bystander folder must not be able to reach a
518527
* run living in the (foreign) assay folder by pointing one of these actions at its row/object id while scoping the request
@@ -523,8 +532,9 @@ private void verifyContainerScopedAccessControl()
523532
{
524533
// Capture a protocol id, a run rowId, and a NAb specimen object id from the (foreign) assay folder — done as the admin, before impersonating.
525534
int protocolId = ((APIAssayHelper) _assayHelper).getIdFromAssayName(TEST_ASSAY_NAB, "/" + getProjectName());
526-
int runId = firstRowId("Runs", null);
527-
int objectId = firstRowId("Data", null);
535+
String runFolderPath = getProjectName() + "/" + TEST_ASSAY_FLDR_NAB;
536+
int runId = firstRowId("Runs", runFolderPath, null);
537+
int objectId = firstRowId("Data", runFolderPath, null);
528538
assertTrue("Expected an imported NAb run and specimen to scope against", runId > 0 && objectId > 0);
529539

530540
// A user who is an Editor (read + delete) in the bystander folder only — no access to the assay folder where the run lives.
@@ -558,20 +568,19 @@ private void verifyContainerScopedAccessControl()
558568
}
559569

560570
// 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))));
571+
assertEquals("Foreign-container delete must not remove the run", runId, firstRowId("Runs", runFolderPath, List.of(new Filter("RowId", runId))));
562572
}
563573

564574
/** 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)
575+
private int firstRowId(String queryName, String containerPath, @Nullable List<Filter> filters)
566576
{
567577
SelectRowsCommand command = new SelectRowsCommand("assay.NAb." + TEST_ASSAY_NAB, queryName);
568578
command.setColumns(List.of("RowId"));
569-
command.setContainerFilter(ContainerFilter.CurrentAndSubfolders);
570579
if (filters != null)
571580
command.setFilters(filters);
572581
try
573582
{
574-
SelectRowsResponse response = command.execute(createDefaultConnection(), "/" + getProjectName());
583+
SelectRowsResponse response = command.execute(createDefaultConnection(), containerPath);
575584
return response.getRows().isEmpty() ? -1 : ((Number) response.getRows().get(0).get("RowId")).intValue();
576585
}
577586
catch (IOException | CommandException e)
@@ -599,6 +608,119 @@ private void assertForeignContainerRejected(String description, String url, Stri
599608
response.getResponseBody().contains("exist"));
600609
}
601610

611+
// GitHub Kanban #1892: verify GetExcludedWellsAction resolves run by RowId and container
612+
@LogMethod
613+
private void verifyCrossContainerExcludedWellsDenied()
614+
{
615+
String runFolderPath = getProjectName() + "/" + TEST_ASSAY_FLDR_NAB;
616+
int runId = firstRowId("Runs", runFolderPath, null);
617+
618+
log("Excluded wells request from the run's own folder should succeed");
619+
SimpleHttpResponse ownFolder = getNabRunApi("getExcludedWells.api", runFolderPath, runId);
620+
assertEquals("Excluded wells request in the run's own folder should succeed", 200, ownFolder.getResponseCode());
621+
assertTrue("Excluded wells response should include the excluded payload", ownFolder.getResponseBody().contains("\"excluded\""));
622+
623+
log("The same run requested from a different container (the project) must be denied, not disclosed");
624+
SimpleHttpResponse crossContainer = getNabRunApi("getExcludedWells.api", getProjectName(), runId);
625+
assertEquals("Cross-container excluded wells request should be denied", 400, crossContainer.getResponseCode());
626+
assertFalse("Cross-container request must not disclose excluded well data", crossContainer.getResponseBody().contains("\"excluded\""));
627+
assertTrue("Cross-container error message not as expected", crossContainer.getResponseBody().contains("NAb Run " + runId + " does not exist."));
628+
}
629+
630+
// GitHub Kanban #1892: verify GetQCControlInfoAction resolves run by RowId and container
631+
@LogMethod
632+
private void verifyCrossContainerQCControlInfoDenied()
633+
{
634+
String runFolderPath = getProjectName() + "/" + TEST_ASSAY_FLDR_NAB;
635+
int runId = firstRowId("Runs", runFolderPath, null);
636+
637+
log("QC control info request from the run's own folder should succeed");
638+
SimpleHttpResponse ownFolder = getNabRunApi("getQCControlInfo.api", runFolderPath, runId);
639+
assertEquals("QC control info request in the run's own folder should succeed", 200, ownFolder.getResponseCode());
640+
assertTrue("QC control info response should include the plates payload", ownFolder.getResponseBody().contains("\"plates\""));
641+
642+
log("The same run requested from a different container (the project) must be denied, not disclosed");
643+
SimpleHttpResponse crossContainer = getNabRunApi("getQCControlInfo.api", getProjectName(), runId);
644+
assertEquals("Cross-container QC control info request should be denied", 404, crossContainer.getResponseCode());
645+
assertFalse("Cross-container request must not disclose plate data", crossContainer.getResponseBody().contains("\"plates\""));
646+
assertTrue("Cross-container error message not as expected", crossContainer.getResponseBody().contains("Run " + runId + " does not exist."));
647+
}
648+
649+
// GitHub Kanban #1892: verify QCDataAction resolves run by RowId and container
650+
@LogMethod
651+
private void verifyCrossContainerQCDataDenied()
652+
{
653+
String runFolderPath = getProjectName() + "/" + TEST_ASSAY_FLDR_NAB;
654+
int runId = firstRowId("Runs", runFolderPath, null);
655+
656+
log("QC data view from the run's own folder should render");
657+
SimpleHttpResponse ownFolder = getNabRunApi("qcData.view", runFolderPath, runId);
658+
assertEquals("QC data view in the run's own folder should render", 200, ownFolder.getResponseCode());
659+
660+
log("The same run requested from a different container (the project) must be denied, not disclosed");
661+
SimpleHttpResponse crossContainer = getNabRunApi("qcData.view", getProjectName(), runId);
662+
assertEquals("Cross-container QC data view should be denied", 404, crossContainer.getResponseCode());
663+
assertTrue("Cross-container error message not as expected", crossContainer.getResponseBody().contains("Run " + runId + " does not exist."));
664+
}
665+
666+
// GitHub Kanban #1892: verify SaveQCControlInfoAction resolves run by RunId and container (write path)
667+
@LogMethod
668+
private void verifyCrossContainerSaveQCControlInfoDenied()
669+
{
670+
String runFolderPath = getProjectName() + "/" + TEST_ASSAY_FLDR_NAB;
671+
int runId = firstRowId("Runs", runFolderPath, null);
672+
673+
log("Saving QC control info for the run from a different container (the project) must be denied");
674+
SimplePostCommand command = new SimplePostCommand("nabassay", "saveQCControlInfo");
675+
command.setJsonObject(new JSONObject().put("runId", runId));
676+
try
677+
{
678+
command.execute(createDefaultConnection(), getProjectName());
679+
fail("Cross-container QC control info save should have been denied");
680+
}
681+
catch (CommandException e)
682+
{
683+
assertEquals("Cross-container QC control info save should be denied", 400, e.getStatusCode());
684+
assertTrue("Cross-container error message not as expected", e.getMessage().contains("NAb Run " + runId + " does not exist."));
685+
}
686+
catch (IOException e)
687+
{
688+
throw new RuntimeException(e);
689+
}
690+
}
691+
692+
// GitHub Kanban #1892: verify DilutionGraphAction (NAb GraphAction) resolves run by RowId and container
693+
@LogMethod
694+
private void verifyCrossContainerGraphDenied()
695+
{
696+
String runFolderPath = getProjectName() + "/" + TEST_ASSAY_FLDR_NAB;
697+
int runId = firstRowId("Runs", runFolderPath, null);
698+
699+
log("NAb graph from the run's own folder should render");
700+
SimpleHttpResponse ownFolder = getNabRunApi("graph.view", runFolderPath, runId);
701+
assertEquals("NAb graph in the run's own folder should render", 200, ownFolder.getResponseCode());
702+
703+
log("The same run requested from a different container (the project) must be denied, not disclosed");
704+
SimpleHttpResponse crossContainer = getNabRunApi("graph.view", getProjectName(), runId);
705+
assertEquals("Cross-container NAb graph should be denied", 404, crossContainer.getResponseCode());
706+
assertTrue("Cross-container error message not as expected", crossContainer.getResponseBody().contains("Run " + runId + " does not exist."));
707+
}
708+
709+
private SimpleHttpResponse getNabRunApi(String action, String containerPath, long runId)
710+
{
711+
String url = WebTestHelper.buildURL("nabassay", containerPath, action, Map.of("rowId", String.valueOf(runId)));
712+
SimpleHttpRequest request = new SimpleHttpRequest(url);
713+
request.copySession(getDriver());
714+
try
715+
{
716+
return request.getResponse();
717+
}
718+
catch (IOException e)
719+
{
720+
throw new RuntimeException(e);
721+
}
722+
}
723+
602724
//Issue 17050: UnsupportedOperationException from org.labkey.nab.query.NabProtocolSchema$NabResultsQueryView.createDataView
603725
private void directBrowserQueryTest()
604726
{

0 commit comments

Comments
 (0)