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
This commit is contained in:
Roman Zabaluev 2023-05-01 08:09:58 +08:00
commit 29bd456a0b
24 changed files with 341 additions and 312 deletions

View file

@ -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;
}

View file

@ -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 { public class LdapSecurityConfig {
@Value("${spring.ldap.urls}") private final LdapProperties props;
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;
@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) { if (props.getBase() != null) {
ba.setUserDnPatterns(new String[] {ldapUserDnPattern}); 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) { if (!props.isActiveDirectory()) {
authenticationProvider = new LdapAuthenticationProvider(ba); 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.setUrl(props.getUrls());
ctx.setUserDn(adminUser); ctx.setUserDn(props.getAdminUser());
ctx.setPassword(adminPassword); 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()
.httpBasic();
return http.csrf().disable().build(); .and()
.formLogin()
.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);
}
} }
} }

View file

@ -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();

View file

@ -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();
}
}

View file

@ -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 {
}
}

View file

@ -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) {

View file

@ -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"))

View file

@ -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());
} }

View file

@ -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) { void apply(int len) {
int len = bytes.length;
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++;

View file

@ -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 Set<ProviderAuthorityExtractor> oauthExtractors = Collections.emptySet();
private final RoleBasedAccessControlProperties properties;
@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(); default -> null;
}).collect(Collectors.toSet())) })
.filter(Objects::nonNull)
.collect(Collectors.toSet()))
.flatMap(Set::stream) .flatMap(Set::stream)
.collect(Collectors.toSet()); .collect(Collectors.toSet());
if ((clientRegistrationRepository == null || !clientRegistrationRepository.iterator().hasNext()) if (!properties.getRoles().isEmpty()
&& !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() { public Set<ProviderAuthorityExtractor> getOauthExtractors() {
return extractors; return oauthExtractors;
} }
public List<Role> getRoles() { public List<Role> getRoles() {

View file

@ -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
}
}

View file

@ -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());
}
}

View file

@ -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}
/> />
)} )}
</> </>

View file

@ -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>

View file

@ -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(), { render(setupWrapper());
initialEntries: [`/messages?${searchParamsContentAVRO}`],
});
}); });
describe('renders', () => { describe('Checking keySerde and valueSerde', () => {
it('key format in document', () => { it('keySerde in document', () => {
expect(screen.getByText('SchemaRegistry')).toBeInTheDocument(); expect(screen.getByText('SchemaRegistry')).toBeInTheDocument();
}); });
it('content format in document', () => { it('valueSerde in document', () => {
expect(screen.getByText('AVRO')).toBeInTheDocument(); 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();
});
});

View file

@ -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 { buildTime, commitId, isLatestRelease } = latestVersionInfo.build;
const tag = actuatorInfo?.build?.version; const { versionTag } = latestVersionInfo?.latestRelease || '';
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;
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>
); );
}; };

View file

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

View file

@ -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"

View file

@ -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',
},
});

View file

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

View file

@ -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);
});
});
});

View file

@ -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( const mock = fetchMock.getOnce(latestVersionPath, latestVersionPayload);
GIT_REPO_LATEST_RELEASE_LINK, const { result } = renderQueryHook(() => useLatestVersion());
latestVersionPayload
);
const { result } = renderQueryHook(() => hooks.useLatestVersion());
await expectQueryWorks(mock, result); await expectQueryWorks(mock, result);
}); });
}); });

View file

@ -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
);
}

View file

@ -1,21 +1,19 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { import { BASE_PARAMS, QUERY_REFETCH_OFF_OPTIONS } from 'lib/constants';
QUERY_REFETCH_OFF_OPTIONS,
GIT_REPO_LATEST_RELEASE_LINK,
} from 'lib/constants';
const fetchLatestVersion = async () => { const fetchLatestVersionInfo = async () => {
const data = await fetch(GIT_REPO_LATEST_RELEASE_LINK).then((res) => const data = await fetch(
res.json() `${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'], ['versionInfo'],
fetchLatestVersion, fetchLatestVersionInfo,
QUERY_REFETCH_OFF_OPTIONS QUERY_REFETCH_OFF_OPTIONS
); );
} }