Merge branch 'master' into ISSUE_754_acl
This commit is contained in:
commit
0be17bc7af
25 changed files with 460 additions and 171 deletions
|
@ -131,8 +131,9 @@ public class ClustersProperties {
|
||||||
@Data
|
@Data
|
||||||
public static class Masking {
|
public static class Masking {
|
||||||
Type type;
|
Type type;
|
||||||
List<String> fields; //if null or empty list - policy will be applied to all fields
|
List<String> fields;
|
||||||
List<String> pattern; //used when type=MASK
|
String fieldsNamePattern;
|
||||||
|
List<String> maskingCharsReplacement; //used when type=MASK
|
||||||
String replacement; //used when type=REPLACE
|
String replacement; //used when type=REPLACE
|
||||||
String topicKeysPattern;
|
String topicKeysPattern;
|
||||||
String topicValuesPattern;
|
String topicValuesPattern;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 String provider = getProviderByProviderId(providerId);
|
final String provider = getProviderByProviderId(providerId);
|
||||||
Optional<ProviderAuthorityExtractor> extractor = acs.getExtractors()
|
Optional<ProviderAuthorityExtractor> extractor = acs.getOauthExtractors()
|
||||||
.stream()
|
.stream()
|
||||||
.filter(e -> e.isApplicable(provider))
|
.filter(e -> e.isApplicable(provider))
|
||||||
.findFirst();
|
.findFirst();
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,7 +44,7 @@ public class DataMasking {
|
||||||
public static DataMasking create(@Nullable List<ClustersProperties.Masking> config) {
|
public static DataMasking create(@Nullable List<ClustersProperties.Masking> config) {
|
||||||
return new DataMasking(
|
return new DataMasking(
|
||||||
Optional.ofNullable(config).orElse(List.of()).stream().map(property -> {
|
Optional.ofNullable(config).orElse(List.of()).stream().map(property -> {
|
||||||
Preconditions.checkNotNull(property.getType(), "masking type not specifed");
|
Preconditions.checkNotNull(property.getType(), "masking type not specified");
|
||||||
Preconditions.checkArgument(
|
Preconditions.checkArgument(
|
||||||
StringUtils.isNotEmpty(property.getTopicKeysPattern())
|
StringUtils.isNotEmpty(property.getTopicKeysPattern())
|
||||||
|| StringUtils.isNotEmpty(property.getTopicValuesPattern()),
|
|| StringUtils.isNotEmpty(property.getTopicValuesPattern()),
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.provectus.kafka.ui.service.masking.policies;
|
||||||
|
|
||||||
|
import com.provectus.kafka.ui.config.ClustersProperties;
|
||||||
|
import com.provectus.kafka.ui.exception.ValidationException;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
interface FieldsSelector {
|
||||||
|
|
||||||
|
static FieldsSelector create(ClustersProperties.Masking property) {
|
||||||
|
if (StringUtils.hasText(property.getFieldsNamePattern()) && !CollectionUtils.isEmpty(property.getFields())) {
|
||||||
|
throw new ValidationException("You can't provide both fieldNames & fieldsNamePattern for masking");
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(property.getFieldsNamePattern())) {
|
||||||
|
Pattern pattern = Pattern.compile(property.getFieldsNamePattern());
|
||||||
|
return f -> pattern.matcher(f).matches();
|
||||||
|
}
|
||||||
|
if (!CollectionUtils.isEmpty(property.getFields())) {
|
||||||
|
return f -> property.getFields().contains(f);
|
||||||
|
}
|
||||||
|
//no pattern, no field names - mean all fields should be masked
|
||||||
|
return fieldName -> true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean shouldBeMasked(String fieldName);
|
||||||
|
|
||||||
|
}
|
|
@ -15,8 +15,8 @@ class Mask extends MaskingPolicy {
|
||||||
|
|
||||||
private final UnaryOperator<String> masker;
|
private final UnaryOperator<String> masker;
|
||||||
|
|
||||||
Mask(List<String> fieldNames, List<String> maskingChars) {
|
Mask(FieldsSelector fieldsSelector, List<String> maskingChars) {
|
||||||
super(fieldNames);
|
super(fieldsSelector);
|
||||||
this.masker = createMasker(maskingChars);
|
this.masker = createMasker(maskingChars);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,22 +38,13 @@ class Mask extends MaskingPolicy {
|
||||||
for (int i = 0; i < input.length(); i++) {
|
for (int i = 0; i < input.length(); i++) {
|
||||||
int cp = input.codePointAt(i);
|
int cp = input.codePointAt(i);
|
||||||
switch (Character.getType(cp)) {
|
switch (Character.getType(cp)) {
|
||||||
case Character.SPACE_SEPARATOR:
|
case Character.SPACE_SEPARATOR,
|
||||||
case Character.LINE_SEPARATOR:
|
Character.LINE_SEPARATOR,
|
||||||
case Character.PARAGRAPH_SEPARATOR:
|
Character.PARAGRAPH_SEPARATOR -> sb.appendCodePoint(cp); // keeping separators as-is
|
||||||
sb.appendCodePoint(cp); // keeping separators as-is
|
case Character.UPPERCASE_LETTER -> sb.append(maskingChars.get(0));
|
||||||
break;
|
case Character.LOWERCASE_LETTER -> sb.append(maskingChars.get(1));
|
||||||
case Character.UPPERCASE_LETTER:
|
case Character.DECIMAL_DIGIT_NUMBER -> sb.append(maskingChars.get(2));
|
||||||
sb.append(maskingChars.get(0));
|
default -> sb.append(maskingChars.get(3));
|
||||||
break;
|
|
||||||
case Character.LOWERCASE_LETTER:
|
|
||||||
sb.append(maskingChars.get(1));
|
|
||||||
break;
|
|
||||||
case Character.DECIMAL_DIGIT_NUMBER:
|
|
||||||
sb.append(maskingChars.get(2));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
sb.append(maskingChars.get(3));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
|
|
|
@ -2,46 +2,36 @@ package com.provectus.kafka.ui.service.masking.policies;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.node.ContainerNode;
|
import com.fasterxml.jackson.databind.node.ContainerNode;
|
||||||
import com.provectus.kafka.ui.config.ClustersProperties;
|
import com.provectus.kafka.ui.config.ClustersProperties;
|
||||||
import java.util.List;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public abstract class MaskingPolicy {
|
public abstract class MaskingPolicy {
|
||||||
|
|
||||||
|
|
||||||
public static MaskingPolicy create(ClustersProperties.Masking property) {
|
public static MaskingPolicy create(ClustersProperties.Masking property) {
|
||||||
List<String> fields = property.getFields() == null
|
FieldsSelector fieldsSelector = FieldsSelector.create(property);
|
||||||
? List.of() // empty list means that policy will be applied to all fields
|
return switch (property.getType()) {
|
||||||
: property.getFields();
|
case REMOVE -> new Remove(fieldsSelector);
|
||||||
switch (property.getType()) {
|
case REPLACE -> new Replace(
|
||||||
case REMOVE:
|
fieldsSelector,
|
||||||
return new Remove(fields);
|
property.getReplacement() == null
|
||||||
case REPLACE:
|
? Replace.DEFAULT_REPLACEMENT
|
||||||
return new Replace(
|
: property.getReplacement()
|
||||||
fields,
|
);
|
||||||
property.getReplacement() == null
|
case MASK -> new Mask(
|
||||||
? Replace.DEFAULT_REPLACEMENT
|
fieldsSelector,
|
||||||
: property.getReplacement()
|
property.getMaskingCharsReplacement() == null
|
||||||
);
|
? Mask.DEFAULT_PATTERN
|
||||||
case MASK:
|
: property.getMaskingCharsReplacement()
|
||||||
return new Mask(
|
);
|
||||||
fields,
|
};
|
||||||
property.getPattern() == null
|
|
||||||
? Mask.DEFAULT_PATTERN
|
|
||||||
: property.getPattern()
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException("Unknown policy type: " + property.getType());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//----------------------------------------------------------------
|
//----------------------------------------------------------------
|
||||||
|
|
||||||
// empty list means policy will be applied to all fields
|
private final FieldsSelector fieldsSelector;
|
||||||
private final List<String> fieldNames;
|
|
||||||
|
|
||||||
protected boolean fieldShouldBeMasked(String fieldName) {
|
protected boolean fieldShouldBeMasked(String fieldName) {
|
||||||
return fieldNames.isEmpty() || fieldNames.contains(fieldName);
|
return fieldsSelector.shouldBeMasked(fieldName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract ContainerNode<?> applyToJsonContainer(ContainerNode<?> node);
|
public abstract ContainerNode<?> applyToJsonContainer(ContainerNode<?> node);
|
||||||
|
|
|
@ -4,12 +4,12 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
import com.fasterxml.jackson.databind.node.ContainerNode;
|
import com.fasterxml.jackson.databind.node.ContainerNode;
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
class Remove extends MaskingPolicy {
|
class Remove extends MaskingPolicy {
|
||||||
|
|
||||||
Remove(List<String> fieldNames) {
|
Remove(FieldsSelector fieldsSelector) {
|
||||||
super(fieldNames);
|
super(fieldsSelector);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -6,7 +6,6 @@ import com.fasterxml.jackson.databind.node.ContainerNode;
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import com.fasterxml.jackson.databind.node.TextNode;
|
import com.fasterxml.jackson.databind.node.TextNode;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
class Replace extends MaskingPolicy {
|
class Replace extends MaskingPolicy {
|
||||||
|
|
||||||
|
@ -14,8 +13,8 @@ class Replace extends MaskingPolicy {
|
||||||
|
|
||||||
private final String replacement;
|
private final String replacement;
|
||||||
|
|
||||||
Replace(List<String> fieldNames, String replacementString) {
|
Replace(FieldsSelector fieldsSelector, String replacementString) {
|
||||||
super(fieldNames);
|
super(fieldsSelector);
|
||||||
this.replacement = Preconditions.checkNotNull(replacementString);
|
this.replacement = Preconditions.checkNotNull(replacementString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,11 +20,11 @@ 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.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;
|
||||||
|
@ -34,6 +35,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;
|
||||||
|
@ -50,10 +52,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() {
|
||||||
|
@ -63,21 +66,26 @@ 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 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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -354,8 +362,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() {
|
||||||
|
|
|
@ -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) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package com.provectus.kafka.ui.service.masking.policies;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
import com.provectus.kafka.ui.config.ClustersProperties;
|
||||||
|
import com.provectus.kafka.ui.exception.ValidationException;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class FieldsSelectorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void selectsFieldsDueToProvidedPattern() {
|
||||||
|
var properties = new ClustersProperties.Masking();
|
||||||
|
properties.setFieldsNamePattern("f1|f2");
|
||||||
|
|
||||||
|
var selector = FieldsSelector.create(properties);
|
||||||
|
assertThat(selector.shouldBeMasked("f1")).isTrue();
|
||||||
|
assertThat(selector.shouldBeMasked("f2")).isTrue();
|
||||||
|
assertThat(selector.shouldBeMasked("doesNotMatchPattern")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void selectsFieldsDueToProvidedFieldNames() {
|
||||||
|
var properties = new ClustersProperties.Masking();
|
||||||
|
properties.setFields(List.of("f1", "f2"));
|
||||||
|
|
||||||
|
var selector = FieldsSelector.create(properties);
|
||||||
|
assertThat(selector.shouldBeMasked("f1")).isTrue();
|
||||||
|
assertThat(selector.shouldBeMasked("f2")).isTrue();
|
||||||
|
assertThat(selector.shouldBeMasked("notInAList")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void selectAllFieldsIfNoPatternAndNoNamesProvided() {
|
||||||
|
var properties = new ClustersProperties.Masking();
|
||||||
|
|
||||||
|
var selector = FieldsSelector.create(properties);
|
||||||
|
assertThat(selector.shouldBeMasked("anyPropertyName")).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void throwsExceptionIfBothFieldListAndPatternProvided() {
|
||||||
|
var properties = new ClustersProperties.Masking();
|
||||||
|
properties.setFieldsNamePattern("f1|f2");
|
||||||
|
properties.setFields(List.of("f3", "f4"));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> FieldsSelector.create(properties))
|
||||||
|
.isInstanceOf(ValidationException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -15,35 +15,35 @@ import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
|
||||||
class MaskTest {
|
class MaskTest {
|
||||||
|
|
||||||
private static final List<String> TARGET_FIELDS = List.of("id", "name");
|
private static final FieldsSelector FIELDS_SELECTOR = fieldName -> List.of("id", "name").contains(fieldName);
|
||||||
private static final List<String> PATTERN = List.of("X", "x", "n", "-");
|
private static final List<String> PATTERN = List.of("X", "x", "n", "-");
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource
|
@MethodSource
|
||||||
void testApplyToJsonContainer(List<String> fields, ContainerNode<?> original, ContainerNode<?> expected) {
|
void testApplyToJsonContainer(FieldsSelector selector, ContainerNode<?> original, ContainerNode<?> expected) {
|
||||||
Mask policy = new Mask(fields, PATTERN);
|
Mask policy = new Mask(selector, PATTERN);
|
||||||
assertThat(policy.applyToJsonContainer(original)).isEqualTo(expected);
|
assertThat(policy.applyToJsonContainer(original)).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Stream<Arguments> testApplyToJsonContainer() {
|
private static Stream<Arguments> testApplyToJsonContainer() {
|
||||||
return Stream.of(
|
return Stream.of(
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
TARGET_FIELDS,
|
FIELDS_SELECTOR,
|
||||||
parse("{ \"id\": 123, \"name\": { \"first\": \"James\", \"surname\": \"Bond777!\"}}"),
|
parse("{ \"id\": 123, \"name\": { \"first\": \"James\", \"surname\": \"Bond777!\"}}"),
|
||||||
parse("{ \"id\": \"nnn\", \"name\": { \"first\": \"Xxxxx\", \"surname\": \"Xxxxnnn-\"}}")
|
parse("{ \"id\": \"nnn\", \"name\": { \"first\": \"Xxxxx\", \"surname\": \"Xxxxnnn-\"}}")
|
||||||
),
|
),
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
TARGET_FIELDS,
|
FIELDS_SELECTOR,
|
||||||
parse("[{ \"id\": 123, \"f2\": 234}, { \"name\": \"1.2\", \"f2\": 345} ]"),
|
parse("[{ \"id\": 123, \"f2\": 234}, { \"name\": \"1.2\", \"f2\": 345} ]"),
|
||||||
parse("[{ \"id\": \"nnn\", \"f2\": 234}, { \"name\": \"n-n\", \"f2\": 345} ]")
|
parse("[{ \"id\": \"nnn\", \"f2\": 234}, { \"name\": \"n-n\", \"f2\": 345} ]")
|
||||||
),
|
),
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
TARGET_FIELDS,
|
FIELDS_SELECTOR,
|
||||||
parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"Bond777!\"}}"),
|
parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"Bond777!\"}}"),
|
||||||
parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"Xxxxnnn-\"}}")
|
parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"Xxxxnnn-\"}}")
|
||||||
),
|
),
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
List.of(),
|
(FieldsSelector) (fieldName -> true),
|
||||||
parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"Bond777!\"}}"),
|
parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"Bond777!\"}}"),
|
||||||
parse("{ \"outer\": { \"f1\": \"Xxxxx\", \"name\": \"Xxxxnnn-\"}}")
|
parse("{ \"outer\": { \"f1\": \"Xxxxx\", \"name\": \"Xxxxnnn-\"}}")
|
||||||
)
|
)
|
||||||
|
@ -57,7 +57,7 @@ class MaskTest {
|
||||||
"null, xxxx"
|
"null, xxxx"
|
||||||
})
|
})
|
||||||
void testApplyToString(String original, String expected) {
|
void testApplyToString(String original, String expected) {
|
||||||
Mask policy = new Mask(List.of(), PATTERN);
|
Mask policy = new Mask(fieldName -> true, PATTERN);
|
||||||
assertThat(policy.applyToString(original)).isEqualTo(expected);
|
assertThat(policy.applyToString(original)).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,39 +15,39 @@ import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
|
||||||
class RemoveTest {
|
class RemoveTest {
|
||||||
|
|
||||||
private static final List<String> TARGET_FIELDS = List.of("id", "name");
|
private static final FieldsSelector FIELDS_SELECTOR = fieldName -> List.of("id", "name").contains(fieldName);
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource
|
@MethodSource
|
||||||
void testApplyToJsonContainer(List<String> fields, ContainerNode<?> original, ContainerNode<?> expected) {
|
void testApplyToJsonContainer(FieldsSelector fieldsSelector, ContainerNode<?> original, ContainerNode<?> expected) {
|
||||||
var policy = new Remove(fields);
|
var policy = new Remove(fieldsSelector);
|
||||||
assertThat(policy.applyToJsonContainer(original)).isEqualTo(expected);
|
assertThat(policy.applyToJsonContainer(original)).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Stream<Arguments> testApplyToJsonContainer() {
|
private static Stream<Arguments> testApplyToJsonContainer() {
|
||||||
return Stream.of(
|
return Stream.of(
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
TARGET_FIELDS,
|
FIELDS_SELECTOR,
|
||||||
parse("{ \"id\": 123, \"name\": { \"first\": \"James\", \"surname\": \"Bond777!\"}}"),
|
parse("{ \"id\": 123, \"name\": { \"first\": \"James\", \"surname\": \"Bond777!\"}}"),
|
||||||
parse("{}")
|
parse("{}")
|
||||||
),
|
),
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
TARGET_FIELDS,
|
FIELDS_SELECTOR,
|
||||||
parse("[{ \"id\": 123, \"f2\": 234}, { \"name\": \"1.2\", \"f2\": 345} ]"),
|
parse("[{ \"id\": 123, \"f2\": 234}, { \"name\": \"1.2\", \"f2\": 345} ]"),
|
||||||
parse("[{ \"f2\": 234}, { \"f2\": 345} ]")
|
parse("[{ \"f2\": 234}, { \"f2\": 345} ]")
|
||||||
),
|
),
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
TARGET_FIELDS,
|
FIELDS_SELECTOR,
|
||||||
parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"Bond777!\"}}"),
|
parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"Bond777!\"}}"),
|
||||||
parse("{ \"outer\": { \"f1\": \"James\"}}")
|
parse("{ \"outer\": { \"f1\": \"James\"}}")
|
||||||
),
|
),
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
List.of(),
|
(FieldsSelector) (fieldName -> true),
|
||||||
parse("{ \"outer\": { \"f1\": \"v1\", \"f2\": \"v2\", \"inner\" : {\"if1\": \"iv1\"}}}"),
|
parse("{ \"outer\": { \"f1\": \"v1\", \"f2\": \"v2\", \"inner\" : {\"if1\": \"iv1\"}}}"),
|
||||||
parse("{}")
|
parse("{}")
|
||||||
),
|
),
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
List.of(),
|
(FieldsSelector) (fieldName -> true),
|
||||||
parse("[{ \"f1\": 123}, { \"f2\": \"1.2\"} ]"),
|
parse("[{ \"f1\": 123}, { \"f2\": \"1.2\"} ]"),
|
||||||
parse("[{}, {}]")
|
parse("[{}, {}]")
|
||||||
)
|
)
|
||||||
|
@ -66,7 +66,7 @@ class RemoveTest {
|
||||||
"null, null"
|
"null, null"
|
||||||
})
|
})
|
||||||
void testApplyToString(String original, String expected) {
|
void testApplyToString(String original, String expected) {
|
||||||
var policy = new Remove(List.of());
|
var policy = new Remove(fieldName -> true);
|
||||||
assertThat(policy.applyToString(original)).isEqualTo(expected);
|
assertThat(policy.applyToString(original)).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,35 +15,35 @@ import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
|
||||||
class ReplaceTest {
|
class ReplaceTest {
|
||||||
|
|
||||||
private static final List<String> TARGET_FIELDS = List.of("id", "name");
|
private static final FieldsSelector FIELDS_SELECTOR = fieldName -> List.of("id", "name").contains(fieldName);
|
||||||
private static final String REPLACEMENT_STRING = "***";
|
private static final String REPLACEMENT_STRING = "***";
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@MethodSource
|
@MethodSource
|
||||||
void testApplyToJsonContainer(List<String> fields, ContainerNode<?> original, ContainerNode<?> expected) {
|
void testApplyToJsonContainer(FieldsSelector fieldsSelector, ContainerNode<?> original, ContainerNode<?> expected) {
|
||||||
var policy = new Replace(fields, REPLACEMENT_STRING);
|
var policy = new Replace(fieldsSelector, REPLACEMENT_STRING);
|
||||||
assertThat(policy.applyToJsonContainer(original)).isEqualTo(expected);
|
assertThat(policy.applyToJsonContainer(original)).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Stream<Arguments> testApplyToJsonContainer() {
|
private static Stream<Arguments> testApplyToJsonContainer() {
|
||||||
return Stream.of(
|
return Stream.of(
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
TARGET_FIELDS,
|
FIELDS_SELECTOR,
|
||||||
parse("{ \"id\": 123, \"name\": { \"first\": \"James\", \"surname\": \"Bond777!\"}}"),
|
parse("{ \"id\": 123, \"name\": { \"first\": \"James\", \"surname\": \"Bond777!\"}}"),
|
||||||
parse("{ \"id\": \"***\", \"name\": { \"first\": \"***\", \"surname\": \"***\"}}")
|
parse("{ \"id\": \"***\", \"name\": { \"first\": \"***\", \"surname\": \"***\"}}")
|
||||||
),
|
),
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
TARGET_FIELDS,
|
FIELDS_SELECTOR,
|
||||||
parse("[{ \"id\": 123, \"f2\": 234}, { \"name\": \"1.2\", \"f2\": 345} ]"),
|
parse("[{ \"id\": 123, \"f2\": 234}, { \"name\": \"1.2\", \"f2\": 345} ]"),
|
||||||
parse("[{ \"id\": \"***\", \"f2\": 234}, { \"name\": \"***\", \"f2\": 345} ]")
|
parse("[{ \"id\": \"***\", \"f2\": 234}, { \"name\": \"***\", \"f2\": 345} ]")
|
||||||
),
|
),
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
TARGET_FIELDS,
|
FIELDS_SELECTOR,
|
||||||
parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"Bond777!\"}}"),
|
parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"Bond777!\"}}"),
|
||||||
parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"***\"}}")
|
parse("{ \"outer\": { \"f1\": \"James\", \"name\": \"***\"}}")
|
||||||
),
|
),
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
List.of(),
|
(FieldsSelector) (fieldName -> true),
|
||||||
parse("{ \"outer\": { \"f1\": \"v1\", \"f2\": \"v2\", \"inner\" : {\"if1\": \"iv1\"}}}"),
|
parse("{ \"outer\": { \"f1\": \"v1\", \"f2\": \"v2\", \"inner\" : {\"if1\": \"iv1\"}}}"),
|
||||||
parse("{ \"outer\": { \"f1\": \"***\", \"f2\": \"***\", \"inner\" : {\"if1\": \"***\"}}}}")
|
parse("{ \"outer\": { \"f1\": \"***\", \"f2\": \"***\", \"inner\" : {\"if1\": \"***\"}}}}")
|
||||||
)
|
)
|
||||||
|
@ -62,7 +62,7 @@ class ReplaceTest {
|
||||||
"null, ***"
|
"null, ***"
|
||||||
})
|
})
|
||||||
void testApplyToString(String original, String expected) {
|
void testApplyToString(String original, String expected) {
|
||||||
var policy = new Replace(List.of(), REPLACEMENT_STRING);
|
var policy = new Replace(fieldName -> true, REPLACEMENT_STRING);
|
||||||
assertThat(policy.applyToString(original)).isEqualTo(expected);
|
assertThat(policy.applyToString(original)).isEqualTo(expected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3809,7 +3809,9 @@ components:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
pattern:
|
fieldsNamePattern:
|
||||||
|
type: string
|
||||||
|
maskingCharsReplacement:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
|
|
|
@ -29,72 +29,65 @@ public class SmokeBacklog extends BaseManualTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Automation(state = TO_BE_AUTOMATED)
|
@Automation(state = TO_BE_AUTOMATED)
|
||||||
@Suite(id = KSQL_DB_SUITE_ID)
|
@Suite(id = BROKERS_SUITE_ID)
|
||||||
@QaseId(278)
|
@QaseId(331)
|
||||||
@Test
|
@Test
|
||||||
public void testCaseC() {
|
public void testCaseC() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Automation(state = TO_BE_AUTOMATED)
|
|
||||||
@Suite(id = BROKERS_SUITE_ID)
|
|
||||||
@QaseId(331)
|
|
||||||
@Test
|
|
||||||
public void testCaseD() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Automation(state = TO_BE_AUTOMATED)
|
@Automation(state = TO_BE_AUTOMATED)
|
||||||
@Suite(id = BROKERS_SUITE_ID)
|
@Suite(id = BROKERS_SUITE_ID)
|
||||||
@QaseId(332)
|
@QaseId(332)
|
||||||
@Test
|
@Test
|
||||||
public void testCaseE() {
|
public void testCaseD() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Automation(state = TO_BE_AUTOMATED)
|
@Automation(state = TO_BE_AUTOMATED)
|
||||||
@Suite(id = TOPICS_PROFILE_SUITE_ID)
|
@Suite(id = TOPICS_PROFILE_SUITE_ID)
|
||||||
@QaseId(335)
|
@QaseId(335)
|
||||||
@Test
|
@Test
|
||||||
public void testCaseF() {
|
public void testCaseE() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Automation(state = TO_BE_AUTOMATED)
|
@Automation(state = TO_BE_AUTOMATED)
|
||||||
@Suite(id = TOPICS_PROFILE_SUITE_ID)
|
@Suite(id = TOPICS_PROFILE_SUITE_ID)
|
||||||
@QaseId(336)
|
@QaseId(336)
|
||||||
@Test
|
@Test
|
||||||
public void testCaseG() {
|
public void testCaseF() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Automation(state = TO_BE_AUTOMATED)
|
@Automation(state = TO_BE_AUTOMATED)
|
||||||
@Suite(id = TOPICS_PROFILE_SUITE_ID)
|
@Suite(id = TOPICS_PROFILE_SUITE_ID)
|
||||||
@QaseId(343)
|
@QaseId(343)
|
||||||
@Test
|
@Test
|
||||||
public void testCaseH() {
|
public void testCaseG() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Automation(state = TO_BE_AUTOMATED)
|
@Automation(state = TO_BE_AUTOMATED)
|
||||||
@Suite(id = KSQL_DB_SUITE_ID)
|
@Suite(id = KSQL_DB_SUITE_ID)
|
||||||
@QaseId(344)
|
@QaseId(344)
|
||||||
@Test
|
@Test
|
||||||
public void testCaseI() {
|
public void testCaseH() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Automation(state = TO_BE_AUTOMATED)
|
@Automation(state = TO_BE_AUTOMATED)
|
||||||
@Suite(id = SCHEMAS_SUITE_ID)
|
@Suite(id = SCHEMAS_SUITE_ID)
|
||||||
@QaseId(345)
|
@QaseId(345)
|
||||||
@Test
|
@Test
|
||||||
public void testCaseJ() {
|
public void testCaseI() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Automation(state = TO_BE_AUTOMATED)
|
@Automation(state = TO_BE_AUTOMATED)
|
||||||
@Suite(id = SCHEMAS_SUITE_ID)
|
@Suite(id = SCHEMAS_SUITE_ID)
|
||||||
@QaseId(346)
|
@QaseId(346)
|
||||||
@Test
|
@Test
|
||||||
public void testCaseK() {
|
public void testCaseJ() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Automation(state = TO_BE_AUTOMATED)
|
@Automation(state = TO_BE_AUTOMATED)
|
||||||
@Suite(id = TOPICS_PROFILE_SUITE_ID)
|
@Suite(id = TOPICS_PROFILE_SUITE_ID)
|
||||||
@QaseId(347)
|
@QaseId(347)
|
||||||
@Test
|
@Test
|
||||||
public void testCaseL() {
|
public void testCaseK() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.provectus.kafka.ui.smokesuite.ksqldb;
|
package com.provectus.kafka.ui.smokesuite.ksqldb;
|
||||||
|
|
||||||
import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlMenuTabs.STREAMS;
|
import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlMenuTabs.STREAMS;
|
||||||
|
import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SHOW_STREAMS;
|
||||||
import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SHOW_TABLES;
|
import static com.provectus.kafka.ui.pages.ksqldb.enums.KsqlQueryConfig.SHOW_TABLES;
|
||||||
import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KSQL_DB;
|
import static com.provectus.kafka.ui.pages.panels.enums.MenuItem.KSQL_DB;
|
||||||
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
|
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
|
||||||
|
@ -80,8 +81,18 @@ public class KsqlDbTest extends BaseTest {
|
||||||
softly.assertAll();
|
softly.assertAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@QaseId(86)
|
@QaseId(278)
|
||||||
@Test(priority = 4)
|
@Test(priority = 4)
|
||||||
|
public void checkShowStreamsRequestExecution() {
|
||||||
|
navigateToKsqlDbAndExecuteRequest(SHOW_STREAMS.getQuery());
|
||||||
|
SoftAssert softly = new SoftAssert();
|
||||||
|
softly.assertTrue(ksqlQueryForm.areResultsVisible(), "areResultsVisible()");
|
||||||
|
softly.assertTrue(ksqlQueryForm.getItemByName(DEFAULT_STREAM.getName()).isVisible(), "getItemByName()");
|
||||||
|
softly.assertAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@QaseId(86)
|
||||||
|
@Test(priority = 5)
|
||||||
public void clearResultsForExecutedRequest() {
|
public void clearResultsForExecutedRequest() {
|
||||||
navigateToKsqlDbAndExecuteRequest(SHOW_TABLES.getQuery());
|
navigateToKsqlDbAndExecuteRequest(SHOW_TABLES.getQuery());
|
||||||
SoftAssert softly = new SoftAssert();
|
SoftAssert softly = new SoftAssert();
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { Button } from 'components/common/Button/Button';
|
||||||
|
|
||||||
export const DiffWrapper = styled.div`
|
export const DiffWrapper = styled.div`
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
@ -81,3 +82,6 @@ export const DiffTile = styled.div`
|
||||||
export const DiffVersionsSelect = styled.div`
|
export const DiffVersionsSelect = styled.div`
|
||||||
width: 0.625em;
|
width: 0.625em;
|
||||||
`;
|
`;
|
||||||
|
export const BackButton = styled(Button)`
|
||||||
|
margin: 10px 9px;
|
||||||
|
`;
|
||||||
|
|
|
@ -20,6 +20,7 @@ import useAppParams from 'lib/hooks/useAppParams';
|
||||||
import PageHeading from 'components/common/PageHeading/PageHeading';
|
import PageHeading from 'components/common/PageHeading/PageHeading';
|
||||||
|
|
||||||
import * as S from './Diff.styled';
|
import * as S from './Diff.styled';
|
||||||
|
import { BackButton } from './Diff.styled';
|
||||||
|
|
||||||
export interface DiffProps {
|
export interface DiffProps {
|
||||||
versions: SchemaSubject[];
|
versions: SchemaSubject[];
|
||||||
|
@ -77,6 +78,13 @@ const Diff: React.FC<DiffProps> = ({ versions, areVersionsFetched }) => {
|
||||||
backText="Schema Registry"
|
backText="Schema Registry"
|
||||||
backTo={clusterSchemasPath(clusterName)}
|
backTo={clusterSchemasPath(clusterName)}
|
||||||
/>
|
/>
|
||||||
|
<BackButton
|
||||||
|
buttonType="secondary"
|
||||||
|
buttonSize="S"
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</BackButton>
|
||||||
<S.Section>
|
<S.Section>
|
||||||
{areVersionsFetched ? (
|
{areVersionsFetched ? (
|
||||||
<S.DiffBox>
|
<S.DiffBox>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import Diff, { DiffProps } from 'components/Schemas/Diff/Diff';
|
||||||
import { render, WithRoute } from 'lib/testHelpers';
|
import { render, WithRoute } from 'lib/testHelpers';
|
||||||
import { screen } from '@testing-library/react';
|
import { screen } from '@testing-library/react';
|
||||||
import { clusterSchemaComparePath } from 'lib/paths';
|
import { clusterSchemaComparePath } from 'lib/paths';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
import { versions } from './fixtures';
|
import { versions } from './fixtures';
|
||||||
|
|
||||||
|
@ -142,4 +143,24 @@ describe('Diff', () => {
|
||||||
expect(select).toHaveTextContent(versions[0].version);
|
expect(select).toHaveTextContent(versions[0].version);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Back button', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setupComponent({
|
||||||
|
areVersionsFetched: true,
|
||||||
|
versions,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('back button is appear', () => {
|
||||||
|
const backButton = screen.getAllByRole('button', { name: 'Back' });
|
||||||
|
expect(backButton[0]).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('click on back button', () => {
|
||||||
|
const backButton = screen.getAllByRole('button', { name: 'Back' });
|
||||||
|
userEvent.click(backButton[0]);
|
||||||
|
expect(screen.queryByRole('Back')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue