From 3f82ec26579ef9ce90d0880314b24fa3519b65c2 Mon Sep 17 00:00:00 2001 From: jquiry Date: Thu, 17 Apr 2025 08:35:17 +0200 Subject: [PATCH] Add subject type 'scope' for role mapping (#1014) --- .../extractor/OauthAuthorityExtractor.java | 46 +++++++++++++------ ...exBasedProviderAuthorityExtractorTest.java | 26 +++++++++-- api/src/test/resources/roles_definition.yaml | 3 ++ 3 files changed, 58 insertions(+), 17 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/OauthAuthorityExtractor.java b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/OauthAuthorityExtractor.java index 61748610e..78a648ff4 100644 --- a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/OauthAuthorityExtractor.java +++ b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/OauthAuthorityExtractor.java @@ -10,11 +10,13 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; import org.springframework.util.Assert; import reactor.core.publisher.Mono; @@ -72,32 +74,48 @@ private Set extractRoles(AccessControlService acs, DefaultOAuth2User pri Map additionalParams) { var provider = (OAuthProperties.OAuth2Provider) additionalParams.get("provider"); Assert.notNull(provider, "provider is null"); + var rolesFieldName = provider.getCustomParams().get(ROLES_FIELD_PARAM_NAME); + Set roles = new HashSet<>(); if (rolesFieldName == null) { - log.warn("Provider [{}] doesn't contain a roles field param name, won't map roles", provider.getClientName()); - return Collections.emptySet(); + log.warn("Provider [{}] doesn't contain a roles field param name, using default authorities/scopes for role mapping", provider.getClientName()); + } else { + var principalRoles = convertRoles(principal.getAttribute(rolesFieldName)); + + if (principalRoles.isEmpty()) { + log.debug("Principal [{}] doesn't have any roles through configured roles-field, using default authorities", principal.getName()); + } else { + log.debug("Token's groups: [{}]. Mapping providers of type role.", String.join(",", principalRoles)); + roles.addAll(acs.getRoles().stream() + .filter(role -> role.getSubjects() + .stream() + .filter(s -> s.getProvider().equals(Provider.OAUTH)) + .filter(s -> s.getType().equals("role")) + .anyMatch(subject -> principalRoles.stream().anyMatch(subject::matches))) + .map(Role::getName) + .collect(Collectors.toSet())); + } } - var principalRoles = convertRoles(principal.getAttribute(rolesFieldName)); - if (principalRoles.isEmpty()) { - log.debug("Principal [{}] doesn't have any roles, nothing to do", principal.getName()); - return Collections.emptySet(); - } + // Mapping groups from scopes + Set scopes = principal.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .map(s -> s.replace("SCOPE_", "")) + .collect(Collectors.toSet()); - log.debug("Token's groups: [{}]", String.join(",", principalRoles)); + log.debug("Available scopes: [{}]. Mapping providers of type scope.", String.join(",", scopes)); - Set roles = acs.getRoles() - .stream() + roles.addAll(acs.getRoles().stream() .filter(role -> role.getSubjects() .stream() .filter(s -> s.getProvider().equals(Provider.OAUTH)) - .filter(s -> s.getType().equals("role")) - .anyMatch(subject -> principalRoles.stream().anyMatch(subject::matches))) + .filter(s -> s.getType().equals("scope")) + .anyMatch(subject -> scopes.stream().anyMatch(subject::matches))) .map(Role::getName) - .collect(Collectors.toSet()); + .collect(Collectors.toSet())); - log.debug("Matched group roles: [{}]", String.join(", ", roles)); + log.debug("Matched group/scope roles: [{}]", String.join(", ", roles)); return roles; } diff --git a/api/src/test/java/io/kafbat/ui/config/RegexBasedProviderAuthorityExtractorTest.java b/api/src/test/java/io/kafbat/ui/config/RegexBasedProviderAuthorityExtractorTest.java index 11eec0ea4..c40ce336d 100644 --- a/api/src/test/java/io/kafbat/ui/config/RegexBasedProviderAuthorityExtractorTest.java +++ b/api/src/test/java/io/kafbat/ui/config/RegexBasedProviderAuthorityExtractorTest.java @@ -7,8 +7,6 @@ import static org.mockito.Mockito.when; import static org.springframework.security.oauth2.client.registration.ClientRegistration.withRegistrationId; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; import io.kafbat.ui.config.auth.OAuthProperties; import io.kafbat.ui.model.rbac.Role; @@ -23,6 +21,7 @@ import java.io.InputStream; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -79,7 +78,6 @@ void extractOauth2Authorities() { assertNotNull(roles); assertEquals(Set.of("viewer", "admin"), roles); assertFalse(roles.contains("no one's role")); - } @SneakyThrows @@ -106,6 +104,28 @@ void extractOauth2Authorities_blankEmail() { } + @SneakyThrows + @Test + void extractOauth2Authorities_fromScopes() { + + extractor = new OauthAuthorityExtractor(); + + OAuth2User oauth2User = new DefaultOAuth2User( + AuthorityUtils.createAuthorityList("SCOPE_message:all"), + Map.of("role_definition", Collections.emptySet(), "user_name", "invalidUser"), + "user_name"); + + HashMap additionalParams = new HashMap<>(); + OAuthProperties.OAuth2Provider provider = new OAuthProperties.OAuth2Provider(); + provider.setCustomParams(Map.of("roles-field", "role_definition")); + additionalParams.put("provider", provider); + + Set roles = extractor.extract(accessControlService, oauth2User, additionalParams).block(); + + assertNotNull(roles); + assertEquals(Set.of("admin"), roles); + } + @SneakyThrows @Test void extractCognitoAuthorities() { diff --git a/api/src/test/resources/roles_definition.yaml b/api/src/test/resources/roles_definition.yaml index f9af4f507..6474ebd4a 100644 --- a/api/src/test/resources/roles_definition.yaml +++ b/api/src/test/resources/roles_definition.yaml @@ -4,6 +4,9 @@ value: 'ROLE-[A-Z]+' type: 'role' isRegex: 'true' + - provider: 'OAUTH' + value: 'message:all' + type: 'scope' - provider: 'OAUTH_COGNITO' value: 'ROLE-ADMIN' type: 'group'