diff --git a/distro/run/assembly/pom.xml b/distro/run/assembly/pom.xml index 6163b80252..9a63326ee4 100644 --- a/distro/run/assembly/pom.xml +++ b/distro/run/assembly/pom.xml @@ -54,7 +54,7 @@ org.operaton.bpm.run operaton-bpm-run-modules-oauth2 ${project.version} - jar + pom diff --git a/distro/run/modules/oauth2/pom.xml b/distro/run/modules/oauth2/pom.xml index 21c13db8fb..833b8203bb 100644 --- a/distro/run/modules/oauth2/pom.xml +++ b/distro/run/modules/oauth2/pom.xml @@ -11,7 +11,7 @@ operaton-bpm-run-modules-oauth2 Operaton Platform - Run - Module Spring Security - jar + pom @@ -31,12 +43,6 @@ spring-boot-starter-oauth2-client - - ${project.groupId} - operaton-bpm-spring-boot-starter-rest - ${project.version} - test - operaton-bpm-spring-boot-starter-test ${project.groupId} diff --git a/distro/run/modules/oauth2/src/main/java/org/operaton/bpm/spring/boot/starter/security/OperatonBpmSpringSecurityDisableAutoConfiguration.java b/spring-boot-starter/starter-security/src/main/java/org/operaton/bpm/spring/boot/starter/security/oauth2/OperatonBpmSpringSecurityDisableAutoConfiguration.java similarity index 86% rename from distro/run/modules/oauth2/src/main/java/org/operaton/bpm/spring/boot/starter/security/OperatonBpmSpringSecurityDisableAutoConfiguration.java rename to spring-boot-starter/starter-security/src/main/java/org/operaton/bpm/spring/boot/starter/security/oauth2/OperatonBpmSpringSecurityDisableAutoConfiguration.java index 53d0ace690..1578a02c87 100644 --- a/distro/run/modules/oauth2/src/main/java/org/operaton/bpm/spring/boot/starter/security/OperatonBpmSpringSecurityDisableAutoConfiguration.java +++ b/spring-boot-starter/starter-security/src/main/java/org/operaton/bpm/spring/boot/starter/security/oauth2/OperatonBpmSpringSecurityDisableAutoConfiguration.java @@ -14,15 +14,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.operaton.bpm.spring.boot.starter.security; +package org.operaton.bpm.spring.boot.starter.security.oauth2; +import org.operaton.bpm.spring.boot.starter.security.oauth2.impl.ClientsNotConfiguredCondition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.web.SecurityFilterChain; +@Conditional(ClientsNotConfiguredCondition.class) public class OperatonBpmSpringSecurityDisableAutoConfiguration { private static final Logger logger = LoggerFactory.getLogger(OperatonBpmSpringSecurityDisableAutoConfiguration.class); diff --git a/spring-boot-starter/starter-security/src/main/java/org/operaton/bpm/spring/boot/starter/security/oauth2/OperatonSpringSecurityOAuth2AutoConfiguration.java b/spring-boot-starter/starter-security/src/main/java/org/operaton/bpm/spring/boot/starter/security/oauth2/OperatonSpringSecurityOAuth2AutoConfiguration.java new file mode 100644 index 0000000000..64c827f6f3 --- /dev/null +++ b/spring-boot-starter/starter-security/src/main/java/org/operaton/bpm/spring/boot/starter/security/oauth2/OperatonSpringSecurityOAuth2AutoConfiguration.java @@ -0,0 +1,92 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. Camunda licenses this file to you under the Apache License, + * Version 2.0; 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.operaton.bpm.spring.boot.starter.security.oauth2; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.Filter; +import org.operaton.bpm.engine.rest.security.auth.ProcessEngineAuthenticationFilter; +import org.operaton.bpm.engine.spring.SpringProcessEngineServicesConfiguration; +import org.operaton.bpm.spring.boot.starter.OperatonBpmAutoConfiguration; +import org.operaton.bpm.spring.boot.starter.property.OperatonBpmProperties; +import org.operaton.bpm.spring.boot.starter.property.WebappProperty; +import org.operaton.bpm.spring.boot.starter.security.oauth2.impl.OAuth2AuthenticationProvider; +import org.operaton.bpm.webapp.impl.security.auth.ContainerBasedAuthenticationFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.boot.autoconfigure.security.oauth2.client.ClientsConfiguredCondition; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.core.Ordered; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; + +import java.util.Map; + +@AutoConfigureOrder(OperatonSpringSecurityOAuth2AutoConfiguration.CAMUNDA_OAUTH2_ORDER) +@AutoConfigureAfter({ OperatonBpmAutoConfiguration.class, SpringProcessEngineServicesConfiguration.class }) +@ConditionalOnBean(OperatonBpmProperties.class) +@Conditional(ClientsConfiguredCondition.class) +public class OperatonSpringSecurityOAuth2AutoConfiguration { + + private static final Logger logger = LoggerFactory.getLogger(OperatonSpringSecurityOAuth2AutoConfiguration.class); + public static final int CAMUNDA_OAUTH2_ORDER = Ordered.HIGHEST_PRECEDENCE + 100; + private final String webappPath; + + public OperatonSpringSecurityOAuth2AutoConfiguration(OperatonBpmProperties properties) { + WebappProperty webapp = properties.getWebapp(); + this.webappPath = webapp.getApplicationPath(); + } + + @Bean + public FilterRegistrationBean webappAuthenticationFilter() { + FilterRegistrationBean filterRegistration = new FilterRegistrationBean<>(); + filterRegistration.setName("Container Based Authentication Filter"); + filterRegistration.setFilter(new ContainerBasedAuthenticationFilter()); + filterRegistration.setInitParameters(Map.of( + ProcessEngineAuthenticationFilter.AUTHENTICATION_PROVIDER_PARAM, OAuth2AuthenticationProvider.class.getName())); + // make sure the filter is registered after the Spring Security Filter Chain + filterRegistration.setOrder(SecurityProperties.DEFAULT_FILTER_ORDER + 1); + filterRegistration.addUrlPatterns(webappPath + "/app/*", webappPath + "/api/*"); + filterRegistration.setDispatcherTypes(DispatcherType.REQUEST); + return filterRegistration; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + logger.info("Enabling Operaton Spring Security oauth2 integration"); + + http.authorizeHttpRequests(c -> c + .requestMatchers(webappPath + "/app/**").authenticated() + .requestMatchers(webappPath + "/api/**").authenticated() + .anyRequest().permitAll() + ) + .oauth2Login(Customizer.withDefaults()) + .oidcLogout(Customizer.withDefaults()) + .oauth2Client(Customizer.withDefaults()) + .csrf(AbstractHttpConfigurer::disable); + + return http.build(); + } + +} diff --git a/spring-boot-starter/starter-security/src/main/java/org/operaton/bpm/spring/boot/starter/security/oauth2/impl/ClientsNotConfiguredCondition.java b/spring-boot-starter/starter-security/src/main/java/org/operaton/bpm/spring/boot/starter/security/oauth2/impl/ClientsNotConfiguredCondition.java new file mode 100644 index 0000000000..1513dc73a5 --- /dev/null +++ b/spring-boot-starter/starter-security/src/main/java/org/operaton/bpm/spring/boot/starter/security/oauth2/impl/ClientsNotConfiguredCondition.java @@ -0,0 +1,34 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. Camunda licenses this file to you under the Apache License, + * Version 2.0; 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.operaton.bpm.spring.boot.starter.security.oauth2.impl; + +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.security.oauth2.client.ClientsConfiguredCondition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * Condition that matches if no {@code spring.security.oauth2.client.registration} properties are defined + * by inverting the outcome of {@link ClientsConfiguredCondition}. + */ +public class ClientsNotConfiguredCondition extends ClientsConfiguredCondition { + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + var matchOutcome = super.getMatchOutcome(context, metadata); + return new ConditionOutcome(!matchOutcome.isMatch(), matchOutcome.getConditionMessage()); + } +} diff --git a/spring-boot-starter/starter-security/src/main/java/org/operaton/bpm/spring/boot/starter/security/oauth2/impl/OAuth2AuthenticationProvider.java b/spring-boot-starter/starter-security/src/main/java/org/operaton/bpm/spring/boot/starter/security/oauth2/impl/OAuth2AuthenticationProvider.java new file mode 100644 index 0000000000..30c73a7816 --- /dev/null +++ b/spring-boot-starter/starter-security/src/main/java/org/operaton/bpm/spring/boot/starter/security/oauth2/impl/OAuth2AuthenticationProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. Camunda licenses this file to you under the Apache License, + * Version 2.0; 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.operaton.bpm.spring.boot.starter.security.oauth2.impl; + +import jakarta.servlet.http.HttpServletRequest; +import org.operaton.bpm.engine.ProcessEngine; +import org.operaton.bpm.engine.rest.security.auth.AuthenticationResult; +import org.operaton.bpm.engine.rest.security.auth.impl.ContainerBasedAuthenticationProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; + +public class OAuth2AuthenticationProvider extends ContainerBasedAuthenticationProvider { + + private static final Logger logger = LoggerFactory.getLogger(OAuth2AuthenticationProvider.class); + + @Override + public AuthenticationResult extractAuthenticatedUser(HttpServletRequest request, ProcessEngine engine) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null) { + logger.debug("Authentication is null"); + return AuthenticationResult.unsuccessful(); + } + + if (!(authentication instanceof OAuth2AuthenticationToken)) { + logger.debug("Authentication is not OAuth2, it is {}", authentication.getClass()); + return AuthenticationResult.unsuccessful(); + } + var oauth2 = (OAuth2AuthenticationToken) authentication; + String operatonUserId = oauth2.getName(); + if (operatonUserId == null || operatonUserId.isEmpty()) { + logger.debug("UserId is empty"); + return AuthenticationResult.unsuccessful(); + } + + logger.debug("Authenticated user '{}'", operatonUserId); + return AuthenticationResult.successful(operatonUserId); + } +} diff --git a/spring-boot-starter/starter-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starter/starter-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000..12f67f2201 --- /dev/null +++ b/spring-boot-starter/starter-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +org.operaton.bpm.spring.boot.starter.security.oauth2.OperatonBpmSpringSecurityDisableAutoConfiguration +org.operaton.bpm.spring.boot.starter.security.oauth2.OperatonSpringSecurityOAuth2AutoConfiguration \ No newline at end of file diff --git a/spring-boot-starter/starter-security/src/test/java/my/own/custom/spring/boot/project/SampleApplication.java b/spring-boot-starter/starter-security/src/test/java/my/own/custom/spring/boot/project/SampleApplication.java index 6e670d2314..cf8b4ebbbb 100644 --- a/spring-boot-starter/starter-security/src/test/java/my/own/custom/spring/boot/project/SampleApplication.java +++ b/spring-boot-starter/starter-security/src/test/java/my/own/custom/spring/boot/project/SampleApplication.java @@ -18,15 +18,13 @@ import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.springframework.boot.SpringApplication; -import org.springframework.boot.SpringBootConfiguration; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; -@SpringBootConfiguration -@EnableAutoConfiguration +@SpringBootApplication public class SampleApplication { public static void main(String... args) { diff --git a/spring-boot-starter/starter-security/src/test/java/my/own/custom/spring/boot/project/SamplePermitAllApplication.java b/spring-boot-starter/starter-security/src/test/java/my/own/custom/spring/boot/project/SamplePermitAllApplication.java deleted file mode 100644 index b18d8c194d..0000000000 --- a/spring-boot-starter/starter-security/src/test/java/my/own/custom/spring/boot/project/SamplePermitAllApplication.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH - * under one or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information regarding copyright - * ownership. Camunda licenses this file to you under the Apache License, - * Version 2.0; 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 my.own.custom.spring.boot.project; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.SpringBootConfiguration; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; -import org.springframework.security.web.SecurityFilterChain; - -@SpringBootConfiguration -@EnableAutoConfiguration -public class SamplePermitAllApplication { - - public static void main(String... args) { - SpringApplication.run(SamplePermitAllApplication.class, args); - } - - @Bean - public SecurityFilterChain filterChainPermitAll(HttpSecurity http) throws Exception { - http.authorizeHttpRequests(customizer -> customizer.anyRequest().permitAll()) - .cors(AbstractHttpConfigurer::disable) - .csrf(AbstractHttpConfigurer::disable); - return http.build(); - } - -} diff --git a/spring-boot-starter/starter-security/src/test/java/org/operaton/bpm/spring/boot/starter/security/OperatonBpmSampleApplicationTest.java b/spring-boot-starter/starter-security/src/test/java/org/operaton/bpm/spring/boot/starter/security/OperatonBpmSampleApplicationTest.java index 09fa9e8df4..b3c94e4d9c 100644 --- a/spring-boot-starter/starter-security/src/test/java/org/operaton/bpm/spring/boot/starter/security/OperatonBpmSampleApplicationTest.java +++ b/spring-boot-starter/starter-security/src/test/java/org/operaton/bpm/spring/boot/starter/security/OperatonBpmSampleApplicationTest.java @@ -24,19 +24,14 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.context.junit4.SpringRunner; -import java.util.List; - import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; -import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; @RunWith(SpringRunner.class) -@SpringBootTest(classes = SampleApplication.class, webEnvironment = RANDOM_PORT) +@SpringBootTest(classes = SampleApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class OperatonBpmSampleApplicationTest { private String baseUrl; @@ -52,11 +47,16 @@ public void postConstruct() { baseUrl = "http://localhost:" + port; } + @Test + public void webappApiIsAvailableAndAuthorized() { + ResponseEntity entity = testRestTemplate.getForEntity(baseUrl + "/operaton/api/engine/engine/default/user", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + @Test public void restApiIsAvailable() { ResponseEntity entity = testRestTemplate.getForEntity(baseUrl + "/engine-rest/engine/", String.class); - // default Spring Security filter chain config redirects to /login - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FOUND); - assertThat(entity.getHeaders()).contains(entry(HttpHeaders.LOCATION, List.of(baseUrl + "/login"))); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).isEqualTo("[{\"name\":\"default\"}]"); } } diff --git a/spring-boot-starter/starter-security/src/test/java/org/operaton/bpm/spring/boot/starter/security/OperatonBpmSamplePermitAllApplicationTest.java b/spring-boot-starter/starter-security/src/test/java/org/operaton/bpm/spring/boot/starter/security/OperatonBpmSamplePermitAllApplicationTest.java deleted file mode 100644 index c0335954f7..0000000000 --- a/spring-boot-starter/starter-security/src/test/java/org/operaton/bpm/spring/boot/starter/security/OperatonBpmSamplePermitAllApplicationTest.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH - * under one or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information regarding copyright - * ownership. Camunda licenses this file to you under the Apache License, - * Version 2.0; 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.operaton.bpm.spring.boot.starter.security; - -import my.own.custom.spring.boot.project.SamplePermitAllApplication; -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.boot.test.web.client.TestRestTemplate; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.test.context.junit4.SpringRunner; - -import static org.assertj.core.api.Assertions.assertThat; - -@RunWith(SpringRunner.class) -@SpringBootTest( - classes = SamplePermitAllApplication.class, - webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT -) -public class OperatonBpmSamplePermitAllApplicationTest { - - @Autowired - private TestRestTemplate testRestTemplate; - - @Test - public void restApiIsAvailable() { - ResponseEntity entity = testRestTemplate.getForEntity("/engine-rest/engine/", String.class); - assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).isEqualTo("[{\"name\":\"default\"}]"); - } -}