Skip to content

Commit 691c82b

Browse files
committed
merge branch 'develop' into the branch
'fb_calendar_based_grouping_1209'
2 parents f449536 + ab40538 commit 691c82b

13 files changed

Lines changed: 275 additions & 64 deletions

File tree

pull_request_template.md

Lines changed: 0 additions & 8 deletions
This file was deleted.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- Drop the AuditLog view so targetedms-create.sql (runs after this script) rebuilds it with the fixed recursive CTE.
2+
SELECT core.fn_dropifexists('AuditLog', 'targetedms', 'VIEW', NULL);

resources/schemas/dbscripts/postgresql/targetedms-create.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ WITH RECURSIVE logTree as (
1919
, nxt.entryHash
2020
, nxt.parentEntryHash
2121
FROM targetedms.AuditLogEntry nxt
22-
JOIN logTree prev ON prev.parentEntryHash = nxt.entryHash
22+
JOIN logTree prev ON prev.entryHash = nxt.parentEntryHash
2323
)
2424
SELECT t.entryId
2525
, e.documentguid

src/org/labkey/targetedms/SkylineAuditLogManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,7 @@ public void testEntryRetrieval() throws AuditLogException
593593
{
594594
AuditLogTree node = new SkylineAuditLogManager(_container, null).buildLogTree(_docGUID);
595595
while(node.iterator().hasNext()){
596-
AuditLogEntry ent = AuditLogEntry.retrieve(node.getEntryId());
596+
AuditLogEntry ent = AuditLogEntry.retrieve(node.getEntryId(), _container);
597597
assertNotNull(ent);
598598
node = node.iterator().next();
599599
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright (c) 2026 LabKey Corporation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.labkey.targetedms;
17+
18+
import jakarta.servlet.http.HttpServletResponse;
19+
import org.apache.logging.log4j.Logger;
20+
import org.junit.After;
21+
import org.junit.Before;
22+
import org.junit.Test;
23+
import org.labkey.api.data.Container;
24+
import org.labkey.api.data.SimpleFilter;
25+
import org.labkey.api.data.Table;
26+
import org.labkey.api.data.TableSelector;
27+
import org.labkey.api.exp.api.ExpRun;
28+
import org.labkey.api.exp.api.ExperimentService;
29+
import org.labkey.api.pipeline.PipeRoot;
30+
import org.labkey.api.pipeline.PipelineService;
31+
import org.labkey.api.query.FieldKey;
32+
import org.labkey.api.security.permissions.AbstractContainerScopingTest;
33+
import org.labkey.api.util.GUID;
34+
import org.labkey.api.util.logging.LogHelper;
35+
import org.labkey.api.view.ActionURL;
36+
import org.labkey.api.view.ViewBackgroundInfo;
37+
import org.labkey.targetedms.parser.skyaudit.UnitTestUtil;
38+
39+
import java.io.File;
40+
import java.util.Collections;
41+
import java.util.List;
42+
import java.util.Set;
43+
44+
/**
45+
* Container-scoping integration tests for {@link TargetedMSController} actions that resolve an object by a
46+
* global id. Each test sets up the same object in one folder and confirms the action rejects a request that addresses
47+
* via the wrong container
48+
*/
49+
public class TargetedMSContainerScopingTest extends AbstractContainerScopingTest
50+
{
51+
private static final Logger LOG = LogHelper.getLogger(TargetedMSContainerScopingTest.class, "TargetedMS container scoping tests");
52+
private static final GUID DOC_GUID = new GUID("8f1c2a44-3c5e-4f0a-9d2b-6e7a1b3c5d9e");
53+
54+
@Before
55+
@After
56+
public void cleanupAuditLog()
57+
{
58+
UnitTestUtil.cleanupDatabase(DOC_GUID);
59+
}
60+
61+
/**
62+
* RemoveLinkVersionAction relinks a Skyline document-version chain by run rowId. It must reject a run that lives in
63+
* a different container (mirroring its sibling SaveLinkVersionsAction), otherwise an editor in one folder could
64+
* rewrite the version chain of documents they can't see.
65+
*/
66+
@Test
67+
public void removeLinkVersionIsContainerScoped() throws Exception
68+
{
69+
Container owner = createContainer("LinkOwner");
70+
Container other = createContainer("LinkOther");
71+
72+
// Negative: a run that lives in 'owner' cannot be relinked through the 'other' folder
73+
ExpRun foreignRun = createRun(owner, "foreign-version");
74+
ActionURL foreign = new ActionURL(TargetedMSController.RemoveLinkVersionAction.class, other)
75+
.addParameter("rowId", foreignRun.getRowId());
76+
assertStatus(HttpServletResponse.SC_BAD_REQUEST, post(foreign, getAdmin()));
77+
78+
// Positive: a run that lives in the acting folder is accepted
79+
ExpRun localRun = createRun(other, "local-version");
80+
ActionURL local = new ActionURL(TargetedMSController.RemoveLinkVersionAction.class, other)
81+
.addParameter("rowId", localRun.getRowId());
82+
assertStatus(HttpServletResponse.SC_OK, post(local, getAdmin()));
83+
}
84+
85+
/**
86+
* ShowSkylineAuditLogExtraInfoAJAXAction renders the full Skyline audit detail (user, timestamp, extra info) for an
87+
* audit entry resolved by global EntryId. It must only render entries whose owning run is in the requested folder,
88+
* otherwise any reader could read another folder's document audit history by guessing entry ids.
89+
*/
90+
@Test
91+
public void auditLogExtraInfoIsContainerScoped() throws Exception
92+
{
93+
Container owner = createContainer("AuditOwner");
94+
Container other = createContainer("AuditOther");
95+
96+
int entryId = importAuditLogEntry(owner);
97+
98+
// Negative: the entry's run lives in 'owner', so requesting it via 'other' must 404 rather than leak detail
99+
ActionURL foreign = new ActionURL(TargetedMSController.ShowSkylineAuditLogExtraInfoAJAXAction.class, other)
100+
.addParameter("entryId", entryId);
101+
assertStatus(HttpServletResponse.SC_NOT_FOUND, get(foreign, getAdmin()));
102+
103+
// Positive: requesting the entry through its owning folder renders normally
104+
ActionURL owned = new ActionURL(TargetedMSController.ShowSkylineAuditLogExtraInfoAJAXAction.class, owner)
105+
.addParameter("entryId", entryId);
106+
assertStatus(HttpServletResponse.SC_OK, get(owned, getAdmin()));
107+
}
108+
109+
/** Persist a minimal, saved {@link ExpRun} in the given container so the controller can resolve it by rowId. */
110+
private ExpRun createRun(Container c, String name) throws Exception
111+
{
112+
ExperimentService svc = ExperimentService.get();
113+
ExpRun run = svc.createExperimentRun(c, name);
114+
PipeRoot pipeRoot = PipelineService.get().findPipelineRoot(c);
115+
assertNotNull("No pipeline root for " + c.getPath(), pipeRoot);
116+
run.setFilePathRoot(pipeRoot.getRootPath());
117+
run.setProtocol(svc.ensureSampleDerivationProtocol(getAdmin()));
118+
ViewBackgroundInfo info = new ViewBackgroundInfo(c, getAdmin(), null);
119+
return svc.saveSimpleExperimentRun(run, Collections.emptyMap(), Collections.emptyMap(),
120+
Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap(), info, null, false);
121+
}
122+
123+
/** Insert a TargetedMS run in the given container, import a sample Skyline audit log for it, and return an EntryId. */
124+
private int importAuditLogEntry(Container c) throws Exception
125+
{
126+
TargetedMSRun run = new TargetedMSRun();
127+
run.setContainer(c);
128+
run.setDocumentGUID(DOC_GUID);
129+
Table.insert(getAdmin(), TargetedMSManager.getTableInfoRuns(), run);
130+
131+
File zip = UnitTestUtil.getSampleDataFile("AuditLogFiles/MethodEdit_v1.zip");
132+
File logFile = UnitTestUtil.extractLogFromZip(zip, LOG);
133+
new SkylineAuditLogManager(c, null).importAuditLogFile(getAdmin(), logFile, DOC_GUID, run);
134+
135+
List<Integer> entryIds = new TableSelector(
136+
TargetedMSManager.getTableInfoSkylineRunAuditLogEntry(),
137+
Collections.singleton("AuditLogEntryId"),
138+
new SimpleFilter(FieldKey.fromParts("VersionId"), run.getId()), null)
139+
.getArrayList(Integer.class);
140+
assertFalse("No audit log entries were imported", entryIds.isEmpty());
141+
return entryIds.get(0);
142+
}
143+
}

src/org/labkey/targetedms/TargetedMSController.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5125,7 +5125,9 @@ public static class ShowSkylineAuditLogExtraInfoAJAXAction extends SimpleViewAct
51255125
@Override
51265126
public ModelAndView getView(SkylineAuditLogExtraInfoForm form, BindException errors)
51275127
{
5128-
AuditLogEntry ent = AuditLogEntry.retrieve(form.getEntryId());
5128+
AuditLogEntry ent = AuditLogEntry.retrieve(form.getEntryId(), getContainer());
5129+
if (ent == null)
5130+
throw new NotFoundException("No audit log entry found for id " + form.getEntryId() + " in this folder.");
51295131
getPageConfig().setTemplate(PageConfig.Template.None);
51305132
return new JspView<>("/org/labkey/targetedms/view/skylineAuditLogExtraInfoView.jsp", ent);
51315133
}
@@ -7821,7 +7823,7 @@ public void validateForm(RowIdForm form, Errors errors)
78217823
// verify that the run rowId is valid and matches an existing run
78227824
// and if the run replaces any other runs, it should only replace one
78237825
ExpRun run = ExperimentService.get().getExpRun(form.getRowId());
7824-
if (run == null)
7826+
if (run == null || !run.getContainer().equals(getContainer()))
78257827
errors.reject(ERROR_MSG, "No run found for id " + form.getRowId() + ".");
78267828
else if (!run.getReplacesRuns().isEmpty() && run.getReplacesRuns().size() > 1)
78277829
errors.reject(ERROR_MSG, "Run " + form.getRowId() + " replaces more than one run.");

src/org/labkey/targetedms/TargetedMSModule.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ public String getName()
228228
@Override
229229
public Double getSchemaVersion()
230230
{
231-
return 26.008;
231+
return 26.009;
232232
}
233233

234234
@Override
@@ -693,7 +693,8 @@ protected void startupAfterSpringConfig(ModuleContext moduleContext)
693693
{
694694
return Set.of(
695695
MsDataSourceUtil.TestCase.class,
696-
SkylineAuditLogManager.TestCase.class
696+
SkylineAuditLogManager.TestCase.class,
697+
TargetedMSContainerScopingTest.class
697698
);
698699
}
699700

src/org/labkey/targetedms/outliers/OutlierGenerator.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.labkey.api.data.TableSelector;
3030
import org.labkey.api.query.QueryService;
3131
import org.labkey.api.security.User;
32+
import org.labkey.api.sql.LabKeySql;
3233
import org.labkey.api.targetedms.model.SampleFileInfo;
3334
import org.labkey.api.util.Pair;
3435
import org.labkey.targetedms.TargetedMSManager;
@@ -130,7 +131,7 @@ else if (configuration.getAnnotationName() != null)
130131
sql.append("(SELECT PrecursorChromInfoId, SampleFileId, ");
131132
sql.append(" CAST(IFDEFINED(SeriesLabel) AS VARCHAR) AS SeriesLabel, ");
132133
sql.append("\nMetricValue, ").append(configuration.getId()).append(" AS MetricId");
133-
sql.append("\n FROM ").append(schemaName).append('.').append(queryName);
134+
sql.append("\n FROM ").append(schemaName).append('.').append(LabKeySql.quoteIdentifier(queryName));
134135
sql.append(")");
135136
}
136137
return sql.toString();

src/org/labkey/targetedms/parser/skyaudit/AuditLogEntry.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
*/
1616
package org.labkey.targetedms.parser.skyaudit;
1717

18+
import org.labkey.api.data.Container;
1819
import org.labkey.api.data.SimpleFilter;
1920
import org.labkey.api.data.Table;
2021
import org.labkey.api.data.TableSelector;
2122
import org.labkey.api.query.FieldKey;
2223
import org.labkey.api.security.User;
2324
import org.labkey.api.util.GUID;
2425
import org.labkey.targetedms.TargetedMSManager;
26+
import org.labkey.targetedms.TargetedMSRun;
2527

2628
import java.math.BigDecimal;
2729
import java.math.BigInteger;
@@ -70,13 +72,25 @@ public AuditLogEntry(BigDecimal documentFormatVersion)
7072
_documentFormatVersion = documentFormatVersion;
7173
}
7274

73-
public static AuditLogEntry retrieve(int pEntryId)
75+
/**
76+
* Container-scoped retrieve. Returns the entry only if its owning Skyline run lives in the supplied container,
77+
* preventing callers from reading audit detail for documents in folders the user can't access. An entryId can
78+
* map to more than one run when documents share an audit history, so check every match for one in the container.
79+
*/
80+
public static AuditLogEntry retrieve(int pEntryId, Container container)
7481
{
7582
TableSelector sel = new TableSelector(TargetedMSManager.getTableInfoSkylineAuditLog(), new SimpleFilter(FieldKey.fromParts("EntryId"), pEntryId), null);
76-
List<AuditLogEntry> results = sel.getArrayList(AuditLogEntry.class);
77-
// Possible to get more than one match if two documents share an audit history. In this case, we don't care
78-
// which we use
79-
return results.isEmpty() ? null : results.getFirst();
83+
for (AuditLogEntry entry : sel.getArrayList(AuditLogEntry.class))
84+
{
85+
Long runId = entry.getRunId();
86+
if (runId != null)
87+
{
88+
TargetedMSRun run = TargetedMSManager.getRun(runId);
89+
if (run != null && container.equals(run.getContainer()))
90+
return entry;
91+
}
92+
}
93+
return null;
8094
}
8195

8296
public AuditLogTree getTreeEntry(){

src/org/labkey/targetedms/view/pharmacokinetics.jsp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
%>
5252
<labkey:panel title="<%=subgroupTitle%>" type="portal">
5353
<div id="targetedms-fom-export" class="export-icon" data-toggle="tooltip" title="Export to Excel">
54-
<%=iconLink("fa fa-file-excel-o", "", null).onClick("exportExcel('" + subgroup + "')") %>
54+
<%=iconLink("fa fa-file-excel-o", "", null).onClick("exportExcel(" + q(subgroup) + ")") %>
5555
</div>
5656

5757
<labkey:panel title="Statistics">
@@ -81,7 +81,7 @@
8181
<span id="nonIVC0Controls-Warn-<%=h(subgroup)%>" class="labkey-error"><h4>WARNING: Please enter a non-IV C0 and recalculate.</h4></span>
8282
non-IV C0
8383
<input type="number" id="nonIVC0-<%=h(subgroup)%>" label="non-IV C0"/>
84-
<% addHandler("btnNonIVC0-"+h(subgroup), "click", "updateStatsForNonIVC0('"+ subgroup + "')"); %>
84+
<% addHandler("btnNonIVC0-"+h(subgroup), "click", "updateStatsForNonIVC0("+ q(subgroup) + ")"); %>
8585
<button id="btnNonIVC0-<%=h(subgroup)%>">Recalculate</button>
8686
</div>
8787
</labkey:panel>

0 commit comments

Comments
 (0)