Compare commits
12 commits
master
...
release/0.
Author | SHA1 | Date | |
---|---|---|---|
![]() |
56fa824510 | ||
![]() |
b0c0e06f19 | ||
![]() |
556ec290eb | ||
![]() |
2b334d5209 | ||
![]() |
59f03200f4 | ||
![]() |
aa633f424c | ||
![]() |
f7aaea85f4 | ||
![]() |
4ae1da6ebf | ||
![]() |
3377289517 | ||
![]() |
8727393501 | ||
![]() |
26464ba37d | ||
![]() |
023e8e3b3c |
22 changed files with 230 additions and 85 deletions
|
@ -21,12 +21,6 @@
|
|||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<!--TODO: remove, when spring-boot fixed dependency to 6.0.8+ (6.0.7 has CVE) -->
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-core</artifactId>
|
||||
<version>6.0.8</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
|
@ -61,7 +55,7 @@
|
|||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.9</version>
|
||||
<version>3.12.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
|
@ -97,7 +91,7 @@
|
|||
<dependency>
|
||||
<groupId>software.amazon.msk</groupId>
|
||||
<artifactId>aws-msk-iam-auth</artifactId>
|
||||
<version>1.1.5</version>
|
||||
<version>1.1.6</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
@ -115,7 +109,6 @@
|
|||
<groupId>io.projectreactor.addons</groupId>
|
||||
<artifactId>reactor-extra</artifactId>
|
||||
</dependency>
|
||||
<!-- https://github.com/provectus/kafka-ui/pull/3693 -->
|
||||
<dependency>
|
||||
<groupId>org.json</groupId>
|
||||
<artifactId>json</artifactId>
|
||||
|
|
|
@ -1,18 +1,41 @@
|
|||
package com.provectus.kafka.ui.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||
import org.springframework.web.reactive.config.CorsRegistry;
|
||||
import org.springframework.web.reactive.config.WebFluxConfigurer;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebFilter;
|
||||
import org.springframework.web.server.WebFilterChain;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Configuration
|
||||
public class CorsGlobalConfiguration implements WebFluxConfigurer {
|
||||
public class CorsGlobalConfiguration {
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOrigins("*")
|
||||
.allowedMethods("*")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(false);
|
||||
@Bean
|
||||
public WebFilter corsFilter() {
|
||||
return (final ServerWebExchange ctx, final WebFilterChain chain) -> {
|
||||
final ServerHttpRequest request = ctx.getRequest();
|
||||
|
||||
final ServerHttpResponse response = ctx.getResponse();
|
||||
final HttpHeaders headers = response.getHeaders();
|
||||
headers.add("Access-Control-Allow-Origin", "*");
|
||||
headers.add("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS");
|
||||
headers.add("Access-Control-Max-Age", "3600");
|
||||
headers.add("Access-Control-Allow-Headers", "Content-Type");
|
||||
|
||||
if (request.getMethod() == HttpMethod.OPTIONS) {
|
||||
response.setStatusCode(HttpStatus.OK);
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
return chain.filter(ctx);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@ public class LdapProperties {
|
|||
private String userFilterSearchBase;
|
||||
private String userFilterSearchFilter;
|
||||
private String groupFilterSearchBase;
|
||||
private String groupFilterSearchFilter;
|
||||
private String groupRoleAttribute;
|
||||
|
||||
@Value("${oauth2.ldap.activeDirectory:false}")
|
||||
private boolean isActiveDirectory;
|
||||
|
|
|
@ -3,14 +3,16 @@ 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 javax.annotation.Nullable;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
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.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
@ -50,9 +52,9 @@ public class LdapSecurityConfig {
|
|||
|
||||
@Bean
|
||||
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);
|
||||
if (props.getBase() != null) {
|
||||
ba.setUserDnPatterns(new String[] {props.getBase()});
|
||||
|
@ -67,7 +69,7 @@ public class LdapSecurityConfig {
|
|||
AbstractLdapAuthenticationProvider authenticationProvider;
|
||||
if (!props.isActiveDirectory()) {
|
||||
authenticationProvider = rbacEnabled
|
||||
? new LdapAuthenticationProvider(ba, ldapAuthoritiesPopulator)
|
||||
? new LdapAuthenticationProvider(ba, authoritiesExtractor)
|
||||
: new LdapAuthenticationProvider(ba);
|
||||
} else {
|
||||
authenticationProvider = new ActiveDirectoryLdapAuthenticationProvider(props.getActiveDirectoryDomain(),
|
||||
|
@ -97,11 +99,24 @@ public class LdapSecurityConfig {
|
|||
|
||||
@Bean
|
||||
@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
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package com.provectus.kafka.ui.controller;
|
||||
|
||||
import static com.provectus.kafka.ui.model.ConnectorActionDTO.RESTART;
|
||||
import static com.provectus.kafka.ui.model.ConnectorActionDTO.RESTART_ALL_TASKS;
|
||||
import static com.provectus.kafka.ui.model.ConnectorActionDTO.RESTART_FAILED_TASKS;
|
||||
|
||||
import com.provectus.kafka.ui.api.KafkaConnectApi;
|
||||
import com.provectus.kafka.ui.model.ConnectDTO;
|
||||
import com.provectus.kafka.ui.model.ConnectorActionDTO;
|
||||
|
@ -17,6 +21,7 @@ import com.provectus.kafka.ui.service.KafkaConnectService;
|
|||
import com.provectus.kafka.ui.service.rbac.AccessControlService;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
@ -30,6 +35,8 @@ import reactor.core.publisher.Mono;
|
|||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class KafkaConnectController extends AbstractController implements KafkaConnectApi {
|
||||
private static final Set<ConnectorActionDTO> RESTART_ACTIONS
|
||||
= Set.of(RESTART, RESTART_FAILED_TASKS, RESTART_ALL_TASKS);
|
||||
private final KafkaConnectService kafkaConnectService;
|
||||
private final AccessControlService accessControlService;
|
||||
|
||||
|
@ -172,10 +179,17 @@ public class KafkaConnectController extends AbstractController implements KafkaC
|
|||
ConnectorActionDTO action,
|
||||
ServerWebExchange exchange) {
|
||||
|
||||
ConnectAction[] connectActions;
|
||||
if (RESTART_ACTIONS.contains(action)) {
|
||||
connectActions = new ConnectAction[] {ConnectAction.VIEW, ConnectAction.RESTART};
|
||||
} else {
|
||||
connectActions = new ConnectAction[] {ConnectAction.VIEW, ConnectAction.EDIT};
|
||||
}
|
||||
|
||||
Mono<Void> validateAccess = accessControlService.validateAccess(AccessContext.builder()
|
||||
.cluster(clusterName)
|
||||
.connect(connectName)
|
||||
.connectActions(ConnectAction.VIEW, ConnectAction.EDIT)
|
||||
.connectActions(connectActions)
|
||||
.build());
|
||||
|
||||
return validateAccess.then(
|
||||
|
@ -253,16 +267,11 @@ public class KafkaConnectController extends AbstractController implements KafkaC
|
|||
if (orderBy == null) {
|
||||
return defaultComparator;
|
||||
}
|
||||
switch (orderBy) {
|
||||
case CONNECT:
|
||||
return Comparator.comparing(FullConnectorInfoDTO::getConnect);
|
||||
case TYPE:
|
||||
return Comparator.comparing(FullConnectorInfoDTO::getType);
|
||||
case STATUS:
|
||||
return Comparator.comparing(fullConnectorInfoDTO -> fullConnectorInfoDTO.getStatus().getState());
|
||||
case NAME:
|
||||
default:
|
||||
return defaultComparator;
|
||||
}
|
||||
return switch (orderBy) {
|
||||
case CONNECT -> Comparator.comparing(FullConnectorInfoDTO::getConnect);
|
||||
case TYPE -> Comparator.comparing(FullConnectorInfoDTO::getType);
|
||||
case STATUS -> Comparator.comparing(fullConnectorInfoDTO -> fullConnectorInfoDTO.getStatus().getState());
|
||||
default -> defaultComparator;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -167,12 +167,13 @@ public class TopicsController extends AbstractController implements TopicsApi {
|
|||
ServerWebExchange exchange) {
|
||||
|
||||
return topicsService.getTopicsForPagination(getCluster(clusterName))
|
||||
.flatMap(existingTopics -> {
|
||||
.flatMap(topics -> accessControlService.filterViewableTopics(topics, clusterName))
|
||||
.flatMap(topics -> {
|
||||
int pageSize = perPage != null && perPage > 0 ? perPage : DEFAULT_PAGE_SIZE;
|
||||
var topicsToSkip = ((page != null && page > 0 ? page : 1) - 1) * pageSize;
|
||||
var comparator = sortOrder == null || !sortOrder.equals(SortOrderDTO.DESC)
|
||||
? getComparatorForTopic(orderBy) : getComparatorForTopic(orderBy).reversed();
|
||||
List<InternalTopic> filtered = existingTopics.stream()
|
||||
List<InternalTopic> filtered = topics.stream()
|
||||
.filter(topic -> !topic.isInternal()
|
||||
|| showInternal != null && showInternal)
|
||||
.filter(topic -> search == null || StringUtils.containsIgnoreCase(topic.getName(), search))
|
||||
|
@ -189,7 +190,6 @@ public class TopicsController extends AbstractController implements TopicsApi {
|
|||
|
||||
return topicsService.loadTopics(getCluster(clusterName), topicsPage)
|
||||
.flatMapMany(Flux::fromIterable)
|
||||
.filterWhen(dto -> accessControlService.isTopicAccessible(dto, clusterName))
|
||||
.collectList()
|
||||
.map(topicsToRender ->
|
||||
new TopicsResponseDTO()
|
||||
|
|
|
@ -202,19 +202,23 @@ public class AccessControlService {
|
|||
return isAccessible(Resource.TOPIC, context.getTopic(), user, context, requiredActions);
|
||||
}
|
||||
|
||||
public Mono<Boolean> isTopicAccessible(InternalTopic dto, String clusterName) {
|
||||
public Mono<List<InternalTopic>> filterViewableTopics(List<InternalTopic> topics, String clusterName) {
|
||||
if (!rbacEnabled) {
|
||||
return Mono.just(true);
|
||||
return Mono.just(topics);
|
||||
}
|
||||
|
||||
AccessContext accessContext = AccessContext
|
||||
.builder()
|
||||
.cluster(clusterName)
|
||||
.topic(dto.getName())
|
||||
.topicActions(TopicAction.VIEW)
|
||||
.build();
|
||||
|
||||
return getUser().map(u -> isTopicAccessible(accessContext, u));
|
||||
return getUser()
|
||||
.map(user -> topics.stream()
|
||||
.filter(topic -> {
|
||||
var accessContext = AccessContext
|
||||
.builder()
|
||||
.cluster(clusterName)
|
||||
.topic(topic.getName())
|
||||
.topicActions(TopicAction.VIEW)
|
||||
.build();
|
||||
return isTopicAccessible(accessContext, user);
|
||||
}
|
||||
).toList());
|
||||
}
|
||||
|
||||
private boolean isConsumerGroupAccessible(AccessContext context, AuthenticatedUser user) {
|
||||
|
|
|
@ -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,7 +3,6 @@ package com.provectus.kafka.ui.util;
|
|||
import com.google.common.annotations.VisibleForTesting;
|
||||
import java.time.Duration;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Slf4j
|
||||
|
@ -31,7 +30,7 @@ public class GithubReleaseInfo {
|
|||
|
||||
@VisibleForTesting
|
||||
GithubReleaseInfo(String url) {
|
||||
this.refreshMono = WebClient.create()
|
||||
this.refreshMono = new WebClientConfigurator().build()
|
||||
.get()
|
||||
.uri(url)
|
||||
.exchangeToMono(resp -> resp.bodyToMono(GithubReleaseDto.class))
|
||||
|
|
|
@ -5,11 +5,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.provectus.kafka.ui.config.ClustersProperties;
|
||||
import com.provectus.kafka.ui.exception.ValidationException;
|
||||
import io.netty.buffer.ByteBufAllocator;
|
||||
import io.netty.handler.ssl.JdkSslContext;
|
||||
import io.netty.handler.ssl.SslContext;
|
||||
import io.netty.handler.ssl.SslContextBuilder;
|
||||
import io.netty.handler.ssl.SslProvider;
|
||||
import java.io.FileInputStream;
|
||||
import java.security.KeyStore;
|
||||
import java.util.function.Consumer;
|
||||
|
@ -93,7 +90,12 @@ public class WebClientConfigurator {
|
|||
// Create webclient
|
||||
SslContext context = contextBuilder.build();
|
||||
|
||||
builder.clientConnector(new ReactorClientHttpConnector(HttpClient.create().secure(t -> t.sslContext(context))));
|
||||
var httpClient = HttpClient
|
||||
.create()
|
||||
.secure(t -> t.sslContext(context))
|
||||
.proxyWithSystemProperties();
|
||||
|
||||
builder.clientConnector(new ReactorClientHttpConnector(httpClient));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import static org.mockito.ArgumentMatchers.anyString;
|
|||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.provectus.kafka.ui.service.rbac.AccessControlService;
|
||||
import java.util.Collections;
|
||||
import org.mockito.Mockito;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
|
@ -16,7 +17,7 @@ public class AccessControlServiceMock {
|
|||
when(mock.validateAccess(any())).thenReturn(Mono.empty());
|
||||
when(mock.isSchemaAccessible(anyString(), anyString())).thenReturn(Mono.just(true));
|
||||
|
||||
when(mock.isTopicAccessible(any(), anyString())).thenReturn(Mono.just(true));
|
||||
when(mock.filterViewableTopics(any(), any())).then(invocation -> Mono.just(invocation.getArgument(0)));
|
||||
|
||||
return mock;
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ const queryClient = new QueryClient({
|
|||
defaultOptions: {
|
||||
queries: {
|
||||
suspense: true,
|
||||
networkMode: 'offlineFirst',
|
||||
onError(error) {
|
||||
showServerError(error as Response);
|
||||
},
|
||||
|
|
|
@ -102,7 +102,7 @@ const Actions: React.FC = () => {
|
|||
disabled={isMutating}
|
||||
permission={{
|
||||
resource: ResourceType.CONNECT,
|
||||
action: Action.EDIT,
|
||||
action: Action.RESTART,
|
||||
value: routerProps.connectorName,
|
||||
}}
|
||||
>
|
||||
|
@ -113,7 +113,7 @@ const Actions: React.FC = () => {
|
|||
disabled={isMutating}
|
||||
permission={{
|
||||
resource: ResourceType.CONNECT,
|
||||
action: Action.EDIT,
|
||||
action: Action.RESTART,
|
||||
value: routerProps.connectorName,
|
||||
}}
|
||||
>
|
||||
|
@ -124,7 +124,7 @@ const Actions: React.FC = () => {
|
|||
disabled={isMutating}
|
||||
permission={{
|
||||
resource: ResourceType.CONNECT,
|
||||
action: Action.EDIT,
|
||||
action: Action.RESTART,
|
||||
value: routerProps.connectorName,
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import React from 'react';
|
||||
import { Task } from 'generated-sources';
|
||||
import { Action, ResourceType, Task } from 'generated-sources';
|
||||
import { CellContext } from '@tanstack/react-table';
|
||||
import useAppParams from 'lib/hooks/useAppParams';
|
||||
import { useRestartConnectorTask } from 'lib/hooks/api/kafkaConnect';
|
||||
import { Dropdown, DropdownItem } from 'components/common/Dropdown';
|
||||
import { Dropdown } from 'components/common/Dropdown';
|
||||
import { ActionDropdownItem } from 'components/common/ActionComponent';
|
||||
import { RouterParamsClusterConnectConnector } from 'lib/paths';
|
||||
|
||||
const ActionsCellTasks: React.FC<CellContext<Task, unknown>> = ({ row }) => {
|
||||
|
@ -18,13 +19,18 @@ const ActionsCellTasks: React.FC<CellContext<Task, unknown>> = ({ row }) => {
|
|||
|
||||
return (
|
||||
<Dropdown>
|
||||
<DropdownItem
|
||||
<ActionDropdownItem
|
||||
onClick={() => restartTaskHandler(id?.task)}
|
||||
danger
|
||||
confirm="Are you sure you want to restart the task?"
|
||||
permission={{
|
||||
resource: ResourceType.CONNECT,
|
||||
action: Action.RESTART,
|
||||
value: routerProps.connectorName,
|
||||
}}
|
||||
>
|
||||
<span>Restart task</span>
|
||||
</DropdownItem>
|
||||
</ActionDropdownItem>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -78,7 +78,7 @@ const ActionsCell: React.FC<CellContext<FullConnectorInfo, unknown>> = ({
|
|||
disabled={isMutating}
|
||||
permission={{
|
||||
resource: ResourceType.CONNECT,
|
||||
action: Action.EDIT,
|
||||
action: Action.RESTART,
|
||||
value: name,
|
||||
}}
|
||||
>
|
||||
|
@ -89,7 +89,7 @@ const ActionsCell: React.FC<CellContext<FullConnectorInfo, unknown>> = ({
|
|||
disabled={isMutating}
|
||||
permission={{
|
||||
resource: ResourceType.CONNECT,
|
||||
action: Action.EDIT,
|
||||
action: Action.RESTART,
|
||||
value: name,
|
||||
}}
|
||||
>
|
||||
|
@ -100,7 +100,7 @@ const ActionsCell: React.FC<CellContext<FullConnectorInfo, unknown>> = ({
|
|||
disabled={isMutating}
|
||||
permission={{
|
||||
resource: ResourceType.CONNECT,
|
||||
action: Action.EDIT,
|
||||
action: Action.RESTART,
|
||||
value: name,
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -38,7 +38,7 @@ const New: React.FC = () => {
|
|||
const { clusterName } = useAppParams<ClusterNameRoute>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data: connects } = useConnects(clusterName);
|
||||
const { data: connects = [] } = useConnects(clusterName);
|
||||
const mutation = useCreateConnector(clusterName);
|
||||
|
||||
const methods = useForm<FormValues>({
|
||||
|
@ -88,10 +88,6 @@ const New: React.FC = () => {
|
|||
}
|
||||
};
|
||||
|
||||
if (!connects || connects.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const connectOptions = connects.map(({ name: connectName }) => ({
|
||||
value: connectName,
|
||||
label: connectName,
|
||||
|
@ -108,10 +104,10 @@ const New: React.FC = () => {
|
|||
onSubmit={handleSubmit(onSubmit)}
|
||||
aria-label="Create connect form"
|
||||
>
|
||||
<S.Filed $hidden={connects.length <= 1}>
|
||||
<S.Filed $hidden={connects?.length <= 1}>
|
||||
<Heading level={3}>Connect *</Heading>
|
||||
<Controller
|
||||
defaultValue={connectOptions[0].value}
|
||||
defaultValue={connectOptions[0]?.value}
|
||||
control={control}
|
||||
name="connectName"
|
||||
render={({ field: { name, onChange } }) => (
|
||||
|
@ -120,7 +116,7 @@ const New: React.FC = () => {
|
|||
name={name}
|
||||
disabled={isSubmitting}
|
||||
onChange={onChange}
|
||||
value={connectOptions[0].value}
|
||||
value={connectOptions[0]?.value}
|
||||
minWidth="100%"
|
||||
options={connectOptions}
|
||||
/>
|
||||
|
|
|
@ -11,7 +11,8 @@ const ClusterTableActionsCell: React.FC<Props> = ({ row }) => {
|
|||
const { name } = row.original;
|
||||
const { data } = useGetUserInfo();
|
||||
|
||||
const isApplicationConfig = useMemo(() => {
|
||||
const hasPermissions = useMemo(() => {
|
||||
if (!data?.rbacEnabled) return true;
|
||||
return !!data?.userInfo?.permissions.some(
|
||||
(permission) => permission.resource === ResourceType.APPLICATIONCONFIG
|
||||
);
|
||||
|
@ -22,7 +23,7 @@ const ClusterTableActionsCell: React.FC<Props> = ({ row }) => {
|
|||
buttonType="secondary"
|
||||
buttonSize="S"
|
||||
to={clusterConfigPath(name)}
|
||||
canDoAction={isApplicationConfig}
|
||||
canDoAction={hasPermissions}
|
||||
>
|
||||
Configure
|
||||
</ActionCanButton>
|
||||
|
|
|
@ -57,7 +57,8 @@ const Dashboard: React.FC = () => {
|
|||
return initialColumns;
|
||||
}, []);
|
||||
|
||||
const isApplicationConfig = useMemo(() => {
|
||||
const hasPermissions = useMemo(() => {
|
||||
if (!data?.rbacEnabled) return true;
|
||||
return !!data?.userInfo?.permissions.some(
|
||||
(permission) => permission.resource === ResourceType.APPLICATIONCONFIG
|
||||
);
|
||||
|
@ -91,7 +92,7 @@ const Dashboard: React.FC = () => {
|
|||
buttonType="primary"
|
||||
buttonSize="M"
|
||||
to={clusterNewConfigPath}
|
||||
canDoAction={isApplicationConfig}
|
||||
canDoAction={hasPermissions}
|
||||
>
|
||||
Configure new cluster
|
||||
</ActionCanButton>
|
||||
|
|
|
@ -185,6 +185,7 @@ const Filters: React.FC<FiltersProps> = ({
|
|||
const handleClearAllFilters = () => {
|
||||
setCurrentSeekType(SeekType.OFFSET);
|
||||
setOffset('');
|
||||
setTimestamp(null);
|
||||
setQuery('');
|
||||
changeSeekDirection(SeekDirection.FORWARD);
|
||||
getSelectedPartitionsFromSeekToParam(searchParams, partitions);
|
||||
|
|
|
@ -8,14 +8,22 @@ import * as S from './Version.styled';
|
|||
|
||||
const Version: React.FC = () => {
|
||||
const { data: latestVersionInfo = {} } = useLatestVersion();
|
||||
const { buildTime, commitId, isLatestRelease } = latestVersionInfo.build;
|
||||
const { buildTime, commitId, isLatestRelease, version } =
|
||||
latestVersionInfo.build;
|
||||
const { versionTag } = latestVersionInfo?.latestRelease || '';
|
||||
|
||||
const currentVersion =
|
||||
isLatestRelease && version?.match(versionTag)
|
||||
? versionTag
|
||||
: formatTimestamp(buildTime);
|
||||
|
||||
return (
|
||||
<S.Wrapper>
|
||||
{!isLatestRelease && (
|
||||
<S.OutdatedWarning
|
||||
title={`Your app version is outdated. Current latest version is ${versionTag}`}
|
||||
title={`Your app version is outdated. Latest version is ${
|
||||
versionTag || 'UNKNOWN'
|
||||
}`}
|
||||
>
|
||||
<WarningIcon />
|
||||
</S.OutdatedWarning>
|
||||
|
@ -32,7 +40,7 @@ const Version: React.FC = () => {
|
|||
</S.CurrentCommitLink>
|
||||
</div>
|
||||
)}
|
||||
<S.CurrentVersion>{formatTimestamp(buildTime)}</S.CurrentVersion>
|
||||
<S.CurrentVersion>{currentVersion}</S.CurrentVersion>
|
||||
</S.Wrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -304,6 +304,11 @@ export function useTopicAnalysis(
|
|||
useErrorBoundary: true,
|
||||
retry: false,
|
||||
suspense: false,
|
||||
onError: (error: Response) => {
|
||||
if (error.status !== 404) {
|
||||
showServerError(error as Response);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
2
pom.xml
2
pom.xml
|
@ -36,7 +36,7 @@
|
|||
<protobuf-java.version>3.21.9</protobuf-java.version>
|
||||
<scala-lang.library.version>2.13.9</scala-lang.library.version>
|
||||
<snakeyaml.version>2.0</snakeyaml.version>
|
||||
<spring-boot.version>3.0.5</spring-boot.version>
|
||||
<spring-boot.version>3.0.6</spring-boot.version>
|
||||
<kafka-ui-serde-api.version>1.0.0</kafka-ui-serde-api.version>
|
||||
<odd-oddrn-generator.version>0.1.15</odd-oddrn-generator.version>
|
||||
<odd-oddrn-client.version>0.1.23</odd-oddrn-client.version>
|
||||
|
|
Loading…
Add table
Reference in a new issue