JAMES-1960 Allow CORS request on WebAdmin (configuration defined)
Project: http://git-wip-us.apache.org/repos/asf/james-project/repo Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/14634877 Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/14634877 Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/14634877 Branch: refs/heads/master Commit: 1463487763dc70f801922067a1d5e7b0f933bd6f Parents: 0b5d95b Author: benwa <btell...@linagora.com> Authored: Thu Mar 9 14:40:56 2017 +0700 Committer: benwa <btell...@linagora.com> Committed: Wed Mar 15 09:02:26 2017 +0700 ---------------------------------------------------------------------- .../destination/conf/webadmin.properties | 6 +- .../destination/conf/webadmin.properties | 6 +- .../modules/server/WebAdminServerModule.java | 2 + .../org/apache/james/webadmin/CORSFilter.java | 40 +++++++++++ .../james/webadmin/WebAdminConfiguration.java | 49 ++++++++++++-- .../apache/james/webadmin/WebAdminServer.java | 30 ++++++--- .../webadmin/authentication/JwtFilter.java | 15 +++-- .../apache/james/webadmin/routes/CORSRoute.java | 42 ++++++++++++ .../webadmin/WebAdminConfigurationTest.java | 70 +++++++++++++++++++- 9 files changed, 236 insertions(+), 24 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/james-project/blob/14634877/dockerfiles/run/guice/cassandra-ldap/destination/conf/webadmin.properties ---------------------------------------------------------------------- diff --git a/dockerfiles/run/guice/cassandra-ldap/destination/conf/webadmin.properties b/dockerfiles/run/guice/cassandra-ldap/destination/conf/webadmin.properties index a9aced0..38e2ba0 100644 --- a/dockerfiles/run/guice/cassandra-ldap/destination/conf/webadmin.properties +++ b/dockerfiles/run/guice/cassandra-ldap/destination/conf/webadmin.properties @@ -33,4 +33,8 @@ https.enabled=false #https.trust.password # Defaults to false -#jwt.enabled=true \ No newline at end of file +#jwt.enabled=true + +# Defaults to false +#cors.enable=true +#cors.origin \ No newline at end of file http://git-wip-us.apache.org/repos/asf/james-project/blob/14634877/dockerfiles/run/guice/cassandra/destination/conf/webadmin.properties ---------------------------------------------------------------------- diff --git a/dockerfiles/run/guice/cassandra/destination/conf/webadmin.properties b/dockerfiles/run/guice/cassandra/destination/conf/webadmin.properties index a9aced0..38e2ba0 100644 --- a/dockerfiles/run/guice/cassandra/destination/conf/webadmin.properties +++ b/dockerfiles/run/guice/cassandra/destination/conf/webadmin.properties @@ -33,4 +33,8 @@ https.enabled=false #https.trust.password # Defaults to false -#jwt.enabled=true \ No newline at end of file +#jwt.enabled=true + +# Defaults to false +#cors.enable=true +#cors.origin \ No newline at end of file http://git-wip-us.apache.org/repos/asf/james-project/blob/14634877/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/WebAdminServerModule.java ---------------------------------------------------------------------- diff --git a/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/WebAdminServerModule.java b/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/WebAdminServerModule.java index 68e1b43..1532471 100644 --- a/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/WebAdminServerModule.java +++ b/server/container/guice/protocols/webadmin/src/main/java/org/apache/james/modules/server/WebAdminServerModule.java @@ -80,6 +80,8 @@ public class WebAdminServerModule extends AbstractModule { .enable(configurationFile.getBoolean("enabled", false)) .port(new FixedPort(configurationFile.getInt("port", WebAdminServer.DEFAULT_PORT))) .https(readHttpsConfiguration(configurationFile)) + .enableCORS(configurationFile.getBoolean("cors.enable", false)) + .urlCORSOrigin(configurationFile.getString("cors.origin", null)) .build(); } catch (FileNotFoundException e) { return WebAdminConfiguration.builder() http://git-wip-us.apache.org/repos/asf/james-project/blob/14634877/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/CORSFilter.java ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/CORSFilter.java b/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/CORSFilter.java new file mode 100644 index 0000000..c4cfb29 --- /dev/null +++ b/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/CORSFilter.java @@ -0,0 +1,40 @@ +/**************************************************************** + * 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.james.webadmin; + +import spark.Filter; +import spark.Request; +import spark.Response; + + +public class CORSFilter implements Filter { + private final String urlCORSOrigin; + + public CORSFilter(String urlCORSOrigin) { + this.urlCORSOrigin = urlCORSOrigin; + } + + @Override + public void handle(Request request, Response response) throws Exception { + response.header("Access-Control-Allow-Origin", urlCORSOrigin); + response.header("Access-Control-Request-Method", "DELETE, GET, POST, PUT"); + response.header("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept"); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/14634877/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/WebAdminConfiguration.java ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/WebAdminConfiguration.java b/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/WebAdminConfiguration.java index 8cb022a..c87589b 100644 --- a/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/WebAdminConfiguration.java +++ b/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/WebAdminConfiguration.java @@ -28,6 +28,9 @@ import com.google.common.base.Preconditions; public class WebAdminConfiguration { + public static final boolean DEFAULT_CORS_DISABLED = false; + public static final String CORS_ALL_ORIGINS = "*"; + public static Builder builder() { return new Builder(); } @@ -35,7 +38,9 @@ public class WebAdminConfiguration { public static class Builder { private Optional<Boolean> enabled = Optional.empty(); private Port port; + private Optional<Boolean> enableCORS = Optional.empty(); private Optional<HttpsConfiguration> httpsConfiguration = Optional.empty(); + private Optional<String> urlCORSOrigin = Optional.empty(); public Builder https(HttpsConfiguration httpsConfiguration) { this.httpsConfiguration = Optional.of(httpsConfiguration); @@ -51,7 +56,6 @@ public class WebAdminConfiguration { this.enabled = Optional.of(isEnabled); return this; } - public Builder enabled() { return enable(true); } @@ -60,6 +64,24 @@ public class WebAdminConfiguration { return enable(false); } + public Builder urlCORSOrigin(String origin) { + this.urlCORSOrigin = Optional.ofNullable(origin); + return this; + } + + public Builder enableCORS(boolean isEnabled) { + this.enableCORS = Optional.of(isEnabled); + return this; + } + + public Builder CORSenabled() { + return enableCORS(true); + } + + public Builder CORSdisabled() { + return enableCORS(false); + } + public WebAdminConfiguration build() { Preconditions.checkState(enabled.isPresent(), "You need to explicitly enable or disable WebAdmin server"); Preconditions.checkState(!enabled.get() || port != null, "You need to specify a port for WebAdminConfiguration"); @@ -68,25 +90,35 @@ public class WebAdminConfiguration { httpsConfiguration.orElse( HttpsConfiguration.builder() .disabled() - .build())); + .build()), + enableCORS.orElse(DEFAULT_CORS_DISABLED), + urlCORSOrigin.orElse(CORS_ALL_ORIGINS)); } } private final boolean enabled; private final Port port; private final HttpsConfiguration httpsConfiguration; + private final boolean enableCORS; + private final String urlCORSOrigin; @VisibleForTesting - WebAdminConfiguration(boolean enabled, Port port, HttpsConfiguration httpsConfiguration) { + WebAdminConfiguration(boolean enabled, Port port, HttpsConfiguration httpsConfiguration, boolean enableCORS, String urlCORSOrigin) { this.enabled = enabled; this.port = port; this.httpsConfiguration = httpsConfiguration; + this.enableCORS = enableCORS; + this.urlCORSOrigin = urlCORSOrigin; } public boolean isEnabled() { return enabled; } + public String getUrlCORSOrigin() { + return urlCORSOrigin; + } + public Port getPort() { return port; } @@ -95,19 +127,26 @@ public class WebAdminConfiguration { return httpsConfiguration; } + public boolean isEnableCORS() { + return enableCORS; + } + @Override public final boolean equals(Object o) { if (o instanceof WebAdminConfiguration) { WebAdminConfiguration that = (WebAdminConfiguration) o; return Objects.equals(this.enabled, that.enabled) - && Objects.equals(this.port, that.port); + && Objects.equals(this.port, that.port) + && Objects.equals(this.httpsConfiguration, that.httpsConfiguration) + && Objects.equals(this.enableCORS, that.enableCORS) + && Objects.equals(this.urlCORSOrigin, that.urlCORSOrigin); } return false; } @Override public final int hashCode() { - return Objects.hash(enabled, port); + return Objects.hash(enabled, port, httpsConfiguration, enableCORS, urlCORSOrigin); } } http://git-wip-us.apache.org/repos/asf/james-project/blob/14634877/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/WebAdminServer.java ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/WebAdminServer.java b/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/WebAdminServer.java index 7457219..9c398b4 100644 --- a/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/WebAdminServer.java +++ b/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/WebAdminServer.java @@ -30,6 +30,7 @@ import org.apache.commons.configuration.HierarchicalConfiguration; import org.apache.james.lifecycle.api.Configurable; import org.apache.james.webadmin.authentication.AuthenticationFilter; import org.apache.james.webadmin.authentication.NoAuthenticationFilter; +import org.apache.james.webadmin.routes.CORSRoute; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -72,20 +73,33 @@ public class WebAdminServer implements Configurable { public void configure(HierarchicalConfiguration config) throws ConfigurationException { if (configuration.isEnabled()) { service.port(configuration.getPort().toInt()); - HttpsConfiguration httpsConfiguration = configuration.getHttpsConfiguration(); - if (httpsConfiguration.isEnabled()) { - service.secure(httpsConfiguration.getKeystoreFilePath(), - httpsConfiguration.getKeystorePassword(), - httpsConfiguration.getTruststoreFilePath(), - httpsConfiguration.getTruststorePassword()); - LOGGER.info("Web admin set up to use HTTPS"); - } + configureHTTPS(); + configureCORS(); service.before(authenticationFilter); routesList.forEach(routes -> routes.define(service)); LOGGER.info("Web admin server started"); } } + private void configureHTTPS() { + HttpsConfiguration httpsConfiguration = configuration.getHttpsConfiguration(); + if (httpsConfiguration.isEnabled()) { + service.secure(httpsConfiguration.getKeystoreFilePath(), + httpsConfiguration.getKeystorePassword(), + httpsConfiguration.getTruststoreFilePath(), + httpsConfiguration.getTruststorePassword()); + LOGGER.info("Web admin set up to use HTTPS"); + } + } + + private void configureCORS() { + if (configuration.isEnabled()) { + service.before(new CORSFilter(configuration.getUrlCORSOrigin())); + new CORSRoute().define(service); + LOGGER.info("Web admin set up to enable CORS from " + configuration.getUrlCORSOrigin()); + } + } + @PreDestroy public void destroy() { if (configuration.isEnabled()) { http://git-wip-us.apache.org/repos/asf/james-project/blob/14634877/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/authentication/JwtFilter.java ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/authentication/JwtFilter.java b/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/authentication/JwtFilter.java index 991f412..4832d6f 100644 --- a/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/authentication/JwtFilter.java +++ b/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/authentication/JwtFilter.java @@ -33,6 +33,7 @@ import spark.Response; public class JwtFilter implements AuthenticationFilter { public static final String AUTHORIZATION_HEADER_PREFIX = "Bearer "; public static final String AUTHORIZATION_HEADER_NAME = "Authorization"; + public static final String OPTIONS = "OPTIONS"; private final JwtTokenVerifier jwtTokenVerifier; @@ -43,13 +44,15 @@ public class JwtFilter implements AuthenticationFilter { @Override public void handle(Request request, Response response) throws Exception { - Optional<String> bearer = Optional.ofNullable(request.headers(AUTHORIZATION_HEADER_NAME)) - .filter(value -> value.startsWith(AUTHORIZATION_HEADER_PREFIX)) - .map(value -> value.substring(AUTHORIZATION_HEADER_PREFIX.length())); + if (request.requestMethod() != OPTIONS) { + Optional<String> bearer = Optional.ofNullable(request.headers(AUTHORIZATION_HEADER_NAME)) + .filter(value -> value.startsWith(AUTHORIZATION_HEADER_PREFIX)) + .map(value -> value.substring(AUTHORIZATION_HEADER_PREFIX.length())); - checkHeaderPresent(bearer); - checkValidSignature(bearer); - checkIsAdmin(bearer); + checkHeaderPresent(bearer); + checkValidSignature(bearer); + checkIsAdmin(bearer); + } } private void checkHeaderPresent(Optional<String> bearer) { http://git-wip-us.apache.org/repos/asf/james-project/blob/14634877/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/routes/CORSRoute.java ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/routes/CORSRoute.java b/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/routes/CORSRoute.java new file mode 100644 index 0000000..39e9bd7 --- /dev/null +++ b/server/protocols/webadmin/src/main/java/org/apache/james/webadmin/routes/CORSRoute.java @@ -0,0 +1,42 @@ +/**************************************************************** + * 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.james.webadmin.routes; + +import org.apache.james.webadmin.Routes; + +import spark.Service; + +public class CORSRoute implements Routes { + + @Override + public void define(Service service) { + service.options("/*", (request, response) -> { + String accessControlRequestHeaders = request.headers("Access-Control-Request-Headers"); + if (accessControlRequestHeaders != null) { + response.header("Access-Control-Allow-Headers", accessControlRequestHeaders); + } + String accessControlRequestMethod = request.headers("Access-Control-Request-Method"); + if (accessControlRequestMethod != null) { + response.header("Access-Control-Allow-Methods", accessControlRequestMethod); + } + return ""; + }); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/14634877/server/protocols/webadmin/src/test/java/org/apache/james/webadmin/WebAdminConfigurationTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/src/test/java/org/apache/james/webadmin/WebAdminConfigurationTest.java b/server/protocols/webadmin/src/test/java/org/apache/james/webadmin/WebAdminConfigurationTest.java index d20100c..4606e94 100644 --- a/server/protocols/webadmin/src/test/java/org/apache/james/webadmin/WebAdminConfigurationTest.java +++ b/server/protocols/webadmin/src/test/java/org/apache/james/webadmin/WebAdminConfigurationTest.java @@ -46,7 +46,11 @@ public class WebAdminConfigurationTest { assertThat(WebAdminConfiguration.builder() .disabled() .build()) - .isEqualTo(new WebAdminConfiguration(false, null, HttpsConfiguration.builder().disabled().build())); + .isEqualTo(new WebAdminConfiguration(false, + null, + HttpsConfiguration.builder().disabled().build(), + false, + "*")); } @Test @@ -63,7 +67,12 @@ public class WebAdminConfigurationTest { .enabled() .port(PORT) .build()) - .isEqualTo(new WebAdminConfiguration(true, PORT, HttpsConfiguration.builder().disabled().build())); + .isEqualTo(new WebAdminConfiguration( + true, + PORT, + HttpsConfiguration.builder().disabled().build(), + false, + "*")); } @Test @@ -79,7 +88,62 @@ public class WebAdminConfigurationTest { .https(httpsConfiguration) .port(PORT) .build()) - .isEqualTo(new WebAdminConfiguration(true, PORT, httpsConfiguration)); + .isEqualTo(new WebAdminConfiguration( + true, + PORT, + httpsConfiguration, + false, + "*")); + } + + @Test + public void builderShouldCORSEnabled() { + assertThat( + WebAdminConfiguration.builder() + .enabled() + .port(PORT) + .CORSenabled() + .build()) + .isEqualTo(new WebAdminConfiguration( + true, + PORT, + HttpsConfiguration.builder().disabled().build(), + true, + "*")); + } + + @Test + public void builderShouldCORSDisabled() { + assertThat( + WebAdminConfiguration.builder() + .enabled() + .port(PORT) + .CORSdisabled() + .build()) + .isEqualTo(new WebAdminConfiguration( + true, + PORT, + HttpsConfiguration.builder().disabled().build(), + false, + "*")); + } + + @Test + public void builderShouldCORSWithOrigin() { + String origin = "linagora.com"; + assertThat( + WebAdminConfiguration.builder() + .enabled() + .port(PORT) + .CORSenabled() + .urlCORSOrigin(origin) + .build()) + .isEqualTo(new WebAdminConfiguration( + true, + PORT, + HttpsConfiguration.builder().disabled().build(), + true, + origin)); } @Test --------------------------------------------------------------------- To unsubscribe, e-mail: server-dev-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org