Skip to content

Commit

Permalink
Add CXF CrossOriginResourceSharing filter
Browse files Browse the repository at this point in the history
  • Loading branch information
bostko committed Jan 17, 2017
1 parent 2bdbf14 commit 61fa6a5
Show file tree
Hide file tree
Showing 10 changed files with 264 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ 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";

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
Expand Down
1 change: 1 addition & 0 deletions karaf/features/src/main/feature/feature.xml
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@
<feature>brooklyn-camp-base</feature>

<feature>cxf-jaxrs</feature>
<bundle dependency="true">mvn:org.apache.cxf/cxf-rt-rs-security-cors/${cxf.version}</bundle>

<bundle dependency="true">mvn:com.fasterxml.jackson.jaxrs/jackson-jaxrs-json-provider/${fasterxml.jackson.version}</bundle>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -80,6 +83,7 @@
import org.apache.brooklyn.util.text.Identifiers;
import org.apache.brooklyn.util.text.Strings;
import org.apache.brooklyn.util.web.ContextHandlerCollectionHotSwappable;
import org.apache.cxf.rs.security.cors.CrossOriginResourceSharingFilter;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.jaas.JAASLoginService;
import org.eclipse.jetty.server.Connector;
Expand All @@ -99,6 +103,8 @@
import com.google.common.base.Throwables;
import com.google.common.collect.Maps;

import static org.apache.brooklyn.rest.filter.CorsImplSupplierFilter.ALLOWED_ORIGINS;

/**
* Starts the web-app running, connected to the given management context
*/
Expand Down Expand Up @@ -455,14 +461,21 @@ public synchronized void start() throws Exception {
}

private WebAppContext deployRestApi(WebAppContext context) {
RestApiSetup.installRest(context,
ImmutableList.Builder<Object> providersListBuilder = ImmutableList.builder();
providersListBuilder.add(
new ManagementContextProvider(),
new ShutdownHandlerProvider(shutdownHandler),
new RequestTaggingRsFilter(),
new NoCacheFilter(),
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);
Expand Down
5 changes: 5 additions & 0 deletions rest/rest-resources/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-rs-security-cors</artifactId>
<version>${cxf.version}</version>
</dependency>

<dependency>
<groupId>org.apache.brooklyn</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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.reflect.TypeToken;
import org.apache.brooklyn.api.mgmt.ManagementContext;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.BrooklynFeatureEnablement;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.util.JavaGroovyEquivalents;
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;

@Provider
public class CorsImplSupplierFilter extends CrossOriginResourceSharingFilter {
public static final ConfigKey<List<String>> ALLOWED_ORIGINS = ConfigKeys.newConfigKey(new TypeToken<List<String>>() {}, BrooklynFeatureEnablement.FEATURE_CORS_CXF_PROPERTY + ".allowedOrigins");
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.warn("CORS brooklyn feature enabled.");
}
}

public CorsImplSupplierFilter(@Nullable ManagementContext managementContext) {
setFindResourceMethod(false);
if (managementContext != null) {
setAllowOrigins(JavaGroovyEquivalents.<List<String>>elvis(managementContext.getConfig().getConfig(ALLOWED_ORIGINS), Collections.<String>emptyList()));
}
}

@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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public Response toResponse(Throwable throwable1) {
Exceptions.collapse(throwable1));
} else {
LOG.debug("REST request running as {} threw: {}", Entitlements.getEntitlementContext(),
Exceptions.collapse(throwable1));
Exceptions.collapse(throwable1));
}
if (LOG.isTraceEnabled()) {
LOG.trace("Full details of "+Entitlements.getEntitlementContext()+" "+throwable1, throwable1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ limitations under the License.
<bean class="org.apache.brooklyn.rest.util.ShutdownHandlerProvider">
<argument ref="shutdownHandler" />
</bean>
<bean class="org.apache.brooklyn.rest.filter.CorsImplSupplierFilter" >
<argument ref="localManagementContext" />
</bean>
</jaxrs:providers>

<jaxrs:properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,11 +32,13 @@
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;
Expand All @@ -56,6 +59,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;
Expand Down Expand Up @@ -232,14 +236,21 @@ private WebAppContext servletContextHandler(ManagementContext managementContext)
context.setAttribute(BrooklynServiceAttributes.BROOKLYN_MANAGEMENT_CONTEXT, managementContext);

installWar(context);
RestApiSetup.installRest(context,
ImmutableList.Builder<Object> providersListBuilder = ImmutableList.builder();
providersListBuilder.add(
new ManagementContextProvider(),
new ShutdownHandlerProvider(shutdownListener),
new RequestTaggingRsFilter(),
new NoCacheFilter(),
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("/");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* 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.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.HEADER_AC_ALLOW_ORIGIN;
import static org.apache.cxf.rs.security.cors.CorsHeaderConstants.HEADER_AC_REQUEST_METHOD;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNull;

public class CorsFilterLauncherTest extends BrooklynRestApiLauncherTestFixture {
@Test
public void testEnabledCorsSendsBasicAllowResponse() throws IOException {
BrooklynFeatureEnablement.enable(BrooklynFeatureEnablement.FEATURE_CORS_CXF_PROPERTY);
final String shouldAllowOrigin = "http://foo.bar.com";
BrooklynRestApiLauncher apiLauncher = baseLauncher()
.withoutJsgui();
ManagementContext mgmt = LocalManagementContextForTests.builder(true)
.useAdditionalProperties(ImmutableMap.<String, Object>of(
BrooklynFeatureEnablement.FEATURE_CORS_CXF_PROPERTY, true,
CorsImplSupplierFilter.ALLOWED_ORIGINS.getName(), ImmutableList.of(shouldAllowOrigin))
).build();
apiLauncher.managementContext(mgmt);
useServerForTest(apiLauncher.start());

// preflight request
HttpClient client = client();
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");
assertOkayResponse(response, "MASTER");

// preflight request
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.<String,String>of(
"Origin", shouldAllowOrigin,
HttpHeaders.CONTENT_TYPE, "application/text"),
"return 0;".getBytes());
assertAcAllowOrigin(response, shouldAllowOrigin, "POST");
assertOkayResponse(response, "{\"result\":\"0\"}");

final String thirdPartyOrigin = "http://foo.bar1.com";

// preflight request
response = HttpTool.execAndConsume(client, httpOptionsRequest("server/status", "GET", thirdPartyOrigin));
assertAcNotAllowOrigin(response);
assertOkayResponse(response, "");

// 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.<String,String>of(
"Origin", thirdPartyOrigin,
HttpHeaders.CONTENT_TYPE, "application/text"),
"return 0;".getBytes());
assertAcNotAllowOrigin(response);
assertOkayResponse(response, "{\"result\":\"0\"}");
}

@Test
public void testCorsIsDisabled() throws IOException {
BrooklynFeatureEnablement.disable(BrooklynFeatureEnablement.FEATURE_CORS_CXF_PROPERTY);
final String shouldAllowOrigin = "http://foo.bar.com";
BrooklynRestApiLauncher apiLauncher = baseLauncher()
.withoutJsgui();
ManagementContext mgmt = LocalManagementContextForTests.builder(true)
.useAdditionalProperties(ImmutableMap.<String, Object>of(
BrooklynFeatureEnablement.FEATURE_CORS_CXF_PROPERTY, false)
).build();
apiLauncher.managementContext(mgmt);
useServerForTest(apiLauncher.start());

HttpClient client = client();
HttpToolResponse response = HttpTool.execAndConsume(client, httpOptionsRequest("server/status", "GET", shouldAllowOrigin));
assertNull(response.getHeaderLists().get(HEADER_AC_ALLOW_ORIGIN), "Access Control Header should not be available.");
assertOkayResponse(response, "");

response = HttpTool.execAndConsume(client, httpOptionsRequest("script/groovy", shouldAllowOrigin, "POST"));
assertNull(response.getHeaderLists().get(HEADER_AC_ALLOW_ORIGIN), "Access Control Header should not be available.");
assertOkayResponse(response, "");
}

protected HttpClient client() {
return HttpClients.createMinimal();
}

public void assertAcAllowOrigin(HttpToolResponse response, String shouldAllowOrigin, String method) {
List<String> accessControlAllowOrigin = response.getHeaderLists().get(HEADER_AC_ALLOW_ORIGIN);
assertEquals(accessControlAllowOrigin.size(), 1);
assertEquals(accessControlAllowOrigin.get(0), shouldAllowOrigin, "Should allow " + method + " requests made from " + shouldAllowOrigin);
}

public void assertAcNotAllowOrigin(HttpToolResponse response) {
List<String> 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_METHOD, acRequestMethod)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

}

0 comments on commit 61fa6a5

Please sign in to comment.