Fixes CORS headers needed by Elastic clients (#85791)

* Fixes CORS headers needed by Elastic clients

Updates the default value for the `http.cors.allow-headers`
setting to include headers used by Elastic client libraries.

Also adds the `access-control-expose-headers` header to responses to
CORS requests so that clients can successfully perform their product
check.
This commit is contained in:
Sylvain Wallez 2023-02-09 16:44:37 +01:00 committed by GitHub
parent 354d3aea18
commit 484d3f4ada
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 62 additions and 4 deletions

View file

@ -0,0 +1,5 @@
pr: 85791
summary: Fixes CORS headers needed by Elastic clients
area: Infra/REST API
type: bug
issues: []

View file

@ -119,9 +119,16 @@ Which methods to allow. Defaults to `OPTIONS, HEAD, GET, POST, PUT, DELETE`.
// tag::http-cors-allow-headers-tag[] // tag::http-cors-allow-headers-tag[]
`http.cors.allow-headers` {ess-icon}:: `http.cors.allow-headers` {ess-icon}::
(<<static-cluster-setting,Static>>, string) (<<static-cluster-setting,Static>>, string)
Which headers to allow. Defaults to `X-Requested-With, Content-Type, Content-Length`. Which headers to allow. Defaults to `X-Requested-With, Content-Type, Content-Length, Authorization, Accept, User-Agent, X-Elastic-Client-Meta`.
// end::http-cors-allow-headers-tag[] // end::http-cors-allow-headers-tag[]
[[http-cors-expose-headers]]
// tag::http-cors-expose-headers-tag[]
`http.cors.expose-headers` {ess-icon}::
(<<static-cluster-setting,Static>>)
Which response headers to expose in the client. Defaults to `X-elastic-product`.
// end::http-cors-expose-headers-tag[]
[[http-cors-allow-credentials]] [[http-cors-allow-credentials]]
// tag::http-cors-allow-credentials-tag[] // tag::http-cors-allow-credentials-tag[]
`http.cors.allow-credentials` {ess-icon}:: `http.cors.allow-credentials` {ess-icon}::

View file

@ -54,6 +54,7 @@ import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_HE
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_METHODS; import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_METHODS;
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_ORIGIN; import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ALLOW_ORIGIN;
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ENABLED; import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_ENABLED;
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_EXPOSE_HEADERS;
import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_MAX_AGE; import static org.elasticsearch.http.HttpTransportSettings.SETTING_CORS_MAX_AGE;
/** /**
@ -77,6 +78,7 @@ public class CorsHandler {
public static final String ACCESS_CONTROL_ALLOW_METHODS = "access-control-allow-methods"; public static final String ACCESS_CONTROL_ALLOW_METHODS = "access-control-allow-methods";
public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "access-control-allow-origin"; public static final String ACCESS_CONTROL_ALLOW_ORIGIN = "access-control-allow-origin";
public static final String ACCESS_CONTROL_MAX_AGE = "access-control-max-age"; public static final String ACCESS_CONTROL_MAX_AGE = "access-control-max-age";
public static final String ACCESS_CONTROL_EXPOSE_HEADERS = "access-control-expose-headers";
private static final Pattern SCHEME_PATTERN = Pattern.compile("^https?://"); private static final Pattern SCHEME_PATTERN = Pattern.compile("^https?://");
private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss O", Locale.ENGLISH); private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss O", Locale.ENGLISH);
@ -105,6 +107,7 @@ public class CorsHandler {
} }
if (setOrigin(httpRequest, httpResponse)) { if (setOrigin(httpRequest, httpResponse)) {
setAllowCredentials(httpResponse); setAllowCredentials(httpResponse);
setExposeHeaders(httpResponse);
} }
} }
@ -228,6 +231,12 @@ public class CorsHandler {
} }
} }
private void setExposeHeaders(final HttpResponse response) {
for (String header : config.accessControlExposeHeaders) {
response.addHeader(ACCESS_CONTROL_EXPOSE_HEADERS, header);
}
}
private void setAllowCredentials(final HttpResponse response) { private void setAllowCredentials(final HttpResponse response) {
if (config.isCredentialsAllowed()) { if (config.isCredentialsAllowed()) {
response.addHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); response.addHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
@ -247,6 +256,7 @@ public class CorsHandler {
private final boolean credentialsAllowed; private final boolean credentialsAllowed;
private final Set<RestRequest.Method> allowedRequestMethods; private final Set<RestRequest.Method> allowedRequestMethods;
private final Set<String> allowedRequestHeaders; private final Set<String> allowedRequestHeaders;
private final Set<String> accessControlExposeHeaders;
private final long maxAge; private final long maxAge;
public Config(Builder builder) { public Config(Builder builder) {
@ -257,6 +267,7 @@ public class CorsHandler {
this.credentialsAllowed = builder.allowCredentials; this.credentialsAllowed = builder.allowCredentials;
this.allowedRequestMethods = Collections.unmodifiableSet(builder.requestMethods); this.allowedRequestMethods = Collections.unmodifiableSet(builder.requestMethods);
this.allowedRequestHeaders = Collections.unmodifiableSet(builder.requestHeaders); this.allowedRequestHeaders = Collections.unmodifiableSet(builder.requestHeaders);
this.accessControlExposeHeaders = Collections.unmodifiableSet(builder.accessControlExposeHeaders);
this.maxAge = builder.maxAge; this.maxAge = builder.maxAge;
} }
@ -314,6 +325,8 @@ public class CorsHandler {
+ allowedRequestMethods + allowedRequestMethods
+ ", allowedRequestHeaders=" + ", allowedRequestHeaders="
+ allowedRequestHeaders + allowedRequestHeaders
+ ", accessControlExposeHeaders="
+ accessControlExposeHeaders
+ ", maxAge=" + ", maxAge="
+ maxAge + maxAge
+ '}'; + '}';
@ -329,6 +342,7 @@ public class CorsHandler {
long maxAge; long maxAge;
private final Set<RestRequest.Method> requestMethods = new HashSet<>(); private final Set<RestRequest.Method> requestMethods = new HashSet<>();
private final Set<String> requestHeaders = new HashSet<>(); private final Set<String> requestHeaders = new HashSet<>();
private final Set<String> accessControlExposeHeaders = new HashSet<>();
private Builder() { private Builder() {
anyOrigin = true; anyOrigin = true;
@ -380,6 +394,11 @@ public class CorsHandler {
return this; return this;
} }
public Builder accessControlExposeHeaders(String[] headers) {
accessControlExposeHeaders.addAll(Arrays.asList(headers));
return this;
}
public Config build() { public Config build() {
return new Config(this); return new Config(this);
} }
@ -427,6 +446,7 @@ public class CorsHandler {
Config config = builder.allowedRequestMethods(methods) Config config = builder.allowedRequestMethods(methods)
.maxAge(SETTING_CORS_MAX_AGE.get(settings)) .maxAge(SETTING_CORS_MAX_AGE.get(settings))
.allowedRequestHeaders(Strings.tokenizeToStringArray(SETTING_CORS_ALLOW_HEADERS.get(settings), ",")) .allowedRequestHeaders(Strings.tokenizeToStringArray(SETTING_CORS_ALLOW_HEADERS.get(settings), ","))
.accessControlExposeHeaders(Strings.tokenizeToStringArray(SETTING_CORS_EXPOSE_HEADERS.get(settings), ","))
.build(); .build();
return config; return config;
} }

View file

@ -43,7 +43,13 @@ public final class HttpTransportSettings {
); );
public static final Setting<String> SETTING_CORS_ALLOW_HEADERS = new Setting<>( public static final Setting<String> SETTING_CORS_ALLOW_HEADERS = new Setting<>(
"http.cors.allow-headers", "http.cors.allow-headers",
"X-Requested-With,Content-Type,Content-Length", "X-Requested-With,Content-Type,Content-Length,Authorization,Accept,User-Agent,X-Elastic-Client-Meta",
(value) -> value,
Property.NodeScope
);
public static final Setting<String> SETTING_CORS_EXPOSE_HEADERS = new Setting<>(
"http.cors.expose-headers",
"X-elastic-product",
(value) -> value, (value) -> value,
Property.NodeScope Property.NodeScope
); );

View file

@ -204,7 +204,15 @@ public class CorsHandlerTests extends ESTestCase {
assertThat(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_METHODS), containsInAnyOrder("HEAD", "OPTIONS", "GET", "DELETE", "POST")); assertThat(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_METHODS), containsInAnyOrder("HEAD", "OPTIONS", "GET", "DELETE", "POST"));
assertThat( assertThat(
headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_HEADERS), headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_HEADERS),
containsInAnyOrder("X-Requested-With", "Content-Type", "Content-Length") containsInAnyOrder(
"X-Requested-With",
"Content-Type",
"Content-Length",
"Authorization",
"Accept",
"User-Agent",
"X-Elastic-Client-Meta"
)
); );
assertThat(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_CREDENTIALS), containsInAnyOrder("true")); assertThat(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_CREDENTIALS), containsInAnyOrder("true"));
assertThat(headers.get(CorsHandler.ACCESS_CONTROL_MAX_AGE), containsInAnyOrder("1728000")); assertThat(headers.get(CorsHandler.ACCESS_CONTROL_MAX_AGE), containsInAnyOrder("1728000"));
@ -232,7 +240,15 @@ public class CorsHandlerTests extends ESTestCase {
assertThat(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_METHODS), containsInAnyOrder("HEAD", "OPTIONS", "GET", "DELETE", "POST")); assertThat(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_METHODS), containsInAnyOrder("HEAD", "OPTIONS", "GET", "DELETE", "POST"));
assertThat( assertThat(
headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_HEADERS), headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_HEADERS),
containsInAnyOrder("X-Requested-With", "Content-Type", "Content-Length") containsInAnyOrder(
"X-Requested-With",
"Content-Type",
"Content-Length",
"Authorization",
"Accept",
"User-Agent",
"X-Elastic-Client-Meta"
)
); );
assertThat(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_CREDENTIALS), containsInAnyOrder("true")); assertThat(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_CREDENTIALS), containsInAnyOrder("true"));
assertThat(headers.get(CorsHandler.ACCESS_CONTROL_MAX_AGE), containsInAnyOrder("1728000")); assertThat(headers.get(CorsHandler.ACCESS_CONTROL_MAX_AGE), containsInAnyOrder("1728000"));
@ -254,6 +270,7 @@ public class CorsHandlerTests extends ESTestCase {
Map<String, List<String>> headers = response.headers(); Map<String, List<String>> headers = response.headers();
assertNull(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_ORIGIN)); assertNull(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_ORIGIN));
assertNull(headers.get(CorsHandler.ACCESS_CONTROL_EXPOSE_HEADERS));
} }
public void testSetResponseHeadersWithWildcardOrigin() { public void testSetResponseHeadersWithWildcardOrigin() {
@ -270,6 +287,7 @@ public class CorsHandlerTests extends ESTestCase {
Map<String, List<String>> headers = response.headers(); Map<String, List<String>> headers = response.headers();
assertThat(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_ORIGIN), containsInAnyOrder("*")); assertThat(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_ORIGIN), containsInAnyOrder("*"));
assertThat(headers.get(CorsHandler.ACCESS_CONTROL_EXPOSE_HEADERS), containsInAnyOrder("X-elastic-product"));
assertNull(headers.get(CorsHandler.VARY)); assertNull(headers.get(CorsHandler.VARY));
} }
@ -288,6 +306,7 @@ public class CorsHandlerTests extends ESTestCase {
Map<String, List<String>> headers = response.headers(); Map<String, List<String>> headers = response.headers();
assertThat(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_ORIGIN), containsInAnyOrder("valid-origin")); assertThat(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_ORIGIN), containsInAnyOrder("valid-origin"));
assertThat(headers.get(CorsHandler.ACCESS_CONTROL_EXPOSE_HEADERS), containsInAnyOrder("X-elastic-product"));
assertThat(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_CREDENTIALS), containsInAnyOrder("true")); assertThat(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_CREDENTIALS), containsInAnyOrder("true"));
assertThat(headers.get(CorsHandler.VARY), containsInAnyOrder(CorsHandler.ORIGIN)); assertThat(headers.get(CorsHandler.VARY), containsInAnyOrder(CorsHandler.ORIGIN));
} }
@ -308,6 +327,7 @@ public class CorsHandlerTests extends ESTestCase {
Map<String, List<String>> headers = response.headers(); Map<String, List<String>> headers = response.headers();
assertThat(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_ORIGIN), containsInAnyOrder("valid-origin")); assertThat(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_ORIGIN), containsInAnyOrder("valid-origin"));
assertThat(headers.get(CorsHandler.ACCESS_CONTROL_EXPOSE_HEADERS), containsInAnyOrder("X-elastic-product"));
assertThat(headers.get(CorsHandler.VARY), containsInAnyOrder(CorsHandler.ORIGIN)); assertThat(headers.get(CorsHandler.VARY), containsInAnyOrder(CorsHandler.ORIGIN));
if (allowCredentials) { if (allowCredentials) {
assertThat(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_CREDENTIALS), containsInAnyOrder("true")); assertThat(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_CREDENTIALS), containsInAnyOrder("true"));