From 2d02d61410068e259bc6c4e76812584c74468329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaqu=C3=ADn?= Date: Thu, 12 Dec 2024 15:10:26 +0100 Subject: [PATCH] tests(starter): add tests for oauth2 and identity provider related to https://github.com/camunda/camunda-bpm-platform/issues/4453 --- spring-boot-starter/starter-security/pom.xml | 12 +- .../starter/security}/SampleApplication.java | 34 ++-- .../oauth2/AbstractSpringSecurityIT.java | 100 ++++++++++ .../CamundaBpmSampleApplicationIT.java} | 42 ++-- ...SecurityAutoConfigOauth2ApplicationIT.java | 173 +++++++++++++++++ .../impl/CamundaIdentityProviderIT.java | 181 ++++++++++++++++++ .../OAuth2GrantedAuthoritiesMapperIT.java | 106 ++++++++++ .../src/test/resources/oauth2-mock.properties | 9 + 8 files changed, 619 insertions(+), 38 deletions(-) rename spring-boot-starter/starter-security/src/test/java/{my/own/custom/spring/boot/project => org/camunda/bpm/spring/boot/starter/security}/SampleApplication.java (59%) create mode 100644 spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/oauth2/AbstractSpringSecurityIT.java rename spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/{CamundaBpmSampleApplicationTest.java => oauth2/CamundaBpmSampleApplicationIT.java} (58%) create mode 100644 spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/oauth2/CamundaBpmSecurityAutoConfigOauth2ApplicationIT.java create mode 100644 spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/oauth2/impl/CamundaIdentityProviderIT.java create mode 100644 spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/oauth2/impl/OAuth2GrantedAuthoritiesMapperIT.java create mode 100644 spring-boot-starter/starter-security/src/test/resources/oauth2-mock.properties diff --git a/spring-boot-starter/starter-security/pom.xml b/spring-boot-starter/starter-security/pom.xml index 5520de3c58e..32001cdf0df 100644 --- a/spring-boot-starter/starter-security/pom.xml +++ b/spring-boot-starter/starter-security/pom.xml @@ -41,7 +41,6 @@ org.springframework.boot spring-boot-starter-oauth2-client - camunda-bpm-spring-boot-starter-test ${project.groupId} @@ -59,6 +58,17 @@ h2 test + + org.springframework.security + spring-security-test + test + + + org.camunda.commons + camunda-commons-testing + ${version} + test + 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/org/camunda/bpm/spring/boot/starter/security/SampleApplication.java similarity index 59% rename from spring-boot-starter/starter-security/src/test/java/my/own/custom/spring/boot/project/SampleApplication.java rename to spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/SampleApplication.java index cf8b4ebbbb1..e1e9297436d 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/org/camunda/bpm/spring/boot/starter/security/SampleApplication.java @@ -14,15 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package my.own.custom.spring.boot.project; +package org.camunda.bpm.spring.boot.starter.security; -import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.springframework.boot.SpringApplication; 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; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; @SpringBootApplication public class SampleApplication { @@ -31,15 +33,15 @@ public static void main(String... args) { SpringApplication.run(SampleApplication.class, args); } - @Bean - public TestRestTemplate restTemplate(RestTemplateBuilder builder) { - builder.requestFactory(() -> { - var factory = new HttpComponentsClientHttpRequestFactory(); - var httpClient = HttpClientBuilder.create().disableRedirectHandling().build(); - factory.setHttpClient(httpClient); - return factory; - }); - return new TestRestTemplate(builder); - } + @RestController + @RequestMapping("/camunda/api/engine/engine") + public static class TestController { + @GetMapping("/{username}/user") + public List> getUserInfo(@PathVariable("username") String username) { + Map users = new HashMap<>(); + users.put("name", username); + return List.of(users); + } + } } diff --git a/spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/oauth2/AbstractSpringSecurityIT.java b/spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/oauth2/AbstractSpringSecurityIT.java new file mode 100644 index 00000000000..3640d1550d4 --- /dev/null +++ b/spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/oauth2/AbstractSpringSecurityIT.java @@ -0,0 +1,100 @@ +/* + * 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.camunda.bpm.spring.boot.starter.security.oauth2; + +import static java.lang.String.format; +import static org.mockito.Mockito.when; + +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import org.camunda.bpm.spring.boot.starter.security.SampleApplication; +import org.junit.Before; +import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.springframework.beans.BeansException; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.context.WebApplicationContext; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = SampleApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +public abstract class AbstractSpringSecurityIT { + + protected static final String EXPECTED_NAME_DEFAULT = "[{\"name\":\"default\"}]"; + protected static final String PROVIDER = "mock-provider"; + protected static final String AUTHORIZED_USER = "bob"; + + protected String baseUrl; + + @LocalServerPort + protected int port; + + @Before + public void setup() throws Exception { + baseUrl = format("http://localhost:%d", port); + } + + protected static Object getBeanForClass(Class type, WebApplicationContext context) { + try { + return context.getBean(type); + } catch (BeansException e) { + return null; + } + } + + protected OAuth2AuthenticationToken createToken(String user) { + List authorities = AuthorityUtils.createAuthorityList("USER"); + OAuth2User oAuth2User = new DefaultOAuth2User(authorities, Map.of("name", user), "name"); + return new OAuth2AuthenticationToken(oAuth2User, authorities, AbstractSpringSecurityIT.PROVIDER); + } + + protected void createAuthorizedClient(OAuth2AuthenticationToken authenticationToken, + ClientRegistrationRepository registrations, + OAuth2AuthorizedClientService authorizedClientService) { + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "value", Instant.now(), Instant.now().plus(Duration.ofDays(1))); + ClientRegistration clientRegistration = registrations.findByRegistrationId(authenticationToken.getAuthorizedClientRegistrationId()); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, authenticationToken.getName(), accessToken); + when(authorizedClientService.loadAuthorizedClient(AbstractSpringSecurityIT.PROVIDER, AbstractSpringSecurityIT.AUTHORIZED_USER)).thenReturn(authorizedClient); + } + + public static class ResultCaptor implements Answer { + public T result = null; + + @Override + public T answer(InvocationOnMock invocationOnMock) throws Throwable { + result = (T) invocationOnMock.callRealMethod(); + return result; + } + } + +} diff --git a/spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/CamundaBpmSampleApplicationTest.java b/spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/oauth2/CamundaBpmSampleApplicationIT.java similarity index 58% rename from spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/CamundaBpmSampleApplicationTest.java rename to spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/oauth2/CamundaBpmSampleApplicationIT.java index c7c99d4e48c..d1bbd9afa1f 100644 --- a/spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/CamundaBpmSampleApplicationTest.java +++ b/spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/oauth2/CamundaBpmSampleApplicationIT.java @@ -14,49 +14,49 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.camunda.bpm.spring.boot.starter.security; +package org.camunda.bpm.spring.boot.starter.security.oauth2; -import jakarta.annotation.PostConstruct; -import my.own.custom.spring.boot.project.SampleApplication; 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.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.context.WebApplicationContext; import static org.assertj.core.api.Assertions.assertThat; -@RunWith(SpringRunner.class) -@SpringBootTest(classes = SampleApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class CamundaBpmSampleApplicationTest { - - private String baseUrl; - - @LocalServerPort - private int port; +public class CamundaBpmSampleApplicationIT extends AbstractSpringSecurityIT { @Autowired private TestRestTemplate testRestTemplate; - @PostConstruct - public void postConstruct() { - baseUrl = "http://localhost:" + port; + @Autowired + private WebApplicationContext webApplicationContext; + + @Test + public void testSpringSecurityAutoConfigurationCorrectlySet() { + // given oauth2 client not configured + // when retrieving config beans then only SpringSecurityDisabledAutoConfiguration is present + assertThat(getBeanForClass(CamundaSpringSecurityOAuth2AutoConfiguration.class, webApplicationContext)).isNull(); + assertThat(getBeanForClass(CamundaBpmSpringSecurityDisableAutoConfiguration.class, webApplicationContext)).isNotNull(); } @Test - public void webappApiIsAvailableAndAuthorized() { + public void testWebappApiIsAvailableAndRequiresAuthorization() { + // given oauth2 client disabled + // when calling the webapp api ResponseEntity entity = testRestTemplate.getForEntity(baseUrl + "/camunda/api/engine/engine/default/user", String.class); + // then webapp api returns unauthorized assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } @Test - public void restApiIsAvailable() { + public void testRestApiIsAvailable() { + // given oauth2 client disabled + // when calling the rest api ResponseEntity entity = testRestTemplate.getForEntity(baseUrl + "/engine-rest/engine/", String.class); + // then rest api is accessible assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(entity.getBody()).isEqualTo("[{\"name\":\"default\"}]"); + assertThat(entity.getBody()).isEqualTo(EXPECTED_NAME_DEFAULT); } } diff --git a/spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/oauth2/CamundaBpmSecurityAutoConfigOauth2ApplicationIT.java b/spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/oauth2/CamundaBpmSecurityAutoConfigOauth2ApplicationIT.java new file mode 100644 index 00000000000..fef735e3dc6 --- /dev/null +++ b/spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/oauth2/CamundaBpmSecurityAutoConfigOauth2ApplicationIT.java @@ -0,0 +1,173 @@ +/* + * 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.camunda.bpm.spring.boot.starter.security.oauth2; + +import jakarta.servlet.Filter; +import java.lang.reflect.Field; +import org.camunda.bpm.engine.rest.security.auth.AuthenticationResult; +import org.camunda.bpm.spring.boot.starter.security.oauth2.impl.AuthorizeTokenFilter; +import org.camunda.bpm.spring.boot.starter.security.oauth2.impl.OAuth2AuthenticationProvider; +import org.camunda.bpm.webapp.impl.security.auth.ContainerBasedAuthenticationFilter; +import org.camunda.commons.testing.ProcessEngineLoggingRule; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.http.MediaType; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; + +@AutoConfigureMockMvc +@TestPropertySource("/oauth2-mock.properties") +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class CamundaBpmSecurityAutoConfigOauth2ApplicationIT extends AbstractSpringSecurityIT { + + protected static final String UNAUTHORIZED_USER = "mary"; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private FilterRegistrationBean filterRegistrationBean; + + @Autowired + private ClientRegistrationRepository registrations; + + @MockBean + private OAuth2AuthorizedClientService authorizedClientService; + + @Rule + public ProcessEngineLoggingRule loggingRule = new ProcessEngineLoggingRule().watch(AuthorizeTokenFilter.class.getCanonicalName()); + + private OAuth2AuthenticationProvider spiedAuthenticationProvider; + + @Before + public void setup() throws Exception { + super.setup(); + spyAuthenticationProvider(); + } + + @Test + public void testSpringSecurityAutoConfigurationCorrectlySet() { + // given oauth2 client configured + // when retrieving config beans then only OAuth2AutoConfiguration is present + assertThat(getBeanForClass(CamundaSpringSecurityOAuth2AutoConfiguration.class, mockMvc.getDispatcherServlet().getWebApplicationContext())).isNotNull(); + assertThat(getBeanForClass(CamundaBpmSpringSecurityDisableAutoConfiguration.class, mockMvc.getDispatcherServlet().getWebApplicationContext())).isNull(); + } + + @Test + public void testWebappWithoutAuthentication() throws Exception { + // given no authentication + + // when + mockMvc.perform(MockMvcRequestBuilders.get(baseUrl + "/camunda/api/engine/engine/default/user") + .accept(MediaType.APPLICATION_JSON)) + .andDo(MockMvcResultHandlers.print()) + // then oauth2 redirection occurs + .andExpect(MockMvcResultMatchers.status().isFound()) + .andExpect(MockMvcResultMatchers.header().exists("Location")) + .andExpect(MockMvcResultMatchers.header().string("Location", baseUrl + "/oauth2/authorization/" + PROVIDER)); + } + + @Test + public void testWebappApiWithAuthorizedUser() throws Exception { + // given authorized oauth2 authentication token + OAuth2AuthenticationToken authenticationToken = createToken(AUTHORIZED_USER); + createAuthorizedClient(authenticationToken, registrations, authorizedClientService); + + // when + mockMvc.perform(MockMvcRequestBuilders.get(baseUrl + "/camunda/api/engine/engine/default/user") + .accept(MediaType.APPLICATION_JSON) + .with(authentication(authenticationToken))) + // then call is successful + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.content().json(EXPECTED_NAME_DEFAULT)); + } + + @Test + public void testWebappWithUnauthorizedUser() throws Exception { + // given unauthorized oauth2 authentication token + OAuth2AuthenticationToken authenticationToken = createToken(UNAUTHORIZED_USER); + createAuthorizedClient(authenticationToken, registrations, authorizedClientService); + + // when + mockMvc.perform(MockMvcRequestBuilders.get(baseUrl + "/camunda/api/engine/engine/default/user") + .accept(MediaType.APPLICATION_JSON) + .with(authentication(authenticationToken))) + // then authorization fails and redirection occurs + .andExpect(MockMvcResultMatchers.status().isFound()) + .andExpect(MockMvcResultMatchers.header().exists("Location")) + .andExpect(MockMvcResultMatchers.header().string("Location", baseUrl + "/oauth2/authorization/" + PROVIDER)); + + String expectedWarn = "Authorize failed for '" + UNAUTHORIZED_USER + "'"; + assertThat(loggingRule.getFilteredLog(expectedWarn)).hasSize(1); + verifyNoInteractions(spiedAuthenticationProvider); + } + + + @Test + public void testOauth2AuthenticationProvider() throws Exception { + // given authorized oauth2 authentication token + ResultCaptor resultCaptor = new ResultCaptor<>(); + doAnswer(resultCaptor).when(spiedAuthenticationProvider).extractAuthenticatedUser(any(), any()); + OAuth2AuthenticationToken authenticationToken = createToken(AUTHORIZED_USER); + createAuthorizedClient(authenticationToken, registrations, authorizedClientService); + + // when + mockMvc.perform(MockMvcRequestBuilders.get(baseUrl + "/camunda/api/engine/engine/default/user") + .accept(MediaType.APPLICATION_JSON) + .with(authentication(authenticationToken))) + // then call is successful + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.content().json(EXPECTED_NAME_DEFAULT)); + + // and authentication provider was called and returned expected authentication result + verify(spiedAuthenticationProvider).extractAuthenticatedUser(any(), any()); + AuthenticationResult authenticationResult = resultCaptor.result; + assertThat(authenticationResult.isAuthenticated()).isTrue(); + assertThat(authenticationResult.getAuthenticatedUser()).isEqualTo(AUTHORIZED_USER); + } + + private void spyAuthenticationProvider() throws NoSuchFieldException, IllegalAccessException { + ContainerBasedAuthenticationFilter filter = (ContainerBasedAuthenticationFilter) filterRegistrationBean.getFilter(); + Field authProviderField = ContainerBasedAuthenticationFilter.class.getDeclaredField("authenticationProvider"); + authProviderField.setAccessible(true); + Object realAuthenticationProvider = authProviderField.get(filter); + spiedAuthenticationProvider = (OAuth2AuthenticationProvider) spy(realAuthenticationProvider); + authProviderField.set(filter, spiedAuthenticationProvider); + } +} diff --git a/spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/oauth2/impl/CamundaIdentityProviderIT.java b/spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/oauth2/impl/CamundaIdentityProviderIT.java new file mode 100644 index 00000000000..e841537aeec --- /dev/null +++ b/spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/oauth2/impl/CamundaIdentityProviderIT.java @@ -0,0 +1,181 @@ +/* + * 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.camunda.bpm.spring.boot.starter.security.oauth2.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication; + +import org.camunda.bpm.engine.IdentityService; +import org.camunda.bpm.engine.identity.Group; +import org.camunda.bpm.engine.identity.GroupQuery; +import org.camunda.bpm.engine.identity.User; +import org.camunda.bpm.engine.identity.UserQuery; +import org.camunda.bpm.engine.impl.identity.WritableIdentityProvider; +import org.camunda.bpm.engine.impl.identity.db.DbGroupQueryImpl; +import org.camunda.bpm.engine.impl.identity.db.DbUserQueryImpl; +import org.camunda.bpm.spring.boot.starter.security.oauth2.AbstractSpringSecurityIT; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +@AutoConfigureMockMvc +@TestPropertySource("/oauth2-mock.properties") +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class CamundaIdentityProviderIT extends AbstractSpringSecurityIT { + + @Autowired + private IdentityService identityService; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private ClientRegistrationRepository registrations; + + @MockBean + private OAuth2AuthorizedClientService authorizedClientService; + + public static OAuth2IdentityProvider spiedIdentityProvider = spy(new OAuth2IdentityProvider()); + + static { // needs to be executed before spring context initialization + mockIdentityProviderFactory(); + } + + @Before + public void setup() throws Exception { + super.setup(); + } + + @Test + public void testIdentityProviderForUsersWithoutSpringSecurity() { + // given no spring security authentication + User newUser = identityService.newUser("newUser"); + identityService.saveUser(newUser); + ResultCaptor resultCaptor = new ResultCaptor<>(); + doAnswer(resultCaptor).when(spiedIdentityProvider).createUserQuery(); + + // when calling rest api + ResponseEntity entity = restTemplate.getForEntity(baseUrl + "/engine-rest/user/", String.class); + + // then identity provider does fallback to db provider + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + verify(spiedIdentityProvider, atLeastOnce()).createUserQuery(); + UserQuery userQueryResult = resultCaptor.result; + assertThat(userQueryResult).isInstanceOf(DbUserQueryImpl.class); + assertThat(userQueryResult).isNotNull(); + assertThat(entity.getBody()).contains(newUser.getId()); + } + + @Test + public void testIdentityProviderForGroupsWithoutSpringSecurity() { + // given no spring security authentication + Group newGroup = identityService.newGroup("newGroup"); + identityService.saveGroup(newGroup); + ResultCaptor resultCaptor = new ResultCaptor<>(); + doAnswer(resultCaptor).when(spiedIdentityProvider).createGroupQuery(); + + // when calling rest api + ResponseEntity entity = restTemplate.getForEntity(baseUrl + "/engine-rest/group/", String.class); + + // then identity provider does fallback to db provider + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + verify(spiedIdentityProvider, atLeastOnce()).createGroupQuery(); + GroupQuery groupQueryResult = resultCaptor.result; + assertThat(groupQueryResult).isInstanceOf(DbGroupQueryImpl.class); + assertThat(groupQueryResult).isNotNull(); + assertThat(entity.getBody()).contains(newGroup.getId()); + } + + @Test + public void testIdentityProviderForUsersWithSpringSecurity() throws Exception { + // given spring security context + ResultCaptor resultCaptor = new ResultCaptor<>(); + doAnswer(resultCaptor).when(spiedIdentityProvider).createUserQuery(); + OAuth2AuthenticationToken authenticationToken = createToken(AUTHORIZED_USER); + createAuthorizedClient(authenticationToken, registrations, authorizedClientService); + + // when calling webapp api + mockMvc.perform(MockMvcRequestBuilders.get(baseUrl + "/camunda/api/engine/engine/default/user") + .accept(MediaType.APPLICATION_JSON) + .with(authentication(authenticationToken))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()); + + // then identity provider handles oauth2 authentication + verify(spiedIdentityProvider, atLeastOnce()).createUserQuery(); + UserQuery userQueryResult = resultCaptor.result; + assertThat(userQueryResult).isInstanceOf(OAuth2IdentityProvider.OAuth2UserQuery.class); + assertThat(userQueryResult).isNotNull(); + assertThat(((OAuth2IdentityProvider.OAuth2UserQuery) userQueryResult).getId()).isEqualTo(AUTHORIZED_USER); + } + + @Test + public void testIdentityProviderForGroupsWithSpringSecurity() throws Exception { + // given spring security context + ResultCaptor resultCaptor = new ResultCaptor<>(); + doAnswer(resultCaptor).when(spiedIdentityProvider).createGroupQuery(); + OAuth2AuthenticationToken authenticationToken = createToken(AUTHORIZED_USER); + createAuthorizedClient(authenticationToken, registrations, authorizedClientService); + + // when calling webapp api + mockMvc.perform(MockMvcRequestBuilders.get(baseUrl + "/camunda/api/engine/engine/default/user") + .accept(MediaType.APPLICATION_JSON) + .with(authentication(authenticationToken))) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()); + + // then identity provider handles oauth2 authentication + verify(spiedIdentityProvider, atLeastOnce()).createGroupQuery(); + GroupQuery groupQueryResult = resultCaptor.result; + assertThat(groupQueryResult).isInstanceOf(OAuth2IdentityProvider.OAuth2GroupQuery.class); + assertThat(groupQueryResult).isNotNull(); + assertThat(((OAuth2IdentityProvider.OAuth2GroupQuery) groupQueryResult).getUserId()).isEqualTo(AUTHORIZED_USER); + } + + private static void mockIdentityProviderFactory() { + // mocks methods of all instances of OAuth2IdentityProviderFactory so instead of always + // returning a new instance of the identity provider, it always returns our spy object + mockConstruction(OAuth2IdentityProviderFactory.class, (mock, context) -> { + doReturn(spiedIdentityProvider).when(mock).openSession(); + doReturn(WritableIdentityProvider.class).when(mock).getSessionType(); + }); + } +} diff --git a/spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/oauth2/impl/OAuth2GrantedAuthoritiesMapperIT.java b/spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/oauth2/impl/OAuth2GrantedAuthoritiesMapperIT.java new file mode 100644 index 00000000000..e24b37aba20 --- /dev/null +++ b/spring-boot-starter/starter-security/src/test/java/org/camunda/bpm/spring/boot/starter/security/oauth2/impl/OAuth2GrantedAuthoritiesMapperIT.java @@ -0,0 +1,106 @@ +/* + * 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.camunda.bpm.spring.boot.starter.security.oauth2.impl; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.camunda.bpm.spring.boot.starter.security.oauth2.AbstractSpringSecurityIT; +import org.camunda.commons.testing.ProcessEngineLoggingRule; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; +import org.springframework.test.context.TestPropertySource; + +@TestPropertySource("/oauth2-mock.properties") +public class OAuth2GrantedAuthoritiesMapperIT extends AbstractSpringSecurityIT { + + protected static final String GROUP_NAME_ATTRIBUTE = "groupName"; + + @Autowired + private OAuth2GrantedAuthoritiesMapper authoritiesMapper; + + @Rule + public ProcessEngineLoggingRule loggingRule = new ProcessEngineLoggingRule().watch(OAuth2GrantedAuthoritiesMapper.class.getCanonicalName()); + + @Test + public void testMappingNotOauth2Authorities() { + // given + List authorities = List.of(new SimpleGrantedAuthority("USER")); + // when + Collection mappedAuthorities = authoritiesMapper.mapAuthorities(authorities); + // then + assertThat(mappedAuthorities).isEmpty(); + } + + @Test + public void testMappingGroupNotAvailable() { + // given + List authorities = List.of(new OAuth2UserAuthority("USER", Map.of("name", "name"))); + // when + Collection mappedAuthorities = authoritiesMapper.mapAuthorities(authorities); + // then + assertThat(mappedAuthorities).isEmpty(); + String expectedWarn = "Attribute " + GROUP_NAME_ATTRIBUTE + " is not available"; + assertThat(loggingRule.getFilteredLog(expectedWarn)).hasSize(1); + } + + @Test + public void testMappingSingleAuthority() { + // given + List authorities = List.of(new OAuth2UserAuthority("USER", Map.of(GROUP_NAME_ATTRIBUTE, "group"))); + // when + Collection mappedAuthorities = authoritiesMapper.mapAuthorities(authorities); + // then + assertThat(mappedAuthorities).hasSize(1); + assertThat(mappedAuthorities.iterator().next().getAuthority()).isEqualTo("group"); + } + + @Test + public void testMappingMultipleAuthorities() { + // given + List authorities = List.of(new OAuth2UserAuthority("USER", Map.of(GROUP_NAME_ATTRIBUTE, List.of("group1", "group2")))); + // when + Collection mappedAuthorities = authoritiesMapper.mapAuthorities(authorities); + // then + assertThat(mappedAuthorities).hasSize(2); + List groups = mappedAuthorities.stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()); + assertThat(groups).contains("group1", "group2"); + } + + + @Test + public void testMappingUnsupportedType() { + // given + Object object = new Object(); + List authorities = List.of(new OAuth2UserAuthority("USER", Map.of(GROUP_NAME_ATTRIBUTE, object))); + // when + Collection mappedAuthorities = authoritiesMapper.mapAuthorities(authorities); + // then + assertThat(mappedAuthorities).isEmpty(); + String expectedError = "Could not map granted authorities, unsupported group attribute type: " + object.getClass(); + assertThat(loggingRule.getFilteredLog(expectedError)).hasSize(1); + } + + +} \ No newline at end of file diff --git a/spring-boot-starter/starter-security/src/test/resources/oauth2-mock.properties b/spring-boot-starter/starter-security/src/test/resources/oauth2-mock.properties new file mode 100644 index 00000000000..4bdd3714e89 --- /dev/null +++ b/spring-boot-starter/starter-security/src/test/resources/oauth2-mock.properties @@ -0,0 +1,9 @@ +spring.security.oauth2.client.registration.mock-provider.client-id=test-client +spring.security.oauth2.client.registration.mock-provider.client-secret=test-secret +spring.security.oauth2.client.registration.mock-provider.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.mock-provider.redirect-uri={baseUrl}/login/oauth2/code/{registrationId} +spring.security.oauth2.client.registration.mock-provider.scope=openid +spring.security.oauth2.client.provider.mock-provider.authorization-uri=http://localhost/issuer1/authorize +spring.security.oauth2.client.provider.mock-provider.token-uri=http://localhost/issuer1/token +spring.security.oauth2.client.provider.mock-provider.jwk-set-uri=http://localhost/issuer1/jwks +camunda.bpm.oauth2.identity-provider.group-name-attribute = groupName \ No newline at end of file