Преглед изворни кода

Merge branch 'master' into sort-by-topic-fix-style

Roman Zabaluev пре 2 година
родитељ
комит
37f4f98136

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

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

+ 2 - 1
SECURITY.md

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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