From d32db1553180ea4402debce3ea25ce3583477426 Mon Sep 17 00:00:00 2001 From: Valentin Aitken Date: Tue, 10 Jan 2017 23:51:06 +0200 Subject: [PATCH] Add experimental CORS server support Include org.apache.cxf.rs.security.cors.CrossOriginResourceSharing implementation --- .../core/BrooklynFeatureEnablement.java | 19 ++ karaf/features/src/main/feature/feature.xml | 1 + .../brooklyn/launcher/BrooklynWebServer.java | 12 +- rest/rest-resources/pom.xml | 5 + .../rest/filter/CorsImplSupplierFilter.java | 137 ++++++++++ .../resources/OSGI-INF/blueprint/service.xml | 3 + .../src/main/webapp/WEB-INF/web.xml | 1 + .../rest/BrooklynRestApiLauncher.java | 14 +- .../brooklyn/rest/CorsFilterLauncherTest.java | 246 ++++++++++++++++++ .../rest/CsrfTokenFilterLauncherTest.java | 3 +- 10 files changed, 437 insertions(+), 4 deletions(-) create mode 100644 rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/CorsImplSupplierFilter.java create mode 100644 rest/rest-server/src/test/java/org/apache/brooklyn/rest/CorsFilterLauncherTest.java diff --git a/core/src/main/java/org/apache/brooklyn/core/BrooklynFeatureEnablement.java b/core/src/main/java/org/apache/brooklyn/core/BrooklynFeatureEnablement.java index 8bc2ffd298d..4e50e7f7940 100644 --- a/core/src/main/java/org/apache/brooklyn/core/BrooklynFeatureEnablement.java +++ b/core/src/main/java/org/apache/brooklyn/core/BrooklynFeatureEnablement.java @@ -61,6 +61,25 @@ public class BrooklynFeatureEnablement { /** whether feeds are automatically registered when set on entities, so that they are persisted */ public static final String FEATURE_FEED_REGISTRATION_PROPERTY = FEATURE_PROPERTY_PREFIX+".feedRegistration"; + /** + *

+ * Enables support for Cross Origin Resource Sharing (CORS) filtering on requests in BrooklynWebServer. + * If enabled, the allowed origins for the CORS headers should be configured + * using the brooklyn.experimental.feature.corsCxfFeature.allowedOrigins=[] property. + *

+ *

+ * If brooklyn.experimental.feature.corsCxfFeature.allowedOrigins is not is not supplied then allowedOrigins will be a wildcard on all domains.
+ * Not specifying allowedOrigins is strongly discouraged. + *

+ *

+ * Currently there is no support for varying these headers on a per-API-resource basis, that is, the same configured headers are applied to all requests. + *

+ *

+ * Apache Brooklyn API requests should be exposed to third party web apps with great attention. + *

+ */ + public static final String FEATURE_CORS_CXF_PROPERTY = FEATURE_PROPERTY_PREFIX + ".corsCxfFeature"; + public static final String FEATURE_CATALOG_PERSISTENCE_PROPERTY = FEATURE_PROPERTY_PREFIX+".catalogPersistence"; /** whether the default standby mode is {@link HighAvailabilityMode#HOT_STANDBY} or falling back to the traditional diff --git a/karaf/features/src/main/feature/feature.xml b/karaf/features/src/main/feature/feature.xml index 9cfc49aac07..effcf5226ea 100644 --- a/karaf/features/src/main/feature/feature.xml +++ b/karaf/features/src/main/feature/feature.xml @@ -194,6 +194,7 @@ brooklyn-camp-base cxf-jaxrs + mvn:org.apache.cxf/cxf-rt-rs-security-cors/${cxf.version} mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-json-provider/${fasterxml.jackson.version} diff --git a/launcher/src/main/java/org/apache/brooklyn/launcher/BrooklynWebServer.java b/launcher/src/main/java/org/apache/brooklyn/launcher/BrooklynWebServer.java index 933ce68dc50..d9c3c7d1f71 100644 --- a/launcher/src/main/java/org/apache/brooklyn/launcher/BrooklynWebServer.java +++ b/launcher/src/main/java/org/apache/brooklyn/launcher/BrooklynWebServer.java @@ -36,6 +36,8 @@ import javax.annotation.Nullable; import javax.security.auth.spi.LoginModule; +import com.google.common.collect.ImmutableList; +import org.apache.brooklyn.core.BrooklynFeatureEnablement; import org.apache.brooklyn.rest.NopSecurityHandler; import org.apache.brooklyn.api.location.PortRange; import org.apache.brooklyn.api.mgmt.ManagementContext; @@ -52,6 +54,7 @@ import org.apache.brooklyn.rest.RestApiSetup; import org.apache.brooklyn.rest.filter.CsrfTokenFilter; import org.apache.brooklyn.rest.filter.EntitlementContextFilter; +import org.apache.brooklyn.rest.filter.CorsImplSupplierFilter; import org.apache.brooklyn.rest.filter.HaHotCheckResourceFilter; import org.apache.brooklyn.rest.filter.LoggingFilter; import org.apache.brooklyn.rest.filter.NoCacheFilter; @@ -455,7 +458,8 @@ public synchronized void start() throws Exception { } private WebAppContext deployRestApi(WebAppContext context) { - RestApiSetup.installRest(context, + ImmutableList.Builder providersListBuilder = ImmutableList.builder(); + providersListBuilder.add( new ManagementContextProvider(), new ShutdownHandlerProvider(shutdownHandler), new RequestTaggingRsFilter(), @@ -463,6 +467,12 @@ private WebAppContext deployRestApi(WebAppContext context) { new HaHotCheckResourceFilter(), new EntitlementContextFilter(), new CsrfTokenFilter()); + if (BrooklynFeatureEnablement.isEnabled(BrooklynFeatureEnablement.FEATURE_CORS_CXF_PROPERTY)) { + providersListBuilder.add(new CorsImplSupplierFilter(managementContext)); + } + + RestApiSetup.installRest(context, + providersListBuilder.build().toArray()); RestApiSetup.installServletFilters(context, RequestTaggingFilter.class, LoggingFilter.class); diff --git a/rest/rest-resources/pom.xml b/rest/rest-resources/pom.xml index 6587993d473..80c60c0bfbf 100644 --- a/rest/rest-resources/pom.xml +++ b/rest/rest-resources/pom.xml @@ -116,6 +116,11 @@ org.eclipse.jetty jetty-server + + org.apache.cxf + cxf-rt-rs-security-cors + ${cxf.version} + org.apache.brooklyn diff --git a/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/CorsImplSupplierFilter.java b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/CorsImplSupplierFilter.java new file mode 100644 index 00000000000..392480f1976 --- /dev/null +++ b/rest/rest-resources/src/main/java/org/apache/brooklyn/rest/filter/CorsImplSupplierFilter.java @@ -0,0 +1,137 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.rest.filter; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.reflect.TypeToken; +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.config.ConfigKey; +import org.apache.brooklyn.config.StringConfigMap; +import org.apache.brooklyn.core.BrooklynFeatureEnablement; +import org.apache.brooklyn.core.config.ConfigKeys; +import org.apache.cxf.rs.security.cors.CrossOriginResourceSharingFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.ext.Provider; +import java.util.Collections; +import java.util.List; + +/** + * @see BrooklynFeatureEnablement#FEATURE_CORS_CXF_PROPERTY + */ +@Provider +public class CorsImplSupplierFilter extends CrossOriginResourceSharingFilter { + /** + * @see CrossOriginResourceSharingFilter#setAllowOrigins(List) + */ + public static final ConfigKey> ALLOWED_ORIGINS = ConfigKeys.newConfigKey(new TypeToken>() {}, + BrooklynFeatureEnablement.FEATURE_CORS_CXF_PROPERTY + ".allowedOrigins", + "List of allowed origins. Access-Control-Allow-Origin header will be returned to client if Origin header in request is matching exactly a value among the list allowed origins. " + + "If AllowedOrigins is empty or not specified then all origins are allowed. " + + "No wildcard allowed origins are supported.", + Collections.emptyList()); + + /** + * @see CrossOriginResourceSharingFilter#setAllowHeaders(List) + */ + public static final ConfigKey> ALLOWED_HEADERS = ConfigKeys.newConfigKey(new TypeToken>() {}, + BrooklynFeatureEnablement.FEATURE_CORS_CXF_PROPERTY + ".allowedHeaders", + "List of allowed headers for preflight checks.", + Collections.emptyList()); + + /** + * @see CrossOriginResourceSharingFilter#setAllowCredentials(boolean) + */ + public static final ConfigKey ALLOW_CREDENTIALS = ConfigKeys.newBooleanConfigKey( + BrooklynFeatureEnablement.FEATURE_CORS_CXF_PROPERTY + ".allowCredentials", + "The value for the Access-Control-Allow-Credentials header. If false, no header is added. If true, the\n" + + " * header is added with the value 'true'. False by default.", + false); + + /** + * @see CrossOriginResourceSharingFilter#setExposeHeaders(List) + */ + public static final ConfigKey> EXPOSED_HEADERS = ConfigKeys.newConfigKey(new TypeToken>() {}, + BrooklynFeatureEnablement.FEATURE_CORS_CXF_PROPERTY + ".exposedHeaders", + "A list of non-simple headers to be exposed via Access-Control-Expose-Headers.", + Collections.emptyList()); + + /** + * @see CrossOriginResourceSharingFilter#setMaxAge(Integer) + */ + public static final ConfigKey MAX_AGE = ConfigKeys.newIntegerConfigKey( + BrooklynFeatureEnablement.FEATURE_CORS_CXF_PROPERTY + ".maxAge", + "The value for Access-Control-Max-Age.", + null); + + /** + * @see CrossOriginResourceSharingFilter#setPreflightErrorStatus(Integer) + */ + public static final ConfigKey PREFLIGHT_FAIL_STATUS = ConfigKeys.newIntegerConfigKey( + BrooklynFeatureEnablement.FEATURE_CORS_CXF_PROPERTY + ".preflightFailStatus", + "Preflight error response status, default is 200.", + 200); + + public static final ConfigKey BLOCK_CORS_IF_UNAUTHORIZED = ConfigKeys.newBooleanConfigKey( + BrooklynFeatureEnablement.FEATURE_CORS_CXF_PROPERTY + ".blockCorsIfUnauthorized", + "Do not apply CORS if response is going to be with UNAUTHORIZED status.", + false); + + private static final Logger LOGGER = LoggerFactory.getLogger(CorsImplSupplierFilter.class); + + private static final boolean brooklynFeatureEnabled = BrooklynFeatureEnablement.isEnabled(BrooklynFeatureEnablement.FEATURE_CORS_CXF_PROPERTY); + static { + if (brooklynFeatureEnabled) { + LOGGER.info("CORS brooklyn feature enabled."); + } + } + + @VisibleForTesting + public CorsImplSupplierFilter(@Nullable ManagementContext mgmt) { + Preconditions.checkNotNull(mgmt,"ManagementContext should be suppplied to CORS filter."); + setFindResourceMethod(false); + StringConfigMap configMap = mgmt.getConfig(); + setAllowOrigins(configMap.getConfig(ALLOWED_ORIGINS)); + setAllowHeaders(configMap.getConfig(ALLOWED_HEADERS)); + setAllowCredentials(configMap.getConfig(ALLOW_CREDENTIALS)); + setExposeHeaders(configMap.getConfig(EXPOSED_HEADERS)); + setMaxAge(configMap.getConfig(MAX_AGE)); + setPreflightErrorStatus(configMap.getConfig(PREFLIGHT_FAIL_STATUS)); + setBlockCorsIfUnauthorized(configMap.getConfig(BLOCK_CORS_IF_UNAUTHORIZED)); + } + + @Override + public void filter(ContainerRequestContext requestContext) { + if (brooklynFeatureEnabled) { + super.filter(requestContext); + } + } + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { + if (brooklynFeatureEnabled) { + super.filter(requestContext, responseContext); + } + } +} diff --git a/rest/rest-resources/src/main/resources/OSGI-INF/blueprint/service.xml b/rest/rest-resources/src/main/resources/OSGI-INF/blueprint/service.xml index c53c623f68b..fef7a7d419a 100644 --- a/rest/rest-resources/src/main/resources/OSGI-INF/blueprint/service.xml +++ b/rest/rest-resources/src/main/resources/OSGI-INF/blueprint/service.xml @@ -110,6 +110,9 @@ limitations under the License. + + + diff --git a/rest/rest-server/src/main/webapp/WEB-INF/web.xml b/rest/rest-server/src/main/webapp/WEB-INF/web.xml index 6aac0986831..de15d08513a 100644 --- a/rest/rest-server/src/main/webapp/WEB-INF/web.xml +++ b/rest/rest-server/src/main/webapp/WEB-INF/web.xml @@ -77,6 +77,7 @@ org.apache.brooklyn.rest.filter.EntitlementContextFilter, org.apache.brooklyn.rest.filter.CsrfTokenFilter, org.apache.brooklyn.rest.util.ManagementContextProvider + diff --git a/rest/rest-server/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncher.java b/rest/rest-server/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncher.java index 722e3d9fa7a..4c771c5f23b 100644 --- a/rest/rest-server/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncher.java +++ b/rest/rest-server/src/test/java/org/apache/brooklyn/rest/BrooklynRestApiLauncher.java @@ -19,6 +19,7 @@ package org.apache.brooklyn.rest; import static com.google.common.base.Preconditions.checkNotNull; +import static org.apache.brooklyn.rest.filter.CorsImplSupplierFilter.ALLOWED_ORIGINS; import java.io.File; import java.io.FilenameFilter; @@ -27,15 +28,18 @@ import java.util.List; import javax.servlet.Filter; +import javax.ws.rs.ext.ContextResolver; import org.apache.brooklyn.api.mgmt.ManagementContext; import org.apache.brooklyn.camp.brooklyn.BrooklynCampPlatformLauncherAbstract; import org.apache.brooklyn.camp.brooklyn.BrooklynCampPlatformLauncherNoServer; +import org.apache.brooklyn.core.BrooklynFeatureEnablement; import org.apache.brooklyn.core.internal.BrooklynProperties; import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext; import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal; import org.apache.brooklyn.core.server.BrooklynServerConfig; import org.apache.brooklyn.core.server.BrooklynServiceAttributes; +import org.apache.brooklyn.rest.filter.CorsImplSupplierFilter; import org.apache.brooklyn.rest.filter.CsrfTokenFilter; import org.apache.brooklyn.rest.filter.EntitlementContextFilter; import org.apache.brooklyn.rest.filter.HaHotCheckResourceFilter; @@ -56,6 +60,7 @@ import org.apache.brooklyn.util.net.Networking; import org.apache.brooklyn.util.os.Os; import org.apache.brooklyn.util.text.WildcardGlobs; +import org.apache.cxf.rs.security.cors.CrossOriginResourceSharingFilter; import org.eclipse.jetty.jaas.JAASLoginService; import org.eclipse.jetty.server.NetworkConnector; import org.eclipse.jetty.server.Server; @@ -232,7 +237,8 @@ private WebAppContext servletContextHandler(ManagementContext managementContext) context.setAttribute(BrooklynServiceAttributes.BROOKLYN_MANAGEMENT_CONTEXT, managementContext); installWar(context); - RestApiSetup.installRest(context, + ImmutableList.Builder providersListBuilder = ImmutableList.builder(); + providersListBuilder.add( new ManagementContextProvider(), new ShutdownHandlerProvider(shutdownListener), new RequestTaggingRsFilter(), @@ -240,6 +246,12 @@ private WebAppContext servletContextHandler(ManagementContext managementContext) new HaHotCheckResourceFilter(), new EntitlementContextFilter(), new CsrfTokenFilter()); + if (BrooklynFeatureEnablement.isEnabled(BrooklynFeatureEnablement.FEATURE_CORS_CXF_PROPERTY)) { + providersListBuilder.add(new CorsImplSupplierFilter(managementContext)); + } + RestApiSetup.installRest(context, + providersListBuilder.build().toArray()); + RestApiSetup.installServletFilters(context, this.filters); context.setContextPath("/"); diff --git a/rest/rest-server/src/test/java/org/apache/brooklyn/rest/CorsFilterLauncherTest.java b/rest/rest-server/src/test/java/org/apache/brooklyn/rest/CorsFilterLauncherTest.java new file mode 100644 index 00000000000..9c0df7580c3 --- /dev/null +++ b/rest/rest-server/src/test/java/org/apache/brooklyn/rest/CorsFilterLauncherTest.java @@ -0,0 +1,246 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.rest; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import org.apache.brooklyn.api.mgmt.ManagementContext; +import org.apache.brooklyn.core.BrooklynFeatureEnablement; +import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests; +import org.apache.brooklyn.rest.filter.CorsImplSupplierFilter; +import org.apache.brooklyn.util.collections.MutableMap; +import org.apache.brooklyn.util.http.HttpTool; +import org.apache.brooklyn.util.http.HttpToolResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.impl.client.HttpClients; +import org.testng.annotations.Test; + +import javax.ws.rs.core.HttpHeaders; +import java.io.IOException; +import java.net.URI; +import java.util.List; + +import static org.apache.brooklyn.rest.CsrfTokenFilterLauncherTest.assertOkayResponse; +import static org.apache.cxf.rs.security.cors.CorsHeaderConstants.*; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +/** + * It is important to first execute tests where CorsFilterImpl is enabled + * after that execute tests where CorsImplSupplierFilter is disabled. + * This is because CorsImplSupplierFilter is designed to be enabled and disabled only on startup. + **/ +public class CorsFilterLauncherTest extends BrooklynRestApiLauncherTestFixture { + @Test + public void test1CorsIsEnabledOnOneOriginGET() throws IOException { + final String shouldAllowOrigin = "http://foo.bar.com"; + final String thirdPartyOrigin = "http://foo.bar1.com"; + setCorsFilterFeature(true, ImmutableList.of(shouldAllowOrigin)); + + HttpClient client = client(); + // preflight request + HttpToolResponse response = HttpTool.execAndConsume(client, httpOptionsRequest("server/status", "GET", shouldAllowOrigin)); + assertAcAllowOrigin(response, shouldAllowOrigin, "GET"); + assertOkayResponse(response, ""); + + HttpUriRequest httpRequest = RequestBuilder.get(getBaseUriRest() + "server/status") + .addHeader("Origin", shouldAllowOrigin) + .addHeader(HEADER_AC_REQUEST_METHOD, "GET") + .build(); + response = HttpTool.execAndConsume(client, httpRequest); + assertAcAllowOrigin(response, shouldAllowOrigin, "GET", false); + assertOkayResponse(response, "MASTER"); + + // preflight request + response = HttpTool.execAndConsume(client, httpOptionsRequest("server/status", "GET", thirdPartyOrigin)); + assertAcNotAllowOrigin(response); + assertOkayResponse(response, ""); + + httpRequest = RequestBuilder.get(getBaseUriRest() + "server/status") + .addHeader("Origin", thirdPartyOrigin) + .addHeader(HEADER_AC_REQUEST_METHOD, "GET") + .build(); + response = HttpTool.execAndConsume(client, httpRequest); + assertAcNotAllowOrigin(response); + assertOkayResponse(response, "MASTER"); + } + + @Test + public void test1CorsIsEnabledOnOneOriginPOST() throws IOException { + final String shouldAllowOrigin = "http://foo.bar.com"; + final String thirdPartyOrigin = "http://foo.bar1.com"; + setCorsFilterFeature(true, ImmutableList.of(shouldAllowOrigin)); + HttpClient client = client(); + // preflight request + HttpToolResponse response = HttpTool.execAndConsume(client, httpOptionsRequest("script/groovy", "POST", shouldAllowOrigin)); + assertAcAllowOrigin(response, shouldAllowOrigin, "POST"); + assertOkayResponse(response, ""); + + response = HttpTool.httpPost( + client, URI.create(getBaseUriRest() + "script/groovy"), + ImmutableMap.of( + "Origin", shouldAllowOrigin, + HttpHeaders.CONTENT_TYPE, "application/text"), + "return 0;".getBytes()); + assertAcAllowOrigin(response, shouldAllowOrigin, "POST", false); + assertOkayResponse(response, "{\"result\":\"0\"}"); + + // preflight request + response = HttpTool.execAndConsume(client, httpOptionsRequest("script/groovy", "POST", thirdPartyOrigin)); + assertAcNotAllowOrigin(response); + assertOkayResponse(response, ""); + + response = HttpTool.httpPost( + client, URI.create(getBaseUriRest() + "script/groovy"), + ImmutableMap.of( + "Origin", thirdPartyOrigin, + HttpHeaders.CONTENT_TYPE, "application/text"), + "return 0;".getBytes()); + assertAcNotAllowOrigin(response); + assertOkayResponse(response, "{\"result\":\"0\"}"); + } + + @Test + public void test1CorsIsEnabledOnAllDomainsGET() throws IOException { + final String shouldAllowOrigin = "http://foo.bar.com"; + setCorsFilterFeature(true, ImmutableList.of()); + HttpClient client = client(); + // preflight request + HttpToolResponse response = HttpTool.execAndConsume(client, httpOptionsRequest("server/status", "GET", shouldAllowOrigin)); + List accessControlAllowOrigin = response.getHeaderLists().get(HEADER_AC_ALLOW_ORIGIN); + assertEquals(accessControlAllowOrigin.size(), 1); + assertEquals(accessControlAllowOrigin.get(0), "*", "Should allow GET requests made from " + shouldAllowOrigin); + + assertEquals(response.getHeaderLists().get(HEADER_AC_ALLOW_HEADERS).size(), 1); + assertEquals(response.getHeaderLists().get(HEADER_AC_ALLOW_HEADERS).get(0), "x-csrf-token", "Should have asked and allowed x-csrf-token header from " + shouldAllowOrigin); + assertOkayResponse(response, ""); + + HttpUriRequest httpRequest = RequestBuilder.get(getBaseUriRest() + "server/status") + .addHeader("Origin", shouldAllowOrigin) + .addHeader(HEADER_AC_REQUEST_METHOD, "GET") + .build(); + response = HttpTool.execAndConsume(client, httpRequest); + accessControlAllowOrigin = response.getHeaderLists().get(HEADER_AC_ALLOW_ORIGIN); + assertEquals(accessControlAllowOrigin.size(), 1); + assertEquals(accessControlAllowOrigin.get(0), "*", "Should allow GET requests made from " + shouldAllowOrigin); + assertOkayResponse(response, "MASTER"); + } + + @Test + public void test1CorsIsEnabledOnAllDomainsByDefaultPOST() throws IOException { + final String shouldAllowOrigin = "http://foo.bar.com"; + BrooklynFeatureEnablement.enable(BrooklynFeatureEnablement.FEATURE_CORS_CXF_PROPERTY); + BrooklynRestApiLauncher apiLauncher = baseLauncher().withoutJsgui(); + // In this test, management context has no value set for Allowed Origins. + ManagementContext mgmt = LocalManagementContextForTests.builder(true) + .useAdditionalProperties(MutableMap.of( + BrooklynFeatureEnablement.FEATURE_CORS_CXF_PROPERTY, true) + ).build(); + apiLauncher.managementContext(mgmt); + useServerForTest(apiLauncher.start()); + HttpClient client = client(); + // preflight request + HttpToolResponse response = HttpTool.execAndConsume(client, httpOptionsRequest("script/groovy", "POST", shouldAllowOrigin)); + List accessControlAllowOrigin = response.getHeaderLists().get(HEADER_AC_ALLOW_ORIGIN); + assertEquals(accessControlAllowOrigin.size(), 1); + assertEquals(accessControlAllowOrigin.get(0), "*", "Should allow POST requests made from " + shouldAllowOrigin); + assertEquals(response.getHeaderLists().get(HEADER_AC_ALLOW_HEADERS).size(), 1); + assertEquals(response.getHeaderLists().get(HEADER_AC_ALLOW_HEADERS).get(0), "x-csrf-token", "Should have asked and allowed x-csrf-token header from " + shouldAllowOrigin); + assertOkayResponse(response, ""); + + response = HttpTool.httpPost( + client, URI.create(getBaseUriRest() + "script/groovy"), + ImmutableMap.of( + "Origin", shouldAllowOrigin, + HttpHeaders.CONTENT_TYPE, "application/text"), + "return 0;".getBytes()); + accessControlAllowOrigin = response.getHeaderLists().get(HEADER_AC_ALLOW_ORIGIN); + assertEquals(accessControlAllowOrigin.size(), 1); + assertEquals(accessControlAllowOrigin.get(0), "*", "Should allow GET requests made from " + shouldAllowOrigin); + assertOkayResponse(response, "{\"result\":\"0\"}"); + } + + @Test + public void test2CorsIsDisabled() throws IOException { + BrooklynFeatureEnablement.disable(BrooklynFeatureEnablement.FEATURE_CORS_CXF_PROPERTY); + final String shouldAllowOrigin = "http://foo.bar.com"; + setCorsFilterFeature(false, null); + + HttpClient client = client(); + HttpToolResponse response = HttpTool.execAndConsume(client, httpOptionsRequest("server/status", "GET", shouldAllowOrigin)); + assertAcNotAllowOrigin(response); + assertOkayResponse(response, ""); + + response = HttpTool.execAndConsume(client, httpOptionsRequest("script/groovy", shouldAllowOrigin, "POST")); + assertAcNotAllowOrigin(response); + assertOkayResponse(response, ""); + } + + private void setCorsFilterFeature(boolean enable, List allowedOrigins) { + if (enable) { + BrooklynFeatureEnablement.enable(BrooklynFeatureEnablement.FEATURE_CORS_CXF_PROPERTY); + } else { + BrooklynFeatureEnablement.disable(BrooklynFeatureEnablement.FEATURE_CORS_CXF_PROPERTY); + } + BrooklynRestApiLauncher apiLauncher = baseLauncher() + .withoutJsgui(); + ManagementContext mgmt = LocalManagementContextForTests.builder(true) + .useAdditionalProperties(MutableMap.of( + BrooklynFeatureEnablement.FEATURE_CORS_CXF_PROPERTY, enable, + CorsImplSupplierFilter.ALLOWED_ORIGINS.getName(), allowedOrigins) + ).build(); + apiLauncher.managementContext(mgmt); + useServerForTest(apiLauncher.start()); + } + + protected HttpClient client() { + return HttpClients.createMinimal(); + } + + public void assertAcAllowOrigin(HttpToolResponse response, String shouldAllowOrigin, String method) { + assertAcAllowOrigin(response, shouldAllowOrigin, method, true); + } + + public void assertAcAllowOrigin(HttpToolResponse response, String shouldAllowOrigin, String method, boolean preflightRequest) { + List accessControlAllowOrigin = response.getHeaderLists().get(HEADER_AC_ALLOW_ORIGIN); + assertEquals(accessControlAllowOrigin.size(), 1); + assertEquals(accessControlAllowOrigin.get(0), shouldAllowOrigin, "Should allow " + method + " requests made from " + shouldAllowOrigin); + + if (preflightRequest) { + List accessControlAllowHeaders = response.getHeaderLists().get(HEADER_AC_ALLOW_HEADERS); + assertEquals(accessControlAllowHeaders.size(), 1); + assertEquals(accessControlAllowHeaders.get(0), "x-csrf-token", "Should have asked and allowed x-csrf-token header from " + shouldAllowOrigin); + } + } + + public void assertAcNotAllowOrigin(HttpToolResponse response) { + List accessControlAllowOrigin = response.getHeaderLists().get(HEADER_AC_ALLOW_ORIGIN); + assertNull(accessControlAllowOrigin, "Access Control Header should not be available."); + } + + private HttpUriRequest httpOptionsRequest(String apiCall, String acRequestMethod, String origin) { + return RequestBuilder.options(getBaseUriRest() + apiCall) + .addHeader("Origin", origin) + .addHeader(HEADER_AC_REQUEST_HEADERS, "x-csrf-token") + .addHeader(HEADER_AC_REQUEST_METHOD, acRequestMethod) + .build(); + } +} diff --git a/rest/rest-server/src/test/java/org/apache/brooklyn/rest/CsrfTokenFilterLauncherTest.java b/rest/rest-server/src/test/java/org/apache/brooklyn/rest/CsrfTokenFilterLauncherTest.java index 31f9497173f..d4428758a08 100644 --- a/rest/rest-server/src/test/java/org/apache/brooklyn/rest/CsrfTokenFilterLauncherTest.java +++ b/rest/rest-server/src/test/java/org/apache/brooklyn/rest/CsrfTokenFilterLauncherTest.java @@ -117,9 +117,8 @@ protected HttpClient client() { .build(); } - protected void assertOkayResponse(HttpToolResponse response, String expecting) { + public static void assertOkayResponse(HttpToolResponse response, String expecting) { assertEquals(response.getResponseCode(), HttpStatus.SC_OK); assertEquals(response.getContentAsString(), expecting); } - }