Browse Source

Merge remote-tracking branch 'origin/master' into issues/2844

# Conflicts:
#	kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthSecurityConfig.java
#	kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AccessControlService.java
#	kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/LdapAuthorityExtractor.java
Roman Zabaluev 2 years ago
parent
commit
29bd456a0b
24 changed files with 334 additions and 305 deletions
  1. 26 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapProperties.java
  2. 61 35
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapSecurityConfig.java
  3. 1 1
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/OAuthSecurityConfig.java
  4. 60 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacLdapUser.java
  5. 21 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/condition/ActiveDirectoryCondition.java
  6. 2 2
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ConsumerRecordDeserializer.java
  7. 0 2
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/SerdesInitializer.java
  8. 1 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/KafkaConnectService.java
  9. 3 4
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisStats.java
  10. 19 11
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/AccessControlService.java
  11. 0 23
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/LdapAuthorityExtractor.java
  12. 70 0
      kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java
  13. 2 0
      kafka-ui-react-app/src/components/Topics/Topic/Messages/Message.tsx
  14. 6 7
      kafka-ui-react-app/src/components/Topics/Topic/Messages/MessageContent/MessageContent.tsx
  15. 7 66
      kafka-ui-react-app/src/components/Topics/Topic/Messages/MessageContent/__tests__/MessageContent.spec.tsx
  16. 8 22
      kafka-ui-react-app/src/components/Version/Version.tsx
  17. 19 66
      kafka-ui-react-app/src/components/Version/__tests__/Version.spec.tsx
  18. 1 0
      kafka-ui-react-app/src/components/common/Icons/WarningIcon.tsx
  19. 0 12
      kafka-ui-react-app/src/lib/fixtures/actuatorInfo.ts
  20. 14 1
      kafka-ui-react-app/src/lib/fixtures/latestVersion.ts
  21. 0 17
      kafka-ui-react-app/src/lib/hooks/api/__tests__/actuatorInfo.spec.ts
  22. 5 7
      kafka-ui-react-app/src/lib/hooks/api/__tests__/latestVersion.spec.ts
  23. 0 19
      kafka-ui-react-app/src/lib/hooks/api/actuatorInfo.ts
  24. 8 10
      kafka-ui-react-app/src/lib/hooks/api/latestVersion.ts

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

@@ -0,0 +1,26 @@
+package com.provectus.kafka.ui.config.auth;
+
+import lombok.Data;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties("spring.ldap")
+@Data
+public class LdapProperties {
+
+  private String urls;
+  private String base;
+  private String adminUser;
+  private String adminPassword;
+  private String userFilterSearchBase;
+  private String userFilterSearchFilter;
+
+  @Value("${oauth2.ldap.activeDirectory:false}")
+  private boolean isActiveDirectory;
+  @Value("${oauth2.ldap.aсtiveDirectory.domain:@null}")
+  private String activeDirectoryDomain;
+
+  @Value("${oauth2.ldap.groupRoleAttribute:cn}")
+  private String groupRoleAttribute;
+
+}

+ 61 - 35
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapSecurityConfig.java

@@ -1,13 +1,23 @@
 package com.provectus.kafka.ui.config.auth;
 package com.provectus.kafka.ui.config.auth;
 
 
+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.extractor.RbacLdapAuthoritiesExtractor;
+import java.util.Collection;
 import java.util.List;
 import java.util.List;
+import javax.annotation.Nullable;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
 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.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;
+import org.springframework.context.annotation.Primary;
+import org.springframework.ldap.core.DirContextOperations;
 import org.springframework.ldap.core.support.BaseLdapPathContextSource;
 import org.springframework.ldap.core.support.BaseLdapPathContextSource;
 import org.springframework.ldap.core.support.LdapContextSource;
 import org.springframework.ldap.core.support.LdapContextSource;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.AuthenticationManager;
@@ -16,70 +26,71 @@ import org.springframework.security.authentication.ReactiveAuthenticationManager
 import org.springframework.security.authentication.ReactiveAuthenticationManagerAdapter;
 import org.springframework.security.authentication.ReactiveAuthenticationManagerAdapter;
 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.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider;
 import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider;
 import org.springframework.security.ldap.authentication.BindAuthenticator;
 import org.springframework.security.ldap.authentication.BindAuthenticator;
 import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
 import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
 import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
 import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
 import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
 import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
 import org.springframework.security.ldap.search.LdapUserSearch;
 import org.springframework.security.ldap.search.LdapUserSearch;
+import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper;
 import org.springframework.security.web.server.SecurityWebFilterChain;
 import org.springframework.security.web.server.SecurityWebFilterChain;
 
 
 @Configuration
 @Configuration
 @EnableWebFluxSecurity
 @EnableWebFluxSecurity
 @ConditionalOnProperty(value = "auth.type", havingValue = "LDAP")
 @ConditionalOnProperty(value = "auth.type", havingValue = "LDAP")
 @Import(LdapAutoConfiguration.class)
 @Import(LdapAutoConfiguration.class)
+@EnableConfigurationProperties(LdapProperties.class)
+@RequiredArgsConstructor
 @Slf4j
 @Slf4j
-public class LdapSecurityConfig extends AbstractAuthSecurityConfig {
-
-  @Value("${spring.ldap.urls}")
-  private String ldapUrls;
-  @Value("${spring.ldap.dn.pattern:#{null}}")
-  private String ldapUserDnPattern;
-  @Value("${spring.ldap.adminUser:#{null}}")
-  private String adminUser;
-  @Value("${spring.ldap.adminPassword:#{null}}")
-  private String adminPassword;
-  @Value("${spring.ldap.userFilter.searchBase:#{null}}")
-  private String userFilterSearchBase;
-  @Value("${spring.ldap.userFilter.searchFilter:#{null}}")
-  private String userFilterSearchFilter;
-
-  @Value("${oauth2.ldap.activeDirectory:false}")
-  private boolean isActiveDirectory;
-  @Value("${oauth2.ldap.aсtiveDirectory.domain:#{null}}")
-  private String activeDirectoryDomain;
+public class LdapSecurityConfig {
+
+  private final LdapProperties props;
 
 
   @Bean
   @Bean
-  public ReactiveAuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource) {
+  public ReactiveAuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource,
+                                                             ApplicationContext context,
+                                                             @Nullable AccessControlService acs) {
+    var rbacEnabled = acs != null && acs.isRbacEnabled();
     BindAuthenticator ba = new BindAuthenticator(contextSource);
     BindAuthenticator ba = new BindAuthenticator(contextSource);
-    if (ldapUserDnPattern != null) {
-      ba.setUserDnPatterns(new String[] {ldapUserDnPattern});
+    if (props.getBase() != null) {
+      ba.setUserDnPatterns(new String[] {props.getBase()});
     }
     }
-    if (userFilterSearchFilter != null) {
+    if (props.getUserFilterSearchFilter() != null) {
       LdapUserSearch userSearch =
       LdapUserSearch userSearch =
-          new FilterBasedLdapUserSearch(userFilterSearchBase, userFilterSearchFilter, contextSource);
+          new FilterBasedLdapUserSearch(props.getUserFilterSearchBase(), props.getUserFilterSearchFilter(),
+              contextSource);
       ba.setUserSearch(userSearch);
       ba.setUserSearch(userSearch);
     }
     }
 
 
     AbstractLdapAuthenticationProvider authenticationProvider;
     AbstractLdapAuthenticationProvider authenticationProvider;
-    if (!isActiveDirectory) {
-      authenticationProvider = new LdapAuthenticationProvider(ba);
+    if (!props.isActiveDirectory()) {
+      authenticationProvider = rbacEnabled
+          ? new LdapAuthenticationProvider(ba, new RbacLdapAuthoritiesExtractor(context))
+          : new LdapAuthenticationProvider(ba);
     } else {
     } else {
-      authenticationProvider = new ActiveDirectoryLdapAuthenticationProvider(activeDirectoryDomain, ldapUrls);
+      authenticationProvider = new ActiveDirectoryLdapAuthenticationProvider(props.getActiveDirectoryDomain(),
+          props.getUrls()); // TODO Issue #3741
       authenticationProvider.setUseAuthenticationRequestCredentials(true);
       authenticationProvider.setUseAuthenticationRequestCredentials(true);
     }
     }
 
 
+    if (rbacEnabled) {
+      authenticationProvider.setUserDetailsContextMapper(new UserDetailsMapper());
+    }
+
     AuthenticationManager am = new ProviderManager(List.of(authenticationProvider));
     AuthenticationManager am = new ProviderManager(List.of(authenticationProvider));
 
 
     return new ReactiveAuthenticationManagerAdapter(am);
     return new ReactiveAuthenticationManagerAdapter(am);
   }
   }
 
 
   @Bean
   @Bean
+  @Primary
   public BaseLdapPathContextSource contextSource() {
   public BaseLdapPathContextSource contextSource() {
     LdapContextSource ctx = new LdapContextSource();
     LdapContextSource ctx = new LdapContextSource();
-    ctx.setUrl(ldapUrls);
-    ctx.setUserDn(adminUser);
-    ctx.setPassword(adminPassword);
+    ctx.setUrl(props.getUrls());
+    ctx.setUserDn(props.getAdminUser());
+    ctx.setPassword(props.getAdminPassword());
     ctx.afterPropertiesSet();
     ctx.afterPropertiesSet();
     return ctx;
     return ctx;
   }
   }
@@ -87,20 +98,35 @@ public class LdapSecurityConfig extends AbstractAuthSecurityConfig {
   @Bean
   @Bean
   public SecurityWebFilterChain configureLdap(ServerHttpSecurity http) {
   public SecurityWebFilterChain configureLdap(ServerHttpSecurity http) {
     log.info("Configuring LDAP authentication.");
     log.info("Configuring LDAP authentication.");
-    if (isActiveDirectory) {
+    if (props.isActiveDirectory()) {
       log.info("Active Directory support for LDAP has been enabled.");
       log.info("Active Directory support for LDAP has been enabled.");
     }
     }
 
 
-    http
+    return http
         .authorizeExchange()
         .authorizeExchange()
         .pathMatchers(AUTH_WHITELIST)
         .pathMatchers(AUTH_WHITELIST)
         .permitAll()
         .permitAll()
         .anyExchange()
         .anyExchange()
         .authenticated()
         .authenticated()
+
         .and()
         .and()
-        .httpBasic();
+        .formLogin()
 
 
-    return http.csrf().disable().build();
+        .and()
+        .logout()
+
+        .and()
+        .csrf().disable()
+        .build();
+  }
+
+  private static class UserDetailsMapper extends LdapUserDetailsMapper {
+    @Override
+    public UserDetails mapUserFromContext(DirContextOperations ctx, String username,
+                                          Collection<? extends GrantedAuthority> authorities) {
+      UserDetails userDetails = super.mapUserFromContext(ctx, username, authorities);
+      return new RbacLdapUser(userDetails);
+    }
   }
   }
 
 
 }
 }

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

@@ -115,7 +115,7 @@ public class OAuthSecurityConfig extends AbstractAuthSecurityConfig {
   @Nullable
   @Nullable
   private ProviderAuthorityExtractor getExtractor(final String providerId, AccessControlService acs) {
   private ProviderAuthorityExtractor getExtractor(final String providerId, AccessControlService acs) {
     final var provider = getProviderByProviderId(providerId);
     final var provider = getProviderByProviderId(providerId);
-    Optional<ProviderAuthorityExtractor> extractor = acs.getExtractors()
+    Optional<ProviderAuthorityExtractor> extractor = acs.getOauthExtractors()
         .stream()
         .stream()
         .filter(e -> e.isApplicable(provider.getProvider(), provider.getCustomParams()))
         .filter(e -> e.isApplicable(provider.getProvider(), provider.getCustomParams()))
         .findFirst();
         .findFirst();

+ 60 - 0
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/RbacLdapUser.java

@@ -0,0 +1,60 @@
+package com.provectus.kafka.ui.config.auth;
+
+import java.util.Collection;
+import java.util.stream.Collectors;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+public class RbacLdapUser implements UserDetails, RbacUser {
+
+  private final UserDetails userDetails;
+
+  public RbacLdapUser(UserDetails userDetails) {
+    this.userDetails = userDetails;
+  }
+
+  @Override
+  public String name() {
+    return userDetails.getUsername();
+  }
+
+  @Override
+  public Collection<String> groups() {
+    return userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
+  }
+
+  @Override
+  public Collection<? extends GrantedAuthority> getAuthorities() {
+    return userDetails.getAuthorities();
+  }
+
+  @Override
+  public String getPassword() {
+    return userDetails.getPassword();
+  }
+
+  @Override
+  public String getUsername() {
+    return userDetails.getUsername();
+  }
+
+  @Override
+  public boolean isAccountNonExpired() {
+    return userDetails.isAccountNonExpired();
+  }
+
+  @Override
+  public boolean isAccountNonLocked() {
+    return userDetails.isAccountNonLocked();
+  }
+
+  @Override
+  public boolean isCredentialsNonExpired() {
+    return userDetails.isCredentialsNonExpired();
+  }
+
+  @Override
+  public boolean isEnabled() {
+    return userDetails.isEnabled();
+  }
+}

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

@@ -0,0 +1,21 @@
+package com.provectus.kafka.ui.config.auth.condition;
+
+import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+
+public class ActiveDirectoryCondition extends AllNestedConditions {
+
+  public ActiveDirectoryCondition() {
+    super(ConfigurationPhase.PARSE_CONFIGURATION);
+  }
+
+  @ConditionalOnProperty(value = "auth.type", havingValue = "LDAP")
+  public static class OnAuthType {
+
+  }
+
+  @ConditionalOnProperty(value = "${oauth2.ldap.activeDirectory}:false", havingValue = "true", matchIfMissing = false)
+  public static class OnActiveDirectory {
+
+  }
+}

+ 2 - 2
kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/ConsumerRecordDeserializer.java

@@ -123,11 +123,11 @@ public class ConsumerRecordDeserializer {
   }
   }
 
 
   private static Long getKeySize(ConsumerRecord<Bytes, Bytes> consumerRecord) {
   private static Long getKeySize(ConsumerRecord<Bytes, Bytes> consumerRecord) {
-    return consumerRecord.key() != null ? (long) consumerRecord.key().get().length : null;
+    return consumerRecord.key() != null ? (long) consumerRecord.serializedKeySize() : null;
   }
   }
 
 
   private static Long getValueSize(ConsumerRecord<Bytes, Bytes> consumerRecord) {
   private static Long getValueSize(ConsumerRecord<Bytes, Bytes> consumerRecord) {
-    return consumerRecord.value() != null ? (long) consumerRecord.value().get().length : null;
+    return consumerRecord.value() != null ? (long) consumerRecord.serializedValueSize() : null;
   }
   }
 
 
   private static int headerSize(Header header) {
   private static int headerSize(Header header) {

+ 0 - 2
kafka-ui-api/src/main/java/com/provectus/kafka/ui/serdes/SerdesInitializer.java

@@ -122,8 +122,6 @@ public class SerdesInitializer {
         registeredSerdes,
         registeredSerdes,
         Optional.ofNullable(clusterProperties.getDefaultKeySerde())
         Optional.ofNullable(clusterProperties.getDefaultKeySerde())
             .map(name -> Preconditions.checkNotNull(registeredSerdes.get(name), "Default key serde not found"))
             .map(name -> Preconditions.checkNotNull(registeredSerdes.get(name), "Default key serde not found"))
-            .or(() -> Optional.ofNullable(registeredSerdes.get(SchemaRegistrySerde.name())))
-            .or(() -> Optional.ofNullable(registeredSerdes.get(ProtobufFileSerde.name())))
             .orElse(null),
             .orElse(null),
         Optional.ofNullable(clusterProperties.getDefaultValueSerde())
         Optional.ofNullable(clusterProperties.getDefaultValueSerde())
             .map(name -> Preconditions.checkNotNull(registeredSerdes.get(name), "Default value serde not found"))
             .map(name -> Preconditions.checkNotNull(registeredSerdes.get(name), "Default value serde not found"))

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

@@ -109,6 +109,7 @@ public class KafkaConnectService {
   private Stream<String> getStringsForSearch(FullConnectorInfoDTO fullConnectorInfo) {
   private Stream<String> getStringsForSearch(FullConnectorInfoDTO fullConnectorInfo) {
     return Stream.of(
     return Stream.of(
         fullConnectorInfo.getName(),
         fullConnectorInfo.getName(),
+        fullConnectorInfo.getConnect(),
         fullConnectorInfo.getStatus().getState().getValue(),
         fullConnectorInfo.getStatus().getState().getValue(),
         fullConnectorInfo.getType().getValue());
         fullConnectorInfo.getType().getValue());
   }
   }

+ 3 - 4
kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/analyze/TopicAnalysisStats.java

@@ -43,8 +43,7 @@ class TopicAnalysisStats {
     Long max;
     Long max;
     final UpdateDoublesSketch sizeSketch = DoublesSketch.builder().build();
     final UpdateDoublesSketch sizeSketch = DoublesSketch.builder().build();
 
 
-    void apply(byte[] bytes) {
-      int len = bytes.length;
+    void apply(int len) {
       sum += len;
       sum += len;
       min = minNullable(min, len);
       min = minNullable(min, len);
       max = maxNullable(max, len);
       max = maxNullable(max, len);
@@ -98,7 +97,7 @@ class TopicAnalysisStats {
 
 
     if (rec.key() != null) {
     if (rec.key() != null) {
       byte[] keyBytes = rec.key().get();
       byte[] keyBytes = rec.key().get();
-      keysSize.apply(keyBytes);
+      keysSize.apply(rec.serializedKeySize());
       uniqKeys.update(keyBytes);
       uniqKeys.update(keyBytes);
     } else {
     } else {
       nullKeys++;
       nullKeys++;
@@ -106,7 +105,7 @@ class TopicAnalysisStats {
 
 
     if (rec.value() != null) {
     if (rec.value() != null) {
       byte[] valueBytes = rec.value().get();
       byte[] valueBytes = rec.value().get();
-      valuesSize.apply(valueBytes);
+      valuesSize.apply(rec.serializedValueSize());
       uniqValues.update(valueBytes);
       uniqValues.update(valueBytes);
     } else {
     } else {
       nullValues++;
       nullValues++;

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

@@ -12,6 +12,7 @@ import com.provectus.kafka.ui.model.rbac.AccessContext;
 import com.provectus.kafka.ui.model.rbac.Permission;
 import com.provectus.kafka.ui.model.rbac.Permission;
 import com.provectus.kafka.ui.model.rbac.Resource;
 import com.provectus.kafka.ui.model.rbac.Resource;
 import com.provectus.kafka.ui.model.rbac.Role;
 import com.provectus.kafka.ui.model.rbac.Role;
+import com.provectus.kafka.ui.model.rbac.Subject;
 import com.provectus.kafka.ui.model.rbac.permission.ConnectAction;
 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.ConsumerGroupAction;
 import com.provectus.kafka.ui.model.rbac.permission.SchemaAction;
 import com.provectus.kafka.ui.model.rbac.permission.SchemaAction;
@@ -19,12 +20,12 @@ 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.CognitoAuthorityExtractor;
 import com.provectus.kafka.ui.service.rbac.extractor.GithubAuthorityExtractor;
 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.GoogleAuthorityExtractor;
-import com.provectus.kafka.ui.service.rbac.extractor.LdapAuthorityExtractor;
 import com.provectus.kafka.ui.service.rbac.extractor.OauthAuthorityExtractor;
 import com.provectus.kafka.ui.service.rbac.extractor.OauthAuthorityExtractor;
 import com.provectus.kafka.ui.service.rbac.extractor.ProviderAuthorityExtractor;
 import com.provectus.kafka.ui.service.rbac.extractor.ProviderAuthorityExtractor;
 import jakarta.annotation.PostConstruct;
 import jakarta.annotation.PostConstruct;
 import java.util.Collections;
 import java.util.Collections;
 import java.util.List;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 import java.util.Set;
 import java.util.function.Predicate;
 import java.util.function.Predicate;
 import java.util.regex.Pattern;
 import java.util.regex.Pattern;
@@ -35,6 +36,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.core.env.Environment;
 import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.core.context.ReactiveSecurityContextHolder;
 import org.springframework.security.core.context.ReactiveSecurityContextHolder;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContext;
@@ -51,10 +53,11 @@ public class AccessControlService {
 
 
   @Nullable
   @Nullable
   private final InMemoryReactiveClientRegistrationRepository clientRegistrationRepository;
   private final InMemoryReactiveClientRegistrationRepository clientRegistrationRepository;
+  private final RoleBasedAccessControlProperties properties;
+  private final Environment environment;
 
 
   private boolean rbacEnabled = false;
   private boolean rbacEnabled = false;
-  private Set<ProviderAuthorityExtractor> extractors = Collections.emptySet();
-  private final RoleBasedAccessControlProperties properties;
+  private Set<ProviderAuthorityExtractor> oauthExtractors = Collections.emptySet();
 
 
   @PostConstruct
   @PostConstruct
   public void init() {
   public void init() {
@@ -64,22 +67,27 @@ public class AccessControlService {
     }
     }
     rbacEnabled = true;
     rbacEnabled = true;
 
 
-    this.extractors = properties.getRoles()
+    this.oauthExtractors = properties.getRoles()
         .stream()
         .stream()
         .map(role -> role.getSubjects()
         .map(role -> role.getSubjects()
             .stream()
             .stream()
-            .map(provider -> switch (provider.getProvider()) {
+            .map(Subject::getProvider)
+            .distinct()
+            .map(provider -> switch (provider) {
               case OAUTH_COGNITO -> new CognitoAuthorityExtractor();
               case OAUTH_COGNITO -> new CognitoAuthorityExtractor();
               case OAUTH_GOOGLE -> new GoogleAuthorityExtractor();
               case OAUTH_GOOGLE -> new GoogleAuthorityExtractor();
               case OAUTH_GITHUB -> new GithubAuthorityExtractor();
               case OAUTH_GITHUB -> new GithubAuthorityExtractor();
               case OAUTH -> new OauthAuthorityExtractor();
               case OAUTH -> new OauthAuthorityExtractor();
-              case LDAP, LDAP_AD -> new LdapAuthorityExtractor();
-            }).collect(Collectors.toSet()))
+              default -> null;
+            })
+            .filter(Objects::nonNull)
+            .collect(Collectors.toSet()))
         .flatMap(Set::stream)
         .flatMap(Set::stream)
         .collect(Collectors.toSet());
         .collect(Collectors.toSet());
 
 
-    if ((clientRegistrationRepository == null || !clientRegistrationRepository.iterator().hasNext())
-        && !properties.getRoles().isEmpty()) {
+    if (!properties.getRoles().isEmpty()
+        && "oauth2".equalsIgnoreCase(environment.getProperty("auth.type"))
+        && (clientRegistrationRepository == null || !clientRegistrationRepository.iterator().hasNext())) {
       log.error("Roles are configured but no authentication methods are present. Authentication might fail.");
       log.error("Roles are configured but no authentication methods are present. Authentication might fail.");
     }
     }
   }
   }
@@ -356,8 +364,8 @@ public class AccessControlService {
     return isAccessible(Resource.KSQL, null, user, context, requiredActions);
     return isAccessible(Resource.KSQL, null, user, context, requiredActions);
   }
   }
 
 
-  public Set<ProviderAuthorityExtractor> getExtractors() {
-    return extractors;
+  public Set<ProviderAuthorityExtractor> getOauthExtractors() {
+    return oauthExtractors;
   }
   }
 
 
   public List<Role> getRoles() {
   public List<Role> getRoles() {

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

@@ -1,23 +0,0 @@
-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, Map<String, String> params) {
-    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
-  }
-
-}

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

@@ -0,0 +1,70 @@
+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.function.Function;
+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;
+
+  private final Function<Map<String, List<String>>, GrantedAuthority> authorityMapper = (record) -> {
+    String role = record.get(getGroupRoleAttribute()).get(0);
+    return new SimpleGrantedAuthority(role);
+  };
+
+  public RbacLdapAuthoritiesExtractor(ApplicationContext context) {
+    super(context.getBean(BaseLdapPathContextSource.class), null);
+    this.acs = context.getBean(AccessControlService.class);
+    this.props = context.getBean(LdapProperties.class);
+  }
+
+  @Override
+  public Set<GrantedAuthority> getAdditionalRoles(DirContextOperations user, String username) {
+    return acs.getRoles()
+        .stream()
+        .map(Role::getSubjects)
+        .flatMap(List::stream)
+        .filter(s -> s.getProvider().equals(Provider.LDAP))
+        .filter(s -> s.getType().equals("group"))
+        .flatMap(subject -> getRoles(subject.getValue(), user.getNameInNamespace(), username).stream())
+        .collect(Collectors.toSet());
+  }
+
+  private Set<GrantedAuthority> getRoles(String groupSearchBase, String userDn, String username) {
+    Assert.notNull(groupSearchBase, "groupSearchBase is empty");
+
+    log.trace(
+        "Searching for roles for user [{}] with DN [{}], groupRoleAttribute [{}] and filter [{}] in search base [{}]",
+        username, userDn, props.getGroupRoleAttribute(), getGroupSearchFilter(), groupSearchBase);
+
+    var ldapTemplate = getLdapTemplate();
+    ldapTemplate.setIgnoreNameNotFoundException(true);
+
+    Set<Map<String, List<String>>> userRoles = ldapTemplate.searchForMultipleAttributeValues(
+        groupSearchBase, getGroupSearchFilter(), new String[] {userDn, username},
+        new String[] {props.getGroupRoleAttribute()});
+
+    return userRoles.stream()
+        .map(authorityMapper)
+        .peek(a -> log.debug("Mapped role [{}] for user [{}]", a, username))
+        .collect(Collectors.toSet());
+  }
+
+}

+ 2 - 0
kafka-ui-react-app/src/components/Topics/Topic/Messages/Message.tsx

@@ -142,6 +142,8 @@ const Message: React.FC<Props> = ({
           timestampType={timestampType}
           timestampType={timestampType}
           keySize={keySize}
           keySize={keySize}
           contentSize={valueSize}
           contentSize={valueSize}
+          keySerde={keySerde}
+          valueSerde={valueSerde}
         />
         />
       )}
       )}
     </>
     </>

+ 6 - 7
kafka-ui-react-app/src/components/Topics/Topic/Messages/MessageContent/MessageContent.tsx

@@ -3,7 +3,6 @@ import EditorViewer from 'components/common/EditorViewer/EditorViewer';
 import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
 import BytesFormatted from 'components/common/BytesFormatted/BytesFormatted';
 import { SchemaType, TopicMessageTimestampTypeEnum } from 'generated-sources';
 import { SchemaType, TopicMessageTimestampTypeEnum } from 'generated-sources';
 import { formatTimestamp } from 'lib/dateTimeHelpers';
 import { formatTimestamp } from 'lib/dateTimeHelpers';
-import { useSearchParams } from 'react-router-dom';
 
 
 import * as S from './MessageContent.styled';
 import * as S from './MessageContent.styled';
 
 
@@ -17,6 +16,8 @@ export interface MessageContentProps {
   timestampType?: TopicMessageTimestampTypeEnum;
   timestampType?: TopicMessageTimestampTypeEnum;
   keySize?: number;
   keySize?: number;
   contentSize?: number;
   contentSize?: number;
+  keySerde?: string;
+  valueSerde?: string;
 }
 }
 
 
 const MessageContent: React.FC<MessageContentProps> = ({
 const MessageContent: React.FC<MessageContentProps> = ({
@@ -27,12 +28,10 @@ const MessageContent: React.FC<MessageContentProps> = ({
   timestampType,
   timestampType,
   keySize,
   keySize,
   contentSize,
   contentSize,
+  keySerde,
+  valueSerde,
 }) => {
 }) => {
   const [activeTab, setActiveTab] = React.useState<Tab>('content');
   const [activeTab, setActiveTab] = React.useState<Tab>('content');
-  const [searchParams] = useSearchParams();
-  const keyFormat = searchParams.get('keySerde') || '';
-  const valueFormat = searchParams.get('valueSerde') || '';
-
   const activeTabContent = () => {
   const activeTabContent = () => {
     switch (activeTab) {
     switch (activeTab) {
       case 'content':
       case 'content':
@@ -110,7 +109,7 @@ const MessageContent: React.FC<MessageContentProps> = ({
             <S.Metadata>
             <S.Metadata>
               <S.MetadataLabel>Key Serde</S.MetadataLabel>
               <S.MetadataLabel>Key Serde</S.MetadataLabel>
               <span>
               <span>
-                <S.MetadataValue>{keyFormat}</S.MetadataValue>
+                <S.MetadataValue>{keySerde}</S.MetadataValue>
                 <S.MetadataMeta>
                 <S.MetadataMeta>
                   Size: <BytesFormatted value={keySize} />
                   Size: <BytesFormatted value={keySize} />
                 </S.MetadataMeta>
                 </S.MetadataMeta>
@@ -120,7 +119,7 @@ const MessageContent: React.FC<MessageContentProps> = ({
             <S.Metadata>
             <S.Metadata>
               <S.MetadataLabel>Value Serde</S.MetadataLabel>
               <S.MetadataLabel>Value Serde</S.MetadataLabel>
               <span>
               <span>
-                <S.MetadataValue>{valueFormat}</S.MetadataValue>
+                <S.MetadataValue>{valueSerde}</S.MetadataValue>
                 <S.MetadataMeta>
                 <S.MetadataMeta>
                   Size: <BytesFormatted value={contentSize} />
                   Size: <BytesFormatted value={contentSize} />
                 </S.MetadataMeta>
                 </S.MetadataMeta>

+ 7 - 66
kafka-ui-react-app/src/components/Topics/Topic/Messages/MessageContent/__tests__/MessageContent.spec.tsx

@@ -20,6 +20,8 @@ const setupWrapper = (props?: Partial<MessageContentProps>) => {
           headers={{ header: 'test' }}
           headers={{ header: 'test' }}
           timestamp={new Date(0)}
           timestamp={new Date(0)}
           timestampType={TopicMessageTimestampTypeEnum.CREATE_TIME}
           timestampType={TopicMessageTimestampTypeEnum.CREATE_TIME}
+          keySerde="SchemaRegistry"
+          valueSerde="Avro"
           {...props}
           {...props}
         />
         />
       </tbody>
       </tbody>
@@ -27,42 +29,20 @@ const setupWrapper = (props?: Partial<MessageContentProps>) => {
   );
   );
 };
 };
 
 
-const proto =
-  'syntax = "proto3";\npackage com.provectus;\n\nmessage TestProtoRecord {\n  string f1 = 1;\n  int32 f2 = 2;\n}\n';
-
 global.TextEncoder = TextEncoder;
 global.TextEncoder = TextEncoder;
 
 
-const searchParamsContentAVRO = new URLSearchParams({
-  keySerde: 'SchemaRegistry',
-  valueSerde: 'AVRO',
-  limit: '100',
-});
-
-const searchParamsContentJSON = new URLSearchParams({
-  keySerde: 'SchemaRegistry',
-  valueSerde: 'JSON',
-  limit: '100',
-});
-
-const searchParamsContentPROTOBUF = new URLSearchParams({
-  keySerde: 'SchemaRegistry',
-  valueSerde: 'PROTOBUF',
-  limit: '100',
-});
 describe('MessageContent screen', () => {
 describe('MessageContent screen', () => {
   beforeEach(() => {
   beforeEach(() => {
-    render(setupWrapper(), {
-      initialEntries: [`/messages?${searchParamsContentAVRO}`],
-    });
+    render(setupWrapper());
   });
   });
 
 
-  describe('renders', () => {
-    it('key format in document', () => {
+  describe('Checking keySerde and valueSerde', () => {
+    it('keySerde in document', () => {
       expect(screen.getByText('SchemaRegistry')).toBeInTheDocument();
       expect(screen.getByText('SchemaRegistry')).toBeInTheDocument();
     });
     });
 
 
-    it('content format in document', () => {
-      expect(screen.getByText('AVRO')).toBeInTheDocument();
+    it('valueSerde in document', () => {
+      expect(screen.getByText('Avro')).toBeInTheDocument();
     });
     });
   });
   });
 
 
@@ -98,42 +78,3 @@ describe('MessageContent screen', () => {
     });
     });
   });
   });
 });
 });
-
-describe('checking content type depend on message type', () => {
-  it('renders component with message having JSON type', () => {
-    render(
-      setupWrapper({
-        messageContent: '{"data": "test"}',
-      }),
-      { initialEntries: [`/messages?${searchParamsContentJSON}`] }
-    );
-    expect(screen.getByText('JSON')).toBeInTheDocument();
-  });
-  it('renders component with message having AVRO type', () => {
-    render(
-      setupWrapper({
-        messageContent: '{"data": "test"}',
-      }),
-      { initialEntries: [`/messages?${searchParamsContentAVRO}`] }
-    );
-    expect(screen.getByText('AVRO')).toBeInTheDocument();
-  });
-  it('renders component with message having PROTOBUF type', () => {
-    render(
-      setupWrapper({
-        messageContent: proto,
-      }),
-      { initialEntries: [`/messages?${searchParamsContentPROTOBUF}`] }
-    );
-    expect(screen.getByText('PROTOBUF')).toBeInTheDocument();
-  });
-  it('renders component with message having no type which is equal to having PROTOBUF type', () => {
-    render(
-      setupWrapper({
-        messageContent: '',
-      }),
-      { initialEntries: [`/messages?${searchParamsContentPROTOBUF}`] }
-    );
-    expect(screen.getByText('PROTOBUF')).toBeInTheDocument();
-  });
-});

+ 8 - 22
kafka-ui-react-app/src/components/Version/Version.tsx

@@ -1,52 +1,38 @@
 import React from 'react';
 import React from 'react';
 import WarningIcon from 'components/common/Icons/WarningIcon';
 import WarningIcon from 'components/common/Icons/WarningIcon';
 import { gitCommitPath } from 'lib/paths';
 import { gitCommitPath } from 'lib/paths';
-import { useActuatorInfo } from 'lib/hooks/api/actuatorInfo';
-import { BUILD_VERSION_PATTERN } from 'lib/constants';
 import { useLatestVersion } from 'lib/hooks/api/latestVersion';
 import { useLatestVersion } from 'lib/hooks/api/latestVersion';
 import { formatTimestamp } from 'lib/dateTimeHelpers';
 import { formatTimestamp } from 'lib/dateTimeHelpers';
 
 
 import * as S from './Version.styled';
 import * as S from './Version.styled';
-import compareVersions from './compareVersions';
 
 
 const Version: React.FC = () => {
 const Version: React.FC = () => {
-  const { data: actuatorInfo = {} } = useActuatorInfo();
   const { data: latestVersionInfo = {} } = useLatestVersion();
   const { data: latestVersionInfo = {} } = useLatestVersion();
-
-  const tag = actuatorInfo?.build?.version;
-  const commit = actuatorInfo?.git?.commit.id;
-  const { tag_name: latestTag } = latestVersionInfo;
-
-  const outdated = compareVersions(tag, latestTag);
-
-  const currentVersion = tag?.match(BUILD_VERSION_PATTERN)
-    ? tag
-    : formatTimestamp(actuatorInfo?.build?.time);
-
-  if (!tag) return null;
+  const { buildTime, commitId, isLatestRelease } = latestVersionInfo.build;
+  const { versionTag } = latestVersionInfo?.latestRelease || '';
 
 
   return (
   return (
     <S.Wrapper>
     <S.Wrapper>
-      {!!outdated && (
+      {!isLatestRelease && (
         <S.OutdatedWarning
         <S.OutdatedWarning
-          title={`Your app version is outdated. Current latest version is ${latestTag}`}
+          title={`Your app version is outdated. Current latest version is ${versionTag}`}
         >
         >
           <WarningIcon />
           <WarningIcon />
         </S.OutdatedWarning>
         </S.OutdatedWarning>
       )}
       )}
 
 
-      {commit && (
+      {commitId && (
         <div>
         <div>
           <S.CurrentCommitLink
           <S.CurrentCommitLink
             title="Current commit"
             title="Current commit"
             target="__blank"
             target="__blank"
-            href={gitCommitPath(commit)}
+            href={gitCommitPath(commitId)}
           >
           >
-            {commit}
+            {commitId}
           </S.CurrentCommitLink>
           </S.CurrentCommitLink>
         </div>
         </div>
       )}
       )}
-      <S.CurrentVersion>{currentVersion}</S.CurrentVersion>
+      <S.CurrentVersion>{formatTimestamp(buildTime)}</S.CurrentVersion>
     </S.Wrapper>
     </S.Wrapper>
   );
   );
 };
 };

+ 19 - 66
kafka-ui-react-app/src/components/Version/__tests__/Version.spec.tsx

@@ -2,87 +2,40 @@ import React from 'react';
 import { screen } from '@testing-library/dom';
 import { screen } from '@testing-library/dom';
 import Version from 'components/Version/Version';
 import Version from 'components/Version/Version';
 import { render } from 'lib/testHelpers';
 import { render } from 'lib/testHelpers';
-import { formatTimestamp } from 'lib/dateTimeHelpers';
-import { useActuatorInfo } from 'lib/hooks/api/actuatorInfo';
 import { useLatestVersion } from 'lib/hooks/api/latestVersion';
 import { useLatestVersion } from 'lib/hooks/api/latestVersion';
-import { actuatorInfoPayload } from 'lib/fixtures/actuatorInfo';
-import { latestVersionPayload } from 'lib/fixtures/latestVersion';
+import {
+  deprecatedVersionPayload,
+  latestVersionPayload,
+} from 'lib/fixtures/latestVersion';
 
 
-jest.mock('lib/hooks/api/actuatorInfo', () => ({
-  useActuatorInfo: jest.fn(),
-}));
 jest.mock('lib/hooks/api/latestVersion', () => ({
 jest.mock('lib/hooks/api/latestVersion', () => ({
   useLatestVersion: jest.fn(),
   useLatestVersion: jest.fn(),
 }));
 }));
-
 describe('Version Component', () => {
 describe('Version Component', () => {
-  const versionTag = 'v0.5.0';
-  const snapshotTag = 'test-SNAPSHOT';
-  const commitTag = 'befd3b328e2c9c7df57b0c5746561b2f7fee8813';
-
-  const actuatorVersionPayload = actuatorInfoPayload(versionTag);
-  const formattedTimestamp = formatTimestamp(actuatorVersionPayload.build.time);
-
-  beforeEach(() => {
-    (useActuatorInfo as jest.Mock).mockImplementation(() => ({
-      data: actuatorVersionPayload,
-    }));
-    (useLatestVersion as jest.Mock).mockImplementation(() => ({
-      data: latestVersionPayload,
-    }));
-  });
-
-  describe('tag does not exist', () => {
-    it('does not render component', async () => {
-      (useActuatorInfo as jest.Mock).mockImplementation(() => ({
-        data: null,
-      }));
-      const { container } = render(<Version />);
-      expect(container.firstChild).toBeEmptyDOMElement();
-    });
-  });
+  const commitId = '96a577a';
 
 
-  describe('renders current version', () => {
-    it('renders release build version as current version', async () => {
-      render(<Version />);
-      expect(screen.getByText(versionTag)).toBeInTheDocument();
-    });
-    it('renders formatted timestamp as current version when version is commit', async () => {
-      (useActuatorInfo as jest.Mock).mockImplementation(() => ({
-        data: actuatorInfoPayload(commitTag),
+  describe('render latest version', () => {
+    beforeEach(() => {
+      (useLatestVersion as jest.Mock).mockImplementation(() => ({
+        data: latestVersionPayload,
       }));
       }));
-      render(<Version />);
-      expect(screen.getByText(formattedTimestamp)).toBeInTheDocument();
     });
     });
-    it('renders formatted timestamp as current version when version contains -SNAPSHOT', async () => {
-      (useActuatorInfo as jest.Mock).mockImplementation(() => ({
-        data: actuatorInfoPayload(snapshotTag),
-      }));
+    it('renders latest release version as current version', async () => {
       render(<Version />);
       render(<Version />);
-      expect(screen.getByText(formattedTimestamp)).toBeInTheDocument();
+      expect(screen.getByText(commitId)).toBeInTheDocument();
     });
     });
-  });
 
 
-  describe('outdated build version', () => {
-    it('renders warning message', async () => {
-      (useActuatorInfo as jest.Mock).mockImplementation(() => ({
-        data: actuatorInfoPayload('v0.3.0'),
-      }));
+    it('should not show warning icon if it is last release', async () => {
       render(<Version />);
       render(<Version />);
-      expect(
-        screen.getByTitle(
-          `Your app version is outdated. Current latest version is ${latestVersionPayload.tag_name}`
-        )
-      ).toBeInTheDocument();
+      expect(screen.queryByRole('img')).not.toBeInTheDocument();
     });
     });
   });
   });
 
 
-  describe('current commit id with link', () => {
-    it('renders', async () => {
-      render(<Version />);
-      expect(
-        screen.getByText(actuatorVersionPayload.git.commit.id)
-      ).toBeInTheDocument();
-    });
+  it('show warning icon if it is not last release', async () => {
+    (useLatestVersion as jest.Mock).mockImplementation(() => ({
+      data: deprecatedVersionPayload,
+    }));
+    render(<Version />);
+    expect(screen.getByRole('img')).toBeInTheDocument();
   });
   });
 });
 });

+ 1 - 0
kafka-ui-react-app/src/components/common/Icons/WarningIcon.tsx

@@ -13,6 +13,7 @@ const WarningIcon: React.FC = () => {
   return (
   return (
     <WarningIconContainer>
     <WarningIconContainer>
       <svg
       <svg
+        role="img"
         width="14"
         width="14"
         height="13"
         height="13"
         viewBox="0 0 14 13"
         viewBox="0 0 14 13"

+ 0 - 12
kafka-ui-react-app/src/lib/fixtures/actuatorInfo.ts

@@ -1,12 +0,0 @@
-export const actuatorInfoPayload = (
-  version = 'befd3b328e2c9c7df57b0c5746561b2f7fee8813'
-) => ({
-  git: { commit: { id: 'befd3b3' } },
-  build: {
-    artifact: 'kafka-ui-api',
-    name: 'kafka-ui-api',
-    time: '2022-09-15T09:52:21.753Z',
-    version,
-    group: 'com.provectus',
-  },
-});

+ 14 - 1
kafka-ui-react-app/src/lib/fixtures/latestVersion.ts

@@ -1,3 +1,16 @@
+export const deprecatedVersionPayload = {
+  build: {
+    buildTime: '2023-04-14T09:47:35.463Z',
+    commitId: '96a577a',
+    isLatestRelease: false,
+    version: '96a577a98c6069376c5d22ed49cffd3739f1bbdc',
+  },
+};
 export const latestVersionPayload = {
 export const latestVersionPayload = {
-  tag_name: 'v0.4.0',
+  build: {
+    buildTime: '2023-04-14T09:47:35.463Z',
+    commitId: '96a577a',
+    isLatestRelease: true,
+    version: '96a577a98c6069376c5d22ed49cffd3739f1bbdc',
+  },
 };
 };

+ 0 - 17
kafka-ui-react-app/src/lib/hooks/api/__tests__/actuatorInfo.spec.ts

@@ -1,17 +0,0 @@
-import fetchMock from 'fetch-mock';
-import * as hooks from 'lib/hooks/api/actuatorInfo';
-import { expectQueryWorks, renderQueryHook } from 'lib/testHelpers';
-import { actuatorInfoPayload } from 'lib/fixtures/actuatorInfo';
-
-const actuatorInfoPath = '/actuator/info';
-
-describe('Actuator info hooks', () => {
-  beforeEach(() => fetchMock.restore());
-  describe('useActuatorInfo', () => {
-    it('returns the correct data', async () => {
-      const mock = fetchMock.getOnce(actuatorInfoPath, actuatorInfoPayload());
-      const { result } = renderQueryHook(() => hooks.useActuatorInfo());
-      await expectQueryWorks(mock, result);
-    });
-  });
-});

+ 5 - 7
kafka-ui-react-app/src/lib/hooks/api/__tests__/latestVersion.spec.ts

@@ -1,18 +1,16 @@
 import fetchMock from 'fetch-mock';
 import fetchMock from 'fetch-mock';
 import { expectQueryWorks, renderQueryHook } from 'lib/testHelpers';
 import { expectQueryWorks, renderQueryHook } from 'lib/testHelpers';
-import * as hooks from 'lib/hooks/api/latestVersion';
-import { GIT_REPO_LATEST_RELEASE_LINK } from 'lib/constants';
 import { latestVersionPayload } from 'lib/fixtures/latestVersion';
 import { latestVersionPayload } from 'lib/fixtures/latestVersion';
+import { useLatestVersion } from 'lib/hooks/api/latestVersion';
+
+const latestVersionPath = '/api/info';
 
 
 describe('Latest version hooks', () => {
 describe('Latest version hooks', () => {
   beforeEach(() => fetchMock.restore());
   beforeEach(() => fetchMock.restore());
   describe('useLatestVersion', () => {
   describe('useLatestVersion', () => {
     it('returns the correct data', async () => {
     it('returns the correct data', async () => {
-      const mock = fetchMock.getOnce(
-        GIT_REPO_LATEST_RELEASE_LINK,
-        latestVersionPayload
-      );
-      const { result } = renderQueryHook(() => hooks.useLatestVersion());
+      const mock = fetchMock.getOnce(latestVersionPath, latestVersionPayload);
+      const { result } = renderQueryHook(() => useLatestVersion());
       await expectQueryWorks(mock, result);
       await expectQueryWorks(mock, result);
     });
     });
   });
   });

+ 0 - 19
kafka-ui-react-app/src/lib/hooks/api/actuatorInfo.ts

@@ -1,19 +0,0 @@
-import { useQuery } from '@tanstack/react-query';
-import { BASE_PARAMS, QUERY_REFETCH_OFF_OPTIONS } from 'lib/constants';
-
-const fetchActuatorInfo = async () => {
-  const data = await fetch(
-    `${BASE_PARAMS.basePath}/actuator/info`,
-    BASE_PARAMS
-  ).then((res) => res.json());
-
-  return data;
-};
-
-export function useActuatorInfo() {
-  return useQuery(
-    ['actuatorInfo'],
-    fetchActuatorInfo,
-    QUERY_REFETCH_OFF_OPTIONS
-  );
-}

+ 8 - 10
kafka-ui-react-app/src/lib/hooks/api/latestVersion.ts

@@ -1,21 +1,19 @@
 import { useQuery } from '@tanstack/react-query';
 import { useQuery } from '@tanstack/react-query';
-import {
-  QUERY_REFETCH_OFF_OPTIONS,
-  GIT_REPO_LATEST_RELEASE_LINK,
-} from 'lib/constants';
+import { BASE_PARAMS, QUERY_REFETCH_OFF_OPTIONS } from 'lib/constants';
 
 
-const fetchLatestVersion = async () => {
-  const data = await fetch(GIT_REPO_LATEST_RELEASE_LINK).then((res) =>
-    res.json()
-  );
+const fetchLatestVersionInfo = async () => {
+  const data = await fetch(
+    `${BASE_PARAMS.basePath}/api/info`,
+    BASE_PARAMS
+  ).then((res) => res.json());
 
 
   return data;
   return data;
 };
 };
 
 
 export function useLatestVersion() {
 export function useLatestVersion() {
   return useQuery(
   return useQuery(
-    ['latestVersion'],
-    fetchLatestVersion,
+    ['versionInfo'],
+    fetchLatestVersionInfo,
     QUERY_REFETCH_OFF_OPTIONS
     QUERY_REFETCH_OFF_OPTIONS
   );
   );
 }
 }