From 8c4d0ff8e2d33b4b34b707ee49c6ddc919417a1d Mon Sep 17 00:00:00 2001 From: seono Date: Sun, 4 May 2025 23:56:21 +0900 Subject: [PATCH 01/10] BE: RBAC: Impl default role --- .../RoleBasedAccessControlProperties.java | 14 ++++ .../controller/AuthorizationController.java | 8 ++ .../java/io/kafbat/ui/model/rbac/Role.java | 4 + .../ui/service/rbac/AccessControlService.java | 80 ++++++++++++++----- .../extractor/CognitoAuthorityExtractor.java | 7 +- .../extractor/GithubAuthorityExtractor.java | 7 +- .../extractor/GoogleAuthorityExtractor.java | 7 +- .../extractor/OauthAuthorityExtractor.java | 7 +- ...acActiveDirectoryAuthoritiesExtractor.java | 8 +- .../RbacLdapAuthoritiesExtractor.java | 8 +- 10 files changed, 123 insertions(+), 27 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/config/auth/RoleBasedAccessControlProperties.java b/api/src/main/java/io/kafbat/ui/config/auth/RoleBasedAccessControlProperties.java index 8ecf12b99..abe62a0f7 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/RoleBasedAccessControlProperties.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/RoleBasedAccessControlProperties.java @@ -4,6 +4,7 @@ import jakarta.annotation.PostConstruct; import java.util.ArrayList; import java.util.List; +import javax.annotation.Nullable; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties("rbac") @@ -11,13 +12,26 @@ public class RoleBasedAccessControlProperties { private final List roles = new ArrayList<>(); + private Role defaultRole; + @PostConstruct public void init() { roles.forEach(Role::validate); + if (defaultRole != null) { + defaultRole.validateDefaultRole(); + } } public List getRoles() { return roles; } + public void setDefaultRole(Role defaultRole) { + this.defaultRole = defaultRole; + } + + @Nullable + public Role getDefaultRole() { + return defaultRole; + } } diff --git a/api/src/main/java/io/kafbat/ui/controller/AuthorizationController.java b/api/src/main/java/io/kafbat/ui/controller/AuthorizationController.java index 1ac0aeb85..690abc25e 100644 --- a/api/src/main/java/io/kafbat/ui/controller/AuthorizationController.java +++ b/api/src/main/java/io/kafbat/ui/controller/AuthorizationController.java @@ -31,6 +31,12 @@ public class AuthorizationController implements AuthorizationApi { private final AccessControlService accessControlService; public Mono> getUserAuthInfo(ServerWebExchange exchange) { + List defaultRolePermissions = accessControlService.getDefaultRole() != null + ? mapPermissions( + accessControlService.getDefaultRole().getPermissions(), + accessControlService.getDefaultRole().getClusters()) + : Collections.emptyList(); + Mono> permissions = AccessControlService.getUser() .map(user -> accessControlService.getRoles() .stream() @@ -39,12 +45,14 @@ public Mono> getUserAuthInfo(ServerWebExch .flatMap(Collection::stream) .toList() ) + .map(userPermissions -> userPermissions.isEmpty() ? defaultRolePermissions : userPermissions) .switchIfEmpty(Mono.just(Collections.emptyList())); Mono userName = ReactiveSecurityContextHolder.getContext() .map(SecurityContext::getAuthentication) .map(Principal::getName); + var builder = AuthenticationInfoDTO.builder() .rbacEnabled(accessControlService.isRbacEnabled()); diff --git a/api/src/main/java/io/kafbat/ui/model/rbac/Role.java b/api/src/main/java/io/kafbat/ui/model/rbac/Role.java index 20eec030d..0d07f4670 100644 --- a/api/src/main/java/io/kafbat/ui/model/rbac/Role.java +++ b/api/src/main/java/io/kafbat/ui/model/rbac/Role.java @@ -21,4 +21,8 @@ public void validate() { subjects.forEach(Subject::validate); } + public void validateDefaultRole() { + permissions.forEach(Permission::validate); + permissions.forEach(Permission::transform); + } } diff --git a/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java b/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java index 896bbf875..d728d81f2 100644 --- a/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java +++ b/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java @@ -6,6 +6,7 @@ import io.kafbat.ui.model.ClusterDTO; import io.kafbat.ui.model.ConnectDTO; import io.kafbat.ui.model.InternalTopic; +import io.kafbat.ui.model.KafkaCluster; import io.kafbat.ui.model.rbac.AccessContext; import io.kafbat.ui.model.rbac.Permission; import io.kafbat.ui.model.rbac.Role; @@ -14,6 +15,7 @@ import io.kafbat.ui.model.rbac.permission.ConsumerGroupAction; import io.kafbat.ui.model.rbac.permission.SchemaAction; import io.kafbat.ui.model.rbac.permission.TopicAction; +import io.kafbat.ui.service.ClustersStorage; import io.kafbat.ui.service.rbac.extractor.CognitoAuthorityExtractor; import io.kafbat.ui.service.rbac.extractor.GithubAuthorityExtractor; import io.kafbat.ui.service.rbac.extractor.GoogleAuthorityExtractor; @@ -53,6 +55,7 @@ public class AccessControlService { @Nullable private final InMemoryReactiveClientRegistrationRepository clientRegistrationRepository; private final RoleBasedAccessControlProperties properties; + private final ClustersStorage clustersStorage; private final Environment environment; @Getter @@ -62,31 +65,51 @@ public class AccessControlService { @PostConstruct public void init() { - if (CollectionUtils.isEmpty(properties.getRoles())) { + log.info("Initializing Access Control Service"); + log.info("defaultRole: {}", properties.getDefaultRole()); + log.info("roles: {}", properties.getRoles()); + if (CollectionUtils.isEmpty(properties.getRoles()) && properties.getDefaultRole() == null) { log.trace("No roles provided, disabling RBAC"); return; } + if (properties.getDefaultRole() != null) { + properties.getDefaultRole().setClusters( + clustersStorage.getKafkaClusters().stream() + .map(KafkaCluster::getName) + .collect(Collectors.toList()) + ); + } + log.info("defaultRole: {}", properties.getDefaultRole()); rbacEnabled = true; - this.oauthExtractors = properties.getRoles() + if (properties.getDefaultRole() != null) { + this.oauthExtractors = Set.of( + new CognitoAuthorityExtractor(), + new GoogleAuthorityExtractor(), + new GithubAuthorityExtractor(), + new OauthAuthorityExtractor() + ); + } else { + this.oauthExtractors = properties.getRoles() .stream() .map(role -> role.getSubjects() - .stream() - .map(Subject::getProvider) - .distinct() - .map(provider -> switch (provider) { - case OAUTH_COGNITO -> new CognitoAuthorityExtractor(); - case OAUTH_GOOGLE -> new GoogleAuthorityExtractor(); - case OAUTH_GITHUB -> new GithubAuthorityExtractor(); - case OAUTH -> new OauthAuthorityExtractor(); - default -> null; - }) - .filter(Objects::nonNull) - .collect(Collectors.toSet())) + .stream() + .map(Subject::getProvider) + .distinct() + .map(provider -> switch (provider) { + case OAUTH_COGNITO -> new CognitoAuthorityExtractor(); + case OAUTH_GOOGLE -> new GoogleAuthorityExtractor(); + case OAUTH_GITHUB -> new GithubAuthorityExtractor(); + case OAUTH -> new OauthAuthorityExtractor(); + default -> null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toSet())) .flatMap(Set::stream) .collect(Collectors.toSet()); + } - if (!properties.getRoles().isEmpty() + if (!(properties.getRoles().isEmpty() && properties.getDefaultRole() == null) && "oauth2".equalsIgnoreCase(environment.getProperty("auth.type")) && (clientRegistrationRepository == null || !clientRegistrationRepository.iterator().hasNext())) { 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) { } private List getUserPermissions(AuthenticatedUser user, @Nullable String clusterName) { - return properties.getRoles() - .stream() - .filter(filterRole(user)) - .filter(role -> clusterName == null || role.getClusters().stream().anyMatch(clusterName::equalsIgnoreCase)) - .flatMap(role -> role.getPermissions().stream()) - .toList(); + List filteredRoles = properties.getRoles() + .stream() + .filter(filterRole(user)) + .filter(role -> clusterName == null || role.getClusters().stream().anyMatch(clusterName::equalsIgnoreCase)) + .toList(); + + // if no roles are found, check if default role is set + if (filteredRoles.isEmpty() && properties.getDefaultRole() != null) { + return properties.getDefaultRole().getPermissions(); + } + + return filteredRoles.stream() + .flatMap(role -> role.getPermissions().stream()) + .toList(); } public static Mono getUser() { @@ -132,6 +163,9 @@ public static Mono getUser() { private boolean isClusterAccessible(String clusterName, AuthenticatedUser user) { Assert.isTrue(StringUtils.isNotEmpty(clusterName), "cluster value is empty"); + if (properties.getDefaultRole() != null) { + return true; + } return properties.getRoles() .stream() .filter(filterRole(user)) @@ -200,6 +234,10 @@ public List getRoles() { return Collections.unmodifiableList(properties.getRoles()); } + public Role getDefaultRole() { + return properties.getDefaultRole(); + } + private Predicate filterRole(AuthenticatedUser user) { return role -> user.groups().contains(role.getName()); } 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 cc0e419bf..9cdf5ea62 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 @@ -39,8 +39,13 @@ public Mono> extract(AccessControlService acs, Object value, Map extractUsernameRoles(AccessControlService acs, DefaultOAuth2User principal) { 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 79f4907fc..63182b999 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 @@ -71,10 +71,15 @@ public Mono> extract(AccessControlService acs, Object value, Map> organizationRoles = getOrganizationRoles(principal, additionalParams, acs, webClient); Mono> teamRoles = getTeamRoles(webClient, additionalParams, acs); + Set defaultRoles = acs.getDefaultRole() == null + ? Collections.emptySet() + : Set.of(acs.getDefaultRole().getName()); + return Mono.zip(organizationRoles, teamRoles) .map((t) -> Stream.of(t.getT1(), t.getT2(), usernameRoles) .flatMap(Collection::stream) - .collect(Collectors.toSet())); + .collect(Collectors.toSet())) + .map(roles -> roles.isEmpty() ? defaultRoles : roles); } private Set extractUsernameRoles(DefaultOAuth2User principal, AccessControlService acs) { 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 a90ab50ef..a0fd07d32 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 @@ -39,8 +39,13 @@ public Mono> extract(AccessControlService acs, Object value, Map extractUsernameRoles(AccessControlService acs, DefaultOAuth2User principal) { 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 61748610e..2f827c6ae 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 @@ -43,8 +43,13 @@ public Mono> extract(AccessControlService acs, Object value, Map extractUsernameRoles(AccessControlService acs, DefaultOAuth2User principal) { diff --git a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacActiveDirectoryAuthoritiesExtractor.java b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacActiveDirectoryAuthoritiesExtractor.java index 76feff063..9ecb86785 100644 --- a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacActiveDirectoryAuthoritiesExtractor.java +++ b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacActiveDirectoryAuthoritiesExtractor.java @@ -4,6 +4,7 @@ import io.kafbat.ui.model.rbac.provider.Provider; import io.kafbat.ui.service.rbac.AccessControlService; import java.util.Collection; +import java.util.Set; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationContext; @@ -31,7 +32,7 @@ public Collection getGrantedAuthorities(DirContextOp .peek(group -> log.trace("Found AD group [{}] for user [{}]", group, username)) .collect(Collectors.toSet()); - return acs.getRoles() + var grantedAuthorities = acs.getRoles() .stream() .filter(r -> r.getSubjects() .stream() @@ -46,5 +47,10 @@ public Collection getGrantedAuthorities(DirContextOp .peek(role -> log.trace("Mapped role [{}] for user [{}]", role, username)) .map(SimpleGrantedAuthority::new) .collect(Collectors.toSet()); + + if (grantedAuthorities.isEmpty() && acs.getDefaultRole() != null) { + return Set.of(new SimpleGrantedAuthority(acs.getDefaultRole().getName())); + } + return grantedAuthorities; } } diff --git a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java index 78ec4ba19..3598401c2 100644 --- a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java +++ b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java @@ -3,6 +3,7 @@ import io.kafbat.ui.model.rbac.Role; import io.kafbat.ui.model.rbac.provider.Provider; import io.kafbat.ui.service.rbac.AccessControlService; +import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @@ -33,7 +34,7 @@ protected Set getAdditionalRoles(DirContextOperations user, St .peek(group -> log.trace("Found LDAP group [{}] for user [{}]", group, username)) .collect(Collectors.toSet()); - return acs.getRoles() + var simpleGrantedAuthorities = acs.getRoles() .stream() .filter(r -> r.getSubjects() .stream() @@ -48,5 +49,10 @@ protected Set getAdditionalRoles(DirContextOperations user, St .peek(role -> log.trace("Mapped role [{}] for user [{}]", role, username)) .map(SimpleGrantedAuthority::new) .collect(Collectors.toSet()); + + if (simpleGrantedAuthorities.isEmpty() && acs.getDefaultRole() != null) { + return Set.of(new SimpleGrantedAuthority(acs.getDefaultRole().getName())); + } + return new HashSet<>(simpleGrantedAuthorities); } } From 7ddaeb8bd5a75d2a317b109aa94eb480111cabd4 Mon Sep 17 00:00:00 2001 From: seono Date: Mon, 5 May 2025 00:08:58 +0900 Subject: [PATCH 02/10] BE: RBAC: Impl default role --- .../java/io/kafbat/ui/service/rbac/AccessControlService.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java b/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java index d728d81f2..867403a8d 100644 --- a/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java +++ b/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java @@ -65,21 +65,18 @@ public class AccessControlService { @PostConstruct public void init() { - log.info("Initializing Access Control Service"); - log.info("defaultRole: {}", properties.getDefaultRole()); - log.info("roles: {}", properties.getRoles()); if (CollectionUtils.isEmpty(properties.getRoles()) && properties.getDefaultRole() == null) { log.trace("No roles provided, disabling RBAC"); return; } if (properties.getDefaultRole() != null) { + log.trace("Set Default Role Clusters"); properties.getDefaultRole().setClusters( clustersStorage.getKafkaClusters().stream() .map(KafkaCluster::getName) .collect(Collectors.toList()) ); } - log.info("defaultRole: {}", properties.getDefaultRole()); rbacEnabled = true; if (properties.getDefaultRole() != null) { From dad4a37f5d36c20e73e23bed69d8124201050a4d Mon Sep 17 00:00:00 2001 From: seono Date: Mon, 26 May 2025 09:45:04 +0900 Subject: [PATCH 03/10] BE: RBAC: Impl default role - add test code and comment --- .../controller/AuthorizationController.java | 1 + .../java/io/kafbat/ui/model/rbac/Role.java | 1 + .../ui/service/rbac/AccessControlService.java | 7 +- ...acActiveDirectoryAuthoritiesExtractor.java | 1 + .../RbacLdapAuthoritiesExtractor.java | 1 + ...trolServiceDefaultRoleRbacEnabledTest.java | 149 ++++++++++++++++++ .../ui/service/rbac/MockedRbacUtils.java | 41 ++++- 7 files changed, 193 insertions(+), 8 deletions(-) create mode 100644 api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java diff --git a/api/src/main/java/io/kafbat/ui/controller/AuthorizationController.java b/api/src/main/java/io/kafbat/ui/controller/AuthorizationController.java index 690abc25e..b5d940ae9 100644 --- a/api/src/main/java/io/kafbat/ui/controller/AuthorizationController.java +++ b/api/src/main/java/io/kafbat/ui/controller/AuthorizationController.java @@ -45,6 +45,7 @@ public Mono> getUserAuthInfo(ServerWebExch .flatMap(Collection::stream) .toList() ) + // if no roles are found, return default role permissions .map(userPermissions -> userPermissions.isEmpty() ? defaultRolePermissions : userPermissions) .switchIfEmpty(Mono.just(Collections.emptyList())); diff --git a/api/src/main/java/io/kafbat/ui/model/rbac/Role.java b/api/src/main/java/io/kafbat/ui/model/rbac/Role.java index 0d07f4670..242c1e37f 100644 --- a/api/src/main/java/io/kafbat/ui/model/rbac/Role.java +++ b/api/src/main/java/io/kafbat/ui/model/rbac/Role.java @@ -21,6 +21,7 @@ public void validate() { subjects.forEach(Subject::validate); } + // default role need only permissions public void validateDefaultRole() { permissions.forEach(Permission::validate); permissions.forEach(Permission::transform); diff --git a/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java b/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java index d728d81f2..2153c1d50 100644 --- a/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java +++ b/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java @@ -65,24 +65,23 @@ public class AccessControlService { @PostConstruct public void init() { - log.info("Initializing Access Control Service"); - log.info("defaultRole: {}", properties.getDefaultRole()); - log.info("roles: {}", properties.getRoles()); if (CollectionUtils.isEmpty(properties.getRoles()) && properties.getDefaultRole() == null) { log.trace("No roles provided, disabling RBAC"); return; } if (properties.getDefaultRole() != null) { + log.trace("Set Default Role Clusters"); + // set default role for all clusters properties.getDefaultRole().setClusters( clustersStorage.getKafkaClusters().stream() .map(KafkaCluster::getName) .collect(Collectors.toList()) ); } - log.info("defaultRole: {}", properties.getDefaultRole()); rbacEnabled = true; if (properties.getDefaultRole() != null) { + // set all extractors for default role because it is applied to all clusters this.oauthExtractors = Set.of( new CognitoAuthorityExtractor(), new GoogleAuthorityExtractor(), diff --git a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacActiveDirectoryAuthoritiesExtractor.java b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacActiveDirectoryAuthoritiesExtractor.java index 9ecb86785..8c950a33e 100644 --- a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacActiveDirectoryAuthoritiesExtractor.java +++ b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacActiveDirectoryAuthoritiesExtractor.java @@ -48,6 +48,7 @@ public Collection getGrantedAuthorities(DirContextOp .map(SimpleGrantedAuthority::new) .collect(Collectors.toSet()); + // If no roles are found, return default role if (grantedAuthorities.isEmpty() && acs.getDefaultRole() != null) { return Set.of(new SimpleGrantedAuthority(acs.getDefaultRole().getName())); } diff --git a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java index 3598401c2..b37d93854 100644 --- a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java +++ b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java @@ -50,6 +50,7 @@ protected Set getAdditionalRoles(DirContextOperations user, St .map(SimpleGrantedAuthority::new) .collect(Collectors.toSet()); + // If no roles are found, return default role if (simpleGrantedAuthorities.isEmpty() && acs.getDefaultRole() != null) { return Set.of(new SimpleGrantedAuthority(acs.getDefaultRole().getName())); } diff --git a/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java b/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java new file mode 100644 index 000000000..ddf966b9c --- /dev/null +++ b/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java @@ -0,0 +1,149 @@ +package io.kafbat.ui.service.rbac; + + +import static io.kafbat.ui.service.rbac.MockedRbacUtils.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.kafbat.ui.AbstractIntegrationTest; +import io.kafbat.ui.config.auth.RbacUser; +import io.kafbat.ui.config.auth.RoleBasedAccessControlProperties; +import io.kafbat.ui.model.ClusterDTO; +import io.kafbat.ui.model.KafkaCluster; +import io.kafbat.ui.model.rbac.AccessContext; +import io.kafbat.ui.model.rbac.Role; +import io.kafbat.ui.service.ClustersStorage; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.util.ReflectionTestUtils; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + + +/** + * Test class for AccessControlService with default role and RBAC enabled. + */ +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +public class AccessControlServiceDefaultRoleRbacEnabledTest extends AbstractIntegrationTest { + + @Autowired + AccessControlService accessControlService; + + @Mock + SecurityContext securityContext; + + @Mock + Authentication authentication; + + @Mock + RbacUser user; + + @Mock + Role defaultRole; + + @Mock + ClustersStorage clustersStorage; + + @BeforeEach + void setUp() { + + RoleBasedAccessControlProperties properties = mock(); + defaultRole = MockedRbacUtils.getDefaultRole(); + when(properties.getDefaultRole()).thenReturn(defaultRole); + + + ReflectionTestUtils.setField(accessControlService, "properties", properties); + ReflectionTestUtils.setField(accessControlService, "rbacEnabled", true); + ReflectionTestUtils.setField(accessControlService, "clustersStorage", clustersStorage); + + KafkaCluster prodCluster = KafkaCluster.builder().name(PROD_CLUSTER).build(); + KafkaCluster devCluster = KafkaCluster.builder().name(DEV_CLUSTER).build(); + + // set default role for all clusters + when(clustersStorage.getKafkaClusters()).thenReturn(List.of(prodCluster, devCluster)); + accessControlService.init(); + + // Mock security context + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.getPrincipal()).thenReturn(user); + } + + public void withSecurityContext(Runnable runnable) { + try (MockedStatic ctxHolder = Mockito.mockStatic( + ReactiveSecurityContextHolder.class)) { + // Mock static method to get security context + ctxHolder.when(ReactiveSecurityContextHolder::getContext).thenReturn(Mono.just(securityContext)); + runnable.run(); + } + } + + @Test + void validateSetCluster() { + withSecurityContext(() -> { + + List clusters = defaultRole.getClusters(); + assertThat(clusters) + .isNotNull() + .containsExactlyInAnyOrder(PROD_CLUSTER, DEV_CLUSTER); + }); + } + + @Test + void validateAccess() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(DEFAULT_ROLE)); + AccessContext context = getAccessContext(PROD_CLUSTER, true); + Mono validateAccessMono = accessControlService.validateAccess(context); + StepVerifier.create(validateAccessMono) + .expectComplete() + .verify(); + }); + } + + @Test + void isClusterAccessible() { + withSecurityContext(() -> { + ClusterDTO clusterDto = new ClusterDTO(); + clusterDto.setName(PROD_CLUSTER); + Mono clusterAccessibleMono = accessControlService.isClusterAccessible(clusterDto); + StepVerifier.create(clusterAccessibleMono) + .expectNext(true) + .expectComplete() + .verify(); + }); + } + + /** + * Test for isClusterAccessible with unknown cluster. + */ + @Test + void isClusterAccessible_unknownCluster() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(DEFAULT_ROLE)); + ClusterDTO clusterDto = new ClusterDTO(); + clusterDto.setName("unknown"); + Mono clusterAccessibleMono = accessControlService.isClusterAccessible(clusterDto); + StepVerifier.create(clusterAccessibleMono) + .expectNext(false) + .expectComplete() + .verify(); + }); + } + + @Test + void testGetDefaultRole() { + Role defaultRole = accessControlService.getDefaultRole(); + assertThat(defaultRole).isNotNull() + .isEqualTo(this.defaultRole); + } +} diff --git a/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java b/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java index a59322014..38b48561b 100644 --- a/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java +++ b/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java @@ -9,10 +9,7 @@ import io.kafbat.ui.model.rbac.Resource; import io.kafbat.ui.model.rbac.Role; import io.kafbat.ui.model.rbac.Subject; -import io.kafbat.ui.model.rbac.permission.ConnectAction; -import io.kafbat.ui.model.rbac.permission.ConsumerGroupAction; -import io.kafbat.ui.model.rbac.permission.SchemaAction; -import io.kafbat.ui.model.rbac.permission.TopicAction; +import io.kafbat.ui.model.rbac.permission.*; import io.kafbat.ui.model.rbac.provider.Provider; import java.util.List; @@ -20,6 +17,7 @@ public class MockedRbacUtils { public static final String ADMIN_ROLE = "admin_role"; public static final String DEV_ROLE = "dev_role"; + public static final String DEFAULT_ROLE = "default_role"; public static final String PROD_CLUSTER = "prod"; public static final String DEV_CLUSTER = "dev"; @@ -99,6 +97,41 @@ public static Role getDevRole() { return role; } + public static Role getDefaultRole() { + Role role = new Role(); + role.setName(DEFAULT_ROLE); + + Permission topicViewPermission = new Permission(); + topicViewPermission.setResource(Resource.TOPIC.name()); + topicViewPermission.setActions(List.of(TopicAction.VIEW.name())); + topicViewPermission.setValue(TOPIC_NAME); + + Permission consumerGroupPermission = new Permission(); + consumerGroupPermission.setResource(Resource.CONSUMER.name()); + consumerGroupPermission.setActions(List.of(ConsumerGroupAction.VIEW.name())); + consumerGroupPermission.setValue(CONSUMER_GROUP_NAME); + + Permission schemaPermission = new Permission(); + schemaPermission.setResource(Resource.SCHEMA.name()); + schemaPermission.setActions(List.of(SchemaAction.VIEW.name())); + schemaPermission.setValue(SCHEMA_NAME); + + Permission connectPermission = new Permission(); + connectPermission.setResource(Resource.CONNECT.name()); + connectPermission.setActions(List.of(ConnectAction.VIEW.name())); + connectPermission.setValue(CONNECT_NAME); + + List permissions = List.of( + topicViewPermission, + consumerGroupPermission, + schemaPermission, + connectPermission + ); + role.setPermissions(permissions); + role.validateDefaultRole(); + return role; + } + public static AccessContext getAccessContext(String cluster, Boolean resourceAccessible) { AccessContext.ResourceAccess mockedResource = mock(AccessContext.ResourceAccess.class); when(mockedResource.isAccessible(any())).thenReturn(resourceAccessible); From 69045d58ffb514b06370453da7e88fe7fd72934d Mon Sep 17 00:00:00 2001 From: seono Date: Wed, 11 Jun 2025 21:59:28 +0900 Subject: [PATCH 04/10] BE: fix Default Role test --- .../io/kafbat/ui/service/rbac/AccessControlService.java | 3 --- .../AccessControlServiceDefaultRoleRbacEnabledTest.java | 6 ++++-- .../java/io/kafbat/ui/service/rbac/MockedRbacUtils.java | 5 ++++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java b/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java index 965926746..3ab53e670 100644 --- a/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java +++ b/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java @@ -162,9 +162,6 @@ public static Mono getUser() { private boolean isClusterAccessible(String clusterName, AuthenticatedUser user) { Assert.isTrue(StringUtils.isNotEmpty(clusterName), "cluster value is empty"); - if (properties.getDefaultRole() != null) { - return true; - } return properties.getRoles() .stream() .filter(filterRole(user)) diff --git a/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java b/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java index ddf966b9c..f3a83f031 100644 --- a/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java +++ b/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java @@ -1,7 +1,9 @@ package io.kafbat.ui.service.rbac; - -import static io.kafbat.ui.service.rbac.MockedRbacUtils.*; +import static io.kafbat.ui.service.rbac.MockedRbacUtils.DEFAULT_ROLE; +import static io.kafbat.ui.service.rbac.MockedRbacUtils.DEV_CLUSTER; +import static io.kafbat.ui.service.rbac.MockedRbacUtils.PROD_CLUSTER; +import static io.kafbat.ui.service.rbac.MockedRbacUtils.getAccessContext; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; diff --git a/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java b/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java index 38b48561b..ceac8d088 100644 --- a/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java +++ b/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java @@ -9,7 +9,10 @@ import io.kafbat.ui.model.rbac.Resource; import io.kafbat.ui.model.rbac.Role; import io.kafbat.ui.model.rbac.Subject; -import io.kafbat.ui.model.rbac.permission.*; +import io.kafbat.ui.model.rbac.permission.ConnectAction; +import io.kafbat.ui.model.rbac.permission.ConsumerGroupAction; +import io.kafbat.ui.model.rbac.permission.SchemaAction; +import io.kafbat.ui.model.rbac.permission.TopicAction; import io.kafbat.ui.model.rbac.provider.Provider; import java.util.List; From 273da6a52c79973c97835793b4b2f2426ac4698e Mon Sep 17 00:00:00 2001 From: seono Date: Wed, 11 Jun 2025 23:10:40 +0900 Subject: [PATCH 05/10] BE: remove unnecessary test --- ...ontrolServiceDefaultRoleRbacEnabledTest.java | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java b/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java index f3a83f031..fcb00c778 100644 --- a/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java +++ b/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java @@ -125,23 +125,6 @@ void isClusterAccessible() { }); } - /** - * Test for isClusterAccessible with unknown cluster. - */ - @Test - void isClusterAccessible_unknownCluster() { - withSecurityContext(() -> { - when(user.groups()).thenReturn(List.of(DEFAULT_ROLE)); - ClusterDTO clusterDto = new ClusterDTO(); - clusterDto.setName("unknown"); - Mono clusterAccessibleMono = accessControlService.isClusterAccessible(clusterDto); - StepVerifier.create(clusterAccessibleMono) - .expectNext(false) - .expectComplete() - .verify(); - }); - } - @Test void testGetDefaultRole() { Role defaultRole = accessControlService.getDefaultRole(); From e0cba8837aa18e49dce39574d417b7bb7acdcb41 Mon Sep 17 00:00:00 2001 From: seono Date: Thu, 12 Jun 2025 18:44:23 +0900 Subject: [PATCH 06/10] BE: fix test error --- .../java/io/kafbat/ui/service/rbac/AccessControlService.java | 3 +++ .../rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java | 1 + 2 files changed, 4 insertions(+) diff --git a/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java b/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java index 3ab53e670..965926746 100644 --- a/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java +++ b/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java @@ -162,6 +162,9 @@ public static Mono getUser() { private boolean isClusterAccessible(String clusterName, AuthenticatedUser user) { Assert.isTrue(StringUtils.isNotEmpty(clusterName), "cluster value is empty"); + if (properties.getDefaultRole() != null) { + return true; + } return properties.getRoles() .stream() .filter(filterRole(user)) diff --git a/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java b/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java index fcb00c778..20ab0b348 100644 --- a/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java +++ b/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java @@ -62,6 +62,7 @@ void setUp() { RoleBasedAccessControlProperties properties = mock(); defaultRole = MockedRbacUtils.getDefaultRole(); when(properties.getDefaultRole()).thenReturn(defaultRole); + when(properties.getRoles()).thenReturn(List.of()); // Return empty list for roles ReflectionTestUtils.setField(accessControlService, "properties", properties); From 7cbf653915c912f927cd055755e9c18070a09bd6 Mon Sep 17 00:00:00 2001 From: seono Date: Thu, 12 Jun 2025 23:15:29 +0900 Subject: [PATCH 07/10] BE: address PR review comments --- .../RoleBasedAccessControlProperties.java | 11 +++--- .../io/kafbat/ui/model/rbac/DefaultRole.java | 21 +++++++++++ .../java/io/kafbat/ui/model/rbac/Role.java | 6 ---- .../ui/service/rbac/AccessControlService.java | 35 +++++-------------- .../extractor/CognitoAuthorityExtractor.java | 7 +--- .../extractor/GithubAuthorityExtractor.java | 7 +--- .../extractor/GoogleAuthorityExtractor.java | 7 +--- .../extractor/OauthAuthorityExtractor.java | 7 +--- ...acActiveDirectoryAuthoritiesExtractor.java | 9 +---- .../RbacLdapAuthoritiesExtractor.java | 9 +---- ...trolServiceDefaultRoleRbacEnabledTest.java | 30 ++-------------- .../ui/service/rbac/MockedRbacUtils.java | 8 +++-- 12 files changed, 50 insertions(+), 107 deletions(-) create mode 100644 api/src/main/java/io/kafbat/ui/model/rbac/DefaultRole.java diff --git a/api/src/main/java/io/kafbat/ui/config/auth/RoleBasedAccessControlProperties.java b/api/src/main/java/io/kafbat/ui/config/auth/RoleBasedAccessControlProperties.java index abe62a0f7..78b22d9bd 100644 --- a/api/src/main/java/io/kafbat/ui/config/auth/RoleBasedAccessControlProperties.java +++ b/api/src/main/java/io/kafbat/ui/config/auth/RoleBasedAccessControlProperties.java @@ -1,10 +1,11 @@ package io.kafbat.ui.config.auth; +import io.kafbat.ui.model.rbac.DefaultRole; import io.kafbat.ui.model.rbac.Role; +import jakarta.annotation.Nullable; import jakarta.annotation.PostConstruct; import java.util.ArrayList; import java.util.List; -import javax.annotation.Nullable; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties("rbac") @@ -12,13 +13,13 @@ public class RoleBasedAccessControlProperties { private final List roles = new ArrayList<>(); - private Role defaultRole; + private DefaultRole defaultRole; @PostConstruct public void init() { roles.forEach(Role::validate); if (defaultRole != null) { - defaultRole.validateDefaultRole(); + defaultRole.validate(); } } @@ -26,12 +27,12 @@ public List getRoles() { return roles; } - public void setDefaultRole(Role defaultRole) { + public void setDefaultRole(DefaultRole defaultRole) { this.defaultRole = defaultRole; } @Nullable - public Role getDefaultRole() { + public DefaultRole getDefaultRole() { return defaultRole; } } diff --git a/api/src/main/java/io/kafbat/ui/model/rbac/DefaultRole.java b/api/src/main/java/io/kafbat/ui/model/rbac/DefaultRole.java new file mode 100644 index 000000000..2caa10ecd --- /dev/null +++ b/api/src/main/java/io/kafbat/ui/model/rbac/DefaultRole.java @@ -0,0 +1,21 @@ +package io.kafbat.ui.model.rbac; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.util.ArrayList; +import java.util.List; +import lombok.Data; + +@Data +public class DefaultRole { + + private String name; + private List clusters; + private List permissions = new ArrayList<>(); + + public void validate() { + checkArgument(clusters != null && !clusters.isEmpty(), "Default role clusters cannot be empty"); + permissions.forEach(Permission::validate); + permissions.forEach(Permission::transform); + } +} \ No newline at end of file diff --git a/api/src/main/java/io/kafbat/ui/model/rbac/Role.java b/api/src/main/java/io/kafbat/ui/model/rbac/Role.java index 473c39990..c2ec0f556 100644 --- a/api/src/main/java/io/kafbat/ui/model/rbac/Role.java +++ b/api/src/main/java/io/kafbat/ui/model/rbac/Role.java @@ -21,10 +21,4 @@ public void validate() { permissions.forEach(Permission::transform); subjects.forEach(Subject::validate); } - - // default role need only permissions - public void validateDefaultRole() { - permissions.forEach(Permission::validate); - permissions.forEach(Permission::transform); - } } diff --git a/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java b/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java index 965926746..ff780b596 100644 --- a/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java +++ b/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java @@ -6,8 +6,8 @@ import io.kafbat.ui.model.ClusterDTO; import io.kafbat.ui.model.ConnectDTO; import io.kafbat.ui.model.InternalTopic; -import io.kafbat.ui.model.KafkaCluster; import io.kafbat.ui.model.rbac.AccessContext; +import io.kafbat.ui.model.rbac.DefaultRole; import io.kafbat.ui.model.rbac.Permission; import io.kafbat.ui.model.rbac.Role; import io.kafbat.ui.model.rbac.Subject; @@ -69,27 +69,9 @@ public void init() { log.trace("No roles provided, disabling RBAC"); return; } - if (properties.getDefaultRole() != null) { - log.trace("Set Default Role Clusters"); - // set default role for all clusters - properties.getDefaultRole().setClusters( - clustersStorage.getKafkaClusters().stream() - .map(KafkaCluster::getName) - .collect(Collectors.toList()) - ); - } rbacEnabled = true; - if (properties.getDefaultRole() != null) { - // set all extractors for default role because it is applied to all clusters - this.oauthExtractors = Set.of( - new CognitoAuthorityExtractor(), - new GoogleAuthorityExtractor(), - new GithubAuthorityExtractor(), - new OauthAuthorityExtractor() - ); - } else { - this.oauthExtractors = properties.getRoles() + this.oauthExtractors = properties.getRoles() .stream() .map(role -> role.getSubjects() .stream() @@ -106,7 +88,6 @@ public void init() { .collect(Collectors.toSet())) .flatMap(Set::stream) .collect(Collectors.toSet()); - } if (!(properties.getRoles().isEmpty() && properties.getDefaultRole() == null) && "oauth2".equalsIgnoreCase(environment.getProperty("auth.type")) @@ -162,13 +143,15 @@ public static Mono getUser() { private boolean isClusterAccessible(String clusterName, AuthenticatedUser user) { Assert.isTrue(StringUtils.isNotEmpty(clusterName), "cluster value is empty"); - if (properties.getDefaultRole() != null) { - return true; - } - return properties.getRoles() + boolean isAccessible = properties.getRoles() .stream() .filter(filterRole(user)) .anyMatch(role -> role.getClusters().stream().anyMatch(clusterName::equalsIgnoreCase)); + + if (!isAccessible && properties.getDefaultRole() != null) { + return properties.getDefaultRole().getClusters().stream().anyMatch(clusterName::equalsIgnoreCase); + } + return isAccessible; } public Mono isClusterAccessible(ClusterDTO cluster) { @@ -233,7 +216,7 @@ public List getRoles() { return Collections.unmodifiableList(properties.getRoles()); } - public Role getDefaultRole() { + public DefaultRole getDefaultRole() { return properties.getDefaultRole(); } 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 9cdf5ea62..cc0e419bf 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 @@ -39,13 +39,8 @@ public Mono> extract(AccessControlService acs, Object value, Map extractUsernameRoles(AccessControlService acs, DefaultOAuth2User principal) { 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 63182b999..79f4907fc 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 @@ -71,15 +71,10 @@ public Mono> extract(AccessControlService acs, Object value, Map> organizationRoles = getOrganizationRoles(principal, additionalParams, acs, webClient); Mono> teamRoles = getTeamRoles(webClient, additionalParams, acs); - Set defaultRoles = acs.getDefaultRole() == null - ? Collections.emptySet() - : Set.of(acs.getDefaultRole().getName()); - return Mono.zip(organizationRoles, teamRoles) .map((t) -> Stream.of(t.getT1(), t.getT2(), usernameRoles) .flatMap(Collection::stream) - .collect(Collectors.toSet())) - .map(roles -> roles.isEmpty() ? defaultRoles : roles); + .collect(Collectors.toSet())); } private Set extractUsernameRoles(DefaultOAuth2User principal, AccessControlService acs) { 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 a0fd07d32..a90ab50ef 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 @@ -39,13 +39,8 @@ public Mono> extract(AccessControlService acs, Object value, Map extractUsernameRoles(AccessControlService acs, DefaultOAuth2User principal) { 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 2f827c6ae..61748610e 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 @@ -43,13 +43,8 @@ public Mono> extract(AccessControlService acs, Object value, Map extractUsernameRoles(AccessControlService acs, DefaultOAuth2User principal) { diff --git a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacActiveDirectoryAuthoritiesExtractor.java b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacActiveDirectoryAuthoritiesExtractor.java index 2f545a484..f7f5ec1d9 100644 --- a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacActiveDirectoryAuthoritiesExtractor.java +++ b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacActiveDirectoryAuthoritiesExtractor.java @@ -4,7 +4,6 @@ import io.kafbat.ui.model.rbac.provider.Provider; import io.kafbat.ui.service.rbac.AccessControlService; import java.util.Collection; -import java.util.Set; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationContext; @@ -32,7 +31,7 @@ public Collection getGrantedAuthorities(DirContextOp .peek(group -> log.trace("Found AD group [{}] for user [{}]", group, username)) .collect(Collectors.toSet()); - var grantedAuthorities = acs.getRoles() + return acs.getRoles() .stream() .filter(r -> r.getSubjects() .stream() @@ -47,11 +46,5 @@ public Collection getGrantedAuthorities(DirContextOp .peek(role -> log.trace("Mapped role [{}] for user [{}]", role, username)) .map(SimpleGrantedAuthority::new) .collect(Collectors.toSet()); - - // If no roles are found, return default role - if (grantedAuthorities.isEmpty() && acs.getDefaultRole() != null) { - return Set.of(new SimpleGrantedAuthority(acs.getDefaultRole().getName())); - } - return grantedAuthorities; } } diff --git a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java index f19ddc8f8..fd168bc5a 100644 --- a/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java +++ b/api/src/main/java/io/kafbat/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java @@ -3,7 +3,6 @@ import io.kafbat.ui.model.rbac.Role; import io.kafbat.ui.model.rbac.provider.Provider; import io.kafbat.ui.service.rbac.AccessControlService; -import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; @@ -34,7 +33,7 @@ protected Set getAdditionalRoles(DirContextOperations user, St .peek(group -> log.trace("Found LDAP group [{}] for user [{}]", group, username)) .collect(Collectors.toSet()); - var simpleGrantedAuthorities = acs.getRoles() + return acs.getRoles() .stream() .filter(r -> r.getSubjects() .stream() @@ -49,11 +48,5 @@ protected Set getAdditionalRoles(DirContextOperations user, St .peek(role -> log.trace("Mapped role [{}] for user [{}]", role, username)) .map(SimpleGrantedAuthority::new) .collect(Collectors.toSet()); - - // If no roles are found, return default role - if (simpleGrantedAuthorities.isEmpty() && acs.getDefaultRole() != null) { - return Set.of(new SimpleGrantedAuthority(acs.getDefaultRole().getName())); - } - return new HashSet<>(simpleGrantedAuthorities); } } diff --git a/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java b/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java index 20ab0b348..1f3e2c1cf 100644 --- a/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java +++ b/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java @@ -14,6 +14,7 @@ import io.kafbat.ui.model.ClusterDTO; import io.kafbat.ui.model.KafkaCluster; import io.kafbat.ui.model.rbac.AccessContext; +import io.kafbat.ui.model.rbac.DefaultRole; import io.kafbat.ui.model.rbac.Role; import io.kafbat.ui.service.ClustersStorage; import java.util.List; @@ -23,6 +24,7 @@ import org.mockito.MockedStatic; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.SecurityContext; @@ -51,7 +53,7 @@ public class AccessControlServiceDefaultRoleRbacEnabledTest extends AbstractInte RbacUser user; @Mock - Role defaultRole; + DefaultRole defaultRole; @Mock ClustersStorage clustersStorage; @@ -67,14 +69,6 @@ void setUp() { ReflectionTestUtils.setField(accessControlService, "properties", properties); ReflectionTestUtils.setField(accessControlService, "rbacEnabled", true); - ReflectionTestUtils.setField(accessControlService, "clustersStorage", clustersStorage); - - KafkaCluster prodCluster = KafkaCluster.builder().name(PROD_CLUSTER).build(); - KafkaCluster devCluster = KafkaCluster.builder().name(DEV_CLUSTER).build(); - - // set default role for all clusters - when(clustersStorage.getKafkaClusters()).thenReturn(List.of(prodCluster, devCluster)); - accessControlService.init(); // Mock security context when(securityContext.getAuthentication()).thenReturn(authentication); @@ -90,17 +84,6 @@ public void withSecurityContext(Runnable runnable) { } } - @Test - void validateSetCluster() { - withSecurityContext(() -> { - - List clusters = defaultRole.getClusters(); - assertThat(clusters) - .isNotNull() - .containsExactlyInAnyOrder(PROD_CLUSTER, DEV_CLUSTER); - }); - } - @Test void validateAccess() { withSecurityContext(() -> { @@ -125,11 +108,4 @@ void isClusterAccessible() { .verify(); }); } - - @Test - void testGetDefaultRole() { - Role defaultRole = accessControlService.getDefaultRole(); - assertThat(defaultRole).isNotNull() - .isEqualTo(this.defaultRole); - } } diff --git a/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java b/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java index ceac8d088..c883e8d44 100644 --- a/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java +++ b/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java @@ -5,6 +5,7 @@ import static org.mockito.Mockito.when; import io.kafbat.ui.model.rbac.AccessContext; +import io.kafbat.ui.model.rbac.DefaultRole; import io.kafbat.ui.model.rbac.Permission; import io.kafbat.ui.model.rbac.Resource; import io.kafbat.ui.model.rbac.Role; @@ -100,9 +101,10 @@ public static Role getDevRole() { return role; } - public static Role getDefaultRole() { - Role role = new Role(); + public static DefaultRole getDefaultRole() { + DefaultRole role = new DefaultRole(); role.setName(DEFAULT_ROLE); + role.setClusters(List.of(DEV_CLUSTER, PROD_CLUSTER)); Permission topicViewPermission = new Permission(); topicViewPermission.setResource(Resource.TOPIC.name()); @@ -131,7 +133,7 @@ public static Role getDefaultRole() { connectPermission ); role.setPermissions(permissions); - role.validateDefaultRole(); + role.validate(); return role; } From daac844771ad94faa638a8270e96a7d68d84b541 Mon Sep 17 00:00:00 2001 From: seono Date: Mon, 16 Jun 2025 11:51:42 +0900 Subject: [PATCH 08/10] BE: RBAC:remove name from default role --- api/src/main/java/io/kafbat/ui/model/rbac/DefaultRole.java | 1 - api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java | 1 - 2 files changed, 2 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/model/rbac/DefaultRole.java b/api/src/main/java/io/kafbat/ui/model/rbac/DefaultRole.java index 2caa10ecd..bdac9d30c 100644 --- a/api/src/main/java/io/kafbat/ui/model/rbac/DefaultRole.java +++ b/api/src/main/java/io/kafbat/ui/model/rbac/DefaultRole.java @@ -9,7 +9,6 @@ @Data public class DefaultRole { - private String name; private List clusters; private List permissions = new ArrayList<>(); diff --git a/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java b/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java index c883e8d44..de6d7bf44 100644 --- a/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java +++ b/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java @@ -103,7 +103,6 @@ public static Role getDevRole() { public static DefaultRole getDefaultRole() { DefaultRole role = new DefaultRole(); - role.setName(DEFAULT_ROLE); role.setClusters(List.of(DEV_CLUSTER, PROD_CLUSTER)); Permission topicViewPermission = new Permission(); From e5bb2fe127ea638924bf69185d4cba0d0538f450 Mon Sep 17 00:00:00 2001 From: seono Date: Mon, 16 Jun 2025 12:35:53 +0900 Subject: [PATCH 09/10] BE: RBAC: remove default role clusters --- .../io/kafbat/ui/controller/AuthorizationController.java | 5 ++++- .../main/java/io/kafbat/ui/model/rbac/DefaultRole.java | 2 -- .../io/kafbat/ui/service/rbac/AccessControlService.java | 7 +------ .../AccessControlServiceDefaultRoleRbacEnabledTest.java | 9 --------- .../java/io/kafbat/ui/service/rbac/MockedRbacUtils.java | 4 +--- 5 files changed, 6 insertions(+), 21 deletions(-) diff --git a/api/src/main/java/io/kafbat/ui/controller/AuthorizationController.java b/api/src/main/java/io/kafbat/ui/controller/AuthorizationController.java index ee5db08a5..75d2c2401 100644 --- a/api/src/main/java/io/kafbat/ui/controller/AuthorizationController.java +++ b/api/src/main/java/io/kafbat/ui/controller/AuthorizationController.java @@ -3,10 +3,12 @@ import io.kafbat.ui.api.AuthorizationApi; import io.kafbat.ui.model.ActionDTO; import io.kafbat.ui.model.AuthenticationInfoDTO; +import io.kafbat.ui.model.KafkaCluster; import io.kafbat.ui.model.ResourceTypeDTO; import io.kafbat.ui.model.UserInfoDTO; import io.kafbat.ui.model.UserPermissionDTO; import io.kafbat.ui.model.rbac.Permission; +import io.kafbat.ui.service.ClustersStorage; import io.kafbat.ui.service.rbac.AccessControlService; import java.security.Principal; import java.util.Collection; @@ -29,12 +31,13 @@ public class AuthorizationController implements AuthorizationApi { private final AccessControlService accessControlService; + private final ClustersStorage clustersStorage; public Mono> getUserAuthInfo(ServerWebExchange exchange) { List defaultRolePermissions = accessControlService.getDefaultRole() != null ? mapPermissions( accessControlService.getDefaultRole().getPermissions(), - accessControlService.getDefaultRole().getClusters()) + clustersStorage.getKafkaClusters().stream().map(KafkaCluster::getName).toList()) : Collections.emptyList(); Mono> permissions = AccessControlService.getUser() diff --git a/api/src/main/java/io/kafbat/ui/model/rbac/DefaultRole.java b/api/src/main/java/io/kafbat/ui/model/rbac/DefaultRole.java index bdac9d30c..07819b4e4 100644 --- a/api/src/main/java/io/kafbat/ui/model/rbac/DefaultRole.java +++ b/api/src/main/java/io/kafbat/ui/model/rbac/DefaultRole.java @@ -9,11 +9,9 @@ @Data public class DefaultRole { - private List clusters; private List permissions = new ArrayList<>(); public void validate() { - checkArgument(clusters != null && !clusters.isEmpty(), "Default role clusters cannot be empty"); permissions.forEach(Permission::validate); permissions.forEach(Permission::transform); } diff --git a/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java b/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java index ff780b596..8e0270e6a 100644 --- a/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java +++ b/api/src/main/java/io/kafbat/ui/service/rbac/AccessControlService.java @@ -15,7 +15,6 @@ import io.kafbat.ui.model.rbac.permission.ConsumerGroupAction; import io.kafbat.ui.model.rbac.permission.SchemaAction; import io.kafbat.ui.model.rbac.permission.TopicAction; -import io.kafbat.ui.service.ClustersStorage; import io.kafbat.ui.service.rbac.extractor.CognitoAuthorityExtractor; import io.kafbat.ui.service.rbac.extractor.GithubAuthorityExtractor; import io.kafbat.ui.service.rbac.extractor.GoogleAuthorityExtractor; @@ -55,7 +54,6 @@ public class AccessControlService { @Nullable private final InMemoryReactiveClientRegistrationRepository clientRegistrationRepository; private final RoleBasedAccessControlProperties properties; - private final ClustersStorage clustersStorage; private final Environment environment; @Getter @@ -148,10 +146,7 @@ private boolean isClusterAccessible(String clusterName, AuthenticatedUser user) .filter(filterRole(user)) .anyMatch(role -> role.getClusters().stream().anyMatch(clusterName::equalsIgnoreCase)); - if (!isAccessible && properties.getDefaultRole() != null) { - return properties.getDefaultRole().getClusters().stream().anyMatch(clusterName::equalsIgnoreCase); - } - return isAccessible; + return isAccessible || properties.getDefaultRole() != null; } public Mono isClusterAccessible(ClusterDTO cluster) { diff --git a/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java b/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java index 1f3e2c1cf..c96ed9c84 100644 --- a/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java +++ b/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceDefaultRoleRbacEnabledTest.java @@ -1,10 +1,8 @@ package io.kafbat.ui.service.rbac; import static io.kafbat.ui.service.rbac.MockedRbacUtils.DEFAULT_ROLE; -import static io.kafbat.ui.service.rbac.MockedRbacUtils.DEV_CLUSTER; import static io.kafbat.ui.service.rbac.MockedRbacUtils.PROD_CLUSTER; import static io.kafbat.ui.service.rbac.MockedRbacUtils.getAccessContext; -import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -12,11 +10,8 @@ import io.kafbat.ui.config.auth.RbacUser; import io.kafbat.ui.config.auth.RoleBasedAccessControlProperties; import io.kafbat.ui.model.ClusterDTO; -import io.kafbat.ui.model.KafkaCluster; import io.kafbat.ui.model.rbac.AccessContext; import io.kafbat.ui.model.rbac.DefaultRole; -import io.kafbat.ui.model.rbac.Role; -import io.kafbat.ui.service.ClustersStorage; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -24,7 +19,6 @@ import org.mockito.MockedStatic; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.SecurityContext; @@ -55,9 +49,6 @@ public class AccessControlServiceDefaultRoleRbacEnabledTest extends AbstractInte @Mock DefaultRole defaultRole; - @Mock - ClustersStorage clustersStorage; - @BeforeEach void setUp() { diff --git a/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java b/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java index de6d7bf44..dc5811c56 100644 --- a/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java +++ b/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java @@ -102,9 +102,6 @@ public static Role getDevRole() { } public static DefaultRole getDefaultRole() { - DefaultRole role = new DefaultRole(); - role.setClusters(List.of(DEV_CLUSTER, PROD_CLUSTER)); - Permission topicViewPermission = new Permission(); topicViewPermission.setResource(Resource.TOPIC.name()); topicViewPermission.setActions(List.of(TopicAction.VIEW.name())); @@ -131,6 +128,7 @@ public static DefaultRole getDefaultRole() { schemaPermission, connectPermission ); + DefaultRole role = new DefaultRole(); role.setPermissions(permissions); role.validate(); return role; From db756773ea6b79e7803412d0ce9c7974dc9dab62 Mon Sep 17 00:00:00 2001 From: seonho-jeong Date: Mon, 23 Jun 2025 17:49:19 +0900 Subject: [PATCH 10/10] BE: RBAC: Add default role --- .../main/resources/swagger/kafbat-ui-api.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/contract/src/main/resources/swagger/kafbat-ui-api.yaml b/contract/src/main/resources/swagger/kafbat-ui-api.yaml index 8769e6aa1..8e39e94a7 100644 --- a/contract/src/main/resources/swagger/kafbat-ui-api.yaml +++ b/contract/src/main/resources/swagger/kafbat-ui-api.yaml @@ -4278,6 +4278,22 @@ components: type: array items: $ref: '#/components/schemas/Action' + defaultRole: + type: object + properties: + permissions: + type: array + items: + type: object + properties: + resource: + $ref: '#/components/schemas/ResourceType' + value: + type: string + actions: + type: array + items: + $ref: '#/components/schemas/Action' webclient: type: object properties: