Ver Fonte

Merge branch 'master' into Topic_custom_params_are_disabled_upon_editing

David há 2 anos atrás
pai
commit
6b5be5821c
100 ficheiros alterados com 4028 adições e 1278 exclusões
  1. 5 0
      .editorconfig
  2. 0 3
      .github/workflows/release-serde-api.yaml
  3. 1 1
      .github/workflows/release.yaml
  4. 62 1
      .github/workflows/separate_env_public_create.yml
  5. 2 0
      README.md
  6. 2 2
      charts/kafka-ui/Chart.yaml
  7. 1 1
      charts/kafka-ui/templates/deployment.yaml
  8. 1 0
      kafka-ui-api/pom.xml
  9. 8 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/AuthenticatedUser.java
  10. 0 80
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/CognitoOAuthSecurityConfig.java
  11. 43 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthProperties.java
  12. 68 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthPropertiesConverter.java
  13. 101 36
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthSecurityConfig.java
  14. 30 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacOAuth2User.java
  15. 47 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacOidcUser.java
  16. 10 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacUser.java
  17. 23 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RoleBasedAccessControlProperties.java
  18. 13 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/condition/CognitoCondition.java
  19. 18 10
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/CognitoLogoutSuccessHandler.java
  20. 15 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/LogoutSuccessHandler.java
  21. 46 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/OAuthLogoutSuccessHandler.java
  22. 0 44
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/props/CognitoProperties.java
  23. 80 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AccessController.java
  24. 68 27
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/BrokersController.java
  25. 39 11
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ClustersController.java
  26. 127 69
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ConsumerGroupsController.java
  27. 0 32
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/InfoController.java
  28. 132 34
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KafkaConnectController.java
  29. 39 13
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KsqlController.java
  30. 62 19
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/MessagesController.java
  31. 140 40
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/SchemasController.java
  32. 179 65
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/TopicsController.java
  33. 2 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ErrorCode.java
  34. 134 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/AccessContext.java
  35. 72 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Permission.java
  36. 21 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Resource.java
  37. 19 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Role.java
  38. 24 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Subject.java
  39. 18 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ClusterConfigAction.java
  40. 19 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ConnectAction.java
  41. 20 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ConsumerGroupAction.java
  42. 15 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/KsqlAction.java
  43. 4 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/PermissibleAction.java
  44. 21 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/SchemaAction.java
  45. 24 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/TopicAction.java
  46. 27 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/provider/Provider.java
  47. 1 1
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/JsonSchemaSchemaRegistrySerializer.java
  48. 1 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ClusterService.java
  49. 11 5
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ConsumerGroupService.java
  50. 6 9
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConnectService.java
  51. 1 1
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ReactiveAdminClient.java
  52. 31 35
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/SchemaRegistryService.java
  53. 6 2
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/TopicsService.java
  54. 31 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AbstractProviderCondition.java
  55. 398 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AccessControlService.java
  56. 70 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/CognitoAuthorityExtractor.java
  57. 99 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/GithubAuthorityExtractor.java
  58. 69 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/GoogleAuthorityExtractor.java
  59. 23 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/LdapAuthorityExtractor.java
  60. 31 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/OauthAuthorityExtractor.java
  61. 14 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/ProviderAuthorityExtractor.java
  62. 1 1
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/annotation/KafkaClientInternalsDependant.java
  63. 21 1
      kafka-ui-api/src/main/resources/application-local.yml
  64. 38 32
      kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/SchemaRegistryPaginationTest.java
  65. 6 2
      kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/TopicsServicePaginationTest.java
  66. 23 0
      kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/AccessControlServiceMock.java
  67. 77 20
      kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml
  68. 4 1
      kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/BasePage.java
  69. 2 2
      kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/TopPanel.java
  70. 40 0
      kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersConfigTab.java
  71. 36 1
      kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersDetails.java
  72. 97 6
      kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersList.java
  73. 18 4
      kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schema/SchemaCreateForm.java
  74. 103 12
      kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicDetails.java
  75. 65 0
      kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicSettingsTab.java
  76. 108 7
      kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicsList.java
  77. 9 0
      kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/WebUtils.java
  78. 33 0
      kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/base/BaseTest.java
  79. 4 0
      kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/base/Facade.java
  80. 6 15
      kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/SmokeTests.java
  81. 33 12
      kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/brokers/BrokersTests.java
  82. 28 37
      kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/connectors/ConnectorsTests.java
  83. 37 53
      kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/schemas/SchemasTests.java
  84. 30 22
      kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/topics/TopicMessagesTests.java
  85. 245 112
      kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/topics/TopicsTests.java
  86. 0 1
      kafka-ui-react-app/package.json
  87. 0 6
      kafka-ui-react-app/pnpm-lock.yaml
  88. 1 244
      kafka-ui-react-app/src/components/App.styled.ts
  89. 41 106
      kafka-ui-react-app/src/components/App.tsx
  90. 8 3
      kafka-ui-react-app/src/components/Brokers/Broker/Configs/InputCell.tsx
  91. 65 14
      kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx
  92. 8 4
      kafka-ui-react-app/src/components/Connect/List/ListPage.tsx
  93. 1 1
      kafka-ui-react-app/src/components/Connect/New/New.tsx
  94. 2 1
      kafka-ui-react-app/src/components/Connect/New/__tests__/New.spec.tsx
  95. 23 7
      kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx
  96. 11 6
      kafka-ui-react-app/src/components/KsqlDb/List/List.tsx
  97. 5 4
      kafka-ui-react-app/src/components/KsqlDb/List/__test__/List.spec.tsx
  98. 146 0
      kafka-ui-react-app/src/components/NavBar/NavBar.styled.ts
  99. 60 0
      kafka-ui-react-app/src/components/NavBar/NavBar.tsx
  100. 19 0
      kafka-ui-react-app/src/components/NavBar/UserInfo/UserInfo.styled.ts

+ 5 - 0
.editorconfig

@@ -279,3 +279,8 @@ ij_java_wrap_long_lines = false
 insert_final_newline = false
 insert_final_newline = false
 trim_trailing_whitespace = false
 trim_trailing_whitespace = false
 
 
+[*.yaml]
+indent_size = 2
+[*.yml]
+indent_size = 2
+

+ 0 - 3
.github/workflows/release-serde-api.yaml

@@ -27,7 +27,4 @@ jobs:
 
 
       - name: Publish to Maven Central
       - name: Publish to Maven Central
         run: |
         run: |
-          MVN_VERSION=$(curl -s https://search.maven.org/solrsearch/select?q=g:"com.provectus"+AND+a:"kafka-ui-serde-api" | grep -o '"latestVersion": *"[^"]*"' | grep -o '"[^"]*"$' | sed 's/"//g')
-          MVN_VERSION=$(echo "$MVN_VERSION" | awk 'BEGIN{FS=OFS="."} {$2+=1} 1')
-          mvn -B -ntp versions:set -DnewVersion=$MVN_VERSION -pl kafka-ui-serde-api
           mvn source:jar  javadoc:jar  package  gpg:sign -Dgpg.passphrase=${{ secrets.GPG_PASSPHRASE }} -Dserver.username=${{ secrets.NEXUS_USERNAME }} -Dserver.password=${{ secrets.NEXUS_PASSWORD }} nexus-staging:deploy   -pl kafka-ui-serde-api  -s settings.xml
           mvn source:jar  javadoc:jar  package  gpg:sign -Dgpg.passphrase=${{ secrets.GPG_PASSPHRASE }} -Dserver.username=${{ secrets.NEXUS_USERNAME }} -Dserver.password=${{ secrets.NEXUS_PASSWORD }} nexus-staging:deploy   -pl kafka-ui-serde-api  -s settings.xml

+ 1 - 1
.github/workflows/release.yaml

@@ -88,7 +88,7 @@ jobs:
   charts:
   charts:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
     permissions:
     permissions:
-      actions: write
+      contents: write
     needs: release
     needs: release
     steps:
     steps:
       - name: Repository Dispatch
       - name: Repository Dispatch

+ 62 - 1
.github/workflows/separate_env_public_create.yml

@@ -8,8 +8,69 @@ on:
         default: 'demo'
         default: 'demo'
 
 
 jobs:
 jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - name: get branch name
+        id: extract_branch
+        run: |
+          tag="${{ github.event.inputs.ENV_NAME }}-$(date '+%F-%H-%M-%S')"
+          echo "tag=${tag}" >> $GITHUB_OUTPUT
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+      - name: Set up JDK
+        uses: actions/setup-java@v3
+        with:
+          java-version: '17'
+          distribution: 'zulu'
+          cache: 'maven'
+      - name: Build
+        id: build
+        run: |
+          ./mvnw -B -ntp versions:set -DnewVersion=$GITHUB_SHA
+          ./mvnw -B -V -ntp clean package -Pprod -DskipTests
+          export VERSION=$(./mvnw -q -Dexec.executable=echo -Dexec.args='${project.version}' --non-recursive exec:exec)
+          echo "version=${VERSION}" >> $GITHUB_OUTPUT
+      - name: Set up QEMU
+        uses: docker/setup-qemu-action@v2
+      - name: Set up Docker Buildx
+        id: buildx
+        uses: docker/setup-buildx-action@v2
+      - name: Cache Docker layers
+        uses: actions/cache@v3
+        with:
+          path: /tmp/.buildx-cache
+          key: ${{ runner.os }}-buildx-${{ github.sha }}
+          restore-keys: |
+            ${{ runner.os }}-buildx-
+      - name: Configure AWS credentials for Kafka-UI account
+        uses: aws-actions/configure-aws-credentials@v1
+        with:
+          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
+          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+          aws-region: eu-central-1
+      - name: Login to Amazon ECR
+        id: login-ecr
+        uses: aws-actions/amazon-ecr-login@v1
+      - name: Build and push
+        id: docker_build_and_push
+        uses: docker/build-push-action@v3
+        with:
+          builder: ${{ steps.buildx.outputs.name }}
+          context: kafka-ui-api
+          push: true
+          tags: 297478128798.dkr.ecr.eu-central-1.amazonaws.com/kafka-ui:${{ steps.extract_branch.outputs.tag }}
+          build-args: |
+            JAR_FILE=kafka-ui-api-${{ steps.build.outputs.version }}.jar
+          cache-from: type=local,src=/tmp/.buildx-cache
+          cache-to: type=local,dest=/tmp/.buildx-cache
+    outputs:
+      tag: ${{ steps.extract_branch.outputs.tag }}
+
   separate-env-create:
   separate-env-create:
     runs-on: ubuntu-latest
     runs-on: ubuntu-latest
+    needs: build
     steps:
     steps:
       - name: clone
       - name: clone
         run: |
         run: |
@@ -18,7 +79,7 @@ jobs:
       - name: separate env create
       - name: separate env create
         run: |
         run: |
           cd kafka-ui-infra/aws-infrastructure4eks/argocd/scripts
           cd kafka-ui-infra/aws-infrastructure4eks/argocd/scripts
-          bash separate_env_create.sh ${{ github.event.inputs.ENV_NAME }} ${{ secrets.FEATURE_TESTING_UI_PASSWORD }}
+          bash separate_env_create.sh ${{ github.event.inputs.ENV_NAME }} ${{ secrets.FEATURE_TESTING_UI_PASSWORD }} ${{ needs.build.outputs.tag }}
           git config --global user.email "kafka-ui-infra@provectus.com"
           git config --global user.email "kafka-ui-infra@provectus.com"
           git config --global user.name "kafka-ui-infra"
           git config --global user.name "kafka-ui-infra"
           git add -A
           git add -A

+ 2 - 0
README.md

@@ -31,6 +31,8 @@ the cloud.
 * **Dynamic Topic Configuration** — create and configure new topics with dynamic configuration
 * **Dynamic Topic Configuration** — create and configure new topics with dynamic configuration
 * **Configurable Authentification** — secure your installation with optional Github/Gitlab/Google OAuth 2.0
 * **Configurable Authentification** — secure your installation with optional Github/Gitlab/Google OAuth 2.0
 * **Custom serialization/deserialization plugins** - use a ready-to-go serde for your data like AWS Glue or Smile, or code your own!
 * **Custom serialization/deserialization plugins** - use a ready-to-go serde for your data like AWS Glue or Smile, or code your own!
+* **Role based access control** - [manage permissions](https://github.com/provectus/kafka-ui/wiki/RBAC-(role-based-access-control)) to access the UI with granular precision
+* **Data masking** - [obfuscate](https://github.com/provectus/kafka-ui/blob/master/documentation/guides/DataMasking.md) sensitive data in topic messages
 
 
 # The Interface
 # The Interface
 UI for Apache Kafka wraps major functions of Apache Kafka with an intuitive user interface.
 UI for Apache Kafka wraps major functions of Apache Kafka with an intuitive user interface.

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

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

+ 1 - 1
charts/kafka-ui/templates/deployment.yaml

@@ -53,7 +53,7 @@ spec:
               {{- toYaml . | nindent 12 }}
               {{- toYaml . | nindent 12 }}
             {{- end }}
             {{- end }}
             {{- if or .Values.yamlApplicationConfig .Values.yamlApplicationConfigConfigMap}}
             {{- if or .Values.yamlApplicationConfig .Values.yamlApplicationConfigConfigMap}}
-            - name: SPRING_CONFIG_LOCATION
+            - name: SPRING_CONFIG_ADDITIONAL-LOCATION
               {{- if .Values.yamlApplicationConfig }}
               {{- if .Values.yamlApplicationConfig }}
               value: /kafka-ui/config.yml
               value: /kafka-ui/config.yml
               {{- else if .Values.yamlApplicationConfigConfigMap }}
               {{- else if .Values.yamlApplicationConfigConfigMap }}

+ 1 - 0
kafka-ui-api/pom.xml

@@ -306,6 +306,7 @@
                         </configuration>
                         </configuration>
                     </execution>
                     </execution>
                 </executions>
                 </executions>
+
             </plugin>
             </plugin>
             <plugin>
             <plugin>
                 <groupId>org.antlr</groupId>
                 <groupId>org.antlr</groupId>

+ 8 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/AuthenticatedUser.java

@@ -0,0 +1,8 @@
+package com.provectus.kafka.ui.config.auth;
+
+import java.util.Collection;
+import lombok.Value;
+
+public record AuthenticatedUser(String principal, Collection<String> groups) {
+
+}

+ 0 - 80
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/CognitoOAuthSecurityConfig.java

@@ -1,80 +0,0 @@
-package com.provectus.kafka.ui.config.auth;
-
-import com.provectus.kafka.ui.config.CognitoOidcLogoutSuccessHandler;
-import com.provectus.kafka.ui.config.auth.props.CognitoProperties;
-import java.util.Optional;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
-import org.springframework.security.config.web.server.ServerHttpSecurity;
-import org.springframework.security.oauth2.client.registration.ClientRegistration;
-import org.springframework.security.oauth2.client.registration.ClientRegistrations;
-import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
-import org.springframework.security.web.server.SecurityWebFilterChain;
-import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
-
-@Configuration
-@EnableWebFluxSecurity
-@ConditionalOnProperty(value = "auth.type", havingValue = "OAUTH2_COGNITO")
-@RequiredArgsConstructor
-@Slf4j
-public class CognitoOAuthSecurityConfig extends AbstractAuthSecurityConfig {
-
-  private static final String COGNITO = "cognito";
-
-  @Bean
-  public SecurityWebFilterChain configure(ServerHttpSecurity http, CognitoProperties props) {
-    log.info("Configuring Cognito OAUTH2 authentication.");
-
-    String clientId = props.getClientId();
-    String logoutUrl = props.getLogoutUri();
-
-    final ServerLogoutSuccessHandler logoutHandler = new CognitoOidcLogoutSuccessHandler(logoutUrl, clientId);
-
-    return http.authorizeExchange()
-        .pathMatchers(AUTH_WHITELIST)
-        .permitAll()
-        .anyExchange()
-        .authenticated()
-
-        .and()
-        .oauth2Login()
-
-        .and()
-        .oauth2Client()
-
-        .and()
-        .logout()
-        .logoutSuccessHandler(logoutHandler)
-
-        .and()
-        .csrf().disable()
-        .build();
-  }
-
-  @Bean
-  public InMemoryReactiveClientRegistrationRepository clientRegistrationRepository(CognitoProperties props) {
-    ClientRegistration.Builder builder = ClientRegistrations
-        .fromIssuerLocation(props.getIssuerUri())
-        .registrationId(COGNITO);
-
-    builder.clientId(props.getClientId());
-    builder.clientSecret(props.getClientSecret());
-
-    Optional.ofNullable(props.getScope()).ifPresent(builder::scope);
-    Optional.ofNullable(props.getUserNameAttribute()).ifPresent(builder::userNameAttributeName);
-
-    return new InMemoryReactiveClientRegistrationRepository(builder.build());
-  }
-
-  @Bean
-  @ConfigurationProperties("auth.cognito")
-  public CognitoProperties cognitoProperties() {
-    return new CognitoProperties();
-  }
-
-}

+ 43 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthProperties.java

@@ -0,0 +1,43 @@
+package com.provectus.kafka.ui.config.auth;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.PostConstruct;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.util.Assert;
+
+@ConfigurationProperties("auth.oauth2")
+@Data
+public class OAuthProperties {
+  private Map<String, OAuth2Provider> client = new HashMap<>();
+
+  @PostConstruct
+  public void validate() {
+    getClient().values().forEach(this::validateProvider);
+  }
+
+  private void validateProvider(final OAuth2Provider provider) {
+    Assert.hasText(provider.getClientId(), "Client id must not be empty.");
+    Assert.hasText(provider.getProvider(), "Provider name must not be empty");
+  }
+
+  @Data
+  public static class OAuth2Provider {
+    private String provider;
+    private String clientId;
+    private String clientSecret;
+    private String clientName;
+    private String redirectUri;
+    private String authorizationGrantType;
+    private Set<String> scope;
+    private String issuerUri;
+    private String authorizationUri;
+    private String tokenUri;
+    private String userInfoUri;
+    private String jwkSetUri;
+    private String userNameAttribute;
+    private Map<String, String> customParams;
+  }
+}

+ 68 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthPropertiesConverter.java

@@ -0,0 +1,68 @@
+package com.provectus.kafka.ui.config.auth;
+
+import static com.provectus.kafka.ui.config.auth.OAuthProperties.OAuth2Provider;
+import static org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Provider;
+import static org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties.Registration;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class OAuthPropertiesConverter {
+
+  private static final String TYPE = "type";
+  private static final String GOOGLE = "google";
+
+  public static OAuth2ClientProperties convertProperties(final OAuthProperties properties) {
+    final var result = new OAuth2ClientProperties();
+    properties.getClient().forEach((key, provider) -> {
+      var registration = new Registration();
+      registration.setClientId(provider.getClientId());
+      registration.setClientSecret(provider.getClientSecret());
+      registration.setClientName(provider.getClientName());
+      registration.setScope(provider.getScope());
+      registration.setRedirectUri(provider.getRedirectUri());
+      registration.setAuthorizationGrantType(provider.getAuthorizationGrantType());
+
+      result.getRegistration().put(key, registration);
+
+      var clientProvider = new Provider();
+      applyCustomTransformations(provider);
+
+      clientProvider.setAuthorizationUri(provider.getAuthorizationUri());
+      clientProvider.setIssuerUri(provider.getIssuerUri());
+      clientProvider.setJwkSetUri(provider.getJwkSetUri());
+      clientProvider.setTokenUri(provider.getTokenUri());
+      clientProvider.setUserInfoUri(provider.getUserInfoUri());
+      clientProvider.setUserNameAttribute(provider.getUserNameAttribute());
+
+      result.getProvider().put(key, clientProvider);
+    });
+    return result;
+  }
+
+  private static void applyCustomTransformations(OAuth2Provider provider) {
+    applyGoogleTransformations(provider);
+  }
+
+  private static void applyGoogleTransformations(OAuth2Provider provider) {
+    if (!isGoogle(provider)) {
+      return;
+    }
+
+    String allowedDomain = provider.getCustomParams().get("allowedDomain");
+    if (StringUtils.isEmpty(allowedDomain)) {
+      return;
+    }
+
+    final String newUri = provider.getAuthorizationUri() + "?hd=" + allowedDomain;
+    provider.setAuthorizationUri(newUri);
+  }
+
+  private static boolean isGoogle(OAuth2Provider provider) {
+    return provider.getCustomParams().get(TYPE).equalsIgnoreCase(GOOGLE);
+  }
+}
+

+ 101 - 36
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthSecurityConfig.java

@@ -1,66 +1,131 @@
 package com.provectus.kafka.ui.config.auth;
 package com.provectus.kafka.ui.config.auth;
 
 
-import lombok.AllArgsConstructor;
+import com.provectus.kafka.ui.config.auth.logout.OAuthLogoutSuccessHandler;
+import com.provectus.kafka.ui.service.rbac.AccessControlService;
+import com.provectus.kafka.ui.service.rbac.extractor.ProviderAuthorityExtractor;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.log4j.Log4j2;
 import lombok.extern.log4j.Log4j2;
+import org.jetbrains.annotations.Nullable;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.context.ApplicationContext;
+import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
+import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientPropertiesRegistrationAdapter;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
 import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
 import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
 import org.springframework.security.config.web.server.ServerHttpSecurity;
 import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcReactiveOAuth2UserService;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
+import org.springframework.security.oauth2.client.oidc.web.server.logout.OidcClientInitiatedServerLogoutSuccessHandler;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
+import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
+import org.springframework.security.oauth2.client.userinfo.DefaultReactiveOAuth2UserService;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
+import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.security.oauth2.core.user.OAuth2User;
 import org.springframework.security.web.server.SecurityWebFilterChain;
 import org.springframework.security.web.server.SecurityWebFilterChain;
-import org.springframework.util.ClassUtils;
+import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
+import reactor.core.publisher.Mono;
 
 
 @Configuration
 @Configuration
-@EnableWebFluxSecurity
 @ConditionalOnProperty(value = "auth.type", havingValue = "OAUTH2")
 @ConditionalOnProperty(value = "auth.type", havingValue = "OAUTH2")
-@AllArgsConstructor
+@EnableConfigurationProperties(OAuthProperties.class)
+@EnableWebFluxSecurity
+@EnableReactiveMethodSecurity
+@RequiredArgsConstructor
 @Log4j2
 @Log4j2
 public class OAuthSecurityConfig extends AbstractAuthSecurityConfig {
 public class OAuthSecurityConfig extends AbstractAuthSecurityConfig {
 
 
-  public static final String REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME =
-      "org.springframework.security.oauth2.client.registration."
-          + "ReactiveClientRegistrationRepository";
-
-  private static final boolean IS_OAUTH2_PRESENT = ClassUtils.isPresent(
-      REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME,
-      OAuthSecurityConfig.class.getClassLoader()
-  );
-
-  private final ApplicationContext context;
+  private final OAuthProperties properties;
 
 
   @Bean
   @Bean
-  public SecurityWebFilterChain configure(ServerHttpSecurity http) {
+  public SecurityWebFilterChain configure(ServerHttpSecurity http, OAuthLogoutSuccessHandler logoutHandler) {
     log.info("Configuring OAUTH2 authentication.");
     log.info("Configuring OAUTH2 authentication.");
-    http.authorizeExchange()
+
+    return http.authorizeExchange()
         .pathMatchers(AUTH_WHITELIST)
         .pathMatchers(AUTH_WHITELIST)
         .permitAll()
         .permitAll()
         .anyExchange()
         .anyExchange()
-        .authenticated();
+        .authenticated()
+
+        .and()
+        .oauth2Login()
+
+        .and()
+        .logout()
+        .logoutSuccessHandler(logoutHandler)
+
+        .and()
+        .csrf().disable()
+        .build();
+  }
+
+  @Bean
+  public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> customOidcUserService(AccessControlService acs) {
+    final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService();
+    return request -> delegate.loadUser(request)
+        .flatMap(user -> {
+          String providerId = request.getClientRegistration().getRegistrationId();
+          final var extractor = getExtractor(providerId, acs);
+          if (extractor == null) {
+            return Mono.just(user);
+          }
 
 
-    if (IS_OAUTH2_PRESENT && OAuth2ClasspathGuard.shouldConfigure(this.context)) {
-      OAuth2ClasspathGuard.configure(http);
-    }
+          return extractor.extract(acs, user, Map.of("request", request))
+              .map(groups -> new RbacOidcUser(user, groups));
+        });
+  }
+
+  @Bean
+  public ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> customOauth2UserService(AccessControlService acs) {
+    final DefaultReactiveOAuth2UserService delegate = new DefaultReactiveOAuth2UserService();
+    return request -> delegate.loadUser(request)
+        .flatMap(user -> {
+          String providerId = request.getClientRegistration().getRegistrationId();
+          final var extractor = getExtractor(providerId, acs);
+          if (extractor == null) {
+            return Mono.just(user);
+          }
 
 
-    return http.csrf().disable().build();
+          return extractor.extract(acs, user, Map.of("request", request))
+              .map(groups -> new RbacOAuth2User(user, groups));
+        });
   }
   }
 
 
-  private static class OAuth2ClasspathGuard {
-    static void configure(ServerHttpSecurity http) {
-      http
-          .oauth2Login()
-          .and()
-          .oauth2Client();
-    }
-
-    static boolean shouldConfigure(ApplicationContext context) {
-      ClassLoader loader = context.getClassLoader();
-      Class<?> reactiveClientRegistrationRepositoryClass =
-          ClassUtils.resolveClassName(REACTIVE_CLIENT_REGISTRATION_REPOSITORY_CLASSNAME, loader);
-      return context.getBeanNamesForType(reactiveClientRegistrationRepositoryClass).length == 1;
-    }
+  @Bean
+  public InMemoryReactiveClientRegistrationRepository clientRegistrationRepository() {
+    final OAuth2ClientProperties props = OAuthPropertiesConverter.convertProperties(properties);
+    final List<ClientRegistration> registrations =
+        new ArrayList<>(OAuth2ClientPropertiesRegistrationAdapter.getClientRegistrations(props).values());
+    return new InMemoryReactiveClientRegistrationRepository(registrations);
   }
   }
 
 
+  @Bean
+  public ServerLogoutSuccessHandler defaultOidcLogoutHandler(final ReactiveClientRegistrationRepository repository) {
+    return new OidcClientInitiatedServerLogoutSuccessHandler(repository);
+  }
+
+  @Nullable
+  private ProviderAuthorityExtractor getExtractor(final String providerId, AccessControlService acs) {
+    final String provider = getProviderByProviderId(providerId);
+    Optional<ProviderAuthorityExtractor> extractor = acs.getExtractors()
+        .stream()
+        .filter(e -> e.isApplicable(provider))
+        .findFirst();
+
+    return extractor.orElse(null);
+  }
+
+  private String getProviderByProviderId(final String providerId) {
+    return properties.getClient().get(providerId).getProvider();
+  }
 
 
 }
 }
 
 

+ 30 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacOAuth2User.java

@@ -0,0 +1,30 @@
+package com.provectus.kafka.ui.config.auth;
+
+import java.util.Collection;
+import java.util.Map;
+import lombok.Value;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+
+public record RbacOAuth2User(OAuth2User user, Collection<String> groups) implements RbacUser, OAuth2User {
+
+  @Override
+  public Map<String, Object> getAttributes() {
+    return user.getAttributes();
+  }
+
+  @Override
+  public Collection<? extends GrantedAuthority> getAuthorities() {
+    return user.getAuthorities();
+  }
+
+  @Override
+  public String getName() {
+    return user.getName();
+  }
+
+  @Override
+  public String name() {
+    return user.getName();
+  }
+}

+ 47 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacOidcUser.java

@@ -0,0 +1,47 @@
+package com.provectus.kafka.ui.config.auth;
+
+import java.util.Collection;
+import java.util.Map;
+import lombok.Value;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+
+public record RbacOidcUser(OidcUser user, Collection<String> groups) implements RbacUser, OidcUser {
+
+  @Override
+  public Map<String, Object> getClaims() {
+    return user.getClaims();
+  }
+
+  @Override
+  public OidcUserInfo getUserInfo() {
+    return user.getUserInfo();
+  }
+
+  @Override
+  public OidcIdToken getIdToken() {
+    return user.getIdToken();
+  }
+
+  @Override
+  public Map<String, Object> getAttributes() {
+    return user.getAttributes();
+  }
+
+  @Override
+  public Collection<? extends GrantedAuthority> getAuthorities() {
+    return user.getAuthorities();
+  }
+
+  @Override
+  public String getName() {
+    return user.getName();
+  }
+
+  @Override
+  public String name() {
+    return user.getName();
+  }
+}

+ 10 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacUser.java

@@ -0,0 +1,10 @@
+package com.provectus.kafka.ui.config.auth;
+
+import java.util.Collection;
+
+public interface RbacUser {
+  String name();
+
+  Collection<String> groups();
+
+}

+ 23 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RoleBasedAccessControlProperties.java

@@ -0,0 +1,23 @@
+package com.provectus.kafka.ui.config.auth;
+
+import com.provectus.kafka.ui.model.rbac.Role;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.PostConstruct;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties("rbac")
+public class RoleBasedAccessControlProperties {
+
+  private final List<Role> roles = new ArrayList<>();
+
+  @PostConstruct
+  public void init() {
+    roles.forEach(Role::validate);
+  }
+
+  public List<Role> getRoles() {
+    return roles;
+  }
+
+}

+ 13 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/condition/CognitoCondition.java

@@ -0,0 +1,13 @@
+package com.provectus.kafka.ui.config.auth.condition;
+
+import com.provectus.kafka.ui.service.rbac.AbstractProviderCondition;
+import org.springframework.context.annotation.Condition;
+import org.springframework.context.annotation.ConditionContext;
+import org.springframework.core.type.AnnotatedTypeMetadata;
+
+public class CognitoCondition extends AbstractProviderCondition implements Condition {
+  @Override
+  public boolean matches(final ConditionContext context, final AnnotatedTypeMetadata metadata) {
+    return getRegisteredProvidersTypes(context.getEnvironment()).stream().anyMatch(a -> a.equalsIgnoreCase("cognito"));
+  }
+}

+ 18 - 10
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/CognitoOidcLogoutSuccessHandler.java → kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/CognitoLogoutSuccessHandler.java

@@ -1,27 +1,34 @@
-package com.provectus.kafka.ui.config;
+package com.provectus.kafka.ui.config.auth.logout;
 
 
+import com.provectus.kafka.ui.config.auth.OAuthProperties;
+import com.provectus.kafka.ui.config.auth.condition.CognitoCondition;
+import com.provectus.kafka.ui.model.rbac.provider.Provider;
 import java.net.URI;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
 import java.nio.charset.StandardCharsets;
-import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Conditional;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.server.reactive.ServerHttpResponse;
 import org.springframework.http.server.reactive.ServerHttpResponse;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.web.server.WebFilterExchange;
 import org.springframework.security.web.server.WebFilterExchange;
-import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
 import org.springframework.security.web.util.UrlUtils;
 import org.springframework.security.web.util.UrlUtils;
+import org.springframework.stereotype.Component;
 import org.springframework.web.server.WebSession;
 import org.springframework.web.server.WebSession;
 import org.springframework.web.util.UriComponents;
 import org.springframework.web.util.UriComponents;
 import org.springframework.web.util.UriComponentsBuilder;
 import org.springframework.web.util.UriComponentsBuilder;
 import reactor.core.publisher.Mono;
 import reactor.core.publisher.Mono;
 
 
-@RequiredArgsConstructor
-public class CognitoOidcLogoutSuccessHandler implements ServerLogoutSuccessHandler {
+@Component
+@Conditional(CognitoCondition.class)
+public class CognitoLogoutSuccessHandler implements LogoutSuccessHandler {
 
 
-  private final String logoutUrl;
-  private final String clientId;
+  @Override
+  public boolean isApplicable(String provider) {
+    return Provider.Name.COGNITO.equalsIgnoreCase(provider);
+  }
 
 
   @Override
   @Override
-  public Mono<Void> onLogoutSuccess(final WebFilterExchange exchange, final Authentication authentication) {
+  public Mono<Void> handle(WebFilterExchange exchange, Authentication authentication,
+                           OAuthProperties.OAuth2Provider provider) {
     final ServerHttpResponse response = exchange.getExchange().getResponse();
     final ServerHttpResponse response = exchange.getExchange().getResponse();
     response.setStatusCode(HttpStatus.FOUND);
     response.setStatusCode(HttpStatus.FOUND);
 
 
@@ -39,8 +46,8 @@ public class CognitoOidcLogoutSuccessHandler implements ServerLogoutSuccessHandl
         .build();
         .build();
 
 
     final var uri = UriComponentsBuilder
     final var uri = UriComponentsBuilder
-        .fromUri(URI.create(logoutUrl))
-        .queryParam("client_id", clientId)
+        .fromUri(URI.create(provider.getCustomParams().get("logoutUrl")))
+        .queryParam("client_id", provider.getClientId())
         .queryParam("logout_uri", baseUrl)
         .queryParam("logout_uri", baseUrl)
         .encode(StandardCharsets.UTF_8)
         .encode(StandardCharsets.UTF_8)
         .build()
         .build()
@@ -49,5 +56,6 @@ public class CognitoOidcLogoutSuccessHandler implements ServerLogoutSuccessHandl
     response.getHeaders().setLocation(uri);
     response.getHeaders().setLocation(uri);
     return exchange.getExchange().getSession().flatMap(WebSession::invalidate);
     return exchange.getExchange().getSession().flatMap(WebSession::invalidate);
   }
   }
+
 }
 }
 
 

+ 15 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/LogoutSuccessHandler.java

@@ -0,0 +1,15 @@
+package com.provectus.kafka.ui.config.auth.logout;
+
+import com.provectus.kafka.ui.config.auth.OAuthProperties;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.server.WebFilterExchange;
+import reactor.core.publisher.Mono;
+
+public interface LogoutSuccessHandler {
+
+  boolean isApplicable(final String provider);
+
+  Mono<Void> handle(final WebFilterExchange exchange,
+                    final Authentication authentication,
+                    final OAuthProperties.OAuth2Provider provider);
+}

+ 46 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/logout/OAuthLogoutSuccessHandler.java

@@ -0,0 +1,46 @@
+package com.provectus.kafka.ui.config.auth.logout;
+
+import com.provectus.kafka.ui.config.auth.OAuthProperties;
+import java.util.List;
+import java.util.Optional;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
+import org.springframework.security.web.server.WebFilterExchange;
+import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+@Component
+@ConditionalOnProperty(value = "auth.type", havingValue = "OAUTH2")
+public class OAuthLogoutSuccessHandler implements ServerLogoutSuccessHandler {
+  private final OAuthProperties properties;
+  private final List<LogoutSuccessHandler> logoutSuccessHandlers;
+  private final ServerLogoutSuccessHandler defaultOidcLogoutHandler;
+
+  public OAuthLogoutSuccessHandler(final OAuthProperties properties,
+                                   final List<LogoutSuccessHandler> logoutSuccessHandlers,
+                                   final @Qualifier("defaultOidcLogoutHandler") ServerLogoutSuccessHandler handler) {
+    this.properties = properties;
+    this.logoutSuccessHandlers = logoutSuccessHandlers;
+    this.defaultOidcLogoutHandler = handler;
+  }
+
+  @Override
+  public Mono<Void> onLogoutSuccess(final WebFilterExchange exchange,
+                                    final Authentication authentication) {
+    final OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
+    final String providerId = oauthToken.getAuthorizedClientRegistrationId();
+    final OAuthProperties.OAuth2Provider oAuth2Provider = properties.getClient().get(providerId);
+    return getLogoutHandler(oAuth2Provider.getProvider())
+        .map(handler -> handler.handle(exchange, authentication, oAuth2Provider))
+        .orElseGet(() -> defaultOidcLogoutHandler.onLogoutSuccess(exchange, authentication));
+  }
+
+  private Optional<LogoutSuccessHandler> getLogoutHandler(final String provider) {
+    return logoutSuccessHandlers.stream()
+        .filter(h -> h.isApplicable(provider))
+        .findFirst();
+  }
+}

+ 0 - 44
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/props/CognitoProperties.java

@@ -1,44 +0,0 @@
-package com.provectus.kafka.ui.config.auth.props;
-
-import lombok.Data;
-import lombok.ToString;
-import org.jetbrains.annotations.Nullable;
-
-@Data
-@ToString(exclude = "clientSecret")
-public class CognitoProperties {
-
-  String clientId;
-  String logoutUri;
-  String issuerUri;
-  String clientSecret;
-  @Nullable
-  String scope;
-  @Nullable
-  String userNameAttribute;
-
-  public String getClientId() {
-    return clientId;
-  }
-
-  public String getLogoutUri() {
-    return logoutUri;
-  }
-
-  public String getIssuerUri() {
-    return issuerUri;
-  }
-
-  public String getClientSecret() {
-    return clientSecret;
-  }
-
-  public @Nullable String getScope() {
-    return scope;
-  }
-
-  public @Nullable String getUserNameAttribute() {
-    return userNameAttribute;
-  }
-
-}

+ 80 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/AccessController.java

@@ -0,0 +1,80 @@
+package com.provectus.kafka.ui.controller;
+
+import com.provectus.kafka.ui.api.AuthorizationApi;
+import com.provectus.kafka.ui.model.ActionDTO;
+import com.provectus.kafka.ui.model.AuthenticationInfoDTO;
+import com.provectus.kafka.ui.model.ResourceTypeDTO;
+import com.provectus.kafka.ui.model.UserInfoDTO;
+import com.provectus.kafka.ui.model.UserPermissionDTO;
+import com.provectus.kafka.ui.model.rbac.Permission;
+import com.provectus.kafka.ui.service.rbac.AccessControlService;
+import java.security.Principal;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.context.ReactiveSecurityContextHolder;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+@RestController
+@RequiredArgsConstructor
+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()
+            .filter(role -> user.groups().contains(role.getName()))
+            .map(role -> mapPermissions(role.getPermissions(), role.getClusters()))
+            .flatMap(Collection::stream)
+            .collect(Collectors.toList())
+        )
+        .switchIfEmpty(Mono.just(Collections.emptyList()));
+
+    Mono<String> userName = ReactiveSecurityContextHolder.getContext()
+        .map(SecurityContext::getAuthentication)
+        .map(Principal::getName);
+
+    return userName
+        .zipWith(permissions)
+        .map(data -> {
+          userInfo.setUsername(data.getT1());
+          userInfo.setPermissions(data.getT2());
+
+          dto.setUserInfo(userInfo);
+          return dto;
+        })
+        .switchIfEmpty(Mono.just(dto))
+        .map(ResponseEntity::ok);
+  }
+
+  private List<UserPermissionDTO> mapPermissions(List<Permission> permissions, List<String> clusters) {
+    return permissions
+        .stream()
+        .map(permission -> {
+          UserPermissionDTO dto = new UserPermissionDTO();
+          dto.setClusters(clusters);
+          dto.setResource(ResourceTypeDTO.fromValue(permission.getResource().toString().toUpperCase()));
+          dto.setValue(permission.getValue() != null ? permission.getValue().toString() : null);
+          dto.setActions(permission.getActions()
+              .stream()
+              .map(String::toUpperCase)
+              .map(ActionDTO::valueOf)
+              .collect(Collectors.toList()));
+          return dto;
+        })
+        .collect(Collectors.toList());
+  }
+
+}

+ 68 - 27
kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/BrokersController.java

@@ -8,7 +8,10 @@ import com.provectus.kafka.ui.model.BrokerDTO;
 import com.provectus.kafka.ui.model.BrokerLogdirUpdateDTO;
 import com.provectus.kafka.ui.model.BrokerLogdirUpdateDTO;
 import com.provectus.kafka.ui.model.BrokerMetricsDTO;
 import com.provectus.kafka.ui.model.BrokerMetricsDTO;
 import com.provectus.kafka.ui.model.BrokersLogdirsDTO;
 import com.provectus.kafka.ui.model.BrokersLogdirsDTO;
+import com.provectus.kafka.ui.model.rbac.AccessContext;
+import com.provectus.kafka.ui.model.rbac.permission.ClusterConfigAction;
 import com.provectus.kafka.ui.service.BrokerService;
 import com.provectus.kafka.ui.service.BrokerService;
+import com.provectus.kafka.ui.service.rbac.AccessControlService;
 import java.util.List;
 import java.util.List;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
@@ -24,47 +27,78 @@ import reactor.core.publisher.Mono;
 public class BrokersController extends AbstractController implements BrokersApi {
 public class BrokersController extends AbstractController implements BrokersApi {
   private final BrokerService brokerService;
   private final BrokerService brokerService;
   private final ClusterMapper clusterMapper;
   private final ClusterMapper clusterMapper;
+  private final AccessControlService accessControlService;
 
 
   @Override
   @Override
-  public Mono<ResponseEntity<BrokerMetricsDTO>> getBrokersMetrics(String clusterName, Integer id,
-                                                                  ServerWebExchange exchange) {
-    return brokerService.getBrokerMetrics(getCluster(clusterName), id)
-        .map(clusterMapper::toBrokerMetrics)
-        .map(ResponseEntity::ok)
-        .onErrorReturn(ResponseEntity.notFound().build());
+  public Mono<ResponseEntity<Flux<BrokerDTO>>> getBrokers(String clusterName,
+                                                          ServerWebExchange exchange) {
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .build());
+
+    var job = brokerService.getBrokers(getCluster(clusterName)).map(clusterMapper::toBrokerDto);
+
+    return validateAccess.thenReturn(ResponseEntity.ok(job));
   }
   }
 
 
   @Override
   @Override
-  public Mono<ResponseEntity<Flux<BrokerDTO>>> getBrokers(String clusterName,
-                                                          ServerWebExchange exchange) {
-    return Mono.just(ResponseEntity.ok(
-        brokerService.getBrokers(getCluster(clusterName)).map(clusterMapper::toBrokerDto)));
+  public Mono<ResponseEntity<BrokerMetricsDTO>> getBrokersMetrics(String clusterName, Integer id,
+                                                                  ServerWebExchange exchange) {
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .build());
+
+    return validateAccess.then(
+        brokerService.getBrokerMetrics(getCluster(clusterName), id)
+            .map(clusterMapper::toBrokerMetrics)
+            .map(ResponseEntity::ok)
+            .onErrorReturn(ResponseEntity.notFound().build())
+    );
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<Flux<BrokersLogdirsDTO>>> getAllBrokersLogdirs(String clusterName,
   public Mono<ResponseEntity<Flux<BrokersLogdirsDTO>>> getAllBrokersLogdirs(String clusterName,
                                                                             List<Integer> brokers,
                                                                             List<Integer> brokers,
-                                                                            ServerWebExchange exchange
-  ) {
-    return Mono.just(ResponseEntity.ok(
+                                                                            ServerWebExchange exchange) {
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .build());
+
+    return validateAccess.thenReturn(ResponseEntity.ok(
         brokerService.getAllBrokersLogdirs(getCluster(clusterName), brokers)));
         brokerService.getAllBrokersLogdirs(getCluster(clusterName), brokers)));
   }
   }
 
 
   @Override
   @Override
-  public Mono<ResponseEntity<Flux<BrokerConfigDTO>>> getBrokerConfig(String clusterName, Integer id,
+  public Mono<ResponseEntity<Flux<BrokerConfigDTO>>> getBrokerConfig(String clusterName,
+                                                                     Integer id,
                                                                      ServerWebExchange exchange) {
                                                                      ServerWebExchange exchange) {
-    return Mono.just(ResponseEntity.ok(
-        brokerService.getBrokerConfig(getCluster(clusterName), id)
-            .map(clusterMapper::toBrokerConfig)));
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .clusterConfigActions(ClusterConfigAction.VIEW)
+        .build());
+
+    return validateAccess.thenReturn(
+        ResponseEntity.ok(
+            brokerService.getBrokerConfig(getCluster(clusterName), id)
+                .map(clusterMapper::toBrokerConfig))
+    );
   }
   }
 
 
   @Override
   @Override
-  public Mono<ResponseEntity<Void>> updateBrokerTopicPartitionLogDir(
-      String clusterName, Integer id, Mono<BrokerLogdirUpdateDTO> brokerLogdir,
-      ServerWebExchange exchange) {
-    return brokerLogdir
-        .flatMap(bld -> brokerService.updateBrokerLogDir(getCluster(clusterName), id, bld))
-        .map(ResponseEntity::ok);
+  public Mono<ResponseEntity<Void>> updateBrokerTopicPartitionLogDir(String clusterName,
+                                                                     Integer id,
+                                                                     Mono<BrokerLogdirUpdateDTO> brokerLogdir,
+                                                                     ServerWebExchange exchange) {
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .clusterConfigActions(ClusterConfigAction.VIEW, ClusterConfigAction.EDIT)
+        .build());
+
+    return validateAccess.then(
+        brokerLogdir
+            .flatMap(bld -> brokerService.updateBrokerLogDir(getCluster(clusterName), id, bld))
+            .map(ResponseEntity::ok)
+    );
   }
   }
 
 
   @Override
   @Override
@@ -73,9 +107,16 @@ public class BrokersController extends AbstractController implements BrokersApi
                                                              String name,
                                                              String name,
                                                              Mono<BrokerConfigItemDTO> brokerConfig,
                                                              Mono<BrokerConfigItemDTO> brokerConfig,
                                                              ServerWebExchange exchange) {
                                                              ServerWebExchange exchange) {
-    return brokerConfig
-        .flatMap(bci -> brokerService.updateBrokerConfigByName(
-            getCluster(clusterName), id, name, bci.getValue()))
-        .map(ResponseEntity::ok);
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .clusterConfigActions(ClusterConfigAction.VIEW, ClusterConfigAction.EDIT)
+        .build());
+
+    return validateAccess.then(
+        brokerConfig
+            .flatMap(bci -> brokerService.updateBrokerConfigByName(
+                getCluster(clusterName), id, name, bci.getValue()))
+            .map(ResponseEntity::ok)
+    );
   }
   }
 }
 }

+ 39 - 11
kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ClustersController.java

@@ -4,7 +4,9 @@ import com.provectus.kafka.ui.api.ClustersApi;
 import com.provectus.kafka.ui.model.ClusterDTO;
 import com.provectus.kafka.ui.model.ClusterDTO;
 import com.provectus.kafka.ui.model.ClusterMetricsDTO;
 import com.provectus.kafka.ui.model.ClusterMetricsDTO;
 import com.provectus.kafka.ui.model.ClusterStatsDTO;
 import com.provectus.kafka.ui.model.ClusterStatsDTO;
+import com.provectus.kafka.ui.model.rbac.AccessContext;
 import com.provectus.kafka.ui.service.ClusterService;
 import com.provectus.kafka.ui.service.ClusterService;
+import com.provectus.kafka.ui.service.rbac.AccessControlService;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.ResponseEntity;
@@ -18,31 +20,57 @@ import reactor.core.publisher.Mono;
 @Slf4j
 @Slf4j
 public class ClustersController extends AbstractController implements ClustersApi {
 public class ClustersController extends AbstractController implements ClustersApi {
   private final ClusterService clusterService;
   private final ClusterService clusterService;
+  private final AccessControlService accessControlService;
+
+  @Override
+  public Mono<ResponseEntity<Flux<ClusterDTO>>> getClusters(ServerWebExchange exchange) {
+    Flux<ClusterDTO> job = Flux.fromIterable(clusterService.getClusters())
+        .filterWhen(accessControlService::isClusterAccessible);
+
+    return Mono.just(ResponseEntity.ok(job));
+  }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<ClusterMetricsDTO>> getClusterMetrics(String clusterName,
   public Mono<ResponseEntity<ClusterMetricsDTO>> getClusterMetrics(String clusterName,
                                                                    ServerWebExchange exchange) {
                                                                    ServerWebExchange exchange) {
-    return clusterService.getClusterMetrics(getCluster(clusterName))
-        .map(ResponseEntity::ok)
-        .onErrorReturn(ResponseEntity.notFound().build());
+    AccessContext context = AccessContext.builder()
+        .cluster(clusterName)
+        .build();
+
+    return accessControlService.validateAccess(context)
+        .then(
+            clusterService.getClusterMetrics(getCluster(clusterName))
+                .map(ResponseEntity::ok)
+                .onErrorReturn(ResponseEntity.notFound().build())
+        );
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<ClusterStatsDTO>> getClusterStats(String clusterName,
   public Mono<ResponseEntity<ClusterStatsDTO>> getClusterStats(String clusterName,
                                                                ServerWebExchange exchange) {
                                                                ServerWebExchange exchange) {
-    return clusterService.getClusterStats(getCluster(clusterName))
-        .map(ResponseEntity::ok)
-        .onErrorReturn(ResponseEntity.notFound().build());
-  }
+    AccessContext context = AccessContext.builder()
+        .cluster(clusterName)
+        .build();
 
 
-  @Override
-  public Mono<ResponseEntity<Flux<ClusterDTO>>> getClusters(ServerWebExchange exchange) {
-    return Mono.just(ResponseEntity.ok(Flux.fromIterable(clusterService.getClusters())));
+    return accessControlService.validateAccess(context)
+        .then(
+            clusterService.getClusterStats(getCluster(clusterName))
+                .map(ResponseEntity::ok)
+                .onErrorReturn(ResponseEntity.notFound().build())
+        );
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<ClusterDTO>> updateClusterInfo(String clusterName,
   public Mono<ResponseEntity<ClusterDTO>> updateClusterInfo(String clusterName,
                                                             ServerWebExchange exchange) {
                                                             ServerWebExchange exchange) {
-    return clusterService.updateCluster(getCluster(clusterName)).map(ResponseEntity::ok);
+
+    AccessContext context = AccessContext.builder()
+        .cluster(clusterName)
+        .build();
+
+    return accessControlService.validateAccess(context)
+        .then(
+            clusterService.updateCluster(getCluster(clusterName)).map(ResponseEntity::ok)
+        );
   }
   }
 }
 }

+ 127 - 69
kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/ConsumerGroupsController.java

@@ -1,5 +1,8 @@
 package com.provectus.kafka.ui.controller;
 package com.provectus.kafka.ui.controller;
 
 
+import static com.provectus.kafka.ui.model.rbac.permission.ConsumerGroupAction.DELETE;
+import static com.provectus.kafka.ui.model.rbac.permission.ConsumerGroupAction.RESET_OFFSETS;
+import static com.provectus.kafka.ui.model.rbac.permission.ConsumerGroupAction.VIEW;
 import static java.util.stream.Collectors.toMap;
 import static java.util.stream.Collectors.toMap;
 
 
 import com.provectus.kafka.ui.api.ConsumerGroupsApi;
 import com.provectus.kafka.ui.api.ConsumerGroupsApi;
@@ -12,10 +15,14 @@ import com.provectus.kafka.ui.model.ConsumerGroupOrderingDTO;
 import com.provectus.kafka.ui.model.ConsumerGroupsPageResponseDTO;
 import com.provectus.kafka.ui.model.ConsumerGroupsPageResponseDTO;
 import com.provectus.kafka.ui.model.PartitionOffsetDTO;
 import com.provectus.kafka.ui.model.PartitionOffsetDTO;
 import com.provectus.kafka.ui.model.SortOrderDTO;
 import com.provectus.kafka.ui.model.SortOrderDTO;
+import com.provectus.kafka.ui.model.rbac.AccessContext;
+import com.provectus.kafka.ui.model.rbac.permission.TopicAction;
 import com.provectus.kafka.ui.service.ConsumerGroupService;
 import com.provectus.kafka.ui.service.ConsumerGroupService;
 import com.provectus.kafka.ui.service.OffsetsResetService;
 import com.provectus.kafka.ui.service.OffsetsResetService;
+import com.provectus.kafka.ui.service.rbac.AccessControlService;
 import java.util.Map;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Optional;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
@@ -34,33 +41,65 @@ public class ConsumerGroupsController extends AbstractController implements Cons
 
 
   private final ConsumerGroupService consumerGroupService;
   private final ConsumerGroupService consumerGroupService;
   private final OffsetsResetService offsetsResetService;
   private final OffsetsResetService offsetsResetService;
+  private final AccessControlService accessControlService;
 
 
   @Value("${consumer.groups.page.size:25}")
   @Value("${consumer.groups.page.size:25}")
   private int defaultConsumerGroupsPageSize;
   private int defaultConsumerGroupsPageSize;
 
 
   @Override
   @Override
-  public Mono<ResponseEntity<Void>> deleteConsumerGroup(String clusterName, String id,
+  public Mono<ResponseEntity<Void>> deleteConsumerGroup(String clusterName,
+                                                        String id,
                                                         ServerWebExchange exchange) {
                                                         ServerWebExchange exchange) {
-    return consumerGroupService.deleteConsumerGroupById(getCluster(clusterName), id)
-        .thenReturn(ResponseEntity.ok().build());
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .consumerGroup(id)
+        .consumerGroupActions(DELETE)
+        .build());
+
+    return validateAccess.then(
+        consumerGroupService.deleteConsumerGroupById(getCluster(clusterName), id)
+            .thenReturn(ResponseEntity.ok().build())
+    );
   }
   }
 
 
   @Override
   @Override
-  public Mono<ResponseEntity<ConsumerGroupDetailsDTO>> getConsumerGroup(
-      String clusterName, String consumerGroupId, ServerWebExchange exchange) {
-    return consumerGroupService.getConsumerGroupDetail(getCluster(clusterName), consumerGroupId)
-        .map(ConsumerGroupMapper::toDetailsDto)
-        .map(ResponseEntity::ok);
+  public Mono<ResponseEntity<ConsumerGroupDetailsDTO>> getConsumerGroup(String clusterName,
+                                                                        String consumerGroupId,
+                                                                        ServerWebExchange exchange) {
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .consumerGroup(consumerGroupId)
+        .consumerGroupActions(VIEW)
+        .build());
+
+    return validateAccess.then(
+        consumerGroupService.getConsumerGroupDetail(getCluster(clusterName), consumerGroupId)
+            .map(ConsumerGroupMapper::toDetailsDto)
+            .map(ResponseEntity::ok)
+    );
   }
   }
 
 
   @Override
   @Override
-  public Mono<ResponseEntity<Flux<ConsumerGroupDTO>>> getTopicConsumerGroups(
-      String clusterName, String topicName, ServerWebExchange exchange) {
-    return consumerGroupService.getConsumerGroupsForTopic(getCluster(clusterName), topicName)
-        .map(Flux::fromIterable)
-        .map(f -> f.map(ConsumerGroupMapper::toDto))
-        .map(ResponseEntity::ok)
-        .switchIfEmpty(Mono.just(ResponseEntity.notFound().build()));
+  public Mono<ResponseEntity<Flux<ConsumerGroupDTO>>> getTopicConsumerGroups(String clusterName,
+                                                                             String topicName,
+                                                                             ServerWebExchange exchange) {
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .topic(topicName)
+        .topicActions(TopicAction.VIEW)
+        .build());
+
+    Mono<ResponseEntity<Flux<ConsumerGroupDTO>>> job =
+        consumerGroupService.getConsumerGroupsForTopic(getCluster(clusterName), topicName)
+            .flatMapMany(Flux::fromIterable)
+            .filterWhen(cg -> accessControlService.isConsumerGroupAccessible(cg.getGroupId(), clusterName))
+            .map(ConsumerGroupMapper::toDto)
+            .collectList()
+            .map(Flux::fromIterable)
+            .map(ResponseEntity::ok)
+            .switchIfEmpty(Mono.just(ResponseEntity.notFound().build()));
+
+    return validateAccess.then(job);
   }
   }
 
 
   @Override
   @Override
@@ -72,16 +111,79 @@ public class ConsumerGroupsController extends AbstractController implements Cons
       ConsumerGroupOrderingDTO orderBy,
       ConsumerGroupOrderingDTO orderBy,
       SortOrderDTO sortOrderDto,
       SortOrderDTO sortOrderDto,
       ServerWebExchange exchange) {
       ServerWebExchange exchange) {
-    return consumerGroupService.getConsumerGroupsPage(
-            getCluster(clusterName),
-            Optional.ofNullable(page).filter(i -> i > 0).orElse(1),
-            Optional.ofNullable(perPage).filter(i -> i > 0).orElse(defaultConsumerGroupsPageSize),
-            search,
-            Optional.ofNullable(orderBy).orElse(ConsumerGroupOrderingDTO.NAME),
-            Optional.ofNullable(sortOrderDto).orElse(SortOrderDTO.ASC)
-        )
-        .map(this::convertPage)
-        .map(ResponseEntity::ok);
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        // consumer group access validation is within the service
+        .build());
+
+    return validateAccess.then(
+        consumerGroupService.getConsumerGroupsPage(
+                getCluster(clusterName),
+                Optional.ofNullable(page).filter(i -> i > 0).orElse(1),
+                Optional.ofNullable(perPage).filter(i -> i > 0).orElse(defaultConsumerGroupsPageSize),
+                search,
+                Optional.ofNullable(orderBy).orElse(ConsumerGroupOrderingDTO.NAME),
+                Optional.ofNullable(sortOrderDto).orElse(SortOrderDTO.ASC)
+            )
+            .map(this::convertPage)
+            .map(ResponseEntity::ok)
+    );
+  }
+
+  @Override
+  public Mono<ResponseEntity<Void>> resetConsumerGroupOffsets(String clusterName,
+                                                              String group,
+                                                              Mono<ConsumerGroupOffsetsResetDTO> resetDto,
+                                                              ServerWebExchange exchange) {
+    return resetDto.flatMap(reset -> {
+      Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+          .cluster(clusterName)
+          .topic(reset.getTopic())
+          .topicActions(TopicAction.VIEW)
+          .consumerGroupActions(RESET_OFFSETS)
+          .build());
+
+      Supplier<Mono<Void>> mono = () -> {
+        var cluster = getCluster(clusterName);
+        switch (reset.getResetType()) {
+          case EARLIEST:
+            return offsetsResetService
+                .resetToEarliest(cluster, group, reset.getTopic(), reset.getPartitions());
+          case LATEST:
+            return offsetsResetService
+                .resetToLatest(cluster, group, reset.getTopic(), reset.getPartitions());
+          case TIMESTAMP:
+            if (reset.getResetToTimestamp() == null) {
+              return Mono.error(
+                  new ValidationException(
+                      "resetToTimestamp is required when TIMESTAMP reset type used"
+                  )
+              );
+            }
+            return offsetsResetService
+                .resetToTimestamp(cluster, group, reset.getTopic(), reset.getPartitions(),
+                    reset.getResetToTimestamp());
+          case OFFSET:
+            if (CollectionUtils.isEmpty(reset.getPartitionsOffsets())) {
+              return Mono.error(
+                  new ValidationException(
+                      "partitionsOffsets is required when OFFSET reset type used"
+                  )
+              );
+            }
+            Map<Integer, Long> offsets = reset.getPartitionsOffsets().stream()
+                .collect(toMap(PartitionOffsetDTO::getPartition, PartitionOffsetDTO::getOffset));
+            return offsetsResetService.resetToOffsets(cluster, group, reset.getTopic(), offsets);
+          default:
+            return Mono.error(
+                new ValidationException("Unknown resetType " + reset.getResetType())
+            );
+        }
+      };
+
+      return validateAccess.then(mono.get());
+    }).thenReturn(ResponseEntity.ok().build());
   }
   }
 
 
   private ConsumerGroupsPageResponseDTO convertPage(ConsumerGroupService.ConsumerGroupsPage
   private ConsumerGroupsPageResponseDTO convertPage(ConsumerGroupService.ConsumerGroupsPage
@@ -94,48 +196,4 @@ public class ConsumerGroupsController extends AbstractController implements Cons
             .collect(Collectors.toList()));
             .collect(Collectors.toList()));
   }
   }
 
 
-  @Override
-  public Mono<ResponseEntity<Void>> resetConsumerGroupOffsets(String clusterName, String group,
-                                                              Mono<ConsumerGroupOffsetsResetDTO>
-                                                                  consumerGroupOffsetsReset,
-                                                              ServerWebExchange exchange) {
-    return consumerGroupOffsetsReset.flatMap(reset -> {
-      var cluster = getCluster(clusterName);
-      switch (reset.getResetType()) {
-        case EARLIEST:
-          return offsetsResetService
-              .resetToEarliest(cluster, group, reset.getTopic(), reset.getPartitions());
-        case LATEST:
-          return offsetsResetService
-              .resetToLatest(cluster, group, reset.getTopic(), reset.getPartitions());
-        case TIMESTAMP:
-          if (reset.getResetToTimestamp() == null) {
-            return Mono.error(
-                new ValidationException(
-                    "resetToTimestamp is required when TIMESTAMP reset type used"
-                )
-            );
-          }
-          return offsetsResetService
-              .resetToTimestamp(cluster, group, reset.getTopic(), reset.getPartitions(),
-                  reset.getResetToTimestamp());
-        case OFFSET:
-          if (CollectionUtils.isEmpty(reset.getPartitionsOffsets())) {
-            return Mono.error(
-                new ValidationException(
-                    "partitionsOffsets is required when OFFSET reset type used"
-                )
-            );
-          }
-          Map<Integer, Long> offsets = reset.getPartitionsOffsets().stream()
-              .collect(toMap(PartitionOffsetDTO::getPartition, PartitionOffsetDTO::getOffset));
-          return offsetsResetService.resetToOffsets(cluster, group, reset.getTopic(), offsets);
-        default:
-          return Mono.error(
-              new ValidationException("Unknown resetType " + reset.getResetType())
-          );
-      }
-    }).thenReturn(ResponseEntity.ok().build());
-  }
-
 }
 }

+ 0 - 32
kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/InfoController.java

@@ -1,32 +0,0 @@
-package com.provectus.kafka.ui.controller;
-
-import com.provectus.kafka.ui.api.TimeStampFormatApi;
-import com.provectus.kafka.ui.model.TimeStampFormatDTO;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.http.ResponseEntity;
-import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.server.ServerWebExchange;
-import reactor.core.publisher.Mono;
-
-@RestController
-@RequiredArgsConstructor
-@Slf4j
-public class InfoController extends AbstractController implements TimeStampFormatApi {
-
-  @Value("${timestamp.format:dd.MM.YYYY HH:mm:ss}")
-  private String timeStampFormat;
-  @Value("${timestamp.format:DD.MM.YYYY HH:mm:ss}")
-  private String timeStampFormatIso;
-
-  @Override
-  public Mono<ResponseEntity<TimeStampFormatDTO>> getTimeStampFormat(ServerWebExchange exchange) {
-    return Mono.just(ResponseEntity.ok(new TimeStampFormatDTO().timeStampFormat(timeStampFormat)));
-  }
-
-  @Override
-  public Mono<ResponseEntity<TimeStampFormatDTO>> getTimeStampFormatISO(ServerWebExchange exchange) {
-    return Mono.just(ResponseEntity.ok(new TimeStampFormatDTO().timeStampFormat(timeStampFormatIso)));
-  }
-}

+ 132 - 34
kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KafkaConnectController.java

@@ -11,7 +11,10 @@ import com.provectus.kafka.ui.model.FullConnectorInfoDTO;
 import com.provectus.kafka.ui.model.NewConnectorDTO;
 import com.provectus.kafka.ui.model.NewConnectorDTO;
 import com.provectus.kafka.ui.model.SortOrderDTO;
 import com.provectus.kafka.ui.model.SortOrderDTO;
 import com.provectus.kafka.ui.model.TaskDTO;
 import com.provectus.kafka.ui.model.TaskDTO;
+import com.provectus.kafka.ui.model.rbac.AccessContext;
+import com.provectus.kafka.ui.model.rbac.permission.ConnectAction;
 import com.provectus.kafka.ui.service.KafkaConnectService;
 import com.provectus.kafka.ui.service.KafkaConnectService;
+import com.provectus.kafka.ui.service.rbac.AccessControlService;
 import java.util.Comparator;
 import java.util.Comparator;
 import java.util.Map;
 import java.util.Map;
 import javax.validation.Valid;
 import javax.validation.Valid;
@@ -28,42 +31,83 @@ import reactor.core.publisher.Mono;
 @Slf4j
 @Slf4j
 public class KafkaConnectController extends AbstractController implements KafkaConnectApi {
 public class KafkaConnectController extends AbstractController implements KafkaConnectApi {
   private final KafkaConnectService kafkaConnectService;
   private final KafkaConnectService kafkaConnectService;
+  private final AccessControlService accessControlService;
 
 
   @Override
   @Override
   public Mono<ResponseEntity<Flux<ConnectDTO>>> getConnects(String clusterName,
   public Mono<ResponseEntity<Flux<ConnectDTO>>> getConnects(String clusterName,
                                                             ServerWebExchange exchange) {
                                                             ServerWebExchange exchange) {
-    return kafkaConnectService.getConnects(getCluster(clusterName)).map(ResponseEntity::ok);
+
+    Flux<ConnectDTO> flux = Flux.fromIterable(kafkaConnectService.getConnects(getCluster(clusterName)))
+        .filterWhen(dto -> accessControlService.isConnectAccessible(dto, clusterName));
+
+    return Mono.just(ResponseEntity.ok(flux));
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<Flux<String>>> getConnectors(String clusterName, String connectName,
   public Mono<ResponseEntity<Flux<String>>> getConnectors(String clusterName, String connectName,
                                                           ServerWebExchange exchange) {
                                                           ServerWebExchange exchange) {
-    var connectors = kafkaConnectService.getConnectors(getCluster(clusterName), connectName);
-    return Mono.just(ResponseEntity.ok(connectors));
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .connect(connectName)
+        .connectActions(ConnectAction.VIEW)
+        .build());
+
+    return validateAccess.thenReturn(
+        ResponseEntity.ok(kafkaConnectService.getConnectors(getCluster(clusterName), connectName))
+    );
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<ConnectorDTO>> createConnector(String clusterName, String connectName,
   public Mono<ResponseEntity<ConnectorDTO>> createConnector(String clusterName, String connectName,
                                                             @Valid Mono<NewConnectorDTO> connector,
                                                             @Valid Mono<NewConnectorDTO> connector,
                                                             ServerWebExchange exchange) {
                                                             ServerWebExchange exchange) {
-    return kafkaConnectService.createConnector(getCluster(clusterName), connectName, connector)
-        .map(ResponseEntity::ok);
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .connect(connectName)
+        .connectActions(ConnectAction.VIEW, ConnectAction.CREATE)
+        .build());
+
+    return validateAccess.then(
+        kafkaConnectService.createConnector(getCluster(clusterName), connectName, connector)
+            .map(ResponseEntity::ok)
+    );
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<ConnectorDTO>> getConnector(String clusterName, String connectName,
   public Mono<ResponseEntity<ConnectorDTO>> getConnector(String clusterName, String connectName,
                                                          String connectorName,
                                                          String connectorName,
                                                          ServerWebExchange exchange) {
                                                          ServerWebExchange exchange) {
-    return kafkaConnectService.getConnector(getCluster(clusterName), connectName, connectorName)
-        .map(ResponseEntity::ok);
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .connect(connectName)
+        .connectActions(ConnectAction.VIEW)
+        .connector(connectorName)
+        .build());
+
+    return validateAccess.then(
+        kafkaConnectService.getConnector(getCluster(clusterName), connectName, connectorName)
+            .map(ResponseEntity::ok)
+    );
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<Void>> deleteConnector(String clusterName, String connectName,
   public Mono<ResponseEntity<Void>> deleteConnector(String clusterName, String connectName,
                                                     String connectorName,
                                                     String connectorName,
                                                     ServerWebExchange exchange) {
                                                     ServerWebExchange exchange) {
-    return kafkaConnectService.deleteConnector(getCluster(clusterName), connectName, connectorName)
-        .map(ResponseEntity::ok);
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .connect(connectName)
+        .connectActions(ConnectAction.VIEW, ConnectAction.EDIT)
+        .build());
+
+    return validateAccess.then(
+        kafkaConnectService.deleteConnector(getCluster(clusterName), connectName, connectorName)
+            .map(ResponseEntity::ok)
+    );
   }
   }
 
 
 
 
@@ -76,11 +120,13 @@ public class KafkaConnectController extends AbstractController implements KafkaC
       ServerWebExchange exchange
       ServerWebExchange exchange
   ) {
   ) {
     var comparator = sortOrder == null || sortOrder.equals(SortOrderDTO.ASC)
     var comparator = sortOrder == null || sortOrder.equals(SortOrderDTO.ASC)
-            ? getConnectorsComparator(orderBy)
-            : getConnectorsComparator(orderBy).reversed();
-    return Mono.just(ResponseEntity.ok(
-        kafkaConnectService.getAllConnectors(getCluster(clusterName), search).sort(comparator))
-    );
+        ? getConnectorsComparator(orderBy)
+        : getConnectorsComparator(orderBy).reversed();
+    Flux<FullConnectorInfoDTO> job = kafkaConnectService.getAllConnectors(getCluster(clusterName), search)
+        .filterWhen(dto -> accessControlService.isConnectAccessible(dto.getConnect(), clusterName))
+        .filterWhen(dto -> accessControlService.isConnectorAccessible(dto.getConnect(), dto.getName(), clusterName));
+
+    return Mono.just(ResponseEntity.ok(job.sort(comparator)));
   }
   }
 
 
   @Override
   @Override
@@ -88,9 +134,18 @@ public class KafkaConnectController extends AbstractController implements KafkaC
                                                                       String connectName,
                                                                       String connectName,
                                                                       String connectorName,
                                                                       String connectorName,
                                                                       ServerWebExchange exchange) {
                                                                       ServerWebExchange exchange) {
-    return kafkaConnectService
-        .getConnectorConfig(getCluster(clusterName), connectName, connectorName)
-        .map(ResponseEntity::ok);
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .connect(connectName)
+        .connectActions(ConnectAction.VIEW)
+        .build());
+
+    return validateAccess.then(
+        kafkaConnectService
+            .getConnectorConfig(getCluster(clusterName), connectName, connectorName)
+            .map(ResponseEntity::ok)
+    );
   }
   }
 
 
   @Override
   @Override
@@ -99,9 +154,18 @@ public class KafkaConnectController extends AbstractController implements KafkaC
                                                                String connectorName,
                                                                String connectorName,
                                                                @Valid Mono<Object> requestBody,
                                                                @Valid Mono<Object> requestBody,
                                                                ServerWebExchange exchange) {
                                                                ServerWebExchange exchange) {
-    return kafkaConnectService
-        .setConnectorConfig(getCluster(clusterName), connectName, connectorName, requestBody)
-        .map(ResponseEntity::ok);
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .connect(connectName)
+        .connectActions(ConnectAction.VIEW, ConnectAction.EDIT)
+        .build());
+
+    return validateAccess.then(
+        kafkaConnectService
+            .setConnectorConfig(getCluster(clusterName), connectName, connectorName, requestBody)
+            .map(ResponseEntity::ok)
+    );
   }
   }
 
 
   @Override
   @Override
@@ -109,9 +173,18 @@ public class KafkaConnectController extends AbstractController implements KafkaC
                                                          String connectorName,
                                                          String connectorName,
                                                          ConnectorActionDTO action,
                                                          ConnectorActionDTO action,
                                                          ServerWebExchange exchange) {
                                                          ServerWebExchange exchange) {
-    return kafkaConnectService
-        .updateConnectorState(getCluster(clusterName), connectName, connectorName, action)
-        .map(ResponseEntity::ok);
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .connect(connectName)
+        .connectActions(ConnectAction.VIEW, ConnectAction.EDIT)
+        .build());
+
+    return validateAccess.then(
+        kafkaConnectService
+            .updateConnectorState(getCluster(clusterName), connectName, connectorName, action)
+            .map(ResponseEntity::ok)
+    );
   }
   }
 
 
   @Override
   @Override
@@ -119,31 +192,56 @@ public class KafkaConnectController extends AbstractController implements KafkaC
                                                                String connectName,
                                                                String connectName,
                                                                String connectorName,
                                                                String connectorName,
                                                                ServerWebExchange exchange) {
                                                                ServerWebExchange exchange) {
-    return Mono.just(ResponseEntity
-        .ok(kafkaConnectService
-            .getConnectorTasks(getCluster(clusterName), connectName, connectorName)));
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .connect(connectName)
+        .connectActions(ConnectAction.VIEW)
+        .build());
+
+    return validateAccess.thenReturn(
+        ResponseEntity
+            .ok(kafkaConnectService
+                .getConnectorTasks(getCluster(clusterName), connectName, connectorName))
+    );
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<Void>> restartConnectorTask(String clusterName, String connectName,
   public Mono<ResponseEntity<Void>> restartConnectorTask(String clusterName, String connectName,
                                                          String connectorName, Integer taskId,
                                                          String connectorName, Integer taskId,
                                                          ServerWebExchange exchange) {
                                                          ServerWebExchange exchange) {
-    return kafkaConnectService
-        .restartConnectorTask(getCluster(clusterName), connectName, connectorName, taskId)
-        .map(ResponseEntity::ok);
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .connect(connectName)
+        .connectActions(ConnectAction.VIEW, ConnectAction.EDIT)
+        .build());
+
+    return validateAccess.then(
+        kafkaConnectService
+            .restartConnectorTask(getCluster(clusterName), connectName, connectorName, taskId)
+            .map(ResponseEntity::ok)
+    );
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<Flux<ConnectorPluginDTO>>> getConnectorPlugins(
   public Mono<ResponseEntity<Flux<ConnectorPluginDTO>>> getConnectorPlugins(
       String clusterName, String connectName, ServerWebExchange exchange) {
       String clusterName, String connectName, ServerWebExchange exchange) {
-    return kafkaConnectService
-        .getConnectorPlugins(getCluster(clusterName), connectName)
-        .map(ResponseEntity::ok);
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .connect(connectName)
+        .connectActions(ConnectAction.VIEW)
+        .build());
+
+    return validateAccess.then(
+        kafkaConnectService
+            .getConnectorPlugins(getCluster(clusterName), connectName)
+            .map(ResponseEntity::ok)
+    );
   }
   }
 
 
   @Override
   @Override
-  public Mono<ResponseEntity<ConnectorPluginConfigValidationResponseDTO>>
-      validateConnectorPluginConfig(
+  public Mono<ResponseEntity<ConnectorPluginConfigValidationResponseDTO>> validateConnectorPluginConfig(
       String clusterName, String connectName, String pluginName, @Valid Mono<Object> requestBody,
       String clusterName, String connectName, String pluginName, @Valid Mono<Object> requestBody,
       ServerWebExchange exchange) {
       ServerWebExchange exchange) {
     return kafkaConnectService
     return kafkaConnectService

+ 39 - 13
kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/KsqlController.java

@@ -7,7 +7,10 @@ import com.provectus.kafka.ui.model.KsqlResponseDTO;
 import com.provectus.kafka.ui.model.KsqlStreamDescriptionDTO;
 import com.provectus.kafka.ui.model.KsqlStreamDescriptionDTO;
 import com.provectus.kafka.ui.model.KsqlTableDescriptionDTO;
 import com.provectus.kafka.ui.model.KsqlTableDescriptionDTO;
 import com.provectus.kafka.ui.model.KsqlTableResponseDTO;
 import com.provectus.kafka.ui.model.KsqlTableResponseDTO;
+import com.provectus.kafka.ui.model.rbac.AccessContext;
+import com.provectus.kafka.ui.model.rbac.permission.KsqlAction;
 import com.provectus.kafka.ui.service.ksql.KsqlServiceV2;
 import com.provectus.kafka.ui.service.ksql.KsqlServiceV2;
+import com.provectus.kafka.ui.service.rbac.AccessControlService;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Optional;
@@ -19,51 +22,74 @@ import org.springframework.web.server.ServerWebExchange;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 import reactor.core.publisher.Mono;
 
 
-
 @RestController
 @RestController
 @RequiredArgsConstructor
 @RequiredArgsConstructor
 @Slf4j
 @Slf4j
 public class KsqlController extends AbstractController implements KsqlApi {
 public class KsqlController extends AbstractController implements KsqlApi {
 
 
   private final KsqlServiceV2 ksqlServiceV2;
   private final KsqlServiceV2 ksqlServiceV2;
+  private final AccessControlService accessControlService;
 
 
   @Override
   @Override
   public Mono<ResponseEntity<KsqlCommandV2ResponseDTO>> executeKsql(String clusterName,
   public Mono<ResponseEntity<KsqlCommandV2ResponseDTO>> executeKsql(String clusterName,
                                                                     Mono<KsqlCommandV2DTO>
                                                                     Mono<KsqlCommandV2DTO>
                                                                         ksqlCommand2Dto,
                                                                         ksqlCommand2Dto,
                                                                     ServerWebExchange exchange) {
                                                                     ServerWebExchange exchange) {
-    return ksqlCommand2Dto.map(dto -> {
-      var id = ksqlServiceV2.registerCommand(
-          getCluster(clusterName),
-          dto.getKsql(),
-          Optional.ofNullable(dto.getStreamsProperties()).orElse(Map.of()));
-      return new KsqlCommandV2ResponseDTO().pipeId(id);
-    }).map(ResponseEntity::ok);
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .ksqlActions(KsqlAction.EXECUTE)
+        .build());
+
+    return validateAccess.then(
+        ksqlCommand2Dto.map(dto -> {
+          var id = ksqlServiceV2.registerCommand(
+              getCluster(clusterName),
+              dto.getKsql(),
+              Optional.ofNullable(dto.getStreamsProperties()).orElse(Map.of()));
+          return new KsqlCommandV2ResponseDTO().pipeId(id);
+        }).map(ResponseEntity::ok)
+    );
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<Flux<KsqlResponseDTO>>> openKsqlResponsePipe(String clusterName,
   public Mono<ResponseEntity<Flux<KsqlResponseDTO>>> openKsqlResponsePipe(String clusterName,
                                                                           String pipeId,
                                                                           String pipeId,
                                                                           ServerWebExchange exchange) {
                                                                           ServerWebExchange exchange) {
-    return Mono.just(
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .ksqlActions(KsqlAction.EXECUTE)
+        .build());
+
+    return validateAccess.thenReturn(
         ResponseEntity.ok(ksqlServiceV2.execute(pipeId)
         ResponseEntity.ok(ksqlServiceV2.execute(pipeId)
             .map(table -> new KsqlResponseDTO()
             .map(table -> new KsqlResponseDTO()
                 .table(
                 .table(
                     new KsqlTableResponseDTO()
                     new KsqlTableResponseDTO()
                         .header(table.getHeader())
                         .header(table.getHeader())
                         .columnNames(table.getColumnNames())
                         .columnNames(table.getColumnNames())
-                        .values((List<List<Object>>) ((List<?>) (table.getValues())))))));
+                        .values((List<List<Object>>) ((List<?>) (table.getValues()))))))
+    );
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<Flux<KsqlStreamDescriptionDTO>>> listStreams(String clusterName,
   public Mono<ResponseEntity<Flux<KsqlStreamDescriptionDTO>>> listStreams(String clusterName,
-                                                                         ServerWebExchange exchange) {
-    return Mono.just(ResponseEntity.ok(ksqlServiceV2.listStreams(getCluster(clusterName))));
+                                                                          ServerWebExchange exchange) {
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .ksqlActions(KsqlAction.EXECUTE)
+        .build());
+
+    return validateAccess.thenReturn(ResponseEntity.ok(ksqlServiceV2.listStreams(getCluster(clusterName))));
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<Flux<KsqlTableDescriptionDTO>>> listTables(String clusterName,
   public Mono<ResponseEntity<Flux<KsqlTableDescriptionDTO>>> listTables(String clusterName,
                                                                         ServerWebExchange exchange) {
                                                                         ServerWebExchange exchange) {
-    return Mono.just(ResponseEntity.ok(ksqlServiceV2.listTables(getCluster(clusterName))));
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .ksqlActions(KsqlAction.EXECUTE)
+        .build());
+
+    return validateAccess.thenReturn(ResponseEntity.ok(ksqlServiceV2.listTables(getCluster(clusterName))));
   }
   }
 }
 }

+ 62 - 19
kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/MessagesController.java

@@ -1,5 +1,8 @@
 package com.provectus.kafka.ui.controller;
 package com.provectus.kafka.ui.controller;
 
 
+import static com.provectus.kafka.ui.model.rbac.permission.TopicAction.MESSAGES_DELETE;
+import static com.provectus.kafka.ui.model.rbac.permission.TopicAction.MESSAGES_PRODUCE;
+import static com.provectus.kafka.ui.model.rbac.permission.TopicAction.MESSAGES_READ;
 import static com.provectus.kafka.ui.serde.api.Serde.Target.KEY;
 import static com.provectus.kafka.ui.serde.api.Serde.Target.KEY;
 import static com.provectus.kafka.ui.serde.api.Serde.Target.VALUE;
 import static com.provectus.kafka.ui.serde.api.Serde.Target.VALUE;
 import static java.util.stream.Collectors.toMap;
 import static java.util.stream.Collectors.toMap;
@@ -14,8 +17,11 @@ import com.provectus.kafka.ui.model.SeekTypeDTO;
 import com.provectus.kafka.ui.model.SerdeUsageDTO;
 import com.provectus.kafka.ui.model.SerdeUsageDTO;
 import com.provectus.kafka.ui.model.TopicMessageEventDTO;
 import com.provectus.kafka.ui.model.TopicMessageEventDTO;
 import com.provectus.kafka.ui.model.TopicSerdeSuggestionDTO;
 import com.provectus.kafka.ui.model.TopicSerdeSuggestionDTO;
+import com.provectus.kafka.ui.model.rbac.AccessContext;
+import com.provectus.kafka.ui.model.rbac.permission.TopicAction;
 import com.provectus.kafka.ui.service.DeserializationService;
 import com.provectus.kafka.ui.service.DeserializationService;
 import com.provectus.kafka.ui.service.MessagesService;
 import com.provectus.kafka.ui.service.MessagesService;
+import com.provectus.kafka.ui.service.rbac.AccessControlService;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Optional;
@@ -42,16 +48,26 @@ public class MessagesController extends AbstractController implements MessagesAp
 
 
   private final MessagesService messagesService;
   private final MessagesService messagesService;
   private final DeserializationService deserializationService;
   private final DeserializationService deserializationService;
+  private final AccessControlService accessControlService;
 
 
   @Override
   @Override
   public Mono<ResponseEntity<Void>> deleteTopicMessages(
   public Mono<ResponseEntity<Void>> deleteTopicMessages(
       String clusterName, String topicName, @Valid List<Integer> partitions,
       String clusterName, String topicName, @Valid List<Integer> partitions,
       ServerWebExchange exchange) {
       ServerWebExchange exchange) {
-    return messagesService.deleteTopicMessages(
-        getCluster(clusterName),
-        topicName,
-        Optional.ofNullable(partitions).orElse(List.of())
-    ).thenReturn(ResponseEntity.ok().build());
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .topic(topicName)
+        .topicActions(MESSAGES_DELETE)
+        .build());
+
+    return validateAccess.then(
+        messagesService.deleteTopicMessages(
+            getCluster(clusterName),
+            topicName,
+            Optional.ofNullable(partitions).orElse(List.of())
+        ).thenReturn(ResponseEntity.ok().build())
+    );
   }
   }
 
 
   @Override
   @Override
@@ -66,6 +82,12 @@ public class MessagesController extends AbstractController implements MessagesAp
                                                                            String keySerde,
                                                                            String keySerde,
                                                                            String valueSerde,
                                                                            String valueSerde,
                                                                            ServerWebExchange exchange) {
                                                                            ServerWebExchange exchange) {
+    final Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .topic(topicName)
+        .topicActions(MESSAGES_READ)
+        .build());
+
     seekType = seekType != null ? seekType : SeekTypeDTO.BEGINNING;
     seekType = seekType != null ? seekType : SeekTypeDTO.BEGINNING;
     seekDirection = seekDirection != null ? seekDirection : SeekDirectionDTO.FORWARD;
     seekDirection = seekDirection != null ? seekDirection : SeekDirectionDTO.FORWARD;
     filterQueryType = filterQueryType != null ? filterQueryType : MessageFilterTypeDTO.STRING_CONTAINS;
     filterQueryType = filterQueryType != null ? filterQueryType : MessageFilterTypeDTO.STRING_CONTAINS;
@@ -77,22 +99,33 @@ public class MessagesController extends AbstractController implements MessagesAp
         topicName,
         topicName,
         parseSeekTo(topicName, seekType, seekTo)
         parseSeekTo(topicName, seekType, seekTo)
     );
     );
-    return Mono.just(
+    Mono<ResponseEntity<Flux<TopicMessageEventDTO>>> job = Mono.just(
         ResponseEntity.ok(
         ResponseEntity.ok(
             messagesService.loadMessages(
             messagesService.loadMessages(
                 getCluster(clusterName), topicName, positions, q, filterQueryType,
                 getCluster(clusterName), topicName, positions, q, filterQueryType,
                 recordsLimit, seekDirection, keySerde, valueSerde)
                 recordsLimit, seekDirection, keySerde, valueSerde)
         )
         )
     );
     );
+
+    return validateAccess.then(job);
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<Void>> sendTopicMessages(
   public Mono<ResponseEntity<Void>> sendTopicMessages(
       String clusterName, String topicName, @Valid Mono<CreateTopicMessageDTO> createTopicMessage,
       String clusterName, String topicName, @Valid Mono<CreateTopicMessageDTO> createTopicMessage,
       ServerWebExchange exchange) {
       ServerWebExchange exchange) {
-    return createTopicMessage.flatMap(msg ->
-        messagesService.sendMessage(getCluster(clusterName), topicName, msg).then()
-    ).map(ResponseEntity::ok);
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .topic(topicName)
+        .topicActions(MESSAGES_PRODUCE)
+        .build());
+
+    return validateAccess.then(
+        createTopicMessage.flatMap(msg ->
+            messagesService.sendMessage(getCluster(clusterName), topicName, msg).then()
+        ).map(ResponseEntity::ok)
+    );
   }
   }
 
 
   /**
   /**
@@ -128,15 +161,25 @@ public class MessagesController extends AbstractController implements MessagesAp
                                                                  String topicName,
                                                                  String topicName,
                                                                  SerdeUsageDTO use,
                                                                  SerdeUsageDTO use,
                                                                  ServerWebExchange exchange) {
                                                                  ServerWebExchange exchange) {
-    return Mono.just(
-        new TopicSerdeSuggestionDTO()
-            .key(use == SerdeUsageDTO.SERIALIZE
-                ? deserializationService.getSerdesForSerialize(getCluster(clusterName), topicName, KEY)
-                : deserializationService.getSerdesForDeserialize(getCluster(clusterName), topicName, KEY))
-            .value(use == SerdeUsageDTO.SERIALIZE
-                ? deserializationService.getSerdesForSerialize(getCluster(clusterName), topicName, VALUE)
-                : deserializationService.getSerdesForDeserialize(getCluster(clusterName), topicName, VALUE))
-    ).subscribeOn(Schedulers.boundedElastic())
-        .map(ResponseEntity::ok);
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .topic(topicName)
+        .topicActions(TopicAction.VIEW)
+        .build());
+
+    TopicSerdeSuggestionDTO dto = new TopicSerdeSuggestionDTO()
+        .key(use == SerdeUsageDTO.SERIALIZE
+            ? deserializationService.getSerdesForSerialize(getCluster(clusterName), topicName, KEY)
+            : deserializationService.getSerdesForDeserialize(getCluster(clusterName), topicName, KEY))
+        .value(use == SerdeUsageDTO.SERIALIZE
+            ? deserializationService.getSerdesForSerialize(getCluster(clusterName), topicName, VALUE)
+            : deserializationService.getSerdesForDeserialize(getCluster(clusterName), topicName, VALUE));
+
+    return validateAccess.then(
+        Mono.just(dto)
+            .subscribeOn(Schedulers.boundedElastic())
+            .map(ResponseEntity::ok)
+    );
   }
   }
 }
 }

+ 140 - 40
kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/SchemasController.java

@@ -9,8 +9,10 @@ import com.provectus.kafka.ui.model.KafkaCluster;
 import com.provectus.kafka.ui.model.NewSchemaSubjectDTO;
 import com.provectus.kafka.ui.model.NewSchemaSubjectDTO;
 import com.provectus.kafka.ui.model.SchemaSubjectDTO;
 import com.provectus.kafka.ui.model.SchemaSubjectDTO;
 import com.provectus.kafka.ui.model.SchemaSubjectsResponseDTO;
 import com.provectus.kafka.ui.model.SchemaSubjectsResponseDTO;
+import com.provectus.kafka.ui.model.rbac.AccessContext;
+import com.provectus.kafka.ui.model.rbac.permission.SchemaAction;
 import com.provectus.kafka.ui.service.SchemaRegistryService;
 import com.provectus.kafka.ui.service.SchemaRegistryService;
-import java.util.Arrays;
+import com.provectus.kafka.ui.service.rbac.AccessControlService;
 import java.util.List;
 import java.util.List;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 import javax.validation.Valid;
 import javax.validation.Valid;
@@ -33,6 +35,7 @@ public class SchemasController extends AbstractController implements SchemasApi
   private final ClusterMapper mapper;
   private final ClusterMapper mapper;
 
 
   private final SchemaRegistryService schemaRegistryService;
   private final SchemaRegistryService schemaRegistryService;
+  private final AccessControlService accessControlService;
 
 
   @Override
   @Override
   protected KafkaCluster getCluster(String clusterName) {
   protected KafkaCluster getCluster(String clusterName) {
@@ -47,48 +50,105 @@ public class SchemasController extends AbstractController implements SchemasApi
   public Mono<ResponseEntity<CompatibilityCheckResponseDTO>> checkSchemaCompatibility(
   public Mono<ResponseEntity<CompatibilityCheckResponseDTO>> checkSchemaCompatibility(
       String clusterName, String subject, @Valid Mono<NewSchemaSubjectDTO> newSchemaSubject,
       String clusterName, String subject, @Valid Mono<NewSchemaSubjectDTO> newSchemaSubject,
       ServerWebExchange exchange) {
       ServerWebExchange exchange) {
-    return schemaRegistryService.checksSchemaCompatibility(
-            getCluster(clusterName), subject, newSchemaSubject)
-        .map(mapper::toCompatibilityCheckResponse)
-        .map(ResponseEntity::ok);
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .schema(subject)
+        .schemaActions(SchemaAction.VIEW)
+        .build());
+
+    return validateAccess.then(
+        schemaRegistryService.checksSchemaCompatibility(
+                getCluster(clusterName), subject, newSchemaSubject)
+            .map(mapper::toCompatibilityCheckResponse)
+            .map(ResponseEntity::ok)
+    );
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<SchemaSubjectDTO>> createNewSchema(
   public Mono<ResponseEntity<SchemaSubjectDTO>> createNewSchema(
       String clusterName, @Valid Mono<NewSchemaSubjectDTO> newSchemaSubject,
       String clusterName, @Valid Mono<NewSchemaSubjectDTO> newSchemaSubject,
       ServerWebExchange exchange) {
       ServerWebExchange exchange) {
-    return schemaRegistryService
-        .registerNewSchema(getCluster(clusterName), newSchemaSubject)
-        .map(ResponseEntity::ok);
+
+    return newSchemaSubject.flatMap(dto -> {
+      Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+          .cluster(clusterName)
+          .schemaActions(SchemaAction.CREATE)
+          .build());
+
+      return validateAccess.then(
+          schemaRegistryService
+              .registerNewSchema(getCluster(clusterName), dto)
+              .map(ResponseEntity::ok)
+      );
+    });
   }
   }
 
 
   @Override
   @Override
-  public Mono<ResponseEntity<Void>> deleteLatestSchema(
-      String clusterName, String subject, ServerWebExchange exchange) {
-    return schemaRegistryService.deleteLatestSchemaSubject(getCluster(clusterName), subject)
-        .thenReturn(ResponseEntity.ok().build());
+  public Mono<ResponseEntity<Void>> deleteLatestSchema(String clusterName,
+                                                       String subject,
+                                                       ServerWebExchange exchange) {
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .schema(subject)
+        .schemaActions(SchemaAction.DELETE)
+        .build());
+
+    return validateAccess.then(
+        schemaRegistryService.deleteLatestSchemaSubject(getCluster(clusterName), subject)
+            .thenReturn(ResponseEntity.ok().build())
+    );
   }
   }
 
 
   @Override
   @Override
-  public Mono<ResponseEntity<Void>> deleteSchema(
-      String clusterName, String subjectName, ServerWebExchange exchange) {
-    return schemaRegistryService.deleteSchemaSubjectEntirely(getCluster(clusterName), subjectName)
-        .thenReturn(ResponseEntity.ok().build());
+  public Mono<ResponseEntity<Void>> deleteSchema(String clusterName,
+                                                 String subject,
+                                                 ServerWebExchange exchange) {
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .schema(subject)
+        .schemaActions(SchemaAction.DELETE)
+        .build());
+
+    return validateAccess.then(
+        schemaRegistryService.deleteSchemaSubjectEntirely(getCluster(clusterName), subject)
+            .thenReturn(ResponseEntity.ok().build())
+    );
   }
   }
 
 
   @Override
   @Override
-  public Mono<ResponseEntity<Void>> deleteSchemaByVersion(
-      String clusterName, String subjectName, Integer version, ServerWebExchange exchange) {
-    return schemaRegistryService.deleteSchemaSubjectByVersion(getCluster(clusterName), subjectName, version)
-        .thenReturn(ResponseEntity.ok().build());
+  public Mono<ResponseEntity<Void>> deleteSchemaByVersion(String clusterName,
+                                                          String subject,
+                                                          Integer version,
+                                                          ServerWebExchange exchange) {
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .schema(subject)
+        .schemaActions(SchemaAction.DELETE)
+        .build());
+
+    return validateAccess.then(
+        schemaRegistryService.deleteSchemaSubjectByVersion(getCluster(clusterName), subject, version)
+            .thenReturn(ResponseEntity.ok().build())
+    );
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<Flux<SchemaSubjectDTO>>> getAllVersionsBySubject(
   public Mono<ResponseEntity<Flux<SchemaSubjectDTO>>> getAllVersionsBySubject(
-      String clusterName, String subjectName, ServerWebExchange exchange) {
+      String clusterName, String subject, ServerWebExchange exchange) {
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .schema(subject)
+        .schemaActions(SchemaAction.VIEW)
+        .build());
+
     Flux<SchemaSubjectDTO> schemas =
     Flux<SchemaSubjectDTO> schemas =
-        schemaRegistryService.getAllVersionsBySubject(getCluster(clusterName), subjectName);
-    return Mono.just(ResponseEntity.ok(schemas));
+        schemaRegistryService.getAllVersionsBySubject(getCluster(clusterName), subject);
+
+    return validateAccess.thenReturn(ResponseEntity.ok(schemas));
   }
   }
 
 
   @Override
   @Override
@@ -101,18 +161,36 @@ public class SchemasController extends AbstractController implements SchemasApi
   }
   }
 
 
   @Override
   @Override
-  public Mono<ResponseEntity<SchemaSubjectDTO>> getLatestSchema(String clusterName, String subject,
+  public Mono<ResponseEntity<SchemaSubjectDTO>> getLatestSchema(String clusterName,
+                                                                String subject,
                                                                 ServerWebExchange exchange) {
                                                                 ServerWebExchange exchange) {
-    return schemaRegistryService.getLatestSchemaVersionBySubject(getCluster(clusterName), subject)
-        .map(ResponseEntity::ok);
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .schema(subject)
+        .schemaActions(SchemaAction.VIEW)
+        .build());
+
+    return validateAccess.then(
+        schemaRegistryService.getLatestSchemaVersionBySubject(getCluster(clusterName), subject)
+            .map(ResponseEntity::ok)
+    );
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<SchemaSubjectDTO>> getSchemaByVersion(
   public Mono<ResponseEntity<SchemaSubjectDTO>> getSchemaByVersion(
       String clusterName, String subject, Integer version, ServerWebExchange exchange) {
       String clusterName, String subject, Integer version, ServerWebExchange exchange) {
-    return schemaRegistryService.getSchemaSubjectByVersion(
-            getCluster(clusterName), subject, version)
-        .map(ResponseEntity::ok);
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .schema(subject)
+        .schemaActions(SchemaAction.VIEW)
+        .build());
+
+    return validateAccess.then(
+        schemaRegistryService.getSchemaSubjectByVersion(
+                getCluster(clusterName), subject, version)
+            .map(ResponseEntity::ok)
+    );
   }
   }
 
 
   @Override
   @Override
@@ -120,16 +198,19 @@ public class SchemasController extends AbstractController implements SchemasApi
                                                                     @Valid Integer pageNum,
                                                                     @Valid Integer pageNum,
                                                                     @Valid Integer perPage,
                                                                     @Valid Integer perPage,
                                                                     @Valid String search,
                                                                     @Valid String search,
-                                                                    ServerWebExchange serverWebExchange) {
+                                                                    ServerWebExchange exchange) {
     return schemaRegistryService
     return schemaRegistryService
         .getAllSubjectNames(getCluster(clusterName))
         .getAllSubjectNames(getCluster(clusterName))
+        .flatMapMany(Flux::fromArray)
+        .filterWhen(schema -> accessControlService.isSchemaAccessible(schema, clusterName))
+        .collectList()
         .flatMap(subjects -> {
         .flatMap(subjects -> {
           int pageSize = perPage != null && perPage > 0 ? perPage : DEFAULT_PAGE_SIZE;
           int pageSize = perPage != null && perPage > 0 ? perPage : DEFAULT_PAGE_SIZE;
           int subjectToSkip = ((pageNum != null && pageNum > 0 ? pageNum : 1) - 1) * pageSize;
           int subjectToSkip = ((pageNum != null && pageNum > 0 ? pageNum : 1) - 1) * pageSize;
-          List<String> filteredSubjects = Arrays.stream(subjects)
+          List<String> filteredSubjects = subjects
+              .stream()
               .filter(subj -> search == null || StringUtils.containsIgnoreCase(subj, search))
               .filter(subj -> search == null || StringUtils.containsIgnoreCase(subj, search))
-              .sorted()
-              .collect(Collectors.toList());
+              .sorted().toList();
           var totalPages = (filteredSubjects.size() / pageSize)
           var totalPages = (filteredSubjects.size() / pageSize)
               + (filteredSubjects.size() % pageSize == 0 ? 0 : 1);
               + (filteredSubjects.size() % pageSize == 0 ? 0 : 1);
           List<String> subjectsToRender = filteredSubjects.stream()
           List<String> subjectsToRender = filteredSubjects.stream()
@@ -138,26 +219,45 @@ public class SchemasController extends AbstractController implements SchemasApi
               .collect(Collectors.toList());
               .collect(Collectors.toList());
           return schemaRegistryService.getAllLatestVersionSchemas(getCluster(clusterName), subjectsToRender)
           return schemaRegistryService.getAllLatestVersionSchemas(getCluster(clusterName), subjectsToRender)
               .map(a -> new SchemaSubjectsResponseDTO().pageCount(totalPages).schemas(a));
               .map(a -> new SchemaSubjectsResponseDTO().pageCount(totalPages).schemas(a));
-        }).map(ResponseEntity::ok);
+        })
+        .map(ResponseEntity::ok);
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<Void>> updateGlobalSchemaCompatibilityLevel(
   public Mono<ResponseEntity<Void>> updateGlobalSchemaCompatibilityLevel(
       String clusterName, @Valid Mono<CompatibilityLevelDTO> compatibilityLevel,
       String clusterName, @Valid Mono<CompatibilityLevelDTO> compatibilityLevel,
       ServerWebExchange exchange) {
       ServerWebExchange exchange) {
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .schemaActions(SchemaAction.MODIFY_GLOBAL_COMPATIBILITY)
+        .build());
+
     log.info("Updating schema compatibility globally");
     log.info("Updating schema compatibility globally");
-    return schemaRegistryService.updateSchemaCompatibility(
-            getCluster(clusterName), compatibilityLevel)
-        .map(ResponseEntity::ok);
+
+    return validateAccess.then(
+        schemaRegistryService.updateSchemaCompatibility(
+                getCluster(clusterName), compatibilityLevel)
+            .map(ResponseEntity::ok)
+    );
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<Void>> updateSchemaCompatibilityLevel(
   public Mono<ResponseEntity<Void>> updateSchemaCompatibilityLevel(
       String clusterName, String subject, @Valid Mono<CompatibilityLevelDTO> compatibilityLevel,
       String clusterName, String subject, @Valid Mono<CompatibilityLevelDTO> compatibilityLevel,
       ServerWebExchange exchange) {
       ServerWebExchange exchange) {
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .schemaActions(SchemaAction.EDIT)
+        .build());
+
     log.info("Updating schema compatibility for subject: {}", subject);
     log.info("Updating schema compatibility for subject: {}", subject);
-    return schemaRegistryService.updateSchemaCompatibility(
-            getCluster(clusterName), subject, compatibilityLevel)
-        .map(ResponseEntity::ok);
+
+    return validateAccess.then(
+        schemaRegistryService.updateSchemaCompatibility(
+                getCluster(clusterName), subject, compatibilityLevel)
+            .map(ResponseEntity::ok)
+    );
   }
   }
 }
 }

+ 179 - 65
kafka-ui-api/src/main/java/com/provectus/kafka/ui/controller/TopicsController.java

@@ -1,5 +1,10 @@
 package com.provectus.kafka.ui.controller;
 package com.provectus.kafka.ui.controller;
 
 
+import static com.provectus.kafka.ui.model.rbac.permission.TopicAction.CREATE;
+import static com.provectus.kafka.ui.model.rbac.permission.TopicAction.DELETE;
+import static com.provectus.kafka.ui.model.rbac.permission.TopicAction.EDIT;
+import static com.provectus.kafka.ui.model.rbac.permission.TopicAction.MESSAGES_READ;
+import static com.provectus.kafka.ui.model.rbac.permission.TopicAction.VIEW;
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toList;
 
 
 import com.provectus.kafka.ui.api.TopicsApi;
 import com.provectus.kafka.ui.api.TopicsApi;
@@ -19,8 +24,10 @@ import com.provectus.kafka.ui.model.TopicDTO;
 import com.provectus.kafka.ui.model.TopicDetailsDTO;
 import com.provectus.kafka.ui.model.TopicDetailsDTO;
 import com.provectus.kafka.ui.model.TopicUpdateDTO;
 import com.provectus.kafka.ui.model.TopicUpdateDTO;
 import com.provectus.kafka.ui.model.TopicsResponseDTO;
 import com.provectus.kafka.ui.model.TopicsResponseDTO;
+import com.provectus.kafka.ui.model.rbac.AccessContext;
 import com.provectus.kafka.ui.service.TopicsService;
 import com.provectus.kafka.ui.service.TopicsService;
 import com.provectus.kafka.ui.service.analyze.TopicAnalysisService;
 import com.provectus.kafka.ui.service.analyze.TopicAnalysisService;
+import com.provectus.kafka.ui.service.rbac.AccessControlService;
 import java.util.Comparator;
 import java.util.Comparator;
 import java.util.List;
 import java.util.List;
 import javax.validation.Valid;
 import javax.validation.Valid;
@@ -44,66 +51,121 @@ public class TopicsController extends AbstractController implements TopicsApi {
   private final TopicsService topicsService;
   private final TopicsService topicsService;
   private final TopicAnalysisService topicAnalysisService;
   private final TopicAnalysisService topicAnalysisService;
   private final ClusterMapper clusterMapper;
   private final ClusterMapper clusterMapper;
+  private final AccessControlService accessControlService;
 
 
   @Override
   @Override
   public Mono<ResponseEntity<TopicDTO>> createTopic(
   public Mono<ResponseEntity<TopicDTO>> createTopic(
       String clusterName, @Valid Mono<TopicCreationDTO> topicCreation, ServerWebExchange exchange) {
       String clusterName, @Valid Mono<TopicCreationDTO> topicCreation, ServerWebExchange exchange) {
-    return topicsService.createTopic(getCluster(clusterName), topicCreation)
-        .map(clusterMapper::toTopic)
-        .map(s -> new ResponseEntity<>(s, HttpStatus.OK))
-        .switchIfEmpty(Mono.just(ResponseEntity.notFound().build()));
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .topicActions(CREATE)
+        .build());
+
+    return validateAccess.then(
+        topicsService.createTopic(getCluster(clusterName), topicCreation)
+            .map(clusterMapper::toTopic)
+            .map(s -> new ResponseEntity<>(s, HttpStatus.OK))
+            .switchIfEmpty(Mono.just(ResponseEntity.notFound().build()))
+    );
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<TopicDTO>> recreateTopic(String clusterName,
   public Mono<ResponseEntity<TopicDTO>> recreateTopic(String clusterName,
-                                                      String topicName, ServerWebExchange serverWebExchange) {
-    return topicsService.recreateTopic(getCluster(clusterName), topicName)
-        .map(clusterMapper::toTopic)
-        .map(s -> new ResponseEntity<>(s, HttpStatus.CREATED));
+                                                      String topicName, ServerWebExchange exchange) {
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .topic(topicName)
+        .topicActions(VIEW, CREATE, DELETE)
+        .build());
+
+    return validateAccess.then(
+        topicsService.recreateTopic(getCluster(clusterName), topicName)
+            .map(clusterMapper::toTopic)
+            .map(s -> new ResponseEntity<>(s, HttpStatus.CREATED))
+    );
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<TopicDTO>> cloneTopic(
   public Mono<ResponseEntity<TopicDTO>> cloneTopic(
       String clusterName, String topicName, String newTopicName, ServerWebExchange exchange) {
       String clusterName, String topicName, String newTopicName, ServerWebExchange exchange) {
-    return topicsService.cloneTopic(getCluster(clusterName), topicName, newTopicName)
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .topic(topicName)
+        .topicActions(VIEW, CREATE)
+        .build());
+
+    return validateAccess.then(topicsService.cloneTopic(getCluster(clusterName), topicName, newTopicName)
         .map(clusterMapper::toTopic)
         .map(clusterMapper::toTopic)
-        .map(s -> new ResponseEntity<>(s, HttpStatus.CREATED));
+        .map(s -> new ResponseEntity<>(s, HttpStatus.CREATED))
+    );
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<Void>> deleteTopic(
   public Mono<ResponseEntity<Void>> deleteTopic(
       String clusterName, String topicName, ServerWebExchange exchange) {
       String clusterName, String topicName, ServerWebExchange exchange) {
-    return topicsService.deleteTopic(getCluster(clusterName), topicName).map(ResponseEntity::ok);
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .topic(topicName)
+        .topicActions(DELETE)
+        .build());
+
+    return validateAccess.then(
+        topicsService.deleteTopic(getCluster(clusterName), topicName).map(ResponseEntity::ok)
+    );
   }
   }
 
 
 
 
   @Override
   @Override
   public Mono<ResponseEntity<Flux<TopicConfigDTO>>> getTopicConfigs(
   public Mono<ResponseEntity<Flux<TopicConfigDTO>>> getTopicConfigs(
       String clusterName, String topicName, ServerWebExchange exchange) {
       String clusterName, String topicName, ServerWebExchange exchange) {
-    return topicsService.getTopicConfigs(getCluster(clusterName), topicName)
-        .map(lst -> lst.stream()
-            .map(InternalTopicConfig::from)
-            .map(clusterMapper::toTopicConfig)
-            .collect(toList()))
-        .map(Flux::fromIterable)
-        .map(ResponseEntity::ok);
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .topic(topicName)
+        .topicActions(VIEW)
+        .build());
+
+    return validateAccess.then(
+        topicsService.getTopicConfigs(getCluster(clusterName), topicName)
+            .map(lst -> lst.stream()
+                .map(InternalTopicConfig::from)
+                .map(clusterMapper::toTopicConfig)
+                .collect(toList()))
+            .map(Flux::fromIterable)
+            .map(ResponseEntity::ok)
+    );
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<TopicDetailsDTO>> getTopicDetails(
   public Mono<ResponseEntity<TopicDetailsDTO>> getTopicDetails(
       String clusterName, String topicName, ServerWebExchange exchange) {
       String clusterName, String topicName, ServerWebExchange exchange) {
-    return topicsService.getTopicDetails(getCluster(clusterName), topicName)
-        .map(clusterMapper::toTopicDetails)
-        .map(ResponseEntity::ok);
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .topic(topicName)
+        .topicActions(VIEW)
+        .build());
+
+    return validateAccess.then(
+        topicsService.getTopicDetails(getCluster(clusterName), topicName)
+            .map(clusterMapper::toTopicDetails)
+            .map(ResponseEntity::ok)
+    );
   }
   }
 
 
-  public Mono<ResponseEntity<TopicsResponseDTO>> getTopics(String clusterName, @Valid Integer page,
+  @Override
+  public Mono<ResponseEntity<TopicsResponseDTO>> getTopics(String clusterName,
+                                                           @Valid Integer page,
                                                            @Valid Integer perPage,
                                                            @Valid Integer perPage,
                                                            @Valid Boolean showInternal,
                                                            @Valid Boolean showInternal,
                                                            @Valid String search,
                                                            @Valid String search,
                                                            @Valid TopicColumnsToSortDTO orderBy,
                                                            @Valid TopicColumnsToSortDTO orderBy,
                                                            @Valid SortOrderDTO sortOrder,
                                                            @Valid SortOrderDTO sortOrder,
                                                            ServerWebExchange exchange) {
                                                            ServerWebExchange exchange) {
+
     return topicsService.getTopicsForPagination(getCluster(clusterName))
     return topicsService.getTopicsForPagination(getCluster(clusterName))
         .flatMap(existingTopics -> {
         .flatMap(existingTopics -> {
           int pageSize = perPage != null && perPage > 0 ? perPage : DEFAULT_PAGE_SIZE;
           int pageSize = perPage != null && perPage > 0 ? perPage : DEFAULT_PAGE_SIZE;
@@ -115,7 +177,7 @@ public class TopicsController extends AbstractController implements TopicsApi {
                   || showInternal != null && showInternal)
                   || showInternal != null && showInternal)
               .filter(topic -> search == null || StringUtils.contains(topic.getName(), search))
               .filter(topic -> search == null || StringUtils.contains(topic.getName(), search))
               .sorted(comparator)
               .sorted(comparator)
-              .collect(toList());
+              .toList();
           var totalPages = (filtered.size() / pageSize)
           var totalPages = (filtered.size() / pageSize)
               + (filtered.size() % pageSize == 0 ? 0 : 1);
               + (filtered.size() % pageSize == 0 ? 0 : 1);
 
 
@@ -126,42 +188,34 @@ public class TopicsController extends AbstractController implements TopicsApi {
               .collect(toList());
               .collect(toList());
 
 
           return topicsService.loadTopics(getCluster(clusterName), topicsPage)
           return topicsService.loadTopics(getCluster(clusterName), topicsPage)
+              .flatMapMany(Flux::fromIterable)
+              .filterWhen(dto -> accessControlService.isTopicAccessible(dto, clusterName))
+              .collectList()
               .map(topicsToRender ->
               .map(topicsToRender ->
                   new TopicsResponseDTO()
                   new TopicsResponseDTO()
                       .topics(topicsToRender.stream().map(clusterMapper::toTopic).collect(toList()))
                       .topics(topicsToRender.stream().map(clusterMapper::toTopic).collect(toList()))
                       .pageCount(totalPages));
                       .pageCount(totalPages));
-        }).map(ResponseEntity::ok);
-  }
-
-  private Comparator<InternalTopic> getComparatorForTopic(
-      TopicColumnsToSortDTO orderBy) {
-    var defaultComparator = Comparator.comparing(InternalTopic::getName);
-    if (orderBy == null) {
-      return defaultComparator;
-    }
-    switch (orderBy) {
-      case TOTAL_PARTITIONS:
-        return Comparator.comparing(InternalTopic::getPartitionCount);
-      case OUT_OF_SYNC_REPLICAS:
-        return Comparator.comparing(t -> t.getReplicas() - t.getInSyncReplicas());
-      case REPLICATION_FACTOR:
-        return Comparator.comparing(InternalTopic::getReplicationFactor);
-      case SIZE:
-        return Comparator.comparing(InternalTopic::getSegmentSize);
-      case NAME:
-      default:
-        return defaultComparator;
-    }
+        })
+        .map(ResponseEntity::ok);
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<TopicDTO>> updateTopic(
   public Mono<ResponseEntity<TopicDTO>> updateTopic(
-      String clusterId, String topicName, @Valid Mono<TopicUpdateDTO> topicUpdate,
+      String clusterName, String topicName, @Valid Mono<TopicUpdateDTO> topicUpdate,
       ServerWebExchange exchange) {
       ServerWebExchange exchange) {
-    return topicsService
-        .updateTopic(getCluster(clusterId), topicName, topicUpdate)
-        .map(clusterMapper::toTopic)
-        .map(ResponseEntity::ok);
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .topic(topicName)
+        .topicActions(VIEW, EDIT)
+        .build());
+
+    return validateAccess.then(
+        topicsService
+            .updateTopic(getCluster(clusterName), topicName, topicUpdate)
+            .map(clusterMapper::toTopic)
+            .map(ResponseEntity::ok)
+    );
   }
   }
 
 
   @Override
   @Override
@@ -169,9 +223,18 @@ public class TopicsController extends AbstractController implements TopicsApi {
       String clusterName, String topicName,
       String clusterName, String topicName,
       Mono<PartitionsIncreaseDTO> partitionsIncrease,
       Mono<PartitionsIncreaseDTO> partitionsIncrease,
       ServerWebExchange exchange) {
       ServerWebExchange exchange) {
-    return partitionsIncrease.flatMap(partitions ->
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .topic(topicName)
+        .topicActions(VIEW, EDIT)
+        .build());
+
+    return validateAccess.then(
+        partitionsIncrease.flatMap(partitions ->
             topicsService.increaseTopicPartitions(getCluster(clusterName), topicName, partitions)
             topicsService.increaseTopicPartitions(getCluster(clusterName), topicName, partitions)
-        ).map(ResponseEntity::ok);
+        ).map(ResponseEntity::ok)
+    );
   }
   }
 
 
   @Override
   @Override
@@ -179,23 +242,48 @@ public class TopicsController extends AbstractController implements TopicsApi {
       String clusterName, String topicName,
       String clusterName, String topicName,
       Mono<ReplicationFactorChangeDTO> replicationFactorChange,
       Mono<ReplicationFactorChangeDTO> replicationFactorChange,
       ServerWebExchange exchange) {
       ServerWebExchange exchange) {
-    return replicationFactorChange
-        .flatMap(rfc ->
-            topicsService.changeReplicationFactor(getCluster(clusterName), topicName, rfc))
-        .map(ResponseEntity::ok);
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .topic(topicName)
+        .topicActions(VIEW, EDIT)
+        .build());
+
+    return validateAccess.then(
+        replicationFactorChange
+            .flatMap(rfc ->
+                topicsService.changeReplicationFactor(getCluster(clusterName), topicName, rfc))
+            .map(ResponseEntity::ok)
+    );
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<Void>> analyzeTopic(String clusterName, String topicName, ServerWebExchange exchange) {
   public Mono<ResponseEntity<Void>> analyzeTopic(String clusterName, String topicName, ServerWebExchange exchange) {
-    return topicAnalysisService.analyze(getCluster(clusterName), topicName)
-        .thenReturn(ResponseEntity.ok().build());
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .topic(topicName)
+        .topicActions(MESSAGES_READ)
+        .build());
+
+    return validateAccess.then(
+        topicAnalysisService.analyze(getCluster(clusterName), topicName)
+            .thenReturn(ResponseEntity.ok().build())
+    );
   }
   }
 
 
   @Override
   @Override
   public Mono<ResponseEntity<Void>> cancelTopicAnalysis(String clusterName, String topicName,
   public Mono<ResponseEntity<Void>> cancelTopicAnalysis(String clusterName, String topicName,
-                                                       ServerWebExchange exchange) {
+                                                        ServerWebExchange exchange) {
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .topic(topicName)
+        .topicActions(MESSAGES_READ)
+        .build());
+
     topicAnalysisService.cancelAnalysis(getCluster(clusterName), topicName);
     topicAnalysisService.cancelAnalysis(getCluster(clusterName), topicName);
-    return Mono.just(ResponseEntity.ok().build());
+
+    return validateAccess.thenReturn(ResponseEntity.ok().build());
   }
   }
 
 
 
 
@@ -203,10 +291,36 @@ public class TopicsController extends AbstractController implements TopicsApi {
   public Mono<ResponseEntity<TopicAnalysisDTO>> getTopicAnalysis(String clusterName,
   public Mono<ResponseEntity<TopicAnalysisDTO>> getTopicAnalysis(String clusterName,
                                                                  String topicName,
                                                                  String topicName,
                                                                  ServerWebExchange exchange) {
                                                                  ServerWebExchange exchange) {
-    return Mono.just(
-        topicAnalysisService.getTopicAnalysis(getCluster(clusterName), topicName)
-            .map(ResponseEntity::ok)
-            .orElseGet(() -> ResponseEntity.notFound().build())
-    );
+
+    Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
+        .cluster(clusterName)
+        .topic(topicName)
+        .topicActions(MESSAGES_READ)
+        .build());
+
+    return validateAccess.thenReturn(topicAnalysisService.getTopicAnalysis(getCluster(clusterName), topicName)
+        .map(ResponseEntity::ok)
+        .orElseGet(() -> ResponseEntity.notFound().build()));
+  }
+
+  private Comparator<InternalTopic> getComparatorForTopic(
+      TopicColumnsToSortDTO orderBy) {
+    var defaultComparator = Comparator.comparing(InternalTopic::getName);
+    if (orderBy == null) {
+      return defaultComparator;
+    }
+    switch (orderBy) {
+      case TOTAL_PARTITIONS:
+        return Comparator.comparing(InternalTopic::getPartitionCount);
+      case OUT_OF_SYNC_REPLICAS:
+        return Comparator.comparing(t -> t.getReplicas() - t.getInSyncReplicas());
+      case REPLICATION_FACTOR:
+        return Comparator.comparing(InternalTopic::getReplicationFactor);
+      case SIZE:
+        return Comparator.comparing(InternalTopic::getSegmentSize);
+      case NAME:
+      default:
+        return defaultComparator;
+    }
   }
   }
 }
 }

+ 2 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/exception/ErrorCode.java

@@ -7,6 +7,8 @@ import org.springframework.http.HttpStatus;
 
 
 public enum ErrorCode {
 public enum ErrorCode {
 
 
+  FORBIDDEN(403, HttpStatus.FORBIDDEN),
+
   UNEXPECTED(5000, HttpStatus.INTERNAL_SERVER_ERROR),
   UNEXPECTED(5000, HttpStatus.INTERNAL_SERVER_ERROR),
   KSQL_API_ERROR(5001, HttpStatus.INTERNAL_SERVER_ERROR),
   KSQL_API_ERROR(5001, HttpStatus.INTERNAL_SERVER_ERROR),
   BINDING_FAIL(4001, HttpStatus.BAD_REQUEST),
   BINDING_FAIL(4001, HttpStatus.BAD_REQUEST),

+ 134 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/AccessContext.java

@@ -0,0 +1,134 @@
+package com.provectus.kafka.ui.model.rbac;
+
+import com.provectus.kafka.ui.model.rbac.permission.ClusterConfigAction;
+import com.provectus.kafka.ui.model.rbac.permission.ConnectAction;
+import com.provectus.kafka.ui.model.rbac.permission.ConsumerGroupAction;
+import com.provectus.kafka.ui.model.rbac.permission.KsqlAction;
+import com.provectus.kafka.ui.model.rbac.permission.SchemaAction;
+import com.provectus.kafka.ui.model.rbac.permission.TopicAction;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import lombok.Value;
+import org.springframework.util.Assert;
+
+@Value
+public class AccessContext {
+
+  String cluster;
+  Collection<ClusterConfigAction> clusterConfigActions;
+
+  String topic;
+  Collection<TopicAction> topicActions;
+
+  String consumerGroup;
+  Collection<ConsumerGroupAction> consumerGroupActions;
+
+  String connect;
+  Collection<ConnectAction> connectActions;
+
+  String connector;
+
+  String schema;
+  Collection<SchemaAction> schemaActions;
+
+  Collection<KsqlAction> ksqlActions;
+
+  public static AccessContextBuilder builder() {
+    return new AccessContextBuilder();
+  }
+
+  public static final class AccessContextBuilder {
+    private String cluster;
+    private Collection<ClusterConfigAction> clusterConfigActions = Collections.emptySet();
+    private String topic;
+    private Collection<TopicAction> topicActions = Collections.emptySet();
+    private String consumerGroup;
+    private Collection<ConsumerGroupAction> consumerGroupActions = Collections.emptySet();
+    private String connect;
+    private Collection<ConnectAction> connectActions = Collections.emptySet();
+    private String connector;
+    private String schema;
+    private Collection<SchemaAction> schemaActions = Collections.emptySet();
+    private Collection<KsqlAction> ksqlActions = Collections.emptySet();
+
+    private AccessContextBuilder() {
+    }
+
+    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;
+      return this;
+    }
+
+    public AccessContextBuilder consumerGroupActions(ConsumerGroupAction... actions) {
+      Assert.isTrue(actions.length > 0, "actions not present");
+      this.consumerGroupActions = List.of(actions);
+      return this;
+    }
+
+    public AccessContextBuilder connect(String connect) {
+      this.connect = connect;
+      return this;
+    }
+
+    public AccessContextBuilder connectActions(ConnectAction... actions) {
+      Assert.isTrue(actions.length > 0, "actions not present");
+      this.connectActions = List.of(actions);
+      return this;
+    }
+
+    public AccessContextBuilder connector(String connector) {
+      this.connector = connector;
+      return this;
+    }
+
+    public AccessContextBuilder schema(String schema) {
+      this.schema = schema;
+      return this;
+    }
+
+    public AccessContextBuilder schemaActions(SchemaAction... actions) {
+      Assert.isTrue(actions.length > 0, "actions not present");
+      this.schemaActions = List.of(actions);
+      return this;
+    }
+
+    public AccessContextBuilder ksqlActions(KsqlAction... actions) {
+      Assert.isTrue(actions.length > 0, "actions not present");
+      this.ksqlActions = List.of(actions);
+      return this;
+    }
+
+    public AccessContext build() {
+      return new AccessContext(cluster, clusterConfigActions,
+          topic, topicActions,
+          consumerGroup, consumerGroupActions,
+          connect, connectActions,
+          connector,
+          schema, schemaActions,
+          ksqlActions);
+    }
+  }
+}

+ 72 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Permission.java

@@ -0,0 +1,72 @@
+package com.provectus.kafka.ui.model.rbac;
+
+import static com.provectus.kafka.ui.model.rbac.Resource.CLUSTERCONFIG;
+import static com.provectus.kafka.ui.model.rbac.Resource.KSQL;
+
+import com.provectus.kafka.ui.model.rbac.permission.ClusterConfigAction;
+import com.provectus.kafka.ui.model.rbac.permission.ConnectAction;
+import com.provectus.kafka.ui.model.rbac.permission.ConsumerGroupAction;
+import com.provectus.kafka.ui.model.rbac.permission.KsqlAction;
+import com.provectus.kafka.ui.model.rbac.permission.SchemaAction;
+import com.provectus.kafka.ui.model.rbac.permission.TopicAction;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Pattern;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.commons.collections.CollectionUtils;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.util.Assert;
+
+@Getter
+@ToString
+@EqualsAndHashCode
+public class Permission {
+
+  Resource resource;
+
+  @Nullable
+  Pattern value;
+  List<String> actions;
+
+  @SuppressWarnings("unused")
+  public void setResource(String resource) {
+    this.resource = Resource.fromString(resource.toUpperCase());
+  }
+
+  public void setValue(String value) {
+    this.value = Pattern.compile(value);
+  }
+
+  @SuppressWarnings("unused")
+  public void setActions(List<String> actions) {
+    this.actions = actions;
+  }
+
+  public void validate() {
+    Assert.notNull(resource, "resource cannot be null");
+    if (!List.of(KSQL, CLUSTERCONFIG).contains(this.resource)) {
+      Assert.notNull(value, "permission value can't be empty for resource " + resource);
+    }
+  }
+
+  public void transform() {
+    if (CollectionUtils.isEmpty(actions) || this.actions.stream().noneMatch("ALL"::equalsIgnoreCase)) {
+      return;
+    }
+    this.actions = getActionValues();
+  }
+
+  private List<String> getActionValues() {
+    return switch (this.resource) {
+      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();
+    };
+  }
+
+}

+ 21 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Resource.java

@@ -0,0 +1,21 @@
+package com.provectus.kafka.ui.model.rbac;
+
+import org.apache.commons.lang3.EnumUtils;
+import org.jetbrains.annotations.Nullable;
+
+public enum Resource {
+
+  CLUSTERCONFIG,
+  TOPIC,
+  CONSUMER,
+  SCHEMA,
+  CONNECT,
+  KSQL;
+
+  @Nullable
+  public static Resource fromString(String name) {
+    return EnumUtils.getEnum(Resource.class, name);
+  }
+
+
+}

+ 19 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Role.java

@@ -0,0 +1,19 @@
+package com.provectus.kafka.ui.model.rbac;
+
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class Role {
+
+  String name;
+  List<String> clusters;
+  List<Subject> subjects;
+  List<Permission> permissions;
+
+  public void validate() {
+    permissions.forEach(Permission::transform);
+    permissions.forEach(Permission::validate);
+  }
+
+}

+ 24 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/Subject.java

@@ -0,0 +1,24 @@
+package com.provectus.kafka.ui.model.rbac;
+
+import com.provectus.kafka.ui.model.rbac.provider.Provider;
+import lombok.Getter;
+
+@Getter
+public class Subject {
+
+  Provider provider;
+  String type;
+  String value;
+
+  public void setProvider(String provider) {
+    this.provider = Provider.fromString(provider.toUpperCase());
+  }
+
+  public void setType(String type) {
+    this.type = type;
+  }
+
+  public void setValue(String value) {
+    this.value = value;
+  }
+}

+ 18 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ClusterConfigAction.java

@@ -0,0 +1,18 @@
+package com.provectus.kafka.ui.model.rbac.permission;
+
+import org.apache.commons.lang3.EnumUtils;
+import org.jetbrains.annotations.Nullable;
+
+public enum ClusterConfigAction implements PermissibleAction {
+
+  VIEW,
+  EDIT
+
+  ;
+
+  @Nullable
+  public static ClusterConfigAction fromString(String name) {
+    return EnumUtils.getEnum(ClusterConfigAction.class, name);
+  }
+
+}

+ 19 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ConnectAction.java

@@ -0,0 +1,19 @@
+package com.provectus.kafka.ui.model.rbac.permission;
+
+import org.apache.commons.lang3.EnumUtils;
+import org.jetbrains.annotations.Nullable;
+
+public enum ConnectAction implements PermissibleAction {
+
+  VIEW,
+  EDIT,
+  CREATE
+
+  ;
+
+  @Nullable
+  public static ConnectAction fromString(String name) {
+    return EnumUtils.getEnum(ConnectAction.class, name);
+  }
+
+}

+ 20 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/ConsumerGroupAction.java

@@ -0,0 +1,20 @@
+package com.provectus.kafka.ui.model.rbac.permission;
+
+import org.apache.commons.lang3.EnumUtils;
+import org.jetbrains.annotations.Nullable;
+
+public enum ConsumerGroupAction implements PermissibleAction {
+
+  VIEW,
+  DELETE,
+
+  RESET_OFFSETS
+
+  ;
+
+  @Nullable
+  public static ConsumerGroupAction fromString(String name) {
+    return EnumUtils.getEnum(ConsumerGroupAction.class, name);
+  }
+
+}

+ 15 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/KsqlAction.java

@@ -0,0 +1,15 @@
+package com.provectus.kafka.ui.model.rbac.permission;
+
+import org.apache.commons.lang3.EnumUtils;
+import org.jetbrains.annotations.Nullable;
+
+public enum KsqlAction implements PermissibleAction {
+
+  EXECUTE;
+
+  @Nullable
+  public static KsqlAction fromString(String name) {
+    return EnumUtils.getEnum(KsqlAction.class, name);
+  }
+
+}

+ 4 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/PermissibleAction.java

@@ -0,0 +1,4 @@
+package com.provectus.kafka.ui.model.rbac.permission;
+
+public interface PermissibleAction {
+}

+ 21 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/SchemaAction.java

@@ -0,0 +1,21 @@
+package com.provectus.kafka.ui.model.rbac.permission;
+
+import org.apache.commons.lang3.EnumUtils;
+import org.jetbrains.annotations.Nullable;
+
+public enum SchemaAction implements PermissibleAction {
+
+  VIEW,
+  CREATE,
+  DELETE,
+  EDIT,
+  MODIFY_GLOBAL_COMPATIBILITY
+
+  ;
+
+  @Nullable
+  public static SchemaAction fromString(String name) {
+    return EnumUtils.getEnum(SchemaAction.class, name);
+  }
+
+}

+ 24 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/permission/TopicAction.java

@@ -0,0 +1,24 @@
+package com.provectus.kafka.ui.model.rbac.permission;
+
+import org.apache.commons.lang3.EnumUtils;
+import org.jetbrains.annotations.Nullable;
+
+public enum TopicAction implements PermissibleAction {
+
+  VIEW,
+  CREATE,
+  EDIT,
+  DELETE,
+
+  MESSAGES_READ,
+  MESSAGES_PRODUCE,
+  MESSAGES_DELETE,
+
+  ;
+
+  @Nullable
+  public static TopicAction fromString(String name) {
+    return EnumUtils.getEnum(TopicAction.class, name);
+  }
+
+}

+ 27 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/model/rbac/provider/Provider.java

@@ -0,0 +1,27 @@
+package com.provectus.kafka.ui.model.rbac.provider;
+
+import org.apache.commons.lang3.EnumUtils;
+import org.jetbrains.annotations.Nullable;
+
+public enum Provider {
+
+  OAUTH_GOOGLE,
+  OAUTH_GITHUB,
+
+  OAUTH_COGNITO,
+
+  LDAP,
+  LDAP_AD;
+
+  @Nullable
+  public static Provider fromString(String name) {
+    return EnumUtils.getEnum(Provider.class, name);
+  }
+
+  public static class Name {
+    public static String GOOGLE = "google";
+    public static String GITHUB = "github";
+    public static String COGNITO = "cognito";
+  }
+
+}

+ 1 - 1
kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/builtin/sr/JsonSchemaSchemaRegistrySerializer.java

@@ -4,7 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.provectus.kafka.ui.exception.ValidationException;
 import com.provectus.kafka.ui.exception.ValidationException;
-import com.provectus.kafka.ui.util.annotations.KafkaClientInternalsDependant;
+import com.provectus.kafka.ui.util.annotation.KafkaClientInternalsDependant;
 import io.confluent.kafka.schemaregistry.ParsedSchema;
 import io.confluent.kafka.schemaregistry.ParsedSchema;
 import io.confluent.kafka.schemaregistry.client.SchemaMetadata;
 import io.confluent.kafka.schemaregistry.client.SchemaMetadata;
 import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient;
 import io.confluent.kafka.schemaregistry.client.SchemaRegistryClient;

+ 1 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ClusterService.java

@@ -38,6 +38,7 @@ public class ClusterService {
   }
   }
 
 
   public Mono<ClusterMetricsDTO> getClusterMetrics(KafkaCluster cluster) {
   public Mono<ClusterMetricsDTO> getClusterMetrics(KafkaCluster cluster) {
+
     return Mono.just(
     return Mono.just(
         clusterMapper.toClusterMetrics(
         clusterMapper.toClusterMetrics(
             statisticsCache.get(cluster).getMetrics()));
             statisticsCache.get(cluster).getMetrics()));

+ 11 - 5
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ConsumerGroupService.java

@@ -5,6 +5,7 @@ import com.provectus.kafka.ui.model.InternalConsumerGroup;
 import com.provectus.kafka.ui.model.InternalTopicConsumerGroup;
 import com.provectus.kafka.ui.model.InternalTopicConsumerGroup;
 import com.provectus.kafka.ui.model.KafkaCluster;
 import com.provectus.kafka.ui.model.KafkaCluster;
 import com.provectus.kafka.ui.model.SortOrderDTO;
 import com.provectus.kafka.ui.model.SortOrderDTO;
+import com.provectus.kafka.ui.service.rbac.AccessControlService;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashMap;
@@ -35,6 +36,7 @@ import reactor.util.function.Tuples;
 public class ConsumerGroupService {
 public class ConsumerGroupService {
 
 
   private final AdminClientService adminClientService;
   private final AdminClientService adminClientService;
+  private final AccessControlService accessControlService;
 
 
   private Mono<List<InternalConsumerGroup>> getConsumerGroups(
   private Mono<List<InternalConsumerGroup>> getConsumerGroups(
       ReactiveAdminClient ac,
       ReactiveAdminClient ac,
@@ -107,8 +109,7 @@ public class ConsumerGroupService {
       int perPage,
       int perPage,
       @Nullable String search,
       @Nullable String search,
       ConsumerGroupOrderingDTO orderBy,
       ConsumerGroupOrderingDTO orderBy,
-      SortOrderDTO sortOrderDto
-  ) {
+      SortOrderDTO sortOrderDto) {
     var comparator = sortOrderDto.equals(SortOrderDTO.ASC)
     var comparator = sortOrderDto.equals(SortOrderDTO.ASC)
         ? getPaginationComparator(orderBy)
         ? getPaginationComparator(orderBy)
         : getPaginationComparator(orderBy).reversed();
         : getPaginationComparator(orderBy).reversed();
@@ -121,9 +122,14 @@ public class ConsumerGroupService {
                     .skip((long) (page - 1) * perPage)
                     .skip((long) (page - 1) * perPage)
                     .limit(perPage)
                     .limit(perPage)
                     .collect(Collectors.toList())
                     .collect(Collectors.toList())
-            ).map(cgs -> new ConsumerGroupsPage(
-                cgs,
-                (descriptions.size() / perPage) + (descriptions.size() % perPage == 0 ? 0 : 1))))
+            )
+                .flatMapMany(Flux::fromIterable)
+                .filterWhen(
+                    cg -> accessControlService.isConsumerGroupAccessible(cg.getGroupId(), cluster.getName()))
+                .collect(Collectors.toList())
+                .map(cgs -> new ConsumerGroupsPage(
+                    cgs,
+                    (descriptions.size() / perPage) + (descriptions.size() % perPage == 0 ? 0 : 1))))
     );
     );
   }
   }
 
 

+ 6 - 9
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConnectService.java

@@ -52,19 +52,16 @@ public class KafkaConnectService {
   private final KafkaConfigSanitizer kafkaConfigSanitizer;
   private final KafkaConfigSanitizer kafkaConfigSanitizer;
   private final KafkaConnectClientsFactory kafkaConnectClientsFactory;
   private final KafkaConnectClientsFactory kafkaConnectClientsFactory;
 
 
-  public Mono<Flux<ConnectDTO>> getConnects(KafkaCluster cluster) {
-    return Mono.just(
-        Flux.fromIterable(
-            cluster.getKafkaConnect().stream()
-                .map(clusterMapper::toKafkaConnect)
-                .collect(Collectors.toList())
-        )
-    );
+  public List<ConnectDTO> getConnects(KafkaCluster cluster) {
+    return cluster.getKafkaConnect().stream()
+        .map(clusterMapper::toKafkaConnect)
+        .collect(Collectors.toList());
   }
   }
 
 
   public Flux<FullConnectorInfoDTO> getAllConnectors(final KafkaCluster cluster,
   public Flux<FullConnectorInfoDTO> getAllConnectors(final KafkaCluster cluster,
                                                      final String search) {
                                                      final String search) {
-    return getConnects(cluster)
+    Mono<Flux<ConnectDTO>> clusters = Mono.just(Flux.fromIterable(getConnects(cluster))); // TODO get rid
+    return clusters
         .flatMapMany(Function.identity())
         .flatMapMany(Function.identity())
         .flatMap(connect -> getConnectorNames(cluster, connect.getName()))
         .flatMap(connect -> getConnectorNames(cluster, connect.getName()))
         .flatMap(pair -> getConnector(cluster, pair.getT1(), pair.getT2()))
         .flatMap(pair -> getConnector(cluster, pair.getT1(), pair.getT2()))

+ 1 - 1
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/ReactiveAdminClient.java

@@ -11,7 +11,7 @@ import com.provectus.kafka.ui.exception.NotFoundException;
 import com.provectus.kafka.ui.exception.ValidationException;
 import com.provectus.kafka.ui.exception.ValidationException;
 import com.provectus.kafka.ui.util.MapUtil;
 import com.provectus.kafka.ui.util.MapUtil;
 import com.provectus.kafka.ui.util.NumberUtil;
 import com.provectus.kafka.ui.util.NumberUtil;
-import com.provectus.kafka.ui.util.annotations.KafkaClientInternalsDependant;
+import com.provectus.kafka.ui.util.annotation.KafkaClientInternalsDependant;
 import java.io.Closeable;
 import java.io.Closeable;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Arrays;

+ 31 - 35
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/SchemaRegistryService.java

@@ -24,6 +24,7 @@ import com.provectus.kafka.ui.model.schemaregistry.SubjectIdResponse;
 import com.provectus.kafka.ui.util.SecuredWebClient;
 import com.provectus.kafka.ui.util.SecuredWebClient;
 import java.io.IOException;
 import java.io.IOException;
 import java.net.URI;
 import java.net.URI;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.Formatter;
 import java.util.Formatter;
 import java.util.List;
 import java.util.List;
@@ -198,17 +199,12 @@ public class SchemaRegistryService {
    * and then returns the whole content by requesting its latest version.
    * and then returns the whole content by requesting its latest version.
    */
    */
   public Mono<SchemaSubjectDTO> registerNewSchema(KafkaCluster cluster,
   public Mono<SchemaSubjectDTO> registerNewSchema(KafkaCluster cluster,
-                                                  Mono<NewSchemaSubjectDTO> newSchemaSubject) {
-    return newSchemaSubject
-        .flatMap(schema -> {
-          SchemaTypeDTO schemaType =
-              SchemaTypeDTO.AVRO == schema.getSchemaType() ? null : schema.getSchemaType();
-          Mono<InternalNewSchema> newSchema =
-              Mono.just(new InternalNewSchema(schema.getSchema(), schemaType));
-          String subject = schema.getSubject();
-          return submitNewSchema(subject, newSchema, cluster)
-              .flatMap(resp -> getLatestSchemaVersionBySubject(cluster, subject));
-        });
+                                                  NewSchemaSubjectDTO dto) {
+    SchemaTypeDTO schemaType = SchemaTypeDTO.AVRO == dto.getSchemaType() ? null : dto.getSchemaType();
+    Mono<InternalNewSchema> newSchema = Mono.just(new InternalNewSchema(dto.getSchema(), schemaType));
+    String subject = dto.getSubject();
+    return submitNewSchema(subject, newSchema, cluster)
+        .flatMap(resp -> getLatestSchemaVersionBySubject(cluster, subject));
   }
   }
 
 
   @NotNull
   @NotNull
@@ -258,18 +254,18 @@ public class SchemaRegistryService {
                                               Mono<CompatibilityLevelDTO> compatibilityLevel) {
                                               Mono<CompatibilityLevelDTO> compatibilityLevel) {
     String configEndpoint = Objects.isNull(schemaName) ? "/config" : "/config/{schemaName}";
     String configEndpoint = Objects.isNull(schemaName) ? "/config" : "/config/{schemaName}";
     return configuredWebClient(
     return configuredWebClient(
-            cluster,
-            HttpMethod.PUT,
-            configEndpoint,
+        cluster,
+        HttpMethod.PUT,
+        configEndpoint,
         schemaName)
         schemaName)
-            .contentType(MediaType.APPLICATION_JSON)
-            .body(BodyInserters.fromPublisher(compatibilityLevel, CompatibilityLevelDTO.class))
-            .retrieve()
-            .onStatus(NOT_FOUND::equals,
-                throwIfNotFoundStatus(formatted(NO_SUCH_SCHEMA, schemaName)))
-            .bodyToMono(Void.class)
-            .as(m -> failoverAble(m, new FailoverMono<>(cluster.getSchemaRegistry(),
-                () -> this.updateSchemaCompatibility(cluster, schemaName, compatibilityLevel))));
+        .contentType(MediaType.APPLICATION_JSON)
+        .body(BodyInserters.fromPublisher(compatibilityLevel, CompatibilityLevelDTO.class))
+        .retrieve()
+        .onStatus(NOT_FOUND::equals,
+            throwIfNotFoundStatus(formatted(NO_SUCH_SCHEMA, schemaName)))
+        .bodyToMono(Void.class)
+        .as(m -> failoverAble(m, new FailoverMono<>(cluster.getSchemaRegistry(),
+            () -> this.updateSchemaCompatibility(cluster, schemaName, compatibilityLevel))));
   }
   }
 
 
   public Mono<Void> updateSchemaCompatibility(KafkaCluster cluster,
   public Mono<Void> updateSchemaCompatibility(KafkaCluster cluster,
@@ -278,7 +274,7 @@ public class SchemaRegistryService {
   }
   }
 
 
   public Mono<InternalCompatibilityLevel> getSchemaCompatibilityLevel(KafkaCluster cluster,
   public Mono<InternalCompatibilityLevel> getSchemaCompatibilityLevel(KafkaCluster cluster,
-                                                                 String schemaName) {
+                                                                      String schemaName) {
     String globalConfig = Objects.isNull(schemaName) ? "/config" : "/config/{schemaName}";
     String globalConfig = Objects.isNull(schemaName) ? "/config" : "/config/{schemaName}";
     final var values = new LinkedMultiValueMap<String, String>();
     final var values = new LinkedMultiValueMap<String, String>();
     values.add("defaultToGlobal", "true");
     values.add("defaultToGlobal", "true");
@@ -298,7 +294,7 @@ public class SchemaRegistryService {
   }
   }
 
 
   private Mono<InternalCompatibilityLevel> getSchemaCompatibilityInfoOrGlobal(KafkaCluster cluster,
   private Mono<InternalCompatibilityLevel> getSchemaCompatibilityInfoOrGlobal(KafkaCluster cluster,
-                                                                         String schemaName) {
+                                                                              String schemaName) {
     return this.getSchemaCompatibilityLevel(cluster, schemaName)
     return this.getSchemaCompatibilityLevel(cluster, schemaName)
         .switchIfEmpty(this.getGlobalSchemaCompatibilityLevel(cluster));
         .switchIfEmpty(this.getGlobalSchemaCompatibilityLevel(cluster));
   }
   }
@@ -306,18 +302,18 @@ public class SchemaRegistryService {
   public Mono<InternalCompatibilityCheck> checksSchemaCompatibility(
   public Mono<InternalCompatibilityCheck> checksSchemaCompatibility(
       KafkaCluster cluster, String schemaName, Mono<NewSchemaSubjectDTO> newSchemaSubject) {
       KafkaCluster cluster, String schemaName, Mono<NewSchemaSubjectDTO> newSchemaSubject) {
     return configuredWebClient(
     return configuredWebClient(
-            cluster,
-            HttpMethod.POST,
-            "/compatibility/subjects/{schemaName}/versions/latest",
+        cluster,
+        HttpMethod.POST,
+        "/compatibility/subjects/{schemaName}/versions/latest",
         schemaName)
         schemaName)
-            .contentType(MediaType.APPLICATION_JSON)
-            .body(BodyInserters.fromPublisher(newSchemaSubject, NewSchemaSubjectDTO.class))
-            .retrieve()
-            .onStatus(NOT_FOUND::equals,
-                throwIfNotFoundStatus(formatted(NO_SUCH_SCHEMA, schemaName)))
-            .bodyToMono(InternalCompatibilityCheck.class)
-            .as(m -> failoverAble(m, new FailoverMono<>(cluster.getSchemaRegistry(),
-                () -> this.checksSchemaCompatibility(cluster, schemaName, newSchemaSubject))));
+        .contentType(MediaType.APPLICATION_JSON)
+        .body(BodyInserters.fromPublisher(newSchemaSubject, NewSchemaSubjectDTO.class))
+        .retrieve()
+        .onStatus(NOT_FOUND::equals,
+            throwIfNotFoundStatus(formatted(NO_SUCH_SCHEMA, schemaName)))
+        .bodyToMono(InternalCompatibilityCheck.class)
+        .as(m -> failoverAble(m, new FailoverMono<>(cluster.getSchemaRegistry(),
+            () -> this.checksSchemaCompatibility(cluster, schemaName, newSchemaSubject))));
   }
   }
 
 
   public String formatted(String str, Object... args) {
   public String formatted(String str, Object... args) {

+ 6 - 2
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/TopicsService.java

@@ -460,8 +460,12 @@ public class TopicsService {
   }
   }
 
 
   private Mono<List<String>> filterExisting(KafkaCluster cluster, Collection<String> topics) {
   private Mono<List<String>> filterExisting(KafkaCluster cluster, Collection<String> topics) {
-    return adminClientService.get(cluster).flatMap(ac -> ac.listTopics(true))
-        .map(existing -> existing.stream().filter(topics::contains).collect(toList()));
+    return adminClientService.get(cluster)
+        .flatMap(ac -> ac.listTopics(true))
+        .map(existing -> existing
+            .stream()
+            .filter(topics::contains)
+            .collect(toList()));
   }
   }
 
 
 }
 }

+ 31 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AbstractProviderCondition.java

@@ -0,0 +1,31 @@
+package com.provectus.kafka.ui.service.rbac;
+
+import com.provectus.kafka.ui.config.auth.OAuthProperties;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.context.properties.bind.Bindable;
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.core.env.Environment;
+
+public abstract class AbstractProviderCondition {
+  private static final Bindable<Map<String, OAuthProperties.OAuth2Provider>> OAUTH2_PROPERTIES = Bindable
+      .mapOf(String.class, OAuthProperties.OAuth2Provider.class);
+
+  protected Set<String> getRegisteredProvidersTypes(final Environment env) {
+    final Map<String, OAuthProperties.OAuth2Provider> properties = Binder.get(env)
+        .bind("auth.oauth2.client", OAUTH2_PROPERTIES)
+        .orElse(Map.of());
+    return properties.values().stream()
+        .map(OAuthProperties.OAuth2Provider::getCustomParams)
+        .filter(Objects::nonNull)
+        .filter(Predicate.not(Map::isEmpty))
+        .map(params -> params.get("type"))
+        .filter(Objects::nonNull)
+        .filter(StringUtils::isNotEmpty)
+        .collect(Collectors.toSet());
+  }
+}

+ 398 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AccessControlService.java

@@ -0,0 +1,398 @@
+package com.provectus.kafka.ui.service.rbac;
+
+import com.provectus.kafka.ui.config.auth.AuthenticatedUser;
+import com.provectus.kafka.ui.config.auth.RbacUser;
+import com.provectus.kafka.ui.config.auth.RoleBasedAccessControlProperties;
+import com.provectus.kafka.ui.model.ClusterDTO;
+import com.provectus.kafka.ui.model.ConnectDTO;
+import com.provectus.kafka.ui.model.InternalTopic;
+import com.provectus.kafka.ui.model.rbac.AccessContext;
+import com.provectus.kafka.ui.model.rbac.Permission;
+import com.provectus.kafka.ui.model.rbac.Resource;
+import com.provectus.kafka.ui.model.rbac.Role;
+import com.provectus.kafka.ui.model.rbac.permission.ConnectAction;
+import com.provectus.kafka.ui.model.rbac.permission.ConsumerGroupAction;
+import com.provectus.kafka.ui.model.rbac.permission.SchemaAction;
+import com.provectus.kafka.ui.model.rbac.permission.TopicAction;
+import com.provectus.kafka.ui.service.rbac.extractor.CognitoAuthorityExtractor;
+import com.provectus.kafka.ui.service.rbac.extractor.GithubAuthorityExtractor;
+import com.provectus.kafka.ui.service.rbac.extractor.GoogleAuthorityExtractor;
+import com.provectus.kafka.ui.service.rbac.extractor.LdapAuthorityExtractor;
+import com.provectus.kafka.ui.service.rbac.extractor.ProviderAuthorityExtractor;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.core.context.ReactiveSecurityContextHolder;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+import reactor.core.publisher.Mono;
+
+@Service
+@RequiredArgsConstructor
+@EnableConfigurationProperties(RoleBasedAccessControlProperties.class)
+@Slf4j
+public class AccessControlService {
+
+  @Nullable
+  private final InMemoryReactiveClientRegistrationRepository clientRegistrationRepository;
+
+  private boolean rbacEnabled = false;
+  private Set<ProviderAuthorityExtractor> extractors = Collections.emptySet();
+  private final RoleBasedAccessControlProperties properties;
+
+  @PostConstruct
+  public void init() {
+    if (properties.getRoles().isEmpty()) {
+      log.trace("No roles provided, disabling RBAC");
+      return;
+    }
+    rbacEnabled = true;
+
+    this.extractors = properties.getRoles()
+        .stream()
+        .map(role -> role.getSubjects()
+            .stream()
+            .map(provider -> switch (provider.getProvider()) {
+              case OAUTH_COGNITO -> new CognitoAuthorityExtractor();
+              case OAUTH_GOOGLE -> new GoogleAuthorityExtractor();
+              case OAUTH_GITHUB -> new GithubAuthorityExtractor();
+              case LDAP, LDAP_AD -> new LdapAuthorityExtractor();
+            }).collect(Collectors.toSet()))
+        .flatMap(Set::stream)
+        .collect(Collectors.toSet());
+
+    if ((clientRegistrationRepository == null || !clientRegistrationRepository.iterator().hasNext())
+        && !properties.getRoles().isEmpty()) {
+      log.error("Roles are configured but no authentication methods are present. Authentication might fail.");
+    }
+  }
+
+  public Mono<Void> validateAccess(AccessContext context) {
+    if (!rbacEnabled) {
+      return Mono.empty();
+    }
+
+    return getUser()
+        .doOnNext(user -> {
+          boolean accessGranted =
+              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);
+
+          if (!accessGranted) {
+            throw new AccessDeniedException("Access denied");
+          }
+        })
+        .then();
+  }
+
+  public Mono<AuthenticatedUser> getUser() {
+    return ReactiveSecurityContextHolder.getContext()
+        .map(SecurityContext::getAuthentication)
+        .filter(authentication -> authentication.getPrincipal() instanceof RbacUser)
+        .map(authentication -> ((RbacUser) authentication.getPrincipal()))
+        .map(user -> new AuthenticatedUser(user.name(), user.groups()));
+  }
+
+  private boolean isClusterAccessible(AccessContext context, AuthenticatedUser user) {
+    if (!rbacEnabled) {
+      return true;
+    }
+
+    Assert.isTrue(StringUtils.isNotEmpty(context.getCluster()), "cluster value is empty");
+
+    return properties.getRoles()
+        .stream()
+        .filter(filterRole(user))
+        .anyMatch(filterCluster(context.getCluster()));
+  }
+
+  public Mono<Boolean> 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<String> 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<String> requiredActions = context.getTopicActions()
+        .stream()
+        .map(a -> a.toString().toUpperCase())
+        .collect(Collectors.toSet());
+
+    return isAccessible(Resource.TOPIC, context.getTopic(), user, context, requiredActions);
+  }
+
+  public Mono<Boolean> isTopicAccessible(InternalTopic dto, String clusterName) {
+    if (!rbacEnabled) {
+      return Mono.just(true);
+    }
+
+    AccessContext accessContext = AccessContext
+        .builder()
+        .cluster(clusterName)
+        .topic(dto.getName())
+        .topicActions(TopicAction.VIEW)
+        .build();
+
+    return getUser().map(u -> isTopicAccessible(accessContext, u));
+  }
+
+  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<String> requiredActions = context.getConsumerGroupActions()
+        .stream()
+        .map(a -> a.toString().toUpperCase())
+        .collect(Collectors.toSet());
+
+    return isAccessible(Resource.CONSUMER, context.getConsumerGroup(), user, context, requiredActions);
+  }
+
+  public Mono<Boolean> 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<String> requiredActions = context.getSchemaActions()
+        .stream()
+        .map(a -> a.toString().toUpperCase())
+        .collect(Collectors.toSet());
+
+    return isAccessible(Resource.SCHEMA, context.getSchema(), user, context, requiredActions);
+  }
+
+  public Mono<Boolean> 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<String> requiredActions = context.getConnectActions()
+        .stream()
+        .map(a -> a.toString().toUpperCase())
+        .collect(Collectors.toSet());
+
+    return isAccessible(Resource.CONNECT, context.getConnect(), user, context, requiredActions);
+  }
+
+  public Mono<Boolean> isConnectAccessible(ConnectDTO dto, String clusterName) {
+    if (!rbacEnabled) {
+      return Mono.just(true);
+    }
+
+    return isConnectAccessible(dto.getName(), clusterName);
+  }
+
+  public Mono<Boolean> 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<Boolean> 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<String> requiredActions = context.getKsqlActions()
+        .stream()
+        .map(a -> a.toString().toUpperCase())
+        .collect(Collectors.toSet());
+
+    return isAccessible(Resource.KSQL, null, user, context, requiredActions);
+  }
+
+  public Set<ProviderAuthorityExtractor> getExtractors() {
+    return extractors;
+  }
+
+  public List<Role> getRoles() {
+    if (!rbacEnabled) {
+      return Collections.emptyList();
+    }
+    return Collections.unmodifiableList(properties.getRoles());
+  }
+
+  private boolean isAccessible(Resource resource, String resourceValue,
+                               AuthenticatedUser user, AccessContext context, Set<String> requiredActions) {
+    Set<String> grantedActions = properties.getRoles()
+        .stream()
+        .filter(filterRole(user))
+        .filter(filterCluster(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<Role> filterRole(AuthenticatedUser user) {
+    return role -> user.groups().contains(role.getName());
+  }
+
+  private Predicate<Role> filterCluster(String cluster) {
+    return grantedRole -> grantedRole.getClusters()
+        .stream()
+        .anyMatch(cluster::equalsIgnoreCase);
+  }
+
+  private Predicate<Permission> filterResource(Resource resource) {
+    return grantedPermission -> resource == grantedPermission.getResource();
+  }
+
+  private Predicate<Permission> filterResourceValue(String resourceValue) {
+
+    if (resourceValue == null) {
+      return grantedPermission -> true;
+    }
+    return grantedPermission -> {
+      Pattern value = grantedPermission.getValue();
+      if (value == null) {
+        return true;
+      }
+      return value.matcher(resourceValue).matches();
+    };
+  }
+
+  public boolean isRbacEnabled() {
+    return rbacEnabled;
+  }
+}

+ 70 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/CognitoAuthorityExtractor.java

@@ -0,0 +1,70 @@
+package com.provectus.kafka.ui.service.rbac.extractor;
+
+import com.nimbusds.jose.shaded.json.JSONArray;
+import com.provectus.kafka.ui.model.rbac.Role;
+import com.provectus.kafka.ui.model.rbac.provider.Provider;
+import com.provectus.kafka.ui.service.rbac.AccessControlService;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
+import reactor.core.publisher.Mono;
+
+@Slf4j
+public class CognitoAuthorityExtractor implements ProviderAuthorityExtractor {
+
+  private static final String COGNITO_GROUPS_ATTRIBUTE_NAME = "cognito:groups";
+
+  @Override
+  public boolean isApplicable(String provider) {
+    return Provider.Name.COGNITO.equalsIgnoreCase(provider);
+  }
+
+  @Override
+  public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<String, Object> additionalParams) {
+    log.debug("Extracting cognito user authorities");
+
+    DefaultOAuth2User principal;
+    try {
+      principal = (DefaultOAuth2User) value;
+    } catch (ClassCastException e) {
+      log.error("Can't cast value to DefaultOAuth2User", e);
+      throw new RuntimeException();
+    }
+
+    Set<String> groupsByUsername = acs.getRoles()
+        .stream()
+        .filter(r -> r.getSubjects()
+            .stream()
+            .filter(s -> s.getProvider().equals(Provider.OAUTH_COGNITO))
+            .filter(s -> s.getType().equals("user"))
+            .anyMatch(s -> s.getValue().equals(principal.getName())))
+        .map(Role::getName)
+        .collect(Collectors.toSet());
+
+    JSONArray groups = principal.getAttribute(COGNITO_GROUPS_ATTRIBUTE_NAME);
+    if (groups == null) {
+      log.debug("Cognito groups param is not present");
+      return Mono.just(groupsByUsername);
+    }
+
+    Set<String> groupsByGroups = acs.getRoles()
+        .stream()
+        .filter(role -> role.getSubjects()
+            .stream()
+            .filter(s -> s.getProvider().equals(Provider.OAUTH_COGNITO))
+            .filter(s -> s.getType().equals("group"))
+            .anyMatch(subject -> Stream.of(groups.toArray())
+                .map(Object::toString)
+                .distinct()
+                .anyMatch(cognitoGroup -> cognitoGroup.equals(subject.getValue()))
+            ))
+        .map(Role::getName)
+        .collect(Collectors.toSet());
+
+    return Mono.just(Stream.concat(groupsByUsername.stream(), groupsByGroups.stream()).collect(Collectors.toSet()));
+  }
+
+}

+ 99 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/GithubAuthorityExtractor.java

@@ -0,0 +1,99 @@
+package com.provectus.kafka.ui.service.rbac.extractor;
+
+import com.provectus.kafka.ui.model.rbac.Role;
+import com.provectus.kafka.ui.model.rbac.provider.Provider;
+import com.provectus.kafka.ui.service.rbac.AccessControlService;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
+import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+
+@Slf4j
+public class GithubAuthorityExtractor implements ProviderAuthorityExtractor {
+
+  private static final String ORGANIZATION_ATTRIBUTE_NAME = "organizations_url";
+  private static final String USERNAME_ATTRIBUTE_NAME = "login";
+  private static final String ORGANIZATION_NAME = "login";
+  private static final String GITHUB_ACCEPT_HEADER = "application/vnd.github+json";
+
+  private final WebClient webClient = WebClient.create("https://api.github.com");
+
+  @Override
+  public boolean isApplicable(String provider) {
+    return Provider.Name.GITHUB.equalsIgnoreCase(provider);
+  }
+
+  @Override
+  public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<String, Object> additionalParams) {
+    DefaultOAuth2User principal;
+    try {
+      principal = (DefaultOAuth2User) value;
+    } catch (ClassCastException e) {
+      log.error("Can't cast value to DefaultOAuth2User", e);
+      throw new RuntimeException();
+    }
+
+    Set<String> groupsByUsername = new HashSet<>();
+    String username = principal.getAttribute(USERNAME_ATTRIBUTE_NAME);
+    if (username == null) {
+      log.debug("Github username param is not present");
+    } else {
+      acs.getRoles()
+          .stream()
+          .filter(r -> r.getSubjects()
+              .stream()
+              .filter(s -> s.getProvider().equals(Provider.OAUTH_GITHUB))
+              .filter(s -> s.getType().equals("user"))
+              .anyMatch(s -> s.getValue().equals(username)))
+          .map(Role::getName)
+          .forEach(groupsByUsername::add);
+    }
+
+    String organization = principal.getAttribute(ORGANIZATION_ATTRIBUTE_NAME);
+    if (organization == null) {
+      log.debug("Github organization param is not present");
+      return Mono.just(groupsByUsername);
+    }
+
+    final Mono<List<Map<String, Object>>> userOrganizations = webClient
+        .get()
+        .uri("/user/orgs")
+        .headers(headers -> {
+          headers.set(HttpHeaders.ACCEPT, GITHUB_ACCEPT_HEADER);
+          OAuth2UserRequest request = (OAuth2UserRequest) additionalParams.get("request");
+          headers.setBearerAuth(request.getAccessToken().getTokenValue());
+        })
+        .retrieve()
+        //@formatter:off
+        .bodyToMono(new ParameterizedTypeReference<>() {});
+    //@formatter:on
+
+    return userOrganizations
+        .map(orgsMap -> {
+          var groupsByOrg = acs.getRoles()
+              .stream()
+              .filter(role -> role.getSubjects()
+                  .stream()
+                  .filter(s -> s.getProvider().equals(Provider.OAUTH_GITHUB))
+                  .filter(s -> s.getType().equals("organization"))
+                  .anyMatch(subject -> orgsMap.stream()
+                      .map(org -> org.get(ORGANIZATION_NAME).toString())
+                      .distinct()
+                      .anyMatch(orgName -> orgName.equalsIgnoreCase(subject.getValue()))
+                  ))
+              .map(Role::getName);
+
+          return Stream.concat(groupsByOrg, groupsByUsername.stream()).collect(Collectors.toSet());
+        });
+  }
+
+}

+ 69 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/GoogleAuthorityExtractor.java

@@ -0,0 +1,69 @@
+package com.provectus.kafka.ui.service.rbac.extractor;
+
+import com.provectus.kafka.ui.model.rbac.Role;
+import com.provectus.kafka.ui.model.rbac.provider.Provider;
+import com.provectus.kafka.ui.service.rbac.AccessControlService;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
+import reactor.core.publisher.Mono;
+
+@Slf4j
+public class GoogleAuthorityExtractor implements ProviderAuthorityExtractor {
+
+  private static final String GOOGLE_DOMAIN_ATTRIBUTE_NAME = "hd";
+  public static final String EMAIL_ATTRIBUTE_NAME = "email";
+
+  @Override
+  public boolean isApplicable(String provider) {
+    return Provider.Name.GOOGLE.equalsIgnoreCase(provider);
+  }
+
+  @Override
+  public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<String, Object> additionalParams) {
+    log.debug("Extracting google user authorities");
+
+    DefaultOAuth2User principal;
+    try {
+      principal = (DefaultOAuth2User) value;
+    } catch (ClassCastException e) {
+      log.error("Can't cast value to DefaultOAuth2User", e);
+      throw new RuntimeException();
+    }
+
+    Set<String> groupsByUsername = acs.getRoles()
+        .stream()
+        .filter(r -> r.getSubjects()
+            .stream()
+            .filter(s -> s.getProvider().equals(Provider.OAUTH_GOOGLE))
+            .filter(s -> s.getType().equals("user"))
+            .anyMatch(s -> s.getValue().equals(principal.getAttribute(EMAIL_ATTRIBUTE_NAME))))
+        .map(Role::getName)
+        .collect(Collectors.toSet());
+
+
+    String domain = principal.getAttribute(GOOGLE_DOMAIN_ATTRIBUTE_NAME);
+    if (domain == null) {
+      log.debug("Google domain param is not present");
+      return Mono.just(groupsByUsername);
+    }
+
+    List<String> groupsByDomain = acs.getRoles()
+        .stream()
+        .filter(r -> r.getSubjects()
+            .stream()
+            .filter(s -> s.getProvider().equals(Provider.OAUTH_GOOGLE))
+            .filter(s -> s.getType().equals("domain"))
+            .anyMatch(s -> s.getValue().equals(domain)))
+        .map(Role::getName)
+        .toList();
+
+    return Mono.just(Stream.concat(groupsByUsername.stream(), groupsByDomain.stream())
+        .collect(Collectors.toSet()));
+  }
+
+}

+ 23 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/LdapAuthorityExtractor.java

@@ -0,0 +1,23 @@
+package com.provectus.kafka.ui.service.rbac.extractor;
+
+import com.provectus.kafka.ui.service.rbac.AccessControlService;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import lombok.extern.slf4j.Slf4j;
+import reactor.core.publisher.Mono;
+
+@Slf4j
+public class LdapAuthorityExtractor implements ProviderAuthorityExtractor {
+
+  @Override
+  public boolean isApplicable(String provider) {
+    return false; // TODO #2752
+  }
+
+  @Override
+  public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<String, Object> additionalParams) {
+    return Mono.just(Collections.emptySet()); // TODO #2752
+  }
+
+}

+ 31 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/OauthAuthorityExtractor.java

@@ -0,0 +1,31 @@
+package com.provectus.kafka.ui.service.rbac.extractor;
+
+import com.provectus.kafka.ui.service.rbac.AccessControlService;
+import java.util.Map;
+import java.util.Set;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
+import reactor.core.publisher.Mono;
+
+@Slf4j
+public class OauthAuthorityExtractor implements ProviderAuthorityExtractor {
+
+  @Override
+  public boolean isApplicable(String provider) {
+    return false; // TODO #2844
+  }
+
+  @Override
+  public Mono<Set<String>> extract(AccessControlService acs, Object value, Map<String, Object> additionalParams) {
+    DefaultOAuth2User principal;
+    try {
+      principal = (DefaultOAuth2User) value;
+    } catch (ClassCastException e) {
+      log.error("Can't cast value to DefaultOAuth2User", e);
+      throw new RuntimeException();
+    }
+
+    return Mono.just(Set.of(principal.getName())); // TODO #2844
+  }
+
+}

+ 14 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/ProviderAuthorityExtractor.java

@@ -0,0 +1,14 @@
+package com.provectus.kafka.ui.service.rbac.extractor;
+
+import com.provectus.kafka.ui.service.rbac.AccessControlService;
+import java.util.Map;
+import java.util.Set;
+import reactor.core.publisher.Mono;
+
+public interface ProviderAuthorityExtractor {
+
+  boolean isApplicable(String provider);
+
+  Mono<Set<String>> extract(AccessControlService acs, Object value, Map<String, Object> additionalParams);
+
+}

+ 1 - 1
kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/annotations/KafkaClientInternalsDependant.java → kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/annotation/KafkaClientInternalsDependant.java

@@ -1,4 +1,4 @@
-package com.provectus.kafka.ui.util.annotations;
+package com.provectus.kafka.ui.util.annotation;
 
 
 /**
 /**
  * All code places that depend on kafka-client's internals or implementation-specific logic
  * All code places that depend on kafka-client's internals or implementation-specific logic

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

@@ -34,7 +34,27 @@ kafka:
 spring:
 spring:
   jmx:
   jmx:
     enabled: true
     enabled: true
+  security:
+    oauth2:
+      client:
+        registration:
+          cognito:
+            clientId: xx
+            clientSecret: yy
+            scope: openid
+            client-name: cognito
+            provider: cognito
+            redirect-uri: http://localhost:8080/login/oauth2/code/cognito
+            authorization-grant-type: authorization_code
+        provider:
+          cognito:
+            issuer-uri: https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_M7cIUn1nj
+            jwk-set-uri: https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_M7cIUn1nj/.well-known/jwks.json
+            user-name-attribute: username
 auth:
 auth:
   type: DISABLED
   type: DISABLED
+
+roles.file: /tmp/roles.yml
+
 #server:
 #server:
-#  port: 8080 #- Port in which kafka-ui will run.
+#  port: 8080 #- Port in which kafka-ui will run.

+ 38 - 32
kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/SchemaRegistryPaginationTest.java

@@ -11,10 +11,13 @@ import com.provectus.kafka.ui.mapper.ClusterMapper;
 import com.provectus.kafka.ui.model.InternalSchemaRegistry;
 import com.provectus.kafka.ui.model.InternalSchemaRegistry;
 import com.provectus.kafka.ui.model.KafkaCluster;
 import com.provectus.kafka.ui.model.KafkaCluster;
 import com.provectus.kafka.ui.model.SchemaSubjectDTO;
 import com.provectus.kafka.ui.model.SchemaSubjectDTO;
+import com.provectus.kafka.ui.service.rbac.AccessControlService;
+import com.provectus.kafka.ui.util.AccessControlServiceMock;
 import java.util.Comparator;
 import java.util.Comparator;
 import java.util.Optional;
 import java.util.Optional;
 import java.util.stream.IntStream;
 import java.util.stream.IntStream;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
 import reactor.core.publisher.Mono;
 import reactor.core.publisher.Mono;
 
 
 public class SchemaRegistryPaginationTest {
 public class SchemaRegistryPaginationTest {
@@ -24,55 +27,58 @@ public class SchemaRegistryPaginationTest {
   private final SchemaRegistryService schemaRegistryService = mock(SchemaRegistryService.class);
   private final SchemaRegistryService schemaRegistryService = mock(SchemaRegistryService.class);
   private final ClustersStorage clustersStorage = mock(ClustersStorage.class);
   private final ClustersStorage clustersStorage = mock(ClustersStorage.class);
   private final ClusterMapper clusterMapper = mock(ClusterMapper.class);
   private final ClusterMapper clusterMapper = mock(ClusterMapper.class);
+  private final AccessControlService accessControlService = new AccessControlServiceMock().getMock();
 
 
-  private final SchemasController controller = new SchemasController(clusterMapper, schemaRegistryService);
+  private final SchemasController controller
+      = new SchemasController(clusterMapper, schemaRegistryService, accessControlService);
 
 
   private void init(String[] subjects) {
   private void init(String[] subjects) {
     when(schemaRegistryService.getAllSubjectNames(isA(KafkaCluster.class)))
     when(schemaRegistryService.getAllSubjectNames(isA(KafkaCluster.class)))
-                .thenReturn(Mono.just(subjects));
+        .thenReturn(Mono.just(subjects));
     when(schemaRegistryService
     when(schemaRegistryService
-            .getAllLatestVersionSchemas(isA(KafkaCluster.class), anyList())).thenCallRealMethod();
+        .getAllLatestVersionSchemas(isA(KafkaCluster.class), anyList())).thenCallRealMethod();
     when(clustersStorage.getClusterByName(isA(String.class)))
     when(clustersStorage.getClusterByName(isA(String.class)))
-            .thenReturn(Optional.of(buildKafkaCluster(LOCAL_KAFKA_CLUSTER_NAME)));
+        .thenReturn(Optional.of(buildKafkaCluster(LOCAL_KAFKA_CLUSTER_NAME)));
     when(schemaRegistryService.getLatestSchemaVersionBySubject(isA(KafkaCluster.class), isA(String.class)))
     when(schemaRegistryService.getLatestSchemaVersionBySubject(isA(KafkaCluster.class), isA(String.class)))
-            .thenAnswer(a -> Mono.just(new SchemaSubjectDTO().subject(a.getArgument(1))));
-    this.controller.setClustersStorage(clustersStorage);
+        .thenAnswer(a -> Mono.just(new SchemaSubjectDTO().subject(a.getArgument(1))));
+
+    ReflectionTestUtils.setField(controller, "clustersStorage", clustersStorage);
   }
   }
 
 
   @Test
   @Test
   void shouldListFirst25andThen10Schemas() {
   void shouldListFirst25andThen10Schemas() {
     init(
     init(
-            IntStream.rangeClosed(1, 100)
-                    .boxed()
-                    .map(num -> "subject" + num)
-                    .toArray(String[]::new)
+        IntStream.rangeClosed(1, 100)
+            .boxed()
+            .map(num -> "subject" + num)
+            .toArray(String[]::new)
     );
     );
     var schemasFirst25 = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME,
     var schemasFirst25 = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME,
-            null, null, null, null).block();
+        null, null, null, null).block();
     assertThat(schemasFirst25.getBody().getPageCount()).isEqualTo(4);
     assertThat(schemasFirst25.getBody().getPageCount()).isEqualTo(4);
     assertThat(schemasFirst25.getBody().getSchemas()).hasSize(25);
     assertThat(schemasFirst25.getBody().getSchemas()).hasSize(25);
     assertThat(schemasFirst25.getBody().getSchemas())
     assertThat(schemasFirst25.getBody().getSchemas())
-            .isSortedAccordingTo(Comparator.comparing(SchemaSubjectDTO::getSubject));
+        .isSortedAccordingTo(Comparator.comparing(SchemaSubjectDTO::getSubject));
 
 
     var schemasFirst10 = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME,
     var schemasFirst10 = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME,
-            null, 10, null, null).block();
+        null, 10, null, null).block();
 
 
     assertThat(schemasFirst10.getBody().getPageCount()).isEqualTo(10);
     assertThat(schemasFirst10.getBody().getPageCount()).isEqualTo(10);
     assertThat(schemasFirst10.getBody().getSchemas()).hasSize(10);
     assertThat(schemasFirst10.getBody().getSchemas()).hasSize(10);
     assertThat(schemasFirst10.getBody().getSchemas())
     assertThat(schemasFirst10.getBody().getSchemas())
-            .isSortedAccordingTo(Comparator.comparing(SchemaSubjectDTO::getSubject));
+        .isSortedAccordingTo(Comparator.comparing(SchemaSubjectDTO::getSubject));
   }
   }
 
 
   @Test
   @Test
   void shouldListSchemasContaining_1() {
   void shouldListSchemasContaining_1() {
     init(
     init(
-              IntStream.rangeClosed(1, 100)
-                      .boxed()
-                      .map(num -> "subject" + num)
-                      .toArray(String[]::new)
+        IntStream.rangeClosed(1, 100)
+            .boxed()
+            .map(num -> "subject" + num)
+            .toArray(String[]::new)
     );
     );
     var schemasSearch7 = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME,
     var schemasSearch7 = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME,
-            null, null, "1", null).block();
+        null, null, "1", null).block();
     assertThat(schemasSearch7.getBody().getPageCount()).isEqualTo(1);
     assertThat(schemasSearch7.getBody().getPageCount()).isEqualTo(1);
     assertThat(schemasSearch7.getBody().getSchemas()).hasSize(20);
     assertThat(schemasSearch7.getBody().getSchemas()).hasSize(20);
   }
   }
@@ -80,13 +86,13 @@ public class SchemaRegistryPaginationTest {
   @Test
   @Test
   void shouldCorrectlyHandleNonPositivePageNumberAndPageSize() {
   void shouldCorrectlyHandleNonPositivePageNumberAndPageSize() {
     init(
     init(
-                IntStream.rangeClosed(1, 100)
-                        .boxed()
-                        .map(num -> "subject" + num)
-                        .toArray(String[]::new)
+        IntStream.rangeClosed(1, 100)
+            .boxed()
+            .map(num -> "subject" + num)
+            .toArray(String[]::new)
     );
     );
     var schemas = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME,
     var schemas = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME,
-            0, -1, null, null).block();
+        0, -1, null, null).block();
 
 
     assertThat(schemas.getBody().getPageCount()).isEqualTo(4);
     assertThat(schemas.getBody().getPageCount()).isEqualTo(4);
     assertThat(schemas.getBody().getSchemas()).hasSize(25);
     assertThat(schemas.getBody().getSchemas()).hasSize(25);
@@ -96,14 +102,14 @@ public class SchemaRegistryPaginationTest {
   @Test
   @Test
   void shouldCalculateCorrectPageCountForNonDivisiblePageSize() {
   void shouldCalculateCorrectPageCountForNonDivisiblePageSize() {
     init(
     init(
-                IntStream.rangeClosed(1, 100)
-                        .boxed()
-                        .map(num -> "subject" + num)
-                        .toArray(String[]::new)
+        IntStream.rangeClosed(1, 100)
+            .boxed()
+            .map(num -> "subject" + num)
+            .toArray(String[]::new)
     );
     );
 
 
     var schemas = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME,
     var schemas = controller.getSchemas(LOCAL_KAFKA_CLUSTER_NAME,
-            4, 33, null, null).block();
+        4, 33, null, null).block();
 
 
     assertThat(schemas.getBody().getPageCount()).isEqualTo(4);
     assertThat(schemas.getBody().getPageCount()).isEqualTo(4);
     assertThat(schemas.getBody().getSchemas()).hasSize(1);
     assertThat(schemas.getBody().getSchemas()).hasSize(1);
@@ -112,8 +118,8 @@ public class SchemaRegistryPaginationTest {
 
 
   private KafkaCluster buildKafkaCluster(String clusterName) {
   private KafkaCluster buildKafkaCluster(String clusterName) {
     return KafkaCluster.builder()
     return KafkaCluster.builder()
-            .name(clusterName)
-            .schemaRegistry(InternalSchemaRegistry.builder().build())
-            .build();
+        .name(clusterName)
+        .schemaRegistry(InternalSchemaRegistry.builder().build())
+        .build();
   }
   }
 }
 }

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

@@ -19,6 +19,8 @@ import com.provectus.kafka.ui.model.SortOrderDTO;
 import com.provectus.kafka.ui.model.TopicColumnsToSortDTO;
 import com.provectus.kafka.ui.model.TopicColumnsToSortDTO;
 import com.provectus.kafka.ui.model.TopicDTO;
 import com.provectus.kafka.ui.model.TopicDTO;
 import com.provectus.kafka.ui.service.analyze.TopicAnalysisService;
 import com.provectus.kafka.ui.service.analyze.TopicAnalysisService;
+import com.provectus.kafka.ui.service.rbac.AccessControlService;
+import com.provectus.kafka.ui.util.AccessControlServiceMock;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.Comparator;
 import java.util.List;
 import java.util.List;
@@ -32,6 +34,7 @@ import java.util.stream.IntStream;
 import org.apache.kafka.clients.admin.TopicDescription;
 import org.apache.kafka.clients.admin.TopicDescription;
 import org.apache.kafka.common.TopicPartitionInfo;
 import org.apache.kafka.common.TopicPartitionInfo;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
 import reactor.core.publisher.Mono;
 import reactor.core.publisher.Mono;
 
 
 class TopicsServicePaginationTest {
 class TopicsServicePaginationTest {
@@ -41,9 +44,10 @@ class TopicsServicePaginationTest {
   private final TopicsService topicsService = mock(TopicsService.class);
   private final TopicsService topicsService = mock(TopicsService.class);
   private final ClustersStorage clustersStorage = mock(ClustersStorage.class);
   private final ClustersStorage clustersStorage = mock(ClustersStorage.class);
   private final ClusterMapper clusterMapper = new ClusterMapperImpl();
   private final ClusterMapper clusterMapper = new ClusterMapperImpl();
+  private final AccessControlService accessControlService = new AccessControlServiceMock().getMock();
 
 
   private final TopicsController topicsController = new TopicsController(
   private final TopicsController topicsController = new TopicsController(
-      topicsService, mock(TopicAnalysisService.class), clusterMapper);
+      topicsService, mock(TopicAnalysisService.class), clusterMapper, accessControlService);
 
 
   private void init(Map<String, InternalTopic> topicsInCache) {
   private void init(Map<String, InternalTopic> topicsInCache) {
 
 
@@ -56,7 +60,7 @@ class TopicsServicePaginationTest {
           List<String> lst = a.getArgument(1);
           List<String> lst = a.getArgument(1);
           return Mono.just(lst.stream().map(topicsInCache::get).collect(Collectors.toList()));
           return Mono.just(lst.stream().map(topicsInCache::get).collect(Collectors.toList()));
         });
         });
-    this.topicsController.setClustersStorage(clustersStorage);
+    ReflectionTestUtils.setField(topicsController, "clustersStorage", clustersStorage);
   }
   }
 
 
   @Test
   @Test

+ 23 - 0
kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/AccessControlServiceMock.java

@@ -0,0 +1,23 @@
+package com.provectus.kafka.ui.util;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+
+import com.provectus.kafka.ui.service.rbac.AccessControlService;
+import org.mockito.Mockito;
+import reactor.core.publisher.Mono;
+
+public class AccessControlServiceMock {
+
+  public AccessControlService getMock() {
+    AccessControlService mock = Mockito.mock(AccessControlService.class);
+
+    when(mock.validateAccess(any())).thenReturn(Mono.empty());
+    when(mock.isSchemaAccessible(anyString(), anyString())).thenReturn(Mono.just(true));
+
+    when(mock.isTopicAccessible(any(), anyString())).thenReturn(Mono.just(true));
+
+    return mock;
+  }
+}

+ 77 - 20
kafka-ui-contract/src/main/resources/swagger/kafka-ui-api.yaml

@@ -7,7 +7,7 @@ info:
   contact: { }
   contact: { }
   license:
   license:
     name: Apache 2.0
     name: Apache 2.0
-    url: http://www.apache.org/licenses/LICENSE-2.0
+    url: https://www.apache.org/licenses/LICENSE-2.0
 tags:
 tags:
   - name: /api/clusters
   - name: /api/clusters
   - name: /api/clusters/connects
   - name: /api/clusters/connects
@@ -1729,33 +1729,20 @@ paths:
                 $ref: '#/components/schemas/PartitionsIncreaseResponse'
                 $ref: '#/components/schemas/PartitionsIncreaseResponse'
         404:
         404:
           description: Not found
           description: Not found
-  /api/info/timestampformat:
-    get:
-      tags:
-        - TimeStampFormat
-      summary: get system default datetime format
-      operationId: getTimeStampFormat
-      responses:
-        200:
-          description: OK
-          content:
-            application/json:
-              schema:
-                $ref: '#/components/schemas/TimeStampFormat'
 
 
-  /api/info/timestampformat/iso:
+  /api/authorization:
     get:
     get:
       tags:
       tags:
-        - TimeStampFormat
-      summary: get system default datetime format (in ISO format, for JS)
-      operationId: getTimeStampFormatISO
+        - Authorization
+      summary: Get user authentication related info
+      operationId: getUserAuthInfo
       responses:
       responses:
         200:
         200:
           description: OK
           description: OK
           content:
           content:
             application/json:
             application/json:
               schema:
               schema:
-                $ref: '#/components/schemas/TimeStampFormat'
+                $ref: '#/components/schemas/AuthenticationInfo'
 
 
 components:
 components:
   schemas:
   schemas:
@@ -2646,7 +2633,7 @@ components:
           type: string
           type: string
         schemaType:
         schemaType:
           $ref: '#/components/schemas/SchemaType'
           $ref: '#/components/schemas/SchemaType'
-          description: upon updating a schema, the type of an existing schema can't be changed
+          # upon updating a schema, the type of existing schema can't be changed
       required:
       required:
         - subject
         - subject
         - schema
         - schema
@@ -3154,3 +3141,73 @@ components:
         - COMPACT
         - COMPACT
         - COMPACT_DELETE
         - COMPACT_DELETE
         - UNKNOWN
         - UNKNOWN
+
+    AuthenticationInfo:
+      type: object
+      properties:
+        rbacEnabled:
+          type: boolean
+          description: true if role based access control is enabled and granular permission access is required
+        userInfo:
+          $ref: '#/components/schemas/UserInfo'
+      required:
+        - rbacEnabled
+
+    UserInfo:
+      type: object
+      properties:
+        username:
+          type: string
+        permissions:
+          type: array
+          items:
+            $ref: '#/components/schemas/UserPermission'
+      required:
+        - username
+        - permissions
+
+    UserPermission:
+      type: object
+      properties:
+        clusters:
+          type: array
+          items:
+            type: string
+        resource:
+          $ref: '#/components/schemas/ResourceType'
+        value:
+          type: string
+        actions:
+          type: array
+          items:
+            $ref: '#/components/schemas/Action'
+      required:
+        - clusters
+        - resource
+        - actions
+
+    Action:
+      type: string
+      enum:
+        - VIEW
+        - EDIT
+        - CREATE
+        - DELETE
+        - RESET_OFFSETS
+        - EXECUTE
+        - MODIFY_GLOBAL_COMPATIBILITY
+        - ANALYSIS_VIEW
+        - ANALYSIS_RUN
+        - MESSAGES_READ
+        - MESSAGES_PRODUCE
+        - MESSAGES_DELETE
+
+    ResourceType:
+      type: string
+      enum:
+        - CLUSTERCONFIG
+        - TOPIC
+        - CONSUMER
+        - SCHEMA
+        - CONNECT
+        - KSQL

+ 4 - 1
kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/BasePage.java

@@ -1,8 +1,10 @@
 package com.provectus.kafka.ui.pages;
 package com.provectus.kafka.ui.pages;
 
 
+import static com.codeborne.selenide.Selenide.$$x;
 import static com.codeborne.selenide.Selenide.$x;
 import static com.codeborne.selenide.Selenide.$x;
 
 
 import com.codeborne.selenide.Condition;
 import com.codeborne.selenide.Condition;
+import com.codeborne.selenide.ElementsCollection;
 import com.codeborne.selenide.SelenideElement;
 import com.codeborne.selenide.SelenideElement;
 import com.provectus.kafka.ui.utilities.WebUtils;
 import com.provectus.kafka.ui.utilities.WebUtils;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
@@ -16,9 +18,10 @@ public abstract class BasePage extends WebUtils {
   protected SelenideElement dotMenuBtn = $x("//button[@aria-label='Dropdown Toggle']");
   protected SelenideElement dotMenuBtn = $x("//button[@aria-label='Dropdown Toggle']");
   protected SelenideElement alertHeader = $x("//div[@role='alert']//div[@role='heading']");
   protected SelenideElement alertHeader = $x("//div[@role='alert']//div[@role='heading']");
   protected SelenideElement alertMessage = $x("//div[@role='alert']//div[@role='contentinfo']");
   protected SelenideElement alertMessage = $x("//div[@role='alert']//div[@role='contentinfo']");
+  protected ElementsCollection allGridItems = $$x("//tr[@class]");
   protected String summaryCellLocator = "//div[contains(text(),'%s')]";
   protected String summaryCellLocator = "//div[contains(text(),'%s')]";
   protected String tableElementNameLocator = "//tbody//a[contains(text(),'%s')]";
   protected String tableElementNameLocator = "//tbody//a[contains(text(),'%s')]";
-  protected String columnHeaderLocator = "//table//tr/th/div[text()='%s']";
+  protected String columnHeaderLocator = "//table//tr/th//div[text()='%s']";
 
 
   protected void waitUntilSpinnerDisappear() {
   protected void waitUntilSpinnerDisappear() {
     log.debug("\nwaitUntilSpinnerDisappear");
     log.debug("\nwaitUntilSpinnerDisappear");

+ 2 - 2
kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/TopPanel.java

@@ -14,10 +14,10 @@ public class TopPanel extends BasePage{
     protected SelenideElement discordBtn = $x("//a[contains(@href,'https://discord.com/invite')]");
     protected SelenideElement discordBtn = $x("//a[contains(@href,'https://discord.com/invite')]");
 
 
     public List<SelenideElement> getAllVisibleElements() {
     public List<SelenideElement> getAllVisibleElements() {
-        return Arrays.asList(kafkaLogo, kafkaVersion, logOutBtn, gitBtn, discordBtn);
+        return Arrays.asList(kafkaLogo, kafkaVersion, gitBtn, discordBtn);
     }
     }
 
 
     public List<SelenideElement> getAllEnabledElements() {
     public List<SelenideElement> getAllEnabledElements() {
-        return Arrays.asList(logOutBtn, gitBtn, discordBtn, kafkaLogo);
+        return Arrays.asList(gitBtn, discordBtn, kafkaLogo);
     }
     }
 }
 }

+ 40 - 0
kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersConfigTab.java

@@ -0,0 +1,40 @@
+package com.provectus.kafka.ui.pages.brokers;
+
+import static com.codeborne.selenide.Selenide.$$x;
+import static com.codeborne.selenide.Selenide.$x;
+
+import com.codeborne.selenide.Condition;
+import com.codeborne.selenide.SelenideElement;
+import com.provectus.kafka.ui.pages.BasePage;
+import io.qameta.allure.Step;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class BrokersConfigTab extends BasePage {
+
+  protected List<SelenideElement> editBtn = $$x("//button[@aria-label='editAction']");
+  protected SelenideElement searchByKeyField = $x("//input[@placeholder='Search by Key']");
+
+  @Step
+  public BrokersConfigTab waitUntilScreenReady(){
+    waitUntilSpinnerDisappear();
+    searchByKeyField.shouldBe(Condition.visible);
+    return this;
+  }
+
+  @Step
+  public boolean isSearchByKeyVisible() {
+   return isVisible(searchByKeyField);
+  }
+
+  public List<SelenideElement> getColumnHeaders() {
+    return Stream.of("Key", "Value", "Source")
+        .map(name -> $x(String.format(columnHeaderLocator, name)))
+        .collect(Collectors.toList());
+  }
+
+  public List<SelenideElement> getEditButtons() {
+    return editBtn;
+  }
+}

+ 36 - 1
kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersDetails.java

@@ -1,5 +1,6 @@
 package com.provectus.kafka.ui.pages.brokers;
 package com.provectus.kafka.ui.pages.brokers;
 
 
+import static com.codeborne.selenide.Selenide.$;
 import static com.codeborne.selenide.Selenide.$x;
 import static com.codeborne.selenide.Selenide.$x;
 
 
 import com.codeborne.selenide.Condition;
 import com.codeborne.selenide.Condition;
@@ -11,11 +12,13 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.List;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import java.util.stream.Stream;
+import org.openqa.selenium.By;
 
 
 public class BrokersDetails extends BasePage {
 public class BrokersDetails extends BasePage {
 
 
   protected SelenideElement logDirectoriesTab = $x("//a[text()='Log directories']");
   protected SelenideElement logDirectoriesTab = $x("//a[text()='Log directories']");
   protected SelenideElement metricsTab = $x("//a[text()='Metrics']");
   protected SelenideElement metricsTab = $x("//a[text()='Metrics']");
+  protected String brokersTabLocator = "//a[text()='%s']";
 
 
   @Step
   @Step
   public BrokersDetails waitUntilScreenReady() {
   public BrokersDetails waitUntilScreenReady() {
@@ -24,6 +27,13 @@ public class BrokersDetails extends BasePage {
     return this;
     return this;
   }
   }
 
 
+  @Step
+  public BrokersDetails openDetailsTab(DetailsTab menu) {
+    $(By.linkText(menu.toString())).shouldBe(Condition.enabled).click();
+    waitUntilSpinnerDisappear();
+    return this;
+  }
+
   private List<SelenideElement> getVisibleColumnHeaders() {
   private List<SelenideElement> getVisibleColumnHeaders() {
     return Stream.of("Name", "Topics", "Error", "Partitions")
     return Stream.of("Name", "Topics", "Error", "Partitions")
         .map(name -> $x(String.format(columnHeaderLocator, name)))
         .map(name -> $x(String.format(columnHeaderLocator, name)))
@@ -42,15 +52,40 @@ public class BrokersDetails extends BasePage {
         .collect(Collectors.toList());
         .collect(Collectors.toList());
   }
   }
 
 
+  private List<SelenideElement> getDetailsTabs() {
+    return Stream.of(DetailsTab.values())
+        .map(name -> $x(String.format(brokersTabLocator, name)))
+        .collect(Collectors.toList());
+  }
+
   @Step
   @Step
   public List<SelenideElement> getAllEnabledElements() {
   public List<SelenideElement> getAllEnabledElements() {
-    return getEnabledColumnHeaders();
+    List<SelenideElement> enabledElements = new ArrayList<>(getEnabledColumnHeaders());
+    enabledElements.addAll(getDetailsTabs());
+    return enabledElements;
   }
   }
 
 
   @Step
   @Step
   public List<SelenideElement> getAllVisibleElements() {
   public List<SelenideElement> getAllVisibleElements() {
     List<SelenideElement> visibleElements = new ArrayList<>(getVisibleSummaryCells());
     List<SelenideElement> visibleElements = new ArrayList<>(getVisibleSummaryCells());
     visibleElements.addAll(getVisibleColumnHeaders());
     visibleElements.addAll(getVisibleColumnHeaders());
+    visibleElements.addAll(getDetailsTabs());
     return visibleElements;
     return visibleElements;
   }
   }
+
+  public enum DetailsTab {
+    LOG_DIRECTORIES("Log directories"),
+    CONFIGS("Configs"),
+    METRICS("Metrics");
+
+    private final String value;
+
+    DetailsTab(String value) {
+      this.value = value;
+    }
+
+    public String toString() {
+      return value;
+    }
+  }
 }
 }

+ 97 - 6
kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/brokers/BrokersList.java

@@ -2,10 +2,15 @@ package com.provectus.kafka.ui.pages.brokers;
 
 
 import static com.codeborne.selenide.Selenide.$x;
 import static com.codeborne.selenide.Selenide.$x;
 
 
+import com.codeborne.selenide.CollectionCondition;
 import com.codeborne.selenide.Condition;
 import com.codeborne.selenide.Condition;
 import com.codeborne.selenide.SelenideElement;
 import com.codeborne.selenide.SelenideElement;
 import com.provectus.kafka.ui.pages.BasePage;
 import com.provectus.kafka.ui.pages.BasePage;
 import io.qameta.allure.Step;
 import io.qameta.allure.Step;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 
 public class BrokersList extends BasePage {
 public class BrokersList extends BasePage {
 
 
@@ -19,14 +24,100 @@ public class BrokersList extends BasePage {
   }
   }
 
 
   @Step
   @Step
-  public boolean isBrokerVisible(String brokerId) {
-    tableGrid.shouldBe(Condition.visible);
-    return isVisible(getTableElement(brokerId));
+  public BrokersList openBroker(int brokerId) {
+    getBrokerItem(brokerId).openItem();
+    return this;
+  }
+
+  private List<SelenideElement> getUptimeSummaryCells() {
+    return Stream.of("Broker Count", "Active Controller", "Version")
+        .map(name -> $x(String.format(summaryCellLocator, name)))
+        .collect(Collectors.toList());
+  }
+
+  private List<SelenideElement> getPartitionsSummaryCells() {
+    return Stream.of("Online", "URP", "In Sync Replicas", "Out Of Sync Replicas")
+        .map(name -> $x(String.format(summaryCellLocator, name)))
+        .collect(Collectors.toList());
   }
   }
 
 
   @Step
   @Step
-  public BrokersList openBroker(String brokerName) {
-    getTableElement(brokerName).shouldBe(Condition.enabled).click();
-    return this;
+  public List<SelenideElement> getAllVisibleElements() {
+    List<SelenideElement> visibleElements = new ArrayList<>(getUptimeSummaryCells());
+    visibleElements.addAll(getPartitionsSummaryCells());
+    return visibleElements;
+  }
+
+  private List<SelenideElement> getEnabledColumnHeaders() {
+    return Stream.of("Broker ID", "Segment Size", "Segment Count", "Port", "Host")
+        .map(name -> $x(String.format(columnHeaderLocator, name)))
+        .collect(Collectors.toList());
+  }
+
+  @Step
+  public List<SelenideElement> getAllEnabledElements() {
+    return getEnabledColumnHeaders();
+  }
+
+  private List<BrokersList.BrokerGridItem> initGridItems() {
+    List<BrokersList.BrokerGridItem> gridItemList = new ArrayList<>();
+    allGridItems.shouldHave(CollectionCondition.sizeGreaterThan(0))
+        .forEach(item -> gridItemList.add(new BrokersList.BrokerGridItem(item)));
+    return gridItemList;
+  }
+
+  @Step
+  public BrokerGridItem getBrokerItem(int id){
+    return initGridItems().stream()
+        .filter(e ->e.getId() == id)
+        .findFirst().orElse(null);
+  }
+
+  @Step
+  public List<BrokerGridItem> getAllBrokers(){
+    return initGridItems();
+  }
+
+  public static class BrokerGridItem extends BasePage {
+
+    private final SelenideElement element;
+
+    public BrokerGridItem(SelenideElement element) {
+      this.element = element;
+    }
+
+    private SelenideElement getIdElm() {
+      return element.$x("./td[1]/div/a");
+    }
+
+    @Step
+    public int getId() {
+      return Integer.parseInt(getIdElm().getText().trim());
+    }
+
+    @Step
+    public void openItem() {
+      getIdElm().click();
+    }
+
+    @Step
+    public int getSegmentSize(){
+      return Integer.parseInt(element.$x("./td[2]").getText().trim());
+    }
+
+    @Step
+    public int getSegmentCount(){
+      return Integer.parseInt(element.$x("./td[3]").getText().trim());
+    }
+
+    @Step
+    public int getPort(){
+      return Integer.parseInt(element.$x("./td[4]").getText().trim());
+    }
+
+    @Step
+    public String getHost(){
+      return element.$x("./td[5]").getText().trim();
+    }
   }
   }
 }
 }

+ 18 - 4
kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/schema/SchemaCreateForm.java

@@ -11,6 +11,8 @@ import com.provectus.kafka.ui.api.model.SchemaType;
 import com.provectus.kafka.ui.pages.BasePage;
 import com.provectus.kafka.ui.pages.BasePage;
 import io.qameta.allure.Step;
 import io.qameta.allure.Step;
 import java.util.List;
 import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 
 public class SchemaCreateForm extends BasePage {
 public class SchemaCreateForm extends BasePage {
 
 
@@ -21,6 +23,7 @@ public class SchemaCreateForm extends BasePage {
     protected SelenideElement schemaTypeDdl = $x("//ul[@name='schemaType']");
     protected SelenideElement schemaTypeDdl = $x("//ul[@name='schemaType']");
     protected SelenideElement compatibilityLevelList = $x("//ul[@name='compatibilityLevel']");
     protected SelenideElement compatibilityLevelList = $x("//ul[@name='compatibilityLevel']");
     protected SelenideElement newSchemaTextArea = $x("//div[@id='newSchema']");
     protected SelenideElement newSchemaTextArea = $x("//div[@id='newSchema']");
+    protected SelenideElement latestSchemaTextArea = $x("//div[@id='latestSchema']");
     protected SelenideElement schemaVersionDdl = $$x("//ul[@role='listbox']/li[text()='Version 2']").first();
     protected SelenideElement schemaVersionDdl = $$x("//ul[@role='listbox']/li[text()='Version 2']").first();
     protected List<SelenideElement> visibleMarkers = $$x("//div[@class='ace_scroller']//div[contains(@class,'codeMarker')]");
     protected List<SelenideElement> visibleMarkers = $$x("//div[@class='ace_scroller']//div[contains(@class,'codeMarker')]");
     protected List<SelenideElement> elementsCompareVersionDdl = $$x("//ul[@role='listbox']/ul/li");
     protected List<SelenideElement> elementsCompareVersionDdl = $$x("//ul[@role='listbox']/ul/li");
@@ -96,14 +99,25 @@ public class SchemaCreateForm extends BasePage {
     }
     }
 
 
     @Step
     @Step
-    public boolean isSchemaDropDownDisabled(){
-        boolean disabled = false;
+    public List<SelenideElement> getAllDetailsPageElements() {
+      return Stream.of(compatibilityLevelList, newSchemaTextArea, latestSchemaTextArea, submitBtn, schemaTypeDdl)
+          .collect(Collectors.toList());
+    }
+
+    @Step
+    public boolean isSubmitBtnEnabled(){
+      return isEnabled(submitBtn);
+    }
+
+    @Step
+    public boolean isSchemaDropDownEnabled(){
+        boolean enabled = true;
         try{
         try{
             String attribute = schemaTypeDdl.getAttribute("disabled");
             String attribute = schemaTypeDdl.getAttribute("disabled");
-            disabled = true;
+            enabled = false;
         }
         }
         catch (Throwable ignored){
         catch (Throwable ignored){
         }
         }
-        return disabled;
+        return enabled;
     }
     }
 }
 }

+ 103 - 12
kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicDetails.java

@@ -19,17 +19,21 @@ import org.openqa.selenium.By;
 public class TopicDetails extends BasePage {
 public class TopicDetails extends BasePage {
 
 
   protected SelenideElement clearMessagesBtn = $x(("//div[contains(text(), 'Clear messages')]"));
   protected SelenideElement clearMessagesBtn = $x(("//div[contains(text(), 'Clear messages')]"));
+  protected SelenideElement recreateTopicBtn = $x("//div[text()='Recreate Topic']");
   protected SelenideElement messageAmountCell = $x("//tbody/tr/td[5]");
   protected SelenideElement messageAmountCell = $x("//tbody/tr/td[5]");
   protected SelenideElement overviewTab = $x("//a[contains(text(),'Overview')]");
   protected SelenideElement overviewTab = $x("//a[contains(text(),'Overview')]");
   protected SelenideElement messagesTab = $x("//a[contains(text(),'Messages')]");
   protected SelenideElement messagesTab = $x("//a[contains(text(),'Messages')]");
+  protected SelenideElement seekTypeDdl = $x("//ul[@id='selectSeekType']/li");
+  protected SelenideElement seekTypeField = $x("//label[text()='Seek Type']//..//input");
   protected SelenideElement addFiltersBtn = $x("//button[text()='Add Filters']");
   protected SelenideElement addFiltersBtn = $x("//button[text()='Add Filters']");
-  protected SelenideElement savedFiltersField = $x("//div[text()='Saved Filters']");
+  protected SelenideElement savedFiltersLink = $x("//div[text()='Saved Filters']");
   protected SelenideElement addFilterCodeModalTitle = $x("//label[text()='Filter code']");
   protected SelenideElement addFilterCodeModalTitle = $x("//label[text()='Filter code']");
   protected SelenideElement addFilterCodeInput = $x("//div[@id='ace-editor']//textarea");
   protected SelenideElement addFilterCodeInput = $x("//div[@id='ace-editor']//textarea");
   protected SelenideElement saveThisFilterCheckBoxAddFilterMdl = $x("//input[@name='saveFilter']");
   protected SelenideElement saveThisFilterCheckBoxAddFilterMdl = $x("//input[@name='saveFilter']");
   protected SelenideElement displayNameInputAddFilterMdl = $x("//input[@placeholder='Enter Name']");
   protected SelenideElement displayNameInputAddFilterMdl = $x("//input[@placeholder='Enter Name']");
   protected SelenideElement cancelBtnAddFilterMdl = $x("//button[text()='Cancel']");
   protected SelenideElement cancelBtnAddFilterMdl = $x("//button[text()='Cancel']");
   protected SelenideElement addFilterBtnAddFilterMdl = $x("//button[text()='Add filter']");
   protected SelenideElement addFilterBtnAddFilterMdl = $x("//button[text()='Add filter']");
+  protected SelenideElement selectFilterBtnAddFilterMdl = $x("//button[text()='Select filter']");
   protected SelenideElement editSettingsMenu = $x("//li[@role][contains(text(),'Edit settings')]");
   protected SelenideElement editSettingsMenu = $x("//li[@role][contains(text(),'Edit settings')]");
   protected SelenideElement removeTopicBtn = $x("//ul[@role='menu']//div[contains(text(),'Remove Topic')]");
   protected SelenideElement removeTopicBtn = $x("//ul[@role='menu']//div[contains(text(),'Remove Topic')]");
   protected SelenideElement confirmBtn = $x("//div[@role='dialog']//button[contains(text(),'Confirm')]");
   protected SelenideElement confirmBtn = $x("//div[@role='dialog']//button[contains(text(),'Confirm')]");
@@ -37,10 +41,15 @@ public class TopicDetails extends BasePage {
   protected SelenideElement contentMessageTab = $x("//html//div[@id='root']/div/main//table//p");
   protected SelenideElement contentMessageTab = $x("//html//div[@id='root']/div/main//table//p");
   protected SelenideElement cleanUpPolicyField = $x("//div[contains(text(),'Clean Up Policy')]/../span/*");
   protected SelenideElement cleanUpPolicyField = $x("//div[contains(text(),'Clean Up Policy')]/../span/*");
   protected SelenideElement partitionsField = $x("//div[contains(text(),'Partitions')]/../span");
   protected SelenideElement partitionsField = $x("//div[contains(text(),'Partitions')]/../span");
+  protected SelenideElement backToCreateFiltersLink = $x("//div[text()='Back To create filters']");
+  protected SelenideElement confirmationMdl = $x("//div[text()= 'Confirm the action']/..");
   protected ElementsCollection messageGridItems = $$x("//tbody//tr");
   protected ElementsCollection messageGridItems = $$x("//tbody//tr");
+  protected String seekFilterDdlLocator = "//ul[@id='selectSeekType']/ul/li[text()='%s']";
+  protected String savedFilterNameLocator = "//div[@role='savedFilter']/div[contains(text(),'%s')]";
   protected String consumerIdLocator = "//a[@title='%s']";
   protected String consumerIdLocator = "//a[@title='%s']";
   protected String topicHeaderLocator = "//h1[contains(text(),'%s')]";
   protected String topicHeaderLocator = "//h1[contains(text(),'%s')]";
-  protected String filterNameLocator = "//*[@data-testid='activeSmartFilter']";
+  protected String activeFilterNameLocator = "//div[@data-testid='activeSmartFilter'][contains(text(),'%s')]";
+  protected String settingsGridValueLocator = "//tbody/tr/td/span[text()='%s']//ancestor::tr/td[2]/span";
 
 
   @Step
   @Step
   public TopicDetails waitUntilScreenReady() {
   public TopicDetails waitUntilScreenReady() {
@@ -56,6 +65,11 @@ public class TopicDetails extends BasePage {
     return this;
     return this;
   }
   }
 
 
+  @Step
+  public String getSettingsGridValueByKey(String key){
+    return $x(String.format(settingsGridValueLocator, key)).scrollTo().shouldBe(Condition.visible).getText();
+  }
+
   @Step
   @Step
   public TopicDetails openDotMenu() {
   public TopicDetails openDotMenu() {
     clickByJavaScript(dotMenuBtn);
     clickByJavaScript(dotMenuBtn);
@@ -73,12 +87,23 @@ public class TopicDetails extends BasePage {
     return this;
     return this;
   }
   }
 
 
+  @Step
+  public boolean isConfirmationMdlVisible(){
+    return isVisible(confirmationMdl);
+  }
+
   @Step
   @Step
   public TopicDetails clickClearMessagesMenu() {
   public TopicDetails clickClearMessagesMenu() {
     clearMessagesBtn.shouldBe(Condition.visible).click();
     clearMessagesBtn.shouldBe(Condition.visible).click();
     return this;
     return this;
   }
   }
 
 
+  @Step
+  public TopicDetails clickRecreateTopicMenu(){
+    recreateTopicBtn.shouldBe(Condition.visible).click();
+    return this;
+  }
+
   @Step
   @Step
   public String getCleanUpPolicy() {
   public String getCleanUpPolicy() {
     return cleanUpPolicyField.getText();
     return cleanUpPolicyField.getText();
@@ -101,7 +126,7 @@ public class TopicDetails extends BasePage {
   }
   }
 
 
   @Step
   @Step
-  public TopicDetails clickConfirmDeleteBtn() {
+  public TopicDetails clickConfirmBtnMdl() {
     confirmBtn.shouldBe(Condition.enabled).click();
     confirmBtn.shouldBe(Condition.enabled).click();
     confirmBtn.shouldBe(Condition.disappear);
     confirmBtn.shouldBe(Condition.disappear);
     return this;
     return this;
@@ -113,6 +138,26 @@ public class TopicDetails extends BasePage {
     return this;
     return this;
   }
   }
 
 
+  @Step
+  public TopicDetails selectSeekTypeDdlMessagesTab(String seekTypeName){
+    seekTypeDdl.shouldBe(Condition.enabled).click();
+    $x(String.format(seekFilterDdlLocator, seekTypeName)).shouldBe(Condition.visible).click();
+    return this;
+  }
+
+  @Step
+  public TopicDetails setSeekTypeValueFldMessagesTab(String seekTypeValue){
+    seekTypeField.shouldBe(Condition.enabled).sendKeys(seekTypeValue);
+    return this;
+  }
+
+  @Step
+  public TopicDetails clickSubmitFiltersBtnMessagesTab(){
+    clickByJavaScript(submitBtn);
+    waitUntilSpinnerDisappear();
+    return this;
+  }
+
   @Step
   @Step
   public TopicDetails clickMessagesAddFiltersBtn() {
   public TopicDetails clickMessagesAddFiltersBtn() {
     addFiltersBtn.shouldBe(Condition.enabled).click();
     addFiltersBtn.shouldBe(Condition.enabled).click();
@@ -120,15 +165,33 @@ public class TopicDetails extends BasePage {
   }
   }
 
 
   @Step
   @Step
-  public TopicDetails waitUntilAddFiltersMdlVisible() {
-    addFilterCodeModalTitle.shouldBe(Condition.visible);
+  public TopicDetails openSavedFiltersListMdl(){
+    savedFiltersLink.shouldBe(Condition.enabled).click();
+    backToCreateFiltersLink.shouldBe(Condition.visible);
     return this;
     return this;
   }
   }
 
 
   @Step
   @Step
-  public TopicDetails clickAddFilterBtnAddFilterMdl() {
-    addFilterBtnAddFilterMdl.shouldBe(Condition.enabled).click();
-    addFilterCodeModalTitle.shouldBe(Condition.hidden);
+  public boolean isFilterVisibleAtSavedFiltersMdl(String filterName){
+    return isVisible($x(String.format(savedFilterNameLocator,filterName)));
+  }
+
+  @Step
+  public TopicDetails selectFilterAtSavedFiltersMdl(String filterName){
+    $x(String.format(savedFilterNameLocator, filterName)).shouldBe(Condition.enabled).click();
+    return this;
+  }
+
+  @Step
+  public TopicDetails clickSelectFilterBtnAtSavedFiltersMdl(){
+    selectFilterBtnAddFilterMdl.shouldBe(Condition.enabled).click();
+    addFilterCodeModalTitle.shouldBe(Condition.disappear);
+    return this;
+  }
+
+  @Step
+  public TopicDetails waitUntilAddFiltersMdlVisible() {
+    addFilterCodeModalTitle.shouldBe(Condition.visible);
     return this;
     return this;
   }
   }
 
 
@@ -138,23 +201,46 @@ public class TopicDetails extends BasePage {
     return this;
     return this;
   }
   }
 
 
+  @Step
+  public TopicDetails selectSaveThisFilterCheckboxMdl(boolean select){
+    selectElement(saveThisFilterCheckBoxAddFilterMdl, select);
+    return this;
+  }
+
   @Step
   @Step
   public boolean isSaveThisFilterCheckBoxSelected() {
   public boolean isSaveThisFilterCheckBoxSelected() {
     return isSelected(saveThisFilterCheckBoxAddFilterMdl);
     return isSelected(saveThisFilterCheckBoxAddFilterMdl);
   }
   }
 
 
+  @Step
+  public TopicDetails setDisplayNameFldAddFilterMdl(String displayName) {
+    displayNameInputAddFilterMdl.shouldBe(Condition.enabled).sendKeys(displayName);
+    return this;
+  }
+
+  @Step
+  public TopicDetails clickAddFilterBtnAndCloseMdl(boolean closeModal) {
+    addFilterBtnAddFilterMdl.shouldBe(Condition.enabled).click();
+    if(closeModal){
+      addFilterCodeModalTitle.shouldBe(Condition.hidden);}
+    else{
+      addFilterCodeModalTitle.shouldBe(Condition.visible);
+    }
+    return this;
+  }
+
   @Step
   @Step
   public boolean isAddFilterBtnAddFilterMdlEnabled() {
   public boolean isAddFilterBtnAddFilterMdlEnabled() {
     return isEnabled(addFilterBtnAddFilterMdl);
     return isEnabled(addFilterBtnAddFilterMdl);
   }
   }
 
 
   @Step
   @Step
-  public String getFilterName() {
-    return $x(filterNameLocator).getText();
+  public boolean isActiveFilterVisible(String activeFilterName) {
+    return isVisible($x(String.format(activeFilterNameLocator, activeFilterName)));
   }
   }
 
 
   public List<SelenideElement> getAllAddFilterModalVisibleElements() {
   public List<SelenideElement> getAllAddFilterModalVisibleElements() {
-    return Arrays.asList(savedFiltersField, displayNameInputAddFilterMdl, addFilterBtnAddFilterMdl, cancelBtnAddFilterMdl);
+    return Arrays.asList(savedFiltersLink, displayNameInputAddFilterMdl, addFilterBtnAddFilterMdl, cancelBtnAddFilterMdl);
   }
   }
 
 
   public List<SelenideElement> getAllAddFilterModalEnabledElements() {
   public List<SelenideElement> getAllAddFilterModalEnabledElements() {
@@ -188,7 +274,7 @@ public class TopicDetails extends BasePage {
 
 
   private List<TopicDetails.MessageGridItem> initItems() {
   private List<TopicDetails.MessageGridItem> initItems() {
     List<TopicDetails.MessageGridItem> gridItemList = new ArrayList<>();
     List<TopicDetails.MessageGridItem> gridItemList = new ArrayList<>();
-    messageGridItems.shouldHave(CollectionCondition.sizeGreaterThan(0))
+    allGridItems.shouldHave(CollectionCondition.sizeGreaterThan(0))
         .forEach(item -> gridItemList.add(new TopicDetails.MessageGridItem(item)));
         .forEach(item -> gridItemList.add(new TopicDetails.MessageGridItem(item)));
     return gridItemList;
     return gridItemList;
   }
   }
@@ -200,6 +286,11 @@ public class TopicDetails extends BasePage {
         .findFirst().orElse(null);
         .findFirst().orElse(null);
   }
   }
 
 
+  @Step
+  public List<MessageGridItem> getAllMessages(){
+    return initItems();
+  }
+
   @Step
   @Step
   public TopicDetails.MessageGridItem getRandomMessage() {
   public TopicDetails.MessageGridItem getRandomMessage() {
     return getMessage(nextInt(initItems().size() - 1));
     return getMessage(nextInt(initItems().size() - 1));

+ 65 - 0
kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicSettingsTab.java

@@ -0,0 +1,65 @@
+package com.provectus.kafka.ui.pages.topic;
+
+import static com.codeborne.selenide.Selenide.$x;
+
+import com.codeborne.selenide.CollectionCondition;
+import com.codeborne.selenide.Condition;
+import com.codeborne.selenide.SelenideElement;
+import com.provectus.kafka.ui.pages.BasePage;
+import io.qameta.allure.Step;
+import java.util.ArrayList;
+import java.util.List;
+
+public class TopicSettingsTab extends BasePage {
+
+  protected SelenideElement defaultValueColumnHeaderLocator = $x("//div[text() = 'Default Value']");
+
+  @Step
+  public TopicSettingsTab waitUntilScreenReady(){
+    waitUntilSpinnerDisappear();
+    defaultValueColumnHeaderLocator.shouldBe(Condition.visible);
+    return this;
+  }
+
+  private List<SettingsGridItem> initGridItems() {
+    List<SettingsGridItem> gridItemList = new ArrayList<>();
+    allGridItems.shouldHave(CollectionCondition.sizeGreaterThan(0))
+        .forEach(item -> gridItemList.add(new SettingsGridItem(item)));
+    return gridItemList;
+  }
+
+  private TopicSettingsTab.SettingsGridItem getItemByKey(String key){
+    return initGridItems().stream()
+        .filter(e ->e.getKey().equals(key))
+        .findFirst().orElse(null);
+  }
+
+  @Step
+  public String getValueByKey(String key){
+    return getItemByKey(key).getValue();
+  }
+
+  public static class SettingsGridItem extends BasePage {
+
+    private final SelenideElement element;
+
+    public SettingsGridItem(SelenideElement element) {
+      this.element = element;
+    }
+
+    @Step
+    public String getKey(){
+      return element.$x("./td[1]/span").getText().trim();
+    }
+
+    @Step
+    public String getValue(){
+      return element.$x("./td[2]/span").getText().trim();
+    }
+
+    @Step
+    public String getDefaultValue() {
+      return element.$x("./td[3]/span").getText().trim();
+    }
+  }
+}

+ 108 - 7
kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/pages/topic/TopicsList.java

@@ -2,6 +2,7 @@ package com.provectus.kafka.ui.pages.topic;
 
 
 import static com.codeborne.selenide.Selenide.$x;
 import static com.codeborne.selenide.Selenide.$x;
 
 
+import com.codeborne.selenide.CollectionCondition;
 import com.codeborne.selenide.Condition;
 import com.codeborne.selenide.Condition;
 import com.codeborne.selenide.SelenideElement;
 import com.codeborne.selenide.SelenideElement;
 import com.provectus.kafka.ui.pages.BasePage;
 import com.provectus.kafka.ui.pages.BasePage;
@@ -21,7 +22,6 @@ public class TopicsList extends BasePage {
     protected SelenideElement deleteSelectedTopicsBtn = $x("//button[text()='Delete selected topics']");
     protected SelenideElement deleteSelectedTopicsBtn = $x("//button[text()='Delete selected topics']");
     protected SelenideElement copySelectedTopicBtn = $x("//button[text()='Copy selected topic']");
     protected SelenideElement copySelectedTopicBtn = $x("//button[text()='Copy selected topic']");
     protected SelenideElement purgeMessagesOfSelectedTopicsBtn = $x("//button[text()='Purge messages of selected topics']");
     protected SelenideElement purgeMessagesOfSelectedTopicsBtn = $x("//button[text()='Purge messages of selected topics']");
-    protected String checkBoxListLocator = "//a[@title='%s']//ancestor::td/../td/input[@type='checkbox']";
 
 
     @Step
     @Step
     public TopicsList waitUntilScreenReady() {
     public TopicsList waitUntilScreenReady() {
@@ -43,18 +43,22 @@ public class TopicsList extends BasePage {
     }
     }
 
 
     @Step
     @Step
-    public TopicsList openTopic(String topicName) {
-        getTableElement(topicName).shouldBe(Condition.enabled).click();
-        return this;
+    public boolean isShowInternalRadioBtnSelected() {
+      return isSelected(showInternalRadioBtn);
     }
     }
 
 
     @Step
     @Step
-    public TopicsList selectCheckboxByName(String topicName){
-      SelenideElement checkBox = $x(String.format(checkBoxListLocator,topicName));
-      if(!checkBox.is(Condition.selected)){clickByJavaScript(checkBox);}
+    public TopicsList setShowInternalRadioButton(boolean select) {
+      selectElement(showInternalRadioBtn, select);
       return this;
       return this;
     }
     }
 
 
+    @Step
+    public TopicsList openTopic(String topicName) {
+        getTopicItem(topicName).openItem();
+        return this;
+    }
+
     @Step
     @Step
     public boolean isCopySelectedTopicBtnEnabled(){
     public boolean isCopySelectedTopicBtnEnabled(){
       return isEnabled(copySelectedTopicBtn);
       return isEnabled(copySelectedTopicBtn);
@@ -92,4 +96,101 @@ public class TopicsList extends BasePage {
       enabledElements.addAll(Arrays.asList(searchField, showInternalRadioBtn,addTopicBtn));
       enabledElements.addAll(Arrays.asList(searchField, showInternalRadioBtn,addTopicBtn));
       return enabledElements;
       return enabledElements;
     }
     }
+
+    private List<TopicGridItem> initGridItems() {
+      List<TopicGridItem> gridItemList = new ArrayList<>();
+      allGridItems.shouldHave(CollectionCondition.sizeGreaterThan(0))
+          .forEach(item -> gridItemList.add(new TopicGridItem(item)));
+      return gridItemList;
+    }
+
+  @Step
+  public TopicGridItem getTopicItem(String name) {
+    return initGridItems().stream()
+        .filter(e -> e.getName().equals(name))
+        .findFirst().orElse(null);
+  }
+
+    @Step
+    public List<TopicGridItem> getNonInternalTopics() {
+      return initGridItems().stream()
+          .filter(e -> !e.isInternal())
+          .collect(Collectors.toList());
+    }
+
+    @Step
+    public List<TopicGridItem> getInternalTopics() {
+      return initGridItems().stream()
+          .filter(TopicGridItem::isInternal)
+          .collect(Collectors.toList());
+    }
+
+    public static class TopicGridItem extends BasePage {
+
+      private final SelenideElement element;
+
+      public TopicGridItem(SelenideElement element) {
+        this.element = element;
+      }
+
+      @Step
+      public void selectItem(boolean select) {
+        selectElement(element.$x("./td[1]/input"), select);
+      }
+
+      @Step
+      public boolean isInternal() {
+        boolean internal = false;
+        try {
+          element.$x("./td[2]/a/span").shouldBe(Condition.visible);
+          internal = true;
+        } catch (Throwable ignored) {
+        }
+        return internal;
+      }
+
+      private SelenideElement getNameElm() {
+        return element.$x("./td[2]");
+      }
+
+      @Step
+      public String getName() {
+        return getNameElm().getText().trim();
+      }
+
+      @Step
+      public void openItem() {
+        getNameElm().click();
+      }
+
+      @Step
+      public int getPartition() {
+        return Integer.parseInt(element.$x("./td[3]").getText().trim());
+      }
+
+      @Step
+      public int getOutOfSyncReplicas() {
+        return Integer.parseInt(element.$x("./td[4]").getText().trim());
+      }
+
+      @Step
+      public int getReplicationFactor() {
+        return Integer.parseInt(element.$x("./td[5]").getText().trim());
+      }
+
+      @Step
+      public int getNumberOfMessages() {
+        return Integer.parseInt(element.$x("./td[6]").getText().trim());
+      }
+
+      @Step
+      public int getSize() {
+        return Integer.parseInt(element.$x("./td[7]").getText().trim());
+      }
+
+      @Step
+      public void openDotMenu(){
+        element.$x("./td[8]//button").click();
+      }
+    }
 }
 }

+ 9 - 0
kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/WebUtils.java

@@ -69,4 +69,13 @@ public class WebUtils {
     }
     }
     return isSelected;
     return isSelected;
   }
   }
+
+  public static boolean selectElement(SelenideElement element, boolean select){
+    if (select) {
+      if (!element.isSelected()) clickByJavaScript(element);
+    } else {
+      if (element.isSelected()) clickByJavaScript(element);
+    }
+    return true;
+  }
 }
 }

+ 33 - 0
kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/base/BaseTest.java

@@ -1,13 +1,17 @@
 package com.provectus.kafka.ui.base;
 package com.provectus.kafka.ui.base;
 
 
+import com.codeborne.selenide.Condition;
 import com.codeborne.selenide.Selenide;
 import com.codeborne.selenide.Selenide;
+import com.codeborne.selenide.SelenideElement;
 import com.codeborne.selenide.WebDriverRunner;
 import com.codeborne.selenide.WebDriverRunner;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.DisplayNameGenerator;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.DisplayNameGenerator;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.TestCaseGenerator;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.TestCaseGenerator;
 import io.github.cdimascio.dotenv.Dotenv;
 import io.github.cdimascio.dotenv.Dotenv;
 import io.qameta.allure.Allure;
 import io.qameta.allure.Allure;
+import io.qase.api.annotation.Step;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.io.FileUtils;
+import org.assertj.core.api.SoftAssertions;
 import org.junit.jupiter.api.*;
 import org.junit.jupiter.api.*;
 import org.openqa.selenium.Dimension;
 import org.openqa.selenium.Dimension;
 import org.openqa.selenium.OutputType;
 import org.openqa.selenium.OutputType;
@@ -22,8 +26,10 @@ import org.testcontainers.utility.DockerImageName;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.File;
 import java.io.IOException;
 import java.io.IOException;
+import java.util.List;
 
 
 import static com.provectus.kafka.ui.base.Setup.*;
 import static com.provectus.kafka.ui.base.Setup.*;
+import static com.provectus.kafka.ui.pages.NaviSideBar.SideMenuOption.TOPICS;
 import static com.provectus.kafka.ui.settings.Source.BASE_WEB_URL;
 import static com.provectus.kafka.ui.settings.Source.BASE_WEB_URL;
 
 
 @Slf4j
 @Slf4j
@@ -110,4 +116,31 @@ public class BaseTest extends Facade {
             ((TakesScreenshot) webDriverContainer.getWebDriver()).getScreenshotAs(OutputType.BYTES)));
             ((TakesScreenshot) webDriverContainer.getWebDriver()).getScreenshotAs(OutputType.BYTES)));
     browserClear();
     browserClear();
   }
   }
+
+  @Step
+  protected void navigateToTopics(){
+    naviSideBar
+        .openSideMenu(TOPICS);
+    topicsList
+        .waitUntilScreenReady();
+  }
+
+  @Step
+  protected void navigateToTopicsAndOpenDetails(String topicName){
+    naviSideBar
+        .openSideMenu(TOPICS);
+    topicsList
+        .waitUntilScreenReady()
+        .openTopic(topicName);
+    topicDetails
+        .waitUntilScreenReady();
+  }
+
+  @Step
+  protected void verifyElementsCondition(List<SelenideElement> elementList, Condition expectedCondition) {
+    SoftAssertions softly = new SoftAssertions();
+    elementList.forEach(element -> softly.assertThat(element.is(expectedCondition))
+        .as(element.getSearchCriteria() + " is " + expectedCondition).isTrue());
+    softly.assertAll();
+  }
 }
 }

+ 4 - 0
kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/base/Facade.java

@@ -3,6 +3,7 @@ package com.provectus.kafka.ui.base;
 import com.provectus.kafka.ui.helpers.ApiHelper;
 import com.provectus.kafka.ui.helpers.ApiHelper;
 import com.provectus.kafka.ui.pages.NaviSideBar;
 import com.provectus.kafka.ui.pages.NaviSideBar;
 import com.provectus.kafka.ui.pages.TopPanel;
 import com.provectus.kafka.ui.pages.TopPanel;
+import com.provectus.kafka.ui.pages.brokers.BrokersConfigTab;
 import com.provectus.kafka.ui.pages.brokers.BrokersDetails;
 import com.provectus.kafka.ui.pages.brokers.BrokersDetails;
 import com.provectus.kafka.ui.pages.brokers.BrokersList;
 import com.provectus.kafka.ui.pages.brokers.BrokersList;
 import com.provectus.kafka.ui.pages.connector.ConnectorCreateForm;
 import com.provectus.kafka.ui.pages.connector.ConnectorCreateForm;
@@ -14,6 +15,7 @@ import com.provectus.kafka.ui.pages.schema.SchemaCreateForm;
 import com.provectus.kafka.ui.pages.schema.SchemaDetails;
 import com.provectus.kafka.ui.pages.schema.SchemaDetails;
 import com.provectus.kafka.ui.pages.schema.SchemaRegistryList;
 import com.provectus.kafka.ui.pages.schema.SchemaRegistryList;
 import com.provectus.kafka.ui.pages.topic.ProduceMessagePanel;
 import com.provectus.kafka.ui.pages.topic.ProduceMessagePanel;
+import com.provectus.kafka.ui.pages.topic.TopicSettingsTab;
 import com.provectus.kafka.ui.pages.topic.TopicCreateEditForm;
 import com.provectus.kafka.ui.pages.topic.TopicCreateEditForm;
 import com.provectus.kafka.ui.pages.topic.TopicDetails;
 import com.provectus.kafka.ui.pages.topic.TopicDetails;
 import com.provectus.kafka.ui.pages.topic.TopicsList;
 import com.provectus.kafka.ui.pages.topic.TopicsList;
@@ -36,4 +38,6 @@ public abstract class Facade {
     protected TopPanel topPanel = new TopPanel();
     protected TopPanel topPanel = new TopPanel();
     protected BrokersList brokersList = new BrokersList();
     protected BrokersList brokersList = new BrokersList();
     protected BrokersDetails brokersDetails = new BrokersDetails();
     protected BrokersDetails brokersDetails = new BrokersDetails();
+    protected BrokersConfigTab brokersConfigTab = new BrokersConfigTab();
+    protected TopicSettingsTab topicSettingsTab = new TopicSettingsTab();
 }
 }

+ 6 - 15
kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/SmokeTests.java

@@ -5,7 +5,8 @@ import com.provectus.kafka.ui.base.BaseTest;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.AutomationStatus;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.AutomationStatus;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.enums.Status;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.enums.Status;
 import io.qase.api.annotation.CaseId;
 import io.qase.api.annotation.CaseId;
-import org.assertj.core.api.SoftAssertions;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
 
 
 public class SmokeTests extends BaseTest {
 public class SmokeTests extends BaseTest {
@@ -14,19 +15,9 @@ public class SmokeTests extends BaseTest {
     @AutomationStatus(status = Status.AUTOMATED)
     @AutomationStatus(status = Status.AUTOMATED)
     @CaseId(198)
     @CaseId(198)
     public void checkBasePageElements(){
     public void checkBasePageElements(){
-        SoftAssertions softly = new SoftAssertions();
-        topPanel.getAllVisibleElements()
-                .forEach(element ->
-                        softly.assertThat(element.is(Condition.visible))
-                                .as(element.getSearchCriteria() + " isVisible()").isTrue());
-        topPanel.getAllEnabledElements()
-                .forEach(element ->
-                        softly.assertThat(element.is(Condition.enabled))
-                                .as(element.getSearchCriteria() + " isEnabled()").isTrue());
-        naviSideBar.getAllMenuButtons()
-                .forEach(element ->
-                        softly.assertThat(element.is(Condition.enabled) && element.is(Condition.visible))
-                                .as(element.getSearchCriteria() + " isEnabled()").isTrue());
-        softly.assertAll();
+      verifyElementsCondition(Stream.concat(topPanel.getAllVisibleElements().stream(), naviSideBar.getAllMenuButtons().stream())
+          .collect(Collectors.toList()),Condition.visible);
+      verifyElementsCondition(Stream.concat(topPanel.getAllEnabledElements().stream(), naviSideBar.getAllMenuButtons().stream())
+          .collect(Collectors.toList()),Condition.enabled);
     }
     }
 }
 }

+ 33 - 12
kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/brokers/BrokersTests.java

@@ -1,6 +1,7 @@
 package com.provectus.kafka.ui.suite.brokers;
 package com.provectus.kafka.ui.suite.brokers;
 
 
 import static com.provectus.kafka.ui.pages.NaviSideBar.SideMenuOption.BROKERS;
 import static com.provectus.kafka.ui.pages.NaviSideBar.SideMenuOption.BROKERS;
+import static com.provectus.kafka.ui.pages.brokers.BrokersDetails.DetailsTab.CONFIGS;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThat;
 
 
 import com.codeborne.selenide.Condition;
 import com.codeborne.selenide.Condition;
@@ -8,8 +9,8 @@ import com.provectus.kafka.ui.base.BaseTest;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.AutomationStatus;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.AutomationStatus;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.Suite;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.Suite;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.enums.Status;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.enums.Status;
+import io.qameta.allure.Step;
 import io.qase.api.annotation.CaseId;
 import io.qase.api.annotation.CaseId;
-import org.assertj.core.api.SoftAssertions;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
 
 
@@ -17,26 +18,46 @@ public class BrokersTests extends BaseTest {
   private static final String SUITE_TITLE = "Brokers";
   private static final String SUITE_TITLE = "Brokers";
   private static final long SUITE_ID = 1;
   private static final long SUITE_ID = 1;
 
 
+  @DisplayName("Checking the Brokers overview")
+  @Suite(suiteId = SUITE_ID, title = SUITE_TITLE)
+  @AutomationStatus(status = Status.AUTOMATED)
+  @CaseId(1)
+  @Test
+  public void checkBrokersOverview(){
+    navigateToBrokers();
+    assertThat(brokersList.getAllBrokers()).as("getAllBrokers()").size().isGreaterThan(0);
+    verifyElementsCondition(brokersList.getAllVisibleElements(), Condition.visible);
+    verifyElementsCondition(brokersList.getAllEnabledElements(), Condition.enabled);
+  }
+
   @DisplayName("Checking the existing Broker's profile in a cluster")
   @DisplayName("Checking the existing Broker's profile in a cluster")
   @Suite(suiteId = SUITE_ID, title = SUITE_TITLE)
   @Suite(suiteId = SUITE_ID, title = SUITE_TITLE)
   @AutomationStatus(status = Status.AUTOMATED)
   @AutomationStatus(status = Status.AUTOMATED)
   @CaseId(85)
   @CaseId(85)
   @Test
   @Test
   public void checkExistingBrokersInCluster(){
   public void checkExistingBrokersInCluster(){
-    naviSideBar
-        .openSideMenu(BROKERS);
+    navigateToBrokers();
+    assertThat(brokersList.getAllBrokers()).as("getAllBrokers()").size().isGreaterThan(0);
     brokersList
     brokersList
+        .openBroker(1);
+    brokersDetails
         .waitUntilScreenReady();
         .waitUntilScreenReady();
-    assertThat(brokersList.isBrokerVisible("1")).as("isBrokerVisible()").isTrue();
-    brokersList
-        .openBroker("1");
+    verifyElementsCondition(brokersDetails.getAllVisibleElements(), Condition.visible);
+    verifyElementsCondition(brokersDetails.getAllEnabledElements(), Condition.enabled);
     brokersDetails
     brokersDetails
+        .openDetailsTab(CONFIGS);
+    brokersConfigTab
+        .waitUntilScreenReady();
+    verifyElementsCondition(brokersConfigTab.getColumnHeaders(), Condition.visible);
+    verifyElementsCondition(brokersConfigTab.getEditButtons(), Condition.enabled);
+    assertThat(brokersConfigTab.isSearchByKeyVisible()).as("isSearchByKeyVisible()").isTrue();
+  }
+
+  @Step
+  private void navigateToBrokers(){
+    naviSideBar
+        .openSideMenu(BROKERS);
+    brokersList
         .waitUntilScreenReady();
         .waitUntilScreenReady();
-    SoftAssertions softly = new SoftAssertions();
-    brokersDetails.getAllVisibleElements().forEach(element -> softly.assertThat(element.is(Condition.visible))
-        .as(element.getSearchCriteria() + " isVisible()").isTrue());
-    brokersDetails.getAllEnabledElements().forEach(element -> softly.assertThat(element.is(Condition.enabled))
-        .as(element.getSearchCriteria() + " isEnabled()").isTrue());
-    softly.assertAll();
   }
   }
 }
 }

+ 28 - 37
kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/connectors/ConnectorsTests.java

@@ -12,6 +12,7 @@ import com.provectus.kafka.ui.models.Topic;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.AutomationStatus;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.AutomationStatus;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.Suite;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.Suite;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.enums.Status;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.enums.Status;
+import io.qameta.allure.Step;
 import io.qase.api.annotation.CaseId;
 import io.qase.api.annotation.CaseId;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.List;
@@ -68,10 +69,8 @@ public class ConnectorsTests extends BaseTest {
         Connector connectorForCreate = new Connector()
         Connector connectorForCreate = new Connector()
                 .setName("sink_postgres_activities_e2e_checks-" + randomAlphabetic(5))
                 .setName("sink_postgres_activities_e2e_checks-" + randomAlphabetic(5))
                 .setConfig(getResourceAsString("config_for_create_connector.json"));
                 .setConfig(getResourceAsString("config_for_create_connector.json"));
-        naviSideBar
-                .openSideMenu(KAFKA_CONNECT);
-        kafkaConnectList
-                .waitUntilScreenReady()
+        navigateToConnectors();
+      kafkaConnectList
                 .clickCreateConnectorBtn();
                 .clickCreateConnectorBtn();
         connectorCreateForm
         connectorCreateForm
                 .waitUntilScreenReady()
                 .waitUntilScreenReady()
@@ -79,18 +78,9 @@ public class ConnectorsTests extends BaseTest {
                 .clickSubmitButton();
                 .clickSubmitButton();
         connectorDetails
         connectorDetails
                 .waitUntilScreenReady();
                 .waitUntilScreenReady();
-        naviSideBar
-                .openSideMenu(KAFKA_CONNECT);
-        kafkaConnectList
-                .waitUntilScreenReady()
-                .openConnector(connectorForCreate.getName());
-        connectorDetails
-                .waitUntilScreenReady();
+        navigateToConnectorsAndOpenDetails(connectorForCreate.getName());
         Assertions.assertTrue(connectorDetails.isConnectorHeaderVisible(connectorForCreate.getName()),"isConnectorTitleVisible()");
         Assertions.assertTrue(connectorDetails.isConnectorHeaderVisible(connectorForCreate.getName()),"isConnectorTitleVisible()");
-        naviSideBar
-                .openSideMenu(KAFKA_CONNECT);
-        kafkaConnectList
-                .waitUntilScreenReady();
+        navigateToConnectors();
         Assertions.assertTrue(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_DELETE.getName()), "isConnectorVisible()");
         Assertions.assertTrue(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_DELETE.getName()), "isConnectorVisible()");
         CONNECTOR_LIST.add(connectorForCreate);
         CONNECTOR_LIST.add(connectorForCreate);
     }
     }
@@ -101,21 +91,13 @@ public class ConnectorsTests extends BaseTest {
     @CaseId(196)
     @CaseId(196)
     @Test
     @Test
     public void updateConnector() {
     public void updateConnector() {
-        naviSideBar
-                .openSideMenu(KAFKA_CONNECT);
-        kafkaConnectList
-                .waitUntilScreenReady()
-                .openConnector(CONNECTOR_FOR_UPDATE.getName());
-        connectorDetails
-                .waitUntilScreenReady()
+      navigateToConnectorsAndOpenDetails(CONNECTOR_FOR_UPDATE.getName());
+      connectorDetails
                 .openConfigTab()
                 .openConfigTab()
                 .setConfig(CONNECTOR_FOR_UPDATE.getConfig())
                 .setConfig(CONNECTOR_FOR_UPDATE.getConfig())
                 .clickSubmitButton();
                 .clickSubmitButton();
         Assertions.assertTrue(connectorDetails.isAlertWithMessageVisible(SUCCESS,"Config successfully updated."),"isAlertWithMessageVisible()");
         Assertions.assertTrue(connectorDetails.isAlertWithMessageVisible(SUCCESS,"Config successfully updated."),"isAlertWithMessageVisible()");
-        naviSideBar
-                .openSideMenu(KAFKA_CONNECT);
-        kafkaConnectList
-                .waitUntilScreenReady();
+        navigateToConnectors();
         Assertions.assertTrue(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_UPDATE.getName()), "isConnectorVisible()");
         Assertions.assertTrue(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_UPDATE.getName()), "isConnectorVisible()");
     }
     }
 
 
@@ -125,20 +107,12 @@ public class ConnectorsTests extends BaseTest {
     @CaseId(195)
     @CaseId(195)
     @Test
     @Test
     public void deleteConnector() {
     public void deleteConnector() {
-        naviSideBar
-                .openSideMenu(KAFKA_CONNECT);
-        kafkaConnectList
-                .waitUntilScreenReady()
-                .openConnector(CONNECTOR_FOR_DELETE.getName());
-        connectorDetails
-                .waitUntilScreenReady()
+      navigateToConnectorsAndOpenDetails(CONNECTOR_FOR_DELETE.getName());
+      connectorDetails
                 .openDotMenu()
                 .openDotMenu()
                 .clickDeleteBtn()
                 .clickDeleteBtn()
                 .clickConfirmBtn();
                 .clickConfirmBtn();
-        naviSideBar
-                .openSideMenu(KAFKA_CONNECT);
-        kafkaConnectList
-                .waitUntilScreenReady();
+      navigateToConnectors();
         Assertions.assertFalse(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_DELETE.getName()), "isConnectorVisible()");
         Assertions.assertFalse(kafkaConnectList.isConnectorVisible(CONNECTOR_FOR_DELETE.getName()), "isConnectorVisible()");
         CONNECTOR_LIST.remove(CONNECTOR_FOR_DELETE);
         CONNECTOR_LIST.remove(CONNECTOR_FOR_DELETE);
     }
     }
@@ -149,4 +123,21 @@ public class ConnectorsTests extends BaseTest {
                 apiHelper.deleteConnector(CLUSTER_NAME, CONNECT_NAME, connector.getName()));
                 apiHelper.deleteConnector(CLUSTER_NAME, CONNECT_NAME, connector.getName()));
         TOPIC_LIST.forEach(topic -> apiHelper.deleteTopic(CLUSTER_NAME, topic.getName()));
         TOPIC_LIST.forEach(topic -> apiHelper.deleteTopic(CLUSTER_NAME, topic.getName()));
     }
     }
+
+    @Step
+    private void navigateToConnectors(){
+      naviSideBar
+          .openSideMenu(KAFKA_CONNECT);
+      kafkaConnectList
+          .waitUntilScreenReady();
+    }
+
+    @Step
+    private void navigateToConnectorsAndOpenDetails(String connectorName){
+      navigateToConnectors();
+      kafkaConnectList
+          .openConnector(connectorName);
+      connectorDetails
+          .waitUntilScreenReady();
+    }
 }
 }

+ 37 - 53
kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/schemas/SchemasTests.java

@@ -4,12 +4,14 @@ import static com.provectus.kafka.ui.pages.NaviSideBar.SideMenuOption.SCHEMA_REG
 import static com.provectus.kafka.ui.settings.Source.CLUSTER_NAME;
 import static com.provectus.kafka.ui.settings.Source.CLUSTER_NAME;
 import static com.provectus.kafka.ui.utilities.FileUtils.fileToString;
 import static com.provectus.kafka.ui.utilities.FileUtils.fileToString;
 
 
+import com.codeborne.selenide.Condition;
 import com.provectus.kafka.ui.api.model.CompatibilityLevel;
 import com.provectus.kafka.ui.api.model.CompatibilityLevel;
 import com.provectus.kafka.ui.base.BaseTest;
 import com.provectus.kafka.ui.base.BaseTest;
 import com.provectus.kafka.ui.models.Schema;
 import com.provectus.kafka.ui.models.Schema;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.AutomationStatus;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.AutomationStatus;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.Suite;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.annotations.Suite;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.enums.Status;
 import com.provectus.kafka.ui.utilities.qaseIoUtils.enums.Status;
+import io.qameta.allure.Step;
 import io.qase.api.annotation.CaseId;
 import io.qase.api.annotation.CaseId;
 import java.util.ArrayList;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.List;
@@ -50,10 +52,8 @@ public class SchemasTests extends BaseTest {
     @Order(1)
     @Order(1)
     void createSchemaAvro() {
     void createSchemaAvro() {
         Schema schemaAvro = Schema.createSchemaAvro();
         Schema schemaAvro = Schema.createSchemaAvro();
-        naviSideBar
-                .openSideMenu(SCHEMA_REGISTRY);
+        navigateToSchemaRegistry();
         schemaRegistryList
         schemaRegistryList
-                .waitUntilScreenReady()
                 .clickCreateSchema();
                 .clickCreateSchema();
         schemaCreateForm
         schemaCreateForm
                 .setSubjectName(schemaAvro.getName())
                 .setSubjectName(schemaAvro.getName())
@@ -67,10 +67,7 @@ public class SchemasTests extends BaseTest {
         softly.assertThat(schemaDetails.getSchemaType()).as("getSchemaType()").isEqualTo(schemaAvro.getType().getValue());
         softly.assertThat(schemaDetails.getSchemaType()).as("getSchemaType()").isEqualTo(schemaAvro.getType().getValue());
         softly.assertThat(schemaDetails.getCompatibility()).as("getCompatibility()").isEqualTo(CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue());
         softly.assertThat(schemaDetails.getCompatibility()).as("getCompatibility()").isEqualTo(CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue());
         softly.assertAll();
         softly.assertAll();
-        naviSideBar
-                .openSideMenu(SCHEMA_REGISTRY);
-        schemaRegistryList
-                .waitUntilScreenReady();
+        navigateToSchemaRegistry();
         Assertions.assertTrue(schemaRegistryList.isSchemaVisible(AVRO_API.getName()),"isSchemaVisible()");
         Assertions.assertTrue(schemaRegistryList.isSchemaVisible(AVRO_API.getName()),"isSchemaVisible()");
         SCHEMA_LIST.add(schemaAvro);
         SCHEMA_LIST.add(schemaAvro);
     }
     }
@@ -83,17 +80,16 @@ public class SchemasTests extends BaseTest {
     @Order(2)
     @Order(2)
     void updateSchemaAvro() {
     void updateSchemaAvro() {
         AVRO_API.setValuePath(System.getProperty("user.dir") + "/src/main/resources/testData/schema_avro_for_update.json");
         AVRO_API.setValuePath(System.getProperty("user.dir") + "/src/main/resources/testData/schema_avro_for_update.json");
-        naviSideBar
-                .openSideMenu(SCHEMA_REGISTRY);
-        schemaRegistryList
-                .waitUntilScreenReady()
-                .openSchema(AVRO_API.getName());
+        navigateToSchemaRegistryAndOpenDetails(AVRO_API.getName());
         schemaDetails
         schemaDetails
-                .waitUntilScreenReady()
                 .openEditSchema();
                 .openEditSchema();
         schemaCreateForm
         schemaCreateForm
                 .waitUntilScreenReady();
                 .waitUntilScreenReady();
-        Assertions.assertTrue(schemaCreateForm.isSchemaDropDownDisabled(),"isSchemaDropDownDisabled()");
+      verifyElementsCondition(schemaCreateForm.getAllDetailsPageElements(), Condition.visible);
+      SoftAssertions softly = new SoftAssertions();
+        softly.assertThat(schemaCreateForm.isSubmitBtnEnabled()).as("isSubmitBtnEnabled()").isFalse();
+        softly.assertThat(schemaCreateForm.isSchemaDropDownEnabled()).as("isSchemaDropDownEnabled()").isFalse();
+        softly.assertAll();
         schemaCreateForm
         schemaCreateForm
                 .selectCompatibilityLevelFromDropdown(CompatibilityLevel.CompatibilityEnum.NONE)
                 .selectCompatibilityLevelFromDropdown(CompatibilityLevel.CompatibilityEnum.NONE)
                 .setNewSchemaValue(fileToString(AVRO_API.getValuePath()))
                 .setNewSchemaValue(fileToString(AVRO_API.getValuePath()))
@@ -110,11 +106,7 @@ public class SchemasTests extends BaseTest {
     @Test
     @Test
     @Order(3)
     @Order(3)
     void compareVersionsOperation() {
     void compareVersionsOperation() {
-      naviSideBar
-          .openSideMenu(SCHEMA_REGISTRY);
-      schemaRegistryList
-          .waitUntilScreenReady()
-          .openSchema(AVRO_API.getName());
+      navigateToSchemaRegistryAndOpenDetails(AVRO_API.getName());
       int latestVersion = schemaDetails
       int latestVersion = schemaDetails
           .waitUntilScreenReady()
           .waitUntilScreenReady()
           .getLatestVersion();
           .getLatestVersion();
@@ -137,13 +129,8 @@ public class SchemasTests extends BaseTest {
     @Test
     @Test
     @Order(4)
     @Order(4)
     void deleteSchemaAvro() {
     void deleteSchemaAvro() {
-      naviSideBar
-          .openSideMenu(SCHEMA_REGISTRY);
-      schemaRegistryList
-          .waitUntilScreenReady()
-          .openSchema(AVRO_API.getName());
+      navigateToSchemaRegistryAndOpenDetails(AVRO_API.getName());
       schemaDetails
       schemaDetails
-          .waitUntilScreenReady()
           .removeSchema();
           .removeSchema();
       schemaRegistryList
       schemaRegistryList
           .waitUntilScreenReady();
           .waitUntilScreenReady();
@@ -159,10 +146,8 @@ public class SchemasTests extends BaseTest {
     @Order(5)
     @Order(5)
     void createSchemaJson() {
     void createSchemaJson() {
         Schema schemaJson = Schema.createSchemaJson();
         Schema schemaJson = Schema.createSchemaJson();
-        naviSideBar
-                .openSideMenu(SCHEMA_REGISTRY);
+        navigateToSchemaRegistry();
         schemaRegistryList
         schemaRegistryList
-                .waitUntilScreenReady()
                 .clickCreateSchema();
                 .clickCreateSchema();
         schemaCreateForm
         schemaCreateForm
                 .setSubjectName(schemaJson.getName())
                 .setSubjectName(schemaJson.getName())
@@ -176,10 +161,7 @@ public class SchemasTests extends BaseTest {
         softly.assertThat(schemaDetails.getSchemaType()).as("getSchemaType()").isEqualTo(schemaJson.getType().getValue());
         softly.assertThat(schemaDetails.getSchemaType()).as("getSchemaType()").isEqualTo(schemaJson.getType().getValue());
         softly.assertThat(schemaDetails.getCompatibility()).as("getCompatibility()").isEqualTo(CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue());
         softly.assertThat(schemaDetails.getCompatibility()).as("getCompatibility()").isEqualTo(CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue());
         softly.assertAll();
         softly.assertAll();
-        naviSideBar
-                .openSideMenu(SCHEMA_REGISTRY);
-        schemaRegistryList
-                .waitUntilScreenReady();
+        navigateToSchemaRegistry();
         Assertions.assertTrue(schemaRegistryList.isSchemaVisible(JSON_API.getName()),"isSchemaVisible()");
         Assertions.assertTrue(schemaRegistryList.isSchemaVisible(JSON_API.getName()),"isSchemaVisible()");
         SCHEMA_LIST.add(schemaJson);
         SCHEMA_LIST.add(schemaJson);
     }
     }
@@ -191,13 +173,8 @@ public class SchemasTests extends BaseTest {
     @Test
     @Test
     @Order(6)
     @Order(6)
     void deleteSchemaJson() {
     void deleteSchemaJson() {
-        naviSideBar
-                .openSideMenu(SCHEMA_REGISTRY);
-        schemaRegistryList
-                .waitUntilScreenReady()
-                .openSchema(JSON_API.getName());
-        schemaDetails
-                .waitUntilScreenReady()
+      navigateToSchemaRegistryAndOpenDetails(JSON_API.getName());
+      schemaDetails
                 .removeSchema();
                 .removeSchema();
         schemaRegistryList
         schemaRegistryList
                 .waitUntilScreenReady();
                 .waitUntilScreenReady();
@@ -213,10 +190,8 @@ public class SchemasTests extends BaseTest {
     @Order(7)
     @Order(7)
     void createSchemaProtobuf() {
     void createSchemaProtobuf() {
         Schema schemaProtobuf = Schema.createSchemaProtobuf();
         Schema schemaProtobuf = Schema.createSchemaProtobuf();
-        naviSideBar
-                .openSideMenu(SCHEMA_REGISTRY);
+        navigateToSchemaRegistry();
         schemaRegistryList
         schemaRegistryList
-                .waitUntilScreenReady()
                 .clickCreateSchema();
                 .clickCreateSchema();
         schemaCreateForm
         schemaCreateForm
                 .setSubjectName(schemaProtobuf.getName())
                 .setSubjectName(schemaProtobuf.getName())
@@ -230,10 +205,7 @@ public class SchemasTests extends BaseTest {
         softly.assertThat(schemaDetails.getSchemaType()).as("getSchemaType()").isEqualTo(schemaProtobuf.getType().getValue());
         softly.assertThat(schemaDetails.getSchemaType()).as("getSchemaType()").isEqualTo(schemaProtobuf.getType().getValue());
         softly.assertThat(schemaDetails.getCompatibility()).as("getCompatibility()").isEqualTo(CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue());
         softly.assertThat(schemaDetails.getCompatibility()).as("getCompatibility()").isEqualTo(CompatibilityLevel.CompatibilityEnum.BACKWARD.getValue());
         softly.assertAll();
         softly.assertAll();
-        naviSideBar
-                .openSideMenu(SCHEMA_REGISTRY);
-        schemaRegistryList
-                .waitUntilScreenReady();
+        navigateToSchemaRegistry();
         Assertions.assertTrue(schemaRegistryList.isSchemaVisible(PROTOBUF_API.getName()),"isSchemaVisible()");
         Assertions.assertTrue(schemaRegistryList.isSchemaVisible(PROTOBUF_API.getName()),"isSchemaVisible()");
         SCHEMA_LIST.add(schemaProtobuf);
         SCHEMA_LIST.add(schemaProtobuf);
     }
     }
@@ -245,13 +217,8 @@ public class SchemasTests extends BaseTest {
     @Test
     @Test
     @Order(8)
     @Order(8)
     void deleteSchemaProtobuf() {
     void deleteSchemaProtobuf() {
-        naviSideBar
-                .openSideMenu(SCHEMA_REGISTRY);
-        schemaRegistryList
-                .waitUntilScreenReady()
-                .openSchema(PROTOBUF_API.getName());
-        schemaDetails
-                .waitUntilScreenReady()
+      navigateToSchemaRegistryAndOpenDetails(PROTOBUF_API.getName());
+      schemaDetails
                 .removeSchema();
                 .removeSchema();
         schemaRegistryList
         schemaRegistryList
                 .waitUntilScreenReady();
                 .waitUntilScreenReady();
@@ -263,4 +230,21 @@ public class SchemasTests extends BaseTest {
     public void afterAll() {
     public void afterAll() {
         SCHEMA_LIST.forEach(schema -> apiHelper.deleteSchema(CLUSTER_NAME, schema.getName()));
         SCHEMA_LIST.forEach(schema -> apiHelper.deleteSchema(CLUSTER_NAME, schema.getName()));
     }
     }
+
+    @Step
+    private void navigateToSchemaRegistry(){
+      naviSideBar
+          .openSideMenu(SCHEMA_REGISTRY);
+      schemaRegistryList
+          .waitUntilScreenReady();
+    }
+
+    @Step
+    private void navigateToSchemaRegistryAndOpenDetails(String schemaName){
+      navigateToSchemaRegistry();
+      schemaRegistryList
+          .openSchema(schemaName);
+      schemaDetails
+          .waitUntilScreenReady();
+    }
 }
 }

+ 30 - 22
kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/topics/TopicMessagesTests.java

@@ -1,7 +1,7 @@
 package com.provectus.kafka.ui.suite.topics;
 package com.provectus.kafka.ui.suite.topics;
 
 
 import static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS;
 import static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS;
-import static com.provectus.kafka.ui.pages.NaviSideBar.SideMenuOption.TOPICS;
+import static com.provectus.kafka.ui.pages.topic.TopicDetails.TopicMenu.MESSAGES;
 import static com.provectus.kafka.ui.settings.Source.CLUSTER_NAME;
 import static com.provectus.kafka.ui.settings.Source.CLUSTER_NAME;
 import static com.provectus.kafka.ui.utilities.FileUtils.fileToString;
 import static com.provectus.kafka.ui.utilities.FileUtils.fileToString;
 import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
 import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
@@ -47,13 +47,8 @@ public class TopicMessagesTests extends BaseTest {
   @CaseId(222)
   @CaseId(222)
   @Test
   @Test
   void produceMessage() {
   void produceMessage() {
-    naviSideBar
-        .openSideMenu(TOPICS);
-    topicsList
-        .waitUntilScreenReady()
-        .openTopic(TOPIC_FOR_MESSAGES.getName());
+    navigateToTopicsAndOpenDetails(TOPIC_FOR_MESSAGES.getName());
     topicDetails
     topicDetails
-        .waitUntilScreenReady()
         .openDetailsTab(TopicDetails.TopicMenu.MESSAGES)
         .openDetailsTab(TopicDetails.TopicMenu.MESSAGES)
         .clickProduceMessageBtn();
         .clickProduceMessageBtn();
     produceMessagePanel
     produceMessagePanel
@@ -64,8 +59,10 @@ public class TopicMessagesTests extends BaseTest {
     topicDetails
     topicDetails
         .waitUntilScreenReady();
         .waitUntilScreenReady();
     SoftAssertions softly = new SoftAssertions();
     SoftAssertions softly = new SoftAssertions();
-    softly.assertThat(topicDetails.isKeyMessageVisible((TOPIC_FOR_MESSAGES.getMessageKey()))).withFailMessage("isKeyMessageVisible()").isTrue();
-    softly.assertThat(topicDetails.isContentMessageVisible((TOPIC_FOR_MESSAGES.getMessageContent()).trim())).withFailMessage("isContentMessageVisible()").isTrue();
+    softly.assertThat(topicDetails.isKeyMessageVisible((TOPIC_FOR_MESSAGES.getMessageKey())))
+        .withFailMessage("isKeyMessageVisible()").isTrue();
+    softly.assertThat(topicDetails.isContentMessageVisible((TOPIC_FOR_MESSAGES.getMessageContent()).trim()))
+        .withFailMessage("isContentMessageVisible()").isTrue();
     softly.assertAll();
     softly.assertAll();
   }
   }
 
 
@@ -77,13 +74,8 @@ public class TopicMessagesTests extends BaseTest {
   @CaseId(19)
   @CaseId(19)
   @Test
   @Test
   void clearMessage() {
   void clearMessage() {
-    naviSideBar
-        .openSideMenu(TOPICS);
-    topicsList
-        .waitUntilScreenReady()
-        .openTopic(TOPIC_FOR_MESSAGES.getName());
+    navigateToTopicsAndOpenDetails(TOPIC_FOR_MESSAGES.getName());
     topicDetails
     topicDetails
-        .waitUntilScreenReady()
         .openDetailsTab(TopicDetails.TopicMenu.OVERVIEW)
         .openDetailsTab(TopicDetails.TopicMenu.OVERVIEW)
         .clickProduceMessageBtn();
         .clickProduceMessageBtn();
     int messageAmount = topicDetails.getMessageCountAmount();
     int messageAmount = topicDetails.getMessageCountAmount();
@@ -110,14 +102,8 @@ public class TopicMessagesTests extends BaseTest {
   @CaseId(21)
   @CaseId(21)
   @Test
   @Test
   void copyMessageFromTopicProfile() {
   void copyMessageFromTopicProfile() {
-    String topicName = "_schemas";
-    naviSideBar
-        .openSideMenu(TOPICS);
-    topicsList
-        .waitUntilScreenReady()
-        .openTopic(topicName);
+    navigateToTopicsAndOpenDetails("_schemas");
     topicDetails
     topicDetails
-        .waitUntilScreenReady()
         .openDetailsTab(TopicDetails.TopicMenu.MESSAGES)
         .openDetailsTab(TopicDetails.TopicMenu.MESSAGES)
         .getRandomMessage()
         .getRandomMessage()
         .openDotMenu()
         .openDotMenu()
@@ -126,6 +112,28 @@ public class TopicMessagesTests extends BaseTest {
         "isAlertWithMessageVisible()");
         "isAlertWithMessageVisible()");
   }
   }
 
 
+  @Disabled
+  @Issue("https://github.com/provectus/kafka-ui/issues/2856")
+  @DisplayName("Checking messages filtering by Offset within Topic/Messages")
+  @Suite(suiteId = SUITE_ID, title = SUITE_TITLE)
+  @AutomationStatus(status = Status.AUTOMATED)
+  @CaseId(15)
+  @Test
+  void checkingMessageFilteringByOffset() {
+    String offsetValue = "2";
+    navigateToTopicsAndOpenDetails("_schemas");
+    topicDetails
+        .openDetailsTab(MESSAGES)
+        .selectSeekTypeDdlMessagesTab("Offset")
+        .setSeekTypeValueFldMessagesTab(offsetValue)
+        .clickSubmitFiltersBtnMessagesTab();
+    SoftAssertions softly = new SoftAssertions();
+    topicDetails.getAllMessages()
+        .forEach(messages -> softly.assertThat(messages.getOffset() == Integer.parseInt(offsetValue))
+        .as("getAllMessages()").isTrue());
+    softly.assertAll();
+  }
+
   @AfterAll
   @AfterAll
   public void afterAll() {
   public void afterAll() {
     TOPIC_LIST.forEach(topic -> apiHelper.deleteTopic(CLUSTER_NAME, topic.getName()));
     TOPIC_LIST.forEach(topic -> apiHelper.deleteTopic(CLUSTER_NAME, topic.getName()));

+ 245 - 112
kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/suite/topics/TopicsTests.java

@@ -1,15 +1,20 @@
 package com.provectus.kafka.ui.suite.topics;
 package com.provectus.kafka.ui.suite.topics;
 
 
-import static com.provectus.kafka.ui.pages.NaviSideBar.SideMenuOption.TOPICS;
+import static com.provectus.kafka.ui.pages.BasePage.AlertHeader.SUCCESS;
+import static com.provectus.kafka.ui.pages.topic.TopicDetails.TopicMenu.MESSAGES;
+import static com.provectus.kafka.ui.pages.topic.TopicDetails.TopicMenu.SETTINGS;
 import static com.provectus.kafka.ui.pages.topic.enums.CleanupPolicyValue.COMPACT;
 import static com.provectus.kafka.ui.pages.topic.enums.CleanupPolicyValue.COMPACT;
 import static com.provectus.kafka.ui.pages.topic.enums.CleanupPolicyValue.DELETE;
 import static com.provectus.kafka.ui.pages.topic.enums.CleanupPolicyValue.DELETE;
 import static com.provectus.kafka.ui.pages.topic.enums.CustomParameterType.COMPRESSION_TYPE;
 import static com.provectus.kafka.ui.pages.topic.enums.CustomParameterType.COMPRESSION_TYPE;
+import static com.provectus.kafka.ui.pages.topic.enums.MaxSizeOnDisk.NOT_SET;
+import static com.provectus.kafka.ui.pages.topic.enums.MaxSizeOnDisk.SIZE_1_GB;
 import static com.provectus.kafka.ui.pages.topic.enums.MaxSizeOnDisk.SIZE_20_GB;
 import static com.provectus.kafka.ui.pages.topic.enums.MaxSizeOnDisk.SIZE_20_GB;
 import static com.provectus.kafka.ui.settings.Source.CLUSTER_NAME;
 import static com.provectus.kafka.ui.settings.Source.CLUSTER_NAME;
 import static com.provectus.kafka.ui.utilities.FileUtils.fileToString;
 import static com.provectus.kafka.ui.utilities.FileUtils.fileToString;
 import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
 import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
-import static org.apache.commons.lang3.RandomUtils.nextInt;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.apache.commons.lang3.RandomUtils.nextInt;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
 
 
 import com.codeborne.selenide.Condition;
 import com.codeborne.selenide.Condition;
 import com.provectus.kafka.ui.base.BaseTest;
 import com.provectus.kafka.ui.base.BaseTest;
@@ -45,21 +50,26 @@ public class TopicsTests extends BaseTest {
       .setCustomParameterType(COMPRESSION_TYPE)
       .setCustomParameterType(COMPRESSION_TYPE)
       .setCustomParameterValue("producer")
       .setCustomParameterValue("producer")
       .setCleanupPolicyValue(DELETE);
       .setCleanupPolicyValue(DELETE);
-  private static final Topic TOPIC_FOR_UPDATE = new Topic()
+  private static final Topic TOPIC_TO_UPDATE = new Topic()
       .setName("topic-to-update-" + randomAlphabetic(5))
       .setName("topic-to-update-" + randomAlphabetic(5))
+      .setNumberOfPartitions(1)
       .setCleanupPolicyValue(COMPACT)
       .setCleanupPolicyValue(COMPACT)
       .setTimeToRetainData("604800001")
       .setTimeToRetainData("604800001")
       .setMaxSizeOnDisk(SIZE_20_GB)
       .setMaxSizeOnDisk(SIZE_20_GB)
       .setMaxMessageBytes("1000020")
       .setMaxMessageBytes("1000020")
       .setMessageKey(fileToString(System.getProperty("user.dir") + "/src/test/resources/producedkey.txt"))
       .setMessageKey(fileToString(System.getProperty("user.dir") + "/src/test/resources/producedkey.txt"))
       .setMessageContent(fileToString(System.getProperty("user.dir") + "/src/test/resources/testData.txt"));
       .setMessageContent(fileToString(System.getProperty("user.dir") + "/src/test/resources/testData.txt"));
-
+  private static final Topic TOPIC_TO_CHECK_SETTINGS = new Topic()
+      .setName("new-topic-" + randomAlphabetic(5))
+      .setNumberOfPartitions(1)
+      .setMaxMessageBytes("1000012")
+      .setMaxSizeOnDisk(NOT_SET);
   private static final Topic TOPIC_FOR_DELETE = new Topic().setName("topic-to-delete-" + randomAlphabetic(5));
   private static final Topic TOPIC_FOR_DELETE = new Topic().setName("topic-to-delete-" + randomAlphabetic(5));
   private static final List<Topic> TOPIC_LIST = new ArrayList<>();
   private static final List<Topic> TOPIC_LIST = new ArrayList<>();
 
 
   @BeforeAll
   @BeforeAll
   public void beforeAll() {
   public void beforeAll() {
-    TOPIC_LIST.addAll(List.of(TOPIC_FOR_UPDATE, TOPIC_FOR_DELETE));
+    TOPIC_LIST.addAll(List.of(TOPIC_TO_UPDATE, TOPIC_FOR_DELETE));
     TOPIC_LIST.forEach(topic -> apiHelper.createTopic(CLUSTER_NAME, topic.getName()));
     TOPIC_LIST.forEach(topic -> apiHelper.createTopic(CLUSTER_NAME, topic.getName()));
   }
   }
 
 
@@ -70,10 +80,8 @@ public class TopicsTests extends BaseTest {
   @Test
   @Test
   @Order(1)
   @Order(1)
   public void createTopic() {
   public void createTopic() {
-    naviSideBar
-        .openSideMenu(TOPICS);
+    navigateToTopics();
     topicsList
     topicsList
-        .waitUntilScreenReady()
         .clickAddTopicBtn();
         .clickAddTopicBtn();
     topicCreateEditForm
     topicCreateEditForm
         .waitUntilScreenReady()
         .waitUntilScreenReady()
@@ -81,13 +89,7 @@ public class TopicsTests extends BaseTest {
         .setNumberOfPartitions(TOPIC_TO_CREATE.getNumberOfPartitions())
         .setNumberOfPartitions(TOPIC_TO_CREATE.getNumberOfPartitions())
         .selectCleanupPolicy(TOPIC_TO_CREATE.getCleanupPolicyValue())
         .selectCleanupPolicy(TOPIC_TO_CREATE.getCleanupPolicyValue())
         .clickCreateTopicBtn();
         .clickCreateTopicBtn();
-    topicDetails
-        .waitUntilScreenReady();
-    naviSideBar
-        .openSideMenu(TOPICS);
-    topicsList
-        .waitUntilScreenReady()
-        .openTopic(TOPIC_TO_CREATE.getName());
+    navigateToTopicsAndOpenDetails(TOPIC_TO_CREATE.getName());
     SoftAssertions softly = new SoftAssertions();
     SoftAssertions softly = new SoftAssertions();
     softly.assertThat(topicDetails.isTopicHeaderVisible(TOPIC_TO_CREATE.getName())).as("isTopicHeaderVisible()")
     softly.assertThat(topicDetails.isTopicHeaderVisible(TOPIC_TO_CREATE.getName())).as("isTopicHeaderVisible()")
         .isTrue();
         .isTrue();
@@ -96,10 +98,7 @@ public class TopicsTests extends BaseTest {
     softly.assertThat(topicDetails.getPartitions()).as("getPartitions()")
     softly.assertThat(topicDetails.getPartitions()).as("getPartitions()")
         .isEqualTo(TOPIC_TO_CREATE.getNumberOfPartitions());
         .isEqualTo(TOPIC_TO_CREATE.getNumberOfPartitions());
     softly.assertAll();
     softly.assertAll();
-    naviSideBar
-        .openSideMenu(TOPICS);
-    topicsList
-        .waitUntilScreenReady();
+    navigateToTopics();
     Assertions.assertTrue(topicsList.isTopicVisible(TOPIC_TO_CREATE.getName()), "isTopicVisible");
     Assertions.assertTrue(topicsList.isTopicVisible(TOPIC_TO_CREATE.getName()), "isTopicVisible");
     TOPIC_LIST.add(TOPIC_TO_CREATE);
     TOPIC_LIST.add(TOPIC_TO_CREATE);
   }
   }
@@ -110,20 +109,16 @@ public class TopicsTests extends BaseTest {
   @CaseId(7)
   @CaseId(7)
   @Test
   @Test
   @Order(2)
   @Order(2)
-  void checkAvailableOperations(){
-    String processingTopic = "my_ksql_1ksql_processing_log";
-    String confluentTopic = "_confluent-ksql-my_ksql_1_command_topic";
-    naviSideBar
-        .openSideMenu(TOPICS);
+  void checkAvailableOperations() {
+    navigateToTopics();
     topicsList
     topicsList
-        .waitUntilScreenReady()
-        .selectCheckboxByName(processingTopic);
-    topicsList.getActionButtons().
-        forEach(element -> assertThat(element.is(Condition.enabled))
-            .as(element.getSearchCriteria() + " isEnabled()").isTrue());
+        .getTopicItem("my_ksql_1ksql_processing_log")
+        .selectItem(true);
+    verifyElementsCondition(topicsList.getActionButtons(),Condition.enabled);
     topicsList
     topicsList
-        .selectCheckboxByName(confluentTopic);
-    Assertions.assertFalse(topicsList.isCopySelectedTopicBtnEnabled(),"isCopySelectedTopicBtnEnabled()");
+        .getTopicItem("_confluent-ksql-my_ksql_1_command_topic")
+        .selectItem(true);
+    Assertions.assertFalse(topicsList.isCopySelectedTopicBtnEnabled(), "isCopySelectedTopicBtnEnabled()");
   }
   }
 
 
   @Disabled()
   @Disabled()
@@ -135,43 +130,33 @@ public class TopicsTests extends BaseTest {
   @Test
   @Test
   @Order(3)
   @Order(3)
   public void updateTopic() {
   public void updateTopic() {
-    naviSideBar
-        .openSideMenu(TOPICS);
-    topicsList
-        .waitUntilScreenReady()
-        .openTopic(TOPIC_FOR_UPDATE.getName());
+    navigateToTopicsAndOpenDetails(TOPIC_TO_UPDATE.getName());
     topicDetails
     topicDetails
-        .waitUntilScreenReady()
         .openDotMenu()
         .openDotMenu()
         .clickEditSettingsMenu();
         .clickEditSettingsMenu();
     topicCreateEditForm
     topicCreateEditForm
         .waitUntilScreenReady()
         .waitUntilScreenReady()
-        .selectCleanupPolicy((TOPIC_FOR_UPDATE.getCleanupPolicyValue()))
+        .selectCleanupPolicy((TOPIC_TO_UPDATE.getCleanupPolicyValue()))
         .setMinInsyncReplicas(10)
         .setMinInsyncReplicas(10)
-        .setTimeToRetainDataInMs(TOPIC_FOR_UPDATE.getTimeToRetainData())
-        .setMaxSizeOnDiskInGB(TOPIC_FOR_UPDATE.getMaxSizeOnDisk())
-        .setMaxMessageBytes(TOPIC_FOR_UPDATE.getMaxMessageBytes())
+        .setTimeToRetainDataInMs(TOPIC_TO_UPDATE.getTimeToRetainData())
+        .setMaxSizeOnDiskInGB(TOPIC_TO_UPDATE.getMaxSizeOnDisk())
+        .setMaxMessageBytes(TOPIC_TO_UPDATE.getMaxMessageBytes())
         .clickCreateTopicBtn();
         .clickCreateTopicBtn();
     topicDetails
     topicDetails
         .waitUntilScreenReady();
         .waitUntilScreenReady();
-    naviSideBar
-        .openSideMenu(TOPICS);
-    topicsList
-        .waitUntilScreenReady()
-        .openTopic(TOPIC_FOR_UPDATE.getName());
+    navigateToTopicsAndOpenDetails(TOPIC_TO_UPDATE.getName());
     topicDetails
     topicDetails
-        .waitUntilScreenReady()
         .openDotMenu()
         .openDotMenu()
         .clickEditSettingsMenu();
         .clickEditSettingsMenu();
     SoftAssertions softly = new SoftAssertions();
     SoftAssertions softly = new SoftAssertions();
     softly.assertThat(topicCreateEditForm.getCleanupPolicy()).as("getCleanupPolicy()")
     softly.assertThat(topicCreateEditForm.getCleanupPolicy()).as("getCleanupPolicy()")
-        .isEqualTo(TOPIC_FOR_UPDATE.getCleanupPolicyValue().getVisibleText());
+        .isEqualTo(TOPIC_TO_UPDATE.getCleanupPolicyValue().getVisibleText());
     softly.assertThat(topicCreateEditForm.getTimeToRetain()).as("getTimeToRetain()")
     softly.assertThat(topicCreateEditForm.getTimeToRetain()).as("getTimeToRetain()")
-        .isEqualTo(TOPIC_FOR_UPDATE.getTimeToRetainData());
+        .isEqualTo(TOPIC_TO_UPDATE.getTimeToRetainData());
     softly.assertThat(topicCreateEditForm.getMaxSizeOnDisk()).as("getMaxSizeOnDisk()")
     softly.assertThat(topicCreateEditForm.getMaxSizeOnDisk()).as("getMaxSizeOnDisk()")
-        .isEqualTo(TOPIC_FOR_UPDATE.getMaxSizeOnDisk().getVisibleText());
+        .isEqualTo(TOPIC_TO_UPDATE.getMaxSizeOnDisk().getVisibleText());
     softly.assertThat(topicCreateEditForm.getMaxMessageBytes()).as("getMaxMessageBytes()")
     softly.assertThat(topicCreateEditForm.getMaxMessageBytes()).as("getMaxMessageBytes()")
-        .isEqualTo(TOPIC_FOR_UPDATE.getMaxMessageBytes());
+        .isEqualTo(TOPIC_TO_UPDATE.getMaxMessageBytes());
     softly.assertAll();
     softly.assertAll();
   }
   }
 
 
@@ -182,20 +167,12 @@ public class TopicsTests extends BaseTest {
   @Test
   @Test
   @Order(4)
   @Order(4)
   public void deleteTopic() {
   public void deleteTopic() {
-    naviSideBar
-        .openSideMenu(TOPICS);
-    topicsList
-        .waitUntilScreenReady()
-        .openTopic(TOPIC_FOR_DELETE.getName());
+    navigateToTopicsAndOpenDetails(TOPIC_FOR_DELETE.getName());
     topicDetails
     topicDetails
-        .waitUntilScreenReady()
         .openDotMenu()
         .openDotMenu()
         .clickDeleteTopicMenu()
         .clickDeleteTopicMenu()
-        .clickConfirmDeleteBtn();
-    naviSideBar
-        .openSideMenu(TOPICS);
-    topicsList
-        .waitUntilScreenReady();
+        .clickConfirmBtnMdl();
+    navigateToTopics();
     Assertions.assertFalse(topicsList.isTopicVisible(TOPIC_FOR_DELETE.getName()), "isTopicVisible");
     Assertions.assertFalse(topicsList.isTopicVisible(TOPIC_FOR_DELETE.getName()), "isTopicVisible");
     TOPIC_LIST.remove(TOPIC_FOR_DELETE);
     TOPIC_LIST.remove(TOPIC_FOR_DELETE);
   }
   }
@@ -209,13 +186,8 @@ public class TopicsTests extends BaseTest {
   void redirectToConsumerFromTopic() {
   void redirectToConsumerFromTopic() {
     String topicName = "source-activities";
     String topicName = "source-activities";
     String consumerGroupId = "connect-sink_postgres_activities";
     String consumerGroupId = "connect-sink_postgres_activities";
-    naviSideBar
-        .openSideMenu(TOPICS);
-    topicsList
-        .waitUntilScreenReady()
-        .openTopic(topicName);
+    navigateToTopicsAndOpenDetails(topicName);
     topicDetails
     topicDetails
-        .waitUntilScreenReady()
         .openDetailsTab(TopicDetails.TopicMenu.CONSUMERS)
         .openDetailsTab(TopicDetails.TopicMenu.CONSUMERS)
         .openConsumerGroup(consumerGroupId);
         .openConsumerGroup(consumerGroupId);
     consumersDetails
     consumersDetails
@@ -233,10 +205,8 @@ public class TopicsTests extends BaseTest {
   @Test
   @Test
   @Order(6)
   @Order(6)
   void checkTopicCreatePossibility() {
   void checkTopicCreatePossibility() {
-    naviSideBar
-        .openSideMenu(TOPICS);
+    navigateToTopics();
     topicsList
     topicsList
-        .waitUntilScreenReady()
         .clickAddTopicBtn();
         .clickAddTopicBtn();
     topicCreateEditForm
     topicCreateEditForm
         .waitUntilScreenReady();
         .waitUntilScreenReady();
@@ -253,15 +223,49 @@ public class TopicsTests extends BaseTest {
     assertThat(topicCreateEditForm.isCreateTopicButtonEnabled()).as("isCreateTopicButtonEnabled()").isTrue();
     assertThat(topicCreateEditForm.isCreateTopicButtonEnabled()).as("isCreateTopicButtonEnabled()").isTrue();
   }
   }
 
 
+  @DisplayName("Checking 'Time to retain data (in ms)' custom value with editing Topic's settings")
+  @Suite(suiteId = SUITE_ID, title = SUITE_TITLE)
+  @AutomationStatus(status = Status.AUTOMATED)
+  @CaseId(266)
+  @Test
+  @Order(7)
+  void checkTimeToRetainDataCustomValueWithEditingTopic() {
+    Topic topicToRetainData = new Topic()
+        .setName("topic-to-retain-data-" + randomAlphabetic(5))
+        .setTimeToRetainData("86400000");
+    navigateToTopics();
+    topicsList
+        .clickAddTopicBtn();
+    topicCreateEditForm
+        .waitUntilScreenReady()
+        .setTopicName(topicToRetainData.getName())
+        .setNumberOfPartitions(1)
+        .setTimeToRetainDataInMs("604800000");
+    assertThat(topicCreateEditForm.getTimeToRetain()).as("getTimeToRetain()").isEqualTo("604800000");
+    topicCreateEditForm
+        .setTimeToRetainDataInMs(topicToRetainData.getTimeToRetainData())
+        .clickCreateTopicBtn();
+    topicDetails
+        .waitUntilScreenReady()
+        .openDotMenu()
+        .clickEditSettingsMenu();
+    assertThat(topicCreateEditForm.getTimeToRetain()).as("getTimeToRetain()")
+        .isEqualTo(topicToRetainData.getTimeToRetainData());
+    topicDetails
+        .openDetailsTab(SETTINGS);
+    assertThat(topicDetails.getSettingsGridValueByKey("retention.ms")).as("getSettingsGridValueByKey()")
+        .isEqualTo(topicToRetainData.getTimeToRetainData());
+    TOPIC_LIST.add(topicToRetainData);
+  }
+
   @DisplayName("Checking requiredness of Custom parameters within 'Create new Topic'")
   @DisplayName("Checking requiredness of Custom parameters within 'Create new Topic'")
   @Suite(suiteId = SUITE_ID, title = SUITE_TITLE)
   @Suite(suiteId = SUITE_ID, title = SUITE_TITLE)
   @AutomationStatus(status = Status.AUTOMATED)
   @AutomationStatus(status = Status.AUTOMATED)
   @CaseId(6)
   @CaseId(6)
   @Test
   @Test
-  @Order(7)
+  @Order(8)
   void checkCustomParametersWithinCreateNewTopic() {
   void checkCustomParametersWithinCreateNewTopic() {
-    naviSideBar
-        .openSideMenu(TOPICS);
+    navigateToTopics();
     topicsList
     topicsList
         .waitUntilScreenReady()
         .waitUntilScreenReady()
         .clickAddTopicBtn();
         .clickAddTopicBtn();
@@ -283,20 +287,11 @@ public class TopicsTests extends BaseTest {
   @AutomationStatus(status = Status.AUTOMATED)
   @AutomationStatus(status = Status.AUTOMATED)
   @CaseId(2)
   @CaseId(2)
   @Test
   @Test
-  @Order(8)
+  @Order(9)
   void checkTopicListElements() {
   void checkTopicListElements() {
-    naviSideBar
-        .openSideMenu(TOPICS);
-    topicsList
-        .waitUntilScreenReady();
-    SoftAssertions softly = new SoftAssertions();
-    topicsList.getAllVisibleElements().forEach(
-        element -> softly.assertThat(element.is(Condition.visible)).as(element.getSearchCriteria() + " isVisible()")
-            .isTrue());
-    topicsList.getAllEnabledElements().forEach(
-        element -> softly.assertThat(element.is(Condition.enabled)).as(element.getSearchCriteria() + " isEnabled()")
-            .isTrue());
-    softly.assertAll();
+    navigateToTopics();
+    verifyElementsCondition(topicsList.getAllVisibleElements(), Condition.visible);
+    verifyElementsCondition(topicsList.getAllEnabledElements(), Condition.enabled);
   }
   }
 
 
   @DisplayName("Filter adding within Topic")
   @DisplayName("Filter adding within Topic")
@@ -304,39 +299,177 @@ public class TopicsTests extends BaseTest {
   @AutomationStatus(status = Status.AUTOMATED)
   @AutomationStatus(status = Status.AUTOMATED)
   @CaseId(12)
   @CaseId(12)
   @Test
   @Test
-  @Order(9)
+  @Order(10)
   void addingNewFilterWithinTopic() {
   void addingNewFilterWithinTopic() {
-    String topicName = "_schemas";
-    String filterName = "123ABC";
-    naviSideBar
-        .openSideMenu(TOPICS);
-    topicsList
-        .waitUntilScreenReady()
-        .openTopic(topicName);
+    String filterName = randomAlphabetic(5);
+    navigateToTopicsAndOpenDetails("_schemas");
     topicDetails
     topicDetails
-        .openDetailsTab(TopicDetails.TopicMenu.MESSAGES)
+        .openDetailsTab(MESSAGES)
         .clickMessagesAddFiltersBtn()
         .clickMessagesAddFiltersBtn()
         .waitUntilAddFiltersMdlVisible();
         .waitUntilAddFiltersMdlVisible();
-    SoftAssertions softly = new SoftAssertions();
-    topicDetails.getAllAddFilterModalVisibleElements().forEach(element ->
-        softly.assertThat(element.is(Condition.visible))
-            .as(element.getSearchCriteria() + " isVisible()").isTrue());
-    topicDetails.getAllAddFilterModalEnabledElements().forEach(element ->
-        softly.assertThat(element.is(Condition.enabled))
-            .as(element.getSearchCriteria() + " isEnabled()").isTrue());
-    topicDetails.getAllAddFilterModalDisabledElements().forEach(element ->
-        softly.assertThat(element.is(Condition.enabled))
-            .as(element.getSearchCriteria() + " isEnabled()").isFalse());
-    softly.assertThat(topicDetails.isSaveThisFilterCheckBoxSelected()).as("isSaveThisFilterCheckBoxSelected()")
+    verifyElementsCondition(topicDetails.getAllAddFilterModalVisibleElements(), Condition.visible);
+    verifyElementsCondition(topicDetails.getAllAddFilterModalEnabledElements(), Condition.enabled);
+    verifyElementsCondition(topicDetails.getAllAddFilterModalDisabledElements(), Condition.disabled);
+    assertThat(topicDetails.isSaveThisFilterCheckBoxSelected()).as("isSaveThisFilterCheckBoxSelected()")
         .isFalse();
         .isFalse();
-    softly.assertAll();
     topicDetails
     topicDetails
         .setFilterCodeFieldAddFilterMdl(filterName);
         .setFilterCodeFieldAddFilterMdl(filterName);
-    assertThat(topicDetails.isAddFilterBtnAddFilterMdlEnabled()).as("isMessagesAddFilterTabAddFilterBtnEnabled()")
+    assertThat(topicDetails.isAddFilterBtnAddFilterMdlEnabled()).as("isAddFilterBtnAddFilterMdlEnabled()")
+        .isTrue();
+    topicDetails.clickAddFilterBtnAndCloseMdl(true);
+    assertThat(topicDetails.isActiveFilterVisible(filterName)).as("isActiveFilterVisible()")
+        .isTrue();
+  }
+
+  @DisplayName("Checking filter saving within Messages/Topic profile/Saved Filters")
+  @Suite(suiteId = SUITE_ID, title = SUITE_TITLE)
+  @AutomationStatus(status = Status.AUTOMATED)
+  @CaseId(13)
+  @Test
+  @Order(11)
+  void checkFilterSavingWithinSavedFilters() {
+    String displayName = randomAlphabetic(5);
+    navigateToTopicsAndOpenDetails("my_ksql_1ksql_processing_log");
+    topicDetails
+        .openDetailsTab(MESSAGES)
+        .clickMessagesAddFiltersBtn()
+        .waitUntilAddFiltersMdlVisible()
+        .setFilterCodeFieldAddFilterMdl(randomAlphabetic(4))
+        .selectSaveThisFilterCheckboxMdl(true)
+        .setDisplayNameFldAddFilterMdl(displayName);
+    assertThat(topicDetails.isAddFilterBtnAddFilterMdlEnabled()).as("isAddFilterBtnAddFilterMdlEnabled()")
         .isTrue();
         .isTrue();
-    topicDetails.clickAddFilterBtnAddFilterMdl();
-    assertThat(topicDetails.getFilterName()).as("isFilterNameVisible(filterName)")
-        .isEqualTo(filterName);
+    topicDetails
+        .clickAddFilterBtnAndCloseMdl(false)
+        .openSavedFiltersListMdl();
+    assertThat(topicDetails.isFilterVisibleAtSavedFiltersMdl(displayName))
+        .as("isFilterVisibleAtSavedFiltersMdl()").isTrue();
+  }
+
+  @DisplayName("Checking applying saved filter within Topic/Messages")
+  @Suite(suiteId = SUITE_ID, title = SUITE_TITLE)
+  @AutomationStatus(status = Status.AUTOMATED)
+  @CaseId(14)
+  @Test
+  @Order(12)
+  void checkingApplyingSavedFilterWithinTopicMessages() {
+    String displayName = randomAlphabetic(5);
+    navigateToTopicsAndOpenDetails("my_ksql_1ksql_processing_log");
+    topicDetails
+        .openDetailsTab(MESSAGES)
+        .clickMessagesAddFiltersBtn()
+        .waitUntilAddFiltersMdlVisible()
+        .setFilterCodeFieldAddFilterMdl(randomAlphabetic(4))
+        .selectSaveThisFilterCheckboxMdl(true)
+        .setDisplayNameFldAddFilterMdl(displayName)
+        .clickAddFilterBtnAndCloseMdl(false)
+        .openSavedFiltersListMdl()
+        .selectFilterAtSavedFiltersMdl(displayName)
+        .clickSelectFilterBtnAtSavedFiltersMdl();
+    assertThat(topicDetails.isActiveFilterVisible(displayName))
+        .as("isActiveFilterVisible()").isTrue();
+  }
+
+  @DisplayName("Checking 'Show Internal Topics' toggle functionality within 'All Topics' page")
+  @Suite(suiteId = SUITE_ID, title = SUITE_TITLE)
+  @AutomationStatus(status = Status.AUTOMATED)
+  @CaseId(11)
+  @Test
+  @Order(13)
+  void checkShowInternalTopicsButtonFunctionality(){
+    navigateToTopics();
+    SoftAssertions softly = new SoftAssertions();
+    softly.assertThat(topicsList.isShowInternalRadioBtnSelected()).as("isInternalRadioBtnSelected()").isTrue();
+    softly.assertThat(topicsList.getInternalTopics()).as("getInternalTopics()").size().isGreaterThan(0);
+    softly.assertThat(topicsList.getNonInternalTopics()).as("getNonInternalTopics()").size().isGreaterThan(0);
+    softly.assertAll();
+    topicsList
+        .setShowInternalRadioButton(false);
+    softly.assertThat(topicsList.getInternalTopics()).as("getInternalTopics()").size().isEqualTo(0);
+    softly.assertThat(topicsList.getNonInternalTopics()).as("getNonInternalTopics()").size().isGreaterThan(0);
+    softly.assertAll();
+  }
+
+  @DisplayName("Checking Topics settings to make sure retention.bytes is right according to Max size on disk in GB selected value")
+  @Suite(suiteId = SUITE_ID, title = SUITE_TITLE)
+  @AutomationStatus(status = Status.AUTOMATED)
+  @CaseId(56)
+  @Test
+  void checkRetentionBytesAccordingToMaxSizeOnDisk(){
+    navigateToTopics();
+    topicsList
+        .clickAddTopicBtn();
+    topicCreateEditForm
+        .waitUntilScreenReady()
+        .setTopicName(TOPIC_TO_CHECK_SETTINGS.getName())
+        .setNumberOfPartitions(TOPIC_TO_CHECK_SETTINGS.getNumberOfPartitions())
+        .setMaxMessageBytes(TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes())
+        .clickCreateTopicBtn();
+    topicDetails
+        .waitUntilScreenReady();
+    TOPIC_LIST.add(TOPIC_TO_CHECK_SETTINGS);
+    topicDetails
+        .openDetailsTab(SETTINGS);
+    topicSettingsTab
+        .waitUntilScreenReady();
+    SoftAssertions softly = new SoftAssertions();
+    softly.assertThat(topicSettingsTab.getValueByKey("retention.bytes"))
+        .as("getValueOfKey(retention.bytes)").isEqualTo(TOPIC_TO_CHECK_SETTINGS.getMaxSizeOnDisk().getOptionValue());
+    softly.assertThat(topicSettingsTab.getValueByKey("max.message.bytes"))
+        .as("getValueOfKey(max.message.bytes)").isEqualTo(TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes());
+    softly.assertAll();
+    TOPIC_TO_CHECK_SETTINGS
+        .setMaxSizeOnDisk(SIZE_1_GB)
+        .setMaxMessageBytes("1000056");
+    topicDetails
+        .openDotMenu()
+        .clickEditSettingsMenu();
+    topicCreateEditForm
+        .waitUntilScreenReady()
+        .setMaxSizeOnDiskInGB(TOPIC_TO_CHECK_SETTINGS.getMaxSizeOnDisk())
+        .setMaxMessageBytes(TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes())
+        .clickCreateTopicBtn();
+    topicDetails
+        .waitUntilScreenReady()
+        .openDetailsTab(SETTINGS);
+    topicSettingsTab
+        .waitUntilScreenReady();
+    softly.assertThat(topicSettingsTab.getValueByKey("retention.bytes"))
+        .as("getValueOfKey(retention.bytes)").isEqualTo(TOPIC_TO_CHECK_SETTINGS.getMaxSizeOnDisk().getOptionValue());
+    softly.assertThat(topicSettingsTab.getValueByKey("max.message.bytes"))
+        .as("getValueOfKey(max.message.bytes)").isEqualTo(TOPIC_TO_CHECK_SETTINGS.getMaxMessageBytes());
+    softly.assertAll();
+  }
+
+  @DisplayName("TopicTests.recreateTopicFromTopicProfile : Recreate topic from topic profile")
+  @Suite(suiteId = SUITE_ID, title = SUITE_TITLE)
+  @AutomationStatus(status = Status.AUTOMATED)
+  @CaseId(247)
+  @Test
+  void recreateTopicFromTopicProfile(){
+    Topic topicToRecreate = new Topic()
+        .setName("topic-to-recreate-" + randomAlphabetic(5))
+        .setNumberOfPartitions(1);
+    navigateToTopics();
+    topicsList
+        .clickAddTopicBtn();
+    topicCreateEditForm
+        .waitUntilScreenReady()
+        .setTopicName(topicToRecreate.getName())
+        .setNumberOfPartitions(topicToRecreate.getNumberOfPartitions())
+        .clickCreateTopicBtn();
+    topicDetails
+        .waitUntilScreenReady();
+    TOPIC_LIST.add(topicToRecreate);
+    topicDetails
+        .openDotMenu()
+        .clickRecreateTopicMenu();
+    assertThat(topicDetails.isConfirmationMdlVisible()).as("isConfirmationMdlVisible()").isTrue();
+    topicDetails
+        .clickConfirmBtnMdl();
+    assertThat(topicDetails.isAlertWithMessageVisible(SUCCESS,
+        String.format("Topic %s successfully recreated!", topicToRecreate.getName())))
+        .as("isAlertWithMessageVisible()").isTrue();
   }
   }
 
 
   @AfterAll
   @AfterAll

+ 0 - 1
kafka-ui-react-app/package.json

@@ -22,7 +22,6 @@
     "ajv": "^8.6.3",
     "ajv": "^8.6.3",
     "babel-jest": "^29.0.3",
     "babel-jest": "^29.0.3",
     "classnames": "^2.2.6",
     "classnames": "^2.2.6",
-    "dayjs": "^1.11.2",
     "fetch-mock": "^9.11.0",
     "fetch-mock": "^9.11.0",
     "jest": "^29.0.3",
     "jest": "^29.0.3",
     "jest-watch-typeahead": "^2.0.0",
     "jest-watch-typeahead": "^2.0.0",

+ 0 - 6
kafka-ui-react-app/pnpm-lock.yaml

@@ -38,7 +38,6 @@ specifiers:
   ajv: ^8.6.3
   ajv: ^8.6.3
   babel-jest: ^29.0.3
   babel-jest: ^29.0.3
   classnames: ^2.2.6
   classnames: ^2.2.6
-  dayjs: ^1.11.2
   dotenv: ^16.0.1
   dotenv: ^16.0.1
   eslint: ^8.3.0
   eslint: ^8.3.0
   eslint-config-airbnb: ^19.0.4
   eslint-config-airbnb: ^19.0.4
@@ -110,7 +109,6 @@ dependencies:
   ajv: 8.8.2
   ajv: 8.8.2
   babel-jest: 29.0.3_@babel+core@7.18.2
   babel-jest: 29.0.3_@babel+core@7.18.2
   classnames: 2.3.1
   classnames: 2.3.1
-  dayjs: 1.11.3
   fetch-mock: 9.11.0
   fetch-mock: 9.11.0
   jest: 29.0.3_yqiaopbgmqcuvx27p5xxvum6wm
   jest: 29.0.3_yqiaopbgmqcuvx27p5xxvum6wm
   jest-watch-typeahead: 2.0.0_jest@29.0.3
   jest-watch-typeahead: 2.0.0_jest@29.0.3
@@ -4686,10 +4684,6 @@ packages:
     resolution: {integrity: sha512-sj+J0Mo2p2X1e306MHq282WS4/A8Pz/95GIFcsPNMPMZVI3EUrAdSv90al1k+p74WGLCruMXk23bfEDZa71X9Q==}
     resolution: {integrity: sha512-sj+J0Mo2p2X1e306MHq282WS4/A8Pz/95GIFcsPNMPMZVI3EUrAdSv90al1k+p74WGLCruMXk23bfEDZa71X9Q==}
     engines: {node: '>=0.11'}
     engines: {node: '>=0.11'}
 
 
-  /dayjs/1.11.3:
-    resolution: {integrity: sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A==}
-    dev: false
-
   /debug/2.6.9:
   /debug/2.6.9:
     resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
     resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
     peerDependencies:
     peerDependencies:

+ 1 - 244
kafka-ui-react-app/src/components/App.styled.ts

@@ -1,9 +1,4 @@
-import styled, { css } from 'styled-components';
-import { Link } from 'react-router-dom';
-
-import { Button } from './common/Button/Button';
-import GitIcon from './common/Icons/GitIcon';
-import DiscordIcon from './common/Icons/DiscordIcon';
+import styled from 'styled-components';
 
 
 export const Layout = styled.div`
 export const Layout = styled.div`
   min-width: 1200px;
   min-width: 1200px;
@@ -12,241 +7,3 @@ export const Layout = styled.div`
     min-width: initial;
     min-width: initial;
   }
   }
 `;
 `;
-
-export const Container = styled.main(
-  ({ theme }) => css`
-    margin-top: ${theme.layout.navBarHeight};
-    margin-left: ${theme.layout.navBarWidth};
-    position: relative;
-    padding-bottom: 30px;
-    z-index: 20;
-    max-width: calc(100vw - ${theme.layout.navBarWidth});
-    @media screen and (max-width: 1023px) {
-      margin-left: initial;
-      max-width: 100vw;
-    }
-  `
-);
-
-export const Sidebar = styled.div<{ $visible: boolean }>(
-  ({ theme, $visible }) => css`
-    width: ${theme.layout.navBarWidth};
-    display: flex;
-    flex-direction: column;
-    border-right: 1px solid ${theme.layout.stuffBorderColor};
-    position: fixed;
-    top: ${theme.layout.navBarHeight};
-    left: 0;
-    bottom: 0;
-    padding: 8px 16px;
-    overflow-y: scroll;
-    transition: width 0.25s, opacity 0.25s, transform 0.25s,
-      -webkit-transform 0.25s;
-    background: ${theme.menu.backgroundColor.normal};
-    @media screen and (max-width: 1023px) {
-      ${$visible &&
-      css`
-        transform: translate3d(${theme.layout.navBarWidth}, 0, 0);
-      `};
-      left: -${theme.layout.navBarWidth};
-      z-index: 100;
-    }
-
-    &::-webkit-scrollbar {
-      width: 8px;
-    }
-
-    &::-webkit-scrollbar-track {
-      background-color: ${theme.scrollbar.trackColor.normal};
-    }
-
-    &::-webkit-scrollbar-thumb {
-      width: 8px;
-      background-color: ${theme.scrollbar.thumbColor.normal};
-      border-radius: 4px;
-    }
-
-    &:hover::-webkit-scrollbar-thumb {
-      background: ${theme.scrollbar.thumbColor.active};
-    }
-
-    &:hover::-webkit-scrollbar-track {
-      background-color: ${theme.scrollbar.trackColor.active};
-    }
-  `
-);
-
-export const Overlay = styled.div<{ $visible: boolean }>(
-  ({ theme, $visible }) => css`
-    height: calc(100vh - ${theme.layout.navBarHeight});
-    z-index: 99;
-    visibility: hidden;
-    opacity: 0;
-    -webkit-transition: all 0.5s ease;
-    transition: all 0.5s ease;
-    left: 0;
-    position: absolute;
-    top: 0;
-    ${$visible &&
-    css`
-      @media screen and (max-width: 1023px) {
-        bottom: 0;
-        right: 0;
-        visibility: visible;
-        opacity: 0.7;
-        background-color: ${theme.layout.overlay.backgroundColor};
-      }
-    `}
-  `
-);
-
-export const Navbar = styled.nav(
-  ({ theme }) => css`
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    border-bottom: 1px solid ${theme.layout.stuffBorderColor};
-    position: fixed;
-    top: 0;
-    left: 0;
-    right: 0;
-    z-index: 30;
-    background-color: ${theme.menu.backgroundColor.normal};
-    min-height: 3.25rem;
-  `
-);
-
-export const NavbarBrand = styled.div`
-  display: flex;
-  justify-content: flex-end;
-  align-items: center !important;
-  flex-shrink: 0;
-  min-height: 3.25rem;
-`;
-
-export const SocialLink = styled.a(
-  ({ theme: { layout, icons } }) => css`
-    display: block;
-    margin-top: 5px;
-    cursor: pointer;
-    fill: ${layout.socialLink.color};
-
-    &:hover {
-      ${DiscordIcon} {
-        fill: ${icons.discord.hover};
-      }
-      ${GitIcon} {
-        fill: ${icons.git.hover};
-      }
-    }
-    &:active {
-      ${DiscordIcon} {
-        fill: ${icons.discord.active};
-      }
-      ${GitIcon} {
-        fill: ${icons.git.active};
-      }
-    }
-  `
-);
-
-export const NavbarSocial = styled.div`
-  display: flex;
-  align-items: center;
-  gap: 10px;
-  margin: 10px;
-`;
-
-export const NavbarItem = styled.div`
-  display: flex;
-  position: relative;
-  flex-grow: 0;
-  flex-shrink: 0;
-  align-items: center;
-  line-height: 1.5;
-  padding: 0.5rem 0.75rem;
-`;
-
-export const NavbarBurger = styled.div(
-  ({ theme }) => css`
-    display: block;
-    position: relative;
-    cursor: pointer;
-    height: 3.25rem;
-    width: 3.25rem;
-    margin: 0;
-    padding: 0;
-
-    &:hover {
-      background-color: ${theme.menu.backgroundColor.hover};
-    }
-
-    @media screen and (min-width: 1024px) {
-      display: none;
-    }
-  `
-);
-
-export const Span = styled.span(
-  ({ theme }) => css`
-    display: block;
-    position: absolute;
-    background: ${theme.menu.color.active};
-    height: 1px;
-    left: calc(50% - 8px);
-    transform-origin: center;
-    transition-duration: 86ms;
-    transition-property: background-color, opacity, transform, -webkit-transform;
-    transition-timing-function: ease-out;
-    width: 16px;
-
-    &:first-child {
-      top: calc(50% - 6px);
-    }
-    &:nth-child(2) {
-      top: calc(50% - 1px);
-    }
-    &:nth-child(3) {
-      top: calc(50% + 4px);
-    }
-  `
-);
-
-export const Hyperlink = styled(Link)(
-  ({ theme }) => css`
-    position: relative;
-
-    display: flex;
-    flex-grow: 0;
-    flex-shrink: 0;
-    align-items: center;
-    gap: 8px;
-
-    margin: 0;
-    padding: 0.5rem 0.75rem;
-
-    font-family: Inter, sans-serif;
-    font-style: normal;
-    font-weight: bold;
-    font-size: 12px;
-    line-height: 16px;
-    color: ${theme.menu.color.active};
-    text-decoration: none;
-    word-break: break-word;
-    cursor: pointer;
-  `
-);
-
-export const LogoutButton = styled(Button)(
-  ({ theme }) => css`
-    color: ${theme.button.primary.invertedColors.normal};
-    background: none !important;
-    padding: 0 8px;
-  `
-);
-
-export const LogoutLink = styled.a(
-  () => css`
-    margin-right: 2px;
-  `
-);

+ 41 - 106
kafka-ui-react-app/src/components/App.tsx

@@ -1,16 +1,14 @@
-import React, { Suspense, useCallback } from 'react';
-import { Routes, Route, useLocation, Navigate } from 'react-router-dom';
+import React, { Suspense } from 'react';
+import { Routes, Route, Navigate } from 'react-router-dom';
 import {
 import {
   accessErrorPage,
   accessErrorPage,
   clusterPath,
   clusterPath,
   errorPage,
   errorPage,
   getNonExactPath,
   getNonExactPath,
 } from 'lib/paths';
 } from 'lib/paths';
-import Nav from 'components/Nav/Nav';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import Dashboard from 'components/Dashboard/Dashboard';
 import Dashboard from 'components/Dashboard/Dashboard';
 import ClusterPage from 'components/Cluster/Cluster';
 import ClusterPage from 'components/Cluster/Cluster';
-import Version from 'components/Version/Version';
 import { ThemeProvider } from 'styled-components';
 import { ThemeProvider } from 'styled-components';
 import theme from 'theme/theme';
 import theme from 'theme/theme';
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
@@ -18,14 +16,13 @@ import { showServerError } from 'lib/errorHandling';
 import { Toaster } from 'react-hot-toast';
 import { Toaster } from 'react-hot-toast';
 import GlobalCSS from 'components/global.css';
 import GlobalCSS from 'components/global.css';
 import * as S from 'components/App.styled';
 import * as S from 'components/App.styled';
-import Logo from 'components/common/Logo/Logo';
-import GitIcon from 'components/common/Icons/GitIcon';
-import DiscordIcon from 'components/common/Icons/DiscordIcon';
 
 
 import ConfirmationModal from './common/ConfirmationModal/ConfirmationModal';
 import ConfirmationModal from './common/ConfirmationModal/ConfirmationModal';
 import { ConfirmContextProvider } from './contexts/ConfirmContext';
 import { ConfirmContextProvider } from './contexts/ConfirmContext';
 import { GlobalSettingsProvider } from './contexts/GlobalSettingsContext';
 import { GlobalSettingsProvider } from './contexts/GlobalSettingsContext';
 import ErrorPage from './ErrorPage/ErrorPage';
 import ErrorPage from './ErrorPage/ErrorPage';
+import { UserInfoRolesAccessProvider } from './contexts/UserInfoRolesAccessContext';
+import PageContainer from './PageContainer/PageContainer';
 
 
 const queryClient = new QueryClient({
 const queryClient = new QueryClient({
   defaultOptions: {
   defaultOptions: {
@@ -41,109 +38,47 @@ const queryClient = new QueryClient({
 });
 });
 
 
 const App: React.FC = () => {
 const App: React.FC = () => {
-  const [isSidebarVisible, setIsSidebarVisible] = React.useState(false);
-  const onBurgerClick = () => setIsSidebarVisible(!isSidebarVisible);
-  const closeSidebar = useCallback(() => setIsSidebarVisible(false), []);
-  const location = useLocation();
-
-  React.useEffect(() => {
-    closeSidebar();
-  }, [location, closeSidebar]);
-
   return (
   return (
     <QueryClientProvider client={queryClient}>
     <QueryClientProvider client={queryClient}>
       <GlobalSettingsProvider>
       <GlobalSettingsProvider>
         <ThemeProvider theme={theme}>
         <ThemeProvider theme={theme}>
-          <ConfirmContextProvider>
-            <GlobalCSS />
-            <S.Layout>
-              <S.Navbar role="navigation" aria-label="Page Header">
-                <S.NavbarBrand>
-                  <S.NavbarBrand>
-                    <S.NavbarBurger
-                      onClick={onBurgerClick}
-                      onKeyDown={onBurgerClick}
-                      role="button"
-                      tabIndex={0}
-                      aria-label="burger"
-                    >
-                      <S.Span role="separator" />
-                      <S.Span role="separator" />
-                      <S.Span role="separator" />
-                    </S.NavbarBurger>
-
-                    <S.Hyperlink to="/">
-                      <Logo />
-                      UI for Apache Kafka
-                    </S.Hyperlink>
-
-                    <S.NavbarItem>
-                      <Version />
-                    </S.NavbarItem>
-                  </S.NavbarBrand>
-                </S.NavbarBrand>
-                <S.NavbarSocial>
-                  <S.LogoutLink href="/logout">
-                    <S.LogoutButton buttonType="primary" buttonSize="M">
-                      Log out
-                    </S.LogoutButton>
-                  </S.LogoutLink>
-                  <S.SocialLink
-                    href="https://github.com/provectus/kafka-ui"
-                    target="_blank"
-                  >
-                    <GitIcon />
-                  </S.SocialLink>
-                  <S.SocialLink
-                    href="https://discord.com/invite/4DWzD7pGE5"
-                    target="_blank"
-                  >
-                    <DiscordIcon />
-                  </S.SocialLink>
-                </S.NavbarSocial>
-              </S.Navbar>
-
-              <S.Container>
-                <S.Sidebar aria-label="Sidebar" $visible={isSidebarVisible}>
-                  <Suspense fallback={<PageLoader />}>
-                    <Nav />
-                  </Suspense>
-                </S.Sidebar>
-                <S.Overlay
-                  $visible={isSidebarVisible}
-                  onClick={closeSidebar}
-                  onKeyDown={closeSidebar}
-                  tabIndex={-1}
-                  aria-hidden="true"
-                  aria-label="Overlay"
-                />
-                <Routes>
-                  {['/', '/ui', '/ui/clusters'].map((path) => (
-                    <Route
-                      key="Home" // optional: avoid full re-renders on route changes
-                      path={path}
-                      element={<Dashboard />}
-                    />
-                  ))}
-                  <Route
-                    path={getNonExactPath(clusterPath())}
-                    element={<ClusterPage />}
-                  />
-                  <Route
-                    path={accessErrorPage}
-                    element={<ErrorPage status={403} text="Access is Denied" />}
-                  />
-                  <Route path={errorPage} element={<ErrorPage />} />
-                  <Route
-                    path="*"
-                    element={<Navigate to={errorPage} replace />}
-                  />
-                </Routes>
-              </S.Container>
-              <Toaster position="bottom-right" />
-            </S.Layout>
-            <ConfirmationModal />
-          </ConfirmContextProvider>
+          <Suspense fallback={<PageLoader />}>
+            <UserInfoRolesAccessProvider>
+              <ConfirmContextProvider>
+                <GlobalCSS />
+                <S.Layout>
+                  <PageContainer>
+                    <Routes>
+                      {['/', '/ui', '/ui/clusters'].map((path) => (
+                        <Route
+                          key="Home" // optional: avoid full re-renders on route changes
+                          path={path}
+                          element={<Dashboard />}
+                        />
+                      ))}
+                      <Route
+                        path={getNonExactPath(clusterPath())}
+                        element={<ClusterPage />}
+                      />
+                      <Route
+                        path={accessErrorPage}
+                        element={
+                          <ErrorPage status={403} text="Access is Denied" />
+                        }
+                      />
+                      <Route path={errorPage} element={<ErrorPage />} />
+                      <Route
+                        path="*"
+                        element={<Navigate to={errorPage} replace />}
+                      />
+                    </Routes>
+                  </PageContainer>
+                  <Toaster position="bottom-right" />
+                </S.Layout>
+                <ConfirmationModal />
+              </ConfirmContextProvider>
+            </UserInfoRolesAccessProvider>
+          </Suspense>
         </ThemeProvider>
         </ThemeProvider>
       </GlobalSettingsProvider>
       </GlobalSettingsProvider>
     </QueryClientProvider>
     </QueryClientProvider>

+ 8 - 3
kafka-ui-react-app/src/components/Brokers/Broker/Configs/InputCell.tsx

@@ -4,9 +4,10 @@ import CheckmarkIcon from 'components/common/Icons/CheckmarkIcon';
 import EditIcon from 'components/common/Icons/EditIcon';
 import EditIcon from 'components/common/Icons/EditIcon';
 import CancelIcon from 'components/common/Icons/CancelIcon';
 import CancelIcon from 'components/common/Icons/CancelIcon';
 import { useConfirm } from 'lib/hooks/useConfirm';
 import { useConfirm } from 'lib/hooks/useConfirm';
-import { BrokerConfig } from 'generated-sources';
+import { Action, BrokerConfig, ResourceType } from 'generated-sources';
 import { Button } from 'components/common/Button/Button';
 import { Button } from 'components/common/Button/Button';
 import Input from 'components/common/Input/Input';
 import Input from 'components/common/Input/Input';
+import { ActionButton } from 'components/common/ActionComponent';
 
 
 import * as S from './Configs.styled';
 import * as S from './Configs.styled';
 
 
@@ -71,14 +72,18 @@ const InputCell: React.FC<InputCellProps> = ({ row, getValue, onUpdate }) => {
       }
       }
     >
     >
       <S.Value title={initialValue}>{initialValue}</S.Value>
       <S.Value title={initialValue}>{initialValue}</S.Value>
-      <Button
+      <ActionButton
         buttonType="primary"
         buttonType="primary"
         buttonSize="S"
         buttonSize="S"
         aria-label="editAction"
         aria-label="editAction"
         onClick={() => setIsEdit(true)}
         onClick={() => setIsEdit(true)}
+        permission={{
+          resource: ResourceType.CLUSTERCONFIG,
+          action: Action.EDIT,
+        }}
       >
       >
         <EditIcon /> Edit
         <EditIcon /> Edit
-      </Button>
+      </ActionButton>
     </S.ValueWrapper>
     </S.ValueWrapper>
   );
   );
 };
 };

+ 65 - 14
kafka-ui-react-app/src/components/Connect/Details/Actions/Actions.tsx

@@ -2,7 +2,12 @@ import React from 'react';
 import styled from 'styled-components';
 import styled from 'styled-components';
 import { useNavigate } from 'react-router-dom';
 import { useNavigate } from 'react-router-dom';
 import { useIsMutating } from '@tanstack/react-query';
 import { useIsMutating } from '@tanstack/react-query';
-import { ConnectorState, ConnectorAction } from 'generated-sources';
+import {
+  Action,
+  ConnectorAction,
+  ConnectorState,
+  ResourceType,
+} from 'generated-sources';
 import useAppParams from 'lib/hooks/useAppParams';
 import useAppParams from 'lib/hooks/useAppParams';
 import {
 import {
   useConnector,
   useConnector,
@@ -14,7 +19,8 @@ import {
   RouterParamsClusterConnectConnector,
   RouterParamsClusterConnectConnector,
 } from 'lib/paths';
 } from 'lib/paths';
 import { useConfirm } from 'lib/hooks/useConfirm';
 import { useConfirm } from 'lib/hooks/useConfirm';
-import { Dropdown, DropdownItem } from 'components/common/Dropdown';
+import { Dropdown } from 'components/common/Dropdown';
+import { ActionDropdownItem } from 'components/common/ActionComponent';
 
 
 const ConnectorActionsWrapperStyled = styled.div`
 const ConnectorActionsWrapperStyled = styled.div`
   display: flex;
   display: flex;
@@ -65,31 +71,76 @@ const Actions: React.FC = () => {
     <ConnectorActionsWrapperStyled>
     <ConnectorActionsWrapperStyled>
       <Dropdown>
       <Dropdown>
         {connector?.status.state === ConnectorState.RUNNING && (
         {connector?.status.state === ConnectorState.RUNNING && (
-          <DropdownItem onClick={pauseConnectorHandler} disabled={isMutating}>
+          <ActionDropdownItem
+            onClick={pauseConnectorHandler}
+            disabled={isMutating}
+            permission={{
+              resource: ResourceType.CONNECT,
+              action: Action.EDIT,
+              value: routerProps.connectorName,
+            }}
+          >
             Pause
             Pause
-          </DropdownItem>
+          </ActionDropdownItem>
         )}
         )}
         {connector?.status.state === ConnectorState.PAUSED && (
         {connector?.status.state === ConnectorState.PAUSED && (
-          <DropdownItem onClick={resumeConnectorHandler} disabled={isMutating}>
+          <ActionDropdownItem
+            onClick={resumeConnectorHandler}
+            disabled={isMutating}
+            permission={{
+              resource: ResourceType.CONNECT,
+              action: Action.EDIT,
+              value: routerProps.connectorName,
+            }}
+          >
             Resume
             Resume
-          </DropdownItem>
+          </ActionDropdownItem>
         )}
         )}
-        <DropdownItem onClick={restartConnectorHandler} disabled={isMutating}>
+        <ActionDropdownItem
+          onClick={restartConnectorHandler}
+          disabled={isMutating}
+          permission={{
+            resource: ResourceType.CONNECT,
+            action: Action.EDIT,
+            value: routerProps.connectorName,
+          }}
+        >
           Restart Connector
           Restart Connector
-        </DropdownItem>
-        <DropdownItem onClick={restartAllTasksHandler} disabled={isMutating}>
+        </ActionDropdownItem>
+        <ActionDropdownItem
+          onClick={restartAllTasksHandler}
+          disabled={isMutating}
+          permission={{
+            resource: ResourceType.CONNECT,
+            action: Action.EDIT,
+            value: routerProps.connectorName,
+          }}
+        >
           Restart All Tasks
           Restart All Tasks
-        </DropdownItem>
-        <DropdownItem onClick={restartFailedTasksHandler} disabled={isMutating}>
+        </ActionDropdownItem>
+        <ActionDropdownItem
+          onClick={restartFailedTasksHandler}
+          disabled={isMutating}
+          permission={{
+            resource: ResourceType.CONNECT,
+            action: Action.EDIT,
+            value: routerProps.connectorName,
+          }}
+        >
           Restart Failed Tasks
           Restart Failed Tasks
-        </DropdownItem>
-        <DropdownItem
+        </ActionDropdownItem>
+        <ActionDropdownItem
           onClick={deleteConnectorHandler}
           onClick={deleteConnectorHandler}
           disabled={isMutating}
           disabled={isMutating}
           danger
           danger
+          permission={{
+            resource: ResourceType.CONNECT,
+            action: Action.DELETE,
+            value: routerProps.connectorName,
+          }}
         >
         >
           Delete
           Delete
-        </DropdownItem>
+        </ActionDropdownItem>
       </Dropdown>
       </Dropdown>
     </ConnectorActionsWrapperStyled>
     </ConnectorActionsWrapperStyled>
   );
   );

+ 8 - 4
kafka-ui-react-app/src/components/Connect/List/ListPage.tsx

@@ -5,10 +5,10 @@ import ClusterContext from 'components/contexts/ClusterContext';
 import Search from 'components/common/Search/Search';
 import Search from 'components/common/Search/Search';
 import * as Metrics from 'components/common/Metrics';
 import * as Metrics from 'components/common/Metrics';
 import PageHeading from 'components/common/PageHeading/PageHeading';
 import PageHeading from 'components/common/PageHeading/PageHeading';
-import { Button } from 'components/common/Button/Button';
+import { ActionButton } from 'components/common/ActionComponent';
 import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
 import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
 import PageLoader from 'components/common/PageLoader/PageLoader';
 import PageLoader from 'components/common/PageLoader/PageLoader';
-import { ConnectorState } from 'generated-sources';
+import { Action, ConnectorState, ResourceType } from 'generated-sources';
 import { useConnectors } from 'lib/hooks/api/kafkaConnect';
 import { useConnectors } from 'lib/hooks/api/kafkaConnect';
 
 
 import List from './List';
 import List from './List';
@@ -33,13 +33,17 @@ const ListPage: React.FC = () => {
     <>
     <>
       <PageHeading text="Connectors">
       <PageHeading text="Connectors">
         {!isReadOnly && (
         {!isReadOnly && (
-          <Button
+          <ActionButton
             buttonType="primary"
             buttonType="primary"
             buttonSize="M"
             buttonSize="M"
             to={clusterConnectorNewRelativePath}
             to={clusterConnectorNewRelativePath}
+            permission={{
+              resource: ResourceType.CONNECT,
+              action: Action.CREATE,
+            }}
           >
           >
             Create Connector
             Create Connector
-          </Button>
+          </ActionButton>
         )}
         )}
       </PageHeading>
       </PageHeading>
       <Metrics.Wrapper>
       <Metrics.Wrapper>

+ 1 - 1
kafka-ui-react-app/src/components/Connect/New/New.tsx

@@ -65,7 +65,7 @@ const New: React.FC = () => {
   }, [connects, getValues, setValue]);
   }, [connects, getValues, setValue]);
 
 
   const onSubmit = async (values: FormValues) => {
   const onSubmit = async (values: FormValues) => {
-    const connector = await mutation.mutateAsync({
+    const connector = await mutation.createResource({
       connectName: values.connectName,
       connectName: values.connectName,
       newConnector: {
       newConnector: {
         name: values.name,
         name: values.name,

+ 2 - 1
kafka-ui-react-app/src/components/Connect/New/__tests__/New.spec.tsx

@@ -23,6 +23,7 @@ jest.mock('react-router-dom', () => ({
   ...jest.requireActual('react-router-dom'),
   ...jest.requireActual('react-router-dom'),
   useNavigate: () => mockHistoryPush,
   useNavigate: () => mockHistoryPush,
 }));
 }));
+
 jest.mock('lib/hooks/api/kafkaConnect', () => ({
 jest.mock('lib/hooks/api/kafkaConnect', () => ({
   useConnects: jest.fn(),
   useConnects: jest.fn(),
   useCreateConnector: jest.fn(),
   useCreateConnector: jest.fn(),
@@ -67,7 +68,7 @@ describe('New', () => {
       return Promise.resolve(connector);
       return Promise.resolve(connector);
     });
     });
     (useCreateConnector as jest.Mock).mockImplementation(() => ({
     (useCreateConnector as jest.Mock).mockImplementation(() => ({
-      mutateAsync: createConnectorMock,
+      createResource: createConnectorMock,
     }));
     }));
     renderComponent();
     renderComponent();
     await simulateFormSubmit();
     await simulateFormSubmit();

+ 23 - 7
kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx

@@ -17,15 +17,17 @@ import { Table } from 'components/common/table/Table/Table.styled';
 import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
 import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
 import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
 import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
 import {
 import {
-  fetchConsumerGroupDetails,
   deleteConsumerGroup,
   deleteConsumerGroup,
-  selectById,
-  getIsConsumerGroupDeleted,
+  fetchConsumerGroupDetails,
   getAreConsumerGroupDetailsFulfilled,
   getAreConsumerGroupDetailsFulfilled,
+  getIsConsumerGroupDeleted,
+  selectById,
 } from 'redux/reducers/consumerGroups/consumerGroupsSlice';
 } from 'redux/reducers/consumerGroups/consumerGroupsSlice';
 import getTagColor from 'components/common/Tag/getTagColor';
 import getTagColor from 'components/common/Tag/getTagColor';
-import { Dropdown, DropdownItem } from 'components/common/Dropdown';
+import { Dropdown } from 'components/common/Dropdown';
 import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
 import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel.styled';
+import { Action, ResourceType } from 'generated-sources';
+import { ActionDropdownItem } from 'components/common/ActionComponent';
 
 
 import ListItem from './ListItem';
 import ListItem from './ListItem';
 
 
@@ -84,14 +86,28 @@ const Details: React.FC = () => {
         >
         >
           {!isReadOnly && (
           {!isReadOnly && (
             <Dropdown>
             <Dropdown>
-              <DropdownItem onClick={onResetOffsets}>Reset offset</DropdownItem>
-              <DropdownItem
+              <ActionDropdownItem
+                onClick={onResetOffsets}
+                permission={{
+                  resource: ResourceType.CONSUMER,
+                  action: Action.RESET_OFFSETS,
+                  value: consumerGroupID,
+                }}
+              >
+                Reset offset
+              </ActionDropdownItem>
+              <ActionDropdownItem
                 confirm="Are you sure you want to delete this consumer group?"
                 confirm="Are you sure you want to delete this consumer group?"
                 onClick={onDelete}
                 onClick={onDelete}
                 danger
                 danger
+                permission={{
+                  resource: ResourceType.CONSUMER,
+                  action: Action.DELETE,
+                  value: consumerGroupID,
+                }}
               >
               >
                 Delete consumer group
                 Delete consumer group
-              </DropdownItem>
+              </ActionDropdownItem>
             </Dropdown>
             </Dropdown>
           )}
           )}
         </PageHeading>
         </PageHeading>

+ 11 - 6
kafka-ui-react-app/src/components/KsqlDb/List/List.tsx

@@ -4,18 +4,19 @@ import * as Metrics from 'components/common/Metrics';
 import { getKsqlDbTables } from 'redux/reducers/ksqlDb/selectors';
 import { getKsqlDbTables } from 'redux/reducers/ksqlDb/selectors';
 import {
 import {
   clusterKsqlDbQueryRelativePath,
   clusterKsqlDbQueryRelativePath,
-  ClusterNameRoute,
   clusterKsqlDbStreamsPath,
   clusterKsqlDbStreamsPath,
-  clusterKsqlDbTablesPath,
   clusterKsqlDbStreamsRelativePath,
   clusterKsqlDbStreamsRelativePath,
+  clusterKsqlDbTablesPath,
   clusterKsqlDbTablesRelativePath,
   clusterKsqlDbTablesRelativePath,
+  ClusterNameRoute,
 } from 'lib/paths';
 } from 'lib/paths';
 import PageHeading from 'components/common/PageHeading/PageHeading';
 import PageHeading from 'components/common/PageHeading/PageHeading';
-import { Button } from 'components/common/Button/Button';
+import { ActionButton } from 'components/common/ActionComponent';
 import Navbar from 'components/common/Navigation/Navbar.styled';
 import Navbar from 'components/common/Navigation/Navbar.styled';
-import { NavLink, Route, Routes, Navigate } from 'react-router-dom';
+import { Navigate, NavLink, Route, Routes } from 'react-router-dom';
 import { fetchKsqlDbTables } from 'redux/reducers/ksqlDb/ksqlDbSlice';
 import { fetchKsqlDbTables } from 'redux/reducers/ksqlDb/ksqlDbSlice';
 import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
 import { useAppDispatch, useAppSelector } from 'lib/hooks/redux';
+import { Action, ResourceType } from 'generated-sources';
 
 
 import KsqlDbItem, { KsqlDbItemType } from './KsqlDbItem/KsqlDbItem';
 import KsqlDbItem, { KsqlDbItemType } from './KsqlDbItem/KsqlDbItem';
 
 
@@ -33,13 +34,17 @@ const List: FC = () => {
   return (
   return (
     <>
     <>
       <PageHeading text="KSQL DB">
       <PageHeading text="KSQL DB">
-        <Button
+        <ActionButton
           to={clusterKsqlDbQueryRelativePath}
           to={clusterKsqlDbQueryRelativePath}
           buttonType="primary"
           buttonType="primary"
           buttonSize="M"
           buttonSize="M"
+          permission={{
+            resource: ResourceType.KSQL,
+            action: Action.EXECUTE,
+          }}
         >
         >
           Execute KSQL Request
           Execute KSQL Request
-        </Button>
+        </ActionButton>
       </PageHeading>
       </PageHeading>
       <Metrics.Wrapper>
       <Metrics.Wrapper>
         <Metrics.Section>
         <Metrics.Section>

+ 5 - 4
kafka-ui-react-app/src/components/KsqlDb/List/__test__/List.spec.tsx

@@ -6,15 +6,16 @@ import { screen } from '@testing-library/dom';
 import { act } from '@testing-library/react';
 import { act } from '@testing-library/react';
 
 
 describe('KsqlDb List', () => {
 describe('KsqlDb List', () => {
-  afterEach(() => fetchMock.reset());
-  it('renders List component with Tables and Streams tabs', async () => {
+  const renderComponent = async () => {
     await act(() => {
     await act(() => {
       render(<List />);
       render(<List />);
     });
     });
-
+  };
+  afterEach(() => fetchMock.reset());
+  it('renders List component with Tables and Streams tabs', async () => {
+    await renderComponent();
     const Tables = screen.getByTitle('Tables');
     const Tables = screen.getByTitle('Tables');
     const Streams = screen.getByTitle('Streams');
     const Streams = screen.getByTitle('Streams');
-
     expect(Tables).toBeInTheDocument();
     expect(Tables).toBeInTheDocument();
     expect(Streams).toBeInTheDocument();
     expect(Streams).toBeInTheDocument();
   });
   });

+ 146 - 0
kafka-ui-react-app/src/components/NavBar/NavBar.styled.ts

@@ -0,0 +1,146 @@
+import styled, { css } from 'styled-components';
+import { Link } from 'react-router-dom';
+import DiscordIcon from 'components/common/Icons/DiscordIcon';
+import GitIcon from 'components/common/Icons/GitIcon';
+
+export const Navbar = styled.nav(
+  ({ theme }) => css`
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    border-bottom: 1px solid ${theme.layout.stuffBorderColor};
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    z-index: 30;
+    background-color: ${theme.menu.backgroundColor.normal};
+    min-height: 3.25rem;
+  `
+);
+
+export const NavbarBrand = styled.div`
+  display: flex;
+  justify-content: flex-end;
+  align-items: center !important;
+  flex-shrink: 0;
+  min-height: 3.25rem;
+`;
+
+export const SocialLink = styled.a(
+  ({ theme: { layout, icons } }) => css`
+    display: block;
+    margin-top: 5px;
+    cursor: pointer;
+    fill: ${layout.socialLink.color};
+
+    &:hover {
+      ${DiscordIcon} {
+        fill: ${icons.discord.hover};
+      }
+
+      ${GitIcon} {
+        fill: ${icons.git.hover};
+      }
+    }
+
+    &:active {
+      ${DiscordIcon} {
+        fill: ${icons.discord.active};
+      }
+
+      ${GitIcon} {
+        fill: ${icons.git.active};
+      }
+    }
+  `
+);
+
+export const NavbarSocial = styled.div`
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  margin: 10px;
+`;
+
+export const NavbarItem = styled.div`
+  display: flex;
+  position: relative;
+  flex-grow: 0;
+  flex-shrink: 0;
+  align-items: center;
+  line-height: 1.5;
+  padding: 0.5rem 0.75rem;
+`;
+
+export const NavbarBurger = styled.div(
+  ({ theme }) => css`
+    display: block;
+    position: relative;
+    cursor: pointer;
+    height: 3.25rem;
+    width: 3.25rem;
+    margin: 0;
+    padding: 0;
+
+    &:hover {
+      background-color: ${theme.menu.backgroundColor.hover};
+    }
+
+    @media screen and (min-width: 1024px) {
+      display: none;
+    }
+  `
+);
+
+export const Span = styled.span(
+  ({ theme }) => css`
+    display: block;
+    position: absolute;
+    background: ${theme.menu.color.active};
+    height: 1px;
+    left: calc(50% - 8px);
+    transform-origin: center;
+    transition-duration: 86ms;
+    transition-property: background-color, opacity, transform, -webkit-transform;
+    transition-timing-function: ease-out;
+    width: 16px;
+
+    &:first-child {
+      top: calc(50% - 6px);
+    }
+
+    &:nth-child(2) {
+      top: calc(50% - 1px);
+    }
+
+    &:nth-child(3) {
+      top: calc(50% + 4px);
+    }
+  `
+);
+
+export const Hyperlink = styled(Link)(
+  ({ theme }) => css`
+    position: relative;
+
+    display: flex;
+    flex-grow: 0;
+    flex-shrink: 0;
+    align-items: center;
+    gap: 8px;
+
+    margin: 0;
+    padding: 0.5rem 0.75rem;
+
+    font-family: Inter, sans-serif;
+    font-style: normal;
+    font-weight: bold;
+    font-size: 12px;
+    line-height: 16px;
+    color: ${theme.menu.color.active};
+    text-decoration: none;
+    word-break: break-word;
+    cursor: pointer;
+  `
+);

+ 60 - 0
kafka-ui-react-app/src/components/NavBar/NavBar.tsx

@@ -0,0 +1,60 @@
+import React from 'react';
+import Logo from 'components/common/Logo/Logo';
+import Version from 'components/Version/Version';
+import GitIcon from 'components/common/Icons/GitIcon';
+import DiscordIcon from 'components/common/Icons/DiscordIcon';
+
+import * as S from './NavBar.styled';
+import UserInfo from './UserInfo/UserInfo';
+
+interface Props {
+  onBurgerClick: () => void;
+}
+
+const NavBar: React.FC<Props> = ({ onBurgerClick }) => {
+  return (
+    <S.Navbar role="navigation" aria-label="Page Header">
+      <S.NavbarBrand>
+        <S.NavbarBrand>
+          <S.NavbarBurger
+            onClick={onBurgerClick}
+            onKeyDown={onBurgerClick}
+            role="button"
+            tabIndex={0}
+            aria-label="burger"
+          >
+            <S.Span role="separator" />
+            <S.Span role="separator" />
+            <S.Span role="separator" />
+          </S.NavbarBurger>
+
+          <S.Hyperlink to="/">
+            <Logo />
+            UI for Apache Kafka
+          </S.Hyperlink>
+
+          <S.NavbarItem>
+            <Version />
+          </S.NavbarItem>
+        </S.NavbarBrand>
+      </S.NavbarBrand>
+      <S.NavbarSocial>
+        <S.SocialLink
+          href="https://github.com/provectus/kafka-ui"
+          target="_blank"
+        >
+          <GitIcon />
+        </S.SocialLink>
+        <S.SocialLink
+          href="https://discord.com/invite/4DWzD7pGE5"
+          target="_blank"
+        >
+          <DiscordIcon />
+        </S.SocialLink>
+        <UserInfo />
+      </S.NavbarSocial>
+    </S.Navbar>
+  );
+};
+
+export default NavBar;

+ 19 - 0
kafka-ui-react-app/src/components/NavBar/UserInfo/UserInfo.styled.ts

@@ -0,0 +1,19 @@
+import styled, { css } from 'styled-components';
+
+export const Wrapper = styled.div`
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  gap: 5px;
+  svg {
+    position: relative;
+  }
+`;
+
+export const Text = styled.div(
+  ({ theme }) => css`
+    color: ${theme.button.primary.invertedColors.normal};
+  `
+);
+
+export const LogoutLink = styled.a``;

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff