Browse Source

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.
Sylvain Wallez 2 years ago
parent
commit
484d3f4ada

+ 5 - 0
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: []

+ 8 - 1
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}::
 (<<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[]
 
+[[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]]
 // tag::http-cors-allow-credentials-tag[]
 `http.cors.allow-credentials` {ess-icon}::

+ 20 - 0
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<RestRequest.Method> allowedRequestMethods;
         private final Set<String> allowedRequestHeaders;
+        private final Set<String> 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<RestRequest.Method> requestMethods = new HashSet<>();
             private final Set<String> requestHeaders = new HashSet<>();
+            private final Set<String> 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;
     }

+ 7 - 1
server/src/main/java/org/elasticsearch/http/HttpTransportSettings.java

@@ -43,7 +43,13 @@ public final class HttpTransportSettings {
     );
     public static final Setting<String> 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<String> SETTING_CORS_EXPOSE_HEADERS = new Setting<>(
+        "http.cors.expose-headers",
+        "X-elastic-product",
         (value) -> value,
         Property.NodeScope
     );

+ 22 - 2
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<String, List<String>> 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<String, List<String>> 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<String, List<String>> 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<String, List<String>> 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"));