diff --git a/docs/changelog/85791.yaml b/docs/changelog/85791.yaml new file mode 100644 index 000000000000..4510a43d9d21 --- /dev/null +++ b/docs/changelog/85791.yaml @@ -0,0 +1,5 @@ +pr: 85791 +summary: Fixes CORS headers needed by Elastic clients +area: Infra/REST API +type: bug +issues: [] diff --git a/docs/reference/modules/http.asciidoc b/docs/reference/modules/http.asciidoc index c677eb377316..a50de2a59238 100644 --- a/docs/reference/modules/http.asciidoc +++ b/docs/reference/modules/http.asciidoc @@ -119,9 +119,16 @@ Which methods to allow. Defaults to `OPTIONS, HEAD, GET, POST, PUT, DELETE`. // tag::http-cors-allow-headers-tag[] `http.cors.allow-headers` {ess-icon}:: (<>, 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[] +[[http-cors-expose-headers]] +// tag::http-cors-expose-headers-tag[] +`http.cors.expose-headers` {ess-icon}:: +(<>) +Which response headers to expose in the client. Defaults to `X-elastic-product`. +// end::http-cors-expose-headers-tag[] + [[http-cors-allow-credentials]] // tag::http-cors-allow-credentials-tag[] `http.cors.allow-credentials` {ess-icon}:: diff --git a/server/src/main/java/org/elasticsearch/http/CorsHandler.java b/server/src/main/java/org/elasticsearch/http/CorsHandler.java index 3c30407bd39f..100079cfa38f 100644 --- a/server/src/main/java/org/elasticsearch/http/CorsHandler.java +++ b/server/src/main/java/org/elasticsearch/http/CorsHandler.java @@ -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_ORIGIN; 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; /** @@ -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_ORIGIN = "access-control-allow-origin"; 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 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)) { 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) { if (config.isCredentialsAllowed()) { response.addHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); @@ -247,6 +256,7 @@ public class CorsHandler { private final boolean credentialsAllowed; private final Set allowedRequestMethods; private final Set allowedRequestHeaders; + private final Set accessControlExposeHeaders; private final long maxAge; public Config(Builder builder) { @@ -257,6 +267,7 @@ public class CorsHandler { this.credentialsAllowed = builder.allowCredentials; this.allowedRequestMethods = Collections.unmodifiableSet(builder.requestMethods); this.allowedRequestHeaders = Collections.unmodifiableSet(builder.requestHeaders); + this.accessControlExposeHeaders = Collections.unmodifiableSet(builder.accessControlExposeHeaders); this.maxAge = builder.maxAge; } @@ -314,6 +325,8 @@ public class CorsHandler { + allowedRequestMethods + ", allowedRequestHeaders=" + allowedRequestHeaders + + ", accessControlExposeHeaders=" + + accessControlExposeHeaders + ", maxAge=" + maxAge + '}'; @@ -329,6 +342,7 @@ public class CorsHandler { long maxAge; private final Set requestMethods = new HashSet<>(); private final Set requestHeaders = new HashSet<>(); + private final Set accessControlExposeHeaders = new HashSet<>(); private Builder() { anyOrigin = true; @@ -380,6 +394,11 @@ public class CorsHandler { return this; } + public Builder accessControlExposeHeaders(String[] headers) { + accessControlExposeHeaders.addAll(Arrays.asList(headers)); + return this; + } + public Config build() { return new Config(this); } @@ -427,6 +446,7 @@ public class CorsHandler { Config config = builder.allowedRequestMethods(methods) .maxAge(SETTING_CORS_MAX_AGE.get(settings)) .allowedRequestHeaders(Strings.tokenizeToStringArray(SETTING_CORS_ALLOW_HEADERS.get(settings), ",")) + .accessControlExposeHeaders(Strings.tokenizeToStringArray(SETTING_CORS_EXPOSE_HEADERS.get(settings), ",")) .build(); return config; } diff --git a/server/src/main/java/org/elasticsearch/http/HttpTransportSettings.java b/server/src/main/java/org/elasticsearch/http/HttpTransportSettings.java index b45f83b01f32..6e72516dad07 100644 --- a/server/src/main/java/org/elasticsearch/http/HttpTransportSettings.java +++ b/server/src/main/java/org/elasticsearch/http/HttpTransportSettings.java @@ -43,7 +43,13 @@ public final class HttpTransportSettings { ); public static final Setting SETTING_CORS_ALLOW_HEADERS = new Setting<>( "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 SETTING_CORS_EXPOSE_HEADERS = new Setting<>( + "http.cors.expose-headers", + "X-elastic-product", (value) -> value, Property.NodeScope ); diff --git a/server/src/test/java/org/elasticsearch/http/CorsHandlerTests.java b/server/src/test/java/org/elasticsearch/http/CorsHandlerTests.java index c75f331fba35..086faef1a39e 100644 --- a/server/src/test/java/org/elasticsearch/http/CorsHandlerTests.java +++ b/server/src/test/java/org/elasticsearch/http/CorsHandlerTests.java @@ -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_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_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_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_MAX_AGE), containsInAnyOrder("1728000")); @@ -254,6 +270,7 @@ public class CorsHandlerTests extends ESTestCase { Map> headers = response.headers(); assertNull(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_ORIGIN)); + assertNull(headers.get(CorsHandler.ACCESS_CONTROL_EXPOSE_HEADERS)); } public void testSetResponseHeadersWithWildcardOrigin() { @@ -270,6 +287,7 @@ public class CorsHandlerTests extends ESTestCase { Map> headers = response.headers(); 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)); } @@ -288,6 +306,7 @@ public class CorsHandlerTests extends ESTestCase { Map> headers = response.headers(); 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.VARY), containsInAnyOrder(CorsHandler.ORIGIN)); } @@ -308,6 +327,7 @@ public class CorsHandlerTests extends ESTestCase { Map> headers = response.headers(); 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)); if (allowCredentials) { assertThat(headers.get(CorsHandler.ACCESS_CONTROL_ALLOW_CREDENTIALS), containsInAnyOrder("true"));