From 01bce45ec429ef51d924ee643dbdcd6ac468130a Mon Sep 17 00:00:00 2001 From: coryjmaklin <79095425+coryjmaklin@users.noreply.github.com> Date: Tue, 25 May 2021 13:05:37 -0400 Subject: [PATCH] ldap group provider plugin --- .../password/PasswordAuthenticatorPlugin.java | 2 + ...nticatorClient.java => JdkLdapClient.java} | 48 +++++++- .../password/ldap/LdapAuthenticator.java | 38 ++---- .../ldap/LdapAuthenticatorFactory.java | 2 +- ...thenticatorClient.java => LdapClient.java} | 5 +- .../password/ldap/LdapGroupProvider.java | 82 +++++++++++++ .../ldap/LdapGroupProviderFactory.java | 53 ++++++++ .../trino/plugin/password/ldap/LdapUtil.java | 46 +++++++ .../password/ldap/TestLdapAuthenticator.java | 113 +----------------- .../plugin/password/ldap/TestLdapClient.java | 102 ++++++++++++++++ .../password/ldap/TestLdapGroupProvider.java | 40 +++++++ .../plugin/password/ldap/TestLdapUtil.java | 53 ++++++++ 12 files changed, 437 insertions(+), 147 deletions(-) rename plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/{JdkLdapAuthenticatorClient.java => JdkLdapClient.java} (80%) rename plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/{LdapAuthenticatorClient.java => LdapClient.java} (84%) create mode 100644 plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapGroupProvider.java create mode 100644 plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapGroupProviderFactory.java create mode 100644 plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapUtil.java create mode 100644 plugin/trino-password-authenticators/src/test/java/io/trino/plugin/password/ldap/TestLdapClient.java create mode 100644 plugin/trino-password-authenticators/src/test/java/io/trino/plugin/password/ldap/TestLdapGroupProvider.java create mode 100644 plugin/trino-password-authenticators/src/test/java/io/trino/plugin/password/ldap/TestLdapUtil.java diff --git a/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/PasswordAuthenticatorPlugin.java b/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/PasswordAuthenticatorPlugin.java index a1b4648904e35..9d118769d74e8 100644 --- a/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/PasswordAuthenticatorPlugin.java +++ b/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/PasswordAuthenticatorPlugin.java @@ -17,6 +17,7 @@ import io.trino.plugin.password.file.FileAuthenticatorFactory; import io.trino.plugin.password.file.FileGroupProviderFactory; import io.trino.plugin.password.ldap.LdapAuthenticatorFactory; +import io.trino.plugin.password.ldap.LdapGroupProviderFactory; import io.trino.plugin.password.salesforce.SalesforceAuthenticatorFactory; import io.trino.spi.Plugin; import io.trino.spi.security.GroupProviderFactory; @@ -40,6 +41,7 @@ public Iterable getGroupProviderFactories() { return ImmutableList.builder() .add(new FileGroupProviderFactory()) + .add(new LdapGroupProviderFactory()) .build(); } } diff --git a/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/JdkLdapAuthenticatorClient.java b/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/JdkLdapClient.java similarity index 80% rename from plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/JdkLdapAuthenticatorClient.java rename to plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/JdkLdapClient.java index 7e0d5f2af8adc..d680a1b9957fd 100644 --- a/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/JdkLdapAuthenticatorClient.java +++ b/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/JdkLdapClient.java @@ -23,9 +23,13 @@ import javax.naming.AuthenticationException; import javax.naming.NamingEnumeration; import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; import javax.naming.directory.DirContext; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; @@ -36,6 +40,7 @@ import java.security.GeneralSecurityException; import java.security.KeyStore; import java.util.Arrays; +import java.util.Enumeration; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -49,16 +54,16 @@ import static javax.naming.Context.SECURITY_CREDENTIALS; import static javax.naming.Context.SECURITY_PRINCIPAL; -public class JdkLdapAuthenticatorClient - implements LdapAuthenticatorClient +public class JdkLdapClient + implements LdapClient { - private static final Logger log = Logger.get(JdkLdapAuthenticatorClient.class); + private static final Logger log = Logger.get(JdkLdapClient.class); private final Map basicEnvironment; private final Optional sslContext; @Inject - public JdkLdapAuthenticatorClient(LdapConfig ldapConfig) + public JdkLdapClient(LdapConfig ldapConfig) { String ldapUrl = requireNonNull(ldapConfig.getLdapUrl(), "ldapUrl is null"); if (ldapUrl.startsWith("ldap://")) { @@ -72,7 +77,7 @@ public JdkLdapAuthenticatorClient(LdapConfig ldapConfig) .build(); this.sslContext = Optional.ofNullable(ldapConfig.getTrustCertificate()) - .map(JdkLdapAuthenticatorClient::createSslContext); + .map(JdkLdapClient::createSslContext); } @Override @@ -106,11 +111,44 @@ public Set lookupUserDistinguishedNames(String searchBase, String search } } + @Override + public Set lookupUserGroups(String searchBase, String searchFilter, String contextUserDistinguishedName, String contextPassword) + throws NamingException + { + try (CloseableContext context = createUserDirContext(contextUserDistinguishedName, contextPassword); CloseableSearchResults search = searchContext(searchBase, searchFilter, context)) { + ImmutableSet.Builder groupNames = ImmutableSet.builder(); + if (search.hasMore()) { + Attributes attributes = search.next().getAttributes(); + Attribute memberOfAttribute = attributes.get("memberof"); + if (memberOfAttribute == null) { + log.error("No memberOf attribute found... The ldap group provider requires the memberOf overlay to be enabled."); + } + else { + for (Enumeration groupDns = memberOfAttribute.getAll(); groupDns.hasMoreElements(); ) { + String dn = groupDns.nextElement().toString(); + LdapName ln = new LdapName(dn); + for (Rdn rdn : ln.getRdns()) { + if (rdn.getType().equalsIgnoreCase("cn")) { + groupNames.add(rdn.getValue().toString()); + break; + } + } + } + } + } + return groupNames.build(); + } + } + private static CloseableSearchResults searchContext(String searchBase, String searchFilter, CloseableContext context) throws NamingException { SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + searchControls.setReturningAttributes(new String[] + { + "memberOf" + }); return new CloseableSearchResults(context.search(searchBase, searchFilter, searchControls)); } diff --git a/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapAuthenticator.java b/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapAuthenticator.java index 89b418a6cf8be..fc14a8bcb0404 100644 --- a/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapAuthenticator.java +++ b/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapAuthenticator.java @@ -14,7 +14,6 @@ package io.trino.plugin.password.ldap; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.CharMatcher; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; @@ -45,10 +44,8 @@ public class LdapAuthenticator implements PasswordAuthenticator { private static final Logger log = Logger.get(LdapAuthenticator.class); - private static final CharMatcher SPECIAL_CHARACTERS = CharMatcher.anyOf(",=+<>#;*()\"\\\u0000"); - private static final CharMatcher WHITESPACE = CharMatcher.anyOf(" \r"); - private final LdapAuthenticatorClient client; + private final LdapClient client; private final List userBindSearchPatterns; private final Optional groupAuthorizationSearchPattern; @@ -59,7 +56,7 @@ public class LdapAuthenticator private final LoadingCache authenticationCache; @Inject - public LdapAuthenticator(LdapAuthenticatorClient client, LdapConfig ldapConfig) + public LdapAuthenticator(LdapClient client, LdapConfig ldapConfig) { this.client = requireNonNull(client, "client is null"); @@ -110,17 +107,17 @@ public Principal createAuthenticatedPrincipal(String user, String password) private Principal authenticateWithUserBind(Credential credential) { String user = credential.getUser(); - if (containsSpecialCharacters(user)) { + if (LdapUtil.containsSpecialCharacters(user)) { throw new AccessDeniedException("Username contains a special LDAP character"); } Exception lastException = new RuntimeException(); for (String userBindSearchPattern : userBindSearchPatterns) { try { - String userDistinguishedName = replaceUser(userBindSearchPattern, user); + String userDistinguishedName = LdapUtil.replaceUser(userBindSearchPattern, user); if (groupAuthorizationSearchPattern.isPresent()) { // user password is also validated as user DN and password is used for querying LDAP String searchBase = userBaseDistinguishedName.orElseThrow(); - String groupSearch = replaceUser(groupAuthorizationSearchPattern.get(), user); + String groupSearch = LdapUtil.replaceUser(groupAuthorizationSearchPattern.get(), user); if (!client.isGroupMember(searchBase, groupSearch, userDistinguishedName, credential.getPassword())) { String message = format("User [%s] not a member of an authorized group", user); log.debug(message); @@ -144,7 +141,7 @@ private Principal authenticateWithUserBind(Credential credential) private Principal authenticateWithBindDistinguishedName(Credential credential) { String user = credential.getUser(); - if (containsSpecialCharacters(user)) { + if (LdapUtil.containsSpecialCharacters(user)) { throw new AccessDeniedException("Username contains a special LDAP character"); } try { @@ -159,27 +156,11 @@ private Principal authenticateWithBindDistinguishedName(Credential credential) return new BasicPrincipal(credential.getUser()); } - /** - * Returns {@code true} when parameter contains a character that has a special meaning in - * LDAP search or bind name (DN). - *

- * Based on Preventing_LDAP_Injection_in_Java and - * {@link javax.naming.ldap.Rdn#escapeValue(Object) escapeValue} method. - */ - @VisibleForTesting - static boolean containsSpecialCharacters(String user) - { - if (WHITESPACE.indexIn(user) == 0 || WHITESPACE.lastIndexIn(user) == user.length() - 1) { - return true; - } - return SPECIAL_CHARACTERS.matchesAnyOf(user); - } - private String lookupUserDistinguishedName(String user) throws NamingException { String searchBase = userBaseDistinguishedName.orElseThrow(); - String searchFilter = replaceUser(groupAuthorizationSearchPattern.orElseThrow(), user); + String searchFilter = LdapUtil.replaceUser(groupAuthorizationSearchPattern.orElseThrow(), user); Set userDistinguishedNames = client.lookupUserDistinguishedNames(searchBase, searchFilter, bindDistinguishedName.orElseThrow(), bindPassword.orElseThrow()); if (userDistinguishedNames.isEmpty()) { String message = format("User [%s] not a member of an authorized group", user); @@ -193,9 +174,4 @@ private String lookupUserDistinguishedName(String user) } return getOnlyElement(userDistinguishedNames); } - - private static String replaceUser(String pattern, String user) - { - return pattern.replace("${USER}", user); - } } diff --git a/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapAuthenticatorFactory.java b/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapAuthenticatorFactory.java index 598a768a472b0..506c7ee363572 100644 --- a/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapAuthenticatorFactory.java +++ b/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapAuthenticatorFactory.java @@ -39,7 +39,7 @@ public PasswordAuthenticator create(Map config) binder -> { configBinder(binder).bindConfig(LdapConfig.class); binder.bind(LdapAuthenticator.class).in(Scopes.SINGLETON); - binder.bind(LdapAuthenticatorClient.class).to(JdkLdapAuthenticatorClient.class).in(Scopes.SINGLETON); + binder.bind(LdapClient.class).to(JdkLdapClient.class).in(Scopes.SINGLETON); }); Injector injector = app diff --git a/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapAuthenticatorClient.java b/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapClient.java similarity index 84% rename from plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapAuthenticatorClient.java rename to plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapClient.java index 8fc82a89f2c06..aee89ef3b4655 100644 --- a/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapAuthenticatorClient.java +++ b/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapClient.java @@ -17,7 +17,7 @@ import java.util.Set; -public interface LdapAuthenticatorClient +public interface LdapClient { void validatePassword(String userDistinguishedName, String password) throws NamingException; @@ -27,4 +27,7 @@ boolean isGroupMember(String searchBase, String groupSearch, String contextUserD Set lookupUserDistinguishedNames(String searchBase, String searchFilter, String contextUserDistinguishedName, String contextPassword) throws NamingException; + + Set lookupUserGroups(String searchBase, String searchFilter, String contextUserDistinguishedName, String contextPassword) + throws NamingException; } diff --git a/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapGroupProvider.java b/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapGroupProvider.java new file mode 100644 index 0000000000000..8855a839bf63a --- /dev/null +++ b/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapGroupProvider.java @@ -0,0 +1,82 @@ +/* + * 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.password.ldap; + +import io.airlift.log.Logger; +import io.trino.spi.security.AccessDeniedException; +import io.trino.spi.security.GroupProvider; + +import javax.inject.Inject; +import javax.naming.NamingException; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +public class LdapGroupProvider + implements GroupProvider +{ + private static final Logger log = Logger.get(LdapGroupProvider.class); + + private final LdapClient client; + + private final List userBindSearchPatterns; + private final Optional userBaseDistinguishedName; + private final Optional bindDistinguishedName; + private final Optional bindPassword; + + @Inject + public LdapGroupProvider(LdapClient client, LdapConfig ldapConfig) + { + this.client = requireNonNull(client, "client is null"); + + this.userBindSearchPatterns = ldapConfig.getUserBindSearchPatterns(); + this.userBaseDistinguishedName = Optional.ofNullable(ldapConfig.getUserBaseDistinguishedName()); + this.bindDistinguishedName = Optional.ofNullable(ldapConfig.getBindDistingushedName()); + this.bindPassword = Optional.ofNullable(ldapConfig.getBindPassword()); + + checkArgument( + userBaseDistinguishedName.isPresent(), + "Base distinguished name (DN) for user must be provided"); + checkArgument( + bindDistinguishedName.isPresent() == bindPassword.isPresent(), + "Both bind distinguished name and bind password must be provided"); + checkArgument( + !userBindSearchPatterns.isEmpty(), + "User bind search pattern must be provided"); + } + + @Override + public Set getGroups(String user) + { + if (LdapUtil.containsSpecialCharacters(user)) { + throw new AccessDeniedException("Username contains a special LDAP character"); + } + for (String userBindSearchPattern : userBindSearchPatterns) { + String userDistinguishedName = LdapUtil.replaceUser(userBindSearchPattern, user); + String searchBase = userBaseDistinguishedName.orElseThrow(); + try { + return client.lookupUserGroups(searchBase, userDistinguishedName, bindDistinguishedName.orElseThrow(), bindPassword.orElseThrow()); + } + catch (NamingException e) { + log.debug(e, "Authentication failed for user [%s], %s", user, e.getMessage()); + throw new RuntimeException("Authentication error"); + } + } + return null; + } +} diff --git a/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapGroupProviderFactory.java b/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapGroupProviderFactory.java new file mode 100644 index 0000000000000..201e508544497 --- /dev/null +++ b/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapGroupProviderFactory.java @@ -0,0 +1,53 @@ +/* + * 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.password.ldap; + +import com.google.inject.Injector; +import com.google.inject.Scopes; +import io.airlift.bootstrap.Bootstrap; +import io.trino.spi.security.GroupProvider; +import io.trino.spi.security.GroupProviderFactory; + +import java.util.Map; + +import static io.airlift.configuration.ConfigBinder.configBinder; + +public class LdapGroupProviderFactory + implements GroupProviderFactory +{ + @Override + public String getName() + { + return "ldap"; + } + + @Override + public GroupProvider create(Map config) + { + Bootstrap app = new Bootstrap( + binder -> { + configBinder(binder).bindConfig(LdapConfig.class); + binder.bind(LdapGroupProvider.class).in(Scopes.SINGLETON); + binder.bind(LdapClient.class).to(JdkLdapClient.class).in(Scopes.SINGLETON); + }); + + Injector injector = app + .strictConfig() + .doNotInitializeLogging() + .setRequiredConfigurationProperties(config) + .initialize(); + + return injector.getInstance(LdapGroupProvider.class); + } +} diff --git a/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapUtil.java b/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapUtil.java new file mode 100644 index 0000000000000..54d4a647f2ca5 --- /dev/null +++ b/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap/LdapUtil.java @@ -0,0 +1,46 @@ +/* + * 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.password.ldap; + +import com.google.common.base.CharMatcher; + +public class LdapUtil +{ + private static final CharMatcher SPECIAL_CHARACTERS = CharMatcher.anyOf(",=+<>#;*()\"\\\u0000"); + private static final CharMatcher WHITESPACE = CharMatcher.anyOf(" \r"); + + private LdapUtil() + { + } + + /** + * Returns {@code true} when parameter contains a character that has a special meaning in + * LDAP search or bind name (DN). + *

+ * Based on Preventing_LDAP_Injection_in_Java and + * {@link javax.naming.ldap.Rdn#escapeValue(Object) escapeValue} method. + */ + public static boolean containsSpecialCharacters(String user) + { + if (WHITESPACE.indexIn(user) == 0 || WHITESPACE.lastIndexIn(user) == user.length() - 1) { + return true; + } + return SPECIAL_CHARACTERS.matchesAnyOf(user); + } + + public static String replaceUser(String pattern, String user) + { + return pattern.replace("${USER}", user); + } +} diff --git a/plugin/trino-password-authenticators/src/test/java/io/trino/plugin/password/ldap/TestLdapAuthenticator.java b/plugin/trino-password-authenticators/src/test/java/io/trino/plugin/password/ldap/TestLdapAuthenticator.java index 8b107e60b31a6..066a5e5bc75b8 100644 --- a/plugin/trino-password-authenticators/src/test/java/io/trino/plugin/password/ldap/TestLdapAuthenticator.java +++ b/plugin/trino-password-authenticators/src/test/java/io/trino/plugin/password/ldap/TestLdapAuthenticator.java @@ -13,20 +13,10 @@ */ package io.trino.plugin.password.ldap; -import com.google.common.collect.HashMultimap; -import com.google.common.collect.ImmutableSet; -import io.trino.plugin.password.Credential; import io.trino.spi.security.AccessDeniedException; import io.trino.spi.security.BasicPrincipal; import org.testng.annotations.Test; -import javax.naming.NamingException; - -import java.util.HashSet; -import java.util.Optional; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.testng.Assert.assertEquals; @@ -38,7 +28,7 @@ public class TestLdapAuthenticator @Test public void testSingleBindPattern() { - TestLdapAuthenticatorClient client = new TestLdapAuthenticatorClient(); + TestLdapClient client = new TestLdapClient(); client.addCredentials("alice@example.com", "alice-pass"); LdapAuthenticator ldapAuthenticator = new LdapAuthenticator( @@ -56,7 +46,7 @@ public void testSingleBindPattern() @Test public void testMultipleBindPattern() { - TestLdapAuthenticatorClient client = new TestLdapAuthenticatorClient(); + TestLdapClient client = new TestLdapClient(); LdapAuthenticator ldapAuthenticator = new LdapAuthenticator( client, @@ -81,7 +71,7 @@ public void testMultipleBindPattern() @Test public void testGroupMembership() { - TestLdapAuthenticatorClient client = new TestLdapAuthenticatorClient(); + TestLdapClient client = new TestLdapClient(); client.addCredentials("alice@example.com", "alice-pass"); LdapAuthenticator ldapAuthenticator = new LdapAuthenticator( @@ -104,7 +94,7 @@ public void testGroupMembership() @Test public void testDistinguishedNameLookup() { - TestLdapAuthenticatorClient client = new TestLdapAuthenticatorClient(); + TestLdapClient client = new TestLdapClient(); client.addCredentials("alice@example.com", "alice-pass"); LdapAuthenticator ldapAuthenticator = new LdapAuthenticator( @@ -137,99 +127,4 @@ public void testDistinguishedNameLookup() assertThatThrownBy(() -> ldapAuthenticator.createAuthenticatedPrincipal("alice", "alice-pass")) .isInstanceOf(AccessDeniedException.class); } - - @Test - public void testContainsSpecialCharacters() - { - assertThat(LdapAuthenticator.containsSpecialCharacters("The quick brown fox jumped over the lazy dogs")) - .as("English pangram") - .isEqualTo(false); - assertThat(LdapAuthenticator.containsSpecialCharacters("Pchnąć w tę łódź jeża lub ośm skrzyń fig")) - .as("Perfect polish pangram") - .isEqualTo(false); - assertThat(LdapAuthenticator.containsSpecialCharacters("いろはにほへと ちりぬるを わかよたれそ つねならむ うゐのおくやま けふこえて あさきゆめみし ゑひもせす(ん)")) - .as("Japanese hiragana pangram - Iroha") - .isEqualTo(false); - assertThat(LdapAuthenticator.containsSpecialCharacters("*")) - .as("LDAP wildcard") - .isEqualTo(true); - assertThat(LdapAuthenticator.containsSpecialCharacters(" John Doe")) - .as("Beginning with whitespace") - .isEqualTo(true); - assertThat(LdapAuthenticator.containsSpecialCharacters("John Doe \r")) - .as("Ending with whitespace") - .isEqualTo(true); - assertThat(LdapAuthenticator.containsSpecialCharacters("Hi (This) = is * a \\ test # ç à ô")) - .as("Multiple special characters") - .isEqualTo(true); - assertThat(LdapAuthenticator.containsSpecialCharacters("John\u0000Doe")) - .as("NULL character") - .isEqualTo(true); - assertThat(LdapAuthenticator.containsSpecialCharacters("John Doe ")) - .as("Angle brackets") - .isEqualTo(true); - } - - private static class TestLdapAuthenticatorClient - implements LdapAuthenticatorClient - { - private final Set credentials = new HashSet<>(); - private final Set groupMembers = new HashSet<>(); - private final HashMultimap userDNs = HashMultimap.create(); - - public void addCredentials(String userDistinguishedName, String password) - { - credentials.add(new Credential(userDistinguishedName, password)); - } - - public void addGroupMember(String userName) - { - groupMembers.add(userName); - } - - public void addDistinguishedNameForUser(String userName, String distinguishedName) - { - userDNs.put(userName, distinguishedName); - } - - @Override - public void validatePassword(String userDistinguishedName, String password) - throws NamingException - { - if (!credentials.contains(new Credential(userDistinguishedName, password))) { - throw new NamingException(); - } - } - - @Override - public boolean isGroupMember(String searchBase, String groupSearch, String contextUserDistinguishedName, String contextPassword) - throws NamingException - { - validatePassword(contextUserDistinguishedName, contextPassword); - return getSearchUser(searchBase, groupSearch) - .map(groupMembers::contains) - .orElse(false); - } - - @Override - public Set lookupUserDistinguishedNames(String searchBase, String searchFilter, String contextUserDistinguishedName, String contextPassword) - throws NamingException - { - validatePassword(contextUserDistinguishedName, contextPassword); - return getSearchUser(searchBase, searchFilter) - .map(userDNs::get) - .orElse(ImmutableSet.of()); - } - - private static Optional getSearchUser(String searchBase, String groupSearch) - { - if (!searchBase.equals(BASE_DN)) { - return Optional.empty(); - } - if (!groupSearch.startsWith(PATTERN_PREFIX)) { - return Optional.empty(); - } - return Optional.of(groupSearch.substring(PATTERN_PREFIX.length())); - } - } } diff --git a/plugin/trino-password-authenticators/src/test/java/io/trino/plugin/password/ldap/TestLdapClient.java b/plugin/trino-password-authenticators/src/test/java/io/trino/plugin/password/ldap/TestLdapClient.java new file mode 100644 index 0000000000000..3cbd3a8a6732f --- /dev/null +++ b/plugin/trino-password-authenticators/src/test/java/io/trino/plugin/password/ldap/TestLdapClient.java @@ -0,0 +1,102 @@ +/* + * 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.password.ldap; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableSet; +import io.trino.plugin.password.Credential; + +import javax.naming.NamingException; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +public class TestLdapClient + implements LdapClient +{ + private static final String BASE_DN = "base-dn"; + private static final String PATTERN_PREFIX = "pattern::"; + private final Set credentials = new HashSet<>(); + private final Set groupMembers = new HashSet<>(); + private final HashMultimap userDNs = HashMultimap.create(); + private final HashMap> userGroupMapping = new HashMap(); + + public void addCredentials(String userDistinguishedName, String password) + { + credentials.add(new Credential(userDistinguishedName, password)); + } + + public void addGroupMember(String userName) + { + groupMembers.add(userName); + } + + public void addDistinguishedNameForUser(String userName, String distinguishedName) + { + userDNs.put(userName, distinguishedName); + } + + public void addUserGroups(String user, Set groups) + { + userGroupMapping.put(user, groups); + } + + @Override + public void validatePassword(String userDistinguishedName, String password) + throws NamingException + { + if (!credentials.contains(new Credential(userDistinguishedName, password))) { + throw new NamingException(); + } + } + + @Override + public boolean isGroupMember(String searchBase, String groupSearch, String contextUserDistinguishedName, String contextPassword) + throws NamingException + { + validatePassword(contextUserDistinguishedName, contextPassword); + return getSearchUser(searchBase, groupSearch) + .map(groupMembers::contains) + .orElse(false); + } + + @Override + public Set lookupUserDistinguishedNames(String searchBase, String searchFilter, String contextUserDistinguishedName, String contextPassword) + throws NamingException + { + validatePassword(contextUserDistinguishedName, contextPassword); + return getSearchUser(searchBase, searchFilter) + .map(userDNs::get) + .orElse(ImmutableSet.of()); + } + + @Override + public Set lookupUserGroups(String searchBase, String searchFilter, String contextUserDistinguishedName, String contextPassword) throws NamingException + { + return userGroupMapping.get(searchFilter); + } + + private static Optional getSearchUser(String searchBase, String groupSearch) + { + if (!searchBase.equals(BASE_DN)) { + return Optional.empty(); + } + if (!groupSearch.startsWith(PATTERN_PREFIX)) { + return Optional.empty(); + } + return Optional.of(groupSearch.substring(PATTERN_PREFIX.length())); + } +} diff --git a/plugin/trino-password-authenticators/src/test/java/io/trino/plugin/password/ldap/TestLdapGroupProvider.java b/plugin/trino-password-authenticators/src/test/java/io/trino/plugin/password/ldap/TestLdapGroupProvider.java new file mode 100644 index 0000000000000..51e79585d4e1c --- /dev/null +++ b/plugin/trino-password-authenticators/src/test/java/io/trino/plugin/password/ldap/TestLdapGroupProvider.java @@ -0,0 +1,40 @@ +/* + * 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.password.ldap; + +import com.google.common.collect.ImmutableSet; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; + +public class TestLdapGroupProvider +{ + @Test + public void testUserGroupLookup() + { + TestLdapClient client = new TestLdapClient(); + + client.addUserGroups("alice@example.com", ImmutableSet.of("group_a", "group_b")); + + LdapGroupProvider ldapGroupProvider = new LdapGroupProvider( + client, + new LdapConfig() + .setUserBaseDistinguishedName("base-dn") + .setUserBindSearchPatterns("${USER}@example.com") + .setBindDistingushedName("server") + .setBindPassword("server-pass")); + + assertEquals(ldapGroupProvider.getGroups("alice"), ImmutableSet.of("group_a", "group_b")); + } +} diff --git a/plugin/trino-password-authenticators/src/test/java/io/trino/plugin/password/ldap/TestLdapUtil.java b/plugin/trino-password-authenticators/src/test/java/io/trino/plugin/password/ldap/TestLdapUtil.java new file mode 100644 index 0000000000000..d783f569e4eb1 --- /dev/null +++ b/plugin/trino-password-authenticators/src/test/java/io/trino/plugin/password/ldap/TestLdapUtil.java @@ -0,0 +1,53 @@ +/* + * 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.password.ldap; + +import org.testng.annotations.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TestLdapUtil +{ + @Test + public void testContainsSpecialCharacters() + { + assertThat(LdapUtil.containsSpecialCharacters("The quick brown fox jumped over the lazy dogs")) + .as("English pangram") + .isEqualTo(false); + assertThat(LdapUtil.containsSpecialCharacters("Pchnąć w tę łódź jeża lub ośm skrzyń fig")) + .as("Perfect polish pangram") + .isEqualTo(false); + assertThat(LdapUtil.containsSpecialCharacters("いろはにほへと ちりぬるを わかよたれそ つねならむ うゐのおくやま けふこえて あさきゆめみし ゑひもせす(ん)")) + .as("Japanese hiragana pangram - Iroha") + .isEqualTo(false); + assertThat(LdapUtil.containsSpecialCharacters("*")) + .as("LDAP wildcard") + .isEqualTo(true); + assertThat(LdapUtil.containsSpecialCharacters(" John Doe")) + .as("Beginning with whitespace") + .isEqualTo(true); + assertThat(LdapUtil.containsSpecialCharacters("John Doe \r")) + .as("Ending with whitespace") + .isEqualTo(true); + assertThat(LdapUtil.containsSpecialCharacters("Hi (This) = is * a \\ test # ç à ô")) + .as("Multiple special characters") + .isEqualTo(true); + assertThat(LdapUtil.containsSpecialCharacters("John\u0000Doe")) + .as("NULL character") + .isEqualTo(true); + assertThat(LdapUtil.containsSpecialCharacters("John Doe ")) + .as("Angle brackets") + .isEqualTo(true); + } +}