diff --git a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/CognitoAuthorityExtractor.java b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/CognitoAuthorityExtractor.java index a246b8910..e75666d83 100644 --- a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/CognitoAuthorityExtractor.java +++ b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/CognitoAuthorityExtractor.java @@ -50,7 +50,7 @@ private Set extractUsernameRoles(AccessControlService acs, DefaultOAuth2 .stream() .filter(s -> s.getProvider().equals(Provider.OAUTH_COGNITO)) .filter(s -> s.getType().equals("user")) - .anyMatch(s -> s.getValue().equalsIgnoreCase(principal.getName()))) + .anyMatch(s -> principal.getName() != null && principal.getName().matches(s.getValue()))) .map(Role::getName) .collect(Collectors.toSet()); @@ -76,7 +76,7 @@ private Set extractGroupRoles(AccessControlService acs, DefaultOAuth2Use .filter(s -> s.getType().equals("group")) .anyMatch(subject -> groups .stream() - .anyMatch(cognitoGroup -> cognitoGroup.equalsIgnoreCase(subject.getValue())) + .anyMatch(cognitoGroup -> cognitoGroup.matches(subject.getValue())) )) .map(Role::getName) .collect(Collectors.toSet()); diff --git a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/GithubAuthorityExtractor.java b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/GithubAuthorityExtractor.java index b50e76a16..f08e266d3 100644 --- a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/GithubAuthorityExtractor.java +++ b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/GithubAuthorityExtractor.java @@ -90,7 +90,7 @@ private Set extractUsernameRoles(DefaultOAuth2User principal, AccessCont .stream() .filter(s -> s.getProvider().equals(Provider.OAUTH_GITHUB)) .filter(s -> s.getType().equals("user")) - .anyMatch(s -> s.getValue().equals(username))) + .anyMatch(s -> username.matches(s.getValue()))) .map(Role::getName) .collect(Collectors.toSet()); @@ -131,7 +131,7 @@ private Mono> getOrganizationRoles(DefaultOAuth2User principal, Map< .filter(s -> s.getType().equals(ORGANIZATION)) .anyMatch(subject -> orgsMap.stream() .map(org -> org.get(ORGANIZATION_NAME).toString()) - .anyMatch(orgName -> orgName.equalsIgnoreCase(subject.getValue())) + .anyMatch(orgName -> orgName.matches(subject.getValue())) )) .map(Role::getName) .collect(Collectors.toSet())); @@ -189,7 +189,7 @@ private Mono> getTeamRoles(WebClient webClient, Map .filter(s -> s.getProvider().equals(Provider.OAUTH_GITHUB)) .filter(s -> s.getType().equals("team")) .anyMatch(subject -> teams.stream() - .anyMatch(teamName -> teamName.equalsIgnoreCase(subject.getValue())) + .anyMatch(teamName -> teamName.matches(subject.getValue())) )) .map(Role::getName) .collect(Collectors.toSet())); diff --git a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/GoogleAuthorityExtractor.java b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/GoogleAuthorityExtractor.java index 8ea6d2108..c323e7ffd 100644 --- a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/GoogleAuthorityExtractor.java +++ b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/GoogleAuthorityExtractor.java @@ -50,7 +50,10 @@ private Set extractUsernameRoles(AccessControlService acs, DefaultOAuth2 .stream() .filter(s -> s.getProvider().equals(Provider.OAUTH_GOOGLE)) .filter(s -> s.getType().equals("user")) - .anyMatch(s -> s.getValue().equalsIgnoreCase(principal.getAttribute(EMAIL_ATTRIBUTE_NAME)))) + .anyMatch(s -> { + String email = principal.getAttribute(EMAIL_ATTRIBUTE_NAME); + return email != null && email.matches(s.getValue()); + })) .map(Role::getName) .collect(Collectors.toSet()); } @@ -68,7 +71,7 @@ private Set extractDomainRoles(AccessControlService acs, DefaultOAuth2Us .stream() .filter(s -> s.getProvider().equals(Provider.OAUTH_GOOGLE)) .filter(s -> s.getType().equals("domain")) - .anyMatch(s -> s.getValue().equals(domain))) + .anyMatch(s -> domain.matches(s.getValue()))) .map(Role::getName) .collect(Collectors.toSet()); } 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 6d14ab870..8812301a1 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 @@ -59,8 +59,8 @@ private Set extractUsernameRoles(AccessControlService acs, DefaultOAuth2 .filter(s -> s.getProvider().equals(Provider.OAUTH)) .filter(s -> s.getType().equals("user")) .peek(s -> log.trace("[{}] matches [{}]? [{}]", s.getValue(), principalName, - s.getValue().equalsIgnoreCase(principalName))) - .anyMatch(s -> s.getValue().equalsIgnoreCase(principalName))) + principalName != null && principalName.matches(s.getValue()))) + .anyMatch(s -> principalName != null && principalName.matches(s.getValue()))) .map(Role::getName) .collect(Collectors.toSet()); @@ -94,11 +94,7 @@ private Set extractRoles(AccessControlService acs, DefaultOAuth2User pri .stream() .filter(s -> s.getProvider().equals(Provider.OAUTH)) .filter(s -> s.getType().equals("role")) - .anyMatch(subject -> { - var roleName = subject.getValue(); - return principalRoles.contains(roleName); - }) - ) + .anyMatch(subject -> principalRoles.stream().anyMatch(s -> s.matches(subject.getValue())))) .map(Role::getName) .collect(Collectors.toSet()); diff --git a/api/src/test/java/io/kafbat/ui/config/RegexBasedProviderAuthorityExtractorTest.java b/api/src/test/java/io/kafbat/ui/config/RegexBasedProviderAuthorityExtractorTest.java new file mode 100644 index 000000000..7eb8c8bf1 --- /dev/null +++ b/api/src/test/java/io/kafbat/ui/config/RegexBasedProviderAuthorityExtractorTest.java @@ -0,0 +1,159 @@ +package io.kafbat.ui.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; +import static org.springframework.security.oauth2.client.registration.ClientRegistration.withRegistrationId; + +import io.kafbat.ui.config.auth.OAuthProperties; +import io.kafbat.ui.model.rbac.Role; +import io.kafbat.ui.service.rbac.AccessControlService; +import io.kafbat.ui.service.rbac.extractor.CognitoAuthorityExtractor; +import io.kafbat.ui.service.rbac.extractor.GithubAuthorityExtractor; +import io.kafbat.ui.service.rbac.extractor.GoogleAuthorityExtractor; +import io.kafbat.ui.service.rbac.extractor.OauthAuthorityExtractor; +import io.kafbat.ui.service.rbac.extractor.ProviderAuthorityExtractor; +import io.kafbat.ui.util.AccessControlServiceMock; +import java.io.InputStream; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.introspector.BeanAccess; + +public class RegexBasedProviderAuthorityExtractorTest { + + + private final AccessControlService accessControlService = new AccessControlServiceMock().getMock(); + Yaml yaml; + ProviderAuthorityExtractor extractor; + + @BeforeEach + void setUp() { + yaml = new Yaml(); + yaml.setBeanAccess(BeanAccess.FIELD); + + InputStream rolesFile = this.getClass() + .getClassLoader() + .getResourceAsStream("roles_definition.yaml"); + + Role[] roleArray = yaml.loadAs(rolesFile, Role[].class); + when(accessControlService.getRoles()).thenReturn(List.of(roleArray)); + + } + + @SneakyThrows + @Test + void extractOauth2Authorities() { + + extractor = new OauthAuthorityExtractor(); + + OAuth2User oauth2User = new DefaultOAuth2User( + AuthorityUtils.createAuthorityList("SCOPE_message:read"), + Map.of("role_definition", Set.of("ROLE-ADMIN", "ANOTHER-ROLE"), "user_name", "john@kafka.com"), + "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(); + + assertEquals(Set.of("viewer", "admin"), roles); + + } + + @SneakyThrows + @Test + void extractCognitoAuthorities() { + + extractor = new CognitoAuthorityExtractor(); + + OAuth2User oauth2User = new DefaultOAuth2User( + AuthorityUtils.createAuthorityList("SCOPE_message:read"), + Map.of("cognito:groups", List.of("ROLE-ADMIN", "ANOTHER-ROLE"), "user_name", "john@kafka.com"), + "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(); + + assertEquals(Set.of("viewer", "admin"), roles); + + } + + @SneakyThrows + @Test + void extractGithubAuthorities() { + + extractor = new GithubAuthorityExtractor(); + + OAuth2User oauth2User = new DefaultOAuth2User( + AuthorityUtils.createAuthorityList("SCOPE_message:read"), + Map.of("login", "john@kafka.com"), + "login"); + + HashMap additionalParams = new HashMap<>(); + + OAuthProperties.OAuth2Provider provider = new OAuthProperties.OAuth2Provider(); + additionalParams.put("provider", provider); + + additionalParams.put("request", new OAuth2UserRequest( + withRegistrationId("registration-1") + .clientId("client-1") + .clientSecret("secret") + .redirectUri("https://client.com") + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .authorizationUri("https://provider.com/oauth2/authorization") + .tokenUri("https://provider.com/oauth2/token") + .clientName("Client 1") + .build(), + new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "XXXX", Instant.now(), + Instant.now().plus(10, ChronoUnit.HOURS)))); + + Set roles = extractor.extract(accessControlService, oauth2User, additionalParams).block(); + + assertEquals(Set.of("viewer"), roles); + + } + + @SneakyThrows + @Test + void extractGoogleAuthorities() { + + extractor = new GoogleAuthorityExtractor(); + + OAuth2User oauth2User = new DefaultOAuth2User( + AuthorityUtils.createAuthorityList("SCOPE_message:read"), + Map.of("hd", "test.domain.com", "email", "john@kafka.com"), + "email"); + + 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(); + + assertEquals(Set.of("viewer", "admin"), roles); + + } + +} diff --git a/api/src/test/resources/roles_definition.yaml b/api/src/test/resources/roles_definition.yaml new file mode 100644 index 000000000..25e22b8a1 --- /dev/null +++ b/api/src/test/resources/roles_definition.yaml @@ -0,0 +1,49 @@ +- name: 'admin' + subjects: + - provider: 'OAUTH' + value: 'ROLE-[A-Z]+' + type: 'role' + - provider: 'OAUTH_COGNITO' + value: 'ROLE-[A-Z]+' + type: 'group' + - provider: 'OAUTH_GOOGLE' + value: '.*.domain.com' + type: 'domain' + clusters: + - local + - remote + permissions: + - resource: APPLICATIONCONFIG + actions: [ all ] +- name: 'viewer' + subjects: + - provider: 'LDAP' + value: 'CS-XXX' + type: 'kafka-viewer' + - provider: 'OAUTH' + value: '.*@kafka.com' + type: 'user' + - provider: 'OAUTH_COGNITO' + value: '.*@kafka.com' + type: 'user' + - provider: 'OAUTH_GITHUB' + value: '.*@kafka.com' + type: 'user' + - provider: 'OAUTH_GOOGLE' + value: '.*@kafka.com' + type: 'user' + clusters: + - remote + permissions: + - resource: APPLICATIONCONFIG + actions: [ all ] +- name: 'editor' + subjects: + - provider: 'OAUTH' + value: 'ROLE_EDITOR' + type: 'role' + clusters: + - local + permissions: + - resource: APPLICATIONCONFIG + actions: [ all ]