diff --git a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java
index ef0d4ba41..45598c2a9 100644
--- a/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java
+++ b/hawkbit-autoconfigure/src/main/java/org/eclipse/hawkbit/autoconfigure/security/SecurityManagedConfiguration.java
@@ -84,7 +84,11 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationEn
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.security.web.session.SessionManagementFilter;
+import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.vaadin.spring.security.VaadinSecurityContext;
import org.vaadin.spring.security.annotation.EnableVaadinSecurity;
import org.vaadin.spring.security.web.VaadinRedirectStrategy;
@@ -450,6 +454,7 @@ public class SecurityManagedConfiguration {
* Security configuration for the REST management API.
*/
@Configuration
+ @EnableWebSecurity
@Order(350)
@ConditionalOnClass(MgmtApiConfiguration.class)
public static class RestSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
@@ -496,6 +501,11 @@ public class SecurityManagedConfiguration {
basicAuthEntryPoint.setRealmName(securityProperties.getBasicRealm());
HttpSecurity httpSec = http.regexMatcher("\\/rest.*|\\/system/admin.*").csrf().disable();
+
+ if (securityProperties.getCors().isEnabled()) {
+ httpSec = httpSec.cors().and();
+ }
+
if (securityProperties.isRequireSsl()) {
httpSec = httpSec.requiresChannel().anyRequest().requiresSecure().and();
}
@@ -527,6 +537,22 @@ public class SecurityManagedConfiguration {
httpSec.anonymous().disable();
httpSec.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
+
+ @Bean
+ @ConditionalOnProperty(prefix = "hawkbit.server.security.cors", name = "enabled", matchIfMissing = false)
+ CorsConfigurationSource corsConfigurationSource() {
+ final CorsConfiguration restCorsConfiguration = new CorsConfiguration();
+
+ restCorsConfiguration.setAllowedOrigins(securityProperties.getCors().getAllowedOrigins());
+ restCorsConfiguration.setAllowCredentials(true);
+ restCorsConfiguration.setAllowedHeaders(securityProperties.getCors().getAllowedHeaders());
+ restCorsConfiguration.setAllowedMethods(securityProperties.getCors().getAllowedMethods());
+
+ final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/rest/**", restCorsConfiguration);
+
+ return source;
+ }
}
/**
diff --git a/hawkbit-runtime/hawkbit-update-server/pom.xml b/hawkbit-runtime/hawkbit-update-server/pom.xml
index 6f1a52068..242b70f75 100644
--- a/hawkbit-runtime/hawkbit-update-server/pom.xml
+++ b/hawkbit-runtime/hawkbit-update-server/pom.xml
@@ -59,6 +59,34 @@
com.microsoft.sqlserver
mssql-jdbc
+
+
+
+ io.qameta.allure
+ allure-junit4
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+ org.mariadb.jdbc
+ mariadb-java-client
+ test
+
+
+ org.eclipse.hawkbit
+ hawkbit-repository-test
+ ${project.version}
+ test
+
diff --git a/hawkbit-runtime/hawkbit-update-server/src/main/resources/application.properties b/hawkbit-runtime/hawkbit-update-server/src/main/resources/application.properties
index 9f94ae985..b920fe136 100644
--- a/hawkbit-runtime/hawkbit-update-server/src/main/resources/application.properties
+++ b/hawkbit-runtime/hawkbit-update-server/src/main/resources/application.properties
@@ -33,3 +33,7 @@ spring.rabbitmq.port=5672
#hawkbit.server.im.users[0].firstname=Eclipse
#hawkbit.server.im.users[0].lastname=HawkBit
#hawkbit.server.im.users[0].permissions=ALL
+
+# Enable CORS and specify the allowed origins:
+#hawkbit.server.security.cors.enabled=true
+#hawkbit.server.security.cors.allowedOrigins=http://localhost
diff --git a/hawkbit-runtime/hawkbit-update-server/src/test/java/org/eclipse/hawkbit/app/CorsTest.java b/hawkbit-runtime/hawkbit-update-server/src/test/java/org/eclipse/hawkbit/app/CorsTest.java
new file mode 100644
index 000000000..42fe538d8
--- /dev/null
+++ b/hawkbit-runtime/hawkbit-update-server/src/test/java/org/eclipse/hawkbit/app/CorsTest.java
@@ -0,0 +1,88 @@
+/**
+ * Copyright (c) 2019 Bosch Software Innovations GmbH and others.
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.hawkbit.app;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.eclipse.hawkbit.mgmt.rest.api.MgmtRestConstants;
+import org.eclipse.hawkbit.repository.test.util.MsSqlTestDatabase;
+import org.eclipse.hawkbit.repository.test.util.MySqlTestDatabase;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.security.test.context.support.WithUserDetails;
+import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
+import org.springframework.test.context.TestExecutionListeners;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.test.context.TestExecutionListeners.MergeMode;
+
+import io.qameta.allure.Description;
+import io.qameta.allure.Feature;
+import io.qameta.allure.Story;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(properties = {"hawkbit.dmf.rabbitmq.enabled=false", "hawkbit.server.security.cors.enabled=true",
+ "hawkbit.server.security.cors.allowedOrigins=" + CorsTest.ALLOWED_ORIGIN_FIRST + "," + CorsTest.ALLOWED_ORIGIN_SECOND})
+@TestExecutionListeners(listeners = { MySqlTestDatabase.class, MsSqlTestDatabase.class },
+ mergeMode = MergeMode.MERGE_WITH_DEFAULTS)
+@Feature("Integration Test - Security")
+@Story("CORS")
+public class CorsTest {
+
+ final static String ALLOWED_ORIGIN_FIRST = "http://test.first.origin";
+ final static String ALLOWED_ORIGIN_SECOND = "http://test.second.origin";
+
+ private final static String INVALID_ORIGIN = "http://test.invalid.origin";
+ private final static String INVALID_CORS_REQUEST = "Invalid CORS request";
+
+ @Autowired
+ private WebApplicationContext context;
+
+ private MockMvc mvc;
+
+ @Before
+ public void setup() {
+ final DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(context)
+ .apply(SecurityMockMvcConfigurers.springSecurity()).dispatchOptions(true);
+ mvc = builder.build();
+ }
+
+ @WithUserDetails("admin")
+ @Test
+ @Description("Ensures that Cors is working.")
+ public void validateCorsRequest() throws Exception {
+ performOptionsRequestToRestWithOrigin(ALLOWED_ORIGIN_FIRST).andExpect(status().isOk());
+ performOptionsRequestToRestWithOrigin(ALLOWED_ORIGIN_SECOND).andExpect(status().isOk());
+
+ final String invalidOriginResponseBody = performOptionsRequestToRestWithOrigin(INVALID_ORIGIN)
+ .andExpect(status().isForbidden()).andReturn().getResponse().getContentAsString();
+ assertThat(invalidOriginResponseBody).isEqualTo(INVALID_CORS_REQUEST);
+
+ final String invalidCorsUrlResponseBody = performOptionsRequestToUrlWithOrigin(MgmtRestConstants.BASE_SYSTEM_MAPPING, ALLOWED_ORIGIN_FIRST)
+ .andExpect(status().isForbidden()).andReturn().getResponse().getContentAsString();
+ assertThat(invalidCorsUrlResponseBody).isEqualTo(INVALID_CORS_REQUEST);
+ }
+
+ private ResultActions performOptionsRequestToRestWithOrigin(final String origin) throws Exception {
+ return performOptionsRequestToUrlWithOrigin(MgmtRestConstants.BASE_V1_REQUEST_MAPPING, origin);
+ }
+
+ private ResultActions performOptionsRequestToUrlWithOrigin(final String url, final String origin) throws Exception {
+ return mvc.perform(options(url).header("Access-Control-Request-Method", "GET").header("Origin", origin));
+ }
+}
\ No newline at end of file
diff --git a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/HawkbitSecurityProperties.java b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/HawkbitSecurityProperties.java
index 97ef7bc3a..676e1b5f0 100644
--- a/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/HawkbitSecurityProperties.java
+++ b/hawkbit-security-core/src/main/java/org/eclipse/hawkbit/security/HawkbitSecurityProperties.java
@@ -8,6 +8,10 @@
*/
package org.eclipse.hawkbit.security;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
@@ -19,6 +23,7 @@ public class HawkbitSecurityProperties {
private final Clients clients = new Clients();
private final Dos dos = new Dos();
+ private final Cors cors = new Cors();
/**
* Content Security policy Header for Manager UI.
@@ -68,6 +73,69 @@ public class HawkbitSecurityProperties {
return clients;
}
+ public Cors getCors() {
+ return cors;
+ }
+
+ /**
+ * Security configuration related to CORS.
+ *
+ */
+ public static class Cors {
+
+ /**
+ * Flag to enable CORS.
+ */
+ private boolean enabled = false;
+
+ /**
+ * Allowed origins for CORS.
+ */
+ private List allowedOrigins = Collections.singletonList("http://localhost");
+
+ /**
+ * Allowed headers for CORS.
+ */
+ private List allowedHeaders = Collections.singletonList("*");
+
+ /**
+ * Allowed methods for CORS.
+ */
+ private List allowedMethods = Arrays.asList("DELETE", "GET", "POST", "PATCH", "PUT");
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(final boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public List getAllowedOrigins() {
+ return allowedOrigins;
+ }
+
+ public void setAllowedOrigins(final List allowedOrigins) {
+ this.allowedOrigins = allowedOrigins;
+ }
+
+ public List getAllowedHeaders() {
+ return allowedHeaders;
+ }
+
+ public void setAllowedHeaders(final List allowedHeaders) {
+ this.allowedHeaders = allowedHeaders;
+ }
+
+ public List getAllowedMethods() {
+ return allowedMethods;
+ }
+
+ public void setAllowedMethods(final List allowedMethods) {
+ this.allowedMethods = allowedMethods;
+ }
+ }
+
/**
* Security configuration related to clients.
*