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 #5099

Merged
merged 8 commits into from
Dec 14, 2017
Merged

Basic auth extension #5099

merged 8 commits into from
Dec 14, 2017

Conversation

jon-wei
Copy link
Contributor

@jon-wei jon-wei commented Nov 17, 2017

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

  • an Authenticator which supports HTTP Basic authentication
  • an Escalator which supports HTTP Basic authentication
  • an Authorizer which implements basic role-based access control

With the large amount of changes (almost a rewrite) from the last round of PR reviews on #4823, I felt like it would be easier to close the old PR and start a fresh one.

Some design notes:

  • The user/role information is stored in the metadata store, using the "druid_config" table as a key-value store, user/role maps are stored as serialized byte arrays
  • On the coordinator a *MetadataStorageUpdater class is responsible for handling writes to the user/role DB and holds a deserialized cached copy of the latest DB state
  • Each node has a *CacheManager instance that polls the coordinator for the latest DB state periodically, this also syncs the auth DB state at startup time
  • The coordinator can also notify and push updates to remote nodes, using the *CacheNotifier classes
  • The BasicHTTPAuthenticator and BasicRoleBasedAuthorizer perform their checks using cached info from the *CacheManager instance, to avoid overloading the coordinator
  • The coordinator nodes have a jersey resource for configuring the auth DB, which also holds the endpoint that nodes poll to get the latest DB state
  • A jersey resource is also installed on non-coordinator nodes, containing an endpoint that listens for DB state updates

@jon-wei jon-wei mentioned this pull request Nov 17, 2017
@jon-wei jon-wei force-pushed the new_basic_sec2 branch 2 times, most recently from 754b0b3 to b3557b7 Compare November 17, 2017 02:34
@jihoonson
Copy link
Contributor

@jon-wei thanks for raising a new PR, but it looks that this single patch contains quite large changes. Is it possible to split into several smaller PRs? It would be much helpful for reviewers.

@jon-wei
Copy link
Contributor Author

jon-wei commented Nov 20, 2017

@jihoonson

I can split the PR into two if really needed, but the changes are pretty self contained in an extension with few changes to core druid. Within the extension the authentication and authorization portions are also pretty self-contained. I'd rather keep this as one PR if possible since the features are closely related and to avoid the churn/overhead of having two PRs to go through the review process.

@gianm
Copy link
Contributor

gianm commented Nov 28, 2017

I'm reviewing this now.

@jihoonson
Copy link
Contributor

@jon-wei ok. I'm also reviewing.

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.

@jon-wei thank you for the nice work! I did a first round of review. Also please let me know what do you think about the below.

  • There is the sync issue between the coordinator and other nodes. So, an API to check the load status will be much helpful. See the lookup document as an example.
  • I think credential data is usually not much big. What do you think about adding a coordinator API to sync over the whole cluster immediately?

|`druid.auth.authenticator.MyBasicAuthenticator.cacheNotificationTimeout`|The timeout in milliseconds for the cache notifications.|5000|No|
|`druid.auth.authenticator.MyBasicAuthenticator.authorizerName`|Authorizer that requests should be directed to|N/A|Yes|

enableCacheNotifications
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks unnecessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

deleted, thanks

|Property|Description|Default|required|
|--------|-----------|-------|--------|
|`druid.escalator.internalClientUsername`|The escalator will use this username for requests made as the internal systerm user.|n/a|Yes|
|`druid.escalator.internalClientPassword`|The escalator will use this password for requests made as the internal system user.|n/a|Yes|
Copy link
Contributor

Choose a reason for hiding this comment

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

How does this relate to druid.auth.authenticator.MyBasicAuthenticator.initialInternalClientPassword?

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 should be set to the same value, unless the user changes the password later on

#### Properties
|Property|Description|Default|required|
|--------|-----------|-------|--------|
|`druid.auth.authenticator.MyBasicAuthenticator.initialAdminPassword`|Initial password for the automatically created default admin user. If no password is specified, the default admin user will not be created.|null|No|
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the meaning of initial? Is it possible to change the password using other APIs?

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 the password is changed and then the cluster is restarted? The adminPassword is reset to the initial one?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

initial is the password these users will have when the user is first automatically created.

The password can be changed afterwards and won't be reset after cluster restart, I added a comment about that.


Please see [Authentication and Authorization](../../configuration/auth.html) for more information on the extension interfaces being implemented.

## Configuration
Copy link
Contributor

Choose a reason for hiding this comment

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

Should these configurations added to common.runtime.properties?

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 a note about common.runtime.properties here

|Property|Description|Default|required|
|--------|-----------|-------|--------|
|`druid.auth.authenticator.MyBasicAuthenticator.initialAdminPassword`|Initial password for the automatically created default admin user. If no password is specified, the default admin user will not be created.|null|No|
|`druid.auth.authenticator.MyBasicAuthenticator.initialInternalClientPassword`|Initial password for the default internal system user, used for internal node communication. If no password is specified, the default internal system user will not be created.|null|No|
Copy link
Contributor

Choose a reason for hiding this comment

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

Same with initialAdminPassword.

public class DefaultBasicAuthorizerResourceHandler implements BasicAuthorizerResourceHandler
{
private static final Logger log = new Logger(DefaultBasicAuthorizerResourceHandler.class);
private static final Response NOT_FOUND_RESPONSE = Response.status(Response.Status.NOT_FOUND).build();
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe FORBIDDEN

this.resourceNamePattern = Pattern.compile(resourceAction.getResource().getName());
}
catch (PatternSyntaxException pse) {
throw new BasicSecurityDBResourceException(
Copy link
Contributor

Choose a reason for hiding this comment

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

pse should be included.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Included exception


BasicAuthorizerPermission that = (BasicAuthorizerPermission) o;

if (getResourceAction() != null
Copy link
Contributor

Choose a reason for hiding this comment

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

You can use Objects.equals().

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 was autogenerated by IntelliJ

public int hashCode()
{
int result = getResourceAction() != null ? getResourceAction().hashCode() : 0;
result = 31 * result + (getResourceNamePattern().pattern() != null
Copy link
Contributor

Choose a reason for hiding this comment

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

You can use Objects.hash().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also autogenerated, they're not used outside of tests

connector = derbyConnectorRule.getConnector();
tablesConfig = derbyConnectorRule.metadataTablesConfigSupplier().get();
connector.createConfigTable();
//injector = setupInjector();
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 this 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.

removed line

@gianm
Copy link
Contributor

gianm commented Dec 6, 2017

Thanks @jon-wei for working on the extension and @jihoonson for reviewing.

Beyond what @jihoonson has said (especially an API to check the load status -- it's always helpful) I think we should cache the credential db on the local disk of each node, so when they start up, they can use their cache if the coordinator is not accessible. Maybe they should use their cache if they can't contact the coordinator within some amount of time. That way the coordinator being down doesn't torch the cluster completely.

@jon-wei jon-wei force-pushed the new_basic_sec2 branch 4 times, most recently from 1301600 to c937dc8 Compare December 9, 2017 00:08
@jon-wei
Copy link
Contributor Author

jon-wei commented Dec 9, 2017

@jihoonson @gianm

Thanks for the reviews, I addressed the comments and added the following features:

  • Disk snapshots of the caches, these will be used during initialization if a service cannot sync with the coordinator
  • A "refresh all" cache syncing API on the coordinator
  • loadStatus API (I also added "/db/" to some of the API paths to prevent path conflicts")

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.

@jon-wei thanks for the update. Left more comments.

return objectMapper.writeValueAsBytes(userMap);
}
catch (IOException ioe) {
throw new ISE("WTF? Couldn't serialize userMap!");
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add ioe to 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.

added

return objectMapper.writeValueAsBytes(userMap);
}
catch (IOException ioe) {
throw new ISE("WTF? Couldn't serialize userMap!");
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add ioe to 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.

added

return objectMapper.writeValueAsBytes(roleMap);
}
catch (IOException ioe) {
throw new ISE("WTF? Couldn't serialize roleMap!");
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add ioe to 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.

added

private final EmittingLogger logger;
private final String threadName;

private Thread notifierThread;
Copy link
Contributor

Choose a reason for hiding this comment

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

Thread should be stopped on stop(). Suggest to use ExecutorService for easy maintenance.

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 stop(), changed to use ExecutorService

synchronized (itemsToUpdate) {
itemsToUpdate.add(updatedItemName);
serializedMaps.put(updatedItemName, updatedItemData);
itemsToUpdate.notify();
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks to the notifier notifies each update every time. Maybe it's better to collect updates for a given time (like 100 ms) and send them in bulk to reduce API calls. I'm not sure about this because I guess credentials are not frequently updated.

If you want to keep the current behavior, we don't have to keep itemsToUpdate and serializedMaps because there will be only a single element in them. Instead, I think it's better to use AtomicReference<Pair<String, byte[]>> or simply put a Pair<String, byte[]> to the blockingQueue if you decide to use it.

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 think the current behavior is okay for now, since the updates should be pretty infrequent, I changed this to use Pair<String, byte[]> and a BlockingQueue

)
{
this.storageUpdater = storageUpdater;
this.objectMapper = new ObjectMapper(new SmileFactory());
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks that objectMapper can be injected using @Smile annotation.

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 to use @Smile annotation

}
}
catch (Exception e) {
LOG.makeAlert(e, "WTF? Could not deserialize user/role map received from coordinator.").emit();
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this intended to does nothing for all kinds of exceptions but emitting an alert?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, see the previous comment

}

@Nullable
private UserAndRoleMap loadUserAndRoleMapFromDisk(String prefix) 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.

Can be reduced to throws IOException.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reduced to IOException

);
}

private void writeMapToDisk(String prefix, byte[] userMapBytes) 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.

Can be reduced to IOException.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reduced to IOException

File cacheDir = new File(commonCacheConfig.getCacheDirectory());
cacheDir.mkdirs();
File userMapFile = new File(commonCacheConfig.getCacheDirectory(), getUserRoleMapFilename(prefix));
Files.write(userMapBytes, userMapFile);
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. What if the write fails?

private final Map<String, BasicAuthDBConfig> itemConfigMap;
private final String baseUrl;
private final EmittingLogger logger;
private final String threadName;
Copy link
Contributor

Choose a reason for hiding this comment

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

Unused variable.

HttpClient httpClient,
String baseUrl,
String threadName,
EmittingLogger logger
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah it's needed. But I'm still concerned with that it's difficult to find where the logging code is from the log if the logging class is different from the actual class. What do you think about passing a caller name in constructor and adding it to all logs?

return loadUserMapFromDisk(prefix);
}
catch (Exception e2) {
LOG.makeAlert(e2, "Encountered exception while loading user map snapshot for authenticator [%s]", prefix);
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add e to e2 like e2.addSuppressed(e).

return loadUserAndRoleMapFromDisk(prefix);
}
catch (Exception e2) {
LOG.makeAlert(e2, "Encountered exception while loading user-role map snapshot for authorizer [%s]", prefix);
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add e to e2.

@jihoonson
Copy link
Contributor

@jon-wei thanks for the quick update! +1 after CI.

@jon-wei
Copy link
Contributor Author

jon-wei commented Dec 14, 2017

@jihoonson thanks for the review!

@jon-wei jon-wei merged commit f48c9d7 into apache:master Dec 14, 2017
jon-wei added a commit to implydata/druid-public that referenced this pull request Dec 14, 2017
* Basic auth extension

* Add auth configuration integration test

* Fix missing authorizerName property

* PR comments

* Fix missing @JsonProperty annotation

* PR comments

* more PR comments
@jon-wei jon-wei added this to the 0.12.0 milestone Jan 5, 2018
{
private static final EmittingLogger LOG = new EmittingLogger(CommonCacheNotifier.class);

private static final List<String> NODE_TYPES = Arrays.asList(
Copy link
Member

Choose a reason for hiding this comment

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

@jon-wei does it purposely skip the Coordinator type?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@leventov Yes, the coordinator gets its information about the auth state directly from metadata storage, this update notification is for other service types

@zhouyuanchao
Copy link

Why basic security can't use EnvironmentVariablePasswordProvider, such as initialAdminPassword?

@jon-wei
Copy link
Contributor Author

jon-wei commented Oct 31, 2018

@zhouyuanchao It can, check the master branch

@zhouyuanchao
Copy link

@jon-wei internalClientPassword can not use EnvironmentVariablePasswordProvider in master branch

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.

5 participants