diff --git a/.gitignore b/.gitignore index 549e00a2a96fa9d7c5dbc9859664a78d980158c2..9ddbf280ee467a8bf60fb940d990634d1f0cbabf 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ build/ ### VS Code ### .vscode/ + +bin/** diff --git a/pom.xml b/pom.xml index 0b91b4cf1a7876b05b238bace29be0cbb60b9f61..db4befb1550b0778ccbcd185cac6390ee891eab6 100644 --- a/pom.xml +++ b/pom.xml @@ -31,9 +31,10 @@ <parent> <groupId>de.ozgcloud.common</groupId> <artifactId>ozgcloud-common-parent</artifactId> - <version>4.6.0</version> + <version>4.7.0-SNAPSHOT</version> <relativePath/> </parent> + <groupId>de.ozgcloud</groupId> <artifactId>administration</artifactId> <version>1.3.0-SNAPSHOT</version> @@ -46,12 +47,13 @@ <imageTag>build-latest</imageTag> <publishImage>false</publishImage> <build.number>SET_BY_JENKINS</build.number> - <spring-cloud-config-server.version>4.1.2</spring-cloud-config-server.version> + + <spring-cloud-config-server.version>4.1.4</spring-cloud-config-server.version> <testcontainers-keycloak.version>3.3.1</testcontainers-keycloak.version> - <keycloak-admin-client.version>24.0.5</keycloak-admin-client.version> + <mongock.version>5.4.0</mongock.version> <lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version> - <mapstruct-processor.version>${mapstruct.version}</mapstruct-processor.version> + <zufi-manager.version>1.6.0</zufi-manager.version> <shedlock.version>5.16.0</shedlock.version> </properties> @@ -66,42 +68,46 @@ <!-- Spring --> <dependency> - <groupId>net.devh</groupId> - <artifactId>grpc-client-spring-boot-starter</artifactId> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> - <groupId>io.grpc</groupId> - <artifactId>grpc-inprocess</artifactId> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-data-rest</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-actuator</artifactId> + <artifactId>spring-boot-starter-hateoas</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-data-mongodb</artifactId> + <artifactId>spring-boot-starter-security</artifactId> </dependency> + <dependency> <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-web</artifactId> + <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-data-rest</artifactId> + <artifactId>spring-boot-starter-actuator</artifactId> </dependency> + <dependency> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-hateoas</artifactId> + <groupId>net.devh</groupId> + <artifactId>grpc-client-spring-boot-starter</artifactId> </dependency> + <dependency> + <groupId>io.grpc</groupId> + <artifactId>grpc-inprocess</artifactId> + </dependency> + <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> <version>${spring-cloud-config-server.version}</version> </dependency> - <dependency> - <groupId>org.springframework.boot</groupId> - <artifactId>spring-boot-starter-security</artifactId> - </dependency> + <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> @@ -132,7 +138,6 @@ <dependency> <groupId>org.keycloak</groupId> <artifactId>keycloak-admin-client</artifactId> - <version>${keycloak-admin-client.version}</version> </dependency> <!-- tools --> @@ -143,7 +148,7 @@ <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> - <version>${mapstruct-processor.version}</version> + <version>${mapstruct.version}</version> </dependency> <!-- commons --> @@ -317,25 +322,25 @@ <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> - <configuration> - <annotationProcessorPaths> - <path> - <groupId>org.mapstruct</groupId> - <artifactId>mapstruct-processor</artifactId> - <version>${mapstruct-processor.version}</version> - </path> - <path> - <groupId>org.projectlombok</groupId> - <artifactId>lombok</artifactId> - <version>${lombok.version}</version> - </path> - <path> - <groupId>org.projectlombok</groupId> - <artifactId>lombok-mapstruct-binding</artifactId> - <version>${lombok-mapstruct-binding.version}</version> - </path> - </annotationProcessorPaths> - </configuration> +<!-- <configuration>--> +<!-- <annotationProcessorPaths>--> +<!-- <path>--> +<!-- <groupId>org.mapstruct</groupId>--> +<!-- <artifactId>mapstruct-processor</artifactId>--> +<!-- <version>${mapstruct-processor.version}</version>--> +<!-- </path>--> +<!-- <path>--> +<!-- <groupId>org.projectlombok</groupId>--> +<!-- <artifactId>lombok</artifactId>--> +<!-- <version>${lombok.version}</version>--> +<!-- </path>--> +<!-- <path>--> +<!-- <groupId>org.projectlombok</groupId>--> +<!-- <artifactId>lombok-mapstruct-binding</artifactId>--> +<!-- <version>${lombok-mapstruct-binding.version}</version>--> +<!-- </path>--> +<!-- </annotationProcessorPaths>--> +<!-- </configuration>--> </plugin> </plugins> </build> diff --git a/src/main/java/de/ozgcloud/admin/AdministrationRepositoryRestConfigurer.java b/src/main/java/de/ozgcloud/admin/AdministrationRepositoryRestConfigurer.java index 2ab9b687e22cad4e59a978b6558a6a366f60895a..99d1d43f0342bd7185b8ec736f02de5e80c79190 100644 --- a/src/main/java/de/ozgcloud/admin/AdministrationRepositoryRestConfigurer.java +++ b/src/main/java/de/ozgcloud/admin/AdministrationRepositoryRestConfigurer.java @@ -23,17 +23,24 @@ */ package de.ozgcloud.admin; +import jakarta.validation.Validation; + import org.springframework.context.annotation.Configuration; -import org.springframework.data.rest.core.config.RepositoryRestConfiguration; +import org.springframework.data.rest.core.event.ValidatingRepositoryEventListener; import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer; -import org.springframework.hateoas.server.core.DefaultLinkRelationProvider; -import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.validation.beanvalidation.SpringValidatorAdapter; @Configuration public class AdministrationRepositoryRestConfigurer implements RepositoryRestConfigurer { +// @Override +// public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) { +// config.setLinkRelationProvider(new DefaultLinkRelationProvider()); +// } + @Override - public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config, CorsRegistry cors) { - config.setLinkRelationProvider(new DefaultLinkRelationProvider()); + public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener listener) { + var validator = Validation.buildDefaultValidatorFactory().getValidator(); + listener.addValidator("beforeCreate", new SpringValidatorAdapter(validator)); } } diff --git a/src/main/java/de/ozgcloud/admin/common/errorhandling/ExceptionController.java b/src/main/java/de/ozgcloud/admin/common/errorhandling/ExceptionController.java index 260cad4f96c6b4721ee129753af42fbe9c05a1ef..a6865a9eb7ab2fd54530db26884df18769417f92 100644 --- a/src/main/java/de/ozgcloud/admin/common/errorhandling/ExceptionController.java +++ b/src/main/java/de/ozgcloud/admin/common/errorhandling/ExceptionController.java @@ -25,6 +25,7 @@ package de.ozgcloud.admin.common.errorhandling; import jakarta.validation.ConstraintViolationException; +import org.springframework.data.rest.core.RepositoryConstraintViolationException; import org.springframework.data.rest.webmvc.ResourceNotFoundException; import org.springframework.http.HttpStatus; import org.springframework.http.ProblemDetail; @@ -81,4 +82,10 @@ public class ExceptionController extends ResponseEntityExceptionHandler { public ProblemDetail handleConstraintViolationException(ConstraintViolationException e) { return problemDetailMapper.fromConstraintViolationException(e); } + + @ExceptionHandler + public ProblemDetail handleConstraintiolation(RepositoryConstraintViolationException e) { + return ProblemDetail.forStatus(HttpStatus.UNPROCESSABLE_ENTITY); + // TODO map fields to ProblemDetail + } } diff --git a/src/main/java/de/ozgcloud/admin/security/SecurityConfiguration.java b/src/main/java/de/ozgcloud/admin/security/SecurityConfiguration.java index b3d46b9d610865098fc760e8a94e94ee79f594f2..6fb633d5387436af1a9f73663bf4d72ae11bae11 100644 --- a/src/main/java/de/ozgcloud/admin/security/SecurityConfiguration.java +++ b/src/main/java/de/ozgcloud/admin/security/SecurityConfiguration.java @@ -23,14 +23,6 @@ */ package de.ozgcloud.admin.security; -import static java.util.stream.Collectors.*; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; - import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -39,15 +31,9 @@ import org.springframework.security.config.annotation.method.configuration.Enabl import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.oauth2.core.oidc.StandardClaimNames; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.web.SecurityFilterChain; import de.ozgcloud.admin.common.user.UserRole; -import de.ozgcloud.admin.environment.OAuth2Properties; import lombok.RequiredArgsConstructor; @Configuration @@ -58,8 +44,6 @@ public class SecurityConfiguration { private final AdminAuthenticationEntryPoint authenticationEntryPoint; - private final OAuth2Properties oAuth2Properties; - static final String RESOURCE_ACCESS_KEY = "resource_access"; static final String SIMPLE_GRANT_AUTHORITY_PREFIX = "ROLE_"; @@ -88,46 +72,37 @@ public class SecurityConfiguration { return http.build(); } - - @Bean - JwtAuthenticationConverter jwtAuthenticationConverter() { - var jwtConverter = new JwtAuthenticationConverter(); - jwtConverter.setJwtGrantedAuthoritiesConverter( - this::convertJwtToGrantedAuthorities); - jwtConverter.setPrincipalClaimName(StandardClaimNames.PREFERRED_USERNAME); - return jwtConverter; - } - - Set<GrantedAuthority> convertJwtToGrantedAuthorities(Jwt jwt) { - return getRolesFromJwt(jwt) - .stream() - .map(this::mapRoleStringToGrantedAuthority) - .collect(toSet()); - } - - private GrantedAuthority mapRoleStringToGrantedAuthority(String role) { - return new SimpleGrantedAuthority(SIMPLE_GRANT_AUTHORITY_PREFIX + role); - } - - List<String> getRolesFromJwt(Jwt jwt) { - return Optional.ofNullable(jwt.getClaimAsMap(RESOURCE_ACCESS_KEY)) - .flatMap(resourceAccessMap -> getMap(resourceAccessMap, oAuth2Properties.getResource())) - .flatMap(adminClientMap -> getList(adminClientMap, ROLES_KEY)) - .orElse(Collections.emptyList()); - } - - @SuppressWarnings("unchecked") - private Optional<Map<String, Object>> getMap(Map<String, Object> map, String mapKey) { - return Optional.ofNullable(map.get(mapKey)) - .filter(Map.class::isInstance) - .map(obj -> (Map<String, Object>) obj); - } - - @SuppressWarnings("unchecked") - private Optional<List<String>> getList(Map<String, Object> map, String mapKey) { - return Optional.ofNullable(map.get(mapKey)) - .filter(List.class::isInstance) - .map(obj -> (List<String>) obj); - } + /* + * @Bean JwtAuthenticationConverter jwtAuthenticationConverter() { var + * jwtConverter = new JwtAuthenticationConverter(); + * jwtConverter.setJwtGrantedAuthoritiesConverter( + * this::convertJwtToGrantedAuthorities); + * jwtConverter.setPrincipalClaimName(StandardClaimNames.PREFERRED_USERNAME); + * return jwtConverter; } + */ + /* + * Set<GrantedAuthority> convertJwtToGrantedAuthorities(Jwt jwt) { return + * getRolesFromJwt(jwt) .stream() .map(this::mapRoleStringToGrantedAuthority) + * .collect(toSet()); } + * + * private GrantedAuthority mapRoleStringToGrantedAuthority(String role) { + * return new SimpleGrantedAuthority(SIMPLE_GRANT_AUTHORITY_PREFIX + role); } + * + * List<String> getRolesFromJwt(Jwt jwt) { return + * Optional.ofNullable(jwt.getClaimAsMap(RESOURCE_ACCESS_KEY)) + * .flatMap(resourceAccessMap -> getMap(resourceAccessMap, + * oAuth2Properties.getResource())) .flatMap(adminClientMap -> + * getList(adminClientMap, ROLES_KEY)) .orElse(Collections.emptyList()); } + * + * @SuppressWarnings("unchecked") private Optional<Map<String, Object>> + * getMap(Map<String, Object> map, String mapKey) { return + * Optional.ofNullable(map.get(mapKey)) .filter(Map.class::isInstance) .map(obj + * -> (Map<String, Object>) obj); } + * + * @SuppressWarnings("unchecked") private Optional<List<String>> + * getList(Map<String, Object> map, String mapKey) { return + * Optional.ofNullable(map.get(mapKey)) .filter(List.class::isInstance) .map(obj + * -> (List<String>) obj); } + */ } diff --git a/src/main/java/de/ozgcloud/admin/setting/DataRestConfiguration.java b/src/main/java/de/ozgcloud/admin/setting/DataRestConfiguration.java deleted file mode 100644 index b93f96d8665b904d133d44f9e3682b649e5ed5a6..0000000000000000000000000000000000000000 --- a/src/main/java/de/ozgcloud/admin/setting/DataRestConfiguration.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den - * Ministerpräsidenten des Landes Schleswig-Holstein - * Staatskanzlei - * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung - * - * Lizenziert unter der EUPL, Version 1.2 oder - sobald - * diese von der Europäischen Kommission genehmigt wurden - - * Folgeversionen der EUPL ("Lizenz"); - * Sie dürfen dieses Werk ausschließlich gemäß - * dieser Lizenz nutzen. - * Eine Kopie der Lizenz finden Sie hier: - * - * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * Sofern nicht durch anwendbare Rechtsvorschriften - * gefordert oder in schriftlicher Form vereinbart, wird - * die unter der Lizenz verbreitete Software "so wie sie - * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - - * ausdrücklich oder stillschweigend - verbreitet. - * Die sprachspezifischen Genehmigungen und Beschränkungen - * unter der Lizenz sind dem Lizenztext zu entnehmen. - */ -package de.ozgcloud.admin.setting; - -import org.springframework.context.annotation.Configuration; -import org.springframework.data.rest.core.event.ValidatingRepositoryEventListener; -import org.springframework.data.rest.webmvc.config.RepositoryRestConfigurer; - -import lombok.RequiredArgsConstructor; - -@Configuration -@RequiredArgsConstructor -public class DataRestConfiguration implements RepositoryRestConfigurer { - - private final SettingValidator settingValidator; - - @Override - public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener v) { - v.addValidator("beforeSave", settingValidator); - v.addValidator("beforeCreate", settingValidator); - } -} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/admin/AdministrationRepositoryRestConfigurerTest.java b/src/test/java/de/ozgcloud/admin/AdministrationRepositoryRestConfigurerTest.java deleted file mode 100644 index a45fc711558b6073f96283db4ce04e98ad691077..0000000000000000000000000000000000000000 --- a/src/test/java/de/ozgcloud/admin/AdministrationRepositoryRestConfigurerTest.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den - * Ministerpräsidenten des Landes Schleswig-Holstein - * Staatskanzlei - * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung - * - * Lizenziert unter der EUPL, Version 1.2 oder - sobald - * diese von der Europäischen Kommission genehmigt wurden - - * Folgeversionen der EUPL ("Lizenz"); - * Sie dürfen dieses Werk ausschließlich gemäß - * dieser Lizenz nutzen. - * Eine Kopie der Lizenz finden Sie hier: - * - * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * Sofern nicht durch anwendbare Rechtsvorschriften - * gefordert oder in schriftlicher Form vereinbart, wird - * die unter der Lizenz verbreitete Software "so wie sie - * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - - * ausdrücklich oder stillschweigend - verbreitet. - * Die sprachspezifischen Genehmigungen und Beschränkungen - * unter der Lizenz sind dem Lizenztext zu entnehmen. - */ -package de.ozgcloud.admin; - -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.InjectMocks; -import org.springframework.data.rest.core.config.RepositoryRestConfiguration; -import org.springframework.hateoas.server.LinkRelationProvider; -import org.springframework.hateoas.server.core.DefaultLinkRelationProvider; - -class AdministrationRepositoryRestConfigurerTest { - - @InjectMocks - private AdministrationRepositoryRestConfigurer configurer; - - @Nested - class TestConfigureRepositoryRestConfiguration { - - @Captor - private ArgumentCaptor<LinkRelationProvider> linkRelationProviderArgumentCaptor; - - @Test - void shouldUseDefaultLinkRelationProvider() { - var configuration = mock(RepositoryRestConfiguration.class); - - configurer.configureRepositoryRestConfiguration(configuration, null); - - verify(configuration).setLinkRelationProvider(linkRelationProviderArgumentCaptor.capture()); - assertThat(linkRelationProviderArgumentCaptor.getValue()).isInstanceOf(DefaultLinkRelationProvider.class); - } - } - -} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitITCase.java b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitITCase.java index 8483a40cc9f385ecc6c48cbcb1a57a58ac89ed60..6d136531fc4b9de0489a0a69a8ec2232a987ba12 100644 --- a/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitITCase.java +++ b/src/test/java/de/ozgcloud/admin/organisationseinheit/OrganisationsEinheitITCase.java @@ -25,7 +25,6 @@ package de.ozgcloud.admin.organisationseinheit; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import java.util.List; @@ -82,22 +81,22 @@ class OrganisationsEinheitITCase { void shouldContainList() { var response = mockMvc.perform(get(PATH)).andExpect(status().isOk()); - response.andDo(print()).andExpect(jsonPath("$._embedded.organisationsEinheitList").isNotEmpty()); + response.andExpect(jsonPath("$._embedded.organisationsEinheits").isNotEmpty()); } @SneakyThrows @Test void shouldContainOrganisationsEinheit() { mockMvc.perform(get(PATH)) - .andExpect(jsonPath("$._embedded.organisationsEinheitList[0].name").value(OrganisationsEinheitTestFactory.NAME)) - .andExpect(jsonPath("$._embedded.organisationsEinheitList[0].organisationsEinheitId").value( + .andExpect(jsonPath("$._embedded.organisationsEinheits[0].name").value(OrganisationsEinheitTestFactory.NAME)) + .andExpect(jsonPath("$._embedded.organisationsEinheits[0].organisationsEinheitId").value( OrganisationsEinheitTestFactory.ORGANISATIONS_EINHEIT_ID)) .andExpect( - jsonPath("$._embedded.organisationsEinheitList[0].syncResult").value(OrganisationsEinheitTestFactory.SYNC_RESULT.name())) - .andExpect(jsonPath("$._embedded.organisationsEinheitList[0].zufiId").doesNotExist()) - .andExpect(jsonPath("$._embedded.organisationsEinheitList[0].parentId").doesNotExist()) - .andExpect(jsonPath("$._embedded.organisationsEinheitList[0].keycloakId").doesNotExist()) - .andExpect(jsonPath("$._embedded.organisationsEinheitList[0].id").doesNotExist()); + jsonPath("$._embedded.organisationsEinheits[0].syncResult").value(OrganisationsEinheitTestFactory.SYNC_RESULT.name())) + .andExpect(jsonPath("$._embedded.organisationsEinheits[0].zufiId").doesNotExist()) + .andExpect(jsonPath("$._embedded.organisationsEinheits[0].parentId").doesNotExist()) + .andExpect(jsonPath("$._embedded.organisationsEinheits[0].keycloakId").doesNotExist()) + .andExpect(jsonPath("$._embedded.organisationsEinheits[0].id").doesNotExist()); } } diff --git a/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationITCase.java b/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationITCase.java index 00e842613b7e62c8d62236a7f589f5e4affe887d..f8a507f213af4b16ae613bfca2560f5422f35ce1 100644 --- a/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationITCase.java +++ b/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationITCase.java @@ -35,6 +35,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.http.HttpStatus; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; @@ -137,15 +138,10 @@ class SecurityConfigurationITCase { @DisplayName("with authentication") @Nested class TestWithAuthentication { - static final String CLAIMS = """ - { - "preferredUsername": "testUser", - "scope": "openid testscope" - }"""; @Test @SneakyThrows - @WithJwt(CLAIMS) + @WithMockUser void shouldAllowApiEndpoint() { var result = doPerformAuthenticated("/api"); @@ -154,8 +150,8 @@ class SecurityConfigurationITCase { @Test @SneakyThrows - @WithJwt(CLAIMS) - void shouldForbidSettingsEndpoint() { + @WithMockUser + void shouldForbidSettingsEndpointOnMissingRole() { var result = doPerformAuthenticated("/api/configuration/settings"); result.andExpect(status().isForbidden()); @@ -163,7 +159,7 @@ class SecurityConfigurationITCase { @Test @SneakyThrows - @WithJwt(CLAIMS) + @WithMockUser void shouldForbidConfigurationsEndpoint() { var result = doPerformAuthenticated("/api/configuration"); @@ -180,17 +176,9 @@ class SecurityConfigurationITCase { @Nested class TestWithAdminRole { - static final String CLAIMS = """ - { - "preferredUsername": "testUser", - "scope": "openid testscope", - "resource_access": { "admin": { "roles": ["ADMIN_ADMIN"] } } - }"""; - - @Test @SneakyThrows - @WithJwt(CLAIMS) + @WithMockUser(roles = "ADMIN_ADMIN") void shouldAllowSettings() { var result = mockMvc.perform(get("/api/configuration/settings")); @@ -199,7 +187,7 @@ class SecurityConfigurationITCase { @Test @SneakyThrows - @WithJwt(CLAIMS) + @WithMockUser(roles = "ADMIN_ADMIN") void shouldAllowConfiguration() { var result = mockMvc.perform(get("/api/configuration")); diff --git a/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationTest.java b/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationTest.java index df88ea5f503807b4b58878d8ced4e09eff353d79..9e03568d021fb77b90c64e4b9e4c1154c4fea4c8 100644 --- a/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationTest.java +++ b/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationTest.java @@ -23,33 +23,11 @@ */ package de.ozgcloud.admin.security; -import static de.ozgcloud.admin.security.JwtTestFactory.*; -import static de.ozgcloud.admin.security.SecurityConfiguration.*; -import static java.util.Collections.*; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; - -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Stream; - -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.oauth2.core.oidc.StandardClaimNames; -import org.springframework.security.oauth2.jwt.Jwt; - -import com.thedeanda.lorem.LoremIpsum; import de.ozgcloud.admin.environment.OAuth2Properties; @@ -67,113 +45,111 @@ class SecurityConfigurationTest { class TestJwtAuthenticationConverter { private final String roleString = "ROLE_Test"; - - @BeforeEach - void mock() { - doReturn(Set.of(new SimpleGrantedAuthority(roleString))).when(securityConfiguration).convertJwtToGrantedAuthorities(any()); - } - - @DisplayName("should use preferred_username") - @Test - void shouldUsePreferredUsername() { - var preferredName = LoremIpsum.getInstance().getName(); - var jwtWithPreferredName = JwtTestFactory.createBuilder() - .claim(StandardClaimNames.PREFERRED_USERNAME, preferredName) - .build(); - - var jwtAuthenticationConverter = securityConfiguration.jwtAuthenticationConverter(); - - var abstractAuthenticationToken = jwtAuthenticationConverter.convert(jwtWithPreferredName); - assertThat(abstractAuthenticationToken.getName()).isEqualTo(preferredName); - } - - @DisplayName("should use granted authorities converter") - @Test - void shouldUseGrantedAuthoritiesConverter() { - var jwtWithRoles = JwtTestFactory.create(); - - var jwtAuthenticationConverter = securityConfiguration.jwtAuthenticationConverter(); - - var abstractAuthenticationToken = jwtAuthenticationConverter.convert(jwtWithRoles); - var securityRoleStrings = abstractAuthenticationToken.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList(); - - assertThat(securityRoleStrings).isEqualTo(List.of(roleString)); - } - } - - @DisplayName("convert jwt to granted authorities") - @Nested - class TestConvertJwtToGrantedAuthorities { - - private List<String> expectedSecurityRoleStrings; - - @BeforeEach - void mock() { - var keycloakRoles = List.of(ROLE_1, JwtTestFactory.ROLE_2, JwtTestFactory.ROLE_3); - expectedSecurityRoleStrings = keycloakRoles.stream().map(role -> SIMPLE_GRANT_AUTHORITY_PREFIX + role).toList(); - doReturn(keycloakRoles).when(securityConfiguration).getRolesFromJwt(any()); - } - - @DisplayName("should call get keycloak roles from jwt") - @Test - void shouldCallGetKeycloakRolesFromJwt() { - var jwt = JwtTestFactory.create(); - - securityConfiguration.convertJwtToGrantedAuthorities(jwt); - - verify(securityConfiguration).getRolesFromJwt(jwt); - } - - @DisplayName("should return granted authorities with ROLE_ prefix") - @Test - void shouldReturnGrantedAuthoritiesWithRolePrefix() { - var jwt = JwtTestFactory.create(); - - var grantedAuthorities = securityConfiguration.convertJwtToGrantedAuthorities(jwt); - - var securityRoles = grantedAuthorities - .stream() - .map(GrantedAuthority::getAuthority).toList(); - assertThat(securityRoles).containsAll(expectedSecurityRoleStrings); - } - } - - @DisplayName("get roles from jwt") - @Nested - class TestGetRolesFromJwt { - - @BeforeEach - void mock() { - lenient().when(oAuth2Properties.getResource()).thenReturn(JwtTestFactory.AUTH_RESOURCE); - } - - @DisplayName("should return empty list if resource_access.admin.roles path is missing") - @ParameterizedTest - @MethodSource("getIncompleteJwt") - void shouldReturnEmptyListIfResourceAccessAdminRolesPathIsMissing(Jwt incompleteJwt) { - var roleStrings = securityConfiguration.getRolesFromJwt(incompleteJwt); - - assertThat(roleStrings).isEmpty(); - } - - private static Stream<Arguments> getIncompleteJwt() { - return Stream.of(JwtTestFactory.create(), - JwtTestFactory.createBuilder().claim(RESOURCE_ACCESS_KEY, Map.of()).build(), - JwtTestFactory.createBuilder().claim(RESOURCE_ACCESS_KEY, Map.of("admin", Map.of())).build(), - JwtTestFactory.createWithRoles(emptyList()).build()) - .map(Arguments::of); - } - - @DisplayName("should return resource_access.admin.roles list") - @Test - void shouldReturnResourceAccessAdminRolesList() { - var expectedRoles = List.of(ROLE_1, JwtTestFactory.ROLE_2, JwtTestFactory.ROLE_3); - var jwtWithRoles = JwtTestFactory.createWithRoles(expectedRoles).build(); - - var roleStrings = securityConfiguration.getRolesFromJwt(jwtWithRoles); - - assertThat(roleStrings).isEqualTo(expectedRoles); - } - + /* + * @BeforeEach void mock() { doReturn(Set.of(new + * SimpleGrantedAuthority(roleString))).when(securityConfiguration). + * convertJwtToGrantedAuthorities(any()); } + * + * @DisplayName("should use preferred_username") + * + * @Test void shouldUsePreferredUsername() { var preferredName = + * LoremIpsum.getInstance().getName(); var jwtWithPreferredName = + * JwtTestFactory.createBuilder() .claim(StandardClaimNames.PREFERRED_USERNAME, + * preferredName) .build(); + * + * var jwtAuthenticationConverter = + * securityConfiguration.jwtAuthenticationConverter(); + * + * var abstractAuthenticationToken = + * jwtAuthenticationConverter.convert(jwtWithPreferredName); + * assertThat(abstractAuthenticationToken.getName()).isEqualTo(preferredName); } + * + * @DisplayName("should use granted authorities converter") + * + * @Test void shouldUseGrantedAuthoritiesConverter() { var jwtWithRoles = + * JwtTestFactory.create(); + * + * var jwtAuthenticationConverter = + * securityConfiguration.jwtAuthenticationConverter(); + * + * var abstractAuthenticationToken = + * jwtAuthenticationConverter.convert(jwtWithRoles); var securityRoleStrings = + * abstractAuthenticationToken.getAuthorities().stream().map(GrantedAuthority:: + * getAuthority).toList(); + * + * assertThat(securityRoleStrings).isEqualTo(List.of(roleString)); } } + * + * @DisplayName("convert jwt to granted authorities") + * + * @Nested class TestConvertJwtToGrantedAuthorities { + * + * private List<String> expectedSecurityRoleStrings; + * + * @BeforeEach void mock() { var keycloakRoles = List.of(ROLE_1, + * JwtTestFactory.ROLE_2, JwtTestFactory.ROLE_3); expectedSecurityRoleStrings = + * keycloakRoles.stream().map(role -> SIMPLE_GRANT_AUTHORITY_PREFIX + + * role).toList(); + * doReturn(keycloakRoles).when(securityConfiguration).getRolesFromJwt(any()); } + * + * @DisplayName("should call get keycloak roles from jwt") + * + * @Test void shouldCallGetKeycloakRolesFromJwt() { var jwt = + * JwtTestFactory.create(); + * + * securityConfiguration.convertJwtToGrantedAuthorities(jwt); + * + * verify(securityConfiguration).getRolesFromJwt(jwt); } + * + * @DisplayName("should return granted authorities with ROLE_ prefix") + * + * @Test void shouldReturnGrantedAuthoritiesWithRolePrefix() { var jwt = + * JwtTestFactory.create(); + * + * var grantedAuthorities = + * securityConfiguration.convertJwtToGrantedAuthorities(jwt); + * + * var securityRoles = grantedAuthorities .stream() + * .map(GrantedAuthority::getAuthority).toList(); + * assertThat(securityRoles).containsAll(expectedSecurityRoleStrings); } } + * + * @DisplayName("get roles from jwt") + * + * @Nested class TestGetRolesFromJwt { + * + * @BeforeEach void mock() { + * lenient().when(oAuth2Properties.getResource()).thenReturn(JwtTestFactory. + * AUTH_RESOURCE); } + * + * @DisplayName("should return empty list if resource_access.admin.roles path is missing" + * ) + * + * @ParameterizedTest + * + * @MethodSource("getIncompleteJwt") void + * shouldReturnEmptyListIfResourceAccessAdminRolesPathIsMissing(Jwt + * incompleteJwt) { var roleStrings = + * securityConfiguration.getRolesFromJwt(incompleteJwt); + * + * assertThat(roleStrings).isEmpty(); } + * + * private static Stream<Arguments> getIncompleteJwt() { return + * Stream.of(JwtTestFactory.create(), + * JwtTestFactory.createBuilder().claim(RESOURCE_ACCESS_KEY, Map.of()).build(), + * JwtTestFactory.createBuilder().claim(RESOURCE_ACCESS_KEY, Map.of("admin", + * Map.of())).build(), JwtTestFactory.createWithRoles(emptyList()).build()) + * .map(Arguments::of); } + * + * @DisplayName("should return resource_access.admin.roles list") + * + * @Test void shouldReturnResourceAccessAdminRolesList() { var expectedRoles = + * List.of(ROLE_1, JwtTestFactory.ROLE_2, JwtTestFactory.ROLE_3); var + * jwtWithRoles = JwtTestFactory.createWithRoles(expectedRoles).build(); + * + * var roleStrings = securityConfiguration.getRolesFromJwt(jwtWithRoles); + * + * assertThat(roleStrings).isEqualTo(expectedRoles); } + * + * } + */ } } diff --git a/src/test/resources/application-itcase.yaml b/src/test/resources/application-itcase.yaml index 1217c868aaa994671a26171005505ce110d401a5..05de7ad1649a52ab4e7b374aebe380da687ec0dd 100644 --- a/src/test/resources/application-itcase.yaml +++ b/src/test/resources/application-itcase.yaml @@ -1,3 +1,6 @@ +logging: + config: classpath:log4j2-local.xml + spring: data: mongodb: