Skip to content

Commit 3074abc

Browse files
francoisvandenplasFrançoisHaaroleancallaertanthony
authored
RBAC: Make it possible to use regex for values (#663)
Co-authored-by: François <[email protected]> Co-authored-by: Roman Zabaluev <[email protected]> Co-authored-by: Callaert Anthony <[email protected]>
1 parent e4b1c78 commit 3074abc

File tree

9 files changed

+293
-33
lines changed

9 files changed

+293
-33
lines changed

api/src/main/java/io/kafbat/ui/model/rbac/Subject.java

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,19 @@
33
import static com.google.common.base.Preconditions.checkArgument;
44
import static com.google.common.base.Preconditions.checkNotNull;
55

6+
import com.fasterxml.jackson.annotation.JsonProperty;
67
import io.kafbat.ui.model.rbac.provider.Provider;
7-
import lombok.Getter;
8+
import java.util.Objects;
9+
import lombok.Data;
810

9-
@Getter
11+
@Data
1012
public class Subject {
1113

1214
Provider provider;
1315
String type;
1416
String value;
15-
16-
public void setProvider(String provider) {
17-
this.provider = Provider.fromString(provider.toUpperCase());
18-
}
19-
20-
public void setType(String type) {
21-
this.type = type;
22-
}
23-
24-
public void setValue(String value) {
25-
this.value = value;
26-
}
17+
@JsonProperty("isRegex")
18+
boolean isRegex;
2719

2820
public void validate() {
2921
checkNotNull(type, "Subject type cannot be null");
@@ -32,4 +24,11 @@ public void validate() {
3224
checkArgument(!type.isEmpty(), "Subject type cannot be empty");
3325
checkArgument(!value.isEmpty(), "Subject value cannot be empty");
3426
}
27+
28+
public boolean matches(final String attribute) {
29+
if (isRegex) {
30+
return Objects.nonNull(attribute) && attribute.matches(this.value);
31+
}
32+
return this.value.equalsIgnoreCase(attribute);
33+
}
3534
}

api/src/main/java/io/kafbat/ui/service/rbac/extractor/CognitoAuthorityExtractor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ private Set<String> extractUsernameRoles(AccessControlService acs, DefaultOAuth2
5050
.stream()
5151
.filter(s -> s.getProvider().equals(Provider.OAUTH_COGNITO))
5252
.filter(s -> s.getType().equals("user"))
53-
.anyMatch(s -> s.getValue().equalsIgnoreCase(principal.getName())))
53+
.anyMatch(s -> s.matches(principal.getName())))
5454
.map(Role::getName)
5555
.collect(Collectors.toSet());
5656

@@ -76,7 +76,7 @@ private Set<String> extractGroupRoles(AccessControlService acs, DefaultOAuth2Use
7676
.filter(s -> s.getType().equals("group"))
7777
.anyMatch(subject -> groups
7878
.stream()
79-
.anyMatch(cognitoGroup -> cognitoGroup.equalsIgnoreCase(subject.getValue()))
79+
.anyMatch(subject::matches)
8080
))
8181
.map(Role::getName)
8282
.collect(Collectors.toSet());

api/src/main/java/io/kafbat/ui/service/rbac/extractor/GithubAuthorityExtractor.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ private Set<String> extractUsernameRoles(DefaultOAuth2User principal, AccessCont
9090
.stream()
9191
.filter(s -> s.getProvider().equals(Provider.OAUTH_GITHUB))
9292
.filter(s -> s.getType().equals("user"))
93-
.anyMatch(s -> s.getValue().equals(username)))
93+
.anyMatch(s -> s.matches(username)))
9494
.map(Role::getName)
9595
.collect(Collectors.toSet());
9696

@@ -131,7 +131,7 @@ private Mono<Set<String>> getOrganizationRoles(DefaultOAuth2User principal, Map<
131131
.filter(s -> s.getType().equals(ORGANIZATION))
132132
.anyMatch(subject -> orgsMap.stream()
133133
.map(org -> org.get(ORGANIZATION_NAME).toString())
134-
.anyMatch(orgName -> orgName.equalsIgnoreCase(subject.getValue()))
134+
.anyMatch(subject::matches)
135135
))
136136
.map(Role::getName)
137137
.collect(Collectors.toSet()));
@@ -189,7 +189,7 @@ private Mono<Set<String>> getTeamRoles(WebClient webClient, Map<String, Object>
189189
.filter(s -> s.getProvider().equals(Provider.OAUTH_GITHUB))
190190
.filter(s -> s.getType().equals("team"))
191191
.anyMatch(subject -> teams.stream()
192-
.anyMatch(teamName -> teamName.equalsIgnoreCase(subject.getValue()))
192+
.anyMatch(subject::matches)
193193
))
194194
.map(Role::getName)
195195
.collect(Collectors.toSet()));

api/src/main/java/io/kafbat/ui/service/rbac/extractor/GoogleAuthorityExtractor.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,10 @@ private Set<String> extractUsernameRoles(AccessControlService acs, DefaultOAuth2
5050
.stream()
5151
.filter(s -> s.getProvider().equals(Provider.OAUTH_GOOGLE))
5252
.filter(s -> s.getType().equals("user"))
53-
.anyMatch(s -> s.getValue().equalsIgnoreCase(principal.getAttribute(EMAIL_ATTRIBUTE_NAME))))
53+
.anyMatch(s -> {
54+
String email = principal.getAttribute(EMAIL_ATTRIBUTE_NAME);
55+
return s.matches(email);
56+
}))
5457
.map(Role::getName)
5558
.collect(Collectors.toSet());
5659
}
@@ -68,7 +71,7 @@ private Set<String> extractDomainRoles(AccessControlService acs, DefaultOAuth2Us
6871
.stream()
6972
.filter(s -> s.getProvider().equals(Provider.OAUTH_GOOGLE))
7073
.filter(s -> s.getType().equals("domain"))
71-
.anyMatch(s -> s.getValue().equals(domain)))
74+
.anyMatch(s -> s.matches(domain)))
7275
.map(Role::getName)
7376
.collect(Collectors.toSet());
7477
}

api/src/main/java/io/kafbat/ui/service/rbac/extractor/OauthAuthorityExtractor.java

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,8 @@ private Set<String> extractUsernameRoles(AccessControlService acs, DefaultOAuth2
5858
.stream()
5959
.filter(s -> s.getProvider().equals(Provider.OAUTH))
6060
.filter(s -> s.getType().equals("user"))
61-
.peek(s -> log.trace("[{}] matches [{}]? [{}]", s.getValue(), principalName,
62-
s.getValue().equalsIgnoreCase(principalName)))
63-
.anyMatch(s -> s.getValue().equalsIgnoreCase(principalName)))
61+
.peek(s -> log.trace("[{}] matches [{}]? [{}]", s.getValue(), principalName, s.matches(principalName)))
62+
.anyMatch(s -> s.matches(principalName)))
6463
.map(Role::getName)
6564
.collect(Collectors.toSet());
6665

@@ -94,11 +93,7 @@ private Set<String> extractRoles(AccessControlService acs, DefaultOAuth2User pri
9493
.stream()
9594
.filter(s -> s.getProvider().equals(Provider.OAUTH))
9695
.filter(s -> s.getType().equals("role"))
97-
.anyMatch(subject -> {
98-
var roleName = subject.getValue();
99-
return principalRoles.contains(roleName);
100-
})
101-
)
96+
.anyMatch(subject -> principalRoles.stream().anyMatch(subject::matches)))
10297
.map(Role::getName)
10398
.collect(Collectors.toSet());
10499

api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacActiveDirectoryAuthoritiesExtractor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ public Collection<? extends GrantedAuthority> getGrantedAuthorities(DirContextOp
3737
.stream()
3838
.filter(subject -> subject.getProvider().equals(Provider.LDAP_AD))
3939
.anyMatch(subject -> switch (subject.getType()) {
40-
case "user" -> username.equalsIgnoreCase(subject.getValue());
41-
case "group" -> adGroups.contains(subject.getValue());
40+
case "user" -> subject.matches(username);
41+
case "group" -> adGroups.stream().anyMatch(subject::matches);
4242
default -> false;
4343
})
4444
)

api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ protected Set<GrantedAuthority> getAdditionalRoles(DirContextOperations user, St
3939
.stream()
4040
.filter(subject -> subject.getProvider().equals(Provider.LDAP))
4141
.anyMatch(subject -> switch (subject.getType()) {
42-
case "user" -> username.equalsIgnoreCase(subject.getValue());
43-
case "group" -> ldapGroups.contains(subject.getValue());
42+
case "user" -> subject.matches(username);
43+
case "group" -> ldapGroups.stream().anyMatch(subject::matches);
4444
default -> false;
4545
})
4646
)
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package io.kafbat.ui.config;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertFalse;
5+
import static org.junit.jupiter.api.Assertions.assertNotNull;
6+
import static org.junit.jupiter.api.Assertions.assertTrue;
7+
import static org.mockito.Mockito.when;
8+
import static org.springframework.security.oauth2.client.registration.ClientRegistration.withRegistrationId;
9+
10+
import com.fasterxml.jackson.databind.ObjectMapper;
11+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
12+
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
13+
import io.kafbat.ui.config.auth.OAuthProperties;
14+
import io.kafbat.ui.model.rbac.Role;
15+
import io.kafbat.ui.service.rbac.AccessControlService;
16+
import io.kafbat.ui.service.rbac.extractor.CognitoAuthorityExtractor;
17+
import io.kafbat.ui.service.rbac.extractor.GithubAuthorityExtractor;
18+
import io.kafbat.ui.service.rbac.extractor.GoogleAuthorityExtractor;
19+
import io.kafbat.ui.service.rbac.extractor.OauthAuthorityExtractor;
20+
import io.kafbat.ui.service.rbac.extractor.ProviderAuthorityExtractor;
21+
import io.kafbat.ui.util.AccessControlServiceMock;
22+
import java.io.IOException;
23+
import java.io.InputStream;
24+
import java.time.Instant;
25+
import java.time.temporal.ChronoUnit;
26+
import java.util.HashMap;
27+
import java.util.List;
28+
import java.util.Map;
29+
import java.util.Set;
30+
import lombok.SneakyThrows;
31+
import org.junit.jupiter.api.BeforeEach;
32+
import org.junit.jupiter.api.Test;
33+
import org.springframework.security.core.authority.AuthorityUtils;
34+
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
35+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
36+
import org.springframework.security.oauth2.core.OAuth2AccessToken;
37+
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
38+
import org.springframework.security.oauth2.core.user.OAuth2User;
39+
40+
public class RegexBasedProviderAuthorityExtractorTest {
41+
42+
43+
private final AccessControlService accessControlService = new AccessControlServiceMock().getMock();
44+
ProviderAuthorityExtractor extractor;
45+
46+
@BeforeEach
47+
void setUp() throws IOException {
48+
49+
YAMLMapper mapper = new YAMLMapper();
50+
51+
InputStream rolesFile = this.getClass()
52+
.getClassLoader()
53+
.getResourceAsStream("roles_definition.yaml");
54+
55+
Role[] roles = mapper.readValue(rolesFile, Role[].class);
56+
57+
when(accessControlService.getRoles()).thenReturn(List.of(roles));
58+
59+
}
60+
61+
@SneakyThrows
62+
@Test
63+
void extractOauth2Authorities() {
64+
65+
extractor = new OauthAuthorityExtractor();
66+
67+
OAuth2User oauth2User = new DefaultOAuth2User(
68+
AuthorityUtils.createAuthorityList("SCOPE_message:read"),
69+
Map.of("role_definition", Set.of("ROLE-ADMIN", "ANOTHER-ROLE"), "user_name", "[email protected]"),
70+
"user_name");
71+
72+
HashMap<String, Object> additionalParams = new HashMap<>();
73+
OAuthProperties.OAuth2Provider provider = new OAuthProperties.OAuth2Provider();
74+
provider.setCustomParams(Map.of("roles-field", "role_definition"));
75+
additionalParams.put("provider", provider);
76+
77+
Set<String> roles = extractor.extract(accessControlService, oauth2User, additionalParams).block();
78+
79+
assertNotNull(roles);
80+
assertEquals(Set.of("viewer", "admin"), roles);
81+
assertFalse(roles.contains("no one's role"));
82+
83+
}
84+
85+
@SneakyThrows
86+
@Test()
87+
void extractOauth2Authorities_blankEmail() {
88+
89+
extractor = new OauthAuthorityExtractor();
90+
91+
OAuth2User oauth2User = new DefaultOAuth2User(
92+
AuthorityUtils.createAuthorityList("SCOPE_message:read"),
93+
Map.of("role_definition", Set.of("ROLE-ADMIN", "ANOTHER-ROLE"), "user_name", ""),
94+
"user_name");
95+
96+
HashMap<String, Object> additionalParams = new HashMap<>();
97+
OAuthProperties.OAuth2Provider provider = new OAuthProperties.OAuth2Provider();
98+
provider.setCustomParams(Map.of("roles-field", "role_definition"));
99+
additionalParams.put("provider", provider);
100+
101+
Set<String> roles = extractor.extract(accessControlService, oauth2User, additionalParams).block();
102+
103+
assertNotNull(roles);
104+
assertFalse(roles.contains("viewer"));
105+
assertTrue(roles.contains("admin"));
106+
107+
}
108+
109+
@SneakyThrows
110+
@Test
111+
void extractCognitoAuthorities() {
112+
113+
extractor = new CognitoAuthorityExtractor();
114+
115+
OAuth2User oauth2User = new DefaultOAuth2User(
116+
AuthorityUtils.createAuthorityList("SCOPE_message:read"),
117+
Map.of("cognito:groups", List.of("ROLE-ADMIN", "ANOTHER-ROLE"), "user_name", "[email protected]"),
118+
"user_name");
119+
120+
HashMap<String, Object> additionalParams = new HashMap<>();
121+
122+
OAuthProperties.OAuth2Provider provider = new OAuthProperties.OAuth2Provider();
123+
provider.setCustomParams(Map.of("roles-field", "role_definition"));
124+
additionalParams.put("provider", provider);
125+
126+
Set<String> roles = extractor.extract(accessControlService, oauth2User, additionalParams).block();
127+
128+
assertNotNull(roles);
129+
assertEquals(Set.of("viewer", "admin"), roles);
130+
assertFalse(roles.contains("no one's role"));
131+
132+
}
133+
134+
@SneakyThrows
135+
@Test
136+
void extractGithubAuthorities() {
137+
138+
extractor = new GithubAuthorityExtractor();
139+
140+
OAuth2User oauth2User = new DefaultOAuth2User(
141+
AuthorityUtils.createAuthorityList("SCOPE_message:read"),
142+
Map.of("login", "[email protected]"),
143+
"login");
144+
145+
HashMap<String, Object> additionalParams = new HashMap<>();
146+
147+
OAuthProperties.OAuth2Provider provider = new OAuthProperties.OAuth2Provider();
148+
additionalParams.put("provider", provider);
149+
150+
additionalParams.put("request", new OAuth2UserRequest(
151+
withRegistrationId("registration-1")
152+
.clientId("client-1")
153+
.clientSecret("secret")
154+
.redirectUri("https://client.com")
155+
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
156+
.authorizationUri("https://provider.com/oauth2/authorization")
157+
.tokenUri("https://provider.com/oauth2/token")
158+
.clientName("Client 1")
159+
.build(),
160+
new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, "XXXX", Instant.now(),
161+
Instant.now().plus(10, ChronoUnit.HOURS))));
162+
163+
Set<String> roles = extractor.extract(accessControlService, oauth2User, additionalParams).block();
164+
165+
assertNotNull(roles);
166+
assertEquals(Set.of("viewer"), roles);
167+
assertFalse(roles.contains("no one's role"));
168+
169+
}
170+
171+
@SneakyThrows
172+
@Test
173+
void extractGoogleAuthorities() {
174+
175+
extractor = new GoogleAuthorityExtractor();
176+
177+
OAuth2User oauth2User = new DefaultOAuth2User(
178+
AuthorityUtils.createAuthorityList("SCOPE_message:read"),
179+
Map.of("hd", "memelord.lol", "email", "[email protected]"),
180+
"email");
181+
182+
HashMap<String, Object> additionalParams = new HashMap<>();
183+
184+
OAuthProperties.OAuth2Provider provider = new OAuthProperties.OAuth2Provider();
185+
provider.setCustomParams(Map.of("roles-field", "role_definition"));
186+
additionalParams.put("provider", provider);
187+
188+
Set<String> roles = extractor.extract(accessControlService, oauth2User, additionalParams).block();
189+
190+
assertNotNull(roles);
191+
assertEquals(Set.of("viewer", "admin"), roles);
192+
assertFalse(roles.contains("no one's role"));
193+
194+
}
195+
196+
}

0 commit comments

Comments
 (0)