Skip to content

Support LDAP authentication/authorization#6972

Merged
jon-wei merged 33 commits intoapache:masterfrom
mohammadjkhan:PR6416
Oct 9, 2019
Merged

Support LDAP authentication/authorization#6972
jon-wei merged 33 commits intoapache:masterfrom
mohammadjkhan:PR6416

Conversation

@mohammadjkhan
Copy link
Copy Markdown
Contributor

@mohammadjkhan mohammadjkhan commented Jan 31, 2019

Proposal for LDAP authentication/authorization within Druid

Issues/limitations with the existing Druid Basic Security extension:

  1. Inability to authenticate requests using basic authentication with LDAP as the credentials store. Basic Security extension limits clients to use the internal database as the only credentials store option.
  2. Basic Security extension does more than just providing the mechanism to transport/process credentials supplied in http requests
  3. Basic Security extension authorization limits clients to use internal database as the only source to manage and lookup user roles. Inability to retrieve user role information from an external source like LDAP. Inability to map user groups, retrieved from LDAP for example, to roles.

Goals:

  1. Expose the ability to authenticate HTTP requests with basic authentication using LDAP as the credentials store to validate against.
  2. Refactor the existing basic security extension authentication and authorization implementation to be a bit more pluggable (database vs ldap or something else, with database being the default)
  3. Expose a LDAP role-based authorizer that allows druid users to be authorized by enumerating user group/s fetched in LDAP, and group/s to role mappings configured in the internal database

Proposal:

  1. Refactor the existing basic security extension authenticator to make it a bit more pluggable/configurable as to how to validate requests with basic authentication credentials. Abstract out a CredentialsValidator interface for use by BasicHTTPAuthenticator that will be used to select and configure the credential store/s to use (database, ldap, etc).
  2. CredentialsValidator interface will expose a validate method that takes a username and password and give you back whether a user is valid (return an AuthenticationResult).
  3. Support multiple credential validators. Authenticate a local user to the internal database and a ldap user at the same time, it’s common to have a local user in the database that’s used as the system user within the cluster, and ldap user for external access.
  4. Refactor basis security extension authorizer (BasicRoleBasedAuthroizer) and make it configurable for multiple sources of truth for assigning roles to users/groups
  5. Provide the ability to assign and lookup roles to groups instead of just users in the database. Check local database first for user permissions. If not, then check ldap and enumerate all groups and then follow set of rules dynamically configured in database for how to map those set of groups on to set of roles
  6. Groups to role mappings in the database will be stored similar to the current structure of how user to role mappings are stored.

Fixes #6416

@gianm
Copy link
Copy Markdown
Contributor

gianm commented Jan 31, 2019

Hi @mohammadjkhan - thanks for the contribution!!

We are in the midst of discussing a template for proposals, that we haven't settled on yet but it seems that we will be able to soon. Would you mind writing one up to help reviewers understand what your patch is doing?

I don't know what the final template will be, so feel free to make up your own template for now. Or if you want a suggested template, try this one:

  1. Motivation: Describe the problem and why you want to solve it.
  2. Proposed changes: Describe your solution to the problem. This may be fairly extensive or it may just be a sentence or two, depending on the scope of the change. This should include any changes that are proposed to user-facing interfaces (configuration parameters, JSON query/ingest specs, SQL language, emitted metrics, and so on).
  3. Rationale: Discuss why your proposed solution is the best one. The goal of this section is to help people understand why this is the best solution now, and also to prevent churn in the future when old alternatives are reconsidered. This should include any alternative solutions that you considered and decided against.
  4. Operational impact: Is anything going to be deprecated or removed by this change? Is there a migration path that cluster operators need to be aware of? Will there be any effect on the ability to do a rolling upgrade, or to do a rolling downgrade if an operator wants to switch back to a previous version?
  5. Future work: A discussion of things that you believe are out of scope for the particular proposal but would be nice follow-ups. It helps show where a particular change could be leading us. There isn't any commitment that you will actually work on this stuff, and it's okay if this section is empty.

@gianm
Copy link
Copy Markdown
Contributor

gianm commented Jan 31, 2019

Oops, nevermind, you did it already!!!

I just saw it in: #6416. Thanks, I'll go check that out. In the meantime, it looks like the integration tests are having trouble passing if you would like to take a look at that.

@mohammadjkhan
Copy link
Copy Markdown
Contributor Author

Thanks @gianm. I'm taking a look at failing integration tests right now. I see them falling for me with the same errors/issue even for the master branch that doesn't have my changes. I'm following steps described in integration-tests and executing mvn verify -P integration-tests under the integration-tests module (with docker installed on my machine).

@nishantmonu51
Copy link
Copy Markdown
Member

nishantmonu51 commented Feb 6, 2019

A very useful feature, Thanks for the contribution @mohammadjkhan
Can you add a description to the PR and also link to the proposal there.

@mohammadjkhan
Copy link
Copy Markdown
Contributor Author

I'm also looking into the Travis CI build errors related to the druid-security module

@mohammadjkhan
Copy link
Copy Markdown
Contributor Author

I unknowingly did pull -rebase on branch 6972 from upstream master while my PR was open, that resulted in my PR tracking other/unrelated changes from upstream master. So I had to reset and force push to clean it up.

@mohammadjkhan
Copy link
Copy Markdown
Contributor Author

Hi @jon-wei
I'm following up to see if you had the chance to look and review the changes in this PR yet? It's currently assigned to you for review.

Thanks,
Mohammad

@jon-wei
Copy link
Copy Markdown
Contributor

jon-wei commented Mar 20, 2019

@mohammadjkhan Sorry for the delay, I will start reviewing this week.

@jon-wei
Copy link
Copy Markdown
Contributor

jon-wei commented Mar 27, 2019

I am still reviewing and testing out this patch, I will have more comments on it this week.

{
cacheNotifier = new CommonCacheNotifier(
userCacheNotifier = new CommonCacheNotifier(
initAuthenticatorConfigMap(authenticatorMapper),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should this be initAuthenticatorUserMap instead of configMap ?

Copy link
Copy Markdown
Contributor Author

@mohammadjkhan mohammadjkhan Mar 28, 2019

Choose a reason for hiding this comment

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

No, initAuthenticatorConfigMap has nothing to do with user map per say. initAuthenticatorConfigMap is unchanged and just returns the map of authenticator prefix and authenticator configuration (BasicAuthDBConfig) that the CommonCacheNotifier (userCacheNotifier) needs too reference configuration properties like isEnableCacheNotifications

Copy link
Copy Markdown
Member

@nishantmonu51 nishantmonu51 left a comment

Choose a reason for hiding this comment

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

did an initial pass through the code, left few comments, Havn't tested the feature myself yet.

few more general comments, not a blocker for this PR and can also be done in subsequent PRs -

  1. Would be nice to add a tutorial on how to get this feature working would be helpful for new users.
  2. Druid has integration-tests that run on docker containers, would like to see some basic test for ldap security added as well, so as to ensure this feature does not break with subsequent changes.

private final Set<BasicAuthorizerRole> roles;

@JsonCreator
public BasicAuthorizerGroupMappingFull(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this class looks almost duplicate to BasicAuthorizerGroupMapping,
Can we remove duplicates ? Or are there any differences ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I modeled BasicAuthorizerGroupMapping and BasicAuthorizerGroupMappingFull entities similar to already existing BasicAuthorizerUser and BasicAuthorizerUserFull entities.
BasicAuthorizerGroupMapping contains a set of role names whereas BasicAuthorizerGroupMappingFull contains a set of role objects, which contains the role name and list of permission objects associated with the role name.
BasicAuthorizerResource getUser and getGroupMapping rest endpoints allows for query parameter "full" that's used to decide whether to just return the role names or the "full" role object in the response.
Please let me know what you think.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

got it.

@jon-wei
Copy link
Copy Markdown
Contributor

jon-wei commented Mar 28, 2019

@mohammadjkhan @nishantmonu51

Given that the metadata store-backed and LDAP implementations have such different configurations, I would structure this as follows:

  • Create an abstract PasswordBasedAuthenticator, and override the credentials validation in separate BasicHTTPAuthenticator and LDAPAuthenticator classes
  • Create an abstract RoleBasedAuthorizer, and override the rolemap calculation in separate classes for metadata-backed and LDAP-backed authorizer (The PR currently preferentially checks the metadata-backed store and then LDAP, I think these should be separate since the incoming request would be authenticated against only one or the other)

Comment thread docs/content/development/extensions-core/druid-basic-security.md Outdated
@jon-wei
Copy link
Copy Markdown
Contributor

jon-wei commented Apr 5, 2019

@mohammadjkhan

Regarding my earlier comment (#6972 (comment)), after more thought I now feel it would be better to split LDAP into a separate contrib extension and leave the existing basic auth extension unchanged.

  • The authenticator only shares the minimal "check password" logic
  • The authorizer is currently written to shared the role definitions with the basic auth extension, but I think it would be better to have LDAP be the sole source of truth for users/groups/roles/permissions for the LDAP implementation, instead of a mixed model where some information is kept in the Druid metadata store.
  • I think it's possible that the LDAP implementations will evolve over time with more features (maybe some are very specific to LDAP), and separating it into its own extension will give us more freedom to build upon it without affecting the basic auth extension.

@mohammadjkhan
Copy link
Copy Markdown
Contributor Author

@jon-wei

  • LDAP is supposed to be source of truth for users/groups and not roles/permissions. The standard way to implement LDAP is to manage roles and permissions at the application/framework level as rules for managing roles/permissions can (and most likely will) be different from one application to another within an enterprise, and so they aren't managed within LDAP. Also, they are not 1:1. So it's normal/common to have a mix model of where some information is kept/managed at the application level and some information is stored in LDAP

  • The basic authenticator shares logic related to how basic auth credentials are retrieved from the http request; how those credentials are authenticated can be different, and the CredentialsValidator interface now provides a way support for more ways of validating basic auth credentials down the road. As described in our proposal the current basic security extension limits clients to use druid internal metadata store for authentication, and one of the goals/proposals is to refactor basic security extension authenticator to make it a bit more pluggable/configurable as to how to validate requests with basic authentication credentials. Moving in to separate contrib extension will result in duplication of logic related to how basic auth credentials are processed from the http request.

  • How druid authorizes a user in a basic auth credential request can be managed the same way regardless of how they got authenticated (db, ldap, etc). Basically, they differ in authentication methods, but not authorization. Also, it provides a way to reuse roles/permissions. For instance, an admin user and a user belonging to an admin group can be mapped to same role in the metastore. So, it makes sense to share the role definitions

  • If LDAP implementations evolve over time, the changes will be only limited to LDAPCredentialsValidator in how we interact with ldap and authenticate the user. How we retrieve/process basic auth credentails from the http request and authorize the user should stay the same. I think the druid basic auth security module as a whole will be evolved over time to support more ways of authenticating HTTP Basic Authentication requests, not just against db or ldap. How Spring security has achieved that is a really good example.

  • Some of the design decisions were adopted to preserve backward compatibility and not introduce any breaking changes. Leaving the BasicHTTPAuthenticator mostly as is to continue leveraging and make use of the "basic" json type name and introduced CredentialsValidator as a way authenticate a basic auth credential request either against db or ldap, default is db

this.enableCacheNotifications = enableCacheNotifications;
this.cacheNotificationTimeout = cacheNotificationTimeout;
this.iterations = iterations;
this.iterations = credentialIterations;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: Suggest renaming iterations and its getter as well here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, renamed

this.itemConfig = null;
}

public CommonCacheNotifier(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This constructor is unused

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, removed

}

private List<ListenableFuture<StatusResponseHolder>> sendUpdate(String updatedAuthorizerPrefix, byte[] serializedUserMap)
public void addUpdate(byte[] updatedItemData)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This addUpdate is unused

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, removed

BasicAuthDBConfig authorizerConfig = itemConfigMap.get(update.lhs);
if (!authorizerConfig.isEnableCacheNotifications()) {

BasicAuthDBConfig dbConfig = itemConfigMap == null ? itemConfig : itemConfigMap.get(update.lhs);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This line should be reverted back to BasicAuthDBConfig authorizerConfig = itemConfigMap.get(update.lhs); since the constructor that sets itemConfig is not used anymore

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, reverted

serializedMap
);
List<ListenableFuture<StatusResponseHolder>> futures;
if (authorizer != null) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The authorizer will always be non-null here, the addUpdate method that would have allowed it to be null is not used

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, updated

}

private URL getListenerURL(DruidNode druidNode, String baseUrl, String itemName)
private List<ListenableFuture<StatusResponseHolder>> sendUpdate(byte[] serializedEntity)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This sendUpdate method should be removed as a result of deleting the unused codepaths in this class

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, removed

import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

public class BasicAuthenticatorUserPrincipal implements Principal
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggest LdapUserPrincipal classname instead since only the LDAP validator uses this

Copy link
Copy Markdown
Contributor Author

@mohammadjkhan mohammadjkhan Oct 7, 2019

Choose a reason for hiding this comment

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

Done, renamed

{
long now = System.currentTimeMillis();
long cutoff = now - (duration * 1000L);
if (this.lastVerified.get().toEpochMilli() < cutoff) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

From the docs, the maxDuration expiring should override the result of any last verified checks, the maxCutoff check should always happen and only if it passes should last verified time be checked

Copy link
Copy Markdown
Contributor Author

@mohammadjkhan mohammadjkhan Oct 7, 2019

Choose a reason for hiding this comment

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

Done, fixed. Changes are in the new renamed LdapUserPrincipal class


@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", defaultImpl = DBCredentialsValidator.class)
@JsonSubTypes(value = {
@JsonSubTypes.Type(name = "db", value = DBCredentialsValidator.class),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggest renaming the type here to metadata and the credentials validator to MetadataStoreCredentialsValidator

Copy link
Copy Markdown
Contributor Author

@mohammadjkhan mohammadjkhan Oct 7, 2019

Choose a reason for hiding this comment

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

Done, renamed

{
Set<LdapName> groups;
LdapName userDn;
Map<String, Object> contexMap = new HashMap<>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

contexMap -> contextMap

Copy link
Copy Markdown
Contributor Author

@mohammadjkhan mohammadjkhan Oct 7, 2019

Choose a reason for hiding this comment

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

Done, renamed

return null;
}
userDn = new LdapName(userResult.getNameInNamespace());
groups = getGroupsFromLdap(this.ldapConfig, userResult);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think the group lookup should be moved to the LDAPRoleProvider, since the groups aren't needed to validate the user/password

Copy link
Copy Markdown
Contributor Author

@mohammadjkhan mohammadjkhan Oct 7, 2019

Choose a reason for hiding this comment

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

Done. This change required a bit more work/effort than I had expected but I agree that moving it to the LDAPRoleProvider makes more sense. Now there's no group related properties or operations within LDAPCredentialsValidator
With this change I also had to move the groupFilters property to ldap authorizer
druid.auth.authorizer.MyBasicLDAPAuthorizer.roleProvider.groupFilters

import java.util.Set;

@JsonTypeName("db")
public class DBRoleProvider implements RoleProvider
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggest renaming to MetadataStoreRoleProvider

Copy link
Copy Markdown
Contributor Author

@mohammadjkhan mohammadjkhan Oct 7, 2019

Choose a reason for hiding this comment

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

Done. renamed

@jon-wei
Copy link
Copy Markdown
Contributor

jon-wei commented Sep 11, 2019

@mohammadjkhan I'm done reviewing, can you fix conflicts and address comments? Thanks!

# Conflicts:
#	docs/development/extensions-core/druid-basic-security.md
@lgtm-com
Copy link
Copy Markdown

lgtm-com Bot commented Sep 12, 2019

This pull request introduces 1 alert when merging 3db3c05 into 75978e5 - view on LGTM.com

new alerts:

  • 1 for Information exposure through a stack trace

@jon-wei
Copy link
Copy Markdown
Contributor

jon-wei commented Sep 20, 2019

@mohammadjkhan Just checking in again, I think this is ready to merge after final round of comments are addressed. I'm really looking forward to Druid finally supporting LDAP!

@asdf2014
Copy link
Copy Markdown
Member

@mohammadjkhan Please help us fix this new LGTM alert to maintain the A+ quality. image

@mohammadjkhan
Copy link
Copy Markdown
Contributor Author

@jon-wei @asdf2014 Sounds great! Thank you! I'll update the PR with the final round of comments and the LGTM alert today and tomorrow and notify you guys. Thanks

@nishantmonu51
Copy link
Copy Markdown
Member

@mohammadjkhan : Any updates here ? were you able to look into final review comments ?

@lgtm-com
Copy link
Copy Markdown

lgtm-com Bot commented Oct 7, 2019

This pull request introduces 1 alert when merging 0bca543 into 1d42551 - view on LGTM.com

new alerts:

  • 1 for Information exposure through a stack trace

@jon-wei
Copy link
Copy Markdown
Contributor

jon-wei commented Oct 7, 2019

@mohammadjkhan CI is failing on spellcheck, can you add exclusions to https://github.com/apache/incubator-druid/blob/master/website/.spelling, and can you address the 1 new alert from LGTM?

@lgtm-com
Copy link
Copy Markdown

lgtm-com Bot commented Oct 7, 2019

This pull request introduces 1 alert when merging d4a051d into 1d42551 - view on LGTM.com

new alerts:

  • 1 for Information exposure through a stack trace

Copy link
Copy Markdown
Member

@nishantmonu51 nishantmonu51 left a comment

Choose a reason for hiding this comment

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

LGTM, 👍 after travis.

@mohammadjkhan
Copy link
Copy Markdown
Contributor Author

@jon-wei @asdf2014 @nishantmonu51 fixed spellcheck and LGTM alerts. All requested changes/updates are now complete. Thank you.

@jon-wei
Copy link
Copy Markdown
Contributor

jon-wei commented Oct 9, 2019

LGTM, thanks for the contribution!

@jon-wei jon-wei merged commit 18758f5 into apache:master Oct 9, 2019
@mohammadjkhan
Copy link
Copy Markdown
Contributor Author

mohammadjkhan commented Oct 9, 2019

Thanks @jon-wei, @nishantmonu51, and @gianm; I really appreciate all your guys help, support, and direction with introducing this feature, and I hope the Druid community makes the most of it and gets benefited from it!

surekhasaharan pushed a commit to implydata/druid-public that referenced this pull request Oct 24, 2019
* Support LDAP authentication/authorization

* fixed integration-tests

* fixed Travis CI build errors related to druid-security module

* fixed failing test

* fixed failing test header

* added comments, force build

* fixes for strict compilation spotbugs checks

* removed authenticator rolling credential update feature

* removed escalator rolling credential update feature

* fixed teamcity inspection deprecated API usage error

* fixed checkstyle execution error, removed unused import

* removed cached config as part of removing authenticator rolling credential update feature

* removed config bundle entity as part of removing authenticator rolling credential update feature

* refactored ldao configuration

* added support for SSLContext configuration and TLSCertificateChecker

* removed check to return authentication failure when user has no group assigned, will be checked and handled by the authorizer

* Separate out authorizer checks between metadata-backed store user and LDAP user/groups

* refactored BasicSecuritySSLSocketFactory usage to fix strict compilation spotbugs checks

* fixes build issue

* final review comments updates

* final review comments updates

* fixed LGTM and spellcheck alerts

* Fixed Avatica auth failure error message check

* Updated metadata credentials validator exception message string, replaced DB with metadata store
@jon-wei jon-wei added this to the 0.17.0 milestone Dec 17, 2019
@jon-wei jon-wei mentioned this pull request Dec 28, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LDAP authentication/authorization within Druid

6 participants