diff --git a/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceRbacEnabledTest.java b/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceRbacEnabledTest.java new file mode 100644 index 000000000..57264190e --- /dev/null +++ b/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceRbacEnabledTest.java @@ -0,0 +1,294 @@ +package io.kafbat.ui.service.rbac; + +import static io.kafbat.ui.service.rbac.MockedRbacUtils.ADMIN_ROLE; +import static io.kafbat.ui.service.rbac.MockedRbacUtils.CONNECT_NAME; +import static io.kafbat.ui.service.rbac.MockedRbacUtils.CONSUMER_GROUP_NAME; +import static io.kafbat.ui.service.rbac.MockedRbacUtils.DEV_CLUSTER; +import static io.kafbat.ui.service.rbac.MockedRbacUtils.DEV_ROLE; +import static io.kafbat.ui.service.rbac.MockedRbacUtils.PROD_CLUSTER; +import static io.kafbat.ui.service.rbac.MockedRbacUtils.SCHEMA_NAME; +import static io.kafbat.ui.service.rbac.MockedRbacUtils.TOPIC_NAME; +import static io.kafbat.ui.service.rbac.MockedRbacUtils.getAccessContext; +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.ConnectDTO; +import io.kafbat.ui.model.InternalTopic; +import io.kafbat.ui.model.rbac.AccessContext; +import io.kafbat.ui.model.rbac.Role; +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.access.AccessDeniedException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.test.util.ReflectionTestUtils; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +/** + * Test cases for AccessControlService when RBAC is enabled. + */ +class AccessControlServiceRbacEnabledTest extends AbstractIntegrationTest { + + @Autowired + AccessControlService accessControlService; + + @Mock + SecurityContext securityContext; + + @Mock + Authentication authentication; + + @Mock + RbacUser user; + + @BeforeEach + void setUp() { + // Mock roles + List roles = List.of( + MockedRbacUtils.getAdminRole(), + MockedRbacUtils.getDevRole() + ); + RoleBasedAccessControlProperties properties = mock(); + when(properties.getRoles()).thenReturn(roles); + + ReflectionTestUtils.setField(accessControlService, "properties", properties); + ReflectionTestUtils.setField(accessControlService, "rbacEnabled", true); + + // 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 validateAccess() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(ADMIN_ROLE)); + AccessContext context = getAccessContext(PROD_CLUSTER, true); + Mono validateAccessMono = accessControlService.validateAccess(context); + StepVerifier.create(validateAccessMono) + .expectComplete() + .verify(); + }); + } + + @Test + void validateAccess_deniedCluster() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(DEV_ROLE)); + AccessContext context = getAccessContext(PROD_CLUSTER, true); + Mono validateAccessMono = accessControlService.validateAccess(context); + StepVerifier.create(validateAccessMono) + .expectErrorMatches(e -> e instanceof AccessDeniedException) + .verify(); + }); + } + + @Test + void validateAccess_deniedResourceNotAccessible() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(ADMIN_ROLE)); + AccessContext context = getAccessContext(PROD_CLUSTER, false); + Mono validateAccessMono = accessControlService.validateAccess(context); + StepVerifier.create(validateAccessMono) + .expectErrorMatches(e -> e instanceof AccessDeniedException) + .verify(); + }); + } + + @Test + void isClusterAccessible() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(ADMIN_ROLE)); + ClusterDTO clusterDto = new ClusterDTO(); + clusterDto.setName(PROD_CLUSTER); + Mono clusterAccessibleMono = accessControlService.isClusterAccessible(clusterDto); + StepVerifier.create(clusterAccessibleMono) + .expectNext(true) + .expectComplete() + .verify(); + }); + } + + @Test + void isClusterAccessible_deniedCluster() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(DEV_ROLE)); + ClusterDTO clusterDto = new ClusterDTO(); + clusterDto.setName(PROD_CLUSTER); + Mono clusterAccessibleMono = accessControlService.isClusterAccessible(clusterDto); + StepVerifier.create(clusterAccessibleMono) + .expectNext(false) + .expectComplete() + .verify(); + }); + } + + @Test + void filterViewableTopics() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(DEV_ROLE)); + ClusterDTO clusterDto = new ClusterDTO(); + clusterDto.setName(DEV_CLUSTER); + List topics = List.of( + InternalTopic.builder() + .name(TOPIC_NAME) + .build() + ); + Mono> filterTopicsMono = accessControlService.filterViewableTopics(topics, DEV_CLUSTER); + StepVerifier.create(filterTopicsMono) + .expectNextMatches(responseTopics -> responseTopics.stream().anyMatch(t -> t.getName().equals(TOPIC_NAME))) + .expectComplete() + .verify(); + }); + } + + @Test + void filterViewableTopics_notAccessibleTopic() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(DEV_ROLE)); + ClusterDTO clusterDto = new ClusterDTO(); + clusterDto.setName(DEV_CLUSTER); + List topics = List.of( + InternalTopic.builder() + .name("some other topic") + .build() + ); + Mono> filterTopicsMono = accessControlService.filterViewableTopics(topics, DEV_CLUSTER); + StepVerifier.create(filterTopicsMono) + .expectNextMatches(List::isEmpty) + .expectComplete() + .verify(); + }); + } + + @Test + void isConsumerGroupAccessible() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(DEV_ROLE)); + Mono consumerGroupAccessibleMono = + accessControlService.isConsumerGroupAccessible(CONSUMER_GROUP_NAME, DEV_CLUSTER); + StepVerifier.create(consumerGroupAccessibleMono) + .expectNext(true) + .expectComplete() + .verify(); + }); + } + + @Test + void isConsumerGroupAccessible_notAccessible() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(DEV_ROLE)); + Mono consumerGroupAccessibleMono = + accessControlService.isConsumerGroupAccessible("SOME OTHER CONSUMER", DEV_CLUSTER); + StepVerifier.create(consumerGroupAccessibleMono) + .expectNext(false) + .expectComplete() + .verify(); + }); + } + + @Test + void isSchemaAccessible() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(DEV_ROLE)); + Mono consumerGroupAccessibleMono = + accessControlService.isSchemaAccessible(SCHEMA_NAME, DEV_CLUSTER); + StepVerifier.create(consumerGroupAccessibleMono) + .expectNext(true) + .expectComplete() + .verify(); + }); + } + + @Test + void isSchemaAccessible_notAccessible() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(DEV_ROLE)); + Mono consumerGroupAccessibleMono = + accessControlService.isSchemaAccessible("SOME OTHER SCHEMA", DEV_CLUSTER); + StepVerifier.create(consumerGroupAccessibleMono) + .expectNext(false) + .expectComplete() + .verify(); + }); + } + + @Test + void isConnectAccessible() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(DEV_ROLE)); + Mono consumerGroupAccessibleMono = + accessControlService.isConnectAccessible(CONNECT_NAME, DEV_CLUSTER); + StepVerifier.create(consumerGroupAccessibleMono) + .expectNext(true) + .expectComplete() + .verify(); + }); + } + + @Test + void isConnectAccessible_notAccessible() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(DEV_ROLE)); + Mono consumerGroupAccessibleMono = + accessControlService.isConnectAccessible("SOME OTHER CONNECT", DEV_CLUSTER); + StepVerifier.create(consumerGroupAccessibleMono) + .expectNext(false) + .expectComplete() + .verify(); + }); + } + + @Test + void isConnectAccessibleDto() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(DEV_ROLE)); + ConnectDTO connectDto = ConnectDTO.builder() + .name(CONNECT_NAME) + .build(); + Mono consumerGroupAccessibleMono = + accessControlService.isConnectAccessible(connectDto, DEV_CLUSTER); + StepVerifier.create(consumerGroupAccessibleMono) + .expectNext(true) + .expectComplete() + .verify(); + }); + } + + @Test + void isConnectAccessibleDto_notAccessible() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(DEV_ROLE)); + ConnectDTO connectDto = ConnectDTO.builder() + .name("SOME OTHER CONNECT") + .build(); + Mono consumerGroupAccessibleMono = + accessControlService.isConnectAccessible(connectDto, DEV_CLUSTER); + StepVerifier.create(consumerGroupAccessibleMono) + .expectNext(false) + .expectComplete() + .verify(); + }); + } + +} diff --git a/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceTest.java b/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceTest.java deleted file mode 100644 index 39cce976b..000000000 --- a/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceTest.java +++ /dev/null @@ -1,169 +0,0 @@ -package io.kafbat.ui.service.rbac; - -import static org.mockito.ArgumentMatchers.any; -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.rbac.AccessContext; -import java.util.List; -import io.kafbat.ui.model.rbac.Role; -import org.junit.jupiter.api.Assertions; -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.context.ApplicationContextInitializer; -import org.springframework.context.ConfigurableApplicationContext; -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; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.util.ReflectionTestUtils; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -//@ContextConfiguration(initializers = {AccessControlServiceTest.PropertiesInitializer.class}) -class AccessControlServiceTest extends AbstractIntegrationTest { - -// public static class PropertiesInitializer extends AbstractIntegrationTest.Initializer -// implements ApplicationContextInitializer { - -// @Override -// public void initialize(ConfigurableApplicationContext applicationContext) { -// System.setProperty("rbac.roles[0].name", "memelords"); -// System.setProperty("rbac.roles[0].clusters[0]", "local"); -// -// System.setProperty("rbac.roles[0].subjects[0].provider", "oauth_google"); -// System.setProperty("rbac.roles[0].subjects[0].type", "domain"); -// System.setProperty("rbac.roles[0].subjects[0].value", "katbat.dev"); -// -// System.setProperty("rbac.roles[0].subjects[1].provider", "oauth_google"); -// System.setProperty("rbac.roles[0].subjects[1].type", "user"); -// System.setProperty("rbac.roles[0].subjects[1].value", "name@kafbat.dev"); -// -// System.setProperty("rbac.roles[0].permissions[0].resource", "applicationconfig"); -// System.setProperty("rbac.roles[0].permissions[0].actions", "all"); -// -// super.initialize(applicationContext); -// } -// } - - @Autowired - AccessControlService accessControlService; - - @Mock - ReactiveSecurityContextHolder securityContextHolder; - - @Mock - SecurityContext securityContext; - - @Mock - Authentication authentication; - - @Mock - RbacUser user; - - @BeforeEach - void setUp() { - // Mock roles - RoleBasedAccessControlProperties properties = mock(); - - Role memeLordsRole = new Role(); - memeLordsRole.setClusters(List.of("local")); - memeLordsRole.setName("memeLords"); - List roles = List.of( - memeLordsRole - ); - when(properties.getRoles()).thenReturn(roles); - ReflectionTestUtils.setField(accessControlService, "properties", properties); - - // 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 validateAccess() { - withSecurityContext(() -> { - when(user.groups()).thenReturn(List.of("memelords")); - AccessContext.ResourceAccess mockedResource = mock(AccessContext.ResourceAccess.class); - when(mockedResource.isAccessible(any())).thenReturn(true); - var accessContext = new AccessContext("local", List.of( - mockedResource - ), "op", "params"); - - Mono voidMono = accessControlService.validateAccess(accessContext); - StepVerifier.create(voidMono) - .expectComplete() - .verify(); - }); - } - - @Test - void validateAccess_deniedWrongGroup() { - withSecurityContext(() -> { - when(user.groups()).thenReturn(List.of("otherGroup")); // wrong group - AccessContext.ResourceAccess mockedResource = mock(AccessContext.ResourceAccess.class); - when(mockedResource.isAccessible(any())).thenReturn(true); - var accessContext = new AccessContext("local", List.of( - mockedResource - ), "op", "params"); - - Mono voidMono = accessControlService.validateAccess(accessContext); - StepVerifier.create(voidMono) - .expectErrorMatches(e -> e instanceof AccessDeniedException) - .verify(); - }); - } - - @Test - void validateAccess_deniedWrongCluster() { - withSecurityContext(() -> { - when(user.groups()).thenReturn(List.of("memelords")); - AccessContext.ResourceAccess mockedResource = mock(AccessContext.ResourceAccess.class); - when(mockedResource.isAccessible(any())).thenReturn(true); - var accessContext = new AccessContext("prod", // wrong cluster - List.of( - mockedResource - ), "op", "params"); - - Mono voidMono = accessControlService.validateAccess(accessContext); - StepVerifier.create(voidMono) - .expectErrorMatches(e -> e instanceof AccessDeniedException) - .verify(); - }); - } - - @Test - void validateAccess_deniedResourceNotAcessible() { - withSecurityContext(() -> { - when(user.groups()).thenReturn(List.of("memelords")); - AccessContext.ResourceAccess mockedResource = mock(AccessContext.ResourceAccess.class); - when(mockedResource.isAccessible(any())).thenReturn(false); // resource not acessible - var accessContext = new AccessContext("local", List.of( - mockedResource - ), "op", "params"); - - Mono voidMono = accessControlService.validateAccess(accessContext); - StepVerifier.create(voidMono) - .expectErrorMatches(e -> e instanceof AccessDeniedException) - .verify(); - }); - } - -} 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 new file mode 100644 index 000000000..f9e8bb3c9 --- /dev/null +++ b/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java @@ -0,0 +1,99 @@ +package io.kafbat.ui.service.rbac; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.kafbat.ui.model.rbac.AccessContext; +import io.kafbat.ui.model.rbac.Permission; +import io.kafbat.ui.model.rbac.Resource; +import io.kafbat.ui.model.rbac.Role; +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 java.util.List; + +public class MockedRbacUtils { + + public static final String ADMIN_ROLE = "admin_role"; + public static final String DEV_ROLE = "dev_role"; + + public static final String PROD_CLUSTER = "prod"; + public static final String DEV_CLUSTER = "dev"; + + public static final String TOPIC_NAME = "aTopic"; + public static final String CONSUMER_GROUP_NAME = "aConsumerGroup"; + public static final String SCHEMA_NAME = "aSchema"; + public static final String CONNECT_NAME = "aConnect"; + + /** + * All actions to Resource.APPLICATIONCONFIG for dev and prod clusters. + * + * @return admin role + */ + public static Role getAdminRole() { + Role role = new Role(); + role.setName(ADMIN_ROLE); + role.setClusters(List.of(DEV_CLUSTER, PROD_CLUSTER)); + Permission applicationConfigPerm = new Permission(); + applicationConfigPerm.setResource(Resource.APPLICATIONCONFIG.name()); + applicationConfigPerm.setActions(List.of("all")); + List permissions = List.of( + applicationConfigPerm + ); + role.setPermissions(permissions); + role.validate(); + return role; + } + + /** + * View actions to topic, consumer, schema and connect + * + * @return admin role + */ + public static Role getDevRole() { + Role role = new Role(); + role.setName(DEV_ROLE); + role.setClusters(List.of(DEV_CLUSTER)); + + 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.validate(); + return role; + } + + public static AccessContext getAccessContext(String cluster, Boolean resourceAccessible) { + AccessContext.ResourceAccess mockedResource = mock(AccessContext.ResourceAccess.class); + when(mockedResource.isAccessible(any())).thenReturn(resourceAccessible); + return new AccessContext(cluster, List.of( + mockedResource + ), "op", "params"); + } + +}