Skip to content

Commit 3ca417f

Browse files
authored
RBAC: Implement generic OAuth2 authority extractor (#3740)
1 parent 43ec02c commit 3ca417f

File tree

12 files changed

+138
-36
lines changed

12 files changed

+138
-36
lines changed

kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthProperties.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.provectus.kafka.ui.config.auth;
22

33
import jakarta.annotation.PostConstruct;
4+
import java.util.Collections;
45
import java.util.HashMap;
56
import java.util.Map;
67
import java.util.Set;
@@ -14,7 +15,16 @@ public class OAuthProperties {
1415
private Map<String, OAuth2Provider> client = new HashMap<>();
1516

1617
@PostConstruct
17-
public void validate() {
18+
public void init() {
19+
getClient().values().forEach((provider) -> {
20+
if (provider.getCustomParams() == null) {
21+
provider.setCustomParams(Collections.emptyMap());
22+
}
23+
if (provider.getScope() == null) {
24+
provider.setScope(Collections.emptySet());
25+
}
26+
});
27+
1828
getClient().values().forEach(this::validateProvider);
1929
}
2030

kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthPropertiesConverter.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,7 @@ private static void applyGoogleTransformations(OAuth2Provider provider) {
7373
}
7474

7575
private static boolean isGoogle(OAuth2Provider provider) {
76-
return provider.getCustomParams() != null
77-
&& GOOGLE.equalsIgnoreCase(provider.getCustomParams().get(TYPE));
76+
return GOOGLE.equalsIgnoreCase(provider.getCustomParams().get(TYPE));
7877
}
7978
}
8079

kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthSecurityConfig.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,13 @@ public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> customOidcUserServic
7272
final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService();
7373
return request -> delegate.loadUser(request)
7474
.flatMap(user -> {
75-
String providerId = request.getClientRegistration().getRegistrationId();
76-
final var extractor = getExtractor(providerId, acs);
75+
var provider = getProviderByProviderId(request.getClientRegistration().getRegistrationId());
76+
final var extractor = getExtractor(provider, acs);
7777
if (extractor == null) {
7878
return Mono.just(user);
7979
}
8080

81-
return extractor.extract(acs, user, Map.of("request", request))
81+
return extractor.extract(acs, user, Map.of("request", request, "provider", provider))
8282
.map(groups -> new RbacOidcUser(user, groups));
8383
});
8484
}
@@ -88,13 +88,13 @@ public ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> customOauth2User
8888
final DefaultReactiveOAuth2UserService delegate = new DefaultReactiveOAuth2UserService();
8989
return request -> delegate.loadUser(request)
9090
.flatMap(user -> {
91-
String providerId = request.getClientRegistration().getRegistrationId();
92-
final var extractor = getExtractor(providerId, acs);
91+
var provider = getProviderByProviderId(request.getClientRegistration().getRegistrationId());
92+
final var extractor = getExtractor(provider, acs);
9393
if (extractor == null) {
9494
return Mono.just(user);
9595
}
9696

97-
return extractor.extract(acs, user, Map.of("request", request))
97+
return extractor.extract(acs, user, Map.of("request", request, "provider", provider))
9898
.map(groups -> new RbacOAuth2User(user, groups));
9999
});
100100
}
@@ -113,18 +113,18 @@ public ServerLogoutSuccessHandler defaultOidcLogoutHandler(final ReactiveClientR
113113
}
114114

115115
@Nullable
116-
private ProviderAuthorityExtractor getExtractor(final String providerId, AccessControlService acs) {
117-
final String provider = getProviderByProviderId(providerId);
116+
private ProviderAuthorityExtractor getExtractor(final OAuthProperties.OAuth2Provider provider,
117+
AccessControlService acs) {
118118
Optional<ProviderAuthorityExtractor> extractor = acs.getOauthExtractors()
119119
.stream()
120-
.filter(e -> e.isApplicable(provider))
120+
.filter(e -> e.isApplicable(provider.getProvider(), provider.getCustomParams()))
121121
.findFirst();
122122

123123
return extractor.orElse(null);
124124
}
125125

126-
private String getProviderByProviderId(final String providerId) {
127-
return properties.getClient().get(providerId).getProvider();
126+
private OAuthProperties.OAuth2Provider getProviderByProviderId(final String providerId) {
127+
return properties.getClient().get(providerId);
128128
}
129129

130130
}

kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/CognitoLogoutSuccessHandler.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,8 @@ public Mono<Void> handle(WebFilterExchange exchange, Authentication authenticati
4646
.fragment(null)
4747
.build();
4848

49-
Assert.isTrue(
50-
provider.getCustomParams() != null && provider.getCustomParams().containsKey("logoutUrl"),
51-
"Custom params should contain 'logoutUrl'"
52-
);
49+
Assert.isTrue(provider.getCustomParams().containsKey("logoutUrl"),
50+
"Custom params should contain 'logoutUrl'");
5351
final var uri = UriComponentsBuilder
5452
.fromUri(URI.create(provider.getCustomParams().get("logoutUrl")))
5553
.queryParam("client_id", provider.getClientId())

kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/provider/Provider.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ public enum Provider {
1010

1111
OAUTH_COGNITO,
1212

13+
OAUTH,
14+
1315
LDAP,
1416
LDAP_AD;
1517

@@ -22,6 +24,8 @@ public static class Name {
2224
public static String GOOGLE = "google";
2325
public static String GITHUB = "github";
2426
public static String COGNITO = "cognito";
27+
28+
public static String OAUTH = "oauth";
2529
}
2630

2731
}

kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AccessControlService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.provectus.kafka.ui.service.rbac.extractor.CognitoAuthorityExtractor;
2121
import com.provectus.kafka.ui.service.rbac.extractor.GithubAuthorityExtractor;
2222
import com.provectus.kafka.ui.service.rbac.extractor.GoogleAuthorityExtractor;
23+
import com.provectus.kafka.ui.service.rbac.extractor.OauthAuthorityExtractor;
2324
import com.provectus.kafka.ui.service.rbac.extractor.ProviderAuthorityExtractor;
2425
import jakarta.annotation.PostConstruct;
2526
import java.util.Collections;
@@ -76,6 +77,7 @@ public void init() {
7677
case OAUTH_COGNITO -> new CognitoAuthorityExtractor();
7778
case OAUTH_GOOGLE -> new GoogleAuthorityExtractor();
7879
case OAUTH_GITHUB -> new GithubAuthorityExtractor();
80+
case OAUTH -> new OauthAuthorityExtractor();
7981
default -> null;
8082
})
8183
.filter(Objects::nonNull)

kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/CognitoAuthorityExtractor.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package com.provectus.kafka.ui.service.rbac.extractor;
22

3+
import static com.provectus.kafka.ui.model.rbac.provider.Provider.Name.COGNITO;
4+
5+
import com.google.common.collect.Sets;
36
import com.provectus.kafka.ui.model.rbac.Role;
47
import com.provectus.kafka.ui.model.rbac.provider.Provider;
58
import com.provectus.kafka.ui.service.rbac.AccessControlService;
@@ -18,8 +21,8 @@ public class CognitoAuthorityExtractor implements ProviderAuthorityExtractor {
1821
private static final String COGNITO_GROUPS_ATTRIBUTE_NAME = "cognito:groups";
1922

2023
@Override
21-
public boolean isApplicable(String provider) {
22-
return Provider.Name.COGNITO.equalsIgnoreCase(provider);
24+
public boolean isApplicable(String provider, Map<String, String> customParams) {
25+
return COGNITO.equalsIgnoreCase(provider) || COGNITO.equalsIgnoreCase(customParams.get(TYPE));
2326
}
2427

2528
@Override
@@ -63,7 +66,7 @@ public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<Str
6366
.map(Role::getName)
6467
.collect(Collectors.toSet());
6568

66-
return Mono.just(Stream.concat(groupsByUsername.stream(), groupsByGroups.stream()).collect(Collectors.toSet()));
69+
return Mono.just(Sets.union(groupsByUsername, groupsByGroups));
6770
}
6871

6972
}

kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/GithubAuthorityExtractor.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.provectus.kafka.ui.service.rbac.extractor;
22

3+
import static com.provectus.kafka.ui.model.rbac.provider.Provider.Name.GITHUB;
4+
35
import com.provectus.kafka.ui.model.rbac.Role;
46
import com.provectus.kafka.ui.model.rbac.provider.Provider;
57
import com.provectus.kafka.ui.service.rbac.AccessControlService;
@@ -28,8 +30,8 @@ public class GithubAuthorityExtractor implements ProviderAuthorityExtractor {
2830
private static final String DUMMY = "dummy";
2931

3032
@Override
31-
public boolean isApplicable(String provider) {
32-
return Provider.Name.GITHUB.equalsIgnoreCase(provider);
33+
public boolean isApplicable(String provider, Map<String, String> customParams) {
34+
return GITHUB.equalsIgnoreCase(provider) || GITHUB.equalsIgnoreCase(customParams.get(TYPE));
3335
}
3436

3537
@Override

kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/GoogleAuthorityExtractor.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package com.provectus.kafka.ui.service.rbac.extractor;
22

3+
import static com.provectus.kafka.ui.model.rbac.provider.Provider.Name.GOOGLE;
4+
5+
import com.google.common.collect.Sets;
36
import com.provectus.kafka.ui.model.rbac.Role;
47
import com.provectus.kafka.ui.model.rbac.provider.Provider;
58
import com.provectus.kafka.ui.service.rbac.AccessControlService;
6-
import java.util.List;
79
import java.util.Map;
810
import java.util.Set;
911
import java.util.stream.Collectors;
10-
import java.util.stream.Stream;
1112
import lombok.extern.slf4j.Slf4j;
1213
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
1314
import reactor.core.publisher.Mono;
@@ -19,8 +20,8 @@ public class GoogleAuthorityExtractor implements ProviderAuthorityExtractor {
1920
public static final String EMAIL_ATTRIBUTE_NAME = "email";
2021

2122
@Override
22-
public boolean isApplicable(String provider) {
23-
return Provider.Name.GOOGLE.equalsIgnoreCase(provider);
23+
public boolean isApplicable(String provider, Map<String, String> customParams) {
24+
return GOOGLE.equalsIgnoreCase(provider) || GOOGLE.equalsIgnoreCase(customParams.get(TYPE));
2425
}
2526

2627
@Override
@@ -52,18 +53,17 @@ public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<Str
5253
return Mono.just(groupsByUsername);
5354
}
5455

55-
List<String> groupsByDomain = acs.getRoles()
56+
Set<String> groupsByDomain = acs.getRoles()
5657
.stream()
5758
.filter(r -> r.getSubjects()
5859
.stream()
5960
.filter(s -> s.getProvider().equals(Provider.OAUTH_GOOGLE))
6061
.filter(s -> s.getType().equals("domain"))
6162
.anyMatch(s -> s.getValue().equals(domain)))
6263
.map(Role::getName)
63-
.toList();
64+
.collect(Collectors.toSet());
6465

65-
return Mono.just(Stream.concat(groupsByUsername.stream(), groupsByDomain.stream())
66-
.collect(Collectors.toSet()));
66+
return Mono.just(Sets.union(groupsByUsername, groupsByDomain));
6767
}
6868

6969
}
Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,44 @@
11
package com.provectus.kafka.ui.service.rbac.extractor;
22

3+
import static com.provectus.kafka.ui.model.rbac.provider.Provider.Name.OAUTH;
4+
5+
import com.google.common.collect.Sets;
6+
import com.provectus.kafka.ui.config.auth.OAuthProperties;
7+
import com.provectus.kafka.ui.model.rbac.Role;
8+
import com.provectus.kafka.ui.model.rbac.provider.Provider;
39
import com.provectus.kafka.ui.service.rbac.AccessControlService;
10+
import java.util.Arrays;
11+
import java.util.Collection;
12+
import java.util.Collections;
13+
import java.util.List;
414
import java.util.Map;
515
import java.util.Set;
16+
import java.util.stream.Collectors;
617
import lombok.extern.slf4j.Slf4j;
718
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
19+
import org.springframework.util.Assert;
820
import reactor.core.publisher.Mono;
921

1022
@Slf4j
1123
public class OauthAuthorityExtractor implements ProviderAuthorityExtractor {
1224

25+
public static final String ROLES_FIELD_PARAM_NAME = "roles-field";
26+
1327
@Override
14-
public boolean isApplicable(String provider) {
15-
return false; // TODO #2844
28+
public boolean isApplicable(String provider, Map<String, String> customParams) {
29+
var containsRolesFieldNameParam = customParams.containsKey(ROLES_FIELD_PARAM_NAME);
30+
if (!containsRolesFieldNameParam) {
31+
log.debug("Provider [{}] doesn't contain a roles field param name, mapping won't be performed", provider);
32+
return false;
33+
}
34+
35+
return OAUTH.equalsIgnoreCase(provider) || OAUTH.equalsIgnoreCase(customParams.get(TYPE));
1636
}
1737

1838
@Override
1939
public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<String, Object> additionalParams) {
40+
log.trace("Extracting OAuth2 user authorities");
41+
2042
DefaultOAuth2User principal;
2143
try {
2244
principal = (DefaultOAuth2User) value;
@@ -25,7 +47,67 @@ public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<Str
2547
throw new RuntimeException();
2648
}
2749

28-
return Mono.just(Set.of(principal.getName())); // TODO #2844
50+
var provider = (OAuthProperties.OAuth2Provider) additionalParams.get("provider");
51+
Assert.notNull(provider, "provider is null");
52+
var rolesFieldName = provider.getCustomParams().get(ROLES_FIELD_PARAM_NAME);
53+
54+
Set<String> rolesByUsername = acs.getRoles()
55+
.stream()
56+
.filter(r -> r.getSubjects()
57+
.stream()
58+
.filter(s -> s.getProvider().equals(Provider.OAUTH))
59+
.filter(s -> s.getType().equals("user"))
60+
.anyMatch(s -> s.getValue().equals(principal.getName())))
61+
.map(Role::getName)
62+
.collect(Collectors.toSet());
63+
64+
Set<String> rolesByRolesField = acs.getRoles()
65+
.stream()
66+
.filter(role -> role.getSubjects()
67+
.stream()
68+
.filter(s -> s.getProvider().equals(Provider.OAUTH))
69+
.filter(s -> s.getType().equals("role"))
70+
.anyMatch(subject -> {
71+
var roleName = subject.getValue();
72+
var principalRoles = convertRoles(principal.getAttribute(rolesFieldName));
73+
var roleMatched = principalRoles.contains(roleName);
74+
75+
if (roleMatched) {
76+
log.debug("Assigning role [{}] to user [{}]", roleName, principal.getName());
77+
} else {
78+
log.trace("Role [{}] not found in user [{}] roles", roleName, principal.getName());
79+
}
80+
81+
return roleMatched;
82+
})
83+
)
84+
.map(Role::getName)
85+
.collect(Collectors.toSet());
86+
87+
return Mono.just(Sets.union(rolesByUsername, rolesByRolesField));
88+
}
89+
90+
@SuppressWarnings("unchecked")
91+
private Collection<String> convertRoles(Object roles) {
92+
if (roles == null) {
93+
log.debug("Param missing from attributes, skipping");
94+
return Collections.emptySet();
95+
}
96+
97+
if ((roles instanceof List<?>) || (roles instanceof Set<?>)) {
98+
log.trace("The field is either a set or a list, returning as is");
99+
return (Collection<String>) roles;
100+
}
101+
102+
if (!(roles instanceof String)) {
103+
log.debug("The field is not a string, skipping");
104+
return Collections.emptySet();
105+
}
106+
107+
log.trace("Trying to deserialize the field value [{}] as a string", roles);
108+
109+
return Arrays.stream(((String) roles).split(","))
110+
.collect(Collectors.toSet());
29111
}
30112

31113
}

0 commit comments

Comments
 (0)