diff --git a/.gitignore b/.gitignore index 3c0e87d4..537270f9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,4 @@ .attach_pid* .flattened-pom.xml .mvn/wrapper/maven-wrapper.jar - +.factorypath diff --git a/datadir b/datadir index f0cec67a..c6d4d204 160000 --- a/datadir +++ b/datadir @@ -1 +1 @@ -Subproject commit f0cec67aae4b79bb0105c06736d423f20c9ea057 +Subproject commit c6d4d2045d4fb0485b55c70b2b8b6ace821d6b89 diff --git a/docs/roles-mappings.adoc b/docs/roles-mappings.adoc new file mode 100644 index 00000000..43f62abb --- /dev/null +++ b/docs/roles-mappings.adoc @@ -0,0 +1,55 @@ +== Adding additional roles + +The Gateway sends a `sec-roles` HTTP request header to the backend services +with the role names provided by the authentication provider. + +Sometimes, these roles are unsuficcient as some services may require additional +or different role names. + +For example, let's say the OpenID Connect provider gives us a role namded `GDI.ADMIN`, and +we want users with that role to be GeoServer administrators, which requires the user to belong to the `ROLE_ADMINISTRATOR` role. + +Hence, we can tell the Gateway to add the `ROLE_ADMINISTRATOR` role to any user logged in +with the `GDI.ADMIN` role. + +This can be configured in the geOrchestra data directory's `gateway/gateway.yaml` file. Though +for the sake of separation of concerns across config files, we're using the `gateway/roles-mappings.yml` file, which is imported from `gateway/gateway.yml`. + +This configuration file allows to extend the list of security role names +assigned to a user, from the role names extracted by the authentication +provider (e.g. LDAP, Oauth2, OpenID Connect). + +Limited regular expression support: only the `*` character is allowed +as a wildcard on a source role name. For example, the following mapping +will add the `ROLE_USER` role to all authenticated users that already +have any role name starting with `ROLE_GP.GDI.` + +Note that for the key names (source roles) to include special characters, +you must use the format '[role.name.*]' for the literal string role.name.* +to be interpreted correctly. + +[source,yaml] +---- +georchestra: + gateway: + role-mappings: + '[ROLE_GP.GDI.*]' + - ROLE_USER +---- + +If an authentication provider role name matches multiple mappings, +all the matching additional roles will be appended. For example, the +following mappings will add both `ROLE_USER` and `ROLE_ADMINISTRATOR` +to a user with role `ROLE_GP.GDI.ADMINISTRATOR`, but only `ROLE_USER` +to any other with a role starting with `ROLE_GP.GDI.`: + +[source,yaml] +---- +georchestra: + gateway: + role-mappings: + '[ROLE_GP.GDI.*]': + - ROLE_USER + '[ROLE_GP.GDI.ADMINISTRATOR]': + - ROLE_ADMINISTRATOR +---- diff --git a/gateway/docker-compose.yml b/gateway/docker-compose.yml new file mode 100644 index 00000000..c09c61a9 --- /dev/null +++ b/gateway/docker-compose.yml @@ -0,0 +1,99 @@ +version: "3.1" + +volumes: + postgresql_data: + datadir: + driver_opts: + type: none + o: bind + device: $PWD/datadir + +secrets: + slapd_password: + file: ./datadir/secrets/slapd_password.txt + geoserver_privileged_user_passwd: + file: ./datadir/secrets/geoserver_privileged_user_passwd.txt + +services: + database: + image: georchestra/database:latest + environment: + - POSTGRES_USER=georchestra + - POSTGRES_PASSWORD=georchestra + volumes: + - postgresql_data:/var/lib/postgresql/data + restart: always + ports: + - 54321:5432 + + ldap: + image: georchestra/ldap:latest + secrets: + - slapd_password + - geoserver_privileged_user_passwd + environment: + - SLAPD_ORGANISATION=georchestra + - SLAPD_DOMAIN=georchestra.org + - SLAPD_PASSWORD_FILE=/run/secrets/slapd_password + - SLAPD_PASSWORD= + - GEOSERVER_PRIVILEGED_USER_PASSWORD_FILE=/run/secrets/geoserver_privileged_user_passwd + - SLAPD_LOG_LEVEL=32768 # See https://www.openldap.org/doc/admin24/slapdconfig.html#loglevel%20%3Clevel%3E + restart: always + ports: + - 3891:389 + + gateway: + image: georchestra/gateway:latest + depends_on: + - ldap + - database + volumes: + - datadir:/etc/georchestra + environment: + - JAVA_TOOL_OPTIONS=-Dgeorchestra.datadir=/etc/georchestra -Dspring.profiles.active=docker -Xmx512M + restart: always + ports: + - 8080:8080 + - 8090:8090 + + header: + image: georchestra/header:latest + volumes: + - datadir:/etc/georchestra + environment: + - JAVA_OPTIONS=-Dorg.eclipse.jetty.annotations.AnnotationParser.LEVEL=OFF + - XMS=256M + - XMX=512M + restart: always + ports: + - 10003:8080 + + geoserver: + image: georchestra/geoserver:latest + depends_on: + - ldap + volumes: + - datadir:/etc/georchestra + environment: + - JAVA_OPTIONS=-Dorg.eclipse.jetty.annotations.AnnotationParser.LEVEL=OFF + - XMS=256M + - XMX=8G + restart: always + ports: + - 10006:8080 + + console: + image: georchestra/console:latest + depends_on: + - ldap + - database + volumes: + - datadir:/etc/georchestra + environment: + - JAVA_OPTIONS=-Dorg.eclipse.jetty.annotations.AnnotationParser.LEVEL=OFF + - XMS=256M + - XMX=1G + restart: always + ports: + - 10007:8080 + \ No newline at end of file diff --git a/gateway/pom.xml b/gateway/pom.xml index c97b337c..4474c198 100644 --- a/gateway/pom.xml +++ b/gateway/pom.xml @@ -237,11 +237,21 @@ - pl.project13.maven - git-commit-id-plugin + io.github.git-commit-id + git-commit-id-maven-plugin + 5.0.0 + + + get-the-git-infos + initialize + + revision + + + - ${project.basedir}/../.git false + true diff --git a/gateway/src/main/java/org/georchestra/gateway/filter/headers/providers/GeorchestraUserHeadersContributor.java b/gateway/src/main/java/org/georchestra/gateway/filter/headers/providers/GeorchestraUserHeadersContributor.java index b4ceb481..7480cc4a 100644 --- a/gateway/src/main/java/org/georchestra/gateway/filter/headers/providers/GeorchestraUserHeadersContributor.java +++ b/gateway/src/main/java/org/georchestra/gateway/filter/headers/providers/GeorchestraUserHeadersContributor.java @@ -52,8 +52,7 @@ public class GeorchestraUserHeadersContributor extends HeaderContributor { add(headers, "sec-lastname", mappings.getLastname(), user.map(GeorchestraUser::getLastName)); add(headers, "sec-tel", mappings.getTel(), user.map(GeorchestraUser::getTelephoneNumber)); - List roles = user.map(GeorchestraUser::getRoles).orElse(List.of()).stream() - .map(r -> r.startsWith("ROLE_") ? r : "ROLE_" + r).collect(Collectors.toList()); + List roles = user.map(GeorchestraUser::getRoles).orElse(List.of()); add(headers, "sec-roles", mappings.getRoles(), roles); diff --git a/gateway/src/main/java/org/georchestra/gateway/model/GatewayConfigProperties.java b/gateway/src/main/java/org/georchestra/gateway/model/GatewayConfigProperties.java index 75fcb02b..5a6e3a7c 100644 --- a/gateway/src/main/java/org/georchestra/gateway/model/GatewayConfigProperties.java +++ b/gateway/src/main/java/org/georchestra/gateway/model/GatewayConfigProperties.java @@ -38,6 +38,8 @@ @ConfigurationProperties("georchestra.gateway") public class GatewayConfigProperties { + private Map> rolesMappings = Map.of(); + /** * Configures the global security headers to append to all proxied http requests */ diff --git a/gateway/src/main/java/org/georchestra/gateway/security/GatewaySecurityConfiguration.java b/gateway/src/main/java/org/georchestra/gateway/security/GatewaySecurityConfiguration.java index 12b58bc6..65649cb4 100644 --- a/gateway/src/main/java/org/georchestra/gateway/security/GatewaySecurityConfiguration.java +++ b/gateway/src/main/java/org/georchestra/gateway/security/GatewaySecurityConfiguration.java @@ -19,6 +19,7 @@ package org.georchestra.gateway.security; import java.util.List; +import java.util.Map; import java.util.stream.Stream; import org.georchestra.gateway.model.GatewayConfigProperties; @@ -78,12 +79,23 @@ private Stream sortedCustomizers(List Integer.compare(c1.getOrder(), c2.getOrder())); } - public @Bean GeorchestraUserMapper georchestraUserResolver(List resolvers) { - return new GeorchestraUserMapper(resolvers); + public @Bean GeorchestraUserMapper georchestraUserResolver(List resolvers, + List customizers) { + return new GeorchestraUserMapper(resolvers, customizers); } public @Bean ResolveGeorchestraUserGlobalFilter resolveGeorchestraUserGlobalFilter(GeorchestraUserMapper resolver) { return new ResolveGeorchestraUserGlobalFilter(resolver); } + /** + * Extension to make {@link GeorchestraUserMapper} append user roles based on + * {@link GatewayConfigProperties#getRolesMappings()} + */ + public @Bean RolesMappingsUserCustomizer rolesMappingsUserCustomizer(GatewayConfigProperties config) { + Map> rolesMappings = config.getRolesMappings(); + log.info("Creating {}", RolesMappingsUserCustomizer.class.getSimpleName()); + return new RolesMappingsUserCustomizer(rolesMappings); + } + } diff --git a/gateway/src/main/java/org/georchestra/gateway/security/GeorchestraUserCustomizerExtension.java b/gateway/src/main/java/org/georchestra/gateway/security/GeorchestraUserCustomizerExtension.java new file mode 100644 index 00000000..c5898135 --- /dev/null +++ b/gateway/src/main/java/org/georchestra/gateway/security/GeorchestraUserCustomizerExtension.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2022 by the geOrchestra PSC + * + * This file is part of geOrchestra. + * + * geOrchestra is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * geOrchestra is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * geOrchestra. If not, see . + */ + +package org.georchestra.gateway.security; + +import java.util.function.Function; + +import org.georchestra.security.model.GeorchestraUser; +import org.springframework.core.Ordered; + +/** + * Extension point to customize the state of a {@link GeorchestraUser} once it + * was obtained from an authentication provider by means of a + * {@link GeorchestraUserMapperExtension}. + * + * @see GeorchestraUserMapper + */ +public interface GeorchestraUserCustomizerExtension extends Ordered, Function { + + default int getOrder() { + return 0; + } +} diff --git a/gateway/src/main/java/org/georchestra/gateway/security/GeorchestraUserMapper.java b/gateway/src/main/java/org/georchestra/gateway/security/GeorchestraUserMapper.java index d88405f8..e53889c3 100644 --- a/gateway/src/main/java/org/georchestra/gateway/security/GeorchestraUserMapper.java +++ b/gateway/src/main/java/org/georchestra/gateway/security/GeorchestraUserMapper.java @@ -35,11 +35,22 @@ * token. *

* Relies on the provided {@link GeorchestraUserMapperExtension}s to map an - * {@link Authentication} to a {@link GeorchestraUsers}. + * {@link Authentication} to a {@link GeorchestraUsers}, and on + * {@link GeorchestraUserCustomizerExtension} to apply additional user + * customizations once resolved from {@link Authentication} to + * {@link GeorchestraUser}. *

* {@literal GeorchestraUserMapperExtension} beans specialize in mapping auth * tokens for specific authentication sources (e.g. LDAP, OAuth2, OAuth2+OpenID, * etc). + *

+ * {@literal GeorchestraUserCustomizerExtension} beans specialize in applying + * any additional customization to the {@link GeorchestraUser} object after it + * has been extracted from the {@link Authentication} created by the actual + * authentication provider. + * + * @see GeorchestraUserMapperExtension + * @see GeorchestraUserCustomizerExtension */ @RequiredArgsConstructor public class GeorchestraUserMapper { @@ -49,6 +60,16 @@ public class GeorchestraUserMapper { */ private final @NonNull List resolvers; + private final @NonNull List customizers; + + GeorchestraUserMapper() { + this(List.of(), List.of()); + } + + GeorchestraUserMapper(List resolvers) { + this(resolvers, List.of()); + } + /** * @return the first non-empty user from * {@link GeorchestraUserMapperExtension#resolve asking} the extension @@ -60,8 +81,15 @@ public Optional resolve(@NonNull Authentication authToken) { return resolvers.stream()// .map(resolver -> resolver.resolve(authToken))// .filter(Optional::isPresent)// - .map(Optional::get)// - .findFirst(); + .map(Optional::orElseThrow)// + .map(this::customize).findFirst(); } + private GeorchestraUser customize(GeorchestraUser user) { + GeorchestraUser customized = user; + for (GeorchestraUserCustomizerExtension customizer : customizers) { + customized = customizer.apply(customized); + } + return customized; + } } \ No newline at end of file diff --git a/gateway/src/main/java/org/georchestra/gateway/security/RolesMappingsUserCustomizer.java b/gateway/src/main/java/org/georchestra/gateway/security/RolesMappingsUserCustomizer.java new file mode 100644 index 00000000..8272a482 --- /dev/null +++ b/gateway/src/main/java/org/georchestra/gateway/security/RolesMappingsUserCustomizer.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2022 by the geOrchestra PSC + * + * This file is part of geOrchestra. + * + * geOrchestra is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * geOrchestra is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * geOrchestra. If not, see . + */ + +package org.georchestra.gateway.security; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.georchestra.security.model.GeorchestraUser; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * Authenticated user customizer extension to expand the set of role names + * assigned to a user by the actual authentication provider + */ +@Slf4j +public class RolesMappingsUserCustomizer implements GeorchestraUserCustomizerExtension { + + @RequiredArgsConstructor + private static class Matcher { + private final @NonNull Pattern pattern; + private final @NonNull @Getter List extraRoles; + + public boolean matches(String role) { + return pattern.matcher(role).matches(); + } + + public @Override String toString() { + return String.format("%s -> %s", pattern.pattern(), extraRoles); + } + } + + @VisibleForTesting + final List rolesMappings; + + private final Cache> byRoleNameCache = CacheBuilder.newBuilder().maximumSize(1_000).build(); + + public RolesMappingsUserCustomizer(@NonNull Map> rolesMappings) { + this.rolesMappings = keysToRegularExpressions(rolesMappings); + } + + private @NonNull List keysToRegularExpressions(Map> mappings) { + return mappings.entrySet()// + .stream()// + .map(e -> new Matcher(toPattern(e.getKey()), e.getValue()))// + .peek(m -> log.info("Loaded role mapping {}", m))// + .collect(Collectors.toList()); + } + + static Pattern toPattern(String role) { + String regex = role.replace(".", "(\\.)").replace("*", "(.*)"); + return Pattern.compile(regex); + } + + @Override + public GeorchestraUser apply(GeorchestraUser user) { + + Set additionalRoles = computeAdditionalRoles(user.getRoles()); + if (!additionalRoles.isEmpty()) { + additionalRoles.addAll(user.getRoles()); + user.setRoles(new ArrayList<>(additionalRoles)); + } + return user; + } + + /** + * @param authenticatedRoles the role names extracted from the authentication + * provider + * @return the additional role names for the user + */ + private Set computeAdditionalRoles(List authenticatedRoles) { + final ConcurrentMap> cache = byRoleNameCache.asMap(); + return authenticatedRoles.stream().map(role -> cache.computeIfAbsent(role, this::computeAdditionalRoles)) + .flatMap(List::stream).collect(Collectors.toSet()); + } + + private List computeAdditionalRoles(@NonNull String authenticatedRole) { + + List roles = rolesMappings.stream().filter(m -> m.matches(authenticatedRole)) + .map(Matcher::getExtraRoles).flatMap(List::stream).collect(Collectors.toList()); + + log.info("Computed additional roles for {}: {}", authenticatedRole, roles); + return roles; + } +} diff --git a/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/GeorchestraLdapAuthenticatedUserMapper.java b/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/GeorchestraLdapAuthenticatedUserMapper.java index 4ebc78fd..0693915e 100644 --- a/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/GeorchestraLdapAuthenticatedUserMapper.java +++ b/gateway/src/main/java/org/georchestra/gateway/security/ldap/extended/GeorchestraLdapAuthenticatedUserMapper.java @@ -19,12 +19,17 @@ package org.georchestra.gateway.security.ldap.extended; +import java.util.List; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import org.georchestra.gateway.security.GeorchestraUserMapperExtension; import org.georchestra.security.api.UsersApi; import org.georchestra.security.model.GeorchestraUser; import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.ldap.userdetails.LdapUserDetails; import lombok.NonNull; @@ -59,7 +64,24 @@ Optional map(GeorchestraUserNamePasswordAuthenticationToken tok final LdapUserDetails principal = (LdapUserDetails) token.getPrincipal(); final String ldapConfigName = token.getConfigName(); final String username = principal.getUsername(); - return users.findByUsername(ldapConfigName, username); + + Optional user = users.findByUsername(ldapConfigName, username); + return user.map(u -> fixPrefixedRoleNames(u, token)); } + private GeorchestraUser fixPrefixedRoleNames(GeorchestraUser user, + GeorchestraUserNamePasswordAuthenticationToken token) { + + // Fix role name mismatch between authority provider (adds ROLE_ prefix) and + // users api + Set prefixedRoleNames = token.getAuthorities().stream().filter(SimpleGrantedAuthority.class::isInstance) + .map(GrantedAuthority::getAuthority).filter(role -> role.startsWith("ROLE_")) + .collect(Collectors.toSet()); + + List roles = user.getRoles().stream() + .map(r -> prefixedRoleNames.contains("ROLE_" + r) ? "ROLE_" + r : r).collect(Collectors.toList()); + + user.setRoles(roles); + return user; + } } diff --git a/gateway/src/main/resources/application.yml b/gateway/src/main/resources/application.yml index e3a2b036..ef037b07 100644 --- a/gateway/src/main/resources/application.yml +++ b/gateway/src/main/resources/application.yml @@ -136,4 +136,4 @@ logging: --- spring.config.activate.on-profile: dev -spring.config.import: file:../datadir/default.properties,file:../datadir/gateway/gateway.yaml +spring.config.import: file:./datadir/default.properties,file:./datadir/gateway/gateway.yaml diff --git a/gateway/src/test/java/org/georchestra/gateway/filter/headers/providers/GeorchestraUserHeadersContributorTest.java b/gateway/src/test/java/org/georchestra/gateway/filter/headers/providers/GeorchestraUserHeadersContributorTest.java index 47c738fa..67424198 100644 --- a/gateway/src/test/java/org/georchestra/gateway/filter/headers/providers/GeorchestraUserHeadersContributorTest.java +++ b/gateway/src/test/java/org/georchestra/gateway/filter/headers/providers/GeorchestraUserHeadersContributorTest.java @@ -128,22 +128,4 @@ void testContributesHeadersFromUser() { String roles = user.getRoles().stream().collect(Collectors.joining(";")); assertEquals(List.of(roles), target.get("sec-roles")); } - - @Test - void testRolePrefixAppendedToRoleNames() { - - GeorchestraUser user = new GeorchestraUser(); - user.setRoles(List.of("ROLE_ADMIN", "USER", "EDITOR")); - - final List expected = List.of("ROLE_ADMIN;ROLE_USER;ROLE_EDITOR"); - - GeorchestraUsers.store(exchange, user); - - matchedRouteHeadersConfig.disableAll(); - matchedRouteHeadersConfig.setRoles(Optional.of(true)); - - HttpHeaders target = new HttpHeaders(); - headerContributor.prepare(exchange).accept(target); - assertEquals(expected, target.get("sec-roles")); - } } diff --git a/gateway/src/test/java/org/georchestra/gateway/security/GeorchestraUserMapperTest.java b/gateway/src/test/java/org/georchestra/gateway/security/GeorchestraUserMapperTest.java index df0a8550..43591a9c 100644 --- a/gateway/src/test/java/org/georchestra/gateway/security/GeorchestraUserMapperTest.java +++ b/gateway/src/test/java/org/georchestra/gateway/security/GeorchestraUserMapperTest.java @@ -19,6 +19,7 @@ package org.georchestra.gateway.security; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -91,4 +92,32 @@ void testResolveOrder() { assertSame(user1, resolved.get()); } + @Test + void testAppliesPosResolvingCustomizerExtensions() { + Authentication auth = mock(Authentication.class); + + GeorchestraUser user = new GeorchestraUser(); + GeorchestraUserMapperExtension userMapper = mock(GeorchestraUserMapperExtension.class); + when(userMapper.resolve(same(auth))).thenReturn(Optional.of(user)); + + GeorchestraUserCustomizerExtension customizer1 = u -> { + u.setUsername("customizer1"); + return u; + }; + + GeorchestraUserCustomizerExtension customizer2 = u -> { + u.setRoles(List.of("ROLE_1", "ROLE_2")); + return u; + }; + + List postResolveCustomizers = List.of(customizer1, customizer2); + + GeorchestraUserMapper mapper = new GeorchestraUserMapper(List.of(userMapper), postResolveCustomizers); + Optional resolved = mapper.resolve(auth); + assertTrue(resolved.isPresent()); + assertSame(user, resolved.get()); + + assertEquals("customizer1", resolved.get().getUsername()); + assertEquals(List.of("ROLE_1", "ROLE_2"), resolved.get().getRoles()); + } } diff --git a/gateway/src/test/java/org/georchestra/gateway/security/RolesMappingsUserCustomizerTest.java b/gateway/src/test/java/org/georchestra/gateway/security/RolesMappingsUserCustomizerTest.java new file mode 100644 index 00000000..f0d5b5a9 --- /dev/null +++ b/gateway/src/test/java/org/georchestra/gateway/security/RolesMappingsUserCustomizerTest.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2022 by the geOrchestra PSC + * + * This file is part of geOrchestra. + * + * geOrchestra is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * geOrchestra is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * geOrchestra. If not, see . + */ + +package org.georchestra.gateway.security; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import org.georchestra.security.model.GeorchestraUser; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test suite for {@link RolesMappingsUserCustomizer} + */ +class RolesMappingsUserCustomizerTest { + + private GeorchestraUser user; + private Map> config; + + @BeforeEach + void setUp() { + config = new HashMap<>(); + user = new GeorchestraUser(); + } + + private void addConfig(String role, String... additionalRoles) { + config.put(role, List.of(additionalRoles)); + } + + @Test + void constructorCreatesValidPatterns() { + Pattern pattern; + + pattern = RolesMappingsUserCustomizer.toPattern("ROLE.GDI.USER"); + assertTrue(pattern.matcher("ROLE.GDI.USER").matches()); + assertFalse(pattern.matcher("ROLE.GDI_USER").matches()); + + pattern = RolesMappingsUserCustomizer.toPattern("ROLE.*.*.ADMIN"); + assertTrue(pattern.matcher("ROLE.GDI.GS.ADMIN").matches()); + assertFalse(pattern.matcher("ROLE.GDI.GS.USER").matches()); + } + + @Test + void emptyConfig() { + RolesMappingsUserCustomizer customizer = new RolesMappingsUserCustomizer(config); + user.setRoles(List.of("ROLE_USER")); + GeorchestraUser customized = customizer.apply(user); + assertSame(user, customized); + assertEquals(List.of("ROLE_USER"), customized.getRoles()); + } + + @Test + void matchesLiteralMappings() { + addConfig("ROLE_USER", "ROLE_EDITOR", "ROLE_USER", "ROLE_GUEST"); + addConfig("ROLE_ADMIN", "ROLE_GN_ADMIN", "ROLE_ADMINISTRATOR"); + + RolesMappingsUserCustomizer customizer = new RolesMappingsUserCustomizer(config); + GeorchestraUser customized; + + user.setRoles(List.of("ROLE_USER")); + customized = customizer.apply(user); + assertEquals(Set.of("ROLE_EDITOR", "ROLE_USER", "ROLE_GUEST"), Set.copyOf(customized.getRoles())); + + user.setRoles(List.of("ROLE_ADMIN")); + customized = customizer.apply(user); + assertEquals(Set.of("ROLE_ADMIN", "ROLE_GN_ADMIN", "ROLE_ADMINISTRATOR"), Set.copyOf(customized.getRoles())); + + user.setRoles(List.of("ROLE_ADMIN", "ROLE_USER")); + customized = customizer.apply(user); + assertEquals( + Set.of("ROLE_ADMIN", "ROLE_GN_ADMIN", "ROLE_ADMINISTRATOR", "ROLE_EDITOR", "ROLE_USER", "ROLE_GUEST"), + Set.copyOf(customized.getRoles())); + } + + @Test + void matchesRegexMappings() { + addConfig("ROLE.*.USER", "ROLE_USER", "ROLE_GUEST"); + addConfig("ROLE.*.ADMIN", "ROLE_GN_ADMIN", "ROLE_ADMINISTRATOR"); + + RolesMappingsUserCustomizer customizer = new RolesMappingsUserCustomizer(config); + GeorchestraUser customized; + + user.setRoles(List.of("ROLE_USER")); + customized = customizer.apply(user); + assertEquals(Set.of("ROLE_USER"), Set.copyOf(customized.getRoles())); + + user.setRoles(List.of("ROLE.GDI.USER")); + customized = customizer.apply(user); + assertEquals(Set.of("ROLE.GDI.USER", "ROLE_USER", "ROLE_GUEST"), Set.copyOf(customized.getRoles())); + + user.setRoles(List.of("ROLE.GDI.ADMIN")); + customized = customizer.apply(user); + assertEquals(Set.of("ROLE.GDI.ADMIN", "ROLE_GN_ADMIN", "ROLE_ADMINISTRATOR"), + Set.copyOf(customized.getRoles())); + + user.setRoles(List.of("ROLE.TEST.ADMIN", "ROLE.GDI.USER")); + customized = customizer.apply(user); + assertEquals(Set.of("ROLE.TEST.ADMIN", "ROLE.GDI.USER", "ROLE_GN_ADMIN", "ROLE_ADMINISTRATOR", "ROLE_USER", + "ROLE_GUEST"), Set.copyOf(customized.getRoles())); + } +} diff --git a/gateway/src/test/resources/test-datadir/gateway/gateway.yaml b/gateway/src/test/resources/test-datadir/gateway/gateway.yaml index f2ab0b15..638f9852 100644 --- a/gateway/src/test/resources/test-datadir/gateway/gateway.yaml +++ b/gateway/src/test/resources/test-datadir/gateway/gateway.yaml @@ -9,3 +9,18 @@ spring: - Path=/test georchestra: test-datadir: true #used to verify the config is loaded from the datadir + gateway: + roles-mappings: + '[ROLE_GP.GDI.*]': + - ROLE_USER + '[ROLE_GP.GDI.ADMINISTRATOR]': + - ROLE_SUPERUSER + - ROLE_ADMINISTRATOR + - ROLE_GN_ADMIN + '[ROLE_GP.GDI.GEODATA_MANAGER]': + - ROLE_ADMINISTRATOR + - ROLE_GN_ADMIN + '[ROLE_GP.GDI.FPIT_SERVICE_USER]': + - ROLE_ADMINISTRATOR + - ROLE_GN_ADMIN + - ROLE_GP.GDI.ADMINISTRATOR diff --git a/pom.xml b/pom.xml index 9243c0c3..0c9a743e 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ gateway - 21.0-SNAPSHOT + 22.1-SNAPSHOT 2021.0.1 2.10.0 false