diff --git a/pom.xml b/pom.xml index f2ab9d3049519f6a30c5911e54b1053bc205bdad..5b674b88f1f5904d2f838f128d22d424f0581f31 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ </parent> <groupId>de.ozgcloud</groupId> <artifactId>administration</artifactId> - <version>0.4.0-SNAPSHOT</version> + <version>0.5.0-SNAPSHOT</version> <name>Administration</name> <description>Administration Backend Project</description> diff --git a/src/main/java/de/ozgcloud/admin/Root.java b/src/main/java/de/ozgcloud/admin/Root.java index 8fc3d5966abd999afa09d17b2c555cb4cc0b409d..13af4d82bbccf0c18deac076b9358babd4e9a904 100644 --- a/src/main/java/de/ozgcloud/admin/Root.java +++ b/src/main/java/de/ozgcloud/admin/Root.java @@ -30,7 +30,7 @@ import lombok.Getter; @Getter public class Root { private String javaVersion; - private String buildVersion; + private String version; private Instant buildTime; private String buildNumber; } diff --git a/src/main/java/de/ozgcloud/admin/RootController.java b/src/main/java/de/ozgcloud/admin/RootController.java index 887862e364924be5ce3de9b80bb4c4d2afd5a0a7..54cc0572d94c47f3f141071066fd9bf02e776322 100644 --- a/src/main/java/de/ozgcloud/admin/RootController.java +++ b/src/main/java/de/ozgcloud/admin/RootController.java @@ -48,7 +48,7 @@ public class RootController { return Root.builder() .javaVersion(System.getProperty("java.version")) .buildTime(buildProperties.getTime()) - .buildVersion(buildProperties.getVersion()) + .version(buildProperties.getVersion()) .buildNumber(buildProperties.get("number")) .build(); } diff --git a/src/main/java/de/ozgcloud/admin/RootModelAssembler.java b/src/main/java/de/ozgcloud/admin/RootModelAssembler.java index 2cf01448f6fa1ab4f52d3a72fc06b15d57c5cda7..319cb4d3525e1eede720494444555aa7c2f81a8d 100644 --- a/src/main/java/de/ozgcloud/admin/RootModelAssembler.java +++ b/src/main/java/de/ozgcloud/admin/RootModelAssembler.java @@ -21,6 +21,8 @@ */ package de.ozgcloud.admin; +import de.ozgcloud.admin.common.user.CurrentUserService; +import de.ozgcloud.admin.common.user.UserRole; import org.springframework.boot.autoconfigure.data.rest.RepositoryRestProperties; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.Link; @@ -30,6 +32,9 @@ import org.springframework.stereotype.Component; import lombok.RequiredArgsConstructor; +import java.util.ArrayList; +import java.util.List; + @Component @RequiredArgsConstructor public class RootModelAssembler implements RepresentationModelAssembler<Root, EntityModel<Root>> { @@ -37,13 +42,31 @@ public class RootModelAssembler implements RepresentationModelAssembler<Root, En private final RepositoryRestProperties restProperties; + private final CurrentUserService currentUserService; + @Override public EntityModel<Root> toModel(Root root) { - var rootLink = WebMvcLinkBuilder.linkTo(RootController.class); - var configLink = rootLink.toUriComponentsBuilder().replacePath(restProperties.getBasePath()); + List<Link> links = buildRootModelLinks(); return EntityModel.of( root, - Link.of(configLink.toUriString(), REL_CONFIGURATION), - rootLink.withSelfRel()); + links); + } + + List<Link> buildRootModelLinks() { + List<Link> links = new ArrayList<>(); + var rootLinkBuilder = WebMvcLinkBuilder.linkTo(RootController.class); + links.add(rootLinkBuilder.withSelfRel()); + if (currentUserService.hasRole(UserRole.ADMIN_ADMIN)) { + links.add(buildConfigLink()); + } + return links; + } + + private Link buildConfigLink() { + var rootLinkBuilder = WebMvcLinkBuilder.linkTo(RootController.class); + return Link.of( + rootLinkBuilder.toUriComponentsBuilder().replacePath(restProperties.getBasePath()).toUriString(), + REL_CONFIGURATION + ); } } 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 d5e5803b9ec8364e3298402bda28c84291a957e4..51fc68105ef225ee93d66f36a448ca4b54c5a9b0 100644 --- a/src/main/java/de/ozgcloud/admin/common/errorhandling/ExceptionController.java +++ b/src/main/java/de/ozgcloud/admin/common/errorhandling/ExceptionController.java @@ -29,62 +29,70 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import jakarta.validation.ConstraintViolation; -import jakarta.validation.ConstraintViolationException; - import org.springframework.data.rest.webmvc.ResourceNotFoundException; import org.springframework.http.HttpStatus; import org.springframework.http.ProblemDetail; import org.springframework.security.access.AccessDeniedException; -import org.springframework.web.ErrorResponse; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; import de.ozgcloud.common.errorhandling.TechnicalException; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; @RestControllerAdvice public class ExceptionController extends ResponseEntityExceptionHandler { @ExceptionHandler(RuntimeException.class) - public ErrorResponse handleRuntimeException(RuntimeException ex) { - return ErrorResponse.builder(ex, HttpStatus.INTERNAL_SERVER_ERROR, ex.getLocalizedMessage()).build(); + @ResponseBody + public ProblemDetail handleRuntimeException(RuntimeException ex) { + return buildProblemDetail(HttpStatus.INTERNAL_SERVER_ERROR, ex); } @ExceptionHandler(AccessDeniedException.class) - public ErrorResponse handleAccessDeniedException(AccessDeniedException ex) { - return ErrorResponse.builder(ex, HttpStatus.FORBIDDEN, ex.getLocalizedMessage()).build(); + @ResponseBody + public ProblemDetail handleAccessDeniedException(AccessDeniedException ex) { + return buildProblemDetail(HttpStatus.FORBIDDEN, ex); } @ExceptionHandler(ResourceNotFoundException.class) - public ErrorResponse handleResourceNotFoundException(ResourceNotFoundException ex) { - return ErrorResponse.builder(ex, HttpStatus.NOT_FOUND, ex.getLocalizedMessage()).build(); + @ResponseBody + public ProblemDetail handleResourceNotFoundException(ResourceNotFoundException ex) { + return buildProblemDetail(HttpStatus.NOT_FOUND, ex); } @ExceptionHandler(FunctionalException.class) - public ErrorResponse handleFunctionalException(FunctionalException ex) { - return ErrorResponse.builder(ex, HttpStatus.BAD_REQUEST, ex.getLocalizedMessage()).build(); + @ResponseBody + public ProblemDetail handleFunctionalException(FunctionalException ex) { + return buildProblemDetail(HttpStatus.BAD_REQUEST, ex); } @ExceptionHandler(TechnicalException.class) - public ErrorResponse handleTechnicalException(TechnicalException ex) { - return ErrorResponse.builder(ex, HttpStatus.INTERNAL_SERVER_ERROR, ex.getLocalizedMessage()).build(); + @ResponseBody + public ProblemDetail handleTechnicalException(TechnicalException ex) { + return buildProblemDetail(HttpStatus.INTERNAL_SERVER_ERROR, ex); + } + + private ProblemDetail buildProblemDetail(HttpStatus status, Exception ex) { + return ProblemDetail.forStatusAndDetail(status, ex.getLocalizedMessage()); } @ExceptionHandler(ConstraintViolationException.class) - public ErrorResponse handleConstraintViolationException(ConstraintViolationException ex) { - var problemDetail = buildProblemDetail(HttpStatus.UNPROCESSABLE_ENTITY, ex); - return ErrorResponse.builder(ex, problemDetail).build(); + @ResponseBody + public ProblemDetail handleConstraintViolationException(ConstraintViolationException ex) { + return buildConstraintViolationProblemDetail(HttpStatus.UNPROCESSABLE_ENTITY, ex); } - private ProblemDetail buildProblemDetail(HttpStatus status, ConstraintViolationException ex) { + private ProblemDetail buildConstraintViolationProblemDetail(HttpStatus status, ConstraintViolationException ex) { var problemDetail = ProblemDetail.forStatusAndDetail(status, ex.getLocalizedMessage()); problemDetail.setProperty("invalid-params", getDetailedviolationList(ex.getConstraintViolations())); return problemDetail; } private List<Map<String, String>> getDetailedviolationList(Set<ConstraintViolation<?>> violations) { - List<Map<String, String>> detailedViolations = new ArrayList<>(); + var detailedViolations = new ArrayList<Map<String, String>>(); Optional.ofNullable(violations).orElse(Collections.emptySet()).forEach(v -> detailedViolations.add(buildDetailedViolation(v))); return detailedViolations; diff --git a/src/main/java/de/ozgcloud/admin/common/user/CurrentUserHelper.java b/src/main/java/de/ozgcloud/admin/common/user/CurrentUserHelper.java new file mode 100644 index 0000000000000000000000000000000000000000..16f4bf18d38516bbdf07cadfd58b9ce5fe793426 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/common/user/CurrentUserHelper.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch das + * Ministerium für Energiewende, Klimaschutz, Umwelt und Natur + * Zentrales IT-Management + * + * 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.common.user; + +import java.util.Collection; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.security.authentication.AuthenticationTrustResolver; +import org.springframework.security.authentication.AuthenticationTrustResolverImpl; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +class CurrentUserHelper { + static final String ROLE_PREFIX = "ROLE_"; + private static final Predicate<String> IS_ROLE_PREFIX_MISSING = role -> !role.startsWith(ROLE_PREFIX); + private static final AuthenticationTrustResolver TRUST_RESOLVER = new AuthenticationTrustResolverImpl(); + private static final Predicate<Authentication> IS_TRUSTED = auth -> !TRUST_RESOLVER.isAnonymous(auth); + + public static boolean hasRole(String role) { + var auth = getAuthentication(); + + if ((Objects.isNull(auth)) || (Objects.isNull(auth.getPrincipal()))) { + return false; + } + + return containsRole(auth.getAuthorities(), role); + } + + static boolean containsRole(Collection<? extends GrantedAuthority> authorities, String role) { + if (Objects.isNull(authorities)) { + return false; + } + return authorities.stream().anyMatch(a -> StringUtils.equalsIgnoreCase(addRolePrefixIfMissing(role), a.getAuthority())); + } + + static String addRolePrefixIfMissing(String roleToCheck) { + return Optional.ofNullable(roleToCheck) + .filter(IS_ROLE_PREFIX_MISSING) + .map(role -> ROLE_PREFIX + role) + .orElse(roleToCheck); + } + + static Authentication getAuthentication() { + return findAuthentication().orElseThrow(() -> new IllegalStateException("No authenticated User found")); + } + + private static Optional<Authentication> findAuthentication() { + return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication()).filter(IS_TRUSTED); + } +} diff --git a/src/main/java/de/ozgcloud/admin/common/user/CurrentUserService.java b/src/main/java/de/ozgcloud/admin/common/user/CurrentUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..94912e9e4749fe871f4120477fe840fe617bb52b --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/common/user/CurrentUserService.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch das + * Ministerium für Energiewende, Klimaschutz, Umwelt und Natur + * Zentrales IT-Management + * + * 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.common.user; + +import org.springframework.stereotype.Service; + +@Service +public class CurrentUserService { + public boolean hasRole(String role) { + return CurrentUserHelper.hasRole(role); + } +} diff --git a/src/main/java/de/ozgcloud/admin/common/user/UserRole.java b/src/main/java/de/ozgcloud/admin/common/user/UserRole.java new file mode 100644 index 0000000000000000000000000000000000000000..1b0db41b19d75eb43875afd31da0ea2b0beb69b7 --- /dev/null +++ b/src/main/java/de/ozgcloud/admin/common/user/UserRole.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch das + * Ministerium für Energiewende, Klimaschutz, Umwelt und Natur + * Zentrales IT-Management + * + * 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.common.user; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class UserRole { + public static final String ADMIN_ADMIN = "ADMIN_ADMIN"; +} diff --git a/src/main/java/de/ozgcloud/admin/security/SecurityConfiguration.java b/src/main/java/de/ozgcloud/admin/security/SecurityConfiguration.java index 568d79a76fc6268d93b7ecc2e172ae9625d40ed1..b1c6280e10acdf642638dea39854bd5eec7b4446 100644 --- a/src/main/java/de/ozgcloud/admin/security/SecurityConfiguration.java +++ b/src/main/java/de/ozgcloud/admin/security/SecurityConfiguration.java @@ -19,7 +19,13 @@ */ 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; @@ -29,10 +35,15 @@ 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 @@ -43,6 +54,14 @@ 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_"; + + static final String ROLES_KEY = "roles"; + @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -54,6 +73,8 @@ public class SecurityConfiguration { http.authorizeHttpRequests(requests -> requests .requestMatchers(HttpMethod.GET, "/api/environment").permitAll() + .requestMatchers("/api/configuration").hasRole(UserRole.ADMIN_ADMIN) + .requestMatchers("/api/configuration/**").hasRole(UserRole.ADMIN_ADMIN) .requestMatchers("/api").authenticated() .requestMatchers("/api/**").authenticated() .requestMatchers("/actuator").permitAll() @@ -67,9 +88,42 @@ public class SecurityConfiguration { @Bean JwtAuthenticationConverter jwtAuthenticationConverter() { var jwtConverter = new JwtAuthenticationConverter(); - jwtConverter.setJwtGrantedAuthoritiesConverter(jwt -> List.of(() -> "ROLE_USER")); + jwtConverter.setJwtGrantedAuthoritiesConverter( + this::convertJwtToGrantedAuthorities); jwtConverter.setPrincipalClaimName(StandardClaimNames.PREFERRED_USERNAME); return jwtConverter; } -} \ No newline at end of file + 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/test/java/de/ozgcloud/admin/ApiRootITCase.java b/src/test/java/de/ozgcloud/admin/ConfigurationITCase.java similarity index 94% rename from src/test/java/de/ozgcloud/admin/ApiRootITCase.java rename to src/test/java/de/ozgcloud/admin/ConfigurationITCase.java index 327eea8dfbe2120d59233c3891dc807f8882fbae..f3c0c415f3e73aa152eec948f20cf5a47bf9d249 100644 --- a/src/test/java/de/ozgcloud/admin/ApiRootITCase.java +++ b/src/test/java/de/ozgcloud/admin/ConfigurationITCase.java @@ -34,13 +34,14 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; +import de.ozgcloud.admin.common.user.UserRole; import de.ozgcloud.common.test.ITCase; import lombok.SneakyThrows; @ITCase @AutoConfigureMockMvc -@WithMockUser -class ApiRootITCase { +@WithMockUser(roles = UserRole.ADMIN_ADMIN) +class ConfigurationITCase { @Autowired private RepositoryRestProperties restProperties; @@ -49,7 +50,7 @@ class ApiRootITCase { private MockMvc mockMvc; @Nested - class TestRootEndpoint { + class TestConfigurationRestEndpoint { @Test void shouldBetSetToApi() { diff --git a/src/test/java/de/ozgcloud/admin/RootControllerTest.java b/src/test/java/de/ozgcloud/admin/RootControllerTest.java index 88e2bb4aada0d7fd5b28034dd77b3254f5e127d3..9401253fa33c9dd7a7b61a0efc32816dee8ee5c9 100644 --- a/src/test/java/de/ozgcloud/admin/RootControllerTest.java +++ b/src/test/java/de/ozgcloud/admin/RootControllerTest.java @@ -90,7 +90,7 @@ class RootControllerTest { ResultActions result = doRequest(); - result.andExpect(jsonPath("$.buildVersion").value(RootTestFactory.BUILD_VERSION)); + result.andExpect(jsonPath("$.version").value(RootTestFactory.BUILD_VERSION)); } @Test diff --git a/src/test/java/de/ozgcloud/admin/RootModelAssemblerTest.java b/src/test/java/de/ozgcloud/admin/RootModelAssemblerTest.java index b237432d1fc03ba9b2ea3b5afa924bc706ed54c6..9e797f60d83dadc15a280d94e692dc9d191b92d3 100644 --- a/src/test/java/de/ozgcloud/admin/RootModelAssemblerTest.java +++ b/src/test/java/de/ozgcloud/admin/RootModelAssemblerTest.java @@ -22,10 +22,9 @@ package de.ozgcloud.admin; import static de.ozgcloud.admin.RootModelAssembler.*; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.*; -import java.util.Optional; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -33,12 +32,14 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.Spy; import org.springframework.boot.autoconfigure.data.rest.RepositoryRestProperties; -import org.springframework.hateoas.EntityModel; -import org.springframework.hateoas.IanaLinkRelations; import org.springframework.hateoas.Link; +import de.ozgcloud.admin.common.user.CurrentUserService; +import de.ozgcloud.admin.common.user.UserRole; + class RootModelAssemblerTest { private static final String BASE_PATH = "/api/base"; @@ -48,34 +49,65 @@ class RootModelAssemblerTest { @Mock private RepositoryRestProperties restProperties; - - @BeforeEach - void mockBasePath() { - when(restProperties.getBasePath()).thenReturn(BASE_PATH); - } + @Mock + private CurrentUserService currentUserService; @DisplayName("Entity Model") @Nested class TestEntityModel { + @BeforeEach + void beforeEach() { + Mockito.when(currentUserService.hasRole(UserRole.ADMIN_ADMIN)).thenReturn(true); + Mockito.when(restProperties.getBasePath()).thenReturn(BASE_PATH); + } @Test - void shouldHaveHrefToBasePath() { - var configurationLink = toModel().getLink(REL_CONFIGURATION); + void shouldHaveRoot() { + var givenRoot = RootTestFactory.create(); + List<Link> links = List.of(); + Mockito.when(modelAssembler.buildRootModelLinks()).thenReturn(links); - assertEquals(Optional.of(Link.of(BASE_PATH, REL_CONFIGURATION)), configurationLink); + var resultRoot = modelAssembler.toModel(givenRoot).getContent(); + + assertThat(resultRoot).isEqualTo(givenRoot); } @Test - void shouldHaveHrefToSelf() { - var selfLink = toModel().getLink(IanaLinkRelations.SELF); + void shouldHaveLinks() { + List<Link> links = List.of(Link.of(RootController.PATH)); + Mockito.when(modelAssembler.buildRootModelLinks()).thenReturn(links); + + var modelLinks = modelAssembler.toModel(RootTestFactory.create()).getLinks(); - assertEquals(Optional.of(Link.of(RootController.PATH)), selfLink); + assertThat(modelLinks).containsAll(links); } + } - private EntityModel<Root> toModel() { - return modelAssembler.toModel(RootTestFactory.create()); + @DisplayName("Root Model Links") + @Nested + class TestBuildRootModelLinks { + + @Test + void shouldHaveHrefToBasePathIfAuthorized() { + Mockito.when(restProperties.getBasePath()).thenReturn(BASE_PATH); + Mockito.when(currentUserService.hasRole(UserRole.ADMIN_ADMIN)).thenReturn(true); + + List<Link> links = modelAssembler.buildRootModelLinks(); + + assertThat(links).containsExactly( + Link.of(RootController.PATH), + Link.of(BASE_PATH, REL_CONFIGURATION)); } + @Test + void shouldNotHaveHrefToBasePathIfUnauthorized() { + Mockito.when(currentUserService.hasRole(UserRole.ADMIN_ADMIN)).thenReturn(false); + + List<Link> links = modelAssembler.buildRootModelLinks(); + + assertThat(links).containsExactly( + Link.of(RootController.PATH)); + } } } \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/admin/RootTestFactory.java b/src/test/java/de/ozgcloud/admin/RootTestFactory.java index e4a77201b527061485a7dcf7d567dfa161f73f7b..4c0312d9a9014b860c7ef9739245d5c012b1c61e 100644 --- a/src/test/java/de/ozgcloud/admin/RootTestFactory.java +++ b/src/test/java/de/ozgcloud/admin/RootTestFactory.java @@ -40,7 +40,7 @@ public class RootTestFactory { return Root.builder() .buildTime(BUILD_TIME) .javaVersion(JAVA_VERSION) - .buildVersion(BUILD_VERSION) + .version(BUILD_VERSION) .buildNumber(BUILD_NUMBER); } } diff --git a/src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerITCase.java b/src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerITCase.java index 18880e1718ab14fcbddfe5663d8a6968740fce20..e965505fc19fda822757ed5b189c88fb9a1d7484 100644 --- a/src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerITCase.java +++ b/src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerITCase.java @@ -6,7 +6,6 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import java.util.Set; -import java.util.stream.Stream; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; @@ -18,13 +17,9 @@ import org.apache.commons.lang3.StringUtils; import org.assertj.core.util.Arrays; 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.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.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; @@ -47,38 +42,9 @@ class ExceptionControllerITCase { @MockBean private RootModelAssembler modelAssembler; - @Nested - class TestExceptions { - @ParameterizedTest - @MethodSource("exceptionAndExpectedStatus") - @SneakyThrows - void shouldHandleExceptionWithStatus(Class<? extends Exception> exceptionClass, HttpStatus expectedStatus) { - when(modelAssembler.toModel(any())).thenThrow(TestErrorController.EXCEPTION_PRODUCER.get(exceptionClass).produceException()); - - var result = performGet(); - - result.andExpect(status().is(expectedStatus.value())); - } - - @ParameterizedTest - @MethodSource("exceptionAndExpectedStatus") - @SneakyThrows - void shouldRespondWithStatusInBody(Class<? extends Exception> exceptionClass, HttpStatus expectedStatus) { - when(modelAssembler.toModel(any())).thenThrow(exceptionClass); - - var result = performGet(); - - result.andExpect(jsonPath("$.status").value(expectedStatus.value())); - } - - private static Stream<Arguments> exceptionAndExpectedStatus() { - return TestErrorController.STATUS_BY_EXCEPTION.entrySet().stream().map(kv -> Arguments.of(kv.getKey(), kv.getValue())); - } - - } - @Nested class TestConstraintViolationException { + @Test @SneakyThrows void shouldHaveInvalidFieldNameInResponse() { @@ -129,11 +95,9 @@ class ExceptionControllerITCase { private String string2; } + @SneakyThrows + private ResultActions performGet() { + return mockMvc.perform(get(RootController.PATH)); + } } - - @SneakyThrows - private ResultActions performGet() { - return mockMvc.perform(get(RootController.PATH)); - } - -} +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerTest.java b/src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerTest.java index 41f95cf47993da9c9a390d4e7d62702b2f0c79c7..367ed8afd534aa2c443453447e060011b6c1bd22 100644 --- a/src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerTest.java +++ b/src/test/java/de/ozgcloud/admin/common/errorhandling/ExceptionControllerTest.java @@ -21,23 +21,27 @@ */ package de.ozgcloud.admin.common.errorhandling; +import static org.junit.Assert.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -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.springframework.data.rest.webmvc.ResourceNotFoundException; import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.jayway.jsonpath.JsonPath; +import de.ozgcloud.common.errorhandling.TechnicalException; import lombok.SneakyThrows; class ExceptionControllerTest { @@ -51,97 +55,392 @@ class ExceptionControllerTest { .setControllerAdvice(new ExceptionController()).build(); } - @DisplayName("Error handler") + @DisplayName("Runtime Exception") @Nested - class TestErrorHandler { + class TestRuntimeException { + + private final static String TEST_ERROR_ENDPOINT = TestErrorController.BASE_PATH + "/runtime"; + private final static int STATUS_VALUE = HttpStatus.INTERNAL_SERVER_ERROR.value(); - @ParameterizedTest - @MethodSource("exceptionAndExpectedStatus") @SneakyThrows - void shouldHandleExceptionWithStatus(Class<? extends Exception> exceptionClass, HttpStatus expectedStatus) { - var result = doPerformWithError(exceptionClass); + @Test + void shouldReturnStatus() { + var result = doPerformGet(); - result.andExpect(status().is(expectedStatus.value())); + result.andExpect(status().is(STATUS_VALUE)); } - @ParameterizedTest - @MethodSource("exceptionAndExpectedStatus") - @SneakyThrows - void shouldRespondWithStatusInBody(Class<? extends Exception> exceptionClass, HttpStatus expectedStatus) { - var result = doPerformWithError(exceptionClass); + @DisplayName("response body") + @Nested + class TestRuntimeExceptionBody { + + @SneakyThrows + @Test + void shouldHaveStatus() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.status").value(STATUS_VALUE)); + } + + @SneakyThrows + @Test + void shouldHaveTitle() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.title").value("Internal Server Error")); + } + + @SneakyThrows + @Test + void shouldHaveDetail() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.detail").value("error message")); + } + + @SneakyThrows + @Test + void shouldHaveInstance() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.instance").value(TEST_ERROR_ENDPOINT)); + } - result.andExpect(jsonPath("$.status").value(expectedStatus.value())); + @SneakyThrows + void shouldHaveType() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.type").value("about:blank")); + } } - @ParameterizedTest - @MethodSource("exceptionAndExpectedStatus") @SneakyThrows - void shouldRespondWithTitler(Class<? extends Exception> exceptionClass, HttpStatus expectedStatus) { - var result = doPerformWithError(exceptionClass); - - result.andExpect(jsonPath("$.title").exists()); + private ResultActions doPerformGet() { + return mockMvc.perform(get(TEST_ERROR_ENDPOINT)); } + } + + @DisplayName("Access Denied Exception") + @Nested + class TestAccessDeniedException { + + private final static String TEST_ERROR_ENDPOINT = TestErrorController.BASE_PATH + "/access-denied"; + private final static int STATUS_VALUE = HttpStatus.FORBIDDEN.value(); - @ParameterizedTest - @MethodSource("exceptionAndExpectedStatus") @SneakyThrows - void shouldRespondWithDetail(Class<? extends Exception> exceptionClass, HttpStatus expectedStatus) { - var result = doPerformWithError(exceptionClass); + @Test + void shouldReturnStatus() { + var result = doPerformGet(); - result.andExpect(jsonPath("$.detail").exists()); + result.andExpect(status().is(STATUS_VALUE)); } - @ParameterizedTest - @MethodSource("exceptionAndExpectedStatus") - @SneakyThrows - void shouldRespondWithInstance(Class<? extends Exception> exceptionClass, HttpStatus expectedStatus) { - var result = doPerformWithError(exceptionClass); + @DisplayName("response body") + @Nested + class TestAccessDeniedExceptionBody { + + @SneakyThrows + @Test + void shouldHaveStatus() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.status").value(STATUS_VALUE)); + } + + @SneakyThrows + @Test + void shouldHaveTitle() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.title").value("Forbidden")); + } + + @SneakyThrows + @Test + void shouldHaveDetail() { + var result = doPerformGet(); - result.andExpect(jsonPath("$.instance").exists()); + result.andExpect(jsonPath("$.detail").value("error message")); + } + + @SneakyThrows + @Test + void shouldHaveInstance() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.instance").value(TEST_ERROR_ENDPOINT)); + } + + @SneakyThrows + void shouldHaveType() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.type").value("about:blank")); + } } - @ParameterizedTest - @MethodSource("exceptionAndExpectedStatus") @SneakyThrows - void shouldRespondWithType(Class<? extends Exception> exceptionClass, HttpStatus expectedStatus) { - var result = doPerformWithError(exceptionClass); + private ResultActions doPerformGet() { + return mockMvc.perform(get(TEST_ERROR_ENDPOINT)); + } + } - result.andExpect(jsonPath("$.type").exists()); + @DisplayName("ResourceNotFound Exception") + @Nested + class TestResourceNotFoundException { + + private final static String TEST_ERROR_ENDPOINT = TestErrorController.BASE_PATH + "/resource-not-found"; + private final static int STATUS_VALUE = HttpStatus.NOT_FOUND.value(); + + @SneakyThrows + @Test + void shouldReturnStatus() { + var result = doPerformGet(); + + result.andExpect(status().is(STATUS_VALUE)); } - private static Stream<Arguments> exceptionAndExpectedStatus() { - return TestErrorController.STATUS_BY_EXCEPTION.entrySet().stream().map(kv -> Arguments.of(kv.getKey(), kv.getValue())); + @DisplayName("response body") + @Nested + class TestResourceNotFoundExceptionBody { + + @SneakyThrows + @Test + void shouldHaveStatus() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.status").value(STATUS_VALUE)); + } + + @SneakyThrows + @Test + void shouldHaveTitle() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.title").value("Not Found")); + } + + @SneakyThrows + @Test + void shouldHaveDetail() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.detail").value("error message")); + } + + @SneakyThrows + @Test + void shouldHaveInstance() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.instance").value(TEST_ERROR_ENDPOINT)); + } + + @SneakyThrows + void shouldHaveType() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.type").value("about:blank")); + } } @SneakyThrows - private ResultActions doPerformWithError(Class<? extends Exception> exceptionClass) { - return mockMvc.perform(get("/api/test-error").param("errorClassName", exceptionClass.getName())); + private ResultActions doPerformGet() { + return mockMvc.perform(get(TEST_ERROR_ENDPOINT)); } } - @DisplayName("ResourceNotFound error") + @DisplayName("Functional Exception") @Nested - class TestResourceNotFoundError { + class TestFunctionalException { + + private final static String TEST_ERROR_ENDPOINT = TestErrorController.BASE_PATH + "/functional"; + private final static int STATUS_VALUE = HttpStatus.BAD_REQUEST.value(); - @Test @SneakyThrows - void shouldHaveStatus() { - var result = doRequestUnknown(); + @Test + void shouldReturnStatus() { + var result = doPerformGet(); - result.andExpect(status().is(HttpStatus.NOT_FOUND.value())); + result.andExpect(status().is(STATUS_VALUE)); + } + + @DisplayName("response body") + @Nested + class TestFunctionalExceptionBody { + + @SneakyThrows + @Test + void shouldHaveStatus() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.status").value(STATUS_VALUE)); + } + + @SneakyThrows + @Test + void shouldHaveTitle() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.title").value("Bad Request")); + } + + @DisplayName("detail") + @Nested + class TestBodyDetail { + + @SneakyThrows + @Test + void shouldContainErrorMessage() { + var result = doPerformGet(); + + assertTrue(getDetailFromResponseContent(result).contains("Functional error: error message")); + } + + @SneakyThrows + @Test + void shouldContainExceptionId() { + var result = doPerformGet(); + + assertTrue(getDetailFromResponseContent(result).contains("(ExceptionId: ")); + } + } + + @SneakyThrows + @Test + void shouldHaveInstance() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.instance").value(TEST_ERROR_ENDPOINT)); + } + + @SneakyThrows + void shouldHaveType() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.type").value("about:blank")); + } } - @Test @SneakyThrows - void shouldRespondWithStatusInBody() { - var result = doRequestUnknown(); + private ResultActions doPerformGet() { + return mockMvc.perform(get(TEST_ERROR_ENDPOINT)); + } + } + + @DisplayName("TechnicalException") + @Nested + class TestTechnicalException { + + private final static String TEST_ERROR_ENDPOINT = TestErrorController.BASE_PATH + "/technical"; + private final static int STATUS_VALUE = HttpStatus.INTERNAL_SERVER_ERROR.value(); + + @SneakyThrows + @Test + void shouldReturnStatus() { + var result = doPerformGet(); + + result.andExpect(status().is(STATUS_VALUE)); + } + + @DisplayName("response body") + @Nested + class TestTechnicalExceptionBody { - result.andExpect(jsonPath("$.status").value(HttpStatus.NOT_FOUND.value())); + @SneakyThrows + @Test + void shouldHaveStatus() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.status").value(STATUS_VALUE)); + } + + @SneakyThrows + @Test + void shouldHaveTitle() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.title").value("Internal Server Error")); + } + + @DisplayName("detail") + @Nested + class TestBodyDetail { + + @SneakyThrows + @Test + void shouldContainErrorMessage() { + var result = doPerformGet(); + + assertTrue(getDetailFromResponseContent(result).contains("error message")); + } + + @SneakyThrows + @Test + void shouldContainExceptionId() { + var result = doPerformGet(); + + assertTrue(getDetailFromResponseContent(result).contains("(ExceptionId: ")); + } + } + + @SneakyThrows + @Test + void shouldHaveInstance() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.instance").value(TEST_ERROR_ENDPOINT)); + } + + @SneakyThrows + void shouldHaveType() { + var result = doPerformGet(); + + result.andExpect(jsonPath("$.type").value("about:blank")); + } } @SneakyThrows - private ResultActions doRequestUnknown() { - return mockMvc.perform(get("/api/unknown")); + private ResultActions doPerformGet() { + return mockMvc.perform(get(TEST_ERROR_ENDPOINT)); } } + + @SneakyThrows + private String getDetailFromResponseContent(ResultActions resultActions) { + return JsonPath.read(resultActions.andReturn().getResponse().getContentAsString(), "$.detail"); + } } + +@RestController +@RequestMapping(TestErrorController.BASE_PATH) +class TestErrorController { + + public final static String BASE_PATH = "/api/test-error"; + static final String ERROR_MESSAGE = "error message"; + + @GetMapping("/runtime") + String throwRuntimeException() throws Exception { + throw new RuntimeException(ERROR_MESSAGE); + } + + @GetMapping("/access-denied") + String throwAccessException() throws Exception { + throw new AccessDeniedException(ERROR_MESSAGE); + } + + @GetMapping("/resource-not-found") + String throwResourceNotFoundException() throws Exception { + throw new ResourceNotFoundException(ERROR_MESSAGE); + } + + @GetMapping("/functional") + String throwFunctionalExceptionException() throws Exception { + throw new FunctionalException(() -> ERROR_MESSAGE); + } + + @GetMapping("/technical") + String throwTechnicalExceptionException() throws Exception { + throw new TechnicalException(ERROR_MESSAGE); + } +} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/admin/common/errorhandling/TestErrorController.java b/src/test/java/de/ozgcloud/admin/common/errorhandling/TestErrorController.java deleted file mode 100644 index c49345edcbebd221c8b19ab7fd3e08d11177f4ba..0000000000000000000000000000000000000000 --- a/src/test/java/de/ozgcloud/admin/common/errorhandling/TestErrorController.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) 2024. Das Land Schleswig-Holstein vertreten durch das Ministerium für Energiewende, Klimaschutz, Umwelt und Natur - * Zentrales IT-Management - * - * 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.common.errorhandling; - -import java.util.Collections; -import java.util.Map; - -import jakarta.validation.ConstraintViolationException; - -import org.springframework.data.rest.webmvc.ResourceNotFoundException; -import org.springframework.http.HttpStatus; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import de.ozgcloud.common.errorhandling.TechnicalException; - -@RestController -@RequestMapping("/api/test-error") -class TestErrorController { - - @FunctionalInterface - interface ExceptionProducer { - Exception produceException(); - } - - static final Map<Class<? extends Exception>, HttpStatus> STATUS_BY_EXCEPTION = Map.of( - RuntimeException.class, HttpStatus.INTERNAL_SERVER_ERROR, - AccessDeniedException.class, HttpStatus.FORBIDDEN, - ConstraintViolationException.class, HttpStatus.UNPROCESSABLE_ENTITY, - ResourceNotFoundException.class, HttpStatus.NOT_FOUND, - FunctionalException.class, HttpStatus.BAD_REQUEST, - TechnicalException.class, HttpStatus.INTERNAL_SERVER_ERROR); - - static final String ERROR_MESSAGE = "error message"; - - static Map<Class<? extends Exception>, ExceptionProducer> EXCEPTION_PRODUCER = Map.of( - RuntimeException.class, () -> new RuntimeException(ERROR_MESSAGE), - AccessDeniedException.class, () -> new AccessDeniedException(ERROR_MESSAGE), - ConstraintViolationException.class, () -> new ConstraintViolationException(ERROR_MESSAGE, Collections.emptySet()), - ResourceNotFoundException.class, () -> new ResourceNotFoundException(ERROR_MESSAGE), - FunctionalException.class, () -> new FunctionalException(() -> ERROR_MESSAGE), - TechnicalException.class, () -> new TechnicalException(ERROR_MESSAGE)); - - @GetMapping - String throwException(@RequestParam String errorClassName) throws Exception { - throw EXCEPTION_PRODUCER.get( - Class.forName(errorClassName)).produceException(); - } - -} diff --git a/src/test/java/de/ozgcloud/admin/common/user/CurrentUserHelperTest.java b/src/test/java/de/ozgcloud/admin/common/user/CurrentUserHelperTest.java new file mode 100644 index 0000000000000000000000000000000000000000..428668577ed0add18a3c5a9fc041e350ea7d97f8 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/common/user/CurrentUserHelperTest.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch das + * Ministerium für Energiewende, Klimaschutz, Umwelt und Natur + * Zentrales IT-Management + * + * 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.common.user; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Collection; +import java.util.List; + +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.ValueSource; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; + +class CurrentUserHelperTest { + @DisplayName("Has role") + @Nested + class TestHasRole { + @Mock + private final Authentication mockAuthentication = Mockito.mock(Authentication.class); + @Mock + private final User mockPrincipal = Mockito.mock(User.class); + + @Test + void shouldReturnFalseOnMissingAuthentication() { + try (MockedStatic<CurrentUserHelper> mockUserHelper = Mockito.mockStatic( + CurrentUserHelper.class, + Mockito.CALLS_REAL_METHODS)) { + mockUserHelper.when(CurrentUserHelper::getAuthentication).thenReturn(null); + + boolean hasRole = CurrentUserHelper.hasRole(UserRole.ADMIN_ADMIN); + + assertThat(hasRole).isFalse(); + } + } + + @Test + void shouldReturnFalseOnMissingPrincipal() { + Mockito.when(mockAuthentication.getPrincipal()).thenReturn(null); + try (MockedStatic<CurrentUserHelper> mockUserHelper = Mockito.mockStatic( + CurrentUserHelper.class, + Mockito.CALLS_REAL_METHODS)) { + mockUserHelper.when(CurrentUserHelper::getAuthentication).thenReturn(mockAuthentication); + + boolean hasRole = CurrentUserHelper.hasRole(UserRole.ADMIN_ADMIN); + + assertThat(hasRole).isFalse(); + } + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void shouldReturnValue(boolean containsRoleValue) { + Mockito.when(mockAuthentication.getPrincipal()).thenReturn(mockPrincipal); + List<GrantedAuthority> authorities = List.of(); + Mockito.<Collection<? extends GrantedAuthority>>when(mockAuthentication.getAuthorities()).thenReturn(authorities); + + try (MockedStatic<CurrentUserHelper> mockUserHelper = Mockito.mockStatic( + CurrentUserHelper.class, + Mockito.CALLS_REAL_METHODS)) { + mockUserHelper.when(CurrentUserHelper::getAuthentication).thenReturn(mockAuthentication); + mockUserHelper.when(() -> CurrentUserHelper.containsRole(Mockito.anyList(), Mockito.anyString())) + .thenReturn(containsRoleValue); + + boolean hasRole = CurrentUserHelper.hasRole(UserRole.ADMIN_ADMIN); + + mockUserHelper.verify(() -> CurrentUserHelper.containsRole(mockAuthentication.getAuthorities(), UserRole.ADMIN_ADMIN)); + assertThat(hasRole).isEqualTo(containsRoleValue); + } + } + } + + @DisplayName("Contains role") + @Nested + class TestContainsRole { + @Test + void shouldNotContainRoleIfAuthoritiesIsNull() { + boolean containsRole = CurrentUserHelper.containsRole(null, UserRole.ADMIN_ADMIN); + + assertThat(containsRole).isFalse(); + } + + @Test + void shouldNotContainRole() { + List<GrantedAuthority> authorities = List.of( + new SimpleGrantedAuthority(CurrentUserHelper.ROLE_PREFIX + "OTHER")); + + boolean containsRole = CurrentUserHelper.containsRole(authorities, UserRole.ADMIN_ADMIN); + + assertThat(containsRole).isFalse(); + } + + @Test + void shouldContainRole() { + Collection<? extends GrantedAuthority> authorities = List.of( + new SimpleGrantedAuthority(CurrentUserHelper.ROLE_PREFIX + UserRole.ADMIN_ADMIN)); + + boolean containsRole = CurrentUserHelper.containsRole(authorities, UserRole.ADMIN_ADMIN); + + assertThat(containsRole).isTrue(); + } + } + + @DisplayName("Add Role Prefix If Missing") + @Nested + class TestAddRolePrefixIfMissing { + + @Test + void shouldAddPrefixIfMissing() { + var roleWithoutPrefix = UserRole.ADMIN_ADMIN; + + var role = CurrentUserHelper.addRolePrefixIfMissing(roleWithoutPrefix); + + assertThat(role).isEqualTo(CurrentUserHelper.ROLE_PREFIX + UserRole.ADMIN_ADMIN); + } + + @Test + void shouldReturnRoleIfPrefixAlreadyExists() { + var roleWithPrefix = CurrentUserHelper.ROLE_PREFIX + UserRole.ADMIN_ADMIN; + + var role = CurrentUserHelper.addRolePrefixIfMissing(roleWithPrefix); + + assertThat(role).isEqualTo(roleWithPrefix); + } + + @Test + void shouldReturnNullIfPassingNull() { + var role = CurrentUserHelper.addRolePrefixIfMissing(null); + + assertThat(role).isNull(); + } + } + + @DisplayName("Get authentication") + @Nested + class TestGetAuthentication { + @Mock + private final SecurityContext mockSecurityContext = Mockito.mock(SecurityContext.class); + + @Test + void shouldThrowIfNoAuthenticatedUser() { + Mockito.when(mockSecurityContext.getAuthentication()).thenReturn(null); + + try (MockedStatic<SecurityContextHolder> contextHolder = Mockito.mockStatic(SecurityContextHolder.class)) { + contextHolder.when(SecurityContextHolder::getContext).thenReturn(mockSecurityContext); + + assertThatIllegalStateException() + .isThrownBy(CurrentUserHelper::getAuthentication) + .withMessage("No authenticated User found"); + } + } + + @Test + void shouldPassAuthentication() { + Authentication mockAuthentication = Mockito.mock(Authentication.class); + Mockito.when(mockSecurityContext.getAuthentication()).thenReturn(mockAuthentication); + + try (MockedStatic<SecurityContextHolder> contextHolder = Mockito.mockStatic(SecurityContextHolder.class)) { + contextHolder.when(SecurityContextHolder::getContext).thenReturn(mockSecurityContext); + + Authentication authentication = CurrentUserHelper.getAuthentication(); + + assertThat(authentication).isSameAs(mockAuthentication); + } + } + } +} diff --git a/src/test/java/de/ozgcloud/admin/common/user/CurrentUserServiceTest.java b/src/test/java/de/ozgcloud/admin/common/user/CurrentUserServiceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..878292bc14ffc812739213d84ce34b0fd97a3b4a --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/common/user/CurrentUserServiceTest.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch das + * Ministerium für Energiewende, Klimaschutz, Umwelt und Natur + * Zentrales IT-Management + * + * 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.common.user; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import static org.assertj.core.api.Assertions.assertThat; + +class CurrentUserServiceTest { + private final CurrentUserService currentUserService = new CurrentUserService(); + + @DisplayName("Has role") + @Nested + class TestHasRole { + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void shouldReturnValue(boolean hasRoleValue) { + try (MockedStatic<CurrentUserHelper> mockUserHelper = Mockito.mockStatic( + CurrentUserHelper.class) + ){ + mockUserHelper.when(() -> CurrentUserHelper.hasRole(Mockito.anyString())) + .thenReturn(hasRoleValue); + + boolean hasRole = currentUserService.hasRole(UserRole.ADMIN_ADMIN); + + mockUserHelper.verify(() -> CurrentUserHelper.hasRole(UserRole.ADMIN_ADMIN)); + assertThat(hasRole).isEqualTo(hasRoleValue); + } + } + } +} diff --git a/src/test/java/de/ozgcloud/admin/security/AuthenticationExceptionTestFactory.java b/src/test/java/de/ozgcloud/admin/security/AuthenticationExceptionTestFactory.java index 3727bfc0c968540bafa8abe9dffdb6df9add2972..daf3d634ab66c34ea6f6e393d92681b69ed69b59 100644 --- a/src/test/java/de/ozgcloud/admin/security/AuthenticationExceptionTestFactory.java +++ b/src/test/java/de/ozgcloud/admin/security/AuthenticationExceptionTestFactory.java @@ -31,6 +31,7 @@ public class AuthenticationExceptionTestFactory { @Builder public static class DummyAuthenticationException extends AuthenticationException { + @SuppressWarnings("unused") private String msg; DummyAuthenticationException(String msg) { diff --git a/src/test/java/de/ozgcloud/admin/security/JwtTestFactory.java b/src/test/java/de/ozgcloud/admin/security/JwtTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..719c94afe86b01a9f87d4b0bbf1f51a40fcdaa75 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/security/JwtTestFactory.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2024. Das Land Schleswig-Holstein vertreten durch das Ministerium für Energiewende, Klimaschutz, Umwelt und Natur + * Zentrales IT-Management + * + * 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.security; + +import static de.ozgcloud.admin.security.SecurityConfiguration.*; + +import java.util.List; +import java.util.Map; + +import org.springframework.security.oauth2.jwt.Jwt; + +public class JwtTestFactory { + + public static final String ROLE_1 = "ADMIN_ADMIN"; + public static final String ROLE_2 = "Lower_case"; + public static final String ROLE_3 = "UPPER"; + + public static final String AUTH_RESOURCE = "admin"; + + public static Jwt create() { + return createBuilder().build(); + } + + public static Jwt.Builder createWithRoles(List<String> roles) { + return createBuilder().claim(RESOURCE_ACCESS_KEY, Map.of(AUTH_RESOURCE, Map.of(ROLES_KEY, roles))); + } + + public static Jwt.Builder createBuilder() { + return Jwt.withTokenValue("token-value").header("header-key", "header-value").claim("claim-key", "claim-value"); + } + +} diff --git a/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationITCase.java b/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationITCase.java index d8bf3dcae4e6b88a078fa58c860d8365bb77ae60..711a2d8806c5c79b96082073c7bfb09bc7294d76 100644 --- a/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationITCase.java +++ b/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationITCase.java @@ -46,9 +46,9 @@ class SecurityConfigurationITCase { @Autowired private MockMvc mockMvc; - @DisplayName("without authorization") + @DisplayName("without authentication") @Nested - class TestWithoutAuthorization { + class TestWithoutAuthentication { @DisplayName("allow for not found") @SneakyThrows @@ -132,33 +132,76 @@ class SecurityConfigurationITCase { } } - @DisplayName("with authorization") + @DisplayName("with authentication") @Nested - class TestWithAuthorization { - + class TestWithAuthentication { static final String CLAIMS = """ { "preferredUsername": "testUser", "scope": "openid testscope" }"""; + @Test @SneakyThrows - @ParameterizedTest - @ValueSource(strings = { - "/api/environment", - "/configserver/name/profile", - "/api", "/api/configuration", "/api/configuration/settings", - }) @WithJwt(CLAIMS) - void shouldAllow(String path) { - var result = doPerformAuthenticated(path); + void shouldAllowApiEndpoint() { + var result = doPerformAuthenticated("/api"); result.andExpect(status().isOk()); } + @Test + @SneakyThrows + @WithJwt(CLAIMS) + void shouldForbidSettingsEndpoint() { + var result = doPerformAuthenticated("/api/configuration/settings"); + + result.andExpect(status().isForbidden()); + } + + @Test + @SneakyThrows + @WithJwt(CLAIMS) + void shouldForbidConfigurationsEndpoint() { + var result = doPerformAuthenticated("/api/configuration"); + + result.andExpect(status().isForbidden()); + } + @SneakyThrows private ResultActions doPerformAuthenticated(String path) { return mockMvc.perform(get(path)); } } + + @DisplayName("with admin role") + @Nested + class TestWithAdminRole { + + static final String CLAIMS = """ + { + "preferredUsername": "testUser", + "scope": "openid testscope", + "resource_access": { "admin": { "roles": ["ADMIN_ADMIN"] } } + }"""; + + + @Test + @SneakyThrows + @WithJwt(CLAIMS) + void shouldAllowSettings() { + var result = mockMvc.perform(get("/api/configuration/settings")); + + result.andExpect(status().isOk()); + } + + @Test + @SneakyThrows + @WithJwt(CLAIMS) + void shouldAllowConfiguration() { + var result = mockMvc.perform(get("/api/configuration")); + + result.andExpect(status().isOk()); + } + } } diff --git a/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationTest.java b/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..9acdae93189e5d6e25d65361c78929584da935f7 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationTest.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2024. Das Land Schleswig-Holstein vertreten durch das Ministerium für Energiewende, Klimaschutz, Umwelt und Natur + * Zentrales IT-Management + * + * 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.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; + +class SecurityConfigurationTest { + + @Spy + @InjectMocks + private SecurityConfiguration securityConfiguration; + + @Mock + private OAuth2Properties oAuth2Properties; + + @DisplayName("jwt authentication converter") + @Nested + 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); + } + + } +} diff --git a/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationWithKeycloakITCase.java b/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationWithKeycloakITCase.java deleted file mode 100644 index e336b21c6bde4f0904228557051a411c687276a9..0000000000000000000000000000000000000000 --- a/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationWithKeycloakITCase.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright - * (C) 2024 Das Land Schleswig-Holstein vertreten durch das - * Minis - * erium für Energiewende, Klimaschutz, Umwelt und Natur Zentrales - * IT-Management - * - * Lizenziert unter der EUPL, Version 1.2 oder - sobald diese von der - * Europäischen Kommission genehmigt wurden - Folgeversionen der EUPL - * - * - * 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.security; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -import java.net.URI; -import java.util.Collections; -import java.util.Map; - -import org.apache.http.client.utils.URIBuilder; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.params.ParameterizedTest; -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.MediaType; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.client.RestClient; - -import dasniko.testcontainers.keycloak.KeycloakContainer; -import de.ozgcloud.admin.RootController; -import de.ozgcloud.common.test.DataITCase; -import lombok.SneakyThrows; - -@DataITCase -@AutoConfigureMockMvc -class SecurityConfigurationWithKeycloakITCase { - @Autowired - private MockMvc mockMvc; - - static KeycloakContainer keycloak; - - @BeforeAll - static void setupKeycloakContainer() { - keycloak = new KeycloakContainer().withRealmImportFile("keycloak/realm-export.json"); - keycloak.start(); - } - - @AfterAll - static void closeKeycloakContainer() { - keycloak.close(); - } - - @DynamicPropertySource - static void registerResourceServerIssuerProperty(DynamicPropertyRegistry registry) { - registry.add("spring.security.oauth2.resourceserver.jwt.issuer-uri", () -> keycloak.getAuthServerUrl() + "/realms/by-kiel-dev"); - } - - @Nested - class TestSecuredEndpointWithKeycloakToken { - @SneakyThrows - @ParameterizedTest - @ValueSource(strings = { - "/api/environment", - "/configserver/name/profile", - "/api", "/api/configuration", "/api/configuration/param", - }) - void shouldGetAccessWithToken() { - String token = getToken(); - - var result = mockMvc.perform(get(RootController.PATH).header("Authorization", token)); - - result.andExpect(status().isOk()); - } - - @SneakyThrows - String getToken() { - MultiValueMap<String, String> formData = setPostBodyForToken(); - - Map<String, String> resultBody = performPostRequestToKeycloak(formData); - - return "Bearer " + resultBody.get("access_token").toString(); - } - - MultiValueMap<String, String> setPostBodyForToken() { - MultiValueMap<String, String> formData = new LinkedMultiValueMap<>(); - formData.put("grant_type", Collections.singletonList("password")); - formData.put("client_id", Collections.singletonList("admin")); - formData.put("username", Collections.singletonList("admin-test")); - formData.put("password", Collections.singletonList("Password")); - return formData; - } - - @SuppressWarnings("unchecked") - @SneakyThrows - Map<String, String> performPostRequestToKeycloak(MultiValueMap<String, String> formData) { - RestClient restClient = RestClient.create(); - URI authorizationURI = new URIBuilder(keycloak.getAuthServerUrl() + "/realms/by-kiel-dev/protocol/openid-connect/token").build(); - var response = restClient.post().uri(authorizationURI) - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .body(formData); - return response.retrieve().body(Map.class); - - } - - } -} \ No newline at end of file diff --git a/src/test/java/de/ozgcloud/admin/setting/SettingITCase.java b/src/test/java/de/ozgcloud/admin/setting/SettingITCase.java index 7bdbbf2ffc2120affc2cb715f47da788ced89619..04aaf44fb4105118d90343fc2271e58479959173 100644 --- a/src/test/java/de/ozgcloud/admin/setting/SettingITCase.java +++ b/src/test/java/de/ozgcloud/admin/setting/SettingITCase.java @@ -28,6 +28,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import java.util.List; +import de.ozgcloud.admin.common.user.UserRole; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -55,7 +56,7 @@ import lombok.SneakyThrows; @DataITCase @AutoConfigureMockMvc -@WithMockUser +@WithMockUser(roles = UserRole.ADMIN_ADMIN) class SettingITCase { @Autowired @@ -108,7 +109,7 @@ class SettingITCase { class TestForSettingWithPostfach { private static final String POSTFACH_NAME = "Postfach"; - private Setting settingWithPostfach = SettingTestFactory.createBuilder() + private final Setting settingWithPostfach = SettingTestFactory.createBuilder() .name(POSTFACH_NAME) .settingBody(PostfachSettingBodyTestFactory.create()) .build(); @@ -284,13 +285,13 @@ class SettingITCase { @Nested class TestPut { private String id; - private PostfachSettingBody updatedPostfach = PostfachSettingBodyTestFactory.createBuilder() + private final PostfachSettingBody updatedPostfach = PostfachSettingBodyTestFactory.createBuilder() .absender(AbsenderTestFactory.createBuilder() .name("Neuer Name") .anschrift("Neue Anschrift") .build()) .build(); - private Setting updatedSetting = SettingTestFactory.createBuilder() + private final Setting updatedSetting = SettingTestFactory.createBuilder() .name(POSTFACH_NAME) .settingBody(updatedPostfach) .build(); diff --git a/src/test/resources/application-itcase.yaml b/src/test/resources/application-itcase.yaml index fc717c37b9cfb97360022930cb07c950b16d1983..3082babc0c50e52484ef75f5a650980975eea15d 100644 --- a/src/test/resources/application-itcase.yaml +++ b/src/test/resources/application-itcase.yaml @@ -1,2 +1,8 @@ mongock: - enabled: false \ No newline at end of file + enabled: false + +ozgcloud: + oauth2: + auth-server-url: https://sso.it-case.de + realm: by-kiel-dev + resource: admin diff --git a/src/test/resources/application.yaml b/src/test/resources/application.yaml deleted file mode 100644 index bfdb349dcc960848ab0c7815b86b4c923f718393..0000000000000000000000000000000000000000 --- a/src/test/resources/application.yaml +++ /dev/null @@ -1,25 +0,0 @@ - -management: - server: - port: 8081 -spring: - application: - name: OzgCloud_Administration - data: - mongodb: - authentication-database: admin - rest: - basePath: /api/configuration - cloud: - config: - server: - prefix: /configserver - security: - oauth2: - resourceserver: - jwt: - issuer-uri: ${ozgcloud.oauth2.auth-server-url}/realms/${ozgcloud.oauth2.realm} -ozgcloud: - oauth2: - auth-server-url: https://sso.dev.by.ozg-cloud.de - realm: by-kiel-dev \ No newline at end of file diff --git a/src/test/resources/jsonTemplates/security/resource_access.template.json b/src/test/resources/jsonTemplates/security/resource_access.template.json new file mode 100644 index 0000000000000000000000000000000000000000..44f43d8cb40f1ee887fe158bb9668c1074251f5d --- /dev/null +++ b/src/test/resources/jsonTemplates/security/resource_access.template.json @@ -0,0 +1,7 @@ +{ + "admin": { + "roles": [ + "ADMIN_ADMIN" + ] + } +}