Explorar el Código

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

Roman Zabaluev hace 2 años
padre
commit
5c12f1b53f

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

@@ -14,13 +14,11 @@ public class LdapProperties {
   private String adminPassword;
   private String userFilterSearchBase;
   private String userFilterSearchFilter;
+  private String groupFilterSearchBase;
 
   @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;
-
 }

+ 13 - 4
kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapSecurityConfig.java

@@ -3,7 +3,6 @@ package com.provectus.kafka.ui.config.auth;
 import static com.provectus.kafka.ui.config.auth.AbstractAuthSecurityConfig.AUTH_WHITELIST;
 
 import com.provectus.kafka.ui.service.rbac.AccessControlService;
-import com.provectus.kafka.ui.service.rbac.extractor.RbacLdapAuthoritiesExtractor;
 import java.util.Collection;
 import java.util.List;
 import javax.annotation.Nullable;
@@ -12,7 +11,6 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.ApplicationContext;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.context.annotation.Import;
@@ -34,6 +32,8 @@ import org.springframework.security.ldap.authentication.LdapAuthenticationProvid
 import org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider;
 import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
 import org.springframework.security.ldap.search.LdapUserSearch;
+import org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator;
+import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
 import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper;
 import org.springframework.security.web.server.SecurityWebFilterChain;
 
@@ -50,7 +50,7 @@ public class LdapSecurityConfig {
 
   @Bean
   public ReactiveAuthenticationManager authenticationManager(BaseLdapPathContextSource contextSource,
-                                                             ApplicationContext context,
+                                                             LdapAuthoritiesPopulator ldapAuthoritiesPopulator,
                                                              @Nullable AccessControlService acs) {
     var rbacEnabled = acs != null && acs.isRbacEnabled();
     BindAuthenticator ba = new BindAuthenticator(contextSource);
@@ -67,7 +67,7 @@ public class LdapSecurityConfig {
     AbstractLdapAuthenticationProvider authenticationProvider;
     if (!props.isActiveDirectory()) {
       authenticationProvider = rbacEnabled
-          ? new LdapAuthenticationProvider(ba, new RbacLdapAuthoritiesExtractor(context))
+          ? new LdapAuthenticationProvider(ba, ldapAuthoritiesPopulator)
           : new LdapAuthenticationProvider(ba);
     } else {
       authenticationProvider = new ActiveDirectoryLdapAuthenticationProvider(props.getActiveDirectoryDomain(),
@@ -95,6 +95,15 @@ public class LdapSecurityConfig {
     return ctx;
   }
 
+  @Bean
+  @Primary
+  public LdapAuthoritiesPopulator ldapAuthoritiesPopulator(BaseLdapPathContextSource contextSource) {
+    var authoritiesPopulator = new DefaultLdapAuthoritiesPopulator(contextSource, props.getGroupFilterSearchBase());
+    authoritiesPopulator.setRolePrefix("");
+    authoritiesPopulator.setConvertToUpperCase(false);
+    return authoritiesPopulator;
+  }
+
   @Bean
   public SecurityWebFilterChain configureLdap(ServerHttpSecurity http) {
     log.info("Configuring LDAP authentication.");

+ 10 - 9
kafka-ui-api/src/main/java/com/provectus/kafka/ui/emitter/MessageFilters.java

@@ -39,41 +39,42 @@ public class MessageFilters {
   }
 
   static Predicate<TopicMessageDTO> groovyScriptFilter(String script) {
-    var compiledScript = compileScript(script);
+    var engine = getGroovyEngine();
+    var compiledScript = compileScript(engine, script);
     var jsonSlurper = new JsonSlurper();
     return new Predicate<TopicMessageDTO>() {
       @SneakyThrows
       @Override
       public boolean test(TopicMessageDTO msg) {
-        var bindings = getGroovyEngine().createBindings();
+        var bindings = engine.createBindings();
         bindings.put("partition", msg.getPartition());
         bindings.put("offset", msg.getOffset());
         bindings.put("timestampMs", msg.getTimestamp().toInstant().toEpochMilli());
         bindings.put("keyAsText", msg.getKey());
         bindings.put("valueAsText", msg.getContent());
         bindings.put("headers", msg.getHeaders());
-        bindings.put("key", parseToJsonOrReturnNull(jsonSlurper, msg.getKey()));
-        bindings.put("value", parseToJsonOrReturnNull(jsonSlurper, msg.getContent()));
+        bindings.put("key", parseToJsonOrReturnAsIs(jsonSlurper, msg.getKey()));
+        bindings.put("value", parseToJsonOrReturnAsIs(jsonSlurper, msg.getContent()));
         var result = compiledScript.eval(bindings);
         if (result instanceof Boolean) {
           return (Boolean) result;
         } else {
           throw new ValidationException(
-              String.format("Unexpected script result: %s, Boolean should be returned instead", result));
+              "Unexpected script result: %s, Boolean should be returned instead".formatted(result));
         }
       }
     };
   }
 
   @Nullable
-  private static Object parseToJsonOrReturnNull(JsonSlurper parser, @Nullable String str) {
+  private static Object parseToJsonOrReturnAsIs(JsonSlurper parser, @Nullable String str) {
     if (str == null) {
       return null;
     }
     try {
       return parser.parseText(str);
     } catch (Exception e) {
-      return null;
+      return str;
     }
   }
 
@@ -86,9 +87,9 @@ public class MessageFilters {
     return GROOVY_ENGINE;
   }
 
-  private static CompiledScript compileScript(String script) {
+  private static CompiledScript compileScript(GroovyScriptEngineImpl engine, String script) {
     try {
-      return getGroovyEngine().compile(script);
+      return engine.compile(script);
     } catch (ScriptException e) {
       throw new ValidationException("Script syntax error: " + e.getMessage());
     }

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

@@ -1,70 +0,0 @@
-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());
-  }
-
-}

+ 23 - 7
kafka-ui-api/src/test/java/com/provectus/kafka/ui/emitter/MessageFiltersTest.java

@@ -118,10 +118,18 @@ class MessageFiltersTest {
     }
 
     @Test
-    void keySetToNullIfKeyCantBeParsedToJson() {
-      var f = groovyScriptFilter("key == null");
+    void keySetToKeyStringIfCantBeParsedToJson() {
+      var f = groovyScriptFilter("key == \"not json\"");
       assertTrue(f.test(msg().key("not json")));
-      assertFalse(f.test(msg().key("{ \"k\" : \"v\" }")));
+    }
+
+    @Test
+    void keyAndKeyAsTextSetToNullIfRecordsKeyIsNull() {
+      var f = groovyScriptFilter("key == null");
+      assertTrue(f.test(msg().key(null)));
+
+      f = groovyScriptFilter("keyAsText == null");
+      assertTrue(f.test(msg().key(null)));
     }
 
     @Test
@@ -132,10 +140,18 @@ class MessageFiltersTest {
     }
 
     @Test
-    void valueSetToNullIfKeyCantBeParsedToJson() {
-      var f = groovyScriptFilter("value == null");
+    void valueSetToContentStringIfCantBeParsedToJson() {
+      var f = groovyScriptFilter("value == \"not json\"");
       assertTrue(f.test(msg().content("not json")));
-      assertFalse(f.test(msg().content("{ \"k\" : \"v\" }")));
+    }
+
+    @Test
+    void valueAndValueAsTextSetToNullIfRecordsContentIsNull() {
+      var f = groovyScriptFilter("value == null");
+      assertTrue(f.test(msg().content(null)));
+
+      f = groovyScriptFilter("valueAsText == null");
+      assertTrue(f.test(msg().content(null)));
     }
 
     @Test
@@ -185,4 +201,4 @@ class MessageFiltersTest {
         .partition(1);
   }
 
-}
+}

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

@@ -3,6 +3,7 @@ package com.provectus.kafka.ui.pages.ksqldb;
 import static com.codeborne.selenide.Condition.visible;
 import static com.codeborne.selenide.Selenide.$$x;
 import static com.codeborne.selenide.Selenide.$x;
+import static com.codeborne.selenide.Selenide.sleep;
 
 import com.codeborne.selenide.CollectionCondition;
 import com.codeborne.selenide.Condition;
@@ -37,6 +38,7 @@ public class KsqlQueryForm extends BasePage {
   @Step
   public KsqlQueryForm clickClearBtn() {
     clickByJavaScript(clearBtn);
+    sleep(500);
     return this;
   }
 

+ 5 - 1
kafka-ui-react-app/src/components/Brokers/Broker/Configs/Configs.tsx

@@ -34,7 +34,11 @@ const Configs: React.FC = () => {
 
   const getData = () => {
     return data
-      .filter((item) => item.name.toLocaleLowerCase().indexOf(keyword) > -1)
+      .filter(
+        (item) =>
+          item.name.toLocaleLowerCase().indexOf(keyword.toLocaleLowerCase()) >
+          -1
+      )
       .sort((a, b) => {
         if (a.source === b.source) return 0;
 

+ 2 - 9
kafka-ui-react-app/src/components/Dashboard/Dashboard.tsx

@@ -1,4 +1,4 @@
-import React, { useEffect, useMemo } from 'react';
+import React, { useMemo } from 'react';
 import PageHeading from 'components/common/PageHeading/PageHeading';
 import * as Metrics from 'components/common/Metrics';
 import { Tag } from 'components/common/Tag/Tag.styled';
@@ -10,7 +10,6 @@ import Table, { SizeCell } from 'components/common/NewTable';
 import useBoolean from 'lib/hooks/useBoolean';
 import { clusterNewConfigPath } from 'lib/paths';
 import { GlobalSettingsContext } from 'components/contexts/GlobalSettingsContext';
-import { useNavigate } from 'react-router-dom';
 import { ActionCanButton } from 'components/common/ActionComponent';
 import { useGetUserInfo } from 'lib/hooks/api/roles';
 
@@ -23,7 +22,7 @@ const Dashboard: React.FC = () => {
   const clusters = useClusters();
   const { value: showOfflineOnly, toggle } = useBoolean(false);
   const appInfo = React.useContext(GlobalSettingsContext);
-  const navigate = useNavigate();
+
   const config = React.useMemo(() => {
     const clusterList = clusters.data || [];
     const offlineClusters = clusterList.filter(
@@ -58,12 +57,6 @@ const Dashboard: React.FC = () => {
     return initialColumns;
   }, []);
 
-  useEffect(() => {
-    if (appInfo.hasDynamicConfig && !clusters.data) {
-      navigate(clusterNewConfigPath);
-    }
-  }, [clusters, appInfo.hasDynamicConfig]);
-
   const isApplicationConfig = useMemo(() => {
     return !!data?.userInfo?.permissions.some(
       (permission) => permission.resource === ResourceType.APPLICATIONCONFIG

+ 0 - 45
kafka-ui-react-app/src/components/Dashboard/__test__/Dashboard.spec.tsx

@@ -1,45 +0,0 @@
-import React from 'react';
-import { useClusters } from 'lib/hooks/api/clusters';
-import Dashboard from 'components/Dashboard/Dashboard';
-import { Cluster, ServerStatus } from 'generated-sources';
-import { render } from 'lib/testHelpers';
-
-interface DataType {
-  data: Cluster[] | undefined;
-}
-jest.mock('lib/hooks/api/clusters');
-const mockedNavigate = jest.fn();
-jest.mock('react-router-dom', () => ({
-  ...jest.requireActual('react-router-dom'),
-  useNavigate: () => mockedNavigate,
-}));
-describe('Dashboard component', () => {
-  const renderComponent = (hasDynamicConfig: boolean, data: DataType) => {
-    const useClustersMock = useClusters as jest.Mock;
-    useClustersMock.mockReturnValue(data);
-    render(<Dashboard />, {
-      globalSettings: { hasDynamicConfig },
-    });
-  };
-  it('redirects to new cluster configuration page if there are no clusters and dynamic config is enabled', async () => {
-    await renderComponent(true, { data: undefined });
-
-    expect(mockedNavigate).toHaveBeenCalled();
-  });
-
-  it('should not navigate to new cluster config page when there are clusters', async () => {
-    await renderComponent(true, {
-      data: [{ name: 'Cluster 1', status: ServerStatus.ONLINE }],
-    });
-
-    expect(mockedNavigate).not.toHaveBeenCalled();
-  });
-
-  it('should not navigate to new cluster config page when there are no clusters and hasDynamicConfig is false', async () => {
-    await renderComponent(false, {
-      data: [],
-    });
-
-    expect(mockedNavigate).not.toHaveBeenCalled();
-  });
-});

+ 1 - 0
kafka-ui-react-app/src/components/KsqlDb/Query/QueryForm/QueryForm.styled.ts

@@ -33,6 +33,7 @@ export const Fieldset = styled.fieldset`
   flex: 1;
   flex-direction: column;
   gap: 8px;
+  color: ${({ theme }) => theme.default.color.normal};
 `;
 
 export const ButtonsContainer = styled.div`

+ 3 - 6
kafka-ui-react-app/src/components/Schemas/Diff/Diff.styled.ts

@@ -14,9 +14,6 @@ export const DiffWrapper = styled.div`
     background-color: ${({ theme }) => theme.default.backgroundColor};
     color: ${({ theme }) => theme.default.color.normal};
   }
-  .ace_line {
-    background-color: ${({ theme }) => theme.default.backgroundColor};
-  }
   .ace_gutter-cell {
     background-color: ${({ theme }) =>
       theme.ksqlDb.query.editor.cell.backgroundColor};
@@ -39,10 +36,10 @@ export const DiffWrapper = styled.div`
   .ace_string {
     color: ${({ theme }) => theme.ksqlDb.query.editor.aceString};
   }
-  > .codeMarker {
-    background: ${({ theme }) => theme.icons.warningIcon};
+  .codeMarker {
+    background-color: ${({ theme }) => theme.ksqlDb.query.editor.codeMarker};
     position: absolute;
-    z-index: 20;
+    z-index: 2000;
   }
 `;
 

+ 4 - 0
kafka-ui-react-app/src/components/common/Switch/Switch.styled.ts

@@ -39,6 +39,10 @@ export const StyledSlider = styled.span<Props>`
   transition: 0.4s;
   border-radius: 20px;
 
+  :hover {
+    background-color: ${({ theme }) => theme.switch.hover};
+  }
+
   &::before {
     position: absolute;
     content: '';

+ 1 - 1
kafka-ui-react-app/src/components/contexts/GlobalSettingsContext.tsx

@@ -2,7 +2,7 @@ import { useAppInfo } from 'lib/hooks/api/appConfig';
 import React from 'react';
 import { ApplicationInfoEnabledFeaturesEnum } from 'generated-sources';
 
-export interface GlobalSettingsContextProps {
+interface GlobalSettingsContextProps {
   hasDynamicConfig: boolean;
 }
 

+ 2 - 7
kafka-ui-react-app/src/lib/testHelpers.tsx

@@ -26,10 +26,7 @@ import {
 } from '@tanstack/react-query';
 import { ConfirmContextProvider } from 'components/contexts/ConfirmContext';
 import ConfirmationModal from 'components/common/ConfirmationModal/ConfirmationModal';
-import {
-  GlobalSettingsContext,
-  GlobalSettingsContextProps,
-} from 'components/contexts/GlobalSettingsContext';
+import { GlobalSettingsContext } from 'components/contexts/GlobalSettingsContext';
 import { UserInfoRolesAccessContext } from 'components/contexts/UserInfoRolesAccessContext';
 
 import { RolesType, modifyRolesData } from './permissions';
@@ -38,7 +35,6 @@ interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
   preloadedState?: Partial<RootState>;
   store?: Store<Partial<RootState>, AnyAction>;
   initialEntries?: MemoryRouterProps['initialEntries'];
-  globalSettings?: GlobalSettingsContextProps;
   userInfo?: {
     roles?: RolesType;
     rbacFlag: boolean;
@@ -114,7 +110,6 @@ const customRender = (
       preloadedState,
     }),
     initialEntries,
-    globalSettings = { hasDynamicConfig: false },
     userInfo,
     ...renderOptions
   }: CustomRenderOptions = {}
@@ -124,7 +119,7 @@ const customRender = (
     children,
   }) => (
     <TestQueryClientProvider>
-      <GlobalSettingsContext.Provider value={globalSettings}>
+      <GlobalSettingsContext.Provider value={{ hasDynamicConfig: false }}>
         <ThemeProvider theme={theme}>
           <TestUserInfoProvider data={userInfo}>
             <ConfirmContextProvider>

+ 5 - 2
kafka-ui-react-app/src/theme/theme.ts

@@ -235,12 +235,13 @@ const baseTheme = {
     color: Colors.neutral[90],
   },
   switch: {
-    unchecked: Colors.brand[30],
+    unchecked: Colors.neutral[20],
+    hover: Colors.neutral[40],
     checked: Colors.brand[50],
     circle: Colors.neutral[0],
     disabled: Colors.neutral[10],
     checkedIcon: {
-      backgroundColor: Colors.neutral[70],
+      backgroundColor: Colors.neutral[10],
     },
   },
   pageLoader: {
@@ -366,6 +367,7 @@ export const theme = {
         cursor: Colors.neutral[90],
         variable: Colors.red[50],
         aceString: Colors.green[60],
+        codeMarker: Colors.yellow[20],
       },
     },
   },
@@ -767,6 +769,7 @@ export const darkTheme: ThemeType = {
         cursor: Colors.neutral[0],
         variable: Colors.red[50],
         aceString: Colors.green[60],
+        codeMarker: Colors.yellow[20],
       },
     },
   },