Extension points for authentication/authorization#4271
Conversation
|
@jon-wei I just read the proposal and it sounds great. I am not sure if I missed it but I do not find much information about the Basic HTTP Authentication mechanism that would be built-in, can you provide some details on that ? A side note that the RBAC system that we use with Druid was open sourced this week at www.athenz.io in case you are interested to have a look. |
|
havn't looked at the code changes yet, went through the proposal and It seems good,
|
|
like the proposal and I agree with @nishantmonu51 on not having i loved the idea of enforcing the check that the request passed through authorization and didn't silently succeed. |
|
Thanks for the feedback so far! @pjain1 I'll add more documentation for that part @nishantmonu51 I'll add some thoughts to the "Future Work" section on row/column level authorization. Regarding point 3, I think that's reasonable to support for cases where the external security store's model aligns closely enough with the core security model, that would allow such similar systems to reuse the coordinator API endpoints @himanshug Moving this into a core extension sounds good, I'll need to take another look at the MySQL/Postgres support and see if there any complications from that I'll think about some ways to get rid of useBuiltInTables(). |
Maybe we could do this through views. Like, create a view with a particular set of columns and a particular filter, and then grant access to that view but not to the base dataSource. |
|
@pjain1 I updated the proposal with some more details on the Basic HTTP auth implementation. @nishantmonu51 I added a section to Future Work regarding row/column-level authorization. I'm leaning towards supporting that through a "View" system. Regarding extensibility of the RBAC model, after thinking more on this, my current stance is that someone wishing to plug in their own RBAC system should implement their own AuthorizationManager vs. providing additional extension points within the built-in AuthorizationManager. The data model used by other systems would generally be more sophisticated than the basic RBAC model proposed here (for example, the Athenz system linked by @pjain1 has more concepts like domain namespaces and services). I don't think we could capture every data model variation if the extension point is at the User/Role/Permission level, and it would be cleaner/more useful to not impose such assumptions on an extension implementer. @himanshug I revised the proposal to mention that the built-in implementations will be contained inside a core extension. @nishantmonu51 @himanshug re: useBuiltInTables() is eliminated from the proposed interfaces, the coordinator APIs will now be disabled unless the core extension's implementations are in use. |
I think I agree with this -- that way we aren't forcing people to use an identical RBAC model to our "standard" model for authorization. Although, this is apparently the road Hive went down. @nishantmonu51, do you have a sense about how successful that has been? Do implementations usually implement all the HiveAuthorizer methods and do you know if there have been issues mapping that onto systems whose authorization models aren't the same as Hive's? |
057814c to
badcd34
Compare
|
Regarding having modular RBAC system, an example of that would be Apache Ranger. |
068473e to
1846033
Compare
df533ad to
7d48800
Compare
cc58cfe to
83f4dff
Compare
13f5b40 to
880e6b4
Compare
880e6b4 to
eff666c
Compare
gianm
left a comment
There was a problem hiding this comment.
Reviewed changes since the last commit.
| * take care of sending the response. | ||
| */ | ||
| public class SystemAuthorizationInfo implements AuthorizationInfo | ||
| public class ForbiddenException extends SecurityException |
There was a problem hiding this comment.
This should extend RuntimeException directly, rather than SecurityException. This is because SecurityException is an exception type reserved for the JDK (it's "thrown by the security manager to indicate a security violation").
| catch (ForbiddenException e) { | ||
| // don't do anything for an authorization failure, ForbiddenExceptionMapper will catch this later and | ||
| // send an error response if this is thrown. | ||
| Throwables.propagate(e); |
There was a problem hiding this comment.
The return context.gotError(e) here is ignored, because Throwables.propagate always throws an exception. From docs for Throwables.propagate:
This method always throws an exception. The
RuntimeExceptionreturn type is only for client code to make Java type system happy in case a return value is required by the enclosing method.
Also, see this doc for some common pitfalls of using Throwables.propagate: https://github.com/google/guava/wiki/Why-we-deprecated-Throwables.propagate
In this case, since ForbiddenExceptionMapper is going to do all of the handling of sending an error message, the try/catch isn't necessary at all. So it's better to skip the catch block entirely.
| log.error(errorMsg); | ||
|
|
||
| // Send out an alert so there's a centralized collection point for seeing errors of this nature | ||
| log.makeAlert(errorMsg); |
There was a problem hiding this comment.
This needs a couple of tweaks.
- There must be an
.emit()aftermakeAlert(...)or else the alert will not go anywhere. - The
log.erroris not useful since it will just double-log (log.makeAlert(...).emit()will also do an error log).
| loginContext.logout(); | ||
| } | ||
| catch (LoginException ex) { | ||
| log.warn(ex.getMessage(), ex); |
There was a problem hiding this comment.
This is backwards. It should be log.warn(ex, ex.getMessage()).
| loginContext.login(); | ||
| } | ||
| catch (LoginException le) { | ||
| log.warn("Failed to login as [{}]", spnegoPrincipal, le); |
gianm
left a comment
There was a problem hiding this comment.
Noticed some minor doc and annotations issues.
| * @param authorizationInfo authorization info from the request; or null if none is present. This must be non-null | ||
| * if security is enabled, or the request will be considered unauthorized. | ||
| * @param user authentication token from the request | ||
| * @param namespace authentication namespace of the request |
There was a problem hiding this comment.
Some of these parameters don't exist, please make sure the javadoc is up to date.
| public <T> Sequence<T> runSimple( | ||
| final Query<T> query, | ||
| @Nullable final AuthorizationInfo authorizationInfo, | ||
| @Nullable final AuthenticationResult authenticationResult, |
There was a problem hiding this comment.
It seems like authenticationResult is not actually nullable, so remove the annotation.
| public Access authorize(@Nullable final AuthorizationInfo authorizationInfo) | ||
| * */ | ||
| public Access authorize( | ||
| @Nullable final AuthenticationResult authenticationResult |
There was a problem hiding this comment.
It seems like authenticationResult is not actually nullable, so remove the annotation.
| * @param authorizationInfo authorization info from the request; or null if none is present. This must be non-null | ||
| * if security is enabled, or the request will be considered unauthorized. | ||
| * @param token authentication token from the request | ||
| * @param namespace namespace of the authentication token |
There was a problem hiding this comment.
The params here look out of date.
c653a28 to
e833bef
Compare
| { | ||
| for (Authenticator authenticator : authenticators) { | ||
| FilterHolder holder = new FilterHolder(authenticator.getFilter()); | ||
| FilterHolder holder = new FilterHolder( |
There was a problem hiding this comment.
The AuthenticationWrappingFilter change should involve a small doc change to how the authentication chain works (first one to identify the user is respected, and then others are skipped).
| { | ||
| // Send out an alert so there's a centralized collection point for seeing errors of this nature | ||
| log.makeAlert(errorMsg); | ||
| log.makeAlert(errorMsg).emit(); |
There was a problem hiding this comment.
This should be split into something like log.makeAlert(errorMsg).addData("uri", uri).addData("method", method).emit(). The reason is that having a consistent message helps identify these after the alerts have been collected. They're easy to search for and then the "uri" field can be inspected to discover which endpoint is bad. "method" is important too so we can differentiate get/post/delete.
b27cb1f to
0f44de3
Compare
|
Looks good to me. Nice work! |
|
@gianm I've verified this patch on our test cluster @himanshug @nishantmonu51 @pjain1 @gianm @jihoonson Thanks a lot for the reviews! |
| } | ||
| } | ||
|
|
||
| // Since we can't see the request object on the remote side, we can't check whether the remote side actually |
There was a problem hiding this comment.
@jon-wei "remote" in this comment means upstream remote (something, that send a request to router), or downstream remote (broker)? Also, in the sentense
If the remote node failed to perform an authorization check, will log that on the remote node.
apparently the same "remote node" is referred, but in both past tense: "failed" and future tense: "will log", like this code (clientRequest.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);) happens between something else happening on "remote".
I couldn't really understand what is going on here
There was a problem hiding this comment.
Also, don't understand what does "we can't see the request object on the remote side" mean.
There was a problem hiding this comment.
"remote" in that comment refers to the proxy forwarding target (the brokers)
If the remote node failed to perform an authorization check, will log that on the remote node.
Suppose the router forwards a request to a broker, and due to a bug, the broker does not actually perform any authorization checks for that request.
The request will eventually go through the PreResponseAuthorizationCheckFilter on that broker, which will see that no authorization check was performed, and log an error.
"we can't see the request object on the remote side"
This refers to how the router has no visibility into whether the broker has set the DRUID_AUTHORIZATION_CHECKED attribute on the request object that the broker is handling.
clientRequest.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true);
This is there so that "clientRequest" does not fail the validation checks in PreResponseAuthorizationCheckFilter on the router. The "real" authorization check occurs on the broker that receives a forwarded request.
There was a problem hiding this comment.
This refers to how the router has no visibility into whether the broker has set the DRUID_AUTHORIZATION_CHECKED attribute on the request object that the broker is handling.
I don't understand, how this is relevant? If router would magically be able to know, how it would change anything?
This is there so that "clientRequest" does not fail the validation checks in PreResponseAuthorizationCheckFilter on the router. The "real" authorization check occurs on the broker that receives a forwarded request.
Does this mean that on the query, that arrives to the broker, DRUID_AUTHORIZATION_CHECKED is false again, despite this clientRequest.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true); line?
There was a problem hiding this comment.
Is this right, that this line effective just suppresses auth check on the router, because it's done on the broker? If so, why even adding PreResponseAuthorizationCheckFilter to the pipeline on router?
There was a problem hiding this comment.
I don't understand, how this is relevant? If router would magically be able to know, how it would change anything?
If the router could magically know, it could conceivably have clientRequest's authorization check status match the authorization check status of the proxy request (not that crucial, it would give you some extra information on the router side that there is a potential authorization bug on the forwarding target)
The main point of the comment is just to indicate that the real authorization check occurs at the forwarding destination.
Does this mean that on the query, that arrives to the broker, DRUID_AUTHORIZATION_CHECKED is false again, despite this clientRequest.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true); line?
Yes, the broker sees the request represented by "proxyRequest", not "clientRequest". Attributes are not transmitted in HTTP requests on the wire anyway, they're only for keeping internal server-side state related to the request.
Is this right, that this line effective just suppresses auth check on the router, because it's done on the broker? If so, why even adding PreResponseAuthorizationCheckFilter to the pipeline on router?
Yes, it's there to suppress auth checks on the router side for forwarded requests.
There are other endpoints on the router that do need authorization, like /status and /druid/v1/brokers (which is actually missing authorization checks as of this comment, and should be fixed)
| authorizerMapper | ||
| ); | ||
|
|
||
| if (authConfig.isEnabled()) { |
There was a problem hiding this comment.
Since this change, the authConfig field in MetadataResource is unused
Extension points for authentication/authorization
This PR implements several enhancements to the Druid security system with the following goals in mind:
Existing Design
The following section describes the existing procedure for creating an extension that handles authentication and authorization:
Some limitations of the current system are:
PR Change Summary
Example Implementation
An extension that uses these interfaces to provide support for HTTP Basic authentication and a simple RBAC authorization system can be found here:
jon-wei#1
Authentication and Authorization
Authenticator
This interface is essentially a ServletFilterHolder with additional requirements on the getFilter() method contract, plus:
Authentication Chain
To determine what authentication mechanisms are to be used, the user should specify a list of Authenticator type names in the config, which will be used to instantiate a chain of filters by calling getFilter() on the registered Authenticators.
A Filter that handles failed authentication checks will always be placed at the end of the filter chain. If the Druid-Auth-Token attribute is not set, but the request was not anonymous (had an authentication header), a 403 Forbidden error response will be sent. If an anonymous request is received, this filter will build a WWW-Authenticate header for each Authenticator by calling getAuthChallengeHeader() and add these to the 401 Unauthorized error response that will be sent, providing the client with a list of supported HTTP authentication schemes. If authentication succeeded, this filter will do nothing.
A Filter that performs a sanity check on requests will always be placed at the start of the filter chain. This filter will check that the Druid-Auth-Token attribute is not set in the request (i.e., the client trying to fake an authentication result). The filter will also check for a "Druid-Auth-Token-Checked" attribute (described later in the proposal). An error response will be sent if either of these attributes are seen.
Authorizer
An Authorizer is responsible for performing authorization checks for resource accesses.
This interface is intended to replace the current AuthorizationInfo. A single instance of each Authorizer will be created per node.
Security-sensitive endpoints will need to extract the identity string contained in the request's Druid-Auth-Token attribute, previously set by an Authenticator. Each endpoint will pass this identity String to the Authorizer's authorize() method along with any Resource/Action pairs created for the request being handled.
The endpoint can use these checks to filter out resources or deny the request as needed.
After a request is authorized, a new attribute, "Druid-Auth-Token-Checked", should be set in the request header with the result of the authorization decision.
Authorization Validation
Another servlet filter will be defined to check that all requests undergo authorization if security features are enabled.
This filter will be applied after a request has been processed by an endpoint but before the response is sent to the client. If the "Druid-Auth-Token-Checked" attribute is not set in the request, then an error response will be sent instead of the actual response.
This helps to reduce the instances of fail-open behavior. However, this mechanism is imperfect as any state changes that took place during the request handling will still take effect, even if the response is killed. This feature is more intended to help detect authorization bugs or design omissions.
Namespaces
Authenticator and Authorizer implementations are linked through a namespace string. Authenticators tag an authenticated request with a namespace, which is used to route the authenticated request to the Authorizer implementation that registered itself with a matching namespace.
This is to support cases where an Authorizer implementation is only intended to authorize requests from a specific authenticator (an implementation may have assumptions about the user name format, for example).
The details of namespace configuration are left for implementors of Authenticator and Authorizer to decide.
The namespace header field is "Druid-Auth-Token-Namespace" and contains a String. Namespace mapping is handled by the AuthorizerMapper class.