Skip to content

RBAC: Make it possible to use regex for values #663

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Mar 20, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bd2c6fd
Issues/300: rbac now supports regex for values
Nov 6, 2024
33b4a54
Issues/300: fix checkstyle
Nov 6, 2024
f15fc92
Issues/300: Other extractors handle regex as values.
Nov 12, 2024
1c47f7c
Issues/300: Log message now reflects the new matching strategy
Nov 12, 2024
d42b88a
Merge branch 'main' into issues/300-regex-for-rbac
francoisvandenplas Nov 12, 2024
0765d4a
Merge branch 'main' into issues/300-regex-for-rbac
francoisvandenplas Dec 16, 2024
8ff1e16
Merge branch 'main' into issues/300-regex-for-rbac
Haarolean Jan 14, 2025
4eb1f13
Merge branch 'main' into issues/300-regex-for-rbac
francoisvandenplas Feb 12, 2025
b689446
Issues/300: remove useless null check & add test scenarios
Feb 12, 2025
708b463
Issues/300: Remove useless null check
Feb 19, 2025
736cc4f
Merge branch 'main' into issues/300-regex-for-rbac
francoisvandenplas Feb 21, 2025
cf819d9
Merge branch 'main' into issues/300-regex-for-rbac
francoisvandenplas Feb 26, 2025
57d1937
feat: introduce regex option
callaertanthony Feb 27, 2025
997a7c4
Issues/300: Replace Lombok annotation & add log
Feb 28, 2025
9fe4604
Issues/300: Use Jackson to deserialize Roles[]
Mar 3, 2025
b06e9a4
Merge branch 'main' into issues/300-regex-for-rbac
francoisvandenplas Mar 3, 2025
2a9199b
Issues/300: @Data provides required constructor & setters
Mar 6, 2025
aa989b1
Merge branch 'main' into issues/300-regex-for-rbac
Haarolean Mar 8, 2025
d7ed00f
Merge branch 'main' into issues/300-regex-for-rbac
francoisvandenplas Mar 10, 2025
4eb3940
Merge branch 'main' into issues/300-regex-for-rbac
francoisvandenplas Mar 11, 2025
1f40b68
Merge branch 'main' into issues/300-regex-for-rbac
francoisvandenplas Mar 13, 2025
89a75e0
Merge branch 'main' into issues/300-regex-for-rbac
francoisvandenplas Mar 14, 2025
8f16981
Merge branch 'main' into issues/300-regex-for-rbac
Haarolean Mar 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ private Set<String> 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());

Expand All @@ -76,7 +76,7 @@ private Set<String> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ private Set<String> 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());

Expand Down Expand Up @@ -131,7 +131,7 @@ private Mono<Set<String>> 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()));
Expand Down Expand Up @@ -189,7 +189,7 @@ private Mono<Set<String>> getTeamRoles(WebClient webClient, Map<String, Object>
.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()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ private Set<String> 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());
}
Expand All @@ -68,7 +71,7 @@ private Set<String> 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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ private Set<String> 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());

Expand Down Expand Up @@ -94,11 +94,7 @@ private Set<String> 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());

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, Object> additionalParams = new HashMap<>();
OAuthProperties.OAuth2Provider provider = new OAuthProperties.OAuth2Provider();
provider.setCustomParams(Map.of("roles-field", "role_definition"));
additionalParams.put("provider", provider);

Set<String> 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<String, Object> additionalParams = new HashMap<>();

OAuthProperties.OAuth2Provider provider = new OAuthProperties.OAuth2Provider();
provider.setCustomParams(Map.of("roles-field", "role_definition"));
additionalParams.put("provider", provider);

Set<String> 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<String, Object> 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<String> 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<String, Object> additionalParams = new HashMap<>();

OAuthProperties.OAuth2Provider provider = new OAuthProperties.OAuth2Provider();
provider.setCustomParams(Map.of("roles-field", "role_definition"));
additionalParams.put("provider", provider);

Set<String> roles = extractor.extract(accessControlService, oauth2User, additionalParams).block();

assertEquals(Set.of("viewer", "admin"), roles);

}

}
49 changes: 49 additions & 0 deletions api/src/test/resources/roles_definition.yaml
Original file line number Diff line number Diff line change
@@ -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 ]
Loading