1717package org .labkey .test .tests .nab ;
1818
1919import org .jetbrains .annotations .Nullable ;
20+ import org .json .JSONObject ;
2021import org .junit .BeforeClass ;
2122import org .junit .Test ;
2223import org .junit .experimental .categories .Category ;
2324import org .labkey .remoteapi .CommandException ;
24- import org .labkey .remoteapi .query . ContainerFilter ;
25+ import org .labkey .remoteapi .SimplePostCommand ;
2526import org .labkey .remoteapi .query .Filter ;
2627import org .labkey .remoteapi .query .SelectRowsCommand ;
2728import org .labkey .remoteapi .query .SelectRowsResponse ;
7172import static org .junit .Assert .assertEquals ;
7273import static org .junit .Assert .assertFalse ;
7374import 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