Skip to content

Commit 17daf35

Browse files
committed
Merge remote-tracking branch 'origin/develop' into fb_deriveActions
2 parents f4236a6 + 476bb5a commit 17daf35

11 files changed

Lines changed: 147 additions & 77 deletions

File tree

api/src/org/labkey/api/admin/AdminUrls.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ public interface AdminUrls extends UrlProvider
6565
ActionURL getSessionLoggingURL();
6666
ActionURL getTrackedAllocationsViewerURL();
6767
ActionURL getSystemMaintenanceURL();
68-
ActionURL getCspReportToURL(String cspVersion);
68+
ActionURL getCspReportToURL();
69+
70+
ActionURL getAllowedExternalRedirectHostsURL();
6971

7072
/**
7173
* Simply adds an "Admin Console" link to nav trail if invoked in the root container. Otherwise, root is unchanged.

api/src/org/labkey/api/exp/api/ExpProtocol.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ enum Status
9999
String getContact();
100100

101101
List<? extends ExpProtocol> getChildProtocols();
102-
List<? extends ExpExperiment> getBatches();
102+
List<? extends ExpExperiment> getBatches(@Nullable Container c);
103103

104104
void setEntityId(String entityId);
105105
String getEntityId();

api/src/org/labkey/api/reports/ReportService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747

4848
public interface ReportService
4949
{
50+
String R_REPORT_CUSTOM_SHARING = "rReportCustomSharing";
51+
5052
// this logger is to enable all report loggers in the admin ui (org.labkey.api.reports.*)
5153
@SuppressWarnings({"UnusedDeclaration", "SSBasedInspection"})
5254
Logger packageLogger = LogManager.getLogger(ReportService.class.getPackageName());

api/src/org/labkey/api/reports/report/r/RReport.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.labkey.api.rstudio.RStudioService;
4747
import org.labkey.api.security.SecurityManager;
4848
import org.labkey.api.security.User;
49+
import org.labkey.api.settings.OptionalFeatureService;
4950
import org.labkey.api.thumbnail.Thumbnail;
5051
import org.labkey.api.util.FileUtil;
5152
import org.labkey.api.util.PageFlowUtil;
@@ -74,6 +75,8 @@
7475
import java.util.Map;
7576
import java.util.Set;
7677

78+
import static org.labkey.api.reports.ReportService.R_REPORT_CUSTOM_SHARING;
79+
7780
public class RReport extends ExternalScriptEngineReport
7881
{
7982
public static final String TYPE = "ReportService.rReport";
@@ -925,8 +928,12 @@ public String getEditAreaSyntax()
925928
@Override
926929
public boolean allowShareButton(User user, Container container)
927930
{
928-
// allow sharing if this R report is a DB report and the user canShare
929-
return !getDescriptor().isModuleBased() && canShare(user, container);
931+
if (OptionalFeatureService.get().isFeatureEnabled(R_REPORT_CUSTOM_SHARING))
932+
{
933+
// allow sharing if this R report is a DB report and the user canShare
934+
return !getDescriptor().isModuleBased() && canShare(user, container);
935+
}
936+
return false;
930937
}
931938

932939
public static class TestCase extends Assert

api/src/org/labkey/filters/ContentSecurityPolicyFilter.java

Lines changed: 79 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import jakarta.servlet.http.HttpServletResponse;
1111
import org.apache.commons.collections4.SetValuedMap;
1212
import org.apache.commons.collections4.multimap.HashSetValuedHashMap;
13+
import org.apache.commons.lang3.EnumUtils;
1314
import org.apache.commons.lang3.StringUtils;
1415
import org.apache.commons.lang3.Strings;
1516
import org.apache.logging.log4j.Logger;
@@ -19,7 +20,6 @@
1920
import org.junit.Test;
2021
import org.labkey.api.admin.AdminUrls;
2122
import org.labkey.api.collections.CopyOnWriteHashMap;
22-
import org.labkey.api.collections.LabKeyCollectors;
2323
import org.labkey.api.security.Directive;
2424
import org.labkey.api.settings.AppProps;
2525
import org.labkey.api.settings.OptionalFeatureService;
@@ -42,6 +42,8 @@
4242
import java.util.Map;
4343
import java.util.Objects;
4444
import java.util.Set;
45+
import java.util.regex.Matcher;
46+
import java.util.regex.Pattern;
4547
import java.util.stream.Collectors;
4648

4749

@@ -95,6 +97,11 @@ public String getHeaderName()
9597
{
9698
return _headerName;
9799
}
100+
101+
private static @Nullable ContentSecurityPolicyType get(String disposition)
102+
{
103+
return EnumUtils.getEnumIgnoreCase(ContentSecurityPolicyType.class, disposition);
104+
}
98105
}
99106

100107
static
@@ -119,8 +126,9 @@ public void init(FilterConfig filterConfig) throws ServletException
119126
String paramValue = filterConfig.getInitParameter(paramName);
120127
if ("policy".equalsIgnoreCase(paramName))
121128
{
129+
// Extract before filtering since CSP version is in a comment
130+
extractCspVersion(paramValue);
122131
_stashedTemplate = filterPolicy(paramValue);
123-
extractCspVersion(_stashedTemplate);
124132
}
125133
else if ("disposition".equalsIgnoreCase(paramName))
126134
{
@@ -139,12 +147,12 @@ else if ("disposition".equalsIgnoreCase(paramName))
139147
if (CSP_FILTERS.put(getType(), this) != null)
140148
throw new ServletException("ContentSecurityPolicyFilter is misconfigured, duplicate policies of type: " + getType());
141149

142-
// configure a different endpoint for each type to convey the correct csp version (eXX vs. rXX)
150+
// configure a different endpoint for each type. TODO: We only need one CSP violation reporting endpoint now, so one header would do
143151
_reportToEndpointName = "csp-" + getType().name().toLowerCase();
144152
}
145153

146154
/** Filter out block comments and replace special characters in the provided policy */
147-
public static String filterPolicy(String policy)
155+
private static String filterPolicy(String policy)
148156
{
149157
String s = policy.trim();
150158
s = s.replace( '\n', ' ' );
@@ -164,40 +172,24 @@ public static String filterPolicy(String policy)
164172
return s;
165173
}
166174

175+
private static final Pattern CSP_VERSION_PATTERN = Pattern.compile("cspVersion\\s*=\\s*(\\w+)");
176+
167177
/**
168-
* Extract the cspVersion parameter value from the report-uri directive, if possible. Otherwise, cspVersion is left
169-
* as "Unknown". This value is reported as part of usage metrics.
178+
* Extract the cspVersion value from a comment in the CSP, if it exists. Otherwise, cspVersion is left as "Unknown".
179+
* This value is reported as part of usage metrics and included in violation reports that are logged and forwarded.
170180
*/
171181
private void extractCspVersion(String s)
172182
{
173-
// Simple parser that should be compliant with https://www.w3.org/TR/CSP3/#parse-serialized-policy
174-
Map<String, String> cspMap = Arrays.stream(s.split(";"))
175-
.map(String::trim)
176-
.filter(line -> !line.isEmpty())
177-
.map(line -> line.split("\\s+", 2))
178-
.filter(parts -> parts.length == 2)
179-
.collect(LabKeyCollectors.toCaseInsensitiveLinkedMap(parts -> parts[0], parts -> parts[1]));
180-
181-
String directive = "report-uri";
182-
String reportUri = cspMap.get(directive);
183-
184-
if (reportUri != null)
183+
Matcher matcher = CSP_VERSION_PATTERN.matcher(s);
184+
if (matcher.find())
185185
{
186-
try
187-
{
188-
ActionURL reportUrl = new ActionURL(reportUri);
189-
String cspVersion = reportUrl.getParameter("cspVersion");
186+
_cspVersion = matcher.group(1);
190187

191-
if (null != cspVersion)
192-
_cspVersion = cspVersion;
193-
}
194-
catch (IllegalArgumentException e)
195-
{
196-
LOG.warn("Unable to parse {} URI", directive, e);
197-
}
198-
}
188+
if (matcher.find())
189+
LOG.warn("More than one cspVersion=XX assignment found; using the first one.");
199190

200-
LOG.debug("CspVersion: {}", getCspVersion());
191+
LOG.debug("CspVersion: {}", getCspVersion());
192+
}
201193
}
202194

203195
@Override
@@ -277,7 +269,7 @@ private CspFilterSettings(ContentSecurityPolicyFilter filter, String baseServerU
277269
{
278270
// Each filter adds its own "Reporting-Endpoints" header since we want to convey the correct version (eXX vs. rXX)
279271
@SuppressWarnings("DataFlowIssue")
280-
ActionURL violationUrl = PageFlowUtil.urlProvider(AdminUrls.class).getCspReportToURL(filter.getCspVersion());
272+
ActionURL violationUrl = PageFlowUtil.urlProvider(AdminUrls.class).getCspReportToURL();
281273
// Use an absolute URL so we always post to https:, even if the violating request uses http:
282274
_reportingEndpointsHeaderValue = filter.getReportToEndpointName() + "=\"" + violationUrl.getURIString() + "\"";
283275

@@ -406,6 +398,34 @@ public static boolean hasCsp(ContentSecurityPolicyType type)
406398
return CSP_FILTERS.get(type) != null;
407399
}
408400

401+
public static @NotNull String getCspVersion(@Nullable String disposition)
402+
{
403+
if (disposition != null)
404+
{
405+
ContentSecurityPolicyType type = ContentSecurityPolicyType.get(disposition);
406+
407+
if (type != null)
408+
{
409+
var filter = CSP_FILTERS.get(type);
410+
411+
if (null != filter)
412+
{
413+
return filter.getCspVersion();
414+
}
415+
else
416+
{
417+
LOG.error("Disposition {} doesn't match a configured CSP filter", disposition);
418+
}
419+
}
420+
else
421+
{
422+
LOG.error("Bad disposition: {}", disposition);
423+
}
424+
}
425+
426+
return "Unknown";
427+
}
428+
409429
public static List<String> getMissingSubstitutions(ContentSecurityPolicyType type)
410430
{
411431
ContentSecurityPolicyFilter filter = CSP_FILTERS.get(type);
@@ -442,6 +462,32 @@ public static void registerMetricsProvider()
442462

443463
public static class TestCase extends Assert
444464
{
465+
@Test
466+
public void testCspVersionExtraction()
467+
{
468+
testCspExtract("e14", "/* cspVersion=e14 */");
469+
testCspExtract("r14", "/*cspVersion=r14 */");
470+
testCspExtract("e15", "/* cspVersion = e15 */");
471+
testCspExtract("r15", "/* cspVersion=r15*/");
472+
testCspExtract("e15", "/* cspVersion = e15*/");
473+
testCspExtract("e15", "/* cspVersion = e15*/ /* cspVersion=XXX */");
474+
475+
testCspExtract("Unknown", "");
476+
testCspExtract("Unknown", " ");
477+
testCspExtract("Unknown", "/* cspVersin=e14 */");
478+
testCspExtract("Unknown", "/* cspVersion */");
479+
testCspExtract("Unknown", "/* cspVersion= */");
480+
testCspExtract("Unknown", "/* cspVersion=*/");
481+
testCspExtract("Unknown", "/* cspVersion== */");
482+
}
483+
484+
private void testCspExtract(String expected, String csp)
485+
{
486+
ContentSecurityPolicyFilter filter = new ContentSecurityPolicyFilter();
487+
filter.extractCspVersion(csp);
488+
assertEquals(expected, filter.getCspVersion());
489+
}
490+
445491
@Test
446492
public void testPolicyFiltering()
447493
{
@@ -461,7 +507,7 @@ public void testPolicyFiltering()
461507
report-uri /* Whoa! */ /admin-contentsecuritypolicyreport.api?${CSP.REPORT.PARAMS} https://*;
462508
""";
463509

464-
// Multi-line for readability, but notice that newlines are replaced before assignment
510+
// Multi-line for readability, but notice that newlines are replaced when constructing the expected string
465511
String expected = """
466512
default-src 'self' https: http: ;
467513
connect-src 'self' http://www.labkey.org localhost:* ws: ${LABKEY.ALLOWED.CONNECTIONS} ;

assay/src/org/labkey/assay/AssayManager.java

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -378,10 +378,16 @@ public AssaySchema createSchema(User user, Container container, @Nullable Contai
378378

379379
@Override
380380
public @NotNull List<ExpProtocol> getAssayProtocols(Container container)
381+
{
382+
return getAssayProtocols(container, false);
383+
}
384+
385+
private @NotNull List<ExpProtocol> getAssayProtocols(Container container, boolean currentOnly)
381386
{
382387
List<ExpProtocol> allProtocols = new ArrayList<>();
383388

384-
for (Container containerInScope : container.getContainersFor(ContainerType.DataType.protocol))
389+
Collection<Container> containerScopes = currentOnly ? List.of(container) : container.getContainersFor(ContainerType.DataType.protocol);
390+
for (Container containerInScope : containerScopes)
385391
{
386392
List<ExpProtocol> ids = PROTOCOL_CACHE.get(containerInScope);
387393
allProtocols.addAll(ids);
@@ -622,14 +628,14 @@ public ExpExperiment findBatch(ExpRun run)
622628
}
623629

624630
/**
625-
* Creates a single document per assay design/folder combo, with some simple assay info (name, description), plus
626-
* the names and comments from all the runs.
631+
* Creates a single document per assay design, with some simple assay info (name, description).
632+
* Note: the document for an assay protocol will just be associated with the protocol's container
627633
*/
628634
@Override
629635
public void indexAssays(SearchService.TaskIndexingQueue queue)
630636
{
631-
List<ExpProtocol> protocols = getAssayProtocols(queue.getContainer());
632-
637+
// GitHub Issue 895: for the assay protocol search document just user the current container's protocols
638+
List<ExpProtocol> protocols = getAssayProtocols(queue.getContainer(), true);
633639
for (ExpProtocol protocol : protocols)
634640
indexAssay(queue, protocol);
635641
}
@@ -661,22 +667,6 @@ public void indexAssay(SearchService.TaskIndexingQueue queue, ExpProtocol protoc
661667
m.put(SearchService.PROPERTY.keywordsMed.toString(), keywords);
662668
m.put(SearchService.PROPERTY.categories.toString(), ASSAY_CATEGORY.getName());
663669

664-
ExperimentService.get().getExpRuns(c, protocol, null)
665-
.forEach(run -> {
666-
StringBuilder runKeywords = new StringBuilder();
667-
668-
runKeywords.append(" ");
669-
runKeywords.append(run.getName());
670-
671-
if (null != run.getComments())
672-
{
673-
runKeywords.append(" ");
674-
runKeywords.append(run.getComments());
675-
}
676-
677-
body.append(runKeywords);
678-
});
679-
680670
String docId = protocol.getDocumentId();
681671
WebdavResource r = new SimpleDocumentResource(new Path(docId), docId, c.getEntityId(), "text/plain", body.toString(), assayBeginURL, createdBy, created, modifiedBy, modified, m);
682672
queue.addResource(r);
@@ -728,7 +718,7 @@ private void indexAssayBatches(SearchService.TaskIndexingQueue queue, ExpProtoco
728718
{
729719
if (shouldIndexProtocolBatches(protocol))
730720
{
731-
for (ExpExperiment batch : protocol.getBatches())
721+
for (ExpExperiment batch : protocol.getBatches(queue.getContainer()))
732722
{
733723
if (modifiedSince == null || modifiedSince.before(batch.getModified()))
734724
indexAssayBatch(queue, batch);

core/src/org/labkey/core/admin/AdminController.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -889,10 +889,16 @@ public ActionURL getSystemMaintenanceURL()
889889
}
890890

891891
@Override
892-
public ActionURL getCspReportToURL(@NotNull String cspVersion)
892+
public ActionURL getCspReportToURL()
893893
{
894-
return new ActionURL(ContentSecurityPolicyReportToAction.class, ContainerManager.getRoot())
895-
.addParameter("cspVersion", cspVersion);
894+
return new ActionURL(ContentSecurityPolicyReportToAction.class, ContainerManager.getRoot());
895+
}
896+
897+
@Override
898+
public ActionURL getAllowedExternalRedirectHostsURL()
899+
{
900+
return new ActionURL(AllowListAction.class, ContainerManager.getRoot())
901+
.addParameter("type", AllowListType.Redirect.name());
896902
}
897903

898904
public static ActionURL getDeprecatedFeaturesURL()
@@ -12285,6 +12291,7 @@ protected boolean handleOneReport(JSONObject jsonObj, HttpServletRequest request
1228512291
if (!forwarded)
1228612292
{
1228712293
jsonObj.put("labkeyVersion", AppProps.getInstance().getReleaseVersion());
12294+
jsonObj.put("cspVersion", ContentSecurityPolicyFilter.getCspVersion(cspReport.optString("disposition", null)));
1228812295
User user = getUser();
1228912296
String email = null;
1229012297
// If the user is not logged in, we may still be able to snag the email address from our cookie
@@ -12299,9 +12306,6 @@ protected boolean handleOneReport(JSONObject jsonObj, HttpServletRequest request
1229912306
jsonObj.put("ip", ipAddress);
1230012307
if (isNotBlank(userAgent) && !jsonObj.has("user_agent"))
1230112308
jsonObj.put("user_agent", userAgent);
12302-
String cspVersion = request.getParameter("cspVersion");
12303-
if (null != cspVersion)
12304-
jsonObj.put("cspVersion", cspVersion);
1230512309
}
1230612310

1230712311
var jsonStr = jsonObj.toString(2);

0 commit comments

Comments
 (0)