Skip to content

Commit

Permalink
Add LDAP group provider plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
mosiac1 authored and OmerRaifler committed Jan 11, 2025
1 parent 08392be commit dded5a9
Show file tree
Hide file tree
Showing 17 changed files with 1,236 additions and 16 deletions.
6 changes: 6 additions & 0 deletions core/trino-server/src/main/provisio/trino.xml
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,12 @@
</artifact>
</artifactSet>

<artifactSet to="plugin/ldap-group-provider">
<artifact id="${project.groupId}:trino-ldap-group-provider:zip:${project.version}">
<unpack />
</artifact>
</artifactSet>

<artifactSet to="plugin/mariadb">
<artifact id="${project.groupId}:trino-mariadb:zip:${project.version}">
<unpack />
Expand Down
141 changes: 141 additions & 0 deletions plugin/trino-ldap-group-provider/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.trino</groupId>
<artifactId>trino-root</artifactId>
<version>469-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

<artifactId>trino-ldap-group-provider</artifactId>
<packaging>trino-plugin</packaging>
<description>Trino - LDAP Group Provider</description>

<properties>
<air.main.basedir>${project.parent.basedir}</air.main.basedir>
</properties>

<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>

<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
</dependency>

<dependency>
<groupId>io.airlift</groupId>
<artifactId>bootstrap</artifactId>
</dependency>

<dependency>
<groupId>io.airlift</groupId>
<artifactId>configuration</artifactId>
</dependency>

<dependency>
<groupId>io.airlift</groupId>
<artifactId>log</artifactId>
</dependency>

<dependency>
<groupId>io.trino</groupId>
<artifactId>trino-plugin-toolkit</artifactId>
</dependency>

<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>io.airlift</groupId>
<artifactId>slice</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-context</artifactId>
<scope>provided</scope>
</dependency>

<!-- Trino SPI -->
<dependency>
<groupId>io.trino</groupId>
<artifactId>trino-spi</artifactId>
<scope>provided</scope>
</dependency>

<!-- for testing - Trino -->
<dependency>
<groupId>io.trino</groupId>
<artifactId>trino-main</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.trino</groupId>
<artifactId>trino-testing</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.trino</groupId>
<artifactId>trino-testing-containers</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.trino.plugin.ldapgroup;

import com.google.common.collect.ImmutableSet;
import com.google.inject.Inject;
import io.airlift.log.Logger;
import io.trino.plugin.base.ldap.LdapClient;
import io.trino.plugin.base.ldap.LdapQuery;
import io.trino.spi.security.GroupProvider;

import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.SearchResult;

import java.util.Optional;
import java.util.Set;

import static java.util.Objects.requireNonNull;

public class LdapFilteringGroupProvider
implements GroupProvider
{
private static final Logger log = Logger.get(LdapFilteringGroupProvider.class);

private final LdapClient ldapClient;
private final String ldapAdminUser;
private final String ldapAdminPassword;
private final String userBaseDN;
private final String userSearchFilter;
private final String groupBaseDN;
private final String groupsNameAttribute;
private final String combinedGroupSearchFilter;

@Inject
public LdapFilteringGroupProvider(LdapClient ldapClient,
LdapGroupProviderConfig config,
LdapFilteringGroupProviderConfig filteringConfig)
{
this.ldapClient = requireNonNull(ldapClient, "ldap client is null");
this.ldapAdminUser = config.getLdapAdminUser();
this.ldapAdminPassword = config.getLdapAdminPassword();
this.userBaseDN = config.getLdapUserBaseDN();
this.userSearchFilter = config.getLdapUserSearchFilter();
this.groupBaseDN = filteringConfig.getLdapGroupBaseDN();
this.groupsNameAttribute = config.getLdapGroupsNameAttribute();

String groupsSearchMemberAttribute = filteringConfig.getLdapGroupsSearchMemberAttribute();
combinedGroupSearchFilter = filteringConfig.getLdapGroupsSearchFilter()
.map(filter -> String.format("(&(%s)(%s={0}))", filter, groupsSearchMemberAttribute))
.orElse(String.format("(%s={0})", groupsSearchMemberAttribute));
}

/**
* Perform an LDAP search for groups, fetching only the names, and returning the name of each group.
* Filters groups by user membership AND filter expression {@link LdapFilteringGroupProviderConfig#getLdapGroupsSearchFilter()}.
* If {@link LdapGroupProviderConfig#getLdapGroupsNameAttribute()} is missing from group document, fallback on full name.
* Swallows LDAP exceptions.
*
* @return Names of groups that the user is a member of
*/
@Override
public Set<String> getGroups(String user)
{
Optional<String> userDistinguishedName;
try {
userDistinguishedName = ldapClient.executeLdapQuery(ldapAdminUser, ldapAdminPassword,
new LdapQuery.LdapQueryBuilder()
.withSearchBase(userBaseDN)
.withSearchFilter(userSearchFilter)
.withFilterArguments(user)
.build(),
search -> {
if (!search.hasMore()) {
log.warn("LDAP search for user [%s] using filter pattern [%s] found no matches", user, userSearchFilter);
return Optional.empty();
}
SearchResult result = search.next();
return Optional.of(result.getNameInNamespace());
});
}
catch (NamingException e) {
log.error("LDAP search for user [%s] failed", user, e);
return ImmutableSet.of();
}

return userDistinguishedName.map(ldapUser -> {
try {
return ldapClient.executeLdapQuery(ldapAdminUser, ldapAdminPassword,
new LdapQuery.LdapQueryBuilder()
.withSearchBase(groupBaseDN)
.withAttributes(groupsNameAttribute)
.withSearchFilter(combinedGroupSearchFilter)
.withFilterArguments(ldapUser)
.build(),
search -> {
if (!search.hasMore()) {
log.debug("No groups found using search [pattern=%s, arguments={%s}]", combinedGroupSearchFilter, ldapUser);
}
ImmutableSet.Builder<String> groupsBuilder = ImmutableSet.builder();
while (search.hasMore()) {
SearchResult groupResult = search.next();
Attribute groupName = groupResult.getAttributes().get(groupsNameAttribute);
if (groupName == null) {
log.warn("The group object [%s] does not have group name attribute [%s]. Falling back on object full name.", groupResult, groupsNameAttribute);
groupsBuilder.add(groupResult.getNameInNamespace());
}
else {
groupsBuilder.add(groupName.get().toString());
}
}
return groupsBuilder.build();
});
}
catch (NamingException e) {
log.error("LDAP search for user [%s] groups failed", user, e);
return ImmutableSet.<String>of();
}
}).orElse(ImmutableSet.of());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.trino.plugin.ldapgroup;

import io.airlift.configuration.Config;
import io.airlift.configuration.ConfigDescription;
import jakarta.validation.constraints.NotNull;

import java.util.Optional;

public class LdapFilteringGroupProviderConfig
{
private String ldapGroupBaseDN;
private String ldapGroupsSearchFilter;
private String ldapGroupsSearchMemberAttribute = "member";

@NotNull
public String getLdapGroupBaseDN()
{
return ldapGroupBaseDN;
}

@Config("ldap.group-base-dn")
@ConfigDescription("Base distinguished name for groups. Example: dc=example,dc=com")
public LdapFilteringGroupProviderConfig setLdapGroupBaseDN(String ldapGroupBaseDN)
{
this.ldapGroupBaseDN = ldapGroupBaseDN;
return this;
}

@NotNull
public Optional<String> getLdapGroupsSearchFilter()
{
return Optional.ofNullable(ldapGroupsSearchFilter);
}

@Config("ldap.group-search-filter")
@ConfigDescription("Search filter for group documents. Example: (cn=trino_*)")
public LdapFilteringGroupProviderConfig setLdapGroupsSearchFilter(String ldapGroupsSearchFilter)
{
this.ldapGroupsSearchFilter = ldapGroupsSearchFilter;
return this;
}

@NotNull
public String getLdapGroupsSearchMemberAttribute()
{
return ldapGroupsSearchMemberAttribute;
}

@Config("ldap.group-search-member-attribute")
@ConfigDescription("Attribute from group documents used for filtering by member. Example: cn")
public LdapFilteringGroupProviderConfig setLdapGroupsSearchMemberAttribute(String ldapGroupsSearchMemberAttribute)
{
this.ldapGroupsSearchMemberAttribute = ldapGroupsSearchMemberAttribute;
return this;
}
}
Loading

0 comments on commit dded5a9

Please sign in to comment.