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

Basic auth extension #4823

Closed
wants to merge 8 commits into from
Closed

Basic auth extension #4823

wants to merge 8 commits into from

Conversation

jon-wei
Copy link
Contributor

@jon-wei jon-wei commented Sep 19, 2017

This PR adds an extension that provides implementations of the auth interfaces introduced in #4271 :

@fjy
Copy link
Contributor

fjy commented Sep 19, 2017

👍

@gianm
Copy link
Contributor

gianm commented Oct 2, 2017

@jon-wei please merge from master if you haven't recently; I hope that will fix the VM crash we got during CI.

@gianm gianm added the Security label Oct 2, 2017
@jon-wei jon-wei changed the title Basic auth extension [WIP] Basic auth extension Oct 10, 2017
@jon-wei
Copy link
Contributor Author

jon-wei commented Oct 10, 2017

Making some modifications to this re: password initialization, marking WIP

@jon-wei jon-wei added the WIP label Oct 10, 2017
@jon-wei jon-wei force-pushed the basic_sec branch 2 times, most recently from d0c7577 to e9ba4ff Compare October 10, 2017 01:05
@jon-wei jon-wei changed the title [WIP] Basic auth extension Basic auth extension Oct 20, 2017
Copy link
Contributor

@jihoonson jihoonson left a comment

Choose a reason for hiding this comment

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

Reviewed up to SQLBasicSecurityStorageConnector.

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
Copy link
Contributor

@jihoonson jihoonson Oct 20, 2017

Choose a reason for hiding this comment

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

mysql-metadata-storage also has this dependency of the same version. Did you intentionally specify these versions separately? Similar comment on the dependency on postgresql below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm, moved the mysql/postgres versions to the main druid pom.xml, changed these to reference those versions

layout: doc_page
---

# Druid-Basic-Security
Copy link
Contributor

Choose a reason for hiding this comment

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

Please removes the hyphens.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed hyphens

import java.util.Map;
import java.util.concurrent.Callable;

public abstract class SQLBasicSecurityStorageConnector implements BasicSecurityStorageConnector
Copy link
Contributor

@jihoonson jihoonson Oct 20, 2017

Choose a reason for hiding this comment

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

Please add some java doc and unit tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added javadoc and tests

return dataSource;
}

protected boolean connectorIsTransientException(Throwable e)
Copy link
Contributor

Choose a reason for hiding this comment

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

This always returns false. Is this for any user-defined subclasses?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I copied this from SQLMetadataConnector initially, I think that was the intent, this is now in the common BaseSQLMetadataConnector class

*
* @return String representing the SQL type
*/
protected String getPayloadType()
Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove unused method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed

@JsonTypeName("basic")
public class BasicRoleBasedAuthorizer implements Authorizer
{
private static final Logger log = new Logger(BasicRoleBasedAuthorizer.class);
Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove unused variable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed

)
{
if (authenticationResult == null) {
return new Access(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

Any error message like 'null authenticationResult'?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed this to throw an IAE, the authenticationResult should never be null here and indicates a bug if it is

authenticationResult.getIdentity()
);
if (authorizationName == null) {
return new Access(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar comment. 'Cannot find an authorization name for authenticationResult.getIdentity()'?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I decided to remove the authentication name -> authorization name features, felt like the benefit (possibly avoiding cases where two user accounts are essentially the same with a different name) wasn't worth the added complexity, so this block is gone now

}
}

return new Access(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar comment. Maybe 'permission denied'?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hm, I think that's a bit redundant in this case, since Access(false) already directly expresses an authorization denial

String permissionResourceName = permissionResource.getName();
Pattern resourceNamePattern = Pattern.compile(permissionResourceName);
Matcher resourceNameMatcher = resourceNamePattern.matcher(resource.getName());
return resourceNameMatcher.matches();
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, do you have any performance benchmark result? I'm not sure how this is fast enough.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not yet, I'll run some shortly

@jon-wei jon-wei removed the WIP label Oct 20, 2017
ImmutableList.of(
StringUtils.format(
"CREATE TABLE %1$s (\n"
+ " user_name INTEGER NOT NULL, \n"
Copy link
Contributor

Choose a reason for hiding this comment

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

VARCHAR(255)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed, thanks

);
}
catch (IOException ioe) {
return null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe better to return a map containing the exception?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Made this propagate the exception since it should never happen under any normal conditions

public List<Map<String, Object>> inTransaction(Handle handle, TransactionStatus transactionStatus)
throws Exception
{
List<Map<String, Object>> role_permissions = handle
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be the camel case or simply return without defining a variable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed this and similar places to return without defining a variable

public List<Map<String, Object>> inTransaction(Handle handle, TransactionStatus transactionStatus)
throws Exception
{
List<Map<String, Object>> user_permissions = handle
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be the camel case or simply return without defining a variable.

if (existingMapping == null) {
handle.createStatement(
StringUtils.format(
"INSERT INTO %1$s (authentication_name, authorization_name) VALUES (:authenticationName, :authorizationName)",
Copy link
Contributor

Choose a reason for hiding this comment

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

Please break the line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is method is deleted now since I removed the name mapping table

import java.util.List;
import java.util.Map;

public interface BasicSecurityStorageConnector
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about making all non get* methods to return the result indicating success or failure? For example, delete* methods return the number of deleted rows. It may be useful for operators to reduce mistakes.

}
}

public <T> T retryWithHandle(
Copy link
Contributor

Choose a reason for hiding this comment

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

Or maybe it's better to create a new super class which both SQLMetadataConnector and this class extend.


import java.net.InetAddress;

public class DerbyAuthorizationStorage
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks not being used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed unused class, the Derby implementation now uses MetadataStorage

log.info("Derby connector instantiated with metadata storage [%s].", this.storage.getClass().getName());
}

public DerbySQLBasicSecurityStorageConnector(
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks not being used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's not instantiated directly, but the class is bound in BasicSecurityDruidModule:

    PolyBind.optionBinder(binder, Key.get(BasicSecurityStorageConnector.class))
            .addBinding("derby")
            .to(DerbySQLBasicSecurityStorageConnector.class)
            .in(ManageLifecycle.class);

    PolyBind.optionBinder(binder, Key.get(SQLBasicSecurityStorageConnector.class))
            .addBinding("derby")
            .to(DerbySQLBasicSecurityStorageConnector.class)
            .in(ManageLifecycle.class);

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah ok.

@Override
public boolean tableExists(Handle handle, String tableName)
{
// ensure database defaults to utf8, otherwise bail
Copy link
Contributor

Choose a reason for hiding this comment

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

bail -> fail?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

"bail" is used here in the sense of "abandon what it's doing"

Copy link
Contributor

@jihoonson jihoonson left a comment

Choose a reason for hiding this comment

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

The latest patch looks good to me. I have one question.

);
}

private int getUserCountInTransaction(Handle handle, String userName)
Copy link
Contributor

Choose a reason for hiding this comment

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

What does the "InTransaction" suffix mean in this and below methods?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's referring to how they're called from within the DB transactions, for example:

    getDBI().inTransaction(
        new TransactionCallback<Void>()
        {
          @Override
          public Void inTransaction(Handle handle, TransactionStatus transactionStatus) throws Exception
          {

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for the explanation. I was confused with its name. It sounds to me like getUserCount() is executed in a transaction. I think just getUserCount() is fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Cool, I changed the names to remove "inTransaction"

@jon-wei
Copy link
Contributor Author

jon-wei commented Oct 26, 2017

@jihoonson I added a RegexMatchBenchmark for benchmarking regex pattern compilation and matching, these are the results I'm seeing on my macbook:

# Run complete. Total time: 00:06:22

Benchmark                                                   (numPatterns)  Mode  Cnt       Score      Error  Units
RegexMatchBenchmark.compileGranularityPathRegex                    100000  avgt   25  176488.968 ± 2731.548  us/op
RegexMatchBenchmark.compileGranularityPathRegexAndMatch            100000  avgt   25  196734.506 ± 6023.698  us/op
RegexMatchBenchmark.compileUUIDRegex                               100000  avgt   25   62557.928 ± 1232.438  us/op
RegexMatchBenchmark.compileUUIDRegexAndMatch                       100000  avgt   25   98763.608 ± 2052.479  us/op
RegexMatchBenchmark.compileUUIDsAsRegex                            100000  avgt   25   74987.953 ± 1011.895  us/op
RegexMatchBenchmark.compileUUIDsAsRegexAndMatchRandomUUID          100000  avgt   25   79894.899 ± 1000.825  us/op
RegexMatchBenchmark.deserializeGranularityPathRegex                100000  avgt   25  233961.684 ± 2421.426  us/op
RegexMatchBenchmark.deserializeUUIDRegex                           100000  avgt   25  107444.061 ± 1850.141  us/op
RegexMatchBenchmark.precompileGranularityPathRegexAndMatch         100000  avgt   25   19848.358 ±  312.136  us/op
RegexMatchBenchmark.precompileUUIDRegexAndMatch                    100000  avgt   25   29987.644 ±  407.776  us/op

Roughly, with the test regexes used in that benchmark (one for UUID strings, and a more complex one taken from Granularity), compile + match takes ~2us or lower.

Deserializing a byte[] representation of a Pattern using DefaultObjectMapper is also slower than recompiling the pattern from the regex string.

@jihoonson
Copy link
Contributor

@jon-wei thanks for sharing the benchmark result. I was concerned with the performance because BasicRoleBasedAuthorizer.permissionCheck() always compile a new pattern for each permissionResourceName. This method may become slow as the number of resourceAction increases. Do you have any better idea? Maybe caching compiled patterns?

@jon-wei
Copy link
Contributor Author

jon-wei commented Oct 26, 2017

@jihoonson I added a cache for the compiled regex patterns

@jihoonson
Copy link
Contributor

@jon-wei thanks for the quick fix!

# Druid Basic Security

This extension adds:
- an Authenticator which supports [HTTP Basic authentication](https://en.wikipedia.org/wiki/Basic_access_authentication)
Copy link
Contributor

Choose a reason for hiding this comment

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

Link to the docs for Authenticator and Authorizer (druid's auth.md) -- otherwise, if a user comes in to this page first, they will be lost.

### Properties
|Property|Description|Default|required|
|--------|-----------|-------|--------|
|`druid.auth.basic.initialAdminPassword`|Password to assign when Druid automatically creates the default admin account. See [Default user accounts](#default-user-accounts) for more information.|"druid"|No|
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't related to the authenticator or authorizer? That seems strange to me.


The configuration examples in the rest of this document will use "MyBasicAuthenticator" as the name of the authenticator being configured.

Only one instance of a "basic" type authenticator should be created and used, multiple "basic" authenticator instances are not supported.
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if you configure more than one?


The Basic authorizer has no additional configuration properties at this time.

Only one instance of a "basic" type authorizer should be created and used, multiple "basic" authorizer instances are not supported.
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if you configure more than one?


Cluster administrators should change the default passwords for these accounts before exposing a cluster to users.

## Defining permissions
Copy link
Contributor

Choose a reason for hiding this comment

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

This section of the doc looks like it applies to all authorizers and should be part of Druid's auth.md.


String permissionResourceName = permissionResource.getName();

Pattern resourceNamePattern = permissionPatternCache.get(permissionResourceName);
Copy link
Contributor

Choose a reason for hiding this comment

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

The permissionPatternCache won't be needed if all permissions details are cached -- since the permission objects would be long-lived and could contain their own compiled Patterns.

return null;
}

if (dbConnector.checkCredentials(dbConfig.getDbPrefix(), user, password.toCharArray())) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment as in the Authorizer: we need to avoid hitting the DB here.

String user = splits[0];
char[] password = splits[1].toCharArray();

if (dbConnector.checkCredentials(dbConfig.getDbPrefix(), user, password)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar -- need to avoid hitting the DB.

createUserTable(dbConfig.getDbPrefix());
createUserCredentialsTable(dbConfig.getDbPrefix());

makeDefaultSuperuser(
Copy link
Contributor

Choose a reason for hiding this comment

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

IMO, this should be optional -- superuser may use a different authenticator.

dbConfig.getInitialAdminPassword()
);

makeDefaultSuperuser(
Copy link
Contributor

Choose a reason for hiding this comment

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

IMO, this should be optional -- internal may use a different authenticator.

@jon-wei jon-wei mentioned this pull request Nov 17, 2017
@jon-wei
Copy link
Contributor Author

jon-wei commented Nov 17, 2017

closing this in favor of #5099

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.

4 participants