Skip to content

Commit f251ba7

Browse files
committed
Container scoping for NAb including automation
1 parent fe53b58 commit f251ba7

7 files changed

Lines changed: 40 additions & 106 deletions

File tree

flow/src/org/labkey/flow/controllers/editscript/EditScriptForm.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ public void reset()
9797
{
9898
throw new NotFoundException("scriptId not found: " + scriptIdStr);
9999
}
100+
// GitHub Issue #1892: validate container
100101
flowObject.checkContainer(getContainer(), getUser(), url);
101102
_runCount = flowObject.getRunCount();
102103
step = FlowProtocolStep.fromRequest(getRequest());

flow/src/org/labkey/flow/controllers/run/RunController.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ public void validate(DownloadRunForm form, BindException errors)
232232
errors.reject(ERROR_MSG, "run not found");
233233
return;
234234
}
235+
// GitHub Issue #1892: validate container
235236
_run.checkContainer(getContainer(), getUser(), getActionURL());
236237

237238
FlowWell[] wells = _run.getWells(true);
@@ -465,6 +466,7 @@ else if (form.getSendTo() == ExportAnalysisForm.SendTo.Script)
465466
if (run == null)
466467
throw new NotFoundException("Flow run not found");
467468

469+
// GitHub Issue #1892: validate container
468470
run.checkContainer(getContainer(), getUser(), getActionURL());
469471
runs.add(run);
470472
}
@@ -479,6 +481,7 @@ else if (wellId != null && wellId.length > 0)
479481
if (well == null)
480482
throw new NotFoundException("Flow well not found");
481483

484+
// GitHub Issue #1892: validate container
482485
well.checkContainer(getContainer(), getUser(), getActionURL());
483486
wells.add(well);
484487
}
@@ -944,6 +947,7 @@ public static class DownloadAttachmentAction extends BaseDownloadAction<Attachme
944947
{
945948
throw new NotFoundException();
946949
}
950+
// GitHub Issue #1892: validate container
947951
run.checkContainer(getContainer(), getUser(), getViewContext().getActionURL());
948952

949953
return new Pair<>(new ExpRunAttachmentParent(run.getExperimentRun()), form.getName());

flow/src/org/labkey/flow/data/FlowProtocol.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ static public FlowProtocol fromURL(User user, ViewContext context, HttpServletRe
161161
if (ret == null)
162162
return null;
163163

164+
// GitHub Issue #1892: validate container
164165
ret.checkContainer(context.getContainer(), user, url);
165166
if (!ret.getContainer().hasPermission(user, ReadPermission.class))
166167
{

flow/src/org/labkey/flow/persist/FlowManager.java

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1835,22 +1835,17 @@ public void setup()
18351835
@Test
18361836
public void testScriptContainerScoping() throws Exception
18371837
{
1838-
// A flow script that lives in folder B.
1838+
// GitHub Issue #1892: Regression test for FLOW-1.
18391839
FlowScript script = FlowScript.create(_admin, _folderB, "scope-test",
18401840
"<script xmlns=\"http://www.labkey.org/data/xml/flowScript\"/>");
18411841
int scriptId = script.getScriptId();
18421842

1843-
// DownloadAction resolves the script through EditScriptForm; dispatch it against folder A while pointing
1844-
// scriptId at B's script -- the FLOW-1 attack shape (reaching a script that lives in another folder).
18451843
ActionURL foreignUrl = new ActionURL(ScriptController.DownloadAction.class, _folderA)
18461844
.addParameter(FlowParam.scriptId.toString(), scriptId);
18471845

18481846
User readerAonly = createUserInRole(_folderA, ReaderRole.class);
18491847
assertStatus(HttpServletResponse.SC_NOT_FOUND, get(foreignUrl, readerAonly));
18501848

1851-
// Redirect branch: a caller who can read both folders is redirected to B (where its own permissions are
1852-
// re-enforced) instead of being served B's script from A's context. This is the core vulnerability:
1853-
// without checkContainer() in EditScriptForm.reset() the action streams B's script from folder A (200).
18541849
User readerAreaderB = createUserInRole(_folderA, ReaderRole.class);
18551850
grantRole(readerAreaderB, _folderB, ReaderRole.class);
18561851
MockHttpServletResponse resp = get(foreignUrl, readerAreaderB);
@@ -1868,22 +1863,16 @@ public void testScriptContainerScoping() throws Exception
18681863
@Test
18691864
public void testProtocolContainerScoping() throws Exception
18701865
{
1871-
// Regression test for FLOW-2. A FlowProtocol is accessed by a global experimentId.
1872-
// A flow protocol that lives in folder B.
1866+
// GitHub Issue #1892: Regression test for FLOW-2. A FlowProtocol is accessed by a global experimentId.
18731867
FlowProtocol protocolB = FlowProtocol.ensureForContainer(_admin, _folderB);
18741868
int experimentId = protocolB.getProtocol().getRowId();
18751869

1876-
// EditFCSAnalysisFilter is an UpdatePermission action; dispatch it against folder A while pointing
1877-
// experimentId at B's protocol -- the FLOW-2 attack shape (editor in A reaching a protocol in B).
18781870
ActionURL foreignUrl = new ActionURL(ProtocolController.EditFCSAnalysisFilterAction.class, _folderA)
18791871
.addParameter(FlowParam.experimentId.toString(), experimentId);
18801872

18811873
User editorAonly = createUserInRole(_folderA, EditorRole.class);
18821874
assertStatus(HttpServletResponse.SC_NOT_FOUND, get(foreignUrl, editorAonly));
18831875

1884-
// Redirect branch: a caller who can edit folder A and read folder B is redirected to B (where its own
1885-
// permissions are re-enforced) instead of operating on B's protocol from A's context. This is the core
1886-
// vulnerability: without checkContainer() in fromURL() the action proceeds in folder A (HTTP 200).
18871876
User editorAreaderB = createUserInRole(_folderA, EditorRole.class);
18881877
grantRole(editorAreaderB, _folderB, ReaderRole.class);
18891878
MockHttpServletResponse resp = get(foreignUrl, editorAreaderB);
@@ -1901,7 +1890,7 @@ public void testProtocolContainerScoping() throws Exception
19011890
@Test
19021891
public void testExportAnalysisContainerScoping() throws Exception
19031892
{
1904-
// Regression test for FLOW-3 RunController.ExportAnalysis resolves
1893+
// GitHub Issue #1892: Regression test for FLOW-3. RunController.ExportAnalysis resolves
19051894
// URL supplied runId/wellId via the global FlowRun.
19061895
ExpData data = ExperimentService.get().createData(_folderB, FlowDataType.FCSFile, "scope-test-well");
19071896
URI dataFileURI = new URI("file:///attributes.flowdata.xml");
@@ -1912,18 +1901,12 @@ public void testExportAnalysisContainerScoping() throws Exception
19121901
AttributeSetHelper.save(attrs, _admin, data);
19131902
int wellId = data.getRowId();
19141903

1915-
// Point wellId at B's well while dispatching the export against folder A -- the FLOW-3 attack shape.
19161904
ActionURL foreignUrl = new ActionURL(RunController.ExportAnalysis.class, _folderA)
19171905
.addParameter(FlowParam.wellId.toString(), wellId);
19181906

1919-
// Deny branch: a caller who can read folder A but has no rights in folder B must not learn B's well exists,
1920-
// nor export it -> 404.
19211907
User readerAonly = createUserInRole(_folderA, ReaderRole.class);
19221908
assertStatus(HttpServletResponse.SC_NOT_FOUND, post(foreignUrl, readerAonly));
19231909

1924-
// Redirect branch: a caller who can read both folders is redirected to B (where its own permissions are
1925-
// re-enforced) instead of having B's well exported from A's context. This is the core vulnerability: without
1926-
// checkContainer() in validateCommand() the action exports B's well from folder A.
19271910
User readerAreaderB = createUserInRole(_folderA, ReaderRole.class);
19281911
grantRole(readerAreaderB, _folderB, ReaderRole.class);
19291912
MockHttpServletResponse resp = post(foreignUrl, readerAreaderB);
@@ -1940,8 +1923,8 @@ public void testExportAnalysisContainerScoping() throws Exception
19401923
@Test
19411924
public void testRunDownloadContainerScoping() throws Exception
19421925
{
1943-
// Regression test for FLOW-4. RunController.DownloadAction (and
1944-
// DownloadAttachmentAction) resolve a run via RunForm.getRun() -> FlowRun.fromRunId() from a global runId
1926+
// GitHub Issue #1892: Regression test for FLOW-4. RunController.DownloadAction (and
1927+
// DownloadAttachmentAction) resolve a run via RunForm.getRun() -> FlowRun.fromRunId() from a global runId.
19451928
FlowProtocol protocolB = FlowProtocol.ensureForContainer(_admin, _folderB);
19461929
ExpRun expRun = ExperimentService.get().createExperimentRun(_folderB, "scope-test-run");
19471930
expRun.setProtocol(protocolB.getProtocol());
@@ -1954,9 +1937,6 @@ public void testRunDownloadContainerScoping() throws Exception
19541937
User readerAonly = createUserInRole(_folderA, ReaderRole.class);
19551938
assertStatus(HttpServletResponse.SC_NOT_FOUND, get(foreignUrl, readerAonly));
19561939

1957-
// Redirect branch: a caller who can read both folders is redirected to B (where its own permissions are
1958-
// re-enforced) instead of having B's run streamed from A's context. This is the core vulnerability: without
1959-
// checkContainer() in DownloadAction.validate() the action streams B's run from folder A.
19601940
User readerAreaderB = createUserInRole(_folderA, ReaderRole.class);
19611941
grantRole(readerAreaderB, _folderB, ReaderRole.class);
19621942
MockHttpServletResponse resp = get(foreignUrl, readerAreaderB);

nab/src/org/labkey/nab/NabAssayController.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,9 +401,33 @@ public ActionURL getGraphRenderURL()
401401
}
402402
}
403403

404+
private static void _verifyObjectIdsReadable(User user, int[] ids)
405+
{
406+
if (ids == null)
407+
return;
408+
409+
// GitHub Issue #1892: (NAB-9) The object ids come straight from the request and getDilutionSummaries() resolves them to runs
410+
// via a global, cross-container lookup.
411+
for (int id : ids)
412+
{
413+
ExpRun run = NabManager.get().getNAbRunByObjectId(id);
414+
if (run == null)
415+
throw new NotFoundException("One or more requested specimens do not exist.");
416+
if (run.getContainer().hasPermission(user, ReadPermission.class))
417+
continue;
418+
throw new NotFoundException("One or more requested specimens do not exist.");
419+
}
420+
}
421+
404422
@RequiresPermission(ReadPermission.class)
405423
public static class NabGraphSelectedAction extends GraphSelectedAction<GraphSelectedForm>
406424
{
425+
@Override
426+
protected void verifyObjectIdsReadable(int[] ids)
427+
{
428+
_verifyObjectIdsReadable(getUser(), ids);
429+
}
430+
407431
@Override
408432
protected GraphSelectedBean createSelectionBean(ViewContext context, ExpProtocol protocol, int[] cutoffs, int[] dataObjectIds, String caption, String title)
409433
{
@@ -493,6 +517,11 @@ public URLHelper getSuccessURL(DeleteRunForm form)
493517
@ContextualRoles(RunDatasetContextualRoles.class)
494518
public static class NabMultiGraphAction extends MultiGraphAction<GraphSelectedForm>
495519
{
520+
@Override
521+
protected void verifyObjectIdsReadable(int[] ids)
522+
{
523+
_verifyObjectIdsReadable(getUser(), ids);
524+
}
496525
}
497526

498527
@RequiresPermission(ReadPermission.class)

nab/src/org/labkey/nab/NabManager.java

Lines changed: 0 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,8 @@
1616

1717
package org.labkey.nab;
1818

19-
import jakarta.servlet.http.HttpServletResponse;
2019
import org.apache.logging.log4j.LogManager;
2120
import org.apache.logging.log4j.Logger;
22-
import org.junit.Before;
23-
import org.junit.Test;
2421
import org.labkey.api.assay.AssayService;
2522
import org.labkey.api.data.ColumnInfo;
2623
import org.labkey.api.data.Container;
@@ -41,18 +38,13 @@
4138
import org.labkey.api.query.FieldKey;
4239
import org.labkey.api.query.QueryService;
4340
import org.labkey.api.security.User;
44-
import org.labkey.api.security.permissions.AbstractContainerScopingTest;
45-
import org.labkey.api.security.roles.EditorRole;
46-
import org.labkey.api.security.roles.ReaderRole;
47-
import org.labkey.api.view.ActionURL;
4841
import org.labkey.api.study.Dataset;
4942
import org.labkey.api.study.Study;
5043
import org.labkey.api.study.StudyService;
5144
import org.labkey.api.util.PageFlowUtil;
5245
import org.labkey.api.util.Pair;
5346
import org.labkey.nab.query.NabProtocolSchema;
5447
import org.labkey.nab.query.NabRunDataTable;
55-
import org.springframework.mock.web.MockHttpServletResponse;
5648

5749
import java.sql.SQLException;
5850
import java.util.Collection;
@@ -214,70 +206,4 @@ public void getDataPropertiesFromNabRunData(NabRunDataTable nabRunDataTable, Str
214206
throw new RuntimeSQLException(e);
215207
}
216208
}
217-
218-
public static class ContainerScopingTestCase extends AbstractContainerScopingTest
219-
{
220-
private Container _folderA;
221-
private Container _folderB;
222-
private User _admin;
223-
224-
@Before
225-
public void setup()
226-
{
227-
_admin = getAdmin();
228-
_folderA = createContainer("A");
229-
_folderB = createContainer("B");
230-
}
231-
232-
@Test
233-
public void testDeleteRunContainerScoping() throws Exception
234-
{
235-
// Regression test for NAB-1. NabAssayController.DeleteRunAction resolves a
236-
// run via ExperimentService.getExpRun(rowId) from a global rowId and then deletes it.
237-
ExpProtocol protocol = ExperimentService.get().ensureSampleDerivationProtocol(_admin);
238-
ExpRun run = ExperimentService.get().createExperimentRun(_folderB, "scope-test-run");
239-
run.setProtocol(protocol);
240-
run.save(_admin);
241-
int rowId = run.getRowId();
242-
243-
ActionURL foreignUrl = new ActionURL(NabAssayController.DeleteRunAction.class, _folderA)
244-
.addParameter("rowId", rowId);
245-
246-
User deleterAonly = createUserInRole(_folderA, EditorRole.class);
247-
assertStatus(HttpServletResponse.SC_NOT_FOUND, post(foreignUrl, deleterAonly));
248-
assertNotNull("Foreign-container run must not be deleted", ExperimentService.get().getExpRun(rowId));
249-
250-
ActionURL ownUrl = new ActionURL(NabAssayController.DeleteRunAction.class, _folderB)
251-
.addParameter("rowId", rowId);
252-
assertStatus(HttpServletResponse.SC_FOUND, post(ownUrl, _admin));
253-
assertNull("Run should have been deleted from its own container", ExperimentService.get().getExpRun(rowId));
254-
}
255-
256-
@Test
257-
public void testDownloadDatafileContainerScoping() throws Exception
258-
{
259-
// Regression test for NAB-2. NabAssayController.DownloadDatafileAction resolves a run via
260-
// ExperimentService.getExpRun(rowId) from a global rowId
261-
ExpProtocol protocol = ExperimentService.get().ensureSampleDerivationProtocol(_admin);
262-
ExpRun run = ExperimentService.get().createExperimentRun(_folderB, "scope-test-download-run");
263-
run.setProtocol(protocol);
264-
run.save(_admin);
265-
int rowId = run.getRowId();
266-
267-
User readerAonly = createUserInRole(_folderA, ReaderRole.class);
268-
ActionURL foreignUrl = new ActionURL(NabAssayController.DownloadDatafileAction.class, _folderA)
269-
.addParameter("rowId", rowId);
270-
MockHttpServletResponse foreignResponse = get(foreignUrl, readerAonly);
271-
assertStatus(HttpServletResponse.SC_NOT_FOUND, foreignResponse);
272-
assertTrue("Foreign-container download must be rejected by the container check",
273-
foreignResponse.getContentAsString().contains("does not exist"));
274-
275-
ActionURL ownUrl = new ActionURL(NabAssayController.DownloadDatafileAction.class, _folderB)
276-
.addParameter("rowId", rowId);
277-
MockHttpServletResponse ownResponse = get(ownUrl, _admin);
278-
assertStatus(HttpServletResponse.SC_NOT_FOUND, ownResponse);
279-
assertTrue("Same-container request should advance past the container check to the data handler",
280-
ownResponse.getContentAsString().contains("is not a NAb run"));
281-
}
282-
}
283209
}

nab/src/org/labkey/nab/NabModule.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,4 @@ public Set<Class> getUnitTests()
122122
{
123123
return Set.of(PlateParserTests.class);
124124
}
125-
126-
@NotNull
127-
@Override
128-
public Set<Class> getIntegrationTests()
129-
{
130-
return Set.of(NabManager.ContainerScopingTestCase.class);
131-
}
132125
}

0 commit comments

Comments
 (0)