Repository: provectus/kafka-ui
Issue: RBAC: Support LDAP
Labels: type/enhancement
scope/backend
status/accepted
area/rbac

For RBAC AD support see #3741



Feature implemented, how to set this up:
- Set up LDAP authentication, docs [here](https://docs.kafka-ui.provectus.io/configuration/authentication/ldap-active-directory)
- Set up RBAC with LDAP subjects, docs [here](https://docs.kafka-ui.provectus.io/configuration/rbac-role-based-access-control)




Issue comments:
We'd need some volunteers to test things out.

We coordinate in discord, a link to the thread -> [here](https://discord.com/channels/897805035122077716/1098621183584382976). 

Image: `docker pull public.ecr.aws/provectus/kafka-ui-custom-build:3700`
Config example: [here](https://github.com/provectus/kafka-ui/pull/3700#issuecomment-1518369213)

Please check
1. That authentication with all users you need works fine.
2. RBAC groups matching works fine.

AD a bit later, it requires more black magic.
As I found no volunteers w/ AD, it's gonna be a separate issue. Please upvote #3741 if you're interested.
@Haarolean 
Tested RBAC for image public.ecr.aws/provectus/kafka-ui-custom-build:3700 with LDAP configuration but not able login with this image and I dont see any errors in logs
> @Haarolean 
> 
> Tested RBAC for image public.ecr.aws/provectus/kafka-ui-custom-build:3700 with LDAP configuration but not able login with this image and I dont see any errors in logs

Most likely, your configuration is invalid. Please share your config. 

@Haarolean  I have customized the docker image with Ldap configs and roles.yaml file, 
Dockerfile
```
FROM public.ecr.aws/provectus/kafka-ui-custom-build:3700
USER root
ENV KAFKA_CLUSTERS_0_NAME=kafka
ENV KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=<kafka broker:port>
ENV KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL=SASL_PLAINTEXT
ENV KAFKA_CLUSTERS_0_PROPERTIES_SASL_MECHANISM=GSSAPI
ENV KAFKA_CLUSTERS_0_PROPERTIES_SASL_JAAS_CONFIG='com.sun.security.auth.module.Krb5LoginModule required useTicketCache=false principal="******.COM" useKeyTab=true serviceName="kafka" keyTab="/keytabs/******.keytab";'
ENV KAFKA_CLUSTERS_0_SCHEMAREGISTRY=<schema registry:port>
ENV KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME=dev
ENV KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS=<kafka connect:port>
ENV SPRING_CONFIG_ADDITIONAL-LOCATION= /roles.yml
ENV KAFKA_CLUSTERS_0_READONLY=false
COPY krb5.conf /etc/krb5.conf
COPY roles.yml /roles.yml
COPY *****.keytab /keytabs/
COPY entrypoint.sh /
ENTRYPOINT ["/entrypoint.sh"]

```

roles.yml file
```
rbac:
  roles:
    - name: "readonly"
      clusters:
        - <kafka dev>
      subjects:
        - provider: ldap
          type: group
          value: "OU=Users,OU=Accounts,OU=Resources,DC=<domain>,DC=com"
        - provider: ldap_ad
          type: user
          value: "CN  <name>"
      permissions:
        - resource: clusterconfig
          actions: [ "view" ]

        - resource: topic
          value: ".*"
          actions:
            - VIEW
            - MESSAGES_READ

        - resource: consumer
          value: ".*"
          actions: [ view ]

        - resource: schema
          value: ".*"
          actions: [ view ]

        - resource: connect
          value: ".*"
          actions: [ view ]
```
@padmaachuth you haven't configured LDAP auth at all, see the first lines of the config example I provided
> @padmaachuth you haven't configured LDAP auth at all, see the first lines of the config example I provided

@Haarolean deploying image in openshift passing LDAP config from ENV 

KAFKA_CLUSTERS_0_NAME: <Cluster Name>
KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: <Bootstrap servers:port>
KAFKA_CLUSTERS_0_PROPERTIES_SECURITY_PROTOCOL: SASL_PLAINTEXT
KAFKA_CLUSTERS_0_PROPERTIES_SASL_MECHANISM: GSSAPI
KAFKA_CLUSTERS_0_SCHEMAREGISTRY: <Schema Registry URL>
KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME: <Kafka Connector Name>
KAFKA_CLUSTERS_0_PROPERTIES_SASL_JAAS_CONFIG: com.sun.security.auth.module.Krb5LoginModule required useTicketCache=false principal="<principal name>@DOMAIN.COM" useKeyTab=true serviceName="kafka" keyTab="/keytabs/<Principal Name>.keytab";
KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS: <Kafka Connector URL>
KAFKA_CLUSTERS_0_READONLY: false
AUTH_TYPE: LDAP
SPRING_CONFIG_ADDITIONAL-LOCATION: /roles.yml
SPRING_LDAP_URLS: <LDAP URL>
SPRING_LDAP_USER-FILTER-SEARCH-BASE: DC=Domain,DC=com
SPRING_LDAP_USER-FILTER-SEARCH-FILTER: (&(sAMAccountName={0})(objectClass=person))
SPRING_LDAP_ADMINUSER: <admin user>
SPRING_LDAP_ADMINPASSWORD:<password>  

With this LDAP config able to Login but it not listing cluster, topics,schema,connectors,etc.
@padmaachuth please raise a discussion or ask in discord community, that's unrelated to this issue
RBAC with LDAP does not work for me on v0.7.0.
I have the following config:

```yaml
kafka:
  clusters:
    - name: dev
      readOnly: false
      bootstrapServers: ${KAFKA_BOOTSTRAP}:9093

auth:
  type: LDAP
spring:
  ldap:
    urls: ldap://${AD_SERVER}
    base: "sAMAccountName={0},ou=${USERS_OU},dc=${AD_SERVER}"
    admin-user: "cn=${LDAP_ADMIN_USERNAME},dc=${AD_SERVER}"
    admin-password: "${LDAP_ADMIN_PASSWORD}"
    user-filter-search-base: "dc=${AD_SERVER}"
    user-filter-search-filter: "(&(sAMAccountName={0})(objectClass=user)(|(memberof=CN=${GROUP_NAME},OU=${GROUPS_OU},DC=${AD_SERVER})))"
    group-filter-search-base: "OU=${GROUPS_OU},DC=${AD_SERVER}"
oauth2:
  ldap:
    activeDirectory: false
    activeDirectory.domain: "${AD_SERVER}"

rbac:
  roles:
    - name: "admins"
      clusters:
        - dev
      subjects:
        - provider: ldap
          type: group
          value: "${GROUP_NAME}"
      permissions:
        - resource: applicationconfig
          # value not applicable for applicationconfig
          actions:
            - view
            # - edit
        - resource: clusterconfig
          # value not applicable for clusterconfig
          actions:
            - view
            # - edit
        - resource: topic
          value: ".*"
          actions:
            - view
            # - create
            # - edit
            # - delete
            - messages_read
            - messages_produce
            - messages_delete
        - resource: consumer
          value: ".*"
          actions:
            - view
            - delete
            - reset_offsets
        - resource: schema
          value: ".*"
          actions:
            - view
            - create
            - delete
            - edit
            # - modify_global_compatibility
        - resource: connect
          value: ".*"
          actions:
            - view
            # - edit
            # - create
        - resource: ksql
          # value not applicable for ksql
          actions:
            - execute
        # - resource: acl
        #   # value not applicable for acl
        #   value: ".*"  # FIXME: it crashes if this is removed
        #   actions:
        #     - view
        #     # - edit
```

I am able to login but I cannot see any clusters:

![image](https://github.com/provectus/kafka-ui/assets/1152793/e1e79660-bdbd-4876-bffd-944a57946b5c)

I have enabled DEBUG logs and I can see this:

```log
DEBUG [boundedElastic-3] o.s.s.l.u.DefaultLdapAuthoritiesPopulator: Found roles from search [{spring.security.ldap.dn=[CN=${GROUP_NAME},OU=${GROUPS_OU},DC=${AD_SERVER}], cn=[${GROUP_NAME}]}]
DEBUG [reactor-http-epoll-1] o.s.s.w.s.a.DelegatingReactiveAuthorizationManager: Checking authorization on '/api/clusters' using org.springframework.security.authorization.AuthenticatedReactiveAuthorizationManager@XXXXX
DEBUG [reactor-http-epoll-1] o.s.s.w.s.c.WebSessionServerSecurityContextRepository: Found SecurityContext 'SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=com.provectus.kafka.ui.config.auth.RbacLdapUser@XXXXX, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[${GROUP_NAME}]]]' in WebSession: 'org.springframework.web.server.session.InMemoryWebSessionStore$InMemoryWebSession@XXXXX'
DEBUG [reactor-http-epoll-1] o.s.s.w.s.a.AuthorizationWebFilter: Authorization successful
DEBUG [reactor-http-epoll-1] o.s.w.r.r.m.a.RequestMappingHandlerMapping: [XXXXX] Mapped to com.provectus.kafka.ui.controller.ClustersController#getClusters(ServerWebExchange)
DEBUG [reactor-http-epoll-1] o.s.w.r.r.m.a.ResponseEntityResultHandler: [XXXXX] Using 'application/json' given [*/*] and supported [application/json]
DEBUG [reactor-http-epoll-1] o.s.w.r.r.m.a.ResponseEntityResultHandler: [XXXXX] 0..N [com.provectus.kafka.ui.model.ClusterDTO]
DEBUG [reactor-http-epoll-1] o.s.s.w.s.c.WebSessionServerSecurityContextRepository: Found SecurityContext 'SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=com.provectus.kafka.ui.config.auth.RbacLdapUser@XXXXX, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[${GROUP_NAME}]]]' in WebSession: 'org.springframework.web.server.session.InMemoryWebSessionStore$InMemoryWebSession@XXXXX'
DEBUG [reactor-http-epoll-1] o.s.w.s.a.HttpWebHandlerAdapter: [XXXXX] Completed 200 OK
```

If I remove the `rbac` section, I can login and I can see and do everything because there are no roles.

Let me know if I should open a separate issue.

Thanks.
@alexisph please raise either a new _discussion_ or join us on discord (the link available in readme)

Pull request review comments:

Patch that resolved the issue:
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapProperties.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapProperties.java
index 13119b3bb94..9d07aca2dd5 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapProperties.java
+++ b/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;
-
 }
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapSecurityConfig.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapSecurityConfig.java
index fae1125239e..ce04a2e1659 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapSecurityConfig.java
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/config/auth/LdapSecurityConfig.java
@@ -3,7 +3,6 @@
 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 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.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 ReactiveAuthenticationManager authenticationManager(BaseLdapPathContextSo
     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 BaseLdapPathContextSource contextSource() {
     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.");
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java
deleted file mode 100644
index e24fc0aeda9..00000000000
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/service/rbac/extractor/RbacLdapAuthoritiesExtractor.java
+++ /dev/null
@@ -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());
-  }
-
-}


Test patch: