Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LDAP Group Provider Support #8335

Closed
wants to merge 1 commit into from

Conversation

coryjmaklin
Copy link

@coryjmaklin coryjmaklin commented Jun 21, 2021

This PR adds the code necessary to reference LDAP groups in Trino ACLs.

Context

We're using file based ACLs to authorize users connected to Trino.
https://trino.io/docs/current/security/file-system-access-control.html

In the documentation, it's mentioned "For group-based rules to match, users need to be assigned to groups by a Group provider."

In other words, we have to create a group provider to specify the following:

{
"group": "trino_admins",
"privileges": ["SELECT", "INSERT", "DELETE", "OWNERSHIP"]
}

There exists a trino-password-authenticators plugin that takes care of authenticating users using their LDAP credentials.
https://github.com/trinodb/trino/tree/master/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/ldap

In the same plugin, there's already have a file based Group Provider.
https://github.com/trinodb/trino/blob/master/plugin/trino-password-authenticators/src/main/java/io/trino/plugin/password/PasswordAuthenticatorPlugin.java

There are open issues asking to add this as a feature.
#6824
#2919

Implementation

  • We move the common functions into a utility class.
  • We rename the LdapAuthenticatorClient to LdapClient because Authenticator is a type of Trino plugin, and we're re-purposing the class for the Group Provider plugin.
  • We know that a LDAP group will have a common name (i.e. cn) because it's required by the definition
  • When retrieving the "memberof" attribute, the 'O' must be lowercase. When specifying that we want it in the search results, we the 'O' must be uppercase.

Testing

  • Configure the memberOf overlay in your LDAP instance

  • Create a group-provider.properties with the following properties:

group-provider.name=ldap
ldap.allow-insecure=
ldap.url=
ldap.user-base-dn=
ldap.user-bind-pattern=
ldap.bind-dn=
ldap.bind-password=
  • Connect to Trino and verify the ACLs are applied accordingly.

@cla-bot
Copy link

cla-bot bot commented Jun 21, 2021

Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: U-SLT\cjmakli.
This is most likely caused by a git client misconfiguration; please make sure to:

  1. check if your git client is configured with an email to sign commits git config --list | grep email
  2. If not, set it up using git config --global user.email [email protected]
  3. Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails

Copy link
Member

@cccs-tom cccs-tom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I flagged a couple of minor things, but overall, lgtm!

{
this.client = requireNonNull(client, "client is null");

this.userBindSearchPatterns = ldapConfig.getUserBindSearchPatterns();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we perform a requireNonNull() check on ldapConfig before reaching this point? Or are we just letting the NPE happen in that case?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it's unnecessary because unlike the client, we're not keeping a reference, and we check the values afterwards.

}
}
}
search.close();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: This probably doesn't need to be closed explicitly, since it's managed by the try-with-resources.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

@cccs-mjs
Copy link

Typo in PR description: necessarily -> necessary

Copy link

@cccs-mjs cccs-mjs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems good. In the PR description it'd be useful to provide a sentence or two about the proposed code changes in general terms (e.g. new classes, name changes, moving shared methods out LdapUtil etc)

ImmutableSet.Builder<String> groupNames = ImmutableSet.builder();
if (search.hasMore()) {
Attributes attributes = search.next().getAttributes();
Attribute memberOfAttribute = attributes.get("memberof");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is memberof case sensitive? Elsewhere, on lines 124 and 151 you use memberOf.

Also, could make this string a constant.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For whatever reason, memberof has to have a lowercase 'O' on line 122, and uppercase on line 151. If you don't it returns null.

@@ -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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for renaming the class? Could add to PR description.

@@ -17,7 +17,7 @@

import java.util.Set;

public interface LdapAuthenticatorClient
public interface LdapClient

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for renaming the interface? Could add to PR description.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an explanation in the PR description. Essentially, Authenticator is a type of Trino plugin, but we're re-purposing the client for Group Provider plugin as well.

@cla-bot
Copy link

cla-bot bot commented Jun 22, 2021

Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: U-SLT\cjmakli.
This is most likely caused by a git client misconfiguration; please make sure to:

  1. check if your git client is configured with an email to sign commits git config --list | grep email
  2. If not, set it up using git config --global user.email [email protected]
  3. Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails

@cla-bot
Copy link

cla-bot bot commented Jun 22, 2021

Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: U-SLT\cjmakli.
This is most likely caused by a git client misconfiguration; please make sure to:

  1. check if your git client is configured with an email to sign commits git config --list | grep email
  2. If not, set it up using git config --global user.email [email protected]
  3. Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails

@cla-bot
Copy link

cla-bot bot commented Jun 22, 2021

Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: U-SLT\cjmakli.
This is most likely caused by a git client misconfiguration; please make sure to:

  1. check if your git client is configured with an email to sign commits git config --list | grep email
  2. If not, set it up using git config --global user.email [email protected]
  3. Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails

@coryjmaklin
Copy link
Author

Typo in PR description: necessarily -> necessary

Updated

@coryjmaklin coryjmaklin marked this pull request as ready for review June 22, 2021 18:41
@rimolive
Copy link

Any chance to help in this PR to have it merged asap?

throw new RuntimeException("Authentication error");
}
}
return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should return an empty set

Suggested change
return null;
return ImmutableSet.of();

@@ -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)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use static imports from LdapUtil.

@@ -106,11 +111,44 @@ public boolean isGroupMember(String searchBase, String groupSearch, String conte
}
}

@Override
public Set<String> lookupUserGroups(String searchBase, String searchFilter, String contextUserDistinguishedName, String contextPassword)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Would it be possible to avoid two LDAP queries while logging in and getting user groups in a separate call? Logging in is simply checking if a context can be created, and a similar context is later used for getting groups.

binder -> {
configBinder(binder).bindConfig(LdapConfig.class);
binder.bind(LdapGroupProvider.class).in(Scopes.SINGLETON);
binder.bind(LdapClient.class).to(JdkLdapClient.class).in(Scopes.SINGLETON);
Copy link
Member

@MiguelWeezardo MiguelWeezardo Aug 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this fail at runtime if LdapClient interface was already bound in LdapAuthenticatorFactory? Maybe we need some @ForAuthentication and @ForGroupProvider annotations?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is in separate guice context, so I think it is not an issue.

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

public class TestLdapClient
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mock LDAP implementation is great for unit testing, but it would be nice to have tests with an actual LDAP server. I don't think you would have detected the "memberof"/"memberOf" issue using only these tests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice that we have SinglenodeLdap or SinglenodeLdapBindDn environments already created.

Basically to create product tests, you need to add a test io.trino.tests.product.jdbc.TestLdapTrinoJdbc and add to the product test environment configuration for LDAP group provider.

@Override
public String getName()
{
return "ldap";
Copy link
Member

@MiguelWeezardo MiguelWeezardo Aug 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starburst already has a group provider called "ldap", and this will cause conflicts while parsing configuration, since their config properties are different. Maybe use "ldap-group-provider", "ldap-query" or "ldap-trino"?

@@ -17,6 +17,7 @@
import io.trino.plugin.password.file.FileAuthenticatorFactory;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please improve commit message:

Introduce LDAP group provider

@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please follow: https://github.com/trinodb/trino/blob/master/DEVELOPMENT.md#git-merge-strategy

Mechanical changes (like refactoring and renaming) should be separated from logical and functional changes.

Please extract class rename into a separate commit.

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.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should fail instead of silenlty ignore this problem. It will some time to users to understand why ldap group provider does not return groups. Throwing an exception here could make the realize the problem sooner.

@@ -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)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extraction of LdapUtil can go as separate commit too.

@Override
public Set<String> getGroups(String user)
{
if (LdapUtil.containsSpecialCharacters(user)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static import

String userDistinguishedName = LdapUtil.replaceUser(userBindSearchPattern, user);
String searchBase = userBaseDistinguishedName.orElseThrow();
try {
return client.lookupUserGroups(searchBase, userDistinguishedName, bindDistinguishedName.orElseThrow(), bindPassword.orElseThrow());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are breaking a loop in first iteration and so we are ignoring rest of userBindSearchPatterns. Is this intentional? If so, why? Can you please add a comemnt?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminder to keep #8134 in mind. All patterns must be tried and only the last AccessDeniedException must be bubbled up.

binder -> {
configBinder(binder).bindConfig(LdapConfig.class);
binder.bind(LdapGroupProvider.class).in(Scopes.SINGLETON);
binder.bind(LdapClient.class).to(JdkLdapClient.class).in(Scopes.SINGLETON);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is in separate guice context, so I think it is not an issue.


import com.google.common.base.CharMatcher;

public class LdapUtil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final

private static final CharMatcher SPECIAL_CHARACTERS = CharMatcher.anyOf(",=+<>#;*()\"\\\u0000");
private static final CharMatcher WHITESPACE = CharMatcher.anyOf(" \r");

private LdapUtil()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private LdapUtil() {}

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

public class TestLdapClient
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice that we have SinglenodeLdap or SinglenodeLdapBindDn environments already created.

Basically to create product tests, you need to add a test io.trino.tests.product.jdbc.TestLdapTrinoJdbc and add to the product test environment configuration for LDAP group provider.

@MiguelWeezardo
Copy link
Member

@coryjmaklin how can we help to get this merged?

@kokosing
Copy link
Member

@coryjmaklin Would you mind if we would take over this pull request?

@coryjmaklin
Copy link
Author

@coryjmaklin Would you mind if we would take over this pull request?

Sorry. I don't have time to work on it at present. If you want to take it over, by all means.

@rimolive
Copy link

@MiguelWeezardo @kokosing Let me know how can I help in this. I'm very interested to see this merged.

@kokosing
Copy link
Member

@rimolive Thank you ❤️ Please open a new pull request based on this one with all the comments addressed (applied or responded). Please make sure to mark @coryjmaklin as a co-author of the commit that you are goin to create. Together with @MiguelWeezardo we will be happy to do a review and merge the code eventually.

@rimolive
Copy link

Anyone knows why am I getting this error in trino-main tests?

[ERROR] Tests run: 7872, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 969.778 s <<< FAILURE! - in TestSuite [ERROR] io.trino.util.TestTimeZoneUtils.testNamedZones Time elapsed: 0.037 s <<< FAILURE! java.lang.IllegalArgumentException: The datetime zone id 'Pacific/Kanton' is not recognised at org.joda.time.DateTimeZone.forID(DateTimeZone.java:247) at io.trino.util.TestTimeZoneUtils.testNamedZones(TestTimeZoneUtils.java:51) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:104) at org.testng.internal.Invoker.invokeMethod(Invoker.java:645) at org.testng.internal.Invoker.invokeTestMethod(Invoker.java:851) at org.testng.internal.Invoker.invokeTestMethods(Invoker.java:1177) at org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:129) at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:112) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) at java.base/java.lang.Thread.run(Thread.java:829)

I'm using OpenJDK 11.0.13 to run maven build

@MiguelWeezardo
Copy link
Member

Anyone knows why am I getting this error in trino-main tests?

[ERROR] Tests run: 7872, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 969.778 s <<< FAILURE! - in TestSuite [ERROR] io.trino.util.TestTimeZoneUtils.testNamedZones Time elapsed: 0.037 s <<< FAILURE! java.lang.IllegalArgumentException: The datetime zone id 'Pacific/Kanton' is not recognised at org.joda.time.DateTimeZone.forID(DateTimeZone.java:247) at io.trino.util.TestTimeZoneUtils.testNamedZones(TestTimeZoneUtils.java:51) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:104) at org.testng.internal.Invoker.invokeMethod(Invoker.java:645) at org.testng.internal.Invoker.invokeTestMethod(Invoker.java:851) at org.testng.internal.Invoker.invokeTestMethods(Invoker.java:1177) at org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:129) at org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:112) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) at java.base/java.lang.Thread.run(Thread.java:829)

I'm using OpenJDK 11.0.13 to run maven build

Looks like your JDK knows of a zone which isn't yet supported by joda-time version Trino uses.
I see a couple of possible solutions:

  • upgrade joda-time
  • downgrade to OpenJDK 11.0.11
  • add an exception to TestTimeZoneUtils.testNamedZones

@rimolive
Copy link

@kokosing @MiguelWeezardo Please check updated PR: #10116

@kokosing
Copy link
Member

kokosing commented Nov 30, 2021

Closing in favor #10116. Thank you, @rimolive !

@kokosing kokosing closed this Nov 30, 2021
@eformat eformat mentioned this pull request May 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

8 participants