Skip to content

Commit 8c4d0ff

Browse files
committed
BE: RBAC: Impl default role
1 parent 371be00 commit 8c4d0ff

10 files changed

+123
-27
lines changed

api/src/main/java/io/kafbat/ui/config/auth/RoleBasedAccessControlProperties.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,34 @@
44
import jakarta.annotation.PostConstruct;
55
import java.util.ArrayList;
66
import java.util.List;
7+
import javax.annotation.Nullable;
78
import org.springframework.boot.context.properties.ConfigurationProperties;
89

910
@ConfigurationProperties("rbac")
1011
public class RoleBasedAccessControlProperties {
1112

1213
private final List<Role> roles = new ArrayList<>();
1314

15+
private Role defaultRole;
16+
1417
@PostConstruct
1518
public void init() {
1619
roles.forEach(Role::validate);
20+
if (defaultRole != null) {
21+
defaultRole.validateDefaultRole();
22+
}
1723
}
1824

1925
public List<Role> getRoles() {
2026
return roles;
2127
}
2228

29+
public void setDefaultRole(Role defaultRole) {
30+
this.defaultRole = defaultRole;
31+
}
32+
33+
@Nullable
34+
public Role getDefaultRole() {
35+
return defaultRole;
36+
}
2337
}

api/src/main/java/io/kafbat/ui/controller/AuthorizationController.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ public class AuthorizationController implements AuthorizationApi {
3131
private final AccessControlService accessControlService;
3232

3333
public Mono<ResponseEntity<AuthenticationInfoDTO>> getUserAuthInfo(ServerWebExchange exchange) {
34+
List<UserPermissionDTO> defaultRolePermissions = accessControlService.getDefaultRole() != null
35+
? mapPermissions(
36+
accessControlService.getDefaultRole().getPermissions(),
37+
accessControlService.getDefaultRole().getClusters())
38+
: Collections.emptyList();
39+
3440
Mono<List<UserPermissionDTO>> permissions = AccessControlService.getUser()
3541
.map(user -> accessControlService.getRoles()
3642
.stream()
@@ -39,12 +45,14 @@ public Mono<ResponseEntity<AuthenticationInfoDTO>> getUserAuthInfo(ServerWebExch
3945
.flatMap(Collection::stream)
4046
.toList()
4147
)
48+
.map(userPermissions -> userPermissions.isEmpty() ? defaultRolePermissions : userPermissions)
4249
.switchIfEmpty(Mono.just(Collections.emptyList()));
4350

4451
Mono<String> userName = ReactiveSecurityContextHolder.getContext()
4552
.map(SecurityContext::getAuthentication)
4653
.map(Principal::getName);
4754

55+
4856
var builder = AuthenticationInfoDTO.builder()
4957
.rbacEnabled(accessControlService.isRbacEnabled());
5058

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,8 @@ public void validate() {
2121
subjects.forEach(Subject::validate);
2222
}
2323

24+
public void validateDefaultRole() {
25+
permissions.forEach(Permission::validate);
26+
permissions.forEach(Permission::transform);
27+
}
2428
}

api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io.kafbat.ui.model.ClusterDTO;
77
import io.kafbat.ui.model.ConnectDTO;
88
import io.kafbat.ui.model.InternalTopic;
9+
import io.kafbat.ui.model.KafkaCluster;
910
import io.kafbat.ui.model.rbac.AccessContext;
1011
import io.kafbat.ui.model.rbac.Permission;
1112
import io.kafbat.ui.model.rbac.Role;
@@ -14,6 +15,7 @@
1415
import io.kafbat.ui.model.rbac.permission.ConsumerGroupAction;
1516
import io.kafbat.ui.model.rbac.permission.SchemaAction;
1617
import io.kafbat.ui.model.rbac.permission.TopicAction;
18+
import io.kafbat.ui.service.ClustersStorage;
1719
import io.kafbat.ui.service.rbac.extractor.CognitoAuthorityExtractor;
1820
import io.kafbat.ui.service.rbac.extractor.GithubAuthorityExtractor;
1921
import io.kafbat.ui.service.rbac.extractor.GoogleAuthorityExtractor;
@@ -53,6 +55,7 @@ public class AccessControlService {
5355
@Nullable
5456
private final InMemoryReactiveClientRegistrationRepository clientRegistrationRepository;
5557
private final RoleBasedAccessControlProperties properties;
58+
private final ClustersStorage clustersStorage;
5659
private final Environment environment;
5760

5861
@Getter
@@ -62,31 +65,51 @@ public class AccessControlService {
6265

6366
@PostConstruct
6467
public void init() {
65-
if (CollectionUtils.isEmpty(properties.getRoles())) {
68+
log.info("Initializing Access Control Service");
69+
log.info("defaultRole: {}", properties.getDefaultRole());
70+
log.info("roles: {}", properties.getRoles());
71+
if (CollectionUtils.isEmpty(properties.getRoles()) && properties.getDefaultRole() == null) {
6672
log.trace("No roles provided, disabling RBAC");
6773
return;
6874
}
75+
if (properties.getDefaultRole() != null) {
76+
properties.getDefaultRole().setClusters(
77+
clustersStorage.getKafkaClusters().stream()
78+
.map(KafkaCluster::getName)
79+
.collect(Collectors.toList())
80+
);
81+
}
82+
log.info("defaultRole: {}", properties.getDefaultRole());
6983
rbacEnabled = true;
7084

71-
this.oauthExtractors = properties.getRoles()
85+
if (properties.getDefaultRole() != null) {
86+
this.oauthExtractors = Set.of(
87+
new CognitoAuthorityExtractor(),
88+
new GoogleAuthorityExtractor(),
89+
new GithubAuthorityExtractor(),
90+
new OauthAuthorityExtractor()
91+
);
92+
} else {
93+
this.oauthExtractors = properties.getRoles()
7294
.stream()
7395
.map(role -> role.getSubjects()
74-
.stream()
75-
.map(Subject::getProvider)
76-
.distinct()
77-
.map(provider -> switch (provider) {
78-
case OAUTH_COGNITO -> new CognitoAuthorityExtractor();
79-
case OAUTH_GOOGLE -> new GoogleAuthorityExtractor();
80-
case OAUTH_GITHUB -> new GithubAuthorityExtractor();
81-
case OAUTH -> new OauthAuthorityExtractor();
82-
default -> null;
83-
})
84-
.filter(Objects::nonNull)
85-
.collect(Collectors.toSet()))
96+
.stream()
97+
.map(Subject::getProvider)
98+
.distinct()
99+
.map(provider -> switch (provider) {
100+
case OAUTH_COGNITO -> new CognitoAuthorityExtractor();
101+
case OAUTH_GOOGLE -> new GoogleAuthorityExtractor();
102+
case OAUTH_GITHUB -> new GithubAuthorityExtractor();
103+
case OAUTH -> new OauthAuthorityExtractor();
104+
default -> null;
105+
})
106+
.filter(Objects::nonNull)
107+
.collect(Collectors.toSet()))
86108
.flatMap(Set::stream)
87109
.collect(Collectors.toSet());
110+
}
88111

89-
if (!properties.getRoles().isEmpty()
112+
if (!(properties.getRoles().isEmpty() && properties.getDefaultRole() == null)
90113
&& "oauth2".equalsIgnoreCase(environment.getProperty("auth.type"))
91114
&& (clientRegistrationRepository == null || !clientRegistrationRepository.iterator().hasNext())) {
92115
log.error("Roles are configured but no authentication methods are present. Authentication might fail.");
@@ -114,12 +137,20 @@ private boolean isAccessible(AuthenticatedUser user, AccessContext context) {
114137
}
115138

116139
private List<Permission> getUserPermissions(AuthenticatedUser user, @Nullable String clusterName) {
117-
return properties.getRoles()
118-
.stream()
119-
.filter(filterRole(user))
120-
.filter(role -> clusterName == null || role.getClusters().stream().anyMatch(clusterName::equalsIgnoreCase))
121-
.flatMap(role -> role.getPermissions().stream())
122-
.toList();
140+
List<Role> filteredRoles = properties.getRoles()
141+
.stream()
142+
.filter(filterRole(user))
143+
.filter(role -> clusterName == null || role.getClusters().stream().anyMatch(clusterName::equalsIgnoreCase))
144+
.toList();
145+
146+
// if no roles are found, check if default role is set
147+
if (filteredRoles.isEmpty() && properties.getDefaultRole() != null) {
148+
return properties.getDefaultRole().getPermissions();
149+
}
150+
151+
return filteredRoles.stream()
152+
.flatMap(role -> role.getPermissions().stream())
153+
.toList();
123154
}
124155

125156
public static Mono<AuthenticatedUser> getUser() {
@@ -132,6 +163,9 @@ public static Mono<AuthenticatedUser> getUser() {
132163

133164
private boolean isClusterAccessible(String clusterName, AuthenticatedUser user) {
134165
Assert.isTrue(StringUtils.isNotEmpty(clusterName), "cluster value is empty");
166+
if (properties.getDefaultRole() != null) {
167+
return true;
168+
}
135169
return properties.getRoles()
136170
.stream()
137171
.filter(filterRole(user))
@@ -200,6 +234,10 @@ public List<Role> getRoles() {
200234
return Collections.unmodifiableList(properties.getRoles());
201235
}
202236

237+
public Role getDefaultRole() {
238+
return properties.getDefaultRole();
239+
}
240+
203241
private Predicate<Role> filterRole(AuthenticatedUser user) {
204242
return role -> user.groups().contains(role.getName());
205243
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,13 @@ public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<Str
3939

4040
var usernameRoles = extractUsernameRoles(acs, principal);
4141
var groupRoles = extractGroupRoles(acs, principal);
42+
var unionRoles = Sets.union(usernameRoles, groupRoles);
4243

43-
return Mono.just(Sets.union(usernameRoles, groupRoles));
44+
if (unionRoles.isEmpty() && acs.getDefaultRole() != null) {
45+
return Mono.just(Set.of(acs.getDefaultRole().getName()));
46+
}
47+
48+
return Mono.just(unionRoles);
4449
}
4550

4651
private Set<String> extractUsernameRoles(AccessControlService acs, DefaultOAuth2User principal) {

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,15 @@ public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<Str
7171
Mono<Set<String>> organizationRoles = getOrganizationRoles(principal, additionalParams, acs, webClient);
7272
Mono<Set<String>> teamRoles = getTeamRoles(webClient, additionalParams, acs);
7373

74+
Set<String> defaultRoles = acs.getDefaultRole() == null
75+
? Collections.emptySet()
76+
: Set.of(acs.getDefaultRole().getName());
77+
7478
return Mono.zip(organizationRoles, teamRoles)
7579
.map((t) -> Stream.of(t.getT1(), t.getT2(), usernameRoles)
7680
.flatMap(Collection::stream)
77-
.collect(Collectors.toSet()));
81+
.collect(Collectors.toSet()))
82+
.map(roles -> roles.isEmpty() ? defaultRoles : roles);
7883
}
7984

8085
private Set<String> extractUsernameRoles(DefaultOAuth2User principal, AccessControlService acs) {

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,13 @@ public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<Str
3939

4040
var usernameRoles = extractUsernameRoles(acs, principal);
4141
var domainRoles = extractDomainRoles(acs, principal);
42+
var unionRoles = Sets.union(usernameRoles, domainRoles);
4243

43-
return Mono.just(Sets.union(usernameRoles, domainRoles));
44+
if (unionRoles.isEmpty() && acs.getDefaultRole() != null) {
45+
return Mono.just(Set.of(acs.getDefaultRole().getName()));
46+
}
47+
48+
return Mono.just(unionRoles);
4449
}
4550

4651
private Set<String> extractUsernameRoles(AccessControlService acs, DefaultOAuth2User principal) {

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,13 @@ public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<Str
4343

4444
var usernameRoles = extractUsernameRoles(acs, principal);
4545
var roles = extractRoles(acs, principal, additionalParams);
46+
var unionRoles = Sets.union(usernameRoles, roles);
4647

47-
return Mono.just(Sets.union(usernameRoles, roles));
48+
if (unionRoles.isEmpty() && acs.getDefaultRole() != null) {
49+
return Mono.just(Set.of(acs.getDefaultRole().getName()));
50+
}
51+
52+
return Mono.just(unionRoles);
4853
}
4954

5055
private Set<String> extractUsernameRoles(AccessControlService acs, DefaultOAuth2User principal) {

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.kafbat.ui.model.rbac.provider.Provider;
55
import io.kafbat.ui.service.rbac.AccessControlService;
66
import java.util.Collection;
7+
import java.util.Set;
78
import java.util.stream.Collectors;
89
import lombok.extern.slf4j.Slf4j;
910
import org.springframework.context.ApplicationContext;
@@ -31,7 +32,7 @@ public Collection<? extends GrantedAuthority> getGrantedAuthorities(DirContextOp
3132
.peek(group -> log.trace("Found AD group [{}] for user [{}]", group, username))
3233
.collect(Collectors.toSet());
3334

34-
return acs.getRoles()
35+
var grantedAuthorities = acs.getRoles()
3536
.stream()
3637
.filter(r -> r.getSubjects()
3738
.stream()
@@ -46,5 +47,10 @@ public Collection<? extends GrantedAuthority> getGrantedAuthorities(DirContextOp
4647
.peek(role -> log.trace("Mapped role [{}] for user [{}]", role, username))
4748
.map(SimpleGrantedAuthority::new)
4849
.collect(Collectors.toSet());
50+
51+
if (grantedAuthorities.isEmpty() && acs.getDefaultRole() != null) {
52+
return Set.of(new SimpleGrantedAuthority(acs.getDefaultRole().getName()));
53+
}
54+
return grantedAuthorities;
4955
}
5056
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import io.kafbat.ui.model.rbac.Role;
44
import io.kafbat.ui.model.rbac.provider.Provider;
55
import io.kafbat.ui.service.rbac.AccessControlService;
6+
import java.util.HashSet;
67
import java.util.Set;
78
import java.util.stream.Collectors;
89
import lombok.extern.slf4j.Slf4j;
@@ -33,7 +34,7 @@ protected Set<GrantedAuthority> getAdditionalRoles(DirContextOperations user, St
3334
.peek(group -> log.trace("Found LDAP group [{}] for user [{}]", group, username))
3435
.collect(Collectors.toSet());
3536

36-
return acs.getRoles()
37+
var simpleGrantedAuthorities = acs.getRoles()
3738
.stream()
3839
.filter(r -> r.getSubjects()
3940
.stream()
@@ -48,5 +49,10 @@ protected Set<GrantedAuthority> getAdditionalRoles(DirContextOperations user, St
4849
.peek(role -> log.trace("Mapped role [{}] for user [{}]", role, username))
4950
.map(SimpleGrantedAuthority::new)
5051
.collect(Collectors.toSet());
52+
53+
if (simpleGrantedAuthorities.isEmpty() && acs.getDefaultRole() != null) {
54+
return Set.of(new SimpleGrantedAuthority(acs.getDefaultRole().getName()));
55+
}
56+
return new HashSet<>(simpleGrantedAuthorities);
5157
}
5258
}

0 commit comments

Comments
 (0)