From 47a3780dba064028dd34a5cfca4e74f20dd1dea6 Mon Sep 17 00:00:00 2001 From: Ilya Kuramshin Date: Sun, 18 Feb 2024 10:56:08 +0400 Subject: [PATCH] BE: RBAC classes refactoring (#116) Co-authored-by: iliax --- .../ui/controller/AccessController.java | 6 +- .../controller/ConsumerGroupsController.java | 14 +- .../ui/controller/KafkaConnectController.java | 33 +- .../ui/controller/MessagesController.java | 12 +- .../ui/controller/SchemasController.java | 64 ++- .../ui/controller/TopicsController.java | 38 +- .../kafbat/ui/model/rbac/AccessContext.java | 177 ++++----- .../io/kafbat/ui/model/rbac/Permission.java | 56 +-- .../io/kafbat/ui/model/rbac/Resource.java | 62 ++- .../ui/model/rbac/permission/AclAction.java | 13 +- .../permission/ApplicationConfigAction.java | 13 +- .../ui/model/rbac/permission/AuditAction.java | 11 + .../rbac/permission/ClusterConfigAction.java | 13 +- .../model/rbac/permission/ConnectAction.java | 19 +- .../rbac/permission/ConsumerGroupAction.java | 15 +- .../ui/model/rbac/permission/KsqlAction.java | 11 + .../rbac/permission/PermissibleAction.java | 14 + .../model/rbac/permission/SchemaAction.java | 17 +- .../ui/model/rbac/permission/TopicAction.java | 23 +- .../kafbat/ui/service/audit/AuditRecord.java | 44 +- .../kafbat/ui/service/audit/AuditService.java | 9 +- .../kafbat/ui/service/audit/AuditWriter.java | 6 +- .../ui/service/rbac/AccessControlService.java | 375 +++--------------- .../ui/model/rbac/AccessContextTest.java | 112 ++++++ .../kafbat/ui/model/rbac/PermissionTest.java | 52 +++ .../ui/service/audit/AuditWriterTest.java | 16 +- 26 files changed, 571 insertions(+), 654 deletions(-) create mode 100644 api/src/test/java/io/kafbat/ui/model/rbac/AccessContextTest.java create mode 100644 api/src/test/java/io/kafbat/ui/model/rbac/PermissionTest.java diff --git a/api/src/main/java/io/kafbat/ui/controller/AccessController.java b/api/src/main/java/io/kafbat/ui/controller/AccessController.java index 0ac149275..5833f2e3c 100644 --- a/api/src/main/java/io/kafbat/ui/controller/AccessController.java +++ b/api/src/main/java/io/kafbat/ui/controller/AccessController.java @@ -31,7 +31,7 @@ public class AccessController implements AuthorizationApi { private final AccessControlService accessControlService; public Mono> getUserAuthInfo(ServerWebExchange exchange) { - Mono> permissions = accessControlService.getUser() + Mono> permissions = AccessControlService.getUser() .map(user -> accessControlService.getRoles() .stream() .filter(role -> user.groups().contains(role.getName())) @@ -64,9 +64,9 @@ private List mapPermissions(List permissions, Lis dto.setClusters(clusters); dto.setResource(ResourceTypeDTO.fromValue(permission.getResource().toString().toUpperCase())); dto.setValue(permission.getValue()); - dto.setActions(permission.getActions() + dto.setActions(permission.getParsedActions() .stream() - .map(String::toUpperCase) + .map(p -> p.name().toUpperCase()) .map(this::mapAction) .filter(Objects::nonNull) .toList()); diff --git a/api/src/main/java/io/kafbat/ui/controller/ConsumerGroupsController.java b/api/src/main/java/io/kafbat/ui/controller/ConsumerGroupsController.java index e582eddc8..248c6f7d7 100644 --- a/api/src/main/java/io/kafbat/ui/controller/ConsumerGroupsController.java +++ b/api/src/main/java/io/kafbat/ui/controller/ConsumerGroupsController.java @@ -49,8 +49,7 @@ public Mono> deleteConsumerGroup(String clusterName, ServerWebExchange exchange) { var context = AccessContext.builder() .cluster(clusterName) - .consumerGroup(id) - .consumerGroupActions(DELETE) + .consumerGroupActions(id, DELETE) .operationName("deleteConsumerGroup") .build(); @@ -66,8 +65,7 @@ public Mono> getConsumerGroup(String clu ServerWebExchange exchange) { var context = AccessContext.builder() .cluster(clusterName) - .consumerGroup(consumerGroupId) - .consumerGroupActions(VIEW) + .consumerGroupActions(consumerGroupId, VIEW) .operationName("getConsumerGroup") .build(); @@ -84,8 +82,7 @@ public Mono>> getTopicConsumerGroups(Strin ServerWebExchange exchange) { var context = AccessContext.builder() .cluster(clusterName) - .topic(topicName) - .topicActions(TopicAction.VIEW) + .topicActions(topicName, TopicAction.VIEW) .operationName("getTopicConsumerGroups") .build(); @@ -142,9 +139,8 @@ public Mono> resetConsumerGroupOffsets(String clusterName, return resetDto.flatMap(reset -> { var context = AccessContext.builder() .cluster(clusterName) - .topic(reset.getTopic()) - .topicActions(TopicAction.VIEW) - .consumerGroupActions(RESET_OFFSETS) + .topicActions(reset.getTopic(), TopicAction.VIEW) + .consumerGroupActions(group, RESET_OFFSETS) .operationName("resetConsumerGroupOffsets") .build(); diff --git a/api/src/main/java/io/kafbat/ui/controller/KafkaConnectController.java b/api/src/main/java/io/kafbat/ui/controller/KafkaConnectController.java index e648b9ae3..08eb304c0 100644 --- a/api/src/main/java/io/kafbat/ui/controller/KafkaConnectController.java +++ b/api/src/main/java/io/kafbat/ui/controller/KafkaConnectController.java @@ -56,8 +56,7 @@ public Mono>> getConnectors(String clusterName, Stri var context = AccessContext.builder() .cluster(clusterName) - .connect(connectName) - .connectActions(ConnectAction.VIEW) + .connectActions(connectName, ConnectAction.VIEW) .operationName("getConnectors") .build(); @@ -73,8 +72,7 @@ public Mono> createConnector(String clusterName, St var context = AccessContext.builder() .cluster(clusterName) - .connect(connectName) - .connectActions(ConnectAction.VIEW, ConnectAction.CREATE) + .connectActions(connectName, ConnectAction.CREATE) .operationName("createConnector") .build(); @@ -91,9 +89,7 @@ public Mono> getConnector(String clusterName, Strin var context = AccessContext.builder() .cluster(clusterName) - .connect(connectName) - .connectActions(ConnectAction.VIEW) - .connector(connectorName) + .connectActions(connectName, ConnectAction.VIEW) .operationName("getConnector") .build(); @@ -110,8 +106,7 @@ public Mono> deleteConnector(String clusterName, String con var context = AccessContext.builder() .cluster(clusterName) - .connect(connectName) - .connectActions(ConnectAction.DELETE) + .connectActions(connectName, ConnectAction.DELETE) .operationName("deleteConnector") .operationParams(Map.of(CONNECTOR_NAME, connectName)) .build(); @@ -133,7 +128,6 @@ public Mono>> getAllConnectors( ) { var context = AccessContext.builder() .cluster(clusterName) - .connectActions(ConnectAction.VIEW, ConnectAction.EDIT) .operationName("getAllConnectors") .build(); @@ -143,7 +137,6 @@ public Mono>> getAllConnectors( Flux job = kafkaConnectService.getAllConnectors(getCluster(clusterName), search) .filterWhen(dto -> accessControlService.isConnectAccessible(dto.getConnect(), clusterName)) - .filterWhen(dto -> accessControlService.isConnectorAccessible(dto.getConnect(), dto.getName(), clusterName)) .sort(comparator); return Mono.just(ResponseEntity.ok(job)) @@ -158,8 +151,7 @@ public Mono>> getConnectorConfig(String clust var context = AccessContext.builder() .cluster(clusterName) - .connect(connectName) - .connectActions(ConnectAction.VIEW) + .connectActions(connectName, ConnectAction.VIEW) .operationName("getConnectorConfig") .build(); @@ -178,8 +170,7 @@ public Mono> setConnectorConfig(String clusterName, var context = AccessContext.builder() .cluster(clusterName) - .connect(connectName) - .connectActions(ConnectAction.VIEW, ConnectAction.EDIT) + .connectActions(connectName, ConnectAction.VIEW, ConnectAction.EDIT) .operationName("setConnectorConfig") .operationParams(Map.of(CONNECTOR_NAME, connectorName)) .build(); @@ -205,8 +196,7 @@ public Mono> updateConnectorState(String clusterName, Strin var context = AccessContext.builder() .cluster(clusterName) - .connect(connectName) - .connectActions(connectActions) + .connectActions(connectName, connectActions) .operationName("updateConnectorState") .operationParams(Map.of(CONNECTOR_NAME, connectorName)) .build(); @@ -225,8 +215,7 @@ public Mono>> getConnectorTasks(String clusterName, ServerWebExchange exchange) { var context = AccessContext.builder() .cluster(clusterName) - .connect(connectName) - .connectActions(ConnectAction.VIEW) + .connectActions(connectName, ConnectAction.VIEW) .operationName("getConnectorTasks") .operationParams(Map.of(CONNECTOR_NAME, connectorName)) .build(); @@ -245,8 +234,7 @@ public Mono> restartConnectorTask(String clusterName, Strin var context = AccessContext.builder() .cluster(clusterName) - .connect(connectName) - .connectActions(ConnectAction.VIEW, ConnectAction.RESTART) + .connectActions(connectName, ConnectAction.VIEW, ConnectAction.RESTART) .operationName("restartConnectorTask") .operationParams(Map.of(CONNECTOR_NAME, connectorName)) .build(); @@ -264,8 +252,7 @@ public Mono>> getConnectorPlugins( var context = AccessContext.builder() .cluster(clusterName) - .connect(connectName) - .connectActions(ConnectAction.VIEW) + .connectActions(connectName, ConnectAction.VIEW) .operationName("getConnectorPlugins") .build(); diff --git a/api/src/main/java/io/kafbat/ui/controller/MessagesController.java b/api/src/main/java/io/kafbat/ui/controller/MessagesController.java index 3cf866447..978d137b3 100644 --- a/api/src/main/java/io/kafbat/ui/controller/MessagesController.java +++ b/api/src/main/java/io/kafbat/ui/controller/MessagesController.java @@ -54,8 +54,7 @@ public Mono> deleteTopicMessages( var context = AccessContext.builder() .cluster(clusterName) - .topic(topicName) - .topicActions(MESSAGES_DELETE) + .topicActions(topicName, MESSAGES_DELETE) .build(); return validateAccess(context).>then( @@ -89,8 +88,7 @@ public Mono>> getTopicMessages(String ServerWebExchange exchange) { var contextBuilder = AccessContext.builder() .cluster(clusterName) - .topic(topicName) - .topicActions(MESSAGES_READ) + .topicActions(topicName, MESSAGES_READ) .operationName("getTopicMessages"); if (auditService.isAuditTopic(getCluster(clusterName), topicName)) { @@ -127,8 +125,7 @@ public Mono> sendTopicMessages( var context = AccessContext.builder() .cluster(clusterName) - .topic(topicName) - .topicActions(MESSAGES_PRODUCE) + .topicActions(topicName, MESSAGES_PRODUCE) .operationName("sendTopicMessages") .build(); @@ -174,8 +171,7 @@ public Mono> getSerdes(String clusterNam ServerWebExchange exchange) { var context = AccessContext.builder() .cluster(clusterName) - .topic(topicName) - .topicActions(TopicAction.VIEW) + .topicActions(topicName, TopicAction.VIEW) .operationName("getSerdes") .build(); diff --git a/api/src/main/java/io/kafbat/ui/controller/SchemasController.java b/api/src/main/java/io/kafbat/ui/controller/SchemasController.java index d2c126ca8..079ac674c 100644 --- a/api/src/main/java/io/kafbat/ui/controller/SchemasController.java +++ b/api/src/main/java/io/kafbat/ui/controller/SchemasController.java @@ -51,8 +51,7 @@ public Mono> checkSchemaCompatibil ServerWebExchange exchange) { var context = AccessContext.builder() .cluster(clusterName) - .schema(subject) - .schemaActions(SchemaAction.VIEW) + .schemaActions(subject, SchemaAction.VIEW) .operationName("checkSchemaCompatibility") .build(); @@ -72,22 +71,23 @@ public Mono> checkSchemaCompatibil public Mono> createNewSchema( String clusterName, @Valid Mono newSchemaSubjectMono, ServerWebExchange exchange) { - var context = AccessContext.builder() - .cluster(clusterName) - .schemaActions(SchemaAction.CREATE) - .operationName("createNewSchema") - .build(); - - return validateAccess(context).then( - newSchemaSubjectMono.flatMap(newSubject -> - schemaRegistryService.registerNewSchema( - getCluster(clusterName), - newSubject.getSubject(), - kafkaSrMapper.fromDto(newSubject) - ) - ).map(kafkaSrMapper::toDto) - .map(ResponseEntity::ok) - ).doOnEach(sig -> audit(context, sig)); + return newSchemaSubjectMono.flatMap(newSubject -> { + var context = AccessContext.builder() + .cluster(clusterName) + .schemaActions(newSubject.getSubject(), SchemaAction.CREATE) + .operationName("createNewSchema") + .build(); + return validateAccess(context).then( + schemaRegistryService.registerNewSchema( + getCluster(clusterName), + newSubject.getSubject(), + kafkaSrMapper.fromDto(newSubject) + )) + .map(kafkaSrMapper::toDto) + .map(ResponseEntity::ok) + .doOnEach(sig -> audit(context, sig)); + } + ); } @Override @@ -95,8 +95,7 @@ public Mono> deleteLatestSchema( String clusterName, String subject, ServerWebExchange exchange) { var context = AccessContext.builder() .cluster(clusterName) - .schema(subject) - .schemaActions(SchemaAction.DELETE) + .schemaActions(subject, SchemaAction.DELETE) .operationName("deleteLatestSchema") .build(); @@ -112,8 +111,7 @@ public Mono> deleteSchema( String clusterName, String subject, ServerWebExchange exchange) { var context = AccessContext.builder() .cluster(clusterName) - .schema(subject) - .schemaActions(SchemaAction.DELETE) + .schemaActions(subject, SchemaAction.DELETE) .operationName("deleteSchema") .build(); @@ -129,8 +127,7 @@ public Mono> deleteSchemaByVersion( String clusterName, String subjectName, Integer version, ServerWebExchange exchange) { var context = AccessContext.builder() .cluster(clusterName) - .schema(subjectName) - .schemaActions(SchemaAction.DELETE) + .schemaActions(subjectName, SchemaAction.DELETE) .operationName("deleteSchemaByVersion") .build(); @@ -146,8 +143,7 @@ public Mono>> getAllVersionsBySubject( String clusterName, String subjectName, ServerWebExchange exchange) { var context = AccessContext.builder() .cluster(clusterName) - .schema(subjectName) - .schemaActions(SchemaAction.VIEW) + .schemaActions(subjectName, SchemaAction.VIEW) .operationName("getAllVersionsBySubject") .build(); @@ -175,8 +171,7 @@ public Mono> getLatestSchema(String clusterName ServerWebExchange exchange) { var context = AccessContext.builder() .cluster(clusterName) - .schema(subject) - .schemaActions(SchemaAction.VIEW) + .schemaActions(subject, SchemaAction.VIEW) .operationName("getLatestSchema") .build(); @@ -192,8 +187,7 @@ public Mono> getSchemaByVersion( String clusterName, String subject, Integer version, ServerWebExchange exchange) { var context = AccessContext.builder() .cluster(clusterName) - .schema(subject) - .schemaActions(SchemaAction.VIEW) + .schemaActions(subject, SchemaAction.VIEW) .operationName("getSchemaByVersion") .operationParams(Map.of("subject", subject, "version", version)) .build(); @@ -248,7 +242,7 @@ public Mono> updateGlobalSchemaCompatibilityLevel( ServerWebExchange exchange) { var context = AccessContext.builder() .cluster(clusterName) - .schemaActions(SchemaAction.MODIFY_GLOBAL_COMPATIBILITY) + .schemaGlobalCompatChange() .operationName("updateGlobalSchemaCompatibilityLevel") .build(); @@ -268,16 +262,16 @@ public Mono> updateGlobalSchemaCompatibilityLevel( public Mono> updateSchemaCompatibilityLevel( String clusterName, String subject, @Valid Mono compatibilityLevelMono, ServerWebExchange exchange) { + var context = AccessContext.builder() .cluster(clusterName) - .schemaActions(SchemaAction.EDIT) + .schemaActions(subject, SchemaAction.EDIT) .operationName("updateSchemaCompatibilityLevel") .operationParams(Map.of("subject", subject)) .build(); - return validateAccess(context).then( - compatibilityLevelMono - .flatMap(compatibilityLevelDTO -> + return compatibilityLevelMono.flatMap(compatibilityLevelDTO -> + validateAccess(context).then( schemaRegistryService.updateSchemaCompatibility( getCluster(clusterName), subject, diff --git a/api/src/main/java/io/kafbat/ui/controller/TopicsController.java b/api/src/main/java/io/kafbat/ui/controller/TopicsController.java index 617cbc4c9..53e9fc8cd 100644 --- a/api/src/main/java/io/kafbat/ui/controller/TopicsController.java +++ b/api/src/main/java/io/kafbat/ui/controller/TopicsController.java @@ -59,7 +59,7 @@ public Mono> createTopic( return topicCreationMono.flatMap(topicCreation -> { var context = AccessContext.builder() .cluster(clusterName) - .topicActions(CREATE) + .topicActions(topicCreation.getName(), CREATE) .operationName("createTopic") .operationParams(topicCreation) .build(); @@ -78,8 +78,7 @@ public Mono> recreateTopic(String clusterName, String topicName, ServerWebExchange exchange) { var context = AccessContext.builder() .cluster(clusterName) - .topic(topicName) - .topicActions(VIEW, CREATE, DELETE) + .topicActions(topicName, VIEW, CREATE, DELETE) .operationName("recreateTopic") .build(); @@ -96,8 +95,7 @@ public Mono> cloneTopic( var context = AccessContext.builder() .cluster(clusterName) - .topic(topicName) - .topicActions(VIEW, CREATE) + .topicActions(topicName, VIEW, CREATE) .operationName("cloneTopic") .operationParams(Map.of("newTopicName", newTopicName)) .build(); @@ -115,8 +113,7 @@ public Mono> deleteTopic( var context = AccessContext.builder() .cluster(clusterName) - .topic(topicName) - .topicActions(DELETE) + .topicActions(topicName, DELETE) .operationName("deleteTopic") .build(); @@ -134,8 +131,7 @@ public Mono>> getTopicConfigs( var context = AccessContext.builder() .cluster(clusterName) - .topic(topicName) - .topicActions(VIEW) + .topicActions(topicName, VIEW) .operationName("getTopicConfigs") .build(); @@ -156,8 +152,7 @@ public Mono> getTopicDetails( var context = AccessContext.builder() .cluster(clusterName) - .topic(topicName) - .topicActions(VIEW) + .topicActions(topicName, VIEW) .operationName("getTopicDetails") .build(); @@ -222,8 +217,7 @@ public Mono> updateTopic( var context = AccessContext.builder() .cluster(clusterName) - .topic(topicName) - .topicActions(VIEW, EDIT) + .topicActions(topicName, VIEW, EDIT) .operationName("updateTopic") .build(); @@ -243,8 +237,7 @@ public Mono> increaseTopicPartitio var context = AccessContext.builder() .cluster(clusterName) - .topic(topicName) - .topicActions(VIEW, EDIT) + .topicActions(topicName, VIEW, EDIT) .build(); return validateAccess(context).then( @@ -262,8 +255,7 @@ public Mono> changeReplicatio var context = AccessContext.builder() .cluster(clusterName) - .topic(topicName) - .topicActions(VIEW, EDIT) + .topicActions(topicName, VIEW, EDIT) .operationName("changeReplicationFactor") .build(); @@ -280,8 +272,7 @@ public Mono> analyzeTopic(String clusterName, String topicN var context = AccessContext.builder() .cluster(clusterName) - .topic(topicName) - .topicActions(MESSAGES_READ) + .topicActions(topicName, MESSAGES_READ) .operationName("analyzeTopic") .build(); @@ -297,8 +288,7 @@ public Mono> cancelTopicAnalysis(String clusterName, String ServerWebExchange exchange) { var context = AccessContext.builder() .cluster(clusterName) - .topic(topicName) - .topicActions(MESSAGES_READ) + .topicActions(topicName, MESSAGES_READ) .operationName("cancelTopicAnalysis") .build(); @@ -316,8 +306,7 @@ public Mono> getTopicAnalysis(String clusterNam var context = AccessContext.builder() .cluster(clusterName) - .topic(topicName) - .topicActions(MESSAGES_READ) + .topicActions(topicName, MESSAGES_READ) .operationName("getTopicAnalysis") .build(); @@ -334,8 +323,7 @@ public Mono>> getActiveProducerStates ServerWebExchange exchange) { var context = AccessContext.builder() .cluster(clusterName) - .topic(topicName) - .topicActions(VIEW) + .topicActions(topicName, VIEW) .operationName("getActiveProducerStates") .build(); diff --git a/api/src/main/java/io/kafbat/ui/model/rbac/AccessContext.java b/api/src/main/java/io/kafbat/ui/model/rbac/AccessContext.java index 02d3179c3..d15c4f1d7 100644 --- a/api/src/main/java/io/kafbat/ui/model/rbac/AccessContext.java +++ b/api/src/main/java/io/kafbat/ui/model/rbac/AccessContext.java @@ -1,5 +1,6 @@ package io.kafbat.ui.model.rbac; +import com.google.common.base.Preconditions; import io.kafbat.ui.model.rbac.permission.AclAction; import io.kafbat.ui.model.rbac.permission.ApplicationConfigAction; import io.kafbat.ui.model.rbac.permission.AuditAction; @@ -7,156 +8,144 @@ import io.kafbat.ui.model.rbac.permission.ConnectAction; import io.kafbat.ui.model.rbac.permission.ConsumerGroupAction; import io.kafbat.ui.model.rbac.permission.KsqlAction; +import io.kafbat.ui.model.rbac.permission.PermissibleAction; import io.kafbat.ui.model.rbac.permission.SchemaAction; import io.kafbat.ui.model.rbac.permission.TopicAction; +import jakarta.annotation.Nullable; +import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.List; -import java.util.Map; -import lombok.Value; -import org.springframework.util.Assert; +import java.util.stream.Collectors; +import org.springframework.security.access.AccessDeniedException; -@Value -public class AccessContext { +public record AccessContext(String cluster, + List accessedResources, + String operationName, + @Nullable Object operationParams) { - Collection applicationConfigActions; + public interface ResourceAccess { + // will be used for audit, should be serializable via json object mapper + @Nullable + Object resourceId(); - String cluster; - Collection clusterConfigActions; + Resource resourceType(); - String topic; - Collection topicActions; + Collection requestedActions(); - String consumerGroup; - Collection consumerGroupActions; - - String connect; - Collection connectActions; + boolean isAccessible(List userPermissions); + } - String connector; + record SingleResourceAccess(@Nullable String name, + Resource resourceType, + Collection requestedActions) implements ResourceAccess { - String schema; - Collection schemaActions; + SingleResourceAccess(@Nullable String name, + Resource resourceType, + Collection requestedActions) { + Preconditions.checkArgument(!requestedActions.isEmpty(), "actions not present"); + this.name = name; + this.resourceType = resourceType; + this.requestedActions = requestedActions; + } - Collection ksqlActions; + SingleResourceAccess(Resource type, List requestedActions) { + this(null, type, requestedActions); + } - Collection aclActions; + @Override + public Object resourceId() { + return name; + } - Collection auditAction; + @Override + public boolean isAccessible(List userPermissions) throws AccessDeniedException { + var allowedActions = userPermissions.stream() + .filter(permission -> permission.getResource() == resourceType) + .filter(permission -> { + if (name == null && permission.getCompiledValuePattern() == null) { + return true; + } + Preconditions.checkState(permission.getCompiledValuePattern() != null && name != null); + return permission.getCompiledValuePattern().matcher(name).matches(); + }) + .flatMap(p -> p.getParsedActions().stream()) + .collect(Collectors.toSet()); - String operationName; - Object operationParams; + return allowedActions.containsAll(requestedActions); + } + } public static AccessContextBuilder builder() { return new AccessContextBuilder(); } + public boolean isAccessible(List userPermissions) { + return accessedResources().stream() + .allMatch(resourceAccess -> resourceAccess.isAccessible(userPermissions)); + } + public static final class AccessContextBuilder { - private static final String ACTIONS_NOT_PRESENT = "actions not present"; - private Collection applicationConfigActions = Collections.emptySet(); private String cluster; - private Collection clusterConfigActions = Collections.emptySet(); - private String topic; - private Collection topicActions = Collections.emptySet(); - private String consumerGroup; - private Collection consumerGroupActions = Collections.emptySet(); - private String connect; - private Collection connectActions = Collections.emptySet(); - private String connector; - private String schema; - private Collection schemaActions = Collections.emptySet(); - private Collection ksqlActions = Collections.emptySet(); - private Collection aclActions = Collections.emptySet(); - private Collection auditActions = Collections.emptySet(); - private String operationName; private Object operationParams; + private final List accessedResources = new ArrayList<>(); private AccessContextBuilder() { } - public AccessContextBuilder applicationConfigActions(ApplicationConfigAction... actions) { - Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT); - this.applicationConfigActions = List.of(actions); - return this; - } - public AccessContextBuilder cluster(String cluster) { this.cluster = cluster; return this; } - public AccessContextBuilder clusterConfigActions(ClusterConfigAction... actions) { - Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT); - this.clusterConfigActions = List.of(actions); - return this; - } - - public AccessContextBuilder topic(String topic) { - this.topic = topic; - return this; - } - - public AccessContextBuilder topicActions(TopicAction... actions) { - Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT); - this.topicActions = List.of(actions); - return this; - } - - public AccessContextBuilder consumerGroup(String consumerGroup) { - this.consumerGroup = consumerGroup; + public AccessContextBuilder applicationConfigActions(ApplicationConfigAction... actions) { + accessedResources.add(new SingleResourceAccess(Resource.APPLICATIONCONFIG, List.of(actions))); return this; } - public AccessContextBuilder consumerGroupActions(ConsumerGroupAction... actions) { - Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT); - this.consumerGroupActions = List.of(actions); + public AccessContextBuilder clusterConfigActions(ClusterConfigAction... actions) { + accessedResources.add(new SingleResourceAccess(Resource.CLUSTERCONFIG, List.of(actions))); return this; } - public AccessContextBuilder connect(String connect) { - this.connect = connect; + public AccessContextBuilder topicActions(String topic, TopicAction... actions) { + accessedResources.add(new SingleResourceAccess(topic, Resource.TOPIC, List.of(actions))); return this; } - public AccessContextBuilder connectActions(ConnectAction... actions) { - Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT); - this.connectActions = List.of(actions); + public AccessContextBuilder consumerGroupActions(String consumerGroup, ConsumerGroupAction... actions) { + accessedResources.add(new SingleResourceAccess(consumerGroup, Resource.CONSUMER, List.of(actions))); return this; } - public AccessContextBuilder connector(String connector) { - this.connector = connector; + public AccessContextBuilder connectActions(String connect, ConnectAction... actions) { + accessedResources.add(new SingleResourceAccess(connect, Resource.CONNECT, List.of(actions))); return this; } - public AccessContextBuilder schema(String schema) { - this.schema = schema; + public AccessContextBuilder schemaActions(String schema, SchemaAction... actions) { + accessedResources.add(new SingleResourceAccess(schema, Resource.SCHEMA, List.of(actions))); return this; } - public AccessContextBuilder schemaActions(SchemaAction... actions) { - Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT); - this.schemaActions = List.of(actions); + public AccessContextBuilder schemaGlobalCompatChange() { + accessedResources.add(new SingleResourceAccess(Resource.SCHEMA, List.of(SchemaAction.MODIFY_GLOBAL_COMPATIBILITY))); return this; } public AccessContextBuilder ksqlActions(KsqlAction... actions) { - Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT); - this.ksqlActions = List.of(actions); + accessedResources.add(new SingleResourceAccess(Resource.KSQL, List.of(actions))); return this; } public AccessContextBuilder aclActions(AclAction... actions) { - Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT); - this.aclActions = List.of(actions); + accessedResources.add(new SingleResourceAccess(Resource.ACL, List.of(actions))); return this; } public AccessContextBuilder auditActions(AuditAction... actions) { - Assert.isTrue(actions.length > 0, ACTIONS_NOT_PRESENT); - this.auditActions = List.of(actions); + accessedResources.add(new SingleResourceAccess(Resource.AUDIT, List.of(actions))); return this; } @@ -170,22 +159,8 @@ public AccessContextBuilder operationParams(Object operationParams) { return this; } - public AccessContextBuilder operationParams(Map paramsMap) { - this.operationParams = paramsMap; - return this; - } - public AccessContext build() { - return new AccessContext( - applicationConfigActions, - cluster, clusterConfigActions, - topic, topicActions, - consumerGroup, consumerGroupActions, - connect, connectActions, - connector, - schema, schemaActions, - ksqlActions, aclActions, auditActions, - operationName, operationParams); + return new AccessContext(cluster, accessedResources, operationName, operationParams); } } } diff --git a/api/src/main/java/io/kafbat/ui/model/rbac/Permission.java b/api/src/main/java/io/kafbat/ui/model/rbac/Permission.java index be5e68b9e..42b043ae8 100644 --- a/api/src/main/java/io/kafbat/ui/model/rbac/Permission.java +++ b/api/src/main/java/io/kafbat/ui/model/rbac/Permission.java @@ -1,41 +1,25 @@ package io.kafbat.ui.model.rbac; -import static io.kafbat.ui.model.rbac.Resource.ACL; -import static io.kafbat.ui.model.rbac.Resource.APPLICATIONCONFIG; -import static io.kafbat.ui.model.rbac.Resource.AUDIT; -import static io.kafbat.ui.model.rbac.Resource.CLUSTERCONFIG; -import static io.kafbat.ui.model.rbac.Resource.KSQL; +import static org.apache.commons.collections.CollectionUtils.isNotEmpty; -import io.kafbat.ui.model.rbac.permission.AclAction; -import io.kafbat.ui.model.rbac.permission.ApplicationConfigAction; -import io.kafbat.ui.model.rbac.permission.AuditAction; -import io.kafbat.ui.model.rbac.permission.ClusterConfigAction; -import io.kafbat.ui.model.rbac.permission.ConnectAction; -import io.kafbat.ui.model.rbac.permission.ConsumerGroupAction; -import io.kafbat.ui.model.rbac.permission.KsqlAction; -import io.kafbat.ui.model.rbac.permission.SchemaAction; -import io.kafbat.ui.model.rbac.permission.TopicAction; -import java.util.Arrays; -import java.util.Collections; +import com.google.common.base.Preconditions; +import io.kafbat.ui.model.rbac.permission.PermissibleAction; import java.util.List; import java.util.regex.Pattern; import javax.annotation.Nullable; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; -import org.apache.commons.collections.CollectionUtils; -import org.springframework.util.Assert; @Getter @ToString @EqualsAndHashCode public class Permission { - private static final List RBAC_ACTION_EXEMPT_LIST = - List.of(KSQL, CLUSTERCONFIG, APPLICATIONCONFIG, ACL, AUDIT); - Resource resource; + List actions; + transient List parsedActions; //includes all dependant actions @Nullable String value; @@ -58,37 +42,19 @@ public void setActions(List actions) { } public void validate() { - Assert.notNull(resource, "resource cannot be null"); - if (!RBAC_ACTION_EXEMPT_LIST.contains(this.resource)) { - Assert.notNull(value, "permission value can't be empty for resource " + resource); - } + Preconditions.checkNotNull(resource, "resource cannot be null"); + Preconditions.checkArgument(isNotEmpty(actions), "Actions list for %s can't be null or empty", resource); } public void transform() { if (value != null) { this.compiledValuePattern = Pattern.compile(value); } - if (CollectionUtils.isNotEmpty(actions) && actions.stream().anyMatch("ALL"::equalsIgnoreCase)) { - this.actions = getAllActionValues(); + if (actions.stream().anyMatch("ALL"::equalsIgnoreCase)) { + this.parsedActions = resource.allActions(); + } else { + this.parsedActions = resource.parseActionsWithDependantsUnnest(actions); } } - private List getAllActionValues() { - if (resource == null) { - return Collections.emptyList(); - } - - return switch (this.resource) { - case APPLICATIONCONFIG -> Arrays.stream(ApplicationConfigAction.values()).map(Enum::toString).toList(); - case CLUSTERCONFIG -> Arrays.stream(ClusterConfigAction.values()).map(Enum::toString).toList(); - case TOPIC -> Arrays.stream(TopicAction.values()).map(Enum::toString).toList(); - case CONSUMER -> Arrays.stream(ConsumerGroupAction.values()).map(Enum::toString).toList(); - case SCHEMA -> Arrays.stream(SchemaAction.values()).map(Enum::toString).toList(); - case CONNECT -> Arrays.stream(ConnectAction.values()).map(Enum::toString).toList(); - case KSQL -> Arrays.stream(KsqlAction.values()).map(Enum::toString).toList(); - case ACL -> Arrays.stream(AclAction.values()).map(Enum::toString).toList(); - case AUDIT -> Arrays.stream(AuditAction.values()).map(Enum::toString).toList(); - }; - } - } diff --git a/api/src/main/java/io/kafbat/ui/model/rbac/Resource.java b/api/src/main/java/io/kafbat/ui/model/rbac/Resource.java index 7fab0d343..14294a45b 100644 --- a/api/src/main/java/io/kafbat/ui/model/rbac/Resource.java +++ b/api/src/main/java/io/kafbat/ui/model/rbac/Resource.java @@ -1,24 +1,66 @@ package io.kafbat.ui.model.rbac; +import io.kafbat.ui.model.rbac.permission.AclAction; +import io.kafbat.ui.model.rbac.permission.ApplicationConfigAction; +import io.kafbat.ui.model.rbac.permission.ClusterConfigAction; +import io.kafbat.ui.model.rbac.permission.ConnectAction; +import io.kafbat.ui.model.rbac.permission.ConsumerGroupAction; +import io.kafbat.ui.model.rbac.permission.KsqlAction; +import io.kafbat.ui.model.rbac.permission.PermissibleAction; +import io.kafbat.ui.model.rbac.permission.SchemaAction; +import io.kafbat.ui.model.rbac.permission.TopicAction; +import jakarta.annotation.Nullable; +import java.util.List; +import java.util.stream.Stream; import org.apache.commons.lang3.EnumUtils; -import org.jetbrains.annotations.Nullable; public enum Resource { - APPLICATIONCONFIG, - CLUSTERCONFIG, - TOPIC, - CONSUMER, - SCHEMA, - CONNECT, - KSQL, - ACL, - AUDIT; + APPLICATIONCONFIG(ApplicationConfigAction.values()), + + CLUSTERCONFIG(ClusterConfigAction.values()), + + TOPIC(TopicAction.values()), + + CONSUMER(ConsumerGroupAction.values()), + + SCHEMA(SchemaAction.values()), + + CONNECT(ConnectAction.values()), + + KSQL(KsqlAction.values()), + + ACL(AclAction.values()), + + AUDIT(AclAction.values()); + + private final List actions; + + Resource(PermissibleAction[] actions) { + this.actions = List.of(actions); + } + + public List allActions() { + return actions; + } @Nullable public static Resource fromString(String name) { return EnumUtils.getEnum(Resource.class, name); } + public List parseActionsWithDependantsUnnest(List actionsToParse) { + return actionsToParse.stream() + .map(toParse -> actions.stream() + .filter(a -> toParse.equalsIgnoreCase(a.name())) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException( + "'%s' actions not applicable for resource %s".formatted(toParse, name()))) + ) + // unnesting all dependant actions + .flatMap(a -> Stream.concat(Stream.of(a), a.unnestAllDependants())) + .distinct() + .toList(); + } } diff --git a/api/src/main/java/io/kafbat/ui/model/rbac/permission/AclAction.java b/api/src/main/java/io/kafbat/ui/model/rbac/permission/AclAction.java index f8b321842..c263136ad 100644 --- a/api/src/main/java/io/kafbat/ui/model/rbac/permission/AclAction.java +++ b/api/src/main/java/io/kafbat/ui/model/rbac/permission/AclAction.java @@ -7,12 +7,18 @@ public enum AclAction implements PermissibleAction { VIEW, - EDIT + EDIT(VIEW) ; public static final Set ALTER_ACTIONS = Set.of(EDIT); + private final PermissibleAction[] dependantActions; + + AclAction(AclAction... dependantActions) { + this.dependantActions = dependantActions; + } + @Nullable public static AclAction fromString(String name) { return EnumUtils.getEnum(AclAction.class, name); @@ -22,4 +28,9 @@ public static AclAction fromString(String name) { public boolean isAlter() { return ALTER_ACTIONS.contains(this); } + + @Override + public PermissibleAction[] dependantActions() { + return dependantActions; + } } diff --git a/api/src/main/java/io/kafbat/ui/model/rbac/permission/ApplicationConfigAction.java b/api/src/main/java/io/kafbat/ui/model/rbac/permission/ApplicationConfigAction.java index 4a585ba79..36c6b7281 100644 --- a/api/src/main/java/io/kafbat/ui/model/rbac/permission/ApplicationConfigAction.java +++ b/api/src/main/java/io/kafbat/ui/model/rbac/permission/ApplicationConfigAction.java @@ -7,12 +7,18 @@ public enum ApplicationConfigAction implements PermissibleAction { VIEW, - EDIT + EDIT(VIEW) ; public static final Set ALTER_ACTIONS = Set.of(EDIT); + private final PermissibleAction[] dependantActions; + + ApplicationConfigAction(ApplicationConfigAction... dependantActions) { + this.dependantActions = dependantActions; + } + @Nullable public static ApplicationConfigAction fromString(String name) { return EnumUtils.getEnum(ApplicationConfigAction.class, name); @@ -22,4 +28,9 @@ public static ApplicationConfigAction fromString(String name) { public boolean isAlter() { return ALTER_ACTIONS.contains(this); } + + @Override + public PermissibleAction[] dependantActions() { + return dependantActions; + } } diff --git a/api/src/main/java/io/kafbat/ui/model/rbac/permission/AuditAction.java b/api/src/main/java/io/kafbat/ui/model/rbac/permission/AuditAction.java index 1b7e42fe2..ed0c91c42 100644 --- a/api/src/main/java/io/kafbat/ui/model/rbac/permission/AuditAction.java +++ b/api/src/main/java/io/kafbat/ui/model/rbac/permission/AuditAction.java @@ -12,6 +12,12 @@ public enum AuditAction implements PermissibleAction { private static final Set ALTER_ACTIONS = Set.of(); + private final AclAction[] dependantActions; + + AuditAction(AclAction... dependantActions) { + this.dependantActions = dependantActions; + } + @Nullable public static AuditAction fromString(String name) { return EnumUtils.getEnum(AuditAction.class, name); @@ -21,4 +27,9 @@ public static AuditAction fromString(String name) { public boolean isAlter() { return ALTER_ACTIONS.contains(this); } + + @Override + public PermissibleAction[] dependantActions() { + return dependantActions; + } } diff --git a/api/src/main/java/io/kafbat/ui/model/rbac/permission/ClusterConfigAction.java b/api/src/main/java/io/kafbat/ui/model/rbac/permission/ClusterConfigAction.java index 2f997f542..e66aa68a6 100644 --- a/api/src/main/java/io/kafbat/ui/model/rbac/permission/ClusterConfigAction.java +++ b/api/src/main/java/io/kafbat/ui/model/rbac/permission/ClusterConfigAction.java @@ -7,12 +7,18 @@ public enum ClusterConfigAction implements PermissibleAction { VIEW, - EDIT + EDIT(VIEW) ; public static final Set ALTER_ACTIONS = Set.of(EDIT); + private final ClusterConfigAction[] dependantActions; + + ClusterConfigAction(ClusterConfigAction... dependantActions) { + this.dependantActions = dependantActions; + } + @Nullable public static ClusterConfigAction fromString(String name) { return EnumUtils.getEnum(ClusterConfigAction.class, name); @@ -22,4 +28,9 @@ public static ClusterConfigAction fromString(String name) { public boolean isAlter() { return ALTER_ACTIONS.contains(this); } + + @Override + public PermissibleAction[] dependantActions() { + return dependantActions; + } } diff --git a/api/src/main/java/io/kafbat/ui/model/rbac/permission/ConnectAction.java b/api/src/main/java/io/kafbat/ui/model/rbac/permission/ConnectAction.java index d13828007..7634e89c0 100644 --- a/api/src/main/java/io/kafbat/ui/model/rbac/permission/ConnectAction.java +++ b/api/src/main/java/io/kafbat/ui/model/rbac/permission/ConnectAction.java @@ -7,13 +7,19 @@ public enum ConnectAction implements PermissibleAction { VIEW, - EDIT, - CREATE, - DELETE, - RESTART + EDIT(VIEW), + CREATE(VIEW), + RESTART(VIEW), + DELETE(VIEW) ; + private final ConnectAction[] dependantActions; + + ConnectAction(ConnectAction... dependantActions) { + this.dependantActions = dependantActions; + } + public static final Set ALTER_ACTIONS = Set.of(CREATE, EDIT, DELETE, RESTART); @Nullable @@ -25,4 +31,9 @@ public static ConnectAction fromString(String name) { public boolean isAlter() { return ALTER_ACTIONS.contains(this); } + + @Override + public PermissibleAction[] dependantActions() { + return dependantActions; + } } diff --git a/api/src/main/java/io/kafbat/ui/model/rbac/permission/ConsumerGroupAction.java b/api/src/main/java/io/kafbat/ui/model/rbac/permission/ConsumerGroupAction.java index 3b88d8259..6a3e3aae9 100644 --- a/api/src/main/java/io/kafbat/ui/model/rbac/permission/ConsumerGroupAction.java +++ b/api/src/main/java/io/kafbat/ui/model/rbac/permission/ConsumerGroupAction.java @@ -7,13 +7,19 @@ public enum ConsumerGroupAction implements PermissibleAction { VIEW, - DELETE, - RESET_OFFSETS + DELETE(VIEW), + RESET_OFFSETS(VIEW) ; public static final Set ALTER_ACTIONS = Set.of(DELETE, RESET_OFFSETS); + private final ConsumerGroupAction[] dependantActions; + + ConsumerGroupAction(ConsumerGroupAction... dependantActions) { + this.dependantActions = dependantActions; + } + @Nullable public static ConsumerGroupAction fromString(String name) { return EnumUtils.getEnum(ConsumerGroupAction.class, name); @@ -23,4 +29,9 @@ public static ConsumerGroupAction fromString(String name) { public boolean isAlter() { return ALTER_ACTIONS.contains(this); } + + @Override + public PermissibleAction[] dependantActions() { + return dependantActions; + } } diff --git a/api/src/main/java/io/kafbat/ui/model/rbac/permission/KsqlAction.java b/api/src/main/java/io/kafbat/ui/model/rbac/permission/KsqlAction.java index 292d7d3a7..ef2540dca 100644 --- a/api/src/main/java/io/kafbat/ui/model/rbac/permission/KsqlAction.java +++ b/api/src/main/java/io/kafbat/ui/model/rbac/permission/KsqlAction.java @@ -12,6 +12,12 @@ public enum KsqlAction implements PermissibleAction { public static final Set ALTER_ACTIONS = Set.of(EXECUTE); + private final KsqlAction[] dependantActions; + + KsqlAction(KsqlAction... dependantActions) { + this.dependantActions = dependantActions; + } + @Nullable public static KsqlAction fromString(String name) { return EnumUtils.getEnum(KsqlAction.class, name); @@ -21,4 +27,9 @@ public static KsqlAction fromString(String name) { public boolean isAlter() { return ALTER_ACTIONS.contains(this); } + + @Override + public PermissibleAction[] dependantActions() { + return dependantActions; + } } diff --git a/api/src/main/java/io/kafbat/ui/model/rbac/permission/PermissibleAction.java b/api/src/main/java/io/kafbat/ui/model/rbac/permission/PermissibleAction.java index cb727fb2c..34b5d64d1 100644 --- a/api/src/main/java/io/kafbat/ui/model/rbac/permission/PermissibleAction.java +++ b/api/src/main/java/io/kafbat/ui/model/rbac/permission/PermissibleAction.java @@ -1,5 +1,7 @@ package io.kafbat.ui.model.rbac.permission; +import java.util.stream.Stream; + public sealed interface PermissibleAction permits AclAction, ApplicationConfigAction, ConsumerGroupAction, SchemaAction, @@ -10,4 +12,16 @@ public sealed interface PermissibleAction permits boolean isAlter(); + /** + * Actions that are direct parts (childs) of this action. If current action is allowed for user, then + * all dependant actions supposed to be allowed. Dependants can also have their dependants, that can be recursively + * unnested with `unnestAllDependants` method. + */ + PermissibleAction[] dependantActions(); + + // recursively unnest all action's dependants + default Stream unnestAllDependants() { + return Stream.of(dependantActions()).flatMap(dep -> Stream.concat(Stream.of(dep), dep.unnestAllDependants())); + } + } diff --git a/api/src/main/java/io/kafbat/ui/model/rbac/permission/SchemaAction.java b/api/src/main/java/io/kafbat/ui/model/rbac/permission/SchemaAction.java index b6047d206..54ffbcd17 100644 --- a/api/src/main/java/io/kafbat/ui/model/rbac/permission/SchemaAction.java +++ b/api/src/main/java/io/kafbat/ui/model/rbac/permission/SchemaAction.java @@ -7,15 +7,21 @@ public enum SchemaAction implements PermissibleAction { VIEW, - CREATE, - DELETE, - EDIT, + CREATE(VIEW), + DELETE(VIEW), + EDIT(VIEW), MODIFY_GLOBAL_COMPATIBILITY ; public static final Set ALTER_ACTIONS = Set.of(CREATE, DELETE, EDIT, MODIFY_GLOBAL_COMPATIBILITY); + private final SchemaAction[] dependantActions; + + SchemaAction(SchemaAction... dependantActions) { + this.dependantActions = dependantActions; + } + @Nullable public static SchemaAction fromString(String name) { return EnumUtils.getEnum(SchemaAction.class, name); @@ -25,4 +31,9 @@ public static SchemaAction fromString(String name) { public boolean isAlter() { return ALTER_ACTIONS.contains(this); } + + @Override + public PermissibleAction[] dependantActions() { + return dependantActions; + } } diff --git a/api/src/main/java/io/kafbat/ui/model/rbac/permission/TopicAction.java b/api/src/main/java/io/kafbat/ui/model/rbac/permission/TopicAction.java index e0f3bad32..8efbc6fe0 100644 --- a/api/src/main/java/io/kafbat/ui/model/rbac/permission/TopicAction.java +++ b/api/src/main/java/io/kafbat/ui/model/rbac/permission/TopicAction.java @@ -7,17 +7,23 @@ public enum TopicAction implements PermissibleAction { VIEW, - CREATE, - EDIT, - DELETE, - MESSAGES_READ, - MESSAGES_PRODUCE, - MESSAGES_DELETE, + CREATE(VIEW), + EDIT(VIEW), + DELETE(VIEW), + MESSAGES_READ(VIEW), + MESSAGES_PRODUCE(VIEW), + MESSAGES_DELETE(VIEW, EDIT), ; public static final Set ALTER_ACTIONS = Set.of(CREATE, EDIT, DELETE, MESSAGES_PRODUCE, MESSAGES_DELETE); + private final TopicAction[] dependantActions; + + TopicAction(TopicAction... dependantActions) { + this.dependantActions = dependantActions; + } + @Nullable public static TopicAction fromString(String name) { return EnumUtils.getEnum(TopicAction.class, name); @@ -27,4 +33,9 @@ public static TopicAction fromString(String name) { public boolean isAlter() { return ALTER_ACTIONS.contains(this); } + + @Override + public PermissibleAction[] dependantActions() { + return dependantActions; + } } diff --git a/api/src/main/java/io/kafbat/ui/service/audit/AuditRecord.java b/api/src/main/java/io/kafbat/ui/service/audit/AuditRecord.java index 966884cd6..d7ef659bf 100644 --- a/api/src/main/java/io/kafbat/ui/service/audit/AuditRecord.java +++ b/api/src/main/java/io/kafbat/ui/service/audit/AuditRecord.java @@ -7,10 +7,8 @@ import io.kafbat.ui.model.rbac.AccessContext; import io.kafbat.ui.model.rbac.Resource; import io.kafbat.ui.model.rbac.permission.PermissibleAction; -import java.util.ArrayList; -import java.util.LinkedHashMap; +import java.util.Collection; import java.util.List; -import java.util.Map; import javax.annotation.Nullable; import lombok.SneakyThrows; import org.springframework.security.access.AccessDeniedException; @@ -34,43 +32,17 @@ String toJson() { return MAPPER.writeValueAsString(this); } - record AuditResource(String accessType, boolean alter, Resource type, @Nullable Object id) { + record AuditResource(Resource type, @Nullable Object id, boolean alter, List accessType) { - private static AuditResource create(PermissibleAction action, Resource type, @Nullable Object id) { - return new AuditResource(action.name(), action.isAlter(), type, id); + private static AuditResource create(Collection actions, Resource type, @Nullable Object id) { + boolean isAlter = actions.stream().anyMatch(PermissibleAction::isAlter); + return new AuditResource(type, id, isAlter, actions.stream().map(PermissibleAction::name).toList()); } static List getAccessedResources(AccessContext ctx) { - List resources = new ArrayList<>(); - ctx.getClusterConfigActions() - .forEach(a -> resources.add(create(a, Resource.CLUSTERCONFIG, null))); - ctx.getTopicActions() - .forEach(a -> resources.add(create(a, Resource.TOPIC, nameId(ctx.getTopic())))); - ctx.getConsumerGroupActions() - .forEach(a -> resources.add(create(a, Resource.CONSUMER, nameId(ctx.getConsumerGroup())))); - ctx.getConnectActions() - .forEach(a -> { - Map resourceId = new LinkedHashMap<>(); - resourceId.put("connect", ctx.getConnect()); - if (ctx.getConnector() != null) { - resourceId.put("connector", ctx.getConnector()); - } - resources.add(create(a, Resource.CONNECT, resourceId)); - }); - ctx.getSchemaActions() - .forEach(a -> resources.add(create(a, Resource.SCHEMA, nameId(ctx.getSchema())))); - ctx.getKsqlActions() - .forEach(a -> resources.add(create(a, Resource.KSQL, null))); - ctx.getAclActions() - .forEach(a -> resources.add(create(a, Resource.ACL, null))); - ctx.getAuditAction() - .forEach(a -> resources.add(create(a, Resource.AUDIT, null))); - return resources; - } - - @Nullable - private static Map nameId(@Nullable String name) { - return name != null ? Map.of("name", name) : null; + return ctx.accessedResources().stream() + .map(r -> create(r.requestedActions(), r.resourceType(), r.resourceId())) + .toList(); } } diff --git a/api/src/main/java/io/kafbat/ui/service/audit/AuditService.java b/api/src/main/java/io/kafbat/ui/service/audit/AuditService.java index be386c92f..22bb77f0b 100644 --- a/api/src/main/java/io/kafbat/ui/service/audit/AuditService.java +++ b/api/src/main/java/io/kafbat/ui/service/audit/AuditService.java @@ -12,7 +12,6 @@ import java.io.Closeable; import java.io.IOException; import java.time.Duration; -import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -188,9 +187,9 @@ private Mono extractUser(Signal sig) { private static AuthenticatedUser extractUser(Object principal) { if (principal instanceof UserDetails u) { - return new AuthenticatedUser(u.getUsername(), Collections.emptySet()); + return new AuthenticatedUser(u.getUsername(), Set.of()); } else if (principal instanceof AuthenticatedPrincipal p) { - return new AuthenticatedUser(p.getName(), Collections.emptySet()); + return new AuthenticatedUser(p.getName(), Set.of()); } else { if (principal != null) { log.trace("Principal type: [{}]", principal.getClass().getName()); @@ -227,8 +226,8 @@ private void sendAuditRecord(AccessContext ctx, AuthenticatedUser user) { private void sendAuditRecord(AccessContext ctx, AuthenticatedUser user, @Nullable Throwable th) { try { - if (ctx.getCluster() != null) { - var writer = auditWriters.get(ctx.getCluster()); + if (ctx.cluster() != null) { + var writer = auditWriters.get(ctx.cluster()); if (writer != null) { writer.write(ctx, user, th); } diff --git a/api/src/main/java/io/kafbat/ui/service/audit/AuditWriter.java b/api/src/main/java/io/kafbat/ui/service/audit/AuditWriter.java index ab763eeb4..b393975b2 100644 --- a/api/src/main/java/io/kafbat/ui/service/audit/AuditWriter.java +++ b/api/src/main/java/io/kafbat/ui/service/audit/AuditWriter.java @@ -65,10 +65,10 @@ private static AuditRecord createRecord(AccessContext ctx, return new AuditRecord( DateTimeFormatter.ISO_INSTANT.format(Instant.now()), user.principal(), - ctx.getCluster(), //can be null, if it is application-level action + ctx.cluster(), //can be null, if it is application-level action AuditResource.getAccessedResources(ctx), - ctx.getOperationName(), - ctx.getOperationParams(), + ctx.operationName(), + ctx.operationParams(), th == null ? OperationResult.successful() : OperationResult.error(th) ); } 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 0359311c3..7a807d5b6 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 @@ -8,7 +8,6 @@ import io.kafbat.ui.model.InternalTopic; 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; @@ -26,7 +25,6 @@ import java.util.Objects; import java.util.Set; import java.util.function.Predicate; -import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.annotation.Nullable; import lombok.RequiredArgsConstructor; @@ -50,7 +48,6 @@ public class AccessControlService { private static final String ACCESS_DENIED = "Access denied"; - private static final String ACTIONS_ARE_EMPTY = "actions are empty"; @Nullable private final InMemoryReactiveClientRegistrationRepository clientRegistrationRepository; @@ -94,44 +91,33 @@ public void init() { } public Mono validateAccess(AccessContext context) { + return isAccessible(context) + .flatMap(allowed -> allowed ? Mono.empty() : Mono.error(new AccessDeniedException(ACCESS_DENIED))) + .then(); + } + + private Mono isAccessible(AccessContext context) { if (!rbacEnabled) { - return Mono.empty(); + return Mono.just(true); } + return getUser().map(user -> isAccessible(user, context)); + } - if (CollectionUtils.isNotEmpty(context.getApplicationConfigActions())) { - return getUser() - .doOnNext(user -> { - boolean accessGranted = isApplicationConfigAccessible(context, user); - - if (!accessGranted) { - throw new AccessDeniedException(ACCESS_DENIED); - } - }).then(); + private boolean isAccessible(AuthenticatedUser user, AccessContext context) { + if (context.cluster() != null && !isClusterAccessible(context.cluster(), user)) { + return false; } + return context.isAccessible(getUserPermissions(user)); + } - return getUser() - .doOnNext(user -> { - boolean accessGranted = - isApplicationConfigAccessible(context, user) - && isClusterAccessible(context, user) - && isClusterConfigAccessible(context, user) - && isTopicAccessible(context, user) - && isConsumerGroupAccessible(context, user) - && isConnectAccessible(context, user) - && isConnectorAccessible(context, user) // TODO connector selectors - && isSchemaAccessible(context, user) - && isKsqlAccessible(context, user) - && isAclAccessible(context, user) - && isAuditAccessible(context, user); - - if (!accessGranted) { - throw new AccessDeniedException(ACCESS_DENIED); - } - }) - .then(); + private List getUserPermissions(AuthenticatedUser user) { + return properties.getRoles().stream() + .filter(filterRole(user)) + .flatMap(role -> role.getPermissions().stream()) + .toList(); } - public Mono getUser() { + public static Mono getUser() { return ReactiveSecurityContextHolder.getContext() .map(SecurityContext::getAuthentication) .filter(authentication -> authentication.getPrincipal() instanceof RbacUser) @@ -139,281 +125,67 @@ public Mono getUser() { .map(user -> new AuthenticatedUser(user.name(), user.groups())); } - public boolean isApplicationConfigAccessible(AccessContext context, AuthenticatedUser user) { - if (!rbacEnabled) { - return true; - } - if (CollectionUtils.isEmpty(context.getApplicationConfigActions())) { - return true; - } - Set requiredActions = context.getApplicationConfigActions() - .stream() - .map(a -> a.toString().toUpperCase()) - .collect(Collectors.toSet()); - return isAccessible(Resource.APPLICATIONCONFIG, null, user, context, requiredActions); - } - - private boolean isClusterAccessible(AccessContext context, AuthenticatedUser user) { - if (!rbacEnabled) { - return true; - } - - Assert.isTrue(StringUtils.isNotEmpty(context.getCluster()), "cluster value is empty"); - + private boolean isClusterAccessible(String clusterName, AuthenticatedUser user) { + Assert.isTrue(StringUtils.isNotEmpty(clusterName), "cluster value is empty"); return properties.getRoles() .stream() .filter(filterRole(user)) - .anyMatch(filterCluster(context.getCluster())); + .anyMatch(role -> role.getClusters().stream().anyMatch(clusterName::equalsIgnoreCase)); } public Mono isClusterAccessible(ClusterDTO cluster) { if (!rbacEnabled) { return Mono.just(true); } - - AccessContext accessContext = AccessContext - .builder() - .cluster(cluster.getName()) - .build(); - - return getUser().map(u -> isClusterAccessible(accessContext, u)); - } - - public boolean isClusterConfigAccessible(AccessContext context, AuthenticatedUser user) { - if (!rbacEnabled) { - return true; - } - - if (CollectionUtils.isEmpty(context.getClusterConfigActions())) { - return true; - } - Assert.isTrue(StringUtils.isNotEmpty(context.getCluster()), "cluster value is empty"); - - Set requiredActions = context.getClusterConfigActions() - .stream() - .map(a -> a.toString().toUpperCase()) - .collect(Collectors.toSet()); - - return isAccessible(Resource.CLUSTERCONFIG, context.getCluster(), user, context, requiredActions); - } - - public boolean isTopicAccessible(AccessContext context, AuthenticatedUser user) { - if (!rbacEnabled) { - return true; - } - - if (context.getTopic() == null && context.getTopicActions().isEmpty()) { - return true; - } - Assert.isTrue(!context.getTopicActions().isEmpty(), ACTIONS_ARE_EMPTY); - - Set requiredActions = context.getTopicActions() - .stream() - .map(a -> a.toString().toUpperCase()) - .collect(Collectors.toSet()); - - return isAccessible(Resource.TOPIC, context.getTopic(), user, context, requiredActions); + return getUser().map(u -> isClusterAccessible(cluster.getName(), u)); } public Mono> filterViewableTopics(List topics, String clusterName) { if (!rbacEnabled) { return Mono.just(topics); } - return getUser() .map(user -> topics.stream() - .filter(topic -> { - var accessContext = AccessContext - .builder() - .cluster(clusterName) - .topic(topic.getName()) - .topicActions(TopicAction.VIEW) - .build(); - return isTopicAccessible(accessContext, user); - } + .filter(topic -> + isAccessible( + user, + AccessContext.builder() + .cluster(clusterName) + .topicActions(topic.getName(), TopicAction.VIEW) + .build() + ) ).toList()); } - private boolean isConsumerGroupAccessible(AccessContext context, AuthenticatedUser user) { - if (!rbacEnabled) { - return true; - } - - if (context.getConsumerGroup() == null && context.getConsumerGroupActions().isEmpty()) { - return true; - } - Assert.isTrue(!context.getConsumerGroupActions().isEmpty(), ACTIONS_ARE_EMPTY); - - Set requiredActions = context.getConsumerGroupActions() - .stream() - .map(a -> a.toString().toUpperCase()) - .collect(Collectors.toSet()); - - return isAccessible(Resource.CONSUMER, context.getConsumerGroup(), user, context, requiredActions); - } - public Mono isConsumerGroupAccessible(String groupId, String clusterName) { - if (!rbacEnabled) { - return Mono.just(true); - } - - AccessContext accessContext = AccessContext - .builder() - .cluster(clusterName) - .consumerGroup(groupId) - .consumerGroupActions(ConsumerGroupAction.VIEW) - .build(); - - return getUser().map(u -> isConsumerGroupAccessible(accessContext, u)); - } - - public boolean isSchemaAccessible(AccessContext context, AuthenticatedUser user) { - if (!rbacEnabled) { - return true; - } - - if (context.getSchema() == null && context.getSchemaActions().isEmpty()) { - return true; - } - Assert.isTrue(!context.getSchemaActions().isEmpty(), ACTIONS_ARE_EMPTY); - - Set requiredActions = context.getSchemaActions() - .stream() - .map(a -> a.toString().toUpperCase()) - .collect(Collectors.toSet()); - - return isAccessible(Resource.SCHEMA, context.getSchema(), user, context, requiredActions); + return isAccessible( + AccessContext.builder() + .cluster(clusterName) + .consumerGroupActions(groupId, ConsumerGroupAction.VIEW) + .build() + ); } public Mono isSchemaAccessible(String schema, String clusterName) { - if (!rbacEnabled) { - return Mono.just(true); - } - - AccessContext accessContext = AccessContext - .builder() - .cluster(clusterName) - .schema(schema) - .schemaActions(SchemaAction.VIEW) - .build(); - - return getUser().map(u -> isSchemaAccessible(accessContext, u)); - } - - public boolean isConnectAccessible(AccessContext context, AuthenticatedUser user) { - if (!rbacEnabled) { - return true; - } - - if (context.getConnect() == null && context.getConnectActions().isEmpty()) { - return true; - } - Assert.isTrue(!context.getConnectActions().isEmpty(), ACTIONS_ARE_EMPTY); - - Set requiredActions = context.getConnectActions() - .stream() - .map(a -> a.toString().toUpperCase()) - .collect(Collectors.toSet()); - - return isAccessible(Resource.CONNECT, context.getConnect(), user, context, requiredActions); + return isAccessible( + AccessContext.builder() + .cluster(clusterName) + .schemaActions(schema, SchemaAction.VIEW) + .build() + ); } public Mono isConnectAccessible(ConnectDTO dto, String clusterName) { - if (!rbacEnabled) { - return Mono.just(true); - } - return isConnectAccessible(dto.getName(), clusterName); } public Mono isConnectAccessible(String connectName, String clusterName) { - if (!rbacEnabled) { - return Mono.just(true); - } - - AccessContext accessContext = AccessContext - .builder() - .cluster(clusterName) - .connect(connectName) - .connectActions(ConnectAction.VIEW) - .build(); - - return getUser().map(u -> isConnectAccessible(accessContext, u)); - } - - public boolean isConnectorAccessible(AccessContext context, AuthenticatedUser user) { - if (!rbacEnabled) { - return true; - } - - return isConnectAccessible(context, user); - } - - public Mono isConnectorAccessible(String connectName, String connectorName, String clusterName) { - if (!rbacEnabled) { - return Mono.just(true); - } - - AccessContext accessContext = AccessContext - .builder() - .cluster(clusterName) - .connect(connectName) - .connectActions(ConnectAction.VIEW) - .connector(connectorName) - .build(); - - return getUser().map(u -> isConnectorAccessible(accessContext, u)); - } - - private boolean isKsqlAccessible(AccessContext context, AuthenticatedUser user) { - if (!rbacEnabled) { - return true; - } - - if (context.getKsqlActions().isEmpty()) { - return true; - } - - Set requiredActions = context.getKsqlActions() - .stream() - .map(a -> a.toString().toUpperCase()) - .collect(Collectors.toSet()); - - return isAccessible(Resource.KSQL, null, user, context, requiredActions); - } - - private boolean isAclAccessible(AccessContext context, AuthenticatedUser user) { - if (!rbacEnabled) { - return true; - } - - if (context.getAclActions().isEmpty()) { - return true; - } - - Set requiredActions = context.getAclActions() - .stream() - .map(a -> a.toString().toUpperCase()) - .collect(Collectors.toSet()); - - return isAccessible(Resource.ACL, null, user, context, requiredActions); - } - - private boolean isAuditAccessible(AccessContext context, AuthenticatedUser user) { - if (!rbacEnabled) { - return true; - } - - if (context.getAuditAction().isEmpty()) { - return true; - } - - Set requiredActions = context.getAuditAction() - .stream() - .map(a -> a.toString().toUpperCase()) - .collect(Collectors.toSet()); - - return isAccessible(Resource.AUDIT, null, user, context, requiredActions); + return isAccessible( + AccessContext.builder() + .cluster(clusterName) + .connectActions(connectName, ConnectAction.VIEW) + .build() + ); } public Set getOauthExtractors() { @@ -427,57 +199,10 @@ public List getRoles() { return Collections.unmodifiableList(properties.getRoles()); } - private boolean isAccessible(Resource resource, @Nullable String resourceValue, - AuthenticatedUser user, AccessContext context, Set requiredActions) { - Set grantedActions = properties.getRoles() - .stream() - .filter(filterRole(user)) - .filter(filterCluster(resource, context.getCluster())) - .flatMap(grantedRole -> grantedRole.getPermissions().stream()) - .filter(filterResource(resource)) - .filter(filterResourceValue(resourceValue)) - .flatMap(grantedPermission -> grantedPermission.getActions().stream()) - .map(String::toUpperCase) - .collect(Collectors.toSet()); - - return grantedActions.containsAll(requiredActions); - } - private Predicate filterRole(AuthenticatedUser user) { return role -> user.groups().contains(role.getName()); } - private Predicate filterCluster(String cluster) { - return grantedRole -> grantedRole.getClusters() - .stream() - .anyMatch(cluster::equalsIgnoreCase); - } - - private Predicate filterCluster(Resource resource, String cluster) { - if (resource == Resource.APPLICATIONCONFIG) { - return role -> true; - } - return filterCluster(cluster); - } - - private Predicate filterResource(Resource resource) { - return grantedPermission -> resource == grantedPermission.getResource(); - } - - private Predicate filterResourceValue(@Nullable String resourceValue) { - - if (resourceValue == null) { - return grantedPermission -> true; - } - return grantedPermission -> { - Pattern valuePattern = grantedPermission.getCompiledValuePattern(); - if (valuePattern == null) { - return true; - } - return valuePattern.matcher(resourceValue).matches(); - }; - } - public boolean isRbacEnabled() { return rbacEnabled; } diff --git a/api/src/test/java/io/kafbat/ui/model/rbac/AccessContextTest.java b/api/src/test/java/io/kafbat/ui/model/rbac/AccessContextTest.java new file mode 100644 index 000000000..161a34075 --- /dev/null +++ b/api/src/test/java/io/kafbat/ui/model/rbac/AccessContextTest.java @@ -0,0 +1,112 @@ +package io.kafbat.ui.model.rbac; + + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.kafbat.ui.model.rbac.AccessContext.ResourceAccess; +import io.kafbat.ui.model.rbac.AccessContext.SingleResourceAccess; +import io.kafbat.ui.model.rbac.permission.ClusterConfigAction; +import io.kafbat.ui.model.rbac.permission.PermissibleAction; +import io.kafbat.ui.model.rbac.permission.TopicAction; +import jakarta.annotation.Nullable; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class AccessContextTest { + + @Test + void validateReturnsTrueIfAllResourcesAreAccessible() { + ResourceAccess okResourceAccess1 = mock(ResourceAccess.class); + when(okResourceAccess1.isAccessible(any())).thenReturn(true); + + ResourceAccess okResourceAccess2 = mock(ResourceAccess.class); + when(okResourceAccess2.isAccessible(any())).thenReturn(true); + + var cxt = new AccessContext("cluster", List.of(okResourceAccess1, okResourceAccess2), "op", "params"); + assertThat(cxt.isAccessible(List.of())).isTrue(); + } + + @Test + void validateReturnsFalseIfAnyResourcesCantBeAccessible() { + ResourceAccess okResourceAccess = mock(ResourceAccess.class); + when(okResourceAccess.isAccessible(any())).thenReturn(true); + + ResourceAccess failureResourceAccess = mock(ResourceAccess.class); + when(failureResourceAccess.isAccessible(any())).thenReturn(false); + + var cxt = new AccessContext("cluster", List.of(okResourceAccess, failureResourceAccess), "op", "params"); + assertThat(cxt.isAccessible(List.of())).isFalse(); + } + + + @Nested + class SingleResourceAccessTest { + + @Test + void allowsAccessForResourceWithNameIfUserHasAllNeededPermissions() { + SingleResourceAccess sra = + new SingleResourceAccess("test_topic123", Resource.TOPIC, List.of(TopicAction.VIEW, TopicAction.EDIT)); + + var allowed = sra.isAccessible( + List.of( + permission(Resource.TOPIC, "test_topic.*", TopicAction.EDIT), + permission(Resource.TOPIC, "test.*", TopicAction.VIEW))); + + assertThat(allowed).isTrue(); + } + + @Test + void deniesAccessForResourceWithNameIfUserHasSomePermissionsMissing() { + SingleResourceAccess sra = + new SingleResourceAccess("test_topic123", Resource.TOPIC, + List.of(TopicAction.VIEW, TopicAction.MESSAGES_DELETE)); + + var allowed = sra.isAccessible( + List.of( + permission(Resource.TOPIC, "test_topic.*", TopicAction.EDIT), + permission(Resource.TOPIC, "test.*", TopicAction.VIEW))); + + assertThat(allowed).isFalse(); + } + + @Test + void allowsAccessForResourceWithoutNameIfUserHasAllNeededPermissions() { + SingleResourceAccess sra = + new SingleResourceAccess(Resource.CLUSTERCONFIG, List.of(ClusterConfigAction.VIEW)); + + var allowed = sra.isAccessible( + List.of( + permission(Resource.CLUSTERCONFIG, null, ClusterConfigAction.VIEW, ClusterConfigAction.EDIT))); + + assertThat(allowed).isTrue(); + } + + @Test + void deniesAccessForResourceWithoutNameIfUserHasAllNeededPermissions() { + SingleResourceAccess sra = + new SingleResourceAccess(Resource.CLUSTERCONFIG, List.of(ClusterConfigAction.EDIT)); + + var allowed = sra.isAccessible( + List.of( + permission(Resource.CLUSTERCONFIG, null, ClusterConfigAction.VIEW))); + + assertThat(allowed).isFalse(); + } + + private Permission permission(Resource res, @Nullable String namePattern, PermissibleAction... actions) { + Permission p = new Permission(); + p.setResource(res.name()); + p.setActions(Stream.of(actions).map(PermissibleAction::name).toList()); + p.setValue(namePattern); + p.validate(); + p.transform(); + return p; + } + } + +} diff --git a/api/src/test/java/io/kafbat/ui/model/rbac/PermissionTest.java b/api/src/test/java/io/kafbat/ui/model/rbac/PermissionTest.java new file mode 100644 index 000000000..3d2fd72be --- /dev/null +++ b/api/src/test/java/io/kafbat/ui/model/rbac/PermissionTest.java @@ -0,0 +1,52 @@ +package io.kafbat.ui.model.rbac; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.kafbat.ui.model.rbac.permission.TopicAction; +import java.util.List; +import org.junit.jupiter.api.Test; + +class PermissionTest { + + @Test + void transformSetsParseableFields() { + var p = new Permission(); + p.setResource("toPic"); + p.setActions(List.of("vIEW", "EdiT")); + p.setValue("patt|ern"); + + p.transform(); + + assertThat(p.getParsedActions()) + .containsExactlyInAnyOrder(TopicAction.VIEW, TopicAction.EDIT); + + assertThat(p.getCompiledValuePattern()) + .isNotNull() + .matches(pattern -> pattern.pattern().equals("patt|ern")); + } + + @Test + void transformSetsFullActionsListIfAllActionPassed() { + var p = new Permission(); + p.setResource("toPic"); + p.setActions(List.of("All")); + + p.transform(); + + assertThat(p.getParsedActions()) + .isEqualTo(List.of(TopicAction.values())); + } + + @Test + void transformUnnestsDependantActions() { + var p = new Permission(); + p.setResource("toPic"); + p.setActions(List.of("EDIT")); + + p.transform(); + + assertThat(p.getParsedActions()) + .containsExactlyInAnyOrder(TopicAction.VIEW, TopicAction.EDIT); + } + +} diff --git a/api/src/test/java/io/kafbat/ui/service/audit/AuditWriterTest.java b/api/src/test/java/io/kafbat/ui/service/audit/AuditWriterTest.java index 3ca2ffb73..39f3d37e5 100644 --- a/api/src/test/java/io/kafbat/ui/service/audit/AuditWriterTest.java +++ b/api/src/test/java/io/kafbat/ui/service/audit/AuditWriterTest.java @@ -44,17 +44,17 @@ void onlyLogsWhenAlterOperationIsPresentForOneOfResources(AccessContext ctxWithA static Stream onlyLogsWhenAlterOperationIsPresentForOneOfResources() { Stream> topicEditActions = - TopicAction.ALTER_ACTIONS.stream().map(a -> c -> c.topic("test").topicActions(a)); + TopicAction.ALTER_ACTIONS.stream().map(a -> c -> c.topicActions("test", a)); Stream> clusterConfigEditActions = ClusterConfigAction.ALTER_ACTIONS.stream().map(a -> c -> c.clusterConfigActions(a)); Stream> aclEditActions = AclAction.ALTER_ACTIONS.stream().map(a -> c -> c.aclActions(a)); Stream> cgEditActions = - ConsumerGroupAction.ALTER_ACTIONS.stream().map(a -> c -> c.consumerGroup("cg").consumerGroupActions(a)); + ConsumerGroupAction.ALTER_ACTIONS.stream().map(a -> c -> c.consumerGroupActions("cg", a)); Stream> schemaEditActions = - SchemaAction.ALTER_ACTIONS.stream().map(a -> c -> c.schema("sc").schemaActions(a)); + SchemaAction.ALTER_ACTIONS.stream().map(a -> c -> c.schemaActions("sc", a)); Stream> connEditActions = - ConnectAction.ALTER_ACTIONS.stream().map(a -> c -> c.connect("conn").connectActions(a)); + ConnectAction.ALTER_ACTIONS.stream().map(a -> c -> c.connectActions("conn", a)); return Stream.of( topicEditActions, clusterConfigEditActions, aclEditActions, cgEditActions, connEditActions, schemaEditActions @@ -73,12 +73,12 @@ void doesNothingIfNoResourceHasAlterAction(AccessContext readOnlyCxt) { static Stream doesNothingIfNoResourceHasAlterAction() { return Stream.>of( - c -> c.topic("test").topicActions(TopicAction.VIEW), + c -> c.topicActions("test", TopicAction.VIEW), c -> c.clusterConfigActions(ClusterConfigAction.VIEW), c -> c.aclActions(AclAction.VIEW), - c -> c.consumerGroup("cg").consumerGroupActions(ConsumerGroupAction.VIEW), - c -> c.schema("sc").schemaActions(SchemaAction.VIEW), - c -> c.connect("conn").connectActions(ConnectAction.VIEW) + c -> c.consumerGroupActions("cg", ConsumerGroupAction.VIEW), + c -> c.schemaActions("sc", SchemaAction.VIEW), + c -> c.connectActions("conn", ConnectAction.VIEW) ).map(setter -> setter.apply(AccessContext.builder().cluster("test").operationName("test")).build()); } }