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. *