diff --git a/src/main/java/de/ozgcloud/admin/security/SecurityConfiguration.java b/src/main/java/de/ozgcloud/admin/security/SecurityConfiguration.java index fcf7f24bc9836e7c93818a412b5fef855229ebd9..1318b9030d6829dd74f5c8cae32076ff78ff2696 100644 --- a/src/main/java/de/ozgcloud/admin/security/SecurityConfiguration.java +++ b/src/main/java/de/ozgcloud/admin/security/SecurityConfiguration.java @@ -19,7 +19,10 @@ */ package de.ozgcloud.admin.security; +import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Optional; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -30,10 +33,12 @@ 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.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 @@ -44,6 +49,12 @@ public class SecurityConfiguration { private final AdminAuthenticationEntryPoint authenticationEntryPoint; + private final OAuth2Properties oAuth2Properties; + + static final String RESOURCE_ACCESS_KEY = "resource_access"; + + static final String ROLES_KEY = "roles"; + @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -75,4 +86,25 @@ public class SecurityConfiguration { return jwtConverter; } -} \ No newline at end of file + List<String> getKeycloakRolesFromJwt(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/security/JwtTestFactory.java b/src/test/java/de/ozgcloud/admin/security/JwtTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..49c706189c0f436f66bb7af44444b19659672901 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/security/JwtTestFactory.java @@ -0,0 +1,62 @@ +/* + * 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))); + } + + // private static Map<String, Object> readResourceAccessClaim() { + // var claimsJson = TestUtils.loadTextFile("jsonTemplates/security/resource_access.template.json"); + // var mapper = new ObjectMapper(); + // try { + // return mapper.readValue(claimsJson, new TypeReference<Map<String, Object>>() { + // }); + // } catch (IOException e) { + // throw new RuntimeException(e); + // } + // } + + public static Jwt.Builder createBuilder() { + return Jwt.withTokenValue("AAAA").header("aa", "bb"); + } + +} 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..2bd806c7195c83341aebed3e6bbe70d64561d1f7 --- /dev/null +++ b/src/test/java/de/ozgcloud/admin/security/SecurityConfigurationTest.java @@ -0,0 +1,95 @@ +/* + * 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 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.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.oauth2.jwt.Jwt; + +import de.ozgcloud.admin.environment.OAuth2Properties; + +class SecurityConfigurationTest { + + @Spy + @InjectMocks + private SecurityConfiguration securityConfiguration; + @Mock + private AdminAuthenticationEntryPoint authenticationEntryPoint; + + @Mock + private OAuth2Properties oAuth2Properties; + + @BeforeEach + void mock() { + when(oAuth2Properties.getResource()).thenReturn(JwtTestFactory.AUTH_RESOURCE); + } + + @DisplayName("get keycloak roles from claims") + @Nested + class TestGetKeycloakRolesFromClaims { + + @DisplayName("should return empty if resource_access.admin.roles are missing") + @ParameterizedTest + @MethodSource("getIncompleteJwt") + void shouldReturnEmptyIfResourceAccessAdminRolesAreMissing(Jwt incompleteJwt) { + var rolesList = securityConfiguration.getKeycloakRolesFromJwt(incompleteJwt); + + assertThat(rolesList).isEmpty(); + } + + private static Stream<Arguments> getIncompleteJwt() { + return Stream.of(JwtTestFactory.create(), + JwtTestFactory.createBuilder().claim("resource_access", Map.of()).build(), + JwtTestFactory.createBuilder().claim("resource_access", Map.of("admin", Map.of())).build(), + JwtTestFactory.createWithRoles(emptyList()).build()) + .map(Arguments::of); + } + + @DisplayName("should return resource_access.admin.roles list") + @Test + void shouldReturnEmptyIfResourceAccessAdminRolesList() { + var expectedRoles = List.of(JwtTestFactory.ROLE_1, JwtTestFactory.ROLE_2, JwtTestFactory.ROLE_3); + var jwtWithClaims = JwtTestFactory.createWithRoles(expectedRoles).build(); + + var roles = securityConfiguration.getKeycloakRolesFromJwt(jwtWithClaims); + + assertThat(roles).isEqualTo(expectedRoles); + } + + } +} 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" + ] + } +}