diff --git a/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceRbacDisabledTest.java b/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceRbacDisabledTest.java new file mode 100644 index 000000000..199771a78 --- /dev/null +++ b/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceRbacDisabledTest.java @@ -0,0 +1,173 @@ +package io.kafbat.ui.service.rbac; + +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_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.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import io.kafbat.ui.AbstractIntegrationTest; +import io.kafbat.ui.config.auth.RbacUser; +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.core.Authentication; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.test.annotation.DirtiesContext; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +/** + * Test cases for AccessControlService when RBAC is disabled. + * Using PROD cluster and user DEV role for all tests. + */ +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +class AccessControlServiceRbacDisabledTest extends AbstractIntegrationTest { + + @Autowired + AccessControlService accessControlService; + + @Mock + SecurityContext securityContext; + + @Mock + Authentication authentication; + + @Mock + RbacUser user; + + @BeforeEach + void setUp() { + // 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(DEV_ROLE)); + AccessContext context = getAccessContext(PROD_CLUSTER, true); + Mono validateAccessMono = accessControlService.validateAccess(context); + StepVerifier.create(validateAccessMono) + .expectComplete() + .verify(); + }); + } + + @Test + void isClusterAccessible() { + 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(true) + .expectComplete() + .verify(); + }); + } + + @Test + void filterViewableTopics() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(DEV_ROLE)); + List topics = List.of( + InternalTopic.builder() + .name(TOPIC_NAME) + .build() + ); + Mono> filterTopicsMono = accessControlService.filterViewableTopics(topics, PROD_CLUSTER); + StepVerifier.create(filterTopicsMono) + .expectNextMatches(responseTopics -> responseTopics.stream().anyMatch(t -> t.getName().equals(TOPIC_NAME))) + .expectComplete() + .verify(); + }); + } + + @Test + void isConsumerGroupAccessible() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(DEV_ROLE)); + Mono consumerGroupAccessibleMono = + accessControlService.isConsumerGroupAccessible(CONSUMER_GROUP_NAME, PROD_CLUSTER); + StepVerifier.create(consumerGroupAccessibleMono) + .expectNext(true) + .expectComplete() + .verify(); + }); + } + + @Test + void isSchemaAccessible() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(DEV_ROLE)); + Mono consumerGroupAccessibleMono = + accessControlService.isSchemaAccessible(SCHEMA_NAME, PROD_CLUSTER); + StepVerifier.create(consumerGroupAccessibleMono) + .expectNext(true) + .expectComplete() + .verify(); + }); + } + + @Test + void isConnectAccessible() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(DEV_ROLE)); + Mono consumerGroupAccessibleMono = + accessControlService.isConnectAccessible(CONNECT_NAME, PROD_CLUSTER); + StepVerifier.create(consumerGroupAccessibleMono) + .expectNext(true) + .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, PROD_CLUSTER); + StepVerifier.create(consumerGroupAccessibleMono) + .expectNext(true) + .expectComplete() + .verify(); + }); + } + + @Test + void getRoles() { + List roles = accessControlService.getRoles(); + assertThat(roles).isEmpty(); + } + +} 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..b7e307b51 --- /dev/null +++ b/api/src/test/java/io/kafbat/ui/service/rbac/AccessControlServiceRbacEnabledTest.java @@ -0,0 +1,301 @@ +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.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.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.annotation.DirtiesContext; +import org.springframework.test.util.ReflectionTestUtils; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +/** + * Test cases for AccessControlService when RBAC is enabled. + */ +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +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)); + 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)); + 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 isConnectAccessible_connectDto() { + 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 isConnectAccessible_connectDto_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(); + }); + } + + @Test + void testGetRoles() { + List roles = accessControlService.getRoles(); + assertThat(roles).hasSize(2) + .anyMatch(role -> role.getName().equals(DEV_ROLE)) + .anyMatch(role -> role.getName().equals(ADMIN_ROLE)); + } + +} 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..804c0c05c --- /dev/null +++ b/api/src/test/java/io/kafbat/ui/service/rbac/MockedRbacUtils.java @@ -0,0 +1,109 @@ +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.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 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)); + Subject sub = new Subject(); + sub.setType("group"); + sub.setProvider("ldap"); + sub.setValue("kafbat.group"); + role.setSubjects(List.of(sub)); + 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)); + Subject sub = new Subject(); + sub.setType("group"); + sub.setProvider("ldap"); + sub.setValue("kafbat.group"); + role.setSubjects(List.of(sub)); + 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"); + } + +} diff --git a/api/src/test/java/io/kafbat/ui/service/rbac/RbacClusterConfigTest.java b/api/src/test/java/io/kafbat/ui/service/rbac/RbacClusterConfigTest.java new file mode 100644 index 000000000..69c92f4f4 --- /dev/null +++ b/api/src/test/java/io/kafbat/ui/service/rbac/RbacClusterConfigTest.java @@ -0,0 +1,110 @@ +package io.kafbat.ui.service.rbac; + +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 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.ClusterConfigAction; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +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 cases for issue #461. + * Sets the role to any cluster ".*" + */ +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +class RbacClusterConfigTest extends AbstractIntegrationTest { + + public static final String ROLE_NAME = "cluster_config_role"; + + @Autowired + AccessControlService accessControlService; + + @Mock + SecurityContext securityContext; + + @Mock + Authentication authentication; + + @Mock + RbacUser user; + + @BeforeEach + void setUp() { + // Mock roles + List roles = List.of( + getClusterConfigRole() + ); + 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 + @Disabled("expected to work after issue #461 is resolved") + void validateAccess_clusterConfigAll_propertiesAllCluster() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(ROLE_NAME)); + AccessContext context = AccessContext.builder() + .cluster("prod") + .clusterConfigActions(ClusterConfigAction.EDIT) + .build(); + Mono validateAccessMono = accessControlService.validateAccess(context); + StepVerifier.create(validateAccessMono) + .expectComplete() + .verify(); + }); + } + + public static Role getClusterConfigRole() { + Role role = new Role(); + role.setName(ROLE_NAME); + role.setClusters(List.of(".*")); // setting role for any cluster + + Permission permission = new Permission(); + permission.setResource(Resource.CLUSTERCONFIG.name()); + permission.setActions(List.of("all")); + + List permissions = List.of( + permission + ); + role.setPermissions(permissions); + role.validate(); + return role; + } + +} diff --git a/api/src/test/java/io/kafbat/ui/service/rbac/RbacClusterMistakenTest.java b/api/src/test/java/io/kafbat/ui/service/rbac/RbacClusterMistakenTest.java new file mode 100644 index 000000000..4a3a74d38 --- /dev/null +++ b/api/src/test/java/io/kafbat/ui/service/rbac/RbacClusterMistakenTest.java @@ -0,0 +1,216 @@ +package io.kafbat.ui.service.rbac; + +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 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.Subject; +import io.kafbat.ui.model.rbac.permission.AclAction; +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.annotation.DirtiesContext; +import org.springframework.test.util.ReflectionTestUtils; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +/** + * Test cases for issue #274. + */ +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +class RbacClusterMistakenTest extends AbstractIntegrationTest { + + public static final String ADMIN_ROLE_NAME = "Admin Roles"; + + public static final String DEV_CLUSTER_ADM = "DEV"; + public static final String TST_CLUSTER_ADM = "TST"; + public static final String UAT_CLUSTER_ADM = "UAT"; + public static final String LAB_CLUSTER_ALL = "LAB"; + public static final String LAB_ROLE_NAME = "LAB for ALL"; + + @Autowired + AccessControlService accessControlService; + + @Mock + SecurityContext securityContext; + + @Mock + Authentication authentication; + + @Mock + RbacUser user; + + @BeforeEach + void setUp() { + // Mock roles + List roles = List.of( + getAdminRole(), + 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(); + } + } + + /** + * Anyone editing LAB. + */ + @Test + void validateAccess() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(LAB_ROLE_NAME)); + AccessContext context = AccessContext.builder() + .cluster(LAB_CLUSTER_ALL) + .aclActions(AclAction.EDIT) + .build(); + Mono validateAccessMono = accessControlService.validateAccess(context); + StepVerifier.create(validateAccessMono) + .expectComplete() + .verify(); + }); + } + + /** + * Admin with both roles editing LAB. + */ + @Test + void validateAccess_bothRoles() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(ADMIN_ROLE_NAME, LAB_ROLE_NAME)); + AccessContext context = AccessContext.builder() + .cluster(LAB_CLUSTER_ALL) + .aclActions(AclAction.EDIT) + .build(); + Mono validateAccessMono = accessControlService.validateAccess(context); + StepVerifier.create(validateAccessMono) + .expectComplete() + .verify(); + }); + } + + /** + * Anyone editing Dev cluster, denied. + */ + @Test + void validateAccess_Denied() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(LAB_ROLE_NAME)); + AccessContext context = AccessContext.builder() + .cluster(DEV_CLUSTER_ADM) + .aclActions(AclAction.EDIT) + .build(); + Mono validateAccessMono = accessControlService.validateAccess(context); + StepVerifier.create(validateAccessMono) + .expectErrorMatches(e -> e instanceof AccessDeniedException) + .verify(); + }); + } + + /** + * Admin editing dev cluster, denied. + */ + @Test + void validateAccess_DeniedAdminEditing() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(ADMIN_ROLE_NAME)); + AccessContext context = AccessContext.builder() + .cluster(DEV_CLUSTER_ADM) + .aclActions(AclAction.EDIT) + .build(); + Mono validateAccessMono = accessControlService.validateAccess(context); + StepVerifier.create(validateAccessMono) + .expectErrorMatches(e -> e instanceof AccessDeniedException) + .verify(); + }); + } + + /** + * Admin viewing Dev cluster, allowed. + */ + @Test + void validateAccess_viewAllowedAdmin() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(ADMIN_ROLE_NAME)); + AccessContext context = AccessContext.builder() + .cluster(DEV_CLUSTER_ADM) + .aclActions(AclAction.VIEW) + .build(); + Mono validateAccessMono = accessControlService.validateAccess(context); + StepVerifier.create(validateAccessMono) + .expectComplete() + .verify(); + }); + } + + private Role getAdminRole() { + Role role = new Role(); + role.setName(ADMIN_ROLE_NAME); + role.setClusters(List.of(DEV_CLUSTER_ADM, TST_CLUSTER_ADM, UAT_CLUSTER_ADM)); + Subject sub = new Subject(); + sub.setType("group"); + sub.setProvider("ldap"); + sub.setValue("kafbat.group"); + role.setSubjects(List.of(sub)); + Permission permission = new Permission(); + permission.setResource(Resource.ACL.name()); + permission.setActions(List.of(AclAction.VIEW.name())); + + List permissions = List.of( + permission + ); + role.setPermissions(permissions); + role.validate(); + return role; + } + + private Role getDevRole() { + Role role = new Role(); + role.setName(LAB_ROLE_NAME); + role.setClusters(List.of(LAB_CLUSTER_ALL)); + Subject sub = new Subject(); + sub.setType("group"); + sub.setProvider("ldap"); + sub.setValue("kafbat.group"); + role.setSubjects(List.of(sub)); + Permission permission = new Permission(); + permission.setResource(Resource.ACL.name()); + permission.setActions(List.of(AclAction.VIEW.name(), AclAction.EDIT.name())); + + List permissions = List.of( + permission + ); + role.setPermissions(permissions); + role.validate(); + return role; + } + +} diff --git a/api/src/test/java/io/kafbat/ui/service/rbac/RbacEditTopicTest.java b/api/src/test/java/io/kafbat/ui/service/rbac/RbacEditTopicTest.java new file mode 100644 index 000000000..2a53f112e --- /dev/null +++ b/api/src/test/java/io/kafbat/ui/service/rbac/RbacEditTopicTest.java @@ -0,0 +1,135 @@ +package io.kafbat.ui.service.rbac; + +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 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.Subject; +import io.kafbat.ui.model.rbac.permission.TopicAction; +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.annotation.DirtiesContext; +import org.springframework.test.util.ReflectionTestUtils; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +/** + * Test cases for issue #260. + * The role has permissions to delete messages from any topic + */ +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +class RbacEditTopicTest extends AbstractIntegrationTest { + + public static final String ROLE_NAME = "role-name-ro"; + public static final String CLUSTER_NAME = "dev"; + + @Autowired + AccessControlService accessControlService; + + @Mock + SecurityContext securityContext; + + @Mock + Authentication authentication; + + @Mock + RbacUser user; + + @BeforeEach + void setUp() { + // Mock roles + List roles = List.of( + getEditTopicTestRole() + ); + 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(ROLE_NAME)); + AccessContext context = AccessContext.builder() + .cluster(CLUSTER_NAME) + .topicActions("inventorytopic", TopicAction.MESSAGES_DELETE) + .build(); + Mono validateAccessMono = accessControlService.validateAccess(context); + StepVerifier.create(validateAccessMono) + .expectComplete() + .verify(); + }); + } + + @Test + void validateAccess_deniedRole() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of("anotherRole")); + AccessContext context = AccessContext.builder() + .cluster(CLUSTER_NAME) + .topicActions("inventorytopic", TopicAction.MESSAGES_DELETE) + .build(); + Mono validateAccessMono = accessControlService.validateAccess(context); + StepVerifier.create(validateAccessMono) + .expectErrorMatches(e -> e instanceof AccessDeniedException) + .verify(); + }); + } + + public static Role getEditTopicTestRole() { + Role role = new Role(); + role.setName(ROLE_NAME); + role.setClusters(List.of(CLUSTER_NAME)); + Subject sub = new Subject(); + sub.setType("group"); + sub.setProvider("ldap"); + sub.setValue("kafbat.group"); + role.setSubjects(List.of(sub)); + Permission topicPermissionTestPrefix = new Permission(); + topicPermissionTestPrefix.setResource(Resource.TOPIC.name()); + topicPermissionTestPrefix.setActions(List.of( + TopicAction.VIEW.name(), + TopicAction.MESSAGES_READ.name(), + TopicAction.MESSAGES_DELETE.name() + )); + topicPermissionTestPrefix.setValue(".*"); + + List permissions = List.of( + topicPermissionTestPrefix + ); + role.setPermissions(permissions); + role.validate(); + return role; + } + +} diff --git a/api/src/test/java/io/kafbat/ui/service/rbac/RbacTopicCreationTest.java b/api/src/test/java/io/kafbat/ui/service/rbac/RbacTopicCreationTest.java new file mode 100644 index 000000000..c370cbf66 --- /dev/null +++ b/api/src/test/java/io/kafbat/ui/service/rbac/RbacTopicCreationTest.java @@ -0,0 +1,155 @@ +package io.kafbat.ui.service.rbac; + +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 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.Subject; +import io.kafbat.ui.model.rbac.permission.TopicAction; +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.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.util.ReflectionTestUtils; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +/** + * Test cases for issues #76. + * User is allowed to create topic based only on pattern "test-.*" + */ +@ActiveProfiles("topiccreation") +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) +class RbacTopicCreationTest extends AbstractIntegrationTest { + + public static final String ROLE_NAME = "dev_role"; + public static final String CLUSTER_NAME = "dev"; + @Autowired + AccessControlService accessControlService; + + @Mock + SecurityContext securityContext; + + @Mock + Authentication authentication; + + @Mock + RbacUser user; + + @BeforeEach + void setUp() { + // Mock roles + List roles = List.of( + getRole() + ); + 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(); + } + } + + /** + * Create a "test-" prefixed topic, allowed. + */ + @Test + void validateAccess() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(ROLE_NAME)); + AccessContext context = AccessContext.builder() + .cluster(CLUSTER_NAME) + .topicActions("test-topic", TopicAction.CREATE) + .build(); + Mono validateAccessMono = accessControlService.validateAccess(context); + StepVerifier.create(validateAccessMono) + .expectComplete() + .verify(); + }); + } + + /** + * Create a not "test-" prefixed topic, not allowed. + */ + @Test + void validateAccess_accessDenied() { + withSecurityContext(() -> { + when(user.groups()).thenReturn(List.of(ROLE_NAME)); + AccessContext context = AccessContext.builder() + .cluster(CLUSTER_NAME) + .topicActions("another-topic", TopicAction.CREATE) + .build(); + Mono validateAccessMono = accessControlService.validateAccess(context); + StepVerifier.create(validateAccessMono) + .expectErrorMatches(e -> e instanceof AccessDeniedException) + .verify(); + }); + } + + public static Role getRole() { + Role role = new Role(); + role.setName(ROLE_NAME); + role.setClusters(List.of(CLUSTER_NAME)); + Subject sub = new Subject(); + sub.setType("group"); + sub.setProvider("ldap"); + sub.setValue("kafbat.group"); + role.setSubjects(List.of(sub)); + Permission testTopicsPermission = new Permission(); + testTopicsPermission.setResource(Resource.TOPIC.name()); + testTopicsPermission.setActions(List.of( + TopicAction.VIEW.name(), + TopicAction.CREATE.name(), + TopicAction.EDIT.name(), + TopicAction.DELETE.name(), + TopicAction.MESSAGES_READ.name(), + TopicAction.MESSAGES_PRODUCE.name() + )); + testTopicsPermission.setValue("test-.*"); + + Permission notPrefixedTopicPermission = new Permission(); + notPrefixedTopicPermission.setResource(Resource.TOPIC.name()); + notPrefixedTopicPermission.setActions(List.of( + TopicAction.VIEW.name(), + TopicAction.MESSAGES_READ.name(), + TopicAction.MESSAGES_PRODUCE.name() + )); + notPrefixedTopicPermission.setValue("^(?!test-).*"); + + List permissions = List.of( + testTopicsPermission, + notPrefixedTopicPermission + ); + role.setPermissions(permissions); + role.validate(); + return role; + } + +}