Skip to content

Commit 120a1a6

Browse files
Merge branch 'main' into feat/issue-26712-hive-partition-key-flag
2 parents c309045 + 64e254d commit 120a1a6

File tree

10 files changed

+665
-36
lines changed

10 files changed

+665
-36
lines changed

conf/openmetadata.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -689,7 +689,7 @@ web:
689689
block: ${WEB_CONF_XSS_PROTECTION_BLOCK:-true}
690690
csp:
691691
enabled: ${WEB_CONF_XSS_CSP_ENABLED:-false}
692-
policy: ${WEB_CONF_XSS_CSP_POLICY:-"default-src 'self'"}
692+
policy: ${WEB_CONF_XSS_CSP_POLICY:-"default-src 'self'; base-uri 'self'; script-src 'self' 'nonce-__CSP_NONCE__' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com data:; img-src * 'self' blob: data:; media-src * 'self' blob:; worker-src 'self' blob:; frame-src 'self' https://www.youtube.com; object-src 'none'; connect-src 'self';"}
693693
reportOnlyPolicy: ${WEB_CONF_XSS_CSP_REPORT_ONLY_POLICY:-""}
694694
referrer-policy:
695695
enabled: ${WEB_CONF_REFERRER_POLICY_ENABLED:-false}

openmetadata-service/src/main/java/org/openmetadata/service/OpenMetadataApplication.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@
150150
import org.openmetadata.service.security.AuthenticationCodeFlowHandler;
151151
import org.openmetadata.service.security.Authorizer;
152152
import org.openmetadata.service.security.ContainerRequestFilterManager;
153+
import org.openmetadata.service.security.CspNonceHandler;
153154
import org.openmetadata.service.security.DelegatingContainerRequestFilter;
154155
import org.openmetadata.service.security.NoopAuthorizer;
155156
import org.openmetadata.service.security.NoopFilter;
@@ -1056,6 +1057,10 @@ private void registerResources(
10561057
OMErrorPageHandler eph = new OMErrorPageHandler(config.getWebConfiguration());
10571058
eph.addErrorPage(Response.Status.NOT_FOUND.getStatusCode(), "/");
10581059
environment.getApplicationContext().setErrorHandler(eph);
1060+
1061+
CspNonceHandler cspNonceHandler = new CspNonceHandler();
1062+
cspNonceHandler.setHandler(environment.getApplicationContext().getHandler());
1063+
environment.getApplicationContext().setHandler(cspNonceHandler);
10591064
}
10601065

10611066
private void initializeWebsockets(

openmetadata-service/src/main/java/org/openmetadata/service/exception/OMErrorPageHandler.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import org.eclipse.jetty.server.Response;
1515
import org.eclipse.jetty.util.Callback;
1616
import org.openmetadata.service.config.OMWebConfiguration;
17+
import org.openmetadata.service.config.web.CspHeaderFactory;
18+
import org.openmetadata.service.security.CspNonceHandler;
1719

1820
/**
1921
* Custom error page handler that adds security headers to error responses. This is compatible with
@@ -41,7 +43,7 @@ protected boolean generateAcceptableResponse(
4143
Throwable cause)
4244
throws IOException {
4345
// Add security headers to the response before generating the error page
44-
setSecurityHeaders(this.webConfiguration, response);
46+
setSecurityHeaders(this.webConfiguration, request, response);
4547
return super.generateAcceptableResponse(
4648
request, response, callback, contentType, charsets, code, message, cause);
4749
}
@@ -50,9 +52,11 @@ protected boolean generateAcceptableResponse(
5052
* Sets security headers on the Jetty Response object (new Jetty 12.1 API).
5153
*
5254
* @param webConfiguration the web configuration containing header settings
55+
* @param request the Jetty Request object
5356
* @param response the Jetty Response object
5457
*/
55-
public static void setSecurityHeaders(OMWebConfiguration webConfiguration, Response response) {
58+
public static void setSecurityHeaders(
59+
OMWebConfiguration webConfiguration, Request request, Response response) {
5660
// Hsts
5761
if (webConfiguration.getHstsHeaderFactory() != null) {
5862
webConfiguration
@@ -90,7 +94,19 @@ public static void setSecurityHeaders(OMWebConfiguration webConfiguration, Respo
9094
webConfiguration
9195
.getCspHeaderFactory()
9296
.build()
93-
.forEach((name, value) -> response.getHeaders().put(new HttpField(name, value)));
97+
.forEach(
98+
(name, value) -> {
99+
if ((name.equals(CspHeaderFactory.CSP_HEADER)
100+
|| name.equals(CspHeaderFactory.CSP_REPORT_ONLY_HEADER))
101+
&& value.contains(CspNonceHandler.CSP_NONCE_PLACEHOLDER)) {
102+
final String nonce =
103+
(String) request.getAttribute(CspNonceHandler.CSP_NONCE_ATTRIBUTE);
104+
if (nonce != null) {
105+
value = value.replace(CspNonceHandler.CSP_NONCE_PLACEHOLDER, nonce);
106+
}
107+
}
108+
response.getHeaders().put(new HttpField(name, value));
109+
});
94110
}
95111

96112
// Referrer Policy
Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,45 @@
11
package org.openmetadata.service.resources.system;
22

3+
import jakarta.servlet.http.HttpServletRequest;
34
import jakarta.ws.rs.GET;
45
import jakarta.ws.rs.Path;
56
import jakarta.ws.rs.Produces;
7+
import jakarta.ws.rs.core.Context;
68
import jakarta.ws.rs.core.MediaType;
79
import jakarta.ws.rs.core.Response;
810
import java.io.BufferedReader;
11+
import java.io.IOException;
912
import java.io.InputStream;
1013
import java.io.InputStreamReader;
14+
import java.nio.charset.StandardCharsets;
1115
import java.util.stream.Collectors;
1216
import lombok.extern.slf4j.Slf4j;
1317
import org.openmetadata.service.OpenMetadataApplicationConfig;
18+
import org.openmetadata.service.security.CspNonceHandler;
1419

1520
@Slf4j
1621
@Path("/")
1722
public class IndexResource {
23+
private static final String RAW_INDEX_HTML;
24+
25+
static {
26+
try (InputStream inputStream = IndexResource.class.getResourceAsStream("/assets/index.html")) {
27+
if (inputStream == null) {
28+
throw new IllegalStateException("Missing required resource: /assets/index.html");
29+
}
30+
RAW_INDEX_HTML =
31+
new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
32+
.lines()
33+
.collect(Collectors.joining("\n"));
34+
} catch (IOException e) {
35+
throw new IllegalStateException("Failed to load /assets/index.html", e);
36+
}
37+
}
38+
1839
private String indexHtml;
1940

2041
public IndexResource() {
21-
InputStream inputStream = getClass().getResourceAsStream("/assets/index.html");
22-
indexHtml =
23-
new BufferedReader(new InputStreamReader(inputStream))
24-
.lines()
25-
.collect(Collectors.joining("\n"));
42+
indexHtml = RAW_INDEX_HTML;
2643
}
2744

2845
public void initialize(OpenMetadataApplicationConfig config) {
@@ -32,13 +49,7 @@ public void initialize(OpenMetadataApplicationConfig config) {
3249
public static String getIndexFile(String basePath) {
3350
LOG.info("IndexResource.getIndexFile called with basePath: [{}]", basePath);
3451

35-
InputStream inputStream = IndexResource.class.getResourceAsStream("/assets/index.html");
36-
String indexHtml =
37-
new BufferedReader(new InputStreamReader(inputStream))
38-
.lines()
39-
.collect(Collectors.joining("\n"));
40-
41-
String result = indexHtml.replace("${basePath}", basePath);
52+
String result = RAW_INDEX_HTML.replace("${basePath}", basePath);
4253
String basePathLine =
4354
result
4455
.lines()
@@ -50,9 +61,22 @@ public static String getIndexFile(String basePath) {
5061
return result;
5162
}
5263

64+
public static String getIndexFile(String basePath, String cspNonce) {
65+
String html = getIndexFile(basePath);
66+
if (cspNonce != null && !cspNonce.isEmpty()) {
67+
html = html.replace("${cspNonce}", cspNonce);
68+
}
69+
return html;
70+
}
71+
5372
@GET
5473
@Produces(MediaType.TEXT_HTML)
55-
public Response getIndex() {
56-
return Response.ok(indexHtml).build();
74+
public Response getIndex(@Context HttpServletRequest request) {
75+
final String cspNonce = (String) request.getAttribute(CspNonceHandler.CSP_NONCE_ATTRIBUTE);
76+
String html = indexHtml;
77+
if (cspNonce != null && !cspNonce.isEmpty()) {
78+
html = html.replace("${cspNonce}", cspNonce);
79+
}
80+
return Response.ok(html).build();
5781
}
5882
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2021 Collate
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*/
13+
14+
package org.openmetadata.service.security;
15+
16+
import java.security.SecureRandom;
17+
import java.util.Base64;
18+
import org.eclipse.jetty.http.HttpField;
19+
import org.eclipse.jetty.http.HttpFields;
20+
import org.eclipse.jetty.server.Handler;
21+
import org.eclipse.jetty.server.Request;
22+
import org.eclipse.jetty.server.Response;
23+
import org.eclipse.jetty.util.Callback;
24+
import org.openmetadata.service.config.web.CspHeaderFactory;
25+
26+
public final class CspNonceHandler extends Handler.Wrapper {
27+
public static final String CSP_NONCE_ATTRIBUTE = "cspNonce";
28+
public static final String CSP_NONCE_PLACEHOLDER = "__CSP_NONCE__";
29+
private static final int NONCE_SIZE_BYTES = 16;
30+
private static final SecureRandom RANDOM = new SecureRandom();
31+
32+
@Override
33+
public boolean handle(Request request, Response response, Callback callback) throws Exception {
34+
final String nonce = generateNonce();
35+
request.setAttribute(CSP_NONCE_ATTRIBUTE, nonce);
36+
37+
Response.Wrapper wrappedResponse =
38+
new Response.Wrapper(request, response) {
39+
private HttpFields.Mutable wrappedHeaders;
40+
41+
@Override
42+
public HttpFields.Mutable getHeaders() {
43+
if (wrappedHeaders == null) {
44+
final HttpFields.Mutable delegate = super.getHeaders();
45+
wrappedHeaders =
46+
new HttpFields.Mutable.Wrapper(delegate) {
47+
@Override
48+
public HttpFields.Mutable put(HttpField field) {
49+
if (field.getName().equals(CspHeaderFactory.CSP_HEADER)
50+
|| field.getName().equals(CspHeaderFactory.CSP_REPORT_ONLY_HEADER)) {
51+
final String value = field.getValue();
52+
if (value != null && value.contains(CSP_NONCE_PLACEHOLDER)) {
53+
final String replaced = value.replace(CSP_NONCE_PLACEHOLDER, nonce);
54+
super.put(new HttpField(field.getName(), replaced));
55+
return this;
56+
}
57+
}
58+
super.put(field);
59+
return this;
60+
}
61+
};
62+
}
63+
return wrappedHeaders;
64+
}
65+
};
66+
67+
return super.handle(request, wrappedResponse, callback);
68+
}
69+
70+
private static String generateNonce() {
71+
final byte[] nonceBytes = new byte[NONCE_SIZE_BYTES];
72+
RANDOM.nextBytes(nonceBytes);
73+
return Base64.getEncoder().encodeToString(nonceBytes);
74+
}
75+
}

openmetadata-service/src/main/java/org/openmetadata/service/socket/OpenMetadataAssetServlet.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.jetbrains.annotations.Nullable;
3232
import org.openmetadata.service.config.OMWebConfiguration;
3333
import org.openmetadata.service.resources.system.IndexResource;
34+
import org.openmetadata.service.security.CspNonceHandler;
3435

3536
@Slf4j
3637
public class OpenMetadataAssetServlet extends AssetServlet {
@@ -63,9 +64,9 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
6364
String requestUri = req.getRequestURI();
6465

6566
if (requestUri.endsWith("/")) {
66-
// Serve index.html for directory requests
67+
final String cspNonce = (String) req.getAttribute(CspNonceHandler.CSP_NONCE_ATTRIBUTE);
6768
resp.setContentType("text/html");
68-
resp.getWriter().write(IndexResource.getIndexFile(this.basePath));
69+
resp.getWriter().write(IndexResource.getIndexFile(this.basePath, cspNonce));
6970
return;
7071
}
7172

@@ -109,10 +110,10 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
109110
// For SPA routing: serve index.html for 404s that don't look like static asset requests
110111
if (!resp.isCommitted() && (resp.getStatus() == 404)) {
111112
if (isSpaRoute(requestUri)) {
112-
// Serve index file for SPA routes instead of 404
113+
final String cspNonce = (String) req.getAttribute(CspNonceHandler.CSP_NONCE_ATTRIBUTE);
113114
resp.setStatus(200);
114115
resp.setContentType("text/html");
115-
resp.getWriter().write(IndexResource.getIndexFile(this.basePath));
116+
resp.getWriter().write(IndexResource.getIndexFile(this.basePath, cspNonce));
116117
} else {
117118
resp.sendError(404);
118119
}

0 commit comments

Comments
 (0)