Explorar el Código

Merge branch 'master' into issue/3163

Roman Zabaluev hace 2 años
padre
commit
58940a3730
Se han modificado 42 ficheros con 731 adiciones y 116 borrados
  1. 8 0
      .github/release_drafter.yaml
  2. 1 1
      .github/workflows/e2e-checks.yaml
  3. 16 1
      .github/workflows/release_drafter.yml
  4. 2 1
      SECURITY.md
  5. 2 2
      charts/kafka-ui/Chart.yaml
  6. 19 10
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AccessController.java
  7. 1 3
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalConsumerGroup.java
  8. 2 1
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Permission.java
  9. 14 14
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/FeatureService.java
  10. 3 1
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/acl/AclsService.java
  11. 1 4
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/JmxMetricsRetriever.java
  12. 19 1
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AccessControlService.java
  13. 3 5
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/KafkaServicesValidation.java
  14. 1 0
      kafka-ui-api/src/main/resources/application.yml
  15. 5 8
      kafka-ui-api/src/main/resources/logback-spring.xml
  16. 1 1
      kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/TopicsServicePaginationTest.java
  17. 1 0
      kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml
  18. 5 0
      kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlQueryForm.java
  19. 3 3
      kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaDetails.java
  20. 3 3
      kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/ProduceMessagePanel.java
  21. 9 7
      kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/drivers/WebDriver.java
  22. 30 8
      kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SmokeBacklog.java
  23. 10 16
      kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/TopicsTest.java
  24. 22 12
      kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/ksqldb/KsqlDbTest.java
  25. 13 0
      kafka-ui-react-app/src/components/ACLPage/ACLPage.tsx
  26. 44 0
      kafka-ui-react-app/src/components/ACLPage/List/List.styled.ts
  27. 153 0
      kafka-ui-react-app/src/components/ACLPage/List/List.tsx
  28. 74 0
      kafka-ui-react-app/src/components/ACLPage/List/__test__/List.spec.tsx
  29. 11 0
      kafka-ui-react-app/src/components/ClusterPage/ClusterPage.tsx
  30. 5 0
      kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx
  31. 1 1
      kafka-ui-react-app/src/components/common/Button/Button.styled.ts
  32. 1 1
      kafka-ui-react-app/src/components/common/ConfirmationModal/ConfirmationModal.tsx
  33. 3 2
      kafka-ui-react-app/src/components/common/Icons/DeleteIcon.tsx
  34. 8 4
      kafka-ui-react-app/src/components/common/MultiSelect/MultiSelect.styled.ts
  35. 35 1
      kafka-ui-react-app/src/components/common/NewTable/Table.tsx
  36. 5 0
      kafka-ui-react-app/src/components/contexts/ConfirmContext.tsx
  37. 2 0
      kafka-ui-react-app/src/lib/api.ts
  38. 37 0
      kafka-ui-react-app/src/lib/fixtures/acls.ts
  39. 67 0
      kafka-ui-react-app/src/lib/hooks/api/acl.ts
  40. 2 1
      kafka-ui-react-app/src/lib/hooks/useConfirm.ts
  41. 7 0
      kafka-ui-react-app/src/lib/paths.ts
  42. 82 4
      kafka-ui-react-app/src/theme/theme.ts

+ 8 - 0
.github/release_drafter.yaml

@@ -16,18 +16,26 @@ exclude-labels:
   - 'type/refactoring'
 
 categories:
+  - title: '🚩 Breaking Changes'
+    labels:
+      - 'impact/changelog'
+
   - title: '⚙️Features'
     labels:
       - 'type/feature'
+
   - title: '🪛Enhancements'
     labels:
       - 'type/enhancement'
+
   - title: '🔨Bug Fixes'
     labels:
       - 'type/bug'
+
   - title: 'Security'
     labels:
       - 'type/security'
+
   - title: '⎈ Helm/K8S Changes'
     labels:
       - 'scope/k8s'

+ 1 - 1
.github/workflows/e2e-checks.yaml

@@ -45,7 +45,7 @@ jobs:
         # use the following command until #819 will be fixed
         run: |
           docker-compose -f kafka-ui-e2e-checks/docker/selenoid-git.yaml up -d
-          docker-compose -f ./documentation/compose/e2e-tests.yaml up -d
+          docker-compose -f ./documentation/compose/e2e-tests.yaml up -d && until [ "$(docker exec  kafka-ui wget --spider  --server-response  http://localhost:8080/actuator/health 2>&1 |  grep -c 'HTTP/1.1 200 OK')" == "1" ]; do echo "Waiting for kafka-ui ..." && sleep 1; done
       - name: Run test suite
         run: |
           ./mvnw -B -ntp versions:set -DnewVersion=${{ github.event.pull_request.head.sha }}

+ 16 - 1
.github/workflows/release_drafter.yml

@@ -2,18 +2,33 @@ name: Release Drafter
 
 on:
   push:
-    # branches to consider in the event; optional, defaults to all
     branches:
       - master
   workflow_dispatch:
+    inputs:
+      version:
+        description: 'Release version'
+        required: false
+      branch:
+        description: 'Target branch'
+        required: false
+        default: 'master'
+
+permissions:
+  contents: read
 
 jobs:
   update_release_draft:
     runs-on: ubuntu-latest
+    permissions:
+      contents: write
+      pull-requests: write
     steps:
       - uses: release-drafter/release-drafter@v5
         with:
           config-name: release_drafter.yaml
           disable-autolabeler: true
+          version: ${{ github.event.inputs.version }}
+          commitish: ${{ github.event.inputs.branch }}
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 2 - 1
SECURITY.md

@@ -6,7 +6,8 @@ Following versions of the project are currently being supported with security up
 
 | Version | Supported          |
 | ------- | ------------------ |
-| 0.6.x   | :white_check_mark: |
+| 0.7.x   | :white_check_mark: |
+| 0.6.x   | :x:                |
 | 0.5.x   | :x:                |
 | 0.4.x   | :x:                |
 | 0.3.x   | :x:                |

+ 2 - 2
charts/kafka-ui/Chart.yaml

@@ -2,6 +2,6 @@ apiVersion: v2
 name: kafka-ui
 description: A Helm chart for kafka-UI
 type: application
-version: 0.6.2
-appVersion: v0.6.2
+version: 0.7.0
+appVersion: v0.7.0
 icon: https://github.com/provectus/kafka-ui/raw/master/documentation/images/kafka-ui-logo.png

+ 19 - 10
kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AccessController.java

@@ -12,8 +12,11 @@ import java.security.Principal;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 import java.util.stream.Collectors;
+import javax.annotation.Nullable;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.ResponseEntity;
 import org.springframework.security.core.context.ReactiveSecurityContextHolder;
 import org.springframework.security.core.context.SecurityContext;
@@ -23,15 +26,12 @@ import reactor.core.publisher.Mono;
 
 @RestController
 @RequiredArgsConstructor
+@Slf4j
 public class AccessController implements AuthorizationApi {
 
   private final AccessControlService accessControlService;
 
   public Mono<ResponseEntity<AuthenticationInfoDTO>> getUserAuthInfo(ServerWebExchange exchange) {
-    AuthenticationInfoDTO dto = new AuthenticationInfoDTO();
-    dto.setRbacEnabled(accessControlService.isRbacEnabled());
-    UserInfoDTO userInfo = new UserInfoDTO();
-
     Mono<List<UserPermissionDTO>> permissions = accessControlService.getUser()
         .map(user -> accessControlService.getRoles()
             .stream()
@@ -49,13 +49,11 @@ public class AccessController implements AuthorizationApi {
     return userName
         .zipWith(permissions)
         .map(data -> {
-          userInfo.setUsername(data.getT1());
-          userInfo.setPermissions(data.getT2());
-
-          dto.setUserInfo(userInfo);
+          var dto = new AuthenticationInfoDTO(accessControlService.isRbacEnabled());
+          dto.setUserInfo(new UserInfoDTO(data.getT1(), data.getT2()));
           return dto;
         })
-        .switchIfEmpty(Mono.just(dto))
+        .switchIfEmpty(Mono.just(new AuthenticationInfoDTO(accessControlService.isRbacEnabled())))
         .map(ResponseEntity::ok);
   }
 
@@ -70,11 +68,22 @@ public class AccessController implements AuthorizationApi {
           dto.setActions(permission.getActions()
               .stream()
               .map(String::toUpperCase)
-              .map(ActionDTO::valueOf)
+              .map(this::mapAction)
+              .filter(Objects::nonNull)
               .collect(Collectors.toList()));
           return dto;
         })
         .collect(Collectors.toList());
   }
 
+  @Nullable
+  private ActionDTO mapAction(String name) {
+    try {
+      return ActionDTO.fromValue(name);
+    } catch (IllegalArgumentException e) {
+      log.warn("Unknown Action [{}], skipping", name);
+      return null;
+    }
+  }
+
 }

+ 1 - 3
kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/InternalConsumerGroup.java

@@ -74,14 +74,12 @@ public class InternalConsumerGroup {
 
   private static Integer calculateTopicNum(Map<TopicPartition, Long> offsets, Collection<InternalMember> members) {
 
-    long topicNum = Stream.concat(
+    return (int) Stream.concat(
         offsets.keySet().stream().map(TopicPartition::topic),
         members.stream()
             .flatMap(m -> m.getAssignment().stream().map(TopicPartition::topic))
     ).distinct().count();
 
-    return Integer.valueOf((int) topicNum);
-
   }
 
   private static Collection<InternalMember> initInternalMembers(ConsumerGroupDescription description) {

+ 2 - 1
kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Permission.java

@@ -1,5 +1,6 @@
 package com.provectus.kafka.ui.model.rbac;
 
+import static com.provectus.kafka.ui.model.rbac.Resource.ACL;
 import static com.provectus.kafka.ui.model.rbac.Resource.APPLICATIONCONFIG;
 import static com.provectus.kafka.ui.model.rbac.Resource.CLUSTERCONFIG;
 import static com.provectus.kafka.ui.model.rbac.Resource.KSQL;
@@ -27,7 +28,7 @@ import org.springframework.util.Assert;
 @EqualsAndHashCode
 public class Permission {
 
-  private static final List<Resource> RBAC_ACTION_EXEMPT_LIST = List.of(KSQL, CLUSTERCONFIG, APPLICATIONCONFIG);
+  private static final List<Resource> RBAC_ACTION_EXEMPT_LIST = List.of(KSQL, CLUSTERCONFIG, APPLICATIONCONFIG, ACL);
 
   Resource resource;
   List<String> actions;

+ 14 - 14
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/FeatureService.java

@@ -9,7 +9,6 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Predicate;
-import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.kafka.common.acl.AclOperation;
 import org.springframework.stereotype.Service;
@@ -17,12 +16,9 @@ import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 @Service
-@RequiredArgsConstructor
 @Slf4j
 public class FeatureService {
 
-  private final AdminClientService adminClientService;
-
   public Mono<List<ClusterFeature>> getAvailableFeatures(ReactiveAdminClient adminClient,
                                                          KafkaCluster cluster,
                                                          ClusterDescription clusterDescription) {
@@ -43,8 +39,8 @@ public class FeatureService {
     }
 
     features.add(topicDeletionEnabled(adminClient));
-    features.add(aclView(cluster));
-    features.add(aclEdit(clusterDescription));
+    features.add(aclView(adminClient));
+    features.add(aclEdit(adminClient, clusterDescription));
 
     return Flux.fromIterable(features).flatMap(m -> m).collectList();
   }
@@ -55,19 +51,23 @@ public class FeatureService {
         : Mono.empty();
   }
 
-  private Mono<ClusterFeature> aclEdit(ClusterDescription clusterDescription) {
+  private Mono<ClusterFeature> aclEdit(ReactiveAdminClient adminClient, ClusterDescription clusterDescription) {
     var authorizedOps = Optional.ofNullable(clusterDescription.getAuthorizedOperations()).orElse(Set.of());
-    boolean canEdit = authorizedOps.contains(AclOperation.ALL) || authorizedOps.contains(AclOperation.ALTER);
+    boolean canEdit = aclViewEnabled(adminClient)
+        && (authorizedOps.contains(AclOperation.ALL) || authorizedOps.contains(AclOperation.ALTER));
     return canEdit
         ? Mono.just(ClusterFeature.KAFKA_ACL_EDIT)
         : Mono.empty();
   }
 
-  private Mono<ClusterFeature> aclView(KafkaCluster cluster) {
-    return adminClientService.get(cluster).flatMap(
-        ac -> ac.getClusterFeatures().contains(ReactiveAdminClient.SupportedFeature.AUTHORIZED_SECURITY_ENABLED)
-            ? Mono.just(ClusterFeature.KAFKA_ACL_VIEW)
-            : Mono.empty()
-    );
+  private Mono<ClusterFeature> aclView(ReactiveAdminClient adminClient) {
+    return aclViewEnabled(adminClient)
+        ? Mono.just(ClusterFeature.KAFKA_ACL_VIEW)
+        : Mono.empty();
   }
+
+  private boolean aclViewEnabled(ReactiveAdminClient adminClient) {
+    return adminClient.getClusterFeatures().contains(ReactiveAdminClient.SupportedFeature.AUTHORIZED_SECURITY_ENABLED);
+  }
+
 }

+ 3 - 1
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/acl/AclsService.java

@@ -3,6 +3,7 @@ package com.provectus.kafka.ui.service.acl;
 import com.google.common.collect.Sets;
 import com.provectus.kafka.ui.model.KafkaCluster;
 import com.provectus.kafka.ui.service.AdminClientService;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Set;
 import lombok.RequiredArgsConstructor;
@@ -39,7 +40,8 @@ public class AclsService {
   public Flux<AclBinding> listAcls(KafkaCluster cluster, ResourcePatternFilter filter) {
     return adminClientService.get(cluster)
         .flatMap(c -> c.listAcls(filter))
-        .flatMapIterable(acls -> acls);
+        .flatMapIterable(acls -> acls)
+        .sort(Comparator.comparing(AclBinding::toString));  //sorting to keep stable order on different calls
   }
 
   public Mono<String> getAclAsCsvString(KafkaCluster cluster) {

+ 1 - 4
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/metrics/JmxMetricsRetriever.java

@@ -72,17 +72,14 @@ class JmxMetricsRetriever implements MetricsRetriever, Closeable {
                                 KafkaCluster c,
                                 Consumer<JMXConnector> consumer) {
     var env = prepareJmxEnvAndSetThreadLocal(c);
-    try {
-      JMXConnector connector = null;
+    try (JMXConnector connector = JMXConnectorFactory.newJMXConnector(new JMXServiceURL(jmxUrl), env)) {
       try {
-        connector = JMXConnectorFactory.newJMXConnector(new JMXServiceURL(jmxUrl), env);
         connector.connect(env);
       } catch (Exception exception) {
         log.error("Error connecting to {}", jmxUrl, exception);
         return;
       }
       consumer.accept(connector);
-      connector.close();
     } catch (Exception e) {
       log.error("Error getting jmx metrics from {}", jmxUrl, e);
     } finally {

+ 19 - 1
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AccessControlService.java

@@ -108,7 +108,8 @@ public class AccessControlService {
                   && isConnectAccessible(context, user)
                   && isConnectorAccessible(context, user) // TODO connector selectors
                   && isSchemaAccessible(context, user)
-                  && isKsqlAccessible(context, user);
+                  && isKsqlAccessible(context, user)
+                  && isAclAccessible(context, user);
 
           if (!accessGranted) {
             throw new AccessDeniedException("Access denied");
@@ -364,6 +365,23 @@ public class AccessControlService {
     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<String> requiredActions = context.getAclActions()
+        .stream()
+        .map(a -> a.toString().toUpperCase())
+        .collect(Collectors.toSet());
+
+    return isAccessible(Resource.ACL, null, user, context, requiredActions);
+  }
+
   public Set<ProviderAuthorityExtractor> getOauthExtractors() {
     return oauthExtractors;
   }

+ 3 - 5
kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/KafkaServicesValidation.java

@@ -45,12 +45,10 @@ public final class KafkaServicesValidation {
    */
   public static Optional<String> validateTruststore(TruststoreConfig truststoreConfig) {
     if (truststoreConfig.getTruststoreLocation() != null && truststoreConfig.getTruststorePassword() != null) {
-      try {
+      try (FileInputStream fileInputStream = new FileInputStream(
+             (ResourceUtils.getFile(truststoreConfig.getTruststoreLocation())))) {
         KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
-        trustStore.load(
-            new FileInputStream((ResourceUtils.getFile(truststoreConfig.getTruststoreLocation()))),
-            truststoreConfig.getTruststorePassword().toCharArray()
-        );
+        trustStore.load(fileInputStream, truststoreConfig.getTruststorePassword().toCharArray());
         TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
             TrustManagerFactory.getDefaultAlgorithm()
         );

+ 1 - 0
kafka-ui-api/src/main/resources/application.yml

@@ -17,4 +17,5 @@ logging:
     root: INFO
     com.provectus: DEBUG
     reactor.netty.http.server.AccessLog: INFO
+    org.hibernate.validator: WARN
 

+ 5 - 8
kafka-ui-api/src/main/resources/logback-spring.xml

@@ -1,17 +1,14 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <configuration>
 
-        <appender name="STDOUT"
-              class="ch.qos.logback.core.ConsoleAppender">
-        <layout class="ch.qos.logback.classic.PatternLayout">
-            <Pattern>
-                %black(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%c{1}): %msg%n%throwable
-            </Pattern>
-        </layout>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>%black(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%c{1}): %msg%n%throwable</pattern>
+        </encoder>
     </appender>
 
     <root level="info">
-        <appender-ref ref="STDOUT" />
+        <appender-ref ref="STDOUT"/>
     </root>
 
 </configuration>

+ 1 - 1
kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/TopicsServicePaginationTest.java

@@ -131,7 +131,7 @@ class TopicsServicePaginationTest {
 
     assertThat(topics.getBody().getPageCount()).isEqualTo(4);
     assertThat(topics.getBody().getTopics()).hasSize(1);
-    assertThat(topics.getBody().getTopics().get(0).getName().equals("99"));
+    assertThat(topics.getBody().getTopics().get(0).getName()).isEqualTo("99");
   }
 
   @Test

+ 1 - 0
kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml

@@ -3452,6 +3452,7 @@ components:
         - MESSAGES_READ
         - MESSAGES_PRODUCE
         - MESSAGES_DELETE
+        - RESTART
 
     ResourceType:
       type: string

+ 5 - 0
kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/ksqldb/KsqlQueryForm.java

@@ -74,6 +74,11 @@ public class KsqlQueryForm extends BasePage {
     return isVisible(cancelledAlert);
   }
 
+  @Step
+  public boolean isClearResultsBtnEnabled() {
+    return isEnabled(clearResultsBtn);
+  }
+
   @Step
   public KsqlQueryForm clickClearResultsBtn() {
     clickByActions(clearResultsBtn);

+ 3 - 3
kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schemas/SchemaDetails.java

@@ -13,7 +13,7 @@ public class SchemaDetails extends BasePage {
   protected SelenideElement compatibilityField = $x("//h4[contains(text(),'Compatibility')]/../p");
   protected SelenideElement editSchemaBtn = $x("//button[contains(text(),'Edit Schema')]");
   protected SelenideElement removeBtn = $x("//*[contains(text(),'Remove')]");
-  protected SelenideElement confirmBtn = $x("//div[@role='dialog']//button[contains(text(),'Confirm')]");
+  protected SelenideElement schemaConfirmBtn = $x("//div[@role='dialog']//button[contains(text(),'Confirm')]");
   protected SelenideElement schemaTypeField = $x("//h4[contains(text(),'Type')]/../p");
   protected SelenideElement latestVersionField = $x("//h4[contains(text(),'Latest version')]/../p");
   protected SelenideElement compareVersionBtn = $x("//button[text()='Compare Versions']");
@@ -62,8 +62,8 @@ public class SchemaDetails extends BasePage {
   public SchemaDetails removeSchema() {
     clickByJavaScript(dotMenuBtn);
     removeBtn.shouldBe(Condition.enabled).click();
-    confirmBtn.shouldBe(Condition.visible).click();
-    confirmBtn.shouldBe(Condition.disappear);
+    schemaConfirmBtn.shouldBe(Condition.visible).click();
+    schemaConfirmBtn.shouldBe(Condition.disappear);
     return this;
   }
 }

+ 3 - 3
kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topics/ProduceMessagePanel.java

@@ -14,7 +14,7 @@ public class ProduceMessagePanel extends BasePage {
   protected SelenideElement keyTextArea = $x("//div[@id='key']/textarea");
   protected SelenideElement valueTextArea = $x("//div[@id='content']/textarea");
   protected SelenideElement headersTextArea = $x("//div[@id='headers']/textarea");
-  protected SelenideElement submitBtn = headersTextArea.$x("../../../..//button[@type='submit']");
+  protected SelenideElement submitProduceMessageBtn = headersTextArea.$x("../../../..//button[@type='submit']");
   protected SelenideElement partitionDdl = $x("//ul[@name='partition']");
   protected SelenideElement keySerdeDdl = $x("//ul[@name='keySerde']");
   protected SelenideElement contentSerdeDdl = $x("//ul[@name='valueSerde']");
@@ -48,8 +48,8 @@ public class ProduceMessagePanel extends BasePage {
 
   @Step
   public ProduceMessagePanel submitProduceMessage() {
-    clickByActions(submitBtn);
-    submitBtn.shouldBe(Condition.disappear);
+    clickByActions(submitProduceMessageBtn);
+    submitProduceMessageBtn.shouldBe(Condition.disappear);
     refresh();
     return this;
   }

+ 9 - 7
kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/settings/drivers/WebDriver.java

@@ -14,9 +14,10 @@ import com.codeborne.selenide.WebDriverRunner;
 import com.codeborne.selenide.logevents.SelenideLogger;
 import io.qameta.allure.Step;
 import io.qameta.allure.selenide.AllureSelenide;
+import java.util.HashMap;
+import java.util.Map;
 import lombok.extern.slf4j.Slf4j;
 import org.openqa.selenium.chrome.ChromeOptions;
-import org.openqa.selenium.remote.DesiredCapabilities;
 
 @Slf4j
 public abstract class WebDriver {
@@ -29,7 +30,7 @@ public abstract class WebDriver {
     Configuration.screenshots = true;
     Configuration.savePageSource = false;
     Configuration.pageLoadTimeout = 120000;
-    ChromeOptions options = new ChromeOptions()
+    ChromeOptions chromeOptions = new ChromeOptions()
         .addArguments("--no-sandbox")
         .addArguments("--verbose")
         .addArguments("--remote-allow-origins=*")
@@ -37,14 +38,15 @@ public abstract class WebDriver {
         .addArguments("--disable-gpu")
         .addArguments("--lang=en_US");
     switch (BROWSER) {
-      case (LOCAL) -> Configuration.browserCapabilities = options;
+      case (LOCAL) -> Configuration.browserCapabilities = chromeOptions;
       case (CONTAINER) -> {
         Configuration.remote = REMOTE_URL;
         Configuration.remoteConnectionTimeout = 180000;
-        DesiredCapabilities capabilities = new DesiredCapabilities();
-        capabilities.setCapability("enableVNC", true);
-        capabilities.setCapability("enableVideo", false);
-        Configuration.browserCapabilities = capabilities.merge(options);
+        Map<String, Object> selenoidOptions = new HashMap<>();
+        selenoidOptions.put("enableVNC", true);
+        selenoidOptions.put("enableVideo", false);
+        chromeOptions.setCapability("selenoid:options", selenoidOptions);
+        Configuration.browserCapabilities = chromeOptions;
       }
       default -> throw new IllegalStateException("Unexpected value: " + BROWSER);
     }

+ 30 - 8
kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SmokeBacklog.java

@@ -1,9 +1,10 @@
 package com.provectus.kafka.ui.manualsuite.backlog;
 
 import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.BROKERS_SUITE_ID;
-import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.KSQL_DB_SUITE_ID;
 import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.SCHEMAS_SUITE_ID;
 import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.TOPICS_PROFILE_SUITE_ID;
+import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.TOPICS_SUITE_ID;
+import static com.provectus.kafka.ui.utilities.qase.enums.State.NOT_AUTOMATED;
 import static com.provectus.kafka.ui.utilities.qase.enums.State.TO_BE_AUTOMATED;
 
 import com.provectus.kafka.ui.manualsuite.BaseManualTest;
@@ -57,30 +58,51 @@ public class SmokeBacklog extends BaseManualTest {
   }
 
   @Automation(state = TO_BE_AUTOMATED)
-  @Suite(id = KSQL_DB_SUITE_ID)
-  @QaseId(344)
+  @Suite(id = SCHEMAS_SUITE_ID)
+  @QaseId(345)
   @Test
   public void testCaseG() {
   }
 
   @Automation(state = TO_BE_AUTOMATED)
   @Suite(id = SCHEMAS_SUITE_ID)
-  @QaseId(345)
+  @QaseId(346)
   @Test
   public void testCaseH() {
   }
 
   @Automation(state = TO_BE_AUTOMATED)
-  @Suite(id = SCHEMAS_SUITE_ID)
-  @QaseId(346)
+  @Suite(id = TOPICS_PROFILE_SUITE_ID)
+  @QaseId(347)
   @Test
   public void testCaseI() {
   }
 
   @Automation(state = TO_BE_AUTOMATED)
-  @Suite(id = TOPICS_PROFILE_SUITE_ID)
-  @QaseId(347)
+  @Suite(id = BROKERS_SUITE_ID)
+  @QaseId(348)
   @Test
   public void testCaseJ() {
   }
+
+  @Automation(state = TO_BE_AUTOMATED)
+  @Suite(id = BROKERS_SUITE_ID)
+  @QaseId(350)
+  @Test
+  public void testCaseK() {
+  }
+
+  @Automation(state = NOT_AUTOMATED)
+  @Suite(id = TOPICS_SUITE_ID)
+  @QaseId(50)
+  @Test
+  public void testCaseL() {
+  }
+
+  @Automation(state = NOT_AUTOMATED)
+  @Suite(id = SCHEMAS_SUITE_ID)
+  @QaseId(351)
+  @Test
+  public void testCaseM() {
+  }
 }

+ 10 - 16
kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/suite/TopicsTest.java

@@ -51,69 +51,63 @@ public class TopicsTest extends BaseManualTest {
   public void testCaseG() {
   }
 
-  @Automation(state = NOT_AUTOMATED)
-  @QaseId(50)
-  @Test
-  public void testCaseH() {
-  }
-
   @Automation(state = NOT_AUTOMATED)
   @QaseId(57)
   @Test
-  public void testCaseI() {
+  public void testCaseH() {
   }
 
   @Automation(state = NOT_AUTOMATED)
   @QaseId(58)
   @Test
-  public void testCaseJ() {
+  public void testCaseI() {
   }
 
   @Automation(state = NOT_AUTOMATED)
   @QaseId(269)
   @Test
-  public void testCaseK() {
+  public void testCaseJ() {
   }
 
   @Automation(state = NOT_AUTOMATED)
   @QaseId(270)
   @Test
-  public void testCaseL() {
+  public void testCaseK() {
   }
 
   @Automation(state = NOT_AUTOMATED)
   @QaseId(271)
   @Test
-  public void testCaseM() {
+  public void testCaseL() {
   }
 
   @Automation(state = NOT_AUTOMATED)
   @QaseId(272)
   @Test
-  public void testCaseN() {
+  public void testCaseM() {
   }
 
   @Automation(state = NOT_AUTOMATED)
   @QaseId(337)
   @Test
-  public void testCaseO() {
+  public void testCaseN() {
   }
 
   @Automation(state = NOT_AUTOMATED)
   @QaseId(339)
   @Test
-  public void testCaseP() {
+  public void testCaseO() {
   }
 
   @Automation(state = NOT_AUTOMATED)
   @QaseId(341)
   @Test
-  public void testCaseQ() {
+  public void testCaseP() {
   }
 
   @Automation(state = NOT_AUTOMATED)
   @QaseId(342)
   @Test
-  public void testCaseR() {
+  public void testCaseQ() {
   }
 }

+ 22 - 12
kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/ksqldb/KsqlDbTest.java

@@ -4,7 +4,6 @@ import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlMenuTabs.STREAMS;
 import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SELECT_ALL_FROM;
 import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SHOW_STREAMS;
 import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SHOW_TABLES;
-import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KSQL_DB;
 import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
 
 import com.provectus.kafka.ui.BaseTest;
@@ -45,10 +44,7 @@ public class KsqlDbTest extends BaseTest {
   @QaseId(284)
   @Test(priority = 1)
   public void streamsAndTablesVisibilityCheck() {
-    naviSideBar
-        .openSideMenu(KSQL_DB);
-    ksqlDbList
-        .waitUntilScreenReady();
+    navigateToKsqlDb();
     SoftAssert softly = new SoftAssert();
     softly.assertTrue(ksqlDbList.getTableByName(FIRST_TABLE.getName()).isVisible(), "getTableByName()");
     softly.assertTrue(ksqlDbList.getTableByName(SECOND_TABLE.getName()).isVisible(), "getTableByName()");
@@ -69,8 +65,24 @@ public class KsqlDbTest extends BaseTest {
     Assert.assertTrue(ksqlQueryForm.getEnteredQuery().isEmpty(), "getEnteredQuery()");
   }
 
-  @QaseId(41)
+  @QaseId(344)
   @Test(priority = 3)
+  public void clearResultsButtonCheck() {
+    String notValidQuery = "some not valid request";
+    navigateToKsqlDb();
+    ksqlDbList
+        .clickExecuteKsqlRequestBtn();
+    ksqlQueryForm
+        .waitUntilScreenReady()
+        .setQuery(notValidQuery);
+    Assert.assertFalse(ksqlQueryForm.isClearResultsBtnEnabled(), "isClearResultsBtnEnabled()");
+    ksqlQueryForm
+        .clickExecuteBtn(notValidQuery);
+    Assert.assertFalse(ksqlQueryForm.isClearResultsBtnEnabled(), "isClearResultsBtnEnabled()");
+  }
+
+  @QaseId(41)
+  @Test(priority = 4)
   public void checkShowTablesRequestExecution() {
     navigateToKsqlDbAndExecuteRequest(SHOW_TABLES.getQuery());
     SoftAssert softly = new SoftAssert();
@@ -83,7 +95,7 @@ public class KsqlDbTest extends BaseTest {
   }
 
   @QaseId(278)
-  @Test(priority = 4)
+  @Test(priority = 5)
   public void checkShowStreamsRequestExecution() {
     navigateToKsqlDbAndExecuteRequest(SHOW_STREAMS.getQuery());
     SoftAssert softly = new SoftAssert();
@@ -94,7 +106,7 @@ public class KsqlDbTest extends BaseTest {
   }
 
   @QaseId(86)
-  @Test(priority = 5)
+  @Test(priority = 6)
   public void clearResultsForExecutedRequest() {
     navigateToKsqlDbAndExecuteRequest(SHOW_TABLES.getQuery());
     SoftAssert softly = new SoftAssert();
@@ -107,7 +119,7 @@ public class KsqlDbTest extends BaseTest {
   }
 
   @QaseId(277)
-  @Test(priority = 6)
+  @Test(priority = 7)
   public void stopQueryFunctionalCheck() {
     navigateToKsqlDbAndExecuteRequest(String.format(SELECT_ALL_FROM.getQuery(), FIRST_TABLE.getName()));
     Assert.assertTrue(ksqlQueryForm.isAbortBtnVisible(), "isAbortBtnVisible()");
@@ -123,10 +135,8 @@ public class KsqlDbTest extends BaseTest {
 
   @Step
   private void navigateToKsqlDbAndExecuteRequest(String query) {
-    naviSideBar
-        .openSideMenu(KSQL_DB);
+    navigateToKsqlDb();
     ksqlDbList
-        .waitUntilScreenReady()
         .clickExecuteKsqlRequestBtn();
     ksqlQueryForm
         .waitUntilScreenReady()

+ 13 - 0
kafka-ui-react-app/src/components/ACLPage/ACLPage.tsx

@@ -0,0 +1,13 @@
+import React from 'react';
+import { Routes, Route } from 'react-router-dom';
+import ACList from 'components/ACLPage/List/List';
+
+const ACLPage = () => {
+  return (
+    <Routes>
+      <Route index element={<ACList />} />
+    </Routes>
+  );
+};
+
+export default ACLPage;

+ 44 - 0
kafka-ui-react-app/src/components/ACLPage/List/List.styled.ts

@@ -0,0 +1,44 @@
+import styled from 'styled-components';
+
+export const EnumCell = styled.div`
+  text-transform: capitalize;
+`;
+
+export const DeleteCell = styled.div`
+  svg {
+    cursor: pointer;
+  }
+`;
+
+export const Chip = styled.div<{
+  chipType?: 'default' | 'success' | 'danger' | 'secondary' | string;
+}>`
+  width: fit-content;
+  text-transform: capitalize;
+  padding: 2px 8px;
+  font-size: 12px;
+  line-height: 16px;
+  border-radius: 16px;
+  color: ${({ theme }) => theme.tag.color};
+  background-color: ${({ theme, chipType }) => {
+    switch (chipType) {
+      case 'success':
+        return theme.tag.backgroundColor.green;
+      case 'danger':
+        return theme.tag.backgroundColor.red;
+      case 'secondary':
+        return theme.tag.backgroundColor.secondary;
+      default:
+        return theme.tag.backgroundColor.gray;
+    }
+  }};
+`;
+
+export const PatternCell = styled.div`
+  display: flex;
+  align-items: center;
+
+  ${Chip} {
+    margin-left: 4px;
+  }
+`;

+ 153 - 0
kafka-ui-react-app/src/components/ACLPage/List/List.tsx

@@ -0,0 +1,153 @@
+import React from 'react';
+import { ColumnDef } from '@tanstack/react-table';
+import { useTheme } from 'styled-components';
+import PageHeading from 'components/common/PageHeading/PageHeading';
+import Table from 'components/common/NewTable';
+import DeleteIcon from 'components/common/Icons/DeleteIcon';
+import { useConfirm } from 'lib/hooks/useConfirm';
+import useAppParams from 'lib/hooks/useAppParams';
+import { useAcls, useDeleteAcl } from 'lib/hooks/api/acl';
+import { ClusterName } from 'redux/interfaces';
+import {
+  KafkaAcl,
+  KafkaAclNamePatternType,
+  KafkaAclPermissionEnum,
+} from 'generated-sources';
+
+import * as S from './List.styled';
+
+const ACList: React.FC = () => {
+  const { clusterName } = useAppParams<{ clusterName: ClusterName }>();
+  const theme = useTheme();
+  const { data: aclList } = useAcls(clusterName);
+  const { deleteResource } = useDeleteAcl(clusterName);
+  const modal = useConfirm(true);
+
+  const [rowId, setRowId] = React.useState('');
+
+  const onDeleteClick = (acl: KafkaAcl | null) => {
+    if (acl) {
+      modal('Are you sure want to delete this ACL record?', () =>
+        deleteResource(acl)
+      );
+    }
+  };
+
+  const columns = React.useMemo<ColumnDef<KafkaAcl>[]>(
+    () => [
+      {
+        header: 'Principal',
+        accessorKey: 'principal',
+        size: 257,
+      },
+      {
+        header: 'Resource',
+        accessorKey: 'resourceType',
+        // eslint-disable-next-line react/no-unstable-nested-components
+        cell: ({ getValue }) => (
+          <S.EnumCell>{getValue<string>().toLowerCase()}</S.EnumCell>
+        ),
+        size: 145,
+      },
+      {
+        header: 'Pattern',
+        accessorKey: 'resourceName',
+        // eslint-disable-next-line react/no-unstable-nested-components
+        cell: ({ getValue, row }) => {
+          let chipType;
+          if (
+            row.original.namePatternType === KafkaAclNamePatternType.PREFIXED
+          ) {
+            chipType = 'default';
+          }
+
+          if (
+            row.original.namePatternType === KafkaAclNamePatternType.LITERAL
+          ) {
+            chipType = 'secondary';
+          }
+          return (
+            <S.PatternCell>
+              {getValue<string>()}
+              {chipType ? (
+                <S.Chip chipType={chipType}>
+                  {row.original.namePatternType.toLowerCase()}
+                </S.Chip>
+              ) : null}
+            </S.PatternCell>
+          );
+        },
+        size: 257,
+      },
+      {
+        header: 'Host',
+        accessorKey: 'host',
+        size: 257,
+      },
+      {
+        header: 'Operation',
+        accessorKey: 'operation',
+        // eslint-disable-next-line react/no-unstable-nested-components
+        cell: ({ getValue }) => (
+          <S.EnumCell>{getValue<string>().toLowerCase()}</S.EnumCell>
+        ),
+        size: 121,
+      },
+      {
+        header: 'Permission',
+        accessorKey: 'permission',
+        // eslint-disable-next-line react/no-unstable-nested-components
+        cell: ({ getValue }) => (
+          <S.Chip
+            chipType={
+              getValue<string>() === KafkaAclPermissionEnum.ALLOW
+                ? 'success'
+                : 'danger'
+            }
+          >
+            {getValue<string>().toLowerCase()}
+          </S.Chip>
+        ),
+        size: 111,
+      },
+      {
+        id: 'delete',
+        // eslint-disable-next-line react/no-unstable-nested-components
+        cell: ({ row }) => {
+          return (
+            <S.DeleteCell onClick={() => onDeleteClick(row.original)}>
+              <DeleteIcon
+                fill={
+                  rowId === row.id ? theme.acl.table.deleteIcon : 'transparent'
+                }
+              />
+            </S.DeleteCell>
+          );
+        },
+        size: 76,
+      },
+    ],
+    [rowId]
+  );
+
+  const onRowHover = (value: unknown) => {
+    if (value && typeof value === 'object' && 'id' in value) {
+      setRowId(value.id as string);
+    }
+  };
+
+  return (
+    <>
+      <PageHeading text="Access Control List" />
+      <Table
+        columns={columns}
+        data={aclList ?? []}
+        emptyMessage="No ACL items found"
+        onRowHover={onRowHover}
+        onMouseLeave={() => setRowId('')}
+      />
+    </>
+  );
+};
+
+export default ACList;

+ 74 - 0
kafka-ui-react-app/src/components/ACLPage/List/__test__/List.spec.tsx

@@ -0,0 +1,74 @@
+import React from 'react';
+import { render, WithRoute } from 'lib/testHelpers';
+import { screen } from '@testing-library/dom';
+import userEvent from '@testing-library/user-event';
+import { clusterACLPath } from 'lib/paths';
+import ACList from 'components/ACLPage/List/List';
+import { useAcls, useDeleteAcl } from 'lib/hooks/api/acl';
+import { aclPayload } from 'lib/fixtures/acls';
+
+jest.mock('lib/hooks/api/acl', () => ({
+  useAcls: jest.fn(),
+  useDeleteAcl: jest.fn(),
+}));
+
+describe('ACLList Component', () => {
+  const clusterName = 'local';
+  const renderComponent = () =>
+    render(
+      <WithRoute path={clusterACLPath()}>
+        <ACList />
+      </WithRoute>,
+      {
+        initialEntries: [clusterACLPath(clusterName)],
+      }
+    );
+
+  describe('ACLList', () => {
+    describe('when the acls are loaded', () => {
+      beforeEach(() => {
+        (useAcls as jest.Mock).mockImplementation(() => ({
+          data: aclPayload,
+        }));
+        (useDeleteAcl as jest.Mock).mockImplementation(() => ({
+          deleteResource: jest.fn(),
+        }));
+      });
+
+      it('renders ACLList with records', async () => {
+        renderComponent();
+        expect(screen.getByRole('table')).toBeInTheDocument();
+        expect(screen.getAllByRole('row').length).toEqual(4);
+      });
+
+      it('shows delete icon on hover', async () => {
+        const { container } = renderComponent();
+        const [trElement] = screen.getAllByRole('row');
+        await userEvent.hover(trElement);
+        const deleteElement = container.querySelector('svg');
+        expect(deleteElement).not.toHaveStyle({
+          fill: 'transparent',
+        });
+      });
+    });
+
+    describe('when it has no acls', () => {
+      beforeEach(() => {
+        (useAcls as jest.Mock).mockImplementation(() => ({
+          data: [],
+        }));
+        (useDeleteAcl as jest.Mock).mockImplementation(() => ({
+          deleteResource: jest.fn(),
+        }));
+      });
+
+      it('renders empty ACLList with message', async () => {
+        renderComponent();
+        expect(screen.getByRole('table')).toBeInTheDocument();
+        expect(
+          screen.getByRole('row', { name: 'No ACL items found' })
+        ).toBeInTheDocument();
+      });
+    });
+  });
+});

+ 11 - 0
kafka-ui-react-app/src/components/ClusterPage/ClusterPage.tsx

@@ -13,6 +13,7 @@ import {
   clusterTopicsRelativePath,
   clusterConfigRelativePath,
   getNonExactPath,
+  clusterAclRelativePath,
 } from 'lib/paths';
 import ClusterContext from 'components/contexts/ClusterContext';
 import PageLoader from 'components/common/PageLoader/PageLoader';
@@ -30,6 +31,7 @@ const ClusterConfigPage = React.lazy(
 const ConsumerGroups = React.lazy(
   () => import('components/ConsumerGroups/ConsumerGroups')
 );
+const AclPage = React.lazy(() => import('components/ACLPage/ACLPage'));
 
 const ClusterPage: React.FC = () => {
   const { clusterName } = useAppParams<ClusterNameRoute>();
@@ -51,6 +53,9 @@ const ClusterPage: React.FC = () => {
         ClusterFeaturesEnum.TOPIC_DELETION
       ),
       hasKsqlDbConfigured: features.includes(ClusterFeaturesEnum.KSQL_DB),
+      hasAclViewConfigured:
+        features.includes(ClusterFeaturesEnum.KAFKA_ACL_VIEW) ||
+        features.includes(ClusterFeaturesEnum.KAFKA_ACL_EDIT),
     };
   }, [clusterName, data]);
 
@@ -95,6 +100,12 @@ const ClusterPage: React.FC = () => {
                 element={<KsqlDb />}
               />
             )}
+            {contextValue.hasAclViewConfigured && (
+              <Route
+                path={getNonExactPath(clusterAclRelativePath)}
+                element={<AclPage />}
+              />
+            )}
             {appInfo.hasDynamicConfig && (
               <Route
                 path={getNonExactPath(clusterConfigRelativePath)}

+ 5 - 0
kafka-ui-react-app/src/components/Nav/ClusterMenu.tsx

@@ -7,6 +7,7 @@ import {
   clusterSchemasPath,
   clusterConnectorsPath,
   clusterKsqlDbPath,
+  clusterACLPath,
 } from 'lib/paths';
 
 import ClusterMenuItem from './ClusterMenuItem';
@@ -57,6 +58,10 @@ const ClusterMenu: React.FC<Props> = ({
           {hasFeatureConfigured(ClusterFeaturesEnum.KSQL_DB) && (
             <ClusterMenuItem to={clusterKsqlDbPath(name)} title="KSQL DB" />
           )}
+          {(hasFeatureConfigured(ClusterFeaturesEnum.KAFKA_ACL_VIEW) ||
+            hasFeatureConfigured(ClusterFeaturesEnum.KAFKA_ACL_EDIT)) && (
+            <ClusterMenuItem to={clusterACLPath(name)} title="ACL" />
+          )}
         </S.List>
       )}
     </S.List>

+ 1 - 1
kafka-ui-react-app/src/components/common/Button/Button.styled.ts

@@ -1,7 +1,7 @@
 import styled from 'styled-components';
 
 export interface ButtonProps {
-  buttonType: 'primary' | 'secondary';
+  buttonType: 'primary' | 'secondary' | 'danger';
   buttonSize: 'S' | 'M' | 'L';
   isInverted?: boolean;
 }

+ 1 - 1
kafka-ui-react-app/src/components/common/ConfirmationModal/ConfirmationModal.tsx

@@ -26,7 +26,7 @@ const ConfirmationModal: React.FC = () => {
             Cancel
           </Button>
           <Button
-            buttonType="primary"
+            buttonType={context.dangerButton ? 'danger' : 'primary'}
             buttonSize="M"
             onClick={context.confirm}
             type="button"

+ 3 - 2
kafka-ui-react-app/src/components/common/Icons/DeleteIcon.tsx

@@ -1,13 +1,14 @@
 import React from 'react';
 import { useTheme } from 'styled-components';
 
-const DeleteIcon: React.FC = () => {
+const DeleteIcon: React.FC<{ fill?: string }> = ({ fill }) => {
   const theme = useTheme();
+  const curentFill = fill || theme.editFilter.deleteIconColor;
   return (
     <svg
       xmlns="http://www.w3.org/2000/svg"
       viewBox="0 0 448 512"
-      fill={theme.editFilter.deleteIconColor}
+      fill={curentFill}
       width="14"
       height="14"
     >

+ 8 - 4
kafka-ui-react-app/src/components/common/MultiSelect/MultiSelect.styled.ts

@@ -1,9 +1,12 @@
 import styled from 'styled-components';
 import { MultiSelect as ReactMultiSelect } from 'react-multi-select-component';
 
-const MultiSelect = styled(ReactMultiSelect)<{ minWidth?: string }>`
+const MultiSelect = styled(ReactMultiSelect)<{
+  minWidth?: string;
+  height?: string;
+}>`
   min-width: ${({ minWidth }) => minWidth || '200px;'};
-  height: 32px;
+  height: ${({ height }) => height ?? '32px'};
   font-size: 14px;
   .search input {
     color: ${({ theme }) => theme.input.color.normal};
@@ -36,13 +39,14 @@ const MultiSelect = styled(ReactMultiSelect)<{ minWidth?: string }>`
     &:hover {
       border-color: ${({ theme }) => theme.select.borderColor.hover} !important;
     }
-    height: 32px;
+
+    height: ${({ height }) => height ?? '32px'};
     * {
       cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
     }
 
     & > .dropdown-heading {
-      height: 32px;
+      height: ${({ height }) => height ?? '32px'};
       color: ${({ disabled, theme }) =>
         disabled
           ? theme.select.color.disabled

+ 35 - 1
kafka-ui-react-app/src/components/common/NewTable/Table.tsx

@@ -52,6 +52,9 @@ export interface TableProps<TData> {
 
   // Handles row click. Can not be combined with `enableRowSelection` && expandable rows.
   onRowClick?: (row: Row<TData>) => void;
+
+  onRowHover?: (row: Row<TData>) => void;
+  onMouseLeave?: () => void;
 }
 
 type UpdaterFn<T> = (previousState: T) => T;
@@ -127,6 +130,8 @@ const Table: React.FC<TableProps<any>> = ({
   emptyMessage,
   disabled,
   onRowClick,
+  onRowHover,
+  onMouseLeave,
 }) => {
   const [searchParams, setSearchParams] = useSearchParams();
   const location = useLocation();
@@ -194,6 +199,21 @@ const Table: React.FC<TableProps<any>> = ({
     return undefined;
   };
 
+  const handleRowHover = (row: Row<typeof data>) => (e: React.MouseEvent) => {
+    if (onRowHover) {
+      e.stopPropagation();
+      return onRowHover(row);
+    }
+
+    return undefined;
+  };
+
+  const handleMouseLeave = () => {
+    if (onMouseLeave) {
+      onMouseLeave();
+    }
+  };
+
   return (
     <>
       {BatchActionsBar && (
@@ -227,6 +247,12 @@ const Table: React.FC<TableProps<any>> = ({
                     sortable={header.column.getCanSort()}
                     sortOrder={header.column.getIsSorted()}
                     onClick={header.column.getToggleSortingHandler()}
+                    style={{
+                      width:
+                        header.column.getSize() !== 150
+                          ? header.column.getSize()
+                          : undefined,
+                    }}
                   >
                     <div>
                       {flexRender(
@@ -245,6 +271,8 @@ const Table: React.FC<TableProps<any>> = ({
                 <S.Row
                   expanded={row.getIsExpanded()}
                   onClick={handleRowClick(row)}
+                  onMouseOver={onRowHover ? handleRowHover(row) : undefined}
+                  onMouseLeave={onMouseLeave ? handleMouseLeave : undefined}
                   clickable={
                     !enableRowSelection &&
                     (row.getCanExpand() || onRowClick !== undefined)
@@ -269,7 +297,13 @@ const Table: React.FC<TableProps<any>> = ({
                   {row
                     .getVisibleCells()
                     .map(({ id, getContext, column: { columnDef } }) => (
-                      <td key={id} style={columnDef.meta}>
+                      <td
+                        key={id}
+                        style={{
+                          width:
+                            columnDef.size !== 150 ? columnDef.size : undefined,
+                        }}
+                      >
                         {flexRender(columnDef.cell, getContext())}
                       </td>
                     ))}

+ 5 - 0
kafka-ui-react-app/src/components/contexts/ConfirmContext.tsx

@@ -6,6 +6,8 @@ interface ConfirmContextType {
   setContent: React.Dispatch<React.SetStateAction<React.ReactNode>>;
   setConfirm: React.Dispatch<React.SetStateAction<(() => void) | undefined>>;
   cancel: () => void;
+  dangerButton: boolean;
+  setDangerButton: React.Dispatch<React.SetStateAction<boolean>>;
 }
 
 export const ConfirmContext = React.createContext<ConfirmContextType | null>(
@@ -17,6 +19,7 @@ export const ConfirmContextProvider: React.FC<
 > = ({ children }) => {
   const [content, setContent] = useState<React.ReactNode>(null);
   const [confirm, setConfirm] = useState<(() => void) | undefined>(undefined);
+  const [dangerButton, setDangerButton] = useState(false);
 
   const cancel = () => {
     setContent(null);
@@ -31,6 +34,8 @@ export const ConfirmContextProvider: React.FC<
         confirm,
         setConfirm,
         cancel,
+        dangerButton,
+        setDangerButton,
       }}
     >
       {children}

+ 2 - 0
kafka-ui-react-app/src/lib/api.ts

@@ -10,6 +10,7 @@ import {
   ConsumerGroupsApi,
   AuthorizationApi,
   ApplicationConfigApi,
+  AclsApi,
 } from 'generated-sources';
 import { BASE_PARAMS } from 'lib/constants';
 
@@ -25,3 +26,4 @@ export const kafkaConnectApiClient = new KafkaConnectApi(apiClientConf);
 export const consumerGroupsApiClient = new ConsumerGroupsApi(apiClientConf);
 export const authApiClient = new AuthorizationApi(apiClientConf);
 export const appConfigApiClient = new ApplicationConfigApi(apiClientConf);
+export const aclApiClient = new AclsApi(apiClientConf);

+ 37 - 0
kafka-ui-react-app/src/lib/fixtures/acls.ts

@@ -0,0 +1,37 @@
+import {
+  KafkaAcl,
+  KafkaAclResourceType,
+  KafkaAclNamePatternType,
+  KafkaAclPermissionEnum,
+  KafkaAclOperationEnum,
+} from 'generated-sources';
+
+export const aclPayload: KafkaAcl[] = [
+  {
+    principal: 'User 1',
+    resourceName: 'Topic',
+    resourceType: KafkaAclResourceType.TOPIC,
+    host: '_host1',
+    namePatternType: KafkaAclNamePatternType.LITERAL,
+    permission: KafkaAclPermissionEnum.ALLOW,
+    operation: KafkaAclOperationEnum.READ,
+  },
+  {
+    principal: 'User 2',
+    resourceName: 'Topic',
+    resourceType: KafkaAclResourceType.TOPIC,
+    host: '_host1',
+    namePatternType: KafkaAclNamePatternType.PREFIXED,
+    permission: KafkaAclPermissionEnum.ALLOW,
+    operation: KafkaAclOperationEnum.READ,
+  },
+  {
+    principal: 'User 3',
+    resourceName: 'Topic',
+    resourceType: KafkaAclResourceType.TOPIC,
+    host: '_host1',
+    namePatternType: KafkaAclNamePatternType.LITERAL,
+    permission: KafkaAclPermissionEnum.DENY,
+    operation: KafkaAclOperationEnum.READ,
+  },
+];

+ 67 - 0
kafka-ui-react-app/src/lib/hooks/api/acl.ts

@@ -0,0 +1,67 @@
+import { aclApiClient as api } from 'lib/api';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { ClusterName } from 'redux/interfaces';
+import { showSuccessAlert } from 'lib/errorHandling';
+import { KafkaAcl } from 'generated-sources';
+
+export function useAcls(clusterName: ClusterName) {
+  return useQuery(
+    ['clusters', clusterName, 'acls'],
+    () => api.listAcls({ clusterName }),
+    {
+      suspense: false,
+    }
+  );
+}
+
+export function useCreateAclMutation(clusterName: ClusterName) {
+  return useMutation(
+    (data: KafkaAcl) =>
+      api.createAcl({
+        clusterName,
+        kafkaAcl: data,
+      }),
+    {
+      onSuccess() {
+        showSuccessAlert({
+          message: 'Your ACL was created successfully',
+        });
+      },
+    }
+  );
+}
+
+export function useCreateAcl(clusterName: ClusterName) {
+  const mutate = useCreateAclMutation(clusterName);
+
+  return {
+    createResource: async (param: KafkaAcl) => {
+      return mutate.mutateAsync(param);
+    },
+    ...mutate,
+  };
+}
+
+export function useDeleteAclMutation(clusterName: ClusterName) {
+  const queryClient = useQueryClient();
+  return useMutation(
+    (acl: KafkaAcl) => api.deleteAcl({ clusterName, kafkaAcl: acl }),
+    {
+      onSuccess: () => {
+        showSuccessAlert({ message: 'ACL deleted' });
+        queryClient.invalidateQueries(['clusters', clusterName, 'acls']);
+      },
+    }
+  );
+}
+
+export function useDeleteAcl(clusterName: ClusterName) {
+  const mutate = useDeleteAclMutation(clusterName);
+
+  return {
+    deleteResource: async (param: KafkaAcl) => {
+      return mutate.mutateAsync(param);
+    },
+    ...mutate,
+  };
+}

+ 2 - 1
kafka-ui-react-app/src/lib/hooks/useConfirm.ts

@@ -1,12 +1,13 @@
 import { ConfirmContext } from 'components/contexts/ConfirmContext';
 import React, { useContext } from 'react';
 
-export const useConfirm = () => {
+export const useConfirm = (danger = false) => {
   const context = useContext(ConfirmContext);
   return (
     message: React.ReactNode,
     callback: () => void | Promise<unknown>
   ) => {
+    context?.setDangerButton(danger);
     context?.setContent(message);
     context?.setConfirm(() => async () => {
       await callback();

+ 7 - 0
kafka-ui-react-app/src/lib/paths.ts

@@ -285,3 +285,10 @@ export const clusterConfigPath = (
 
 const clusterNewConfigRelativePath = 'create-new-cluster';
 export const clusterNewConfigPath = `/ui/clusters/${clusterNewConfigRelativePath}`;
+
+// ACL
+export const clusterAclRelativePath = 'acl';
+export const clusterAclNewRelativePath = 'create-new-acl';
+export const clusterACLPath = (
+  clusterName: ClusterName = RouteParams.clusterName
+) => `${clusterPath(clusterName)}/${clusterAclRelativePath}`;

+ 82 - 4
kafka-ui-react-app/src/theme/theme.ts

@@ -31,6 +31,7 @@ const Colors = {
     '15': '#C2F0D1',
     '30': '#85E0A3',
     '40': '#5CD685',
+    '50': '#33CC66',
     '60': '#29A352',
   },
   brand: {
@@ -231,6 +232,7 @@ const baseTheme = {
       white: Colors.neutral[10],
       red: Colors.red[10],
       blue: Colors.blue[10],
+      secondary: Colors.neutral[15],
     },
     color: Colors.neutral[90],
   },
@@ -416,8 +418,8 @@ export const theme = {
         disabled: Colors.red[20],
       },
       color: {
-        normal: Colors.neutral[90],
-        disabled: Colors.neutral[30],
+        normal: Colors.neutral[0],
+        disabled: Colors.neutral[0],
       },
       invertedColors: {
         normal: Colors.brand[50],
@@ -695,6 +697,44 @@ export const theme = {
     textColor: Colors.brand[50],
     deleteIconColor: Colors.brand[50],
   },
+  acl: {
+    table: {
+      deleteIcon: Colors.neutral[50],
+    },
+    create: {
+      radioButtons: {
+        green: {
+          normal: {
+            background: Colors.neutral[0],
+            text: Colors.neutral[50],
+          },
+          active: {
+            background: Colors.green[50],
+            text: Colors.neutral[0],
+          },
+          hover: {
+            background: Colors.green[10],
+            text: Colors.neutral[90],
+          },
+        },
+        gray: {
+          normal: {
+            background: Colors.neutral[0],
+            text: Colors.neutral[50],
+          },
+          active: {
+            background: Colors.neutral[10],
+            text: Colors.neutral[90],
+          },
+          hover: {
+            background: Colors.neutral[5],
+            text: Colors.neutral[90],
+          },
+        },
+        red: {},
+      },
+    },
+  },
 };
 
 export type ThemeType = typeof theme;
@@ -818,8 +858,8 @@ export const darkTheme: ThemeType = {
         disabled: Colors.red[20],
       },
       color: {
-        normal: Colors.neutral[90],
-        disabled: Colors.neutral[30],
+        normal: Colors.neutral[0],
+        disabled: Colors.neutral[0],
       },
       invertedColors: {
         normal: Colors.brand[50],
@@ -1155,4 +1195,42 @@ export const darkTheme: ThemeType = {
       color: Colors.neutral[0],
     },
   },
+  acl: {
+    table: {
+      deleteIcon: Colors.neutral[50],
+    },
+    create: {
+      radioButtons: {
+        green: {
+          normal: {
+            background: Colors.neutral[0],
+            text: Colors.neutral[50],
+          },
+          active: {
+            background: Colors.green[50],
+            text: Colors.neutral[0],
+          },
+          hover: {
+            background: Colors.green[10],
+            text: Colors.neutral[0],
+          },
+        },
+        gray: {
+          normal: {
+            background: Colors.neutral[0],
+            text: Colors.neutral[50],
+          },
+          active: {
+            background: Colors.neutral[10],
+            text: Colors.neutral[90],
+          },
+          hover: {
+            background: Colors.neutral[5],
+            text: Colors.neutral[90],
+          },
+        },
+        red: {},
+      },
+    },
+  },
 };