Browse Source

Merge branch 'master' into issues/3615

Denys 2 years ago
parent
commit
c878c9b7f7
42 changed files with 256 additions and 77 deletions
  1. 1 1
      .github/workflows/aws_publisher.yaml
  2. 3 3
      .github/workflows/backend.yml
  3. 1 1
      .github/workflows/block_merge.yml
  4. 1 1
      .github/workflows/branch-deploy.yml
  5. 1 1
      .github/workflows/branch-remove.yml
  6. 2 2
      .github/workflows/build-public-image.yml
  7. 1 1
      .github/workflows/delete-public-image.yml
  8. 1 1
      .github/workflows/documentation.yaml
  9. 1 1
      .github/workflows/e2e-automation.yml
  10. 2 2
      .github/workflows/e2e-checks.yaml
  11. 1 1
      .github/workflows/e2e-manual.yml
  12. 1 1
      .github/workflows/e2e-weekly.yml
  13. 2 2
      .github/workflows/frontend.yaml
  14. 2 1
      .github/workflows/master.yaml
  15. 2 2
      .github/workflows/pr-checks.yaml
  16. 1 1
      .github/workflows/release-serde-api.yaml
  17. 1 1
      .github/workflows/release.yaml
  18. 1 1
      .github/workflows/release_drafter.yml
  19. 1 1
      .github/workflows/separate_env_public_create.yml
  20. 1 1
      .github/workflows/separate_env_public_remove.yml
  21. 1 1
      .github/workflows/stale.yaml
  22. 1 1
      .github/workflows/terraform-deploy.yml
  23. 1 1
      .github/workflows/triage_issues.yml
  24. 1 1
      .github/workflows/triage_prs.yml
  25. 1 1
      .github/workflows/welcome-first-time-contributors.yml
  26. 1 1
      .github/workflows/workflow_linter.yaml
  27. 2 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapProperties.java
  28. 25 10
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapSecurityConfig.java
  29. 3 6
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddExporter.java
  30. 5 5
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/TopicsExporter.java
  31. 78 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java
  32. 3 1
      kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/TopicsExporterTest.java
  33. 2 2
      kafka-ui-e2e-checks/pom.xml
  34. 15 0
      kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/StringUtils.java
  35. 2 10
      kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/manualsuite/backlog/SmokeBacklog.java
  36. 34 0
      kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/brokers/BrokersTest.java
  37. 1 1
      kafka-ui-react-app/public/robots.txt
  38. 16 4
      kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx
  39. 18 3
      kafka-ui-react-app/src/components/ConsumerGroups/List.tsx
  40. 12 1
      kafka-ui-react-app/src/lib/constants.ts
  41. 5 0
      kafka-ui-react-app/src/lib/hooks/api/topics.ts
  42. 2 2
      pom.xml

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

@@ -1,4 +1,4 @@
-name: AWS Marketplace Publisher
+name: "Infra: Release: AWS Marketplace Publisher"
 on:
 on:
   workflow_dispatch:
   workflow_dispatch:
     inputs:
     inputs:

+ 3 - 3
.github/workflows/backend.yml

@@ -1,9 +1,9 @@
-name: Backend build and test
+name: "Backend: PR/master build & test"
 on:
 on:
   push:
   push:
     branches:
     branches:
       - master
       - master
-  pull_request_target:
+  pull_request:
     types: ["opened", "edited", "reopened", "synchronize"]
     types: ["opened", "edited", "reopened", "synchronize"]
     paths:
     paths:
       - "kafka-ui-api/**"
       - "kafka-ui-api/**"
@@ -29,7 +29,7 @@ jobs:
           key: ${{ runner.os }}-sonar
           key: ${{ runner.os }}-sonar
           restore-keys: ${{ runner.os }}-sonar
           restore-keys: ${{ runner.os }}-sonar
       - name: Build and analyze pull request target
       - name: Build and analyze pull request target
-        if: ${{ github.event_name == 'pull_request_target' }}
+        if: ${{ github.event_name == 'pull_request' }}
         env:
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           SONAR_TOKEN: ${{ secrets.SONAR_TOKEN_BACKEND }}
           SONAR_TOKEN: ${{ secrets.SONAR_TOKEN_BACKEND }}

+ 1 - 1
.github/workflows/block_merge.yml

@@ -1,4 +1,4 @@
-name: Pull Request Labels
+name: "Infra: PR block merge"
 on:
 on:
   pull_request:
   pull_request:
     types: [opened, labeled, unlabeled, synchronize]
     types: [opened, labeled, unlabeled, synchronize]

+ 1 - 1
.github/workflows/branch-deploy.yml

@@ -1,4 +1,4 @@
-name: Feature testing init
+name: "Infra: Feature Testing: Init env"
 on:
 on:
   workflow_dispatch:
   workflow_dispatch:
 
 

+ 1 - 1
.github/workflows/branch-remove.yml

@@ -1,4 +1,4 @@
-name: Feature testing destroy
+name: "Infra: Feature Testing: Destroy env"
 on:
 on:
   workflow_dispatch:
   workflow_dispatch:
   pull_request:
   pull_request:

+ 2 - 2
.github/workflows/build-public-image.yml

@@ -1,4 +1,4 @@
-name: Build Docker image and push
+name: "Infra: Image Testing: Deploy"
 on:
 on:
   workflow_dispatch:
   workflow_dispatch:
   pull_request:
   pull_request:
@@ -65,7 +65,7 @@ jobs:
           cache-from: type=local,src=/tmp/.buildx-cache
           cache-from: type=local,src=/tmp/.buildx-cache
           cache-to: type=local,dest=/tmp/.buildx-cache
           cache-to: type=local,dest=/tmp/.buildx-cache
       - name: make comment with private deployment link
       - name: make comment with private deployment link
-        uses: peter-evans/create-or-update-comment@v2
+        uses: peter-evans/create-or-update-comment@v3
         with:
         with:
           issue-number: ${{ github.event.pull_request.number }}
           issue-number: ${{ github.event.pull_request.number }}
           body: |
           body: |

+ 1 - 1
.github/workflows/delete-public-image.yml

@@ -1,4 +1,4 @@
-name: Delete Public ECR Image
+name: "Infra: Image Testing: Delete"
 on:
 on:
   workflow_dispatch:
   workflow_dispatch:
   pull_request:
   pull_request:

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

@@ -1,4 +1,4 @@
-name: Documentation URLs linter
+name: "Infra: Docs: URL linter"
 on:
 on:
   pull_request:
   pull_request:
     types:
     types:

+ 1 - 1
.github/workflows/e2e-automation.yml

@@ -1,4 +1,4 @@
-name: E2E Automation suite
+name: "E2E: Automation suite"
 on:
 on:
   workflow_dispatch:
   workflow_dispatch:
     inputs:
     inputs:

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

@@ -1,6 +1,6 @@
-name: E2E PR health check
+name: "E2E: PR healthcheck"
 on:
 on:
-  pull_request_target:
+  pull_request:
     types: [ "opened", "edited", "reopened", "synchronize" ]
     types: [ "opened", "edited", "reopened", "synchronize" ]
     paths:
     paths:
       - "kafka-ui-api/**"
       - "kafka-ui-api/**"

+ 1 - 1
.github/workflows/e2e-manual.yml

@@ -1,4 +1,4 @@
-name: E2E Manual suite
+name: "E2E: Manual suite"
 on:
 on:
   workflow_dispatch:
   workflow_dispatch:
     inputs:
     inputs:

+ 1 - 1
.github/workflows/e2e-weekly.yml

@@ -1,4 +1,4 @@
-name: E2E Weekly suite
+name: "E2E: Weekly suite"
 on:
 on:
   schedule:
   schedule:
     - cron: '0 1 * * 1'
     - cron: '0 1 * * 1'

+ 2 - 2
.github/workflows/frontend.yaml

@@ -1,9 +1,9 @@
-name: Frontend build and test
+name: "Frontend: PR/master build & test"
 on:
 on:
   push:
   push:
     branches:
     branches:
       - master
       - master
-  pull_request_target:
+  pull_request:
     types: ["opened", "edited", "reopened", "synchronize"]
     types: ["opened", "edited", "reopened", "synchronize"]
     paths:
     paths:
       - "kafka-ui-contract/**"
       - "kafka-ui-contract/**"

+ 2 - 1
.github/workflows/master.yaml

@@ -1,4 +1,4 @@
-name: Master branch build & deploy
+name: "Master: Build & deploy"
 on:
 on:
   workflow_dispatch:
   workflow_dispatch:
   push:
   push:
@@ -58,6 +58,7 @@ jobs:
           builder: ${{ steps.buildx.outputs.name }}
           builder: ${{ steps.buildx.outputs.name }}
           context: kafka-ui-api
           context: kafka-ui-api
           platforms: linux/amd64,linux/arm64
           platforms: linux/amd64,linux/arm64
+          provenance: false
           push: true
           push: true
           tags: |
           tags: |
             provectuslabs/kafka-ui:${{ steps.build.outputs.version }}
             provectuslabs/kafka-ui:${{ steps.build.outputs.version }}

+ 2 - 2
.github/workflows/pr-checks.yaml

@@ -1,6 +1,6 @@
-name: "PR Checklist checked"
+name: "PR: Checklist linter"
 on:
 on:
-  pull_request_target:
+  pull_request:
     types: [opened, edited, synchronize, reopened]
     types: [opened, edited, synchronize, reopened]
 
 
 jobs:
 jobs:

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

@@ -1,4 +1,4 @@
-name: Release serde api
+name: "Infra: Release: Serde API"
 on: workflow_dispatch
 on: workflow_dispatch
 
 
 jobs:
 jobs:

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

@@ -1,4 +1,4 @@
-name: Release
+name: "Infra: Release"
 on:
 on:
   release:
   release:
     types: [published]
     types: [published]

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

@@ -1,4 +1,4 @@
-name: Release Drafter
+name: "Infra: Release Drafter run"
 
 
 on:
 on:
   push:
   push:

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

@@ -1,4 +1,4 @@
-name: Separate environment create
+name: "Infra: Feature Testing Public: Init env"
 on:
 on:
   workflow_dispatch:
   workflow_dispatch:
     inputs:
     inputs:

+ 1 - 1
.github/workflows/separate_env_public_remove.yml

@@ -1,4 +1,4 @@
-name: Separate environment remove
+name: "Infra: Feature Testing Public: Destroy env"
 on:
 on:
   workflow_dispatch:
   workflow_dispatch:
     inputs:
     inputs:

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

@@ -1,4 +1,4 @@
-name: 'Close stale issues'
+name: 'Infra: Close stale issues'
 on:
 on:
   schedule:
   schedule:
     - cron: '30 1 * * *'
     - cron: '30 1 * * *'

+ 1 - 1
.github/workflows/terraform-deploy.yml

@@ -1,4 +1,4 @@
-name: Terraform deploy
+name: "Infra: Terraform deploy"
 on:
 on:
   workflow_dispatch:
   workflow_dispatch:
     inputs:
     inputs:

+ 1 - 1
.github/workflows/triage_issues.yml

@@ -1,4 +1,4 @@
-name: Add triage label to new issues
+name: "Infra: Triage: Apply triage label for issues"
 on:
 on:
   issues:
   issues:
     types:
     types:

+ 1 - 1
.github/workflows/triage_prs.yml

@@ -1,4 +1,4 @@
-name: Add triage label to new PRs
+name: "Infra: Triage: Apply triage label for PRs"
 on:
 on:
   pull_request:
   pull_request:
     types:
     types:

+ 1 - 1
.github/workflows/welcome-first-time-contributors.yml

@@ -1,7 +1,7 @@
 name: Welcome first time contributors
 name: Welcome first time contributors
 
 
 on:
 on:
-  pull_request_target:
+  pull_request:
     types:
     types:
       - opened
       - opened
   issues:
   issues:

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

@@ -1,4 +1,4 @@
-name: "Workflow linter"
+name: "Infra: Workflow linter"
 on:
 on:
   pull_request:
   pull_request:
     types:
     types:

+ 2 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapProperties.java

@@ -15,6 +15,8 @@ public class LdapProperties {
   private String userFilterSearchBase;
   private String userFilterSearchBase;
   private String userFilterSearchFilter;
   private String userFilterSearchFilter;
   private String groupFilterSearchBase;
   private String groupFilterSearchBase;
+  private String groupFilterSearchFilter;
+  private String groupRoleAttribute;
 
 
   @Value("${oauth2.ldap.activeDirectory:false}")
   @Value("${oauth2.ldap.activeDirectory:false}")
   private boolean isActiveDirectory;
   private boolean isActiveDirectory;

+ 25 - 10
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapSecurityConfig.java

@@ -3,14 +3,16 @@ package com.provectus.kafka.ui.config.auth;
 import static com.provectus.kafka.ui.config.auth.AbstractAuthSecurityConfig.AUTH_WHITELIST;
 import static com.provectus.kafka.ui.config.auth.AbstractAuthSecurityConfig.AUTH_WHITELIST;
 
 
 import com.provectus.kafka.ui.service.rbac.AccessControlService;
 import com.provectus.kafka.ui.service.rbac.AccessControlService;
+import com.provectus.kafka.ui.service.rbac.extractor.RbacLdapAuthoritiesExtractor;
 import java.util.Collection;
 import java.util.Collection;
 import java.util.List;
 import java.util.List;
-import javax.annotation.Nullable;
+import java.util.Optional;
 import lombok.RequiredArgsConstructor;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration;
 import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.ApplicationContext;
 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.context.annotation.Import;
 import org.springframework.context.annotation.Import;
@@ -50,9 +52,9 @@ public class LdapSecurityConfig {
 
 
   @Bean
   @Bean
   public ReactiveAuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource,
   public ReactiveAuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource,
-                                                             LdapAuthoritiesPopulator ldapAuthoritiesPopulator,
-                                                             @Nullable AccessControlService acs) {
-    var rbacEnabled = acs != null && acs.isRbacEnabled();
+                                                             LdapAuthoritiesPopulator authoritiesExtractor,
+                                                             AccessControlService acs) {
+    var rbacEnabled = acs.isRbacEnabled();
     BindAuthenticator ba = new BindAuthenticator(contextSource);
     BindAuthenticator ba = new BindAuthenticator(contextSource);
     if (props.getBase() != null) {
     if (props.getBase() != null) {
       ba.setUserDnPatterns(new String[] {props.getBase()});
       ba.setUserDnPatterns(new String[] {props.getBase()});
@@ -67,7 +69,7 @@ public class LdapSecurityConfig {
     AbstractLdapAuthenticationProvider authenticationProvider;
     AbstractLdapAuthenticationProvider authenticationProvider;
     if (!props.isActiveDirectory()) {
     if (!props.isActiveDirectory()) {
       authenticationProvider = rbacEnabled
       authenticationProvider = rbacEnabled
-          ? new LdapAuthenticationProvider(ba, ldapAuthoritiesPopulator)
+          ? new LdapAuthenticationProvider(ba, authoritiesExtractor)
           : new LdapAuthenticationProvider(ba);
           : new LdapAuthenticationProvider(ba);
     } else {
     } else {
       authenticationProvider = new ActiveDirectoryLdapAuthenticationProvider(props.getActiveDirectoryDomain(),
       authenticationProvider = new ActiveDirectoryLdapAuthenticationProvider(props.getActiveDirectoryDomain(),
@@ -97,11 +99,24 @@ public class LdapSecurityConfig {
 
 
   @Bean
   @Bean
   @Primary
   @Primary
-  public LdapAuthoritiesPopulator ldapAuthoritiesPopulator(BaseLdapPathContextSource contextSource) {
-    var authoritiesPopulator = new DefaultLdapAuthoritiesPopulator(contextSource, props.getGroupFilterSearchBase());
-    authoritiesPopulator.setRolePrefix("");
-    authoritiesPopulator.setConvertToUpperCase(false);
-    return authoritiesPopulator;
+  public DefaultLdapAuthoritiesPopulator ldapAuthoritiesExtractor(ApplicationContext context,
+                                                                  BaseLdapPathContextSource contextSource,
+                                                                  AccessControlService acs) {
+    var rbacEnabled = acs != null && acs.isRbacEnabled();
+
+    DefaultLdapAuthoritiesPopulator extractor;
+
+    if (rbacEnabled) {
+      extractor = new RbacLdapAuthoritiesExtractor(context, contextSource, props.getGroupFilterSearchBase());
+    } else {
+      extractor = new DefaultLdapAuthoritiesPopulator(contextSource, props.getGroupFilterSearchBase());
+    }
+
+    Optional.ofNullable(props.getGroupFilterSearchFilter()).ifPresent(extractor::setGroupSearchFilter);
+    extractor.setRolePrefix("");
+    extractor.setConvertToUpperCase(false);
+    extractor.setSearchSubtree(true);
+    return extractor;
   }
   }
 
 
   @Bean
   @Bean

+ 3 - 6
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/OddExporter.java

@@ -5,13 +5,10 @@ import com.google.common.base.Preconditions;
 import com.provectus.kafka.ui.model.KafkaCluster;
 import com.provectus.kafka.ui.model.KafkaCluster;
 import com.provectus.kafka.ui.service.KafkaConnectService;
 import com.provectus.kafka.ui.service.KafkaConnectService;
 import com.provectus.kafka.ui.service.StatisticsCache;
 import com.provectus.kafka.ui.service.StatisticsCache;
-import java.util.List;
 import java.util.function.Predicate;
 import java.util.function.Predicate;
 import java.util.regex.Pattern;
 import java.util.regex.Pattern;
-import lombok.SneakyThrows;
 import org.opendatadiscovery.client.ApiClient;
 import org.opendatadiscovery.client.ApiClient;
 import org.opendatadiscovery.client.api.OpenDataDiscoveryIngestionApi;
 import org.opendatadiscovery.client.api.OpenDataDiscoveryIngestionApi;
-import org.opendatadiscovery.client.model.DataEntity;
 import org.opendatadiscovery.client.model.DataEntityList;
 import org.opendatadiscovery.client.model.DataEntityList;
 import org.opendatadiscovery.client.model.DataSource;
 import org.opendatadiscovery.client.model.DataSource;
 import org.opendatadiscovery.client.model.DataSourceList;
 import org.opendatadiscovery.client.model.DataSourceList;
@@ -68,14 +65,14 @@ class OddExporter {
   private Mono<Void> exportTopics(KafkaCluster c) {
   private Mono<Void> exportTopics(KafkaCluster c) {
     return createKafkaDataSource(c)
     return createKafkaDataSource(c)
         .thenMany(topicsExporter.export(c))
         .thenMany(topicsExporter.export(c))
-        .concatMap(this::sentDataEntities)
+        .concatMap(this::sendDataEntities)
         .then();
         .then();
   }
   }
 
 
   private Mono<Void> exportKafkaConnects(KafkaCluster cluster) {
   private Mono<Void> exportKafkaConnects(KafkaCluster cluster) {
     return createConnectDataSources(cluster)
     return createConnectDataSources(cluster)
         .thenMany(connectorsExporter.export(cluster))
         .thenMany(connectorsExporter.export(cluster))
-        .concatMap(this::sentDataEntities)
+        .concatMap(this::sendDataEntities)
         .then();
         .then();
   }
   }
 
 
@@ -99,7 +96,7 @@ class OddExporter {
     );
     );
   }
   }
 
 
-  private Mono<Void> sentDataEntities(DataEntityList dataEntityList) {
+  private Mono<Void> sendDataEntities(DataEntityList dataEntityList) {
     return oddApi.postDataEntityList(dataEntityList);
     return oddApi.postDataEntityList(dataEntityList);
   }
   }
 
 

+ 5 - 5
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/integration/odd/TopicsExporter.java

@@ -38,6 +38,8 @@ class TopicsExporter {
     return Flux.fromIterable(stats.getTopicDescriptions().keySet())
     return Flux.fromIterable(stats.getTopicDescriptions().keySet())
         .filter(topicFilter)
         .filter(topicFilter)
         .flatMap(topic -> createTopicDataEntity(cluster, topic, stats))
         .flatMap(topic -> createTopicDataEntity(cluster, topic, stats))
+        .onErrorContinue(
+            (th, topic) -> log.warn("Error exporting data for topic {}, cluster {}", topic, cluster.getName(), th))
         .buffer(100)
         .buffer(100)
         .map(topicsEntities ->
         .map(topicsEntities ->
             new DataEntityList()
             new DataEntityList()
@@ -89,10 +91,10 @@ class TopicsExporter {
         .build();
         .build();
   }
   }
 
 
+  //returns empty list if schemaRegistry is not configured or assumed subject not found
   private Mono<List<DataSetField>> getTopicSchema(KafkaCluster cluster,
   private Mono<List<DataSetField>> getTopicSchema(KafkaCluster cluster,
                                                   String topic,
                                                   String topic,
                                                   KafkaPath topicOddrn,
                                                   KafkaPath topicOddrn,
-                                                  //currently we only retrieve value schema
                                                   boolean isKey) {
                                                   boolean isKey) {
     if (cluster.getSchemaRegistryClient() == null) {
     if (cluster.getSchemaRegistryClient() == null) {
       return Mono.just(List.of());
       return Mono.just(List.of());
@@ -102,10 +104,8 @@ class TopicsExporter {
         .mono(client -> client.getSubjectVersion(subject, "latest"))
         .mono(client -> client.getSubjectVersion(subject, "latest"))
         .map(subj -> DataSetFieldsExtractors.extract(subj, topicOddrn, isKey))
         .map(subj -> DataSetFieldsExtractors.extract(subj, topicOddrn, isKey))
         .onErrorResume(WebClientResponseException.NotFound.class, th -> Mono.just(List.of()))
         .onErrorResume(WebClientResponseException.NotFound.class, th -> Mono.just(List.of()))
-        .onErrorResume(th -> true, th -> {
-          log.warn("Error retrieving subject {} for cluster {}", subject, cluster.getName(), th);
-          return Mono.just(List.of());
-        });
+        .onErrorMap(WebClientResponseException.class, err ->
+            new IllegalStateException("Error retrieving subject %s".formatted(subject), err));
   }
   }
 
 
 }
 }

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

@@ -0,0 +1,78 @@
+package com.provectus.kafka.ui.service.rbac.extractor;
+
+import com.provectus.kafka.ui.config.auth.LdapProperties;
+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 lombok.extern.slf4j.Slf4j;
+import org.springframework.context.ApplicationContext;
+import org.springframework.ldap.core.DirContextOperations;
+import org.springframework.ldap.core.support.BaseLdapPathContextSource;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
+import org.springframework.util.Assert;
+
+@Slf4j
+public class RbacLdapAuthoritiesExtractor extends DefaultLdapAuthoritiesPopulator {
+
+  private final AccessControlService acs;
+  private final LdapProperties props;
+
+  public RbacLdapAuthoritiesExtractor(ApplicationContext context,
+                                      BaseLdapPathContextSource contextSource, String groupFilterSearchBase) {
+    super(contextSource, groupFilterSearchBase);
+    this.acs = context.getBean(AccessControlService.class);
+    this.props = context.getBean(LdapProperties.class);
+  }
+
+  @Override
+  protected Set<GrantedAuthority> getAdditionalRoles(DirContextOperations user, String username) {
+    var ldapGroups = getRoles(user.getNameInNamespace(), username);
+
+    return acs.getRoles()
+        .stream()
+        .filter(r -> r.getSubjects()
+            .stream()
+            .filter(subject -> subject.getProvider().equals(Provider.LDAP))
+            .filter(subject -> subject.getType().equals("group"))
+            .anyMatch(subject -> ldapGroups.contains(subject.getValue()))
+        )
+        .map(Role::getName)
+        .peek(role -> log.trace("Mapped role [{}] for user [{}]", role, username))
+        .map(SimpleGrantedAuthority::new)
+        .collect(Collectors.toSet());
+  }
+
+  private Set<String> getRoles(String userDn, String username) {
+    var groupSearchBase = props.getGroupFilterSearchBase();
+    Assert.notNull(groupSearchBase, "groupSearchBase is empty");
+
+    var groupRoleAttribute = props.getGroupRoleAttribute();
+    if (groupRoleAttribute == null) {
+
+      groupRoleAttribute = "cn";
+    }
+
+    log.trace(
+        "Searching for roles for user [{}] with DN [{}], groupRoleAttribute [{}] and filter [{}] in search base [{}]",
+        username, userDn, groupRoleAttribute, getGroupSearchFilter(), groupSearchBase);
+
+    var ldapTemplate = getLdapTemplate();
+    ldapTemplate.setIgnoreNameNotFoundException(true);
+
+    Set<Map<String, List<String>>> userRoles = ldapTemplate.searchForMultipleAttributeValues(
+        groupSearchBase, getGroupSearchFilter(), new String[] {userDn, username},
+        new String[] {groupRoleAttribute});
+
+    return userRoles.stream()
+        .map(record -> record.get(getGroupRoleAttribute()).get(0))
+        .peek(group -> log.trace("Found LDAP group [{}] for user [{}]", group, username))
+        .collect(Collectors.toSet());
+  }
+
+}

+ 3 - 1
kafka-ui-api/src/test/java/com/provectus/kafka/ui/service/integration/odd/TopicsExporterTest.java

@@ -22,6 +22,8 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
 import org.opendatadiscovery.client.model.DataEntity;
 import org.opendatadiscovery.client.model.DataEntity;
 import org.opendatadiscovery.client.model.DataEntityType;
 import org.opendatadiscovery.client.model.DataEntityType;
+import org.springframework.http.HttpHeaders;
+import org.springframework.web.reactive.function.client.WebClientResponseException;
 import reactor.core.publisher.Mono;
 import reactor.core.publisher.Mono;
 import reactor.test.StepVerifier;
 import reactor.test.StepVerifier;
 
 
@@ -53,7 +55,7 @@ class TopicsExporterTest {
   @Test
   @Test
   void doesNotExportTopicsWhichDontFitFiltrationRule() {
   void doesNotExportTopicsWhichDontFitFiltrationRule() {
     when(schemaRegistryClientMock.getSubjectVersion(anyString(), anyString()))
     when(schemaRegistryClientMock.getSubjectVersion(anyString(), anyString()))
-        .thenReturn(Mono.error(new RuntimeException("Not found")));
+        .thenReturn(Mono.error(WebClientResponseException.create(404, "NF", new HttpHeaders(), null, null, null)));
 
 
     stats = Statistics.empty()
     stats = Statistics.empty()
         .toBuilder()
         .toBuilder()

+ 2 - 2
kafka-ui-e2e-checks/pom.xml

@@ -19,12 +19,12 @@
         <selenium.version>4.8.1</selenium.version>
         <selenium.version>4.8.1</selenium.version>
         <selenide.version>6.12.3</selenide.version>
         <selenide.version>6.12.3</selenide.version>
         <testng.version>7.7.1</testng.version>
         <testng.version>7.7.1</testng.version>
-        <allure.version>2.21.0</allure.version>
+        <allure.version>2.22.2</allure.version>
         <qase.io.version>3.0.4</qase.io.version>
         <qase.io.version>3.0.4</qase.io.version>
         <aspectj.version>1.9.9.1</aspectj.version>
         <aspectj.version>1.9.9.1</aspectj.version>
         <assertj.version>3.24.2</assertj.version>
         <assertj.version>3.24.2</assertj.version>
         <hamcrest.version>2.2</hamcrest.version>
         <hamcrest.version>2.2</hamcrest.version>
-        <slf4j.version>2.0.5</slf4j.version>
+        <slf4j.version>2.0.7</slf4j.version>
         <kafka.version>3.3.1</kafka.version>
         <kafka.version>3.3.1</kafka.version>
     </properties>
     </properties>
 
 

+ 15 - 0
kafka-ui-e2e-checks/src/main/java/com/provectus/kafka/ui/utilities/StringUtils.java

@@ -0,0 +1,15 @@
+package com.provectus.kafka.ui.utilities;
+
+import java.util.stream.IntStream;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class StringUtils {
+
+  public static String getMixedCase(String original) {
+    return IntStream.range(0, original.length())
+        .mapToObj(i -> i % 2 == 0 ? Character.toUpperCase(original.charAt(i)) : original.charAt(i))
+        .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
+        .toString();
+  }
+}

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

@@ -1,6 +1,5 @@
 package com.provectus.kafka.ui.manualsuite.backlog;
 package com.provectus.kafka.ui.manualsuite.backlog;
 
 
-import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.BROKERS_SUITE_ID;
 import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.SCHEMAS_SUITE_ID;
 import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.SCHEMAS_SUITE_ID;
 import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.TOPICS_PROFILE_SUITE_ID;
 import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.TOPICS_PROFILE_SUITE_ID;
 import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.TOPICS_SUITE_ID;
 import static com.provectus.kafka.ui.qasesuite.BaseQaseTest.TOPICS_SUITE_ID;
@@ -57,24 +56,17 @@ public class SmokeBacklog extends BaseManualTest {
   public void testCaseF() {
   public void testCaseF() {
   }
   }
 
 
-  @Automation(state = TO_BE_AUTOMATED)
-  @Suite(id = BROKERS_SUITE_ID)
-  @QaseId(348)
-  @Test
-  public void testCaseG() {
-  }
-
   @Automation(state = NOT_AUTOMATED)
   @Automation(state = NOT_AUTOMATED)
   @Suite(id = TOPICS_SUITE_ID)
   @Suite(id = TOPICS_SUITE_ID)
   @QaseId(50)
   @QaseId(50)
   @Test
   @Test
-  public void testCaseH() {
+  public void testCaseG() {
   }
   }
 
 
   @Automation(state = NOT_AUTOMATED)
   @Automation(state = NOT_AUTOMATED)
   @Suite(id = SCHEMAS_SUITE_ID)
   @Suite(id = SCHEMAS_SUITE_ID)
   @QaseId(351)
   @QaseId(351)
   @Test
   @Test
-  public void testCaseI() {
+  public void testCaseH() {
   }
   }
 }
 }

+ 34 - 0
kafka-ui-e2e-checks/src/test/java/com/provectus/kafka/ui/smokesuite/brokers/BrokersTest.java

@@ -1,6 +1,7 @@
 package com.provectus.kafka.ui.smokesuite.brokers;
 package com.provectus.kafka.ui.smokesuite.brokers;
 
 
 import static com.provectus.kafka.ui.pages.brokers.BrokersDetails.DetailsTab.CONFIGS;
 import static com.provectus.kafka.ui.pages.brokers.BrokersDetails.DetailsTab.CONFIGS;
+import static com.provectus.kafka.ui.utilities.StringUtils.getMixedCase;
 import static com.provectus.kafka.ui.variables.Expected.BROKER_SOURCE_INFO_TOOLTIP;
 import static com.provectus.kafka.ui.variables.Expected.BROKER_SOURCE_INFO_TOOLTIP;
 
 
 import com.codeborne.selenide.Condition;
 import com.codeborne.selenide.Condition;
@@ -8,6 +9,7 @@ import com.provectus.kafka.ui.BaseTest;
 import com.provectus.kafka.ui.pages.brokers.BrokersConfigTab;
 import com.provectus.kafka.ui.pages.brokers.BrokersConfigTab;
 import io.qameta.allure.Issue;
 import io.qameta.allure.Issue;
 import io.qase.api.annotation.QaseId;
 import io.qase.api.annotation.QaseId;
+import java.util.List;
 import org.testng.Assert;
 import org.testng.Assert;
 import org.testng.annotations.Ignore;
 import org.testng.annotations.Ignore;
 import org.testng.annotations.Test;
 import org.testng.annotations.Test;
@@ -100,6 +102,38 @@ public class BrokersTest extends BaseTest {
         String.format("getAllConfigs().contains(%s)", anyConfigKeySecondPage));
         String.format("getAllConfigs().contains(%s)", anyConfigKeySecondPage));
   }
   }
 
 
+  @Ignore
+  @Issue("https://github.com/provectus/kafka-ui/issues/3347")
+  @QaseId(348)
+  @Test
+  public void brokersConfigCaseInsensitiveSearchCheck() {
+    navigateToBrokersAndOpenDetails(DEFAULT_BROKER_ID);
+    brokersDetails
+        .openDetailsTab(CONFIGS);
+    String anyConfigKeyFirstPage = brokersConfigTab
+        .getAllConfigs().stream()
+        .findAny().orElseThrow()
+        .getKey();
+    brokersConfigTab
+        .clickNextButton();
+    Assert.assertFalse(brokersConfigTab.getAllConfigs().stream()
+            .map(BrokersConfigTab.BrokersConfigItem::getKey)
+            .toList().contains(anyConfigKeyFirstPage),
+        String.format("getAllConfigs().contains(%s)", anyConfigKeyFirstPage));
+    SoftAssert softly = new SoftAssert();
+    List.of(anyConfigKeyFirstPage.toLowerCase(), anyConfigKeyFirstPage.toUpperCase(),
+            getMixedCase(anyConfigKeyFirstPage))
+        .forEach(configCase -> {
+          brokersConfigTab
+              .searchConfig(configCase);
+          softly.assertTrue(brokersConfigTab.getAllConfigs().stream()
+                  .map(BrokersConfigTab.BrokersConfigItem::getKey)
+                  .toList().contains(anyConfigKeyFirstPage),
+              String.format("getAllConfigs().contains(%s)", configCase));
+        });
+    softly.assertAll();
+  }
+
   @QaseId(331)
   @QaseId(331)
   @Test
   @Test
   public void brokersSourceInfoCheck() {
   public void brokersSourceInfoCheck() {

+ 1 - 1
kafka-ui-react-app/public/robots.txt

@@ -1,2 +1,2 @@
-# https://www.robotstxt.org/robotstxt.html
 User-agent: *
 User-agent: *
+Disallow: /

+ 16 - 4
kafka-ui-react-app/src/components/ConsumerGroups/Details/Details.tsx

@@ -16,13 +16,15 @@ import { Table } from 'components/common/table/Table/Table.styled';
 import getTagColor from 'components/common/Tag/getTagColor';
 import getTagColor from 'components/common/Tag/getTagColor';
 import { Dropdown } 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 { Action, ConsumerGroupState, ResourceType } from 'generated-sources';
 import { ActionDropdownItem } from 'components/common/ActionComponent';
 import { ActionDropdownItem } from 'components/common/ActionComponent';
 import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
 import TableHeaderCell from 'components/common/table/TableHeaderCell/TableHeaderCell';
 import {
 import {
   useConsumerGroupDetails,
   useConsumerGroupDetails,
   useDeleteConsumerGroupMutation,
   useDeleteConsumerGroupMutation,
 } from 'lib/hooks/api/consumers';
 } from 'lib/hooks/api/consumers';
+import Tooltip from 'components/common/Tooltip/Tooltip';
+import { CONSUMER_GROUP_STATE_TOOLTIPS } from 'lib/constants';
 
 
 import ListItem from './ListItem';
 import ListItem from './ListItem';
 
 
@@ -96,9 +98,19 @@ const Details: React.FC = () => {
       <Metrics.Wrapper>
       <Metrics.Wrapper>
         <Metrics.Section>
         <Metrics.Section>
           <Metrics.Indicator label="State">
           <Metrics.Indicator label="State">
-            <Tag color={getTagColor(consumerGroup.data?.state)}>
-              {consumerGroup.data?.state}
-            </Tag>
+            <Tooltip
+              value={
+                <Tag color={getTagColor(consumerGroup.data?.state)}>
+                  {consumerGroup.data?.state}
+                </Tag>
+              }
+              content={
+                CONSUMER_GROUP_STATE_TOOLTIPS[
+                  consumerGroup.data?.state || ConsumerGroupState.UNKNOWN
+                ]
+              }
+              placement="bottom-start"
+            />
           </Metrics.Indicator>
           </Metrics.Indicator>
           <Metrics.Indicator label="Members">
           <Metrics.Indicator label="Members">
             {consumerGroup.data?.members}
             {consumerGroup.data?.members}

+ 18 - 3
kafka-ui-react-app/src/components/ConsumerGroups/List.tsx

@@ -5,15 +5,17 @@ import { ControlPanelWrapper } from 'components/common/ControlPanel/ControlPanel
 import {
 import {
   ConsumerGroupDetails,
   ConsumerGroupDetails,
   ConsumerGroupOrdering,
   ConsumerGroupOrdering,
+  ConsumerGroupState,
   SortOrder,
   SortOrder,
 } from 'generated-sources';
 } from 'generated-sources';
 import useAppParams from 'lib/hooks/useAppParams';
 import useAppParams from 'lib/hooks/useAppParams';
 import { clusterConsumerGroupDetailsPath, ClusterNameRoute } from 'lib/paths';
 import { clusterConsumerGroupDetailsPath, ClusterNameRoute } from 'lib/paths';
 import { ColumnDef } from '@tanstack/react-table';
 import { ColumnDef } from '@tanstack/react-table';
-import Table, { TagCell, LinkCell } from 'components/common/NewTable';
+import Table, { LinkCell, TagCell } from 'components/common/NewTable';
 import { useNavigate, useSearchParams } from 'react-router-dom';
 import { useNavigate, useSearchParams } from 'react-router-dom';
-import { PER_PAGE } from 'lib/constants';
+import { CONSUMER_GROUP_STATE_TOOLTIPS, PER_PAGE } from 'lib/constants';
 import { useConsumerGroups } from 'lib/hooks/api/consumers';
 import { useConsumerGroups } from 'lib/hooks/api/consumers';
+import Tooltip from 'components/common/Tooltip/Tooltip';
 
 
 const List = () => {
 const List = () => {
   const { clusterName } = useAppParams<ClusterNameRoute>();
   const { clusterName } = useAppParams<ClusterNameRoute>();
@@ -59,6 +61,9 @@ const List = () => {
         id: ConsumerGroupOrdering.MESSAGES_BEHIND,
         id: ConsumerGroupOrdering.MESSAGES_BEHIND,
         header: 'Consumer Lag',
         header: 'Consumer Lag',
         accessorKey: 'consumerLag',
         accessorKey: 'consumerLag',
+        cell: (args) => {
+          return args.getValue() || 'N/A';
+        },
       },
       },
       {
       {
         header: 'Coordinator',
         header: 'Coordinator',
@@ -69,7 +74,17 @@ const List = () => {
         id: ConsumerGroupOrdering.STATE,
         id: ConsumerGroupOrdering.STATE,
         header: 'State',
         header: 'State',
         accessorKey: 'state',
         accessorKey: 'state',
-        cell: TagCell,
+        // eslint-disable-next-line react/no-unstable-nested-components
+        cell: (args) => {
+          const value = args.getValue() as ConsumerGroupState;
+          return (
+            <Tooltip
+              value={<TagCell {...args} />}
+              content={CONSUMER_GROUP_STATE_TOOLTIPS[value]}
+              placement="bottom-end"
+            />
+          );
+        },
       },
       },
     ],
     ],
     []
     []

+ 12 - 1
kafka-ui-react-app/src/lib/constants.ts

@@ -1,5 +1,5 @@
 import { SelectOption } from 'components/common/Select/Select';
 import { SelectOption } from 'components/common/Select/Select';
-import { ConfigurationParameters } from 'generated-sources';
+import { ConfigurationParameters, ConsumerGroupState } from 'generated-sources';
 
 
 declare global {
 declare global {
   interface Window {
   interface Window {
@@ -96,3 +96,14 @@ export const METRICS_OPTIONS: SelectOption[] = [
   { value: 'JMX', label: 'JMX' },
   { value: 'JMX', label: 'JMX' },
   { value: 'PROMETHEUS', label: 'PROMETHEUS' },
   { value: 'PROMETHEUS', label: 'PROMETHEUS' },
 ];
 ];
+
+export const CONSUMER_GROUP_STATE_TOOLTIPS: Record<ConsumerGroupState, string> =
+  {
+    EMPTY: 'The group exists but has no members.',
+    STABLE: 'Consumers are happily consuming and have assigned partitions.',
+    PREPARING_REBALANCE:
+      'Something has changed, and the reassignment of partitions is required.',
+    COMPLETING_REBALANCE: 'Partition reassignment is in progress.',
+    DEAD: 'The group is going to be removed. It might be due to the inactivity, or the group is being migrated to different group coordinator.',
+    UNKNOWN: '',
+  } as const;

+ 5 - 0
kafka-ui-react-app/src/lib/hooks/api/topics.ts

@@ -304,6 +304,11 @@ export function useTopicAnalysis(
       useErrorBoundary: true,
       useErrorBoundary: true,
       retry: false,
       retry: false,
       suspense: false,
       suspense: false,
+      onError: (error: Response) => {
+        if (error.status !== 404) {
+          showServerError(error as Response);
+        }
+      },
     }
     }
   );
   );
 }
 }

+ 2 - 2
pom.xml

@@ -31,9 +31,9 @@
         <groovy.version>3.0.13</groovy.version>
         <groovy.version>3.0.13</groovy.version>
         <jackson.version>2.14.0</jackson.version>
         <jackson.version>2.14.0</jackson.version>
         <kafka-clients.version>3.3.1</kafka-clients.version>
         <kafka-clients.version>3.3.1</kafka-clients.version>
-        <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
+        <org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
         <org.projectlombok.version>1.18.24</org.projectlombok.version>
         <org.projectlombok.version>1.18.24</org.projectlombok.version>
-        <protobuf-java.version>3.21.9</protobuf-java.version>
+        <protobuf-java.version>3.23.3</protobuf-java.version>
         <scala-lang.library.version>2.13.9</scala-lang.library.version>
         <scala-lang.library.version>2.13.9</scala-lang.library.version>
         <snakeyaml.version>2.0</snakeyaml.version>
         <snakeyaml.version>2.0</snakeyaml.version>
         <spring-boot.version>3.0.6</spring-boot.version>
         <spring-boot.version>3.0.6</spring-boot.version>