diff --git a/docs/design/auth-model.md b/docs/design/auth-model.md new file mode 100644 index 000000000000..44b84fad1789 --- /dev/null +++ b/docs/design/auth-model.md @@ -0,0 +1,337 @@ +--- +id: auth-model +title: "Authorization model" +--- + + + +## Authorization model + +There are two versions of auth model in Druid, auth v1 model (legacy) and newer auth v2 model. Auth v1 is default and switch +to v2 can be made by setting the flag `druid.auth.authVersion=v2` . + +Here are the details below - + +## Auth V1 Model (Legacy) +There are two action types in Druid: READ and WRITE + +There are three resource types in Druid: DATASOURCE, CONFIG, and STATE. + +### DATASOURCE +Resource names for this type are datasource names. Specifying a datasource permission allows the administrator to grant users access to specific datasources. + +### CONFIG +There are two possible resource names for the "CONFIG" resource type, "CONFIG" and "security". Granting a user access to CONFIG resources allows them to access the following endpoints. + +"CONFIG" resource name covers the following endpoints: + +|Endpoint|Process Type| +|--------|---------| +|`/druid/coordinator/v1/config`|coordinator| +|`/druid/indexer/v1/worker`|overlord| +|`/druid/indexer/v1/worker/history`|overlord| +|`/druid/worker/v1/disable`|middleManager| +|`/druid/worker/v1/enable`|middleManager| + +"security" resource name covers the following endpoint: + +|Endpoint|Process Type| +|--------|---------| +|`/druid-ext/basic-security/authentication`|coordinator| +|`/druid-ext/basic-security/authorization`|coordinator| + +### STATE +There is only one possible resource name for the "STATE" config resource type, "STATE". Granting a user access to STATE resources allows them to access the following endpoints. + +"STATE" resource name covers the following endpoints: + +|Endpoint|Process Type| +|--------|---------| +|`/druid/coordinator/v1`|coordinator| +|`/druid/coordinator/v1/rules`|coordinator| +|`/druid/coordinator/v1/rules/history`|coordinator| +|`/druid/coordinator/v1/servers`|coordinator| +|`/druid/coordinator/v1/tiers`|coordinator| +|`/druid/broker/v1`|broker| +|`/druid/v2/candidates`|broker| +|`/druid/indexer/v1/leader`|overlord| +|`/druid/indexer/v1/isLeader`|overlord| +|`/druid/indexer/v1/action`|overlord| +|`/druid/indexer/v1/workers`|overlord| +|`/druid/indexer/v1/scaling`|overlord| +|`/druid/worker/v1/enabled`|middleManager| +|`/druid/worker/v1/tasks`|middleManager| +|`/druid/worker/v1/task/{taskid}/shutdown`|middleManager| +|`/druid/worker/v1/task/{taskid}/log`|middleManager| +|`/druid/historical/v1`|historical| +|`/druid-internal/v1/segments/`|historical| +|`/druid-internal/v1/segments/`|peon| +|`/druid-internal/v1/segments/`|realtime| +|`/status`|all process types| + +### HTTP methods + +For information on what HTTP methods are supported on a particular request endpoint, please refer to the [API documentation](../operations/api-reference.md). + +GET requires READ permission, while POST and DELETE require WRITE permission. + +### SQL Permissions + +Queries on Druid datasources require DATASOURCE READ permissions for the specified datasource. + +Queries on the [INFORMATION_SCHEMA tables](../querying/sql.html#information-schema) will +return information about datasources that the caller has DATASOURCE READ access to. Other +datasources will be omitted. + +Queries on the [system schema tables](../querying/sql.html#system-schema) require the following permissions: +- `segments`: Segments will be filtered based on DATASOURCE READ permissions. +- `servers`: The user requires STATE READ permissions. +- `server_segments`: The user requires STATE READ permissions and segments will be filtered based on DATASOURCE READ permissions. +- `tasks`: Tasks will be filtered based on DATASOURCE READ permissions. + +## Auth V2 Model + +This model can be enabled by setting the flag `druid.auth.authVersion=v2`. The idea behind this model is to support user +personas like admin, viewer etc. in an easy manner. + +There are two action types in Druid: READ and WRITE. Depending on HTTP method used for endpoints action is decided, for +GET and HEAD method action is `READ`, for all other methods action is `WRITE`. + +There are 4 resource types in Druid: DATASOURCE, INTERNAL, LOOKUP and SERVER. + +1. `DATASOURCE` resource type is concerned with all the actions that can be taken for querying, indexing, setting retention, +compaction rules for a dataset. Thus, if a user has read/write action on datasource they can control the lifecycle of that datasource. + +1. `SERVER` resource type covers all the cluster administration endpoints. + +1. `LOOKUP` resource type covers lookups similar to `DATASOURCE` resource type. + +1. `INTERNAL` resource type covers all the resources/endpoints that druid internally uses to communicate among nodes. + +Examples - A server admin role can have read/write on `DATASOURCE`, `SERVER` and `LOOKUP` resource types. A viewer can have +just read on all or specific `DATASOURCE`. Many times users may want to create roles with custom permissions, all these +is supported using `Resource Name`, each `Resource type` can have multiple of them. Authorizer uses `Action`, `Resource Type` +and `Resource Name` to authorize users action. For example, while querying a datasource named `ds` a `READ` action on +resource type `DATASOURCE` with resource name `ds` is required. Below are the details - + +### DATASOURCE + +Resource names for this type are datasource names. Specifying a datasource permission allows the administrator to grant users access to specific datasources. +When users get `READ` permission on a datasource, they will be able to query that datasource, will be able to see retention/compaction rules +for them, get schema details and vice versa with `WRITE` permissions. + +Below are the endpoints protected/filtered using datasource permissions. These permissions are enforced in SQL queries as well, see `SQL Permissions` section below. + +|Endpoint|Process Type| +|--------|---------| +|`GET/POST /druid/coordinator/v1/datasources/...`|coordinator| +|`GET /druid/coordinator/v1/rules`|coordinator| +|`GET/POST /druid/coordinator/v1/rules/{dataSourceName}`|coordinator| +|`GET /druid/coordinator/v1/rules/{dataSourceName}/history`|coordinator| +|`GET /druid/coordinator/v1/tiers/{tierName}`|coordinator| +|`GET/POST /druid/v2/datasources/...`|broker| +|`GET/POST /druid/coordinator/v1/metadata/...`|coordinator| +|`GET/POST /druid/coordinator/v1/config/compaction`|coordinator| +|`GET/DELETE /druid/coordinator/v1/config/compaction/{dataSource}`|coordinator| +|`GET /druid/coordinator/v1/compaction/progress`|coordinator| +|`GET /druid/coordinator/v1/compaction/status`|coordinator| +|`POST /druid/indexer/v1/task`|overlord| +|`GET /druid/indexer/v1/task/{taskid}`|overlord| +|`GET /druid/indexer/v1/task/{taskid}/status`|overlord| +|`GET /druid/indexer/v1/task/{taskid}/segments`|overlord| +|`POST /druid/indexer/v1/task/{taskid}/shutdown`|overlord| +|`POST /druid/indexer/v1/datasources/{dataSource}/shutdownAllTasks`|overlord| +|`GET /druid/indexer/v1/waitingTasks`|overlord| +|`GET /druid/indexer/v1/pendingTasks`|overlord| +|`GET /druid/indexer/v1/runningTasks`|overlord| +|`GET /druid/indexer/v1/completeTasks`|overlord| +|`GET /druid/indexer/v1/tasks`|overlord| +|`DELETE /druid/indexer/v1/pendingSegments/{dataSource}`|overlord| +|`GET /druid/indexer/v1/task/{taskid}/log`|overlord| +|`GET /druid/indexer/v1/task/{taskid}/reports`|overlord| + +Note - When reindexing from an existing datasource, user needs read on source datasource permission in addition to +write on destination datasource. + +### SERVER + +There are 3 possible resource name for the "SERVER" config resource type, `SERVER`, `STATUS` and `USER`. + +`SERVER` resource name covers following the end points, these are relevant for managing the cluster and can be used by +cluster managers. This resource name is used in SQL queries asking for server information see `SQL Permissions` section below. + +|Endpoint|Process Type| +|--------|---------| +|`GET /druid/indexer/v1/workers`|overlord| +|`POST /druid/indexer/v1/worker/{host}/enable`|overlord| +|`POST /druid/indexer/v1/worker/{host}/disable`|overlord| +|`GET /druid/coordinator/v1/loadstatus`|coordinator| +|`GET /druid/coordinator/v1/loadqueue`|coordinator| +|`GET /druid/coordinator/v1/rules/history`|coordinator| +|`GET /druid/coordinator/v1/servers`|coordinator| +|`GET /druid/coordinator/v1/servers/{serverName}`|coordinator| +|`GET /druid/coordinator/v1/servers/{serverName}/segments`|coordinator| +|`GET /druid/coordinator/v1/servers/{serverName}/segments/{segmentId}`|coordinator| +|`GET /druid/indexer/v1/worker`|overlord| +|`POST /druid/indexer/v1/worker`|overlord| +|`GET /druid/indexer/v1/worker/history`|overlord| +|`GET /status/properties`|all| +|`POST /druid/coordinator/v1/config/compaction/taskslots`|coordinator| +|`GET /druid/coordinator/v1/config`|coordinator| +|`POST /druid/coordinator/v1/config`|coordinator| +|`GET /druid/coordinator/v1/config/history`|coordinator| +|`GET /druid/coordinator/v1/lookups/nodeStatus`|coordinator| +|`GET /druid/coordinator/v1/lookups/nodeStatus/{tier}`|coordinator| +|`GET /druid/coordinator/v1/lookups/config/{tier}/{hostAndPort}`|coordinator| +|`GET/POST /druid-ext/basic-security/authentication`|coordinator| +|`GET/POST /druid-ext/basic-security/authorization`|coordinator| + +`STATUS` resource name covers following the end points and are relevant to check status of various processes and can be +used by cluster managers and systems checking status of different druid processes. + +|Endpoint|Process Type| +|--------|---------| +|`GET /druid/indexer/v1/leader`|overlord| +|`GET /druid/coordinator/v1/leader`|coordinator| +|`GET /druid/coordinator/v1/isLeader`|coordinator| +|`GET /druid/historical/v1/loadstatus`|broker,historical,peon| +|`GET /druid/broker/v1/loadstatus`|broker| +|`GET /status`|all| + +`USER` resource name covers following the end points and are relevant to general users using the cluster needing access +to some `SERVER` resources during routine work. + +|Endpoint|Process Type| +|--------|---------| +|`POST /druid/indexer/v1/sampler`|overlord| +|`GET /druid/coordinator/v1/lookups/config`|coordinator| +|`GET /druid/coordinator/v1/tiers`|coordinator| +|`GET /druid/coordinator/v1/tiers/{tierName}`|coordinator| + +### INTERNAL + +There is only one possible resource names for the `INTERNAL` resource type - `INTERNAL`. These permissions mostly makes sense +for druid nodes talking to each other and cluster admins. + +|Endpoint|Process Type| +|--------|---------| +|`GET /druid-internal/v1/httpRemoteTaskRunner/knownTasks `|overlord| +|`GET /druid-internal/v1/httpRemoteTaskRunner/pendingTasksQueue`|overlord| +|`GET /druid-internal/v1/httpRemoteTaskRunner/workerSyncerDebugInfo`|overlord| +|`GET /druid-internal/v1/httpRemoteTaskRunner/blacklistedWorkers`|overlord| +|`GET /druid-internal/v1/httpRemoteTaskRunner/lazyWorker`|overlord| +|`GET /druid-internal/v1/httpRemoteTaskRunner/workersWithUnacknowledgedTasks`|overlord| +|`GET /druid-internal/v1/httpRemoteTaskRunner/workersEilgibleToRunTasks`|overlord| +|`POST /druid/indexer/v1/taskStatus`|overlord| +|`POST /druid/indexer/v1/action`|overlord| +|`GET /druid/indexer/v1/scaling`|overlord| +|`GET /druid/worker/v1/shuffle/task/{supervisorTaskId}/{subTaskId}/partition`|middleManager| +|`DELETE /druid/worker/v1/shuffle/task/{supervisorTaskId}`|middleManager| +|`GET /druid-internal/v1/worker/`|middleManager| +|`POST /druid-internal/v1/worker/assignTask`|middleManager| +|`GET /druid/worker/v1/enabled`|middleManager| +|`GET /druid/worker/v1/tasks`|middleManager| +|`POST /druid/worker/v1/task/{taskid}/shutdown`|middleManager| +|`GET /druid/worker/v1/task/{taskid}/log`|middleManager| +|`POST /druid/worker/v1/disable`|middleManager| +|`POST /druid/worker/v1/enable`|middleManager| +|`GET /druid-internal/v1/httpServerInventoryView`|broker,coordinator| +|`POST /druid/v2/candidates`|broker| +|`GET /druid/coordinator/v1/cluster`|coordinator| +|`GET /druid/coordinator/v1/cluster/{nodeRole}`|coordinator| +|`GET /druid/coordinator/v1/compaction/compact`|coordinator| +|`GET /druid/router/v1/brokers`|router| +|`GET /druid-internal/v1/segments/`|broker,historical,peon| +|`GET /druid-internal/v1/segments/changeRequests`|broker,historical,peon| +|`GET /status/selfDiscovered/status`|all| +|`GET /status/selfDiscovered`|all| +|`POST /druid/listen/v1/lookups`|broker,historical,peon| +|`POST /druid/listen/v1/lookups/updates`|broker,historical,peon| +|`GET /druid/listen/v1/lookups`|broker,historical,peon| +|`GET /druid/listen/v1/lookups/{id}`|broker,historical,peon| +|`POST /druid/listen/v1/lookups/{id}`|broker,historical,peon| +|`DELETE /druid/listen/v1/lookups/{id}`|broker,historical,peon| + +### LOOKUP + +`LOOKUP` is similar to `DATASOURCE` resource type having lookup name as resource name and specifying a lookup permission +allows the administrator to grant users access to specific lookup name. Endpoints that return list of lookup will return +filtered list by the granted permissions. It covers the following endpoints. + +|Endpoint|Process Type| +|--------|---------| +|`POST /druid/coordinator/v1/lookups/config`|coordinator| +|`GET /druid/coordinator/v1/lookups/config/all`|coordinator| +|`DELETE /druid/coordinator/v1/lookups/config/{tier}`|coordinator| +|`DELETE /druid/coordinator/v1/lookups/config/{tier}/{lookup}`|coordinator| +|`POST /druid/coordinator/v1/lookups/config/{tier}/{lookup}`|coordinator| +|`GET /druid/coordinator/v1/lookups/config/{tier}/{lookup}`|coordinator| +|`GET /druid/coordinator/v1/lookups/config/{tier}`|coordinator| +|`GET /druid/coordinator/v1/lookups/status`|coordinator| +|`GET /druid/coordinator/v1/lookups/status/{tier}`|coordinator| +|`GET /druid/coordinator/v1/lookups/status/{tier}/{lookup}`|coordinator| + +Note - Since lookups are protected using lookup ids, users have to make sure the lookup ids are unique across different +lookup tiers to avoid conflicts. + +### EXAMPLE ROLES-PERMISSION MAPPING + +These are some example policies that be used. Policies are of form `ACTION:RESOURCE_TYPE:RESOURCE_NAME` +1. Cluster admin (super user/druid system) can have following policies - `READ:DATASOURCE:*`, `WRITE:DATASOURCE:*`, +`READ:INTERNAL:*`, `WRITE:INTERNAL:*`, `READ:SERVER:*`, `WRITE:SERVER:*`, `READ:LOOKUP:*` and `WRITE:LOOKUP:*`. +1. Cluster manager can have following policies - `READ:DATASOURCE:*`, `WRITE:DATASOURCE:*`, `READ:SERVER:*`, `WRITE:SERVER:*`, + `READ:LOOKUP:*` and `WRITE:LOOKUP:*`. +1. A user just wanting to read/write to specific datasource and lookup can have following policies - `READ:DATASOURCE:`, +`WRITE:DATASOURCE:`, `READ:LOOKUP:`, `WRITE:LOOKUP:`, `READ:SERVER:USER`, `WRITE:SERVER:USER` AND `READ:SERVER:STATUS`. +1. A user just wanting to read from specific datasource and lookup can have following policies - `READ:DATASOURCE:`, +`READ:LOOKUP:`, and `READ:SERVER:USER`. +1. An external monitoring system can just have `READ:SERVER:STATUS` permission. + +### HTTP methods + +For information on what HTTP methods are supported on a particular request endpoint, please refer to the [API documentation](../operations/api-reference.md). + +GET and HEAD requires READ permission, while POST and DELETE require WRITE permission. + +### SQL Permissions + +Queries on Druid datasources require DATASOURCE READ permissions for the specified datasource. + +Queries on the [INFORMATION_SCHEMA tables](../querying/sql.html#information-schema) will +return information about datasources that the caller has DATASOURCE READ access to. Other +datasources will be omitted. + +Queries on the [system schema tables](../querying/sql.html#system-schema) require the following permissions: +- `segments`: Segments will be filtered based on DATASOURCE READ permissions. +- `servers`: The user requires `READ` on `SERVER` resource type with `SERVER` resource name. +- `server_segments`: The user requires `READ` on `SERVER` resource type with `SERVER` resource name permissions and segments +will be filtered based on DATASOURCE READ permissions. +- `tasks`: Tasks will be filtered based on DATASOURCE READ permissions. + +### Backwards compatibility and rolling upgrade to auth v2 model from v1 +Unless the flag `druid.auth.authVersion` is set to `v2`, older model will be used. For rolling upgrade to v2 model +there are few prerequisites - +1. Define the new permissions and policies in the auth system you are using. +1. Make sure the auth extension you are using supports the newer model, you need to make sure the `Authorizer` implementation +has implemented `authorizeV2` method implemented. +1. Please make sure both newer and older set of permissions/roles or policies exist in the auth system till all the nodes are upgraded. + +Once all the above things are in place follow the rolling upgrade [guide](../operations/rolling-updates.html) and set `druid.auth.authVersion=v2` +on each node before upgrading. \ No newline at end of file diff --git a/docs/design/auth.md b/docs/design/auth.md index 9701f632ddba..c23f0efeb58d 100644 --- a/docs/design/auth.md +++ b/docs/design/auth.md @@ -194,3 +194,7 @@ druid.auth.authorizer..name= ``` These properties provide the authenticator and authorizer names to the implementations as @JsonProperty parameters, potentially useful when multiple authenticators or authorizers of the same type are configured. + +## Defining permissions + +Please see [authorization model](../design/auth-model.md). diff --git a/docs/development/extensions-core/druid-basic-security.md b/docs/development/extensions-core/druid-basic-security.md index 892306e4a07b..248d57252f46 100644 --- a/docs/development/extensions-core/druid-basic-security.md +++ b/docs/development/extensions-core/druid-basic-security.md @@ -457,81 +457,7 @@ Each Authorizer will always have a default "admin" and "druid_system" user with ## Defining permissions -There are two action types in Druid: READ and WRITE - -There are three resource types in Druid: DATASOURCE, CONFIG, and STATE. - -### DATASOURCE -Resource names for this type are datasource names. Specifying a datasource permission allows the administrator to grant users access to specific datasources. - -### CONFIG -There are two possible resource names for the "CONFIG" resource type, "CONFIG" and "security". Granting a user access to CONFIG resources allows them to access the following endpoints. - -"CONFIG" resource name covers the following endpoints: - -|Endpoint|Process Type| -|--------|---------| -|`/druid/coordinator/v1/config`|coordinator| -|`/druid/indexer/v1/worker`|overlord| -|`/druid/indexer/v1/worker/history`|overlord| -|`/druid/worker/v1/disable`|middleManager| -|`/druid/worker/v1/enable`|middleManager| - -"security" resource name covers the following endpoint: - -|Endpoint|Process Type| -|--------|---------| -|`/druid-ext/basic-security/authentication`|coordinator| -|`/druid-ext/basic-security/authorization`|coordinator| - -### STATE -There is only one possible resource name for the "STATE" config resource type, "STATE". Granting a user access to STATE resources allows them to access the following endpoints. - -"STATE" resource name covers the following endpoints: - -|Endpoint|Process Type| -|--------|---------| -|`/druid/coordinator/v1`|coordinator| -|`/druid/coordinator/v1/rules`|coordinator| -|`/druid/coordinator/v1/rules/history`|coordinator| -|`/druid/coordinator/v1/servers`|coordinator| -|`/druid/coordinator/v1/tiers`|coordinator| -|`/druid/broker/v1`|broker| -|`/druid/v2/candidates`|broker| -|`/druid/indexer/v1/leader`|overlord| -|`/druid/indexer/v1/isLeader`|overlord| -|`/druid/indexer/v1/action`|overlord| -|`/druid/indexer/v1/workers`|overlord| -|`/druid/indexer/v1/scaling`|overlord| -|`/druid/worker/v1/enabled`|middleManager| -|`/druid/worker/v1/tasks`|middleManager| -|`/druid/worker/v1/task/{taskid}/shutdown`|middleManager| -|`/druid/worker/v1/task/{taskid}/log`|middleManager| -|`/druid/historical/v1`|historical| -|`/druid-internal/v1/segments/`|historical| -|`/druid-internal/v1/segments/`|peon| -|`/druid-internal/v1/segments/`|realtime| -|`/status`|all process types| - -### HTTP methods - -For information on what HTTP methods are supported on a particular request endpoint, please refer to the [API documentation](../../operations/api-reference.md). - -GET requires READ permission, while POST and DELETE require WRITE permission. - -### SQL Permissions - -Queries on Druid datasources require DATASOURCE READ permissions for the specified datasource. - -Queries on the [INFORMATION_SCHEMA tables](../../querying/sql.html#information-schema) will -return information about datasources that the caller has DATASOURCE READ access to. Other -datasources will be omitted. - -Queries on the [system schema tables](../../querying/sql.html#system-schema) require the following permissions: -- `segments`: Segments will be filtered based on DATASOURCE READ permissions. -- `servers`: The user requires STATE READ permissions. -- `server_segments`: The user requires STATE READ permissions and segments will be filtered based on DATASOURCE READ permissions. -- `tasks`: Tasks will be filtered based on DATASOURCE READ permissions. +Please see [authorization model](../../design/auth-model.md). ## Configuration Propagation diff --git a/docs/querying/sql.md b/docs/querying/sql.md index 861397265bf8..aeaed07926a3 100644 --- a/docs/querying/sql.md +++ b/docs/querying/sql.md @@ -1221,5 +1221,5 @@ Druid SQL planning occurs on the Broker and is configured by ## Security -Please see [Defining SQL permissions](../development/extensions-core/druid-basic-security.html#sql-permissions) in the +Please see [Defining SQL permissions](../design/auth-model.html#sql-permissions) in the basic security documentation for information on what permissions are needed for making SQL queries. diff --git a/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/BasicSecurityResourceFilter.java b/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/BasicSecurityResourceFilter.java index 8294ec1c6bd7..7ac60e0b3b3a 100644 --- a/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/BasicSecurityResourceFilter.java +++ b/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/BasicSecurityResourceFilter.java @@ -24,6 +24,7 @@ import org.apache.druid.java.util.common.StringUtils; import org.apache.druid.server.http.security.AbstractResourceFilter; import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.AuthConfig; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.Resource; @@ -33,6 +34,7 @@ import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response; +@Deprecated public class BasicSecurityResourceFilter extends AbstractResourceFilter { private static final String SECURITY_RESOURCE_NAME = "security"; @@ -48,25 +50,26 @@ public BasicSecurityResourceFilter( @Override public ContainerRequest filter(ContainerRequest request) { - final ResourceAction resourceAction = new ResourceAction( - new Resource(SECURITY_RESOURCE_NAME, ResourceType.CONFIG), - getAction(request) - ); - - final Access authResult = AuthorizationUtils.authorizeResourceAction( - getReq(), - resourceAction, - getAuthorizerMapper() - ); + if (getAuthVersion().equals(AuthConfig.AUTH_VERSION_1)) { + final ResourceAction resourceAction = new ResourceAction( + new Resource(SECURITY_RESOURCE_NAME, ResourceType.CONFIG), + getAction(request) + ); - if (!authResult.isAllowed()) { - throw new WebApplicationException( - Response.status(Response.Status.FORBIDDEN) - .entity(StringUtils.format("Access-Check-Result: %s", authResult.toString())) - .build() + final Access authResult = AuthorizationUtils.authorizeResourceAction( + getReq(), + resourceAction, + getAuthorizerMapper() ); - } + if (!authResult.isAllowed()) { + throw new WebApplicationException( + Response.status(Response.Status.FORBIDDEN) + .entity(StringUtils.format("Access-Check-Result: %s", authResult.toString())) + .build() + ); + } + } return request; } } diff --git a/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authentication/endpoint/BasicAuthenticatorResource.java b/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authentication/endpoint/BasicAuthenticatorResource.java index 23d8316771c4..4eb93f84a641 100644 --- a/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authentication/endpoint/BasicAuthenticatorResource.java +++ b/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authentication/endpoint/BasicAuthenticatorResource.java @@ -25,6 +25,7 @@ import org.apache.druid.guice.LazySingleton; import org.apache.druid.security.basic.BasicSecurityResourceFilter; import org.apache.druid.security.basic.authentication.entity.BasicAuthenticatorCredentialUpdate; +import org.apache.druid.server.http.security.ServerServerResourceFilter; import org.apache.druid.server.security.AuthValidator; import javax.servlet.http.HttpServletRequest; @@ -65,7 +66,7 @@ public BasicAuthenticatorResource( @Path("/loadStatus") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response getLoadStatus( @Context HttpServletRequest req ) @@ -83,7 +84,7 @@ public Response getLoadStatus( @Path("/refreshAll") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response refreshAll( @Context HttpServletRequest req ) @@ -100,7 +101,7 @@ public Response refreshAll( @Path("/db/{authenticatorName}/users") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response getAllUsers( @Context HttpServletRequest req, @PathParam("authenticatorName") final String authenticatorName @@ -120,7 +121,7 @@ public Response getAllUsers( @Path("/db/{authenticatorName}/users/{userName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response getUser( @Context HttpServletRequest req, @PathParam("authenticatorName") final String authenticatorName, @@ -143,7 +144,7 @@ public Response getUser( @Path("/db/{authenticatorName}/users/{userName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response createUser( @Context HttpServletRequest req, @PathParam("authenticatorName") final String authenticatorName, @@ -166,7 +167,7 @@ public Response createUser( @Path("/db/{authenticatorName}/users/{userName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response deleteUser( @Context HttpServletRequest req, @PathParam("authenticatorName") final String authenticatorName, @@ -189,7 +190,7 @@ public Response deleteUser( @Path("/db/{authenticatorName}/users/{userName}/credentials") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response updateUserCredentials( @Context HttpServletRequest req, @PathParam("authenticatorName") final String authenticatorName, @@ -210,7 +211,7 @@ public Response updateUserCredentials( @Path("/db/{authenticatorName}/cachedSerializedUserMap") @Produces(SmileMediaTypes.APPLICATION_JACKSON_SMILE) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response getCachedSerializedUserMap( @Context HttpServletRequest req, @PathParam("authenticatorName") final String authenticatorName @@ -227,7 +228,7 @@ public Response getCachedSerializedUserMap( @Path("/listen/{authenticatorName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response authenticatorUpdateListener( @Context HttpServletRequest req, @PathParam("authenticatorName") final String authenticatorName, diff --git a/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java b/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java index f307b9ea029e..67770d8eea56 100644 --- a/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java +++ b/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authorization/BasicRoleBasedAuthorizer.java @@ -109,6 +109,14 @@ public Access authorize(AuthenticationResult authenticationResult, Resource reso return new Access(false); } + @Override + public Access authorizeV2(AuthenticationResult authenticationResult, Resource resource, Action action) + { + // its same implementation as with v2 permissions for roles will be changed in the metadata store + // checking logic will remain the same + return this.authorize(authenticationResult, resource, action); + } + private boolean permissionCheck(Resource resource, Action action, BasicAuthorizerPermission permission) { if (action != permission.getResourceAction().getAction()) { @@ -121,7 +129,7 @@ private boolean permissionCheck(Resource resource, Action action, BasicAuthorize } Pattern resourceNamePattern = permission.getResourceNamePattern(); - Matcher resourceNameMatcher = resourceNamePattern.matcher(resource.getName()); + Matcher resourceNameMatcher = resourceNamePattern.matcher(resource.getName().toString()); return resourceNameMatcher.matches(); } diff --git a/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authorization/db/updater/CoordinatorBasicAuthorizerMetadataStorageUpdater.java b/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authorization/db/updater/CoordinatorBasicAuthorizerMetadataStorageUpdater.java index 01f409adf8ce..c70c259d03ac 100644 --- a/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authorization/db/updater/CoordinatorBasicAuthorizerMetadataStorageUpdater.java +++ b/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authorization/db/updater/CoordinatorBasicAuthorizerMetadataStorageUpdater.java @@ -53,6 +53,7 @@ import org.apache.druid.security.basic.authorization.entity.GroupMappingAndRoleMap; import org.apache.druid.security.basic.authorization.entity.UserAndRoleMap; import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthConfig; import org.apache.druid.server.security.Authorizer; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.Resource; @@ -86,7 +87,9 @@ public class CoordinatorBasicAuthorizerMetadataStorageUpdater implements BasicAu private static final String GROUP_MAPPINGS = "groupMappings"; private static final String ROLES = "roles"; + @Deprecated public static final List SUPERUSER_PERMISSIONS = makeSuperUserPermissions(); + public static final List SUPERUSER_PERMISSIONS_V2 = makeSuperUserPermissionsV2(); private final AuthorizerMapper authorizerMapper; private final MetadataStorageConnector connector; @@ -175,7 +178,8 @@ public void start() initSuperUsersAndGroupMapping(authorizerName, userMap, roleMap, groupMappingMap, dbConfig.getInitialAdminUser(), dbConfig.getInitialAdminRole(), - dbConfig.getInitialAdminGroupMapping() + dbConfig.getInitialAdminGroupMapping(), + authorizerMapper.getAuthVersion() ); } } @@ -1138,17 +1142,26 @@ private void initSuperUsersAndGroupMapping( Map groupMappingMap, String initialAdminUser, String initialAdminRole, - String initialAdminGroupMapping + String initialAdminGroupMapping, + String authVersion ) { if (!roleMap.containsKey(BasicAuthUtils.ADMIN_NAME)) { createRoleInternal(authorizerName, BasicAuthUtils.ADMIN_NAME); - setPermissionsInternal(authorizerName, BasicAuthUtils.ADMIN_NAME, SUPERUSER_PERMISSIONS); + setPermissionsInternal( + authorizerName, + BasicAuthUtils.ADMIN_NAME, + authVersion.equals(AuthConfig.AUTH_VERSION_2) ? SUPERUSER_PERMISSIONS_V2 : SUPERUSER_PERMISSIONS + ); } if (!roleMap.containsKey(BasicAuthUtils.INTERNAL_USER_NAME)) { createRoleInternal(authorizerName, BasicAuthUtils.INTERNAL_USER_NAME); - setPermissionsInternal(authorizerName, BasicAuthUtils.INTERNAL_USER_NAME, SUPERUSER_PERMISSIONS); + setPermissionsInternal( + authorizerName, + BasicAuthUtils.INTERNAL_USER_NAME, + authVersion.equals(AuthConfig.AUTH_VERSION_2) ? SUPERUSER_PERMISSIONS_V2 : SUPERUSER_PERMISSIONS + ); } if (!userMap.containsKey(BasicAuthUtils.ADMIN_NAME)) { @@ -1165,7 +1178,11 @@ private void initSuperUsersAndGroupMapping( && !(initialAdminRole.equals(BasicAuthUtils.ADMIN_NAME) || initialAdminRole.equals(BasicAuthUtils.INTERNAL_USER_NAME)) && !roleMap.containsKey(initialAdminRole)) { createRoleInternal(authorizerName, initialAdminRole); - setPermissionsInternal(authorizerName, initialAdminRole, SUPERUSER_PERMISSIONS); + setPermissionsInternal( + authorizerName, + initialAdminRole, + authVersion.equals(AuthConfig.AUTH_VERSION_2) ? SUPERUSER_PERMISSIONS_V2 : SUPERUSER_PERMISSIONS + ); } if (initialAdminUser != null @@ -1186,6 +1203,7 @@ private void initSuperUsersAndGroupMapping( } } + @Deprecated private static List makeSuperUserPermissions() { ResourceAction datasourceR = new ResourceAction( @@ -1220,4 +1238,49 @@ private static List makeSuperUserPermissions() return Lists.newArrayList(datasourceR, datasourceW, configR, configW, stateR, stateW); } + + private static List makeSuperUserPermissionsV2() + { + ResourceAction datasourceR = new ResourceAction( + new Resource(".*", ResourceType.DATASOURCE), + Action.READ + ); + + ResourceAction datasourceW = new ResourceAction( + new Resource(".*", ResourceType.DATASOURCE), + Action.WRITE + ); + + ResourceAction internalR = new ResourceAction( + new Resource(".*", ResourceType.INTERNAL), + Action.READ + ); + + ResourceAction internalW = new ResourceAction( + new Resource(".*", ResourceType.INTERNAL), + Action.WRITE + ); + + ResourceAction lookupR = new ResourceAction( + new Resource(".*", ResourceType.LOOKUP), + Action.READ + ); + + ResourceAction lookupW = new ResourceAction( + new Resource(".*", ResourceType.LOOKUP), + Action.WRITE + ); + + ResourceAction serverR = new ResourceAction( + new Resource(".*", ResourceType.SERVER), + Action.READ + ); + + ResourceAction serverW = new ResourceAction( + new Resource(".*", ResourceType.SERVER), + Action.WRITE + ); + + return Lists.newArrayList(datasourceR, datasourceW, internalR, internalW, lookupR, lookupW, serverR, serverW); + } } diff --git a/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authorization/endpoint/BasicAuthorizerResource.java b/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authorization/endpoint/BasicAuthorizerResource.java index cb8a9fa2a240..a7bb2447ab9e 100644 --- a/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authorization/endpoint/BasicAuthorizerResource.java +++ b/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authorization/endpoint/BasicAuthorizerResource.java @@ -25,6 +25,7 @@ import org.apache.druid.guice.LazySingleton; import org.apache.druid.security.basic.BasicSecurityResourceFilter; import org.apache.druid.security.basic.authorization.entity.BasicAuthorizerGroupMapping; +import org.apache.druid.server.http.security.ServerServerResourceFilter; import org.apache.druid.server.security.AuthValidator; import org.apache.druid.server.security.ResourceAction; @@ -68,7 +69,7 @@ public BasicAuthorizerResource( @Path("/loadStatus") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response getLoadStatus( @Context HttpServletRequest req ) @@ -86,7 +87,7 @@ public Response getLoadStatus( @Path("/refreshAll") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response refreshAll( @Context HttpServletRequest req ) @@ -104,7 +105,7 @@ public Response refreshAll( @Path("/db/{authorizerName}/users") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response getAllUsers( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName @@ -123,7 +124,7 @@ public Response getAllUsers( @Path("/db/{authorizerName}/groupMappings") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response getAllGroupMappings( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName @@ -143,7 +144,7 @@ public Response getAllGroupMappings( @Path("/db/{authorizerName}/users/{userName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response getUser( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName, @@ -166,7 +167,7 @@ public Response getUser( @Path("/db/{authorizerName}/groupMappings/{groupMappingName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response getGroupMapping( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName, @@ -190,7 +191,7 @@ public Response getGroupMapping( @Path("/db/{authorizerName}/users/{userName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response createUser( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName, @@ -213,7 +214,7 @@ public Response createUser( @Path("/db/{authorizerName}/users/{userName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response deleteUser( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName, @@ -236,7 +237,7 @@ public Response deleteUser( @Path("/db/{authorizerName}/groupMappings/{groupMappingName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response createGroupMapping( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName, @@ -263,7 +264,7 @@ public Response createGroupMapping( @Path("/db/{authorizerName}/groupMappings/{groupMappingName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response deleteGroupMapping( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName, @@ -283,7 +284,7 @@ public Response deleteGroupMapping( @Path("/db/{authorizerName}/roles") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response getAllRoles( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName @@ -305,7 +306,7 @@ public Response getAllRoles( @Path("/db/{authorizerName}/roles/{roleName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response getRole( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName, @@ -330,7 +331,7 @@ public Response getRole( @Path("/db/{authorizerName}/roles/{roleName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response createRole( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName, @@ -353,7 +354,7 @@ public Response createRole( @Path("/db/{authorizerName}/roles/{roleName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response deleteRole( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName, @@ -377,7 +378,7 @@ public Response deleteRole( @Path("/db/{authorizerName}/users/{userName}/roles/{roleName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response assignRoleToUser( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName, @@ -402,7 +403,7 @@ public Response assignRoleToUser( @Path("/db/{authorizerName}/users/{userName}/roles/{roleName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response unassignRoleFromUser( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName, @@ -427,7 +428,7 @@ public Response unassignRoleFromUser( @Path("/db/{authorizerName}/groupMappings/{groupMappingName}/roles/{roleName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response assignRoleToGroupMapping( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName, @@ -452,7 +453,7 @@ public Response assignRoleToGroupMapping( @Path("/db/{authorizerName}/groupMappings/{groupMappingName}/roles/{roleName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response unassignRoleFromGroupMapping( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName, @@ -477,7 +478,7 @@ public Response unassignRoleFromGroupMapping( @Path("/db/{authorizerName}/roles/{roleName}/permissions") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response setRolePermissions( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName, @@ -501,7 +502,7 @@ public Response setRolePermissions( @Path("/db/{authorizerName}/roles/{roleName}/permissions") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response getRolePermissions( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName, @@ -521,7 +522,7 @@ public Response getRolePermissions( @Path("/db/{authorizerName}/cachedSerializedUserMap") @Produces(SmileMediaTypes.APPLICATION_JACKSON_SMILE) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response getCachedSerializedUserMap( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName @@ -540,7 +541,7 @@ public Response getCachedSerializedUserMap( @Path("/db/{authorizerName}/cachedSerializedGroupMappingMap") @Produces(SmileMediaTypes.APPLICATION_JACKSON_SMILE) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response getCachedSerializedGroupMap( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName @@ -560,7 +561,7 @@ public Response getCachedSerializedGroupMap( @Path("/listen/{authorizerName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) @Deprecated public Response authorizerUpdateListener( @Context HttpServletRequest req, @@ -579,7 +580,7 @@ public Response authorizerUpdateListener( @Path("/listen/users/{authorizerName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response authorizerUserUpdateListener( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName, @@ -597,7 +598,7 @@ public Response authorizerUserUpdateListener( @Path("/listen/groupMappings/{authorizerName}") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(BasicSecurityResourceFilter.class) + @ResourceFilters({ BasicSecurityResourceFilter.class, ServerServerResourceFilter.class }) public Response authorizerGroupMappingUpdateListener( @Context HttpServletRequest req, @PathParam("authorizerName") final String authorizerName, diff --git a/extensions-core/druid-basic-security/src/test/java/org/apache/druid/security/authorization/BasicRoleBasedAuthorizerTest.java b/extensions-core/druid-basic-security/src/test/java/org/apache/druid/security/authorization/BasicRoleBasedAuthorizerTest.java index acdb0ab9dd5b..c28ae7028ac1 100644 --- a/extensions-core/druid-basic-security/src/test/java/org/apache/druid/security/authorization/BasicRoleBasedAuthorizerTest.java +++ b/extensions-core/druid-basic-security/src/test/java/org/apache/druid/security/authorization/BasicRoleBasedAuthorizerTest.java @@ -34,6 +34,7 @@ import org.apache.druid.security.basic.authorization.entity.BasicAuthorizerGroupMapping; import org.apache.druid.server.security.Access; import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthConfig; import org.apache.druid.server.security.AuthenticationResult; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.Resource; @@ -44,6 +45,8 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import javax.naming.directory.BasicAttribute; import javax.naming.directory.BasicAttributes; @@ -54,6 +57,7 @@ import java.util.List; import java.util.Map; +@RunWith(Parameterized.class) public class BasicRoleBasedAuthorizerTest { private static final String DB_AUTHORIZER_NAME = "metadata"; @@ -73,6 +77,18 @@ public class BasicRoleBasedAuthorizerTest private SearchResult userSearchResult; private SearchResult adminSearchResult; + private final String authVersion; + + @Parameterized.Parameters(name = "{index}: authVersion={0}") + public static Iterable data() + { + return Arrays.asList(AuthConfig.AUTH_VERSION_1, AuthConfig.AUTH_VERSION_2); + } + + public BasicRoleBasedAuthorizerTest(String authVersion) + { + this.authVersion = authVersion; + } @Before public void setUp() @@ -115,7 +131,7 @@ public void setUp() null, new LDAPRoleProvider(null, groupFilters) ) - ) + ), authVersion ), connector, tablesConfig, @@ -171,14 +187,16 @@ public void testAuth() Access access = authorizer.authorize( authenticationResult, new Resource("testResource", ResourceType.DATASOURCE), - Action.WRITE + Action.WRITE, + authVersion ); Assert.assertTrue(access.isAllowed()); access = authorizer.authorize( authenticationResult, new Resource("wrongResource", ResourceType.DATASOURCE), - Action.WRITE + Action.WRITE, + authVersion ); Assert.assertFalse(access.isAllowed()); } @@ -205,14 +223,16 @@ public void testAuthGroupMapping() Access access = ldapAuthorizer.authorize( authenticationResult, new Resource("testResource", ResourceType.DATASOURCE), - Action.WRITE + Action.WRITE, + authVersion ); Assert.assertTrue(access.isAllowed()); access = ldapAuthorizer.authorize( authenticationResult, new Resource("wrongResource", ResourceType.DATASOURCE), - Action.WRITE + Action.WRITE, + authVersion ); Assert.assertFalse(access.isAllowed()); } @@ -250,21 +270,24 @@ public void testAuthGroupMappingPatternRightMask() Access access = ldapAuthorizer.authorize( authenticationResult, new Resource("testResource", ResourceType.DATASOURCE), - Action.READ + Action.READ, + authVersion ); Assert.assertTrue(access.isAllowed()); access = ldapAuthorizer.authorize( authenticationResult, new Resource("testResource", ResourceType.DATASOURCE), - Action.WRITE + Action.WRITE, + authVersion ); Assert.assertTrue(access.isAllowed()); access = ldapAuthorizer.authorize( authenticationResult, new Resource("wrongResource", ResourceType.DATASOURCE), - Action.WRITE + Action.WRITE, + authVersion ); Assert.assertFalse(access.isAllowed()); @@ -275,21 +298,24 @@ public void testAuthGroupMappingPatternRightMask() access = ldapAuthorizer.authorize( authenticationResult, new Resource("testResource", ResourceType.DATASOURCE), - Action.WRITE + Action.WRITE, + authVersion ); Assert.assertFalse(access.isAllowed()); access = ldapAuthorizer.authorize( authenticationResult, new Resource("testResource", ResourceType.DATASOURCE), - Action.READ + Action.READ, + authVersion ); Assert.assertTrue(access.isAllowed()); access = ldapAuthorizer.authorize( authenticationResult, new Resource("wrongResource", ResourceType.DATASOURCE), - Action.READ + Action.READ, + authVersion ); Assert.assertFalse(access.isAllowed()); } @@ -328,21 +354,24 @@ public void testAuthGroupMappingPatternLeftMask() Access access = ldapAuthorizer.authorize( authenticationResult, new Resource("testResource", ResourceType.DATASOURCE), - Action.READ + Action.READ, + authVersion ); Assert.assertTrue(access.isAllowed()); access = ldapAuthorizer.authorize( authenticationResult, new Resource("testResource", ResourceType.DATASOURCE), - Action.WRITE + Action.WRITE, + authVersion ); Assert.assertTrue(access.isAllowed()); access = ldapAuthorizer.authorize( authenticationResult, new Resource("wrongResource", ResourceType.DATASOURCE), - Action.WRITE + Action.WRITE, + authVersion ); Assert.assertFalse(access.isAllowed()); @@ -353,21 +382,24 @@ public void testAuthGroupMappingPatternLeftMask() access = ldapAuthorizer.authorize( authenticationResult, new Resource("testResource", ResourceType.DATASOURCE), - Action.WRITE + Action.WRITE, + authVersion ); Assert.assertFalse(access.isAllowed()); access = ldapAuthorizer.authorize( authenticationResult, new Resource("testResource", ResourceType.DATASOURCE), - Action.READ + Action.READ, + authVersion ); Assert.assertTrue(access.isAllowed()); access = ldapAuthorizer.authorize( authenticationResult, new Resource("wrongResource", ResourceType.DATASOURCE), - Action.READ + Action.READ, + authVersion ); Assert.assertFalse(access.isAllowed()); } @@ -395,28 +427,32 @@ public void testAuthMissingGroupMapping() Access access = ldapAuthorizer.authorize( authenticationResult, new Resource("testResource", ResourceType.DATASOURCE), - Action.WRITE + Action.WRITE, + authVersion ); Assert.assertFalse(access.isAllowed()); access = ldapAuthorizer.authorize( authenticationResult, new Resource("testResource", ResourceType.DATASOURCE), - Action.READ + Action.READ, + authVersion ); Assert.assertFalse(access.isAllowed()); access = ldapAuthorizer.authorize( authenticationResult, new Resource("wrongResource", ResourceType.DATASOURCE), - Action.WRITE + Action.WRITE, + authVersion ); Assert.assertFalse(access.isAllowed()); access = ldapAuthorizer.authorize( authenticationResult, new Resource("wrongResource", ResourceType.DATASOURCE), - Action.READ + Action.READ, + authVersion ); Assert.assertFalse(access.isAllowed()); } diff --git a/extensions-core/druid-basic-security/src/test/java/org/apache/druid/security/authorization/CoordinatorBasicAuthorizerMetadataStorageUpdaterTest.java b/extensions-core/druid-basic-security/src/test/java/org/apache/druid/security/authorization/CoordinatorBasicAuthorizerMetadataStorageUpdaterTest.java index 7a322acce6e0..bf7f6b4fcbec 100644 --- a/extensions-core/druid-basic-security/src/test/java/org/apache/druid/security/authorization/CoordinatorBasicAuthorizerMetadataStorageUpdaterTest.java +++ b/extensions-core/druid-basic-security/src/test/java/org/apache/druid/security/authorization/CoordinatorBasicAuthorizerMetadataStorageUpdaterTest.java @@ -36,6 +36,7 @@ import org.apache.druid.security.basic.authorization.entity.BasicAuthorizerRole; import org.apache.druid.security.basic.authorization.entity.BasicAuthorizerUser; import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthConfig; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.Resource; import org.apache.druid.server.security.ResourceAction; @@ -45,11 +46,15 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +@RunWith(Parameterized.class) public class CoordinatorBasicAuthorizerMetadataStorageUpdaterTest { private static final String AUTHORIZER_NAME = "test"; @@ -62,18 +67,7 @@ public class CoordinatorBasicAuthorizerMetadataStorageUpdaterTest BasicAuthUtils.INTERNAL_USER_NAME)) ); - private static final Map BASE_ROLE_MAP = ImmutableMap.of( - BasicAuthUtils.ADMIN_NAME, - new BasicAuthorizerRole( - BasicAuthUtils.ADMIN_NAME, - BasicAuthorizerPermission.makePermissionList(CoordinatorBasicAuthorizerMetadataStorageUpdater.SUPERUSER_PERMISSIONS) - ), - BasicAuthUtils.INTERNAL_USER_NAME, - new BasicAuthorizerRole( - BasicAuthUtils.INTERNAL_USER_NAME, - BasicAuthorizerPermission.makePermissionList(CoordinatorBasicAuthorizerMetadataStorageUpdater.SUPERUSER_PERMISSIONS) - ) - ); + private final Map baseRoleMap; @Rule public ExpectedException expectedException = ExpectedException.none(); @@ -83,6 +77,53 @@ public class CoordinatorBasicAuthorizerMetadataStorageUpdaterTest private CoordinatorBasicAuthorizerMetadataStorageUpdater updater; private ObjectMapper objectMapper; + private final AuthorizerMapper authorizerMapper; + + @Parameterized.Parameters(name = "{index}: authVersion={0}") + public static Iterable data() + { + return Arrays.asList(AuthConfig.AUTH_VERSION_1, AuthConfig.AUTH_VERSION_2); + } + + public CoordinatorBasicAuthorizerMetadataStorageUpdaterTest(String authVersion) + { + this.authorizerMapper = new AuthorizerMapper( + ImmutableMap.of( + AUTHORIZER_NAME, + new BasicRoleBasedAuthorizer( + null, + AUTHORIZER_NAME, + null, + null, + null, + null, + null, + null + ) + ), + authVersion + ); + baseRoleMap = ImmutableMap.of( + BasicAuthUtils.ADMIN_NAME, + new BasicAuthorizerRole( + BasicAuthUtils.ADMIN_NAME, + authVersion.equals(AuthConfig.AUTH_VERSION_2) + ? BasicAuthorizerPermission + .makePermissionList(CoordinatorBasicAuthorizerMetadataStorageUpdater.SUPERUSER_PERMISSIONS_V2) + : BasicAuthorizerPermission + .makePermissionList(CoordinatorBasicAuthorizerMetadataStorageUpdater.SUPERUSER_PERMISSIONS) + ), + BasicAuthUtils.INTERNAL_USER_NAME, + new BasicAuthorizerRole( + BasicAuthUtils.INTERNAL_USER_NAME, + authVersion.equals(AuthConfig.AUTH_VERSION_2) + ? BasicAuthorizerPermission + .makePermissionList(CoordinatorBasicAuthorizerMetadataStorageUpdater.SUPERUSER_PERMISSIONS_V2) + : BasicAuthorizerPermission + .makePermissionList(CoordinatorBasicAuthorizerMetadataStorageUpdater.SUPERUSER_PERMISSIONS) + ) + ); + } @Before public void setUp() @@ -93,21 +134,7 @@ public void setUp() connector.createConfigTable(); updater = new CoordinatorBasicAuthorizerMetadataStorageUpdater( - new AuthorizerMapper( - ImmutableMap.of( - AUTHORIZER_NAME, - new BasicRoleBasedAuthorizer( - null, - AUTHORIZER_NAME, - null, - null, - null, - null, - null, - null - ) - ) - ), + authorizerMapper, connector, tablesConfig, new BasicAuthCommonCacheConfig(null, null, null, null), @@ -201,7 +228,7 @@ public void testCreateDuplicateGroupMapping() public void testCreateDeleteRole() { updater.createRole(AUTHORIZER_NAME, "druid"); - Map expectedRoleMap = new HashMap<>(BASE_ROLE_MAP); + Map expectedRoleMap = new HashMap<>(baseRoleMap); expectedRoleMap.put("druid", new BasicAuthorizerRole("druid", ImmutableList.of())); Map actualRoleMap = BasicAuthUtils.deserializeAuthorizerRoleMap( objectMapper, @@ -246,7 +273,7 @@ public void testAddAndRemoveRoleToUser() Map expectedUserMap = new HashMap<>(BASE_USER_MAP); expectedUserMap.put("druid", new BasicAuthorizerUser("druid", ImmutableSet.of("druidRole"))); - Map expectedRoleMap = new HashMap<>(BASE_ROLE_MAP); + Map expectedRoleMap = new HashMap<>(baseRoleMap); expectedRoleMap.put("druidRole", new BasicAuthorizerRole("druidRole", ImmutableList.of())); Map actualUserMap = BasicAuthUtils.deserializeAuthorizerUserMap( @@ -284,7 +311,7 @@ public void testAddAndRemoveRoleToGroupMapping() Map expectedGroupMappingMap = new HashMap<>(); expectedGroupMappingMap.put("druid", new BasicAuthorizerGroupMapping("druid", "CN=test", ImmutableSet.of("druidRole"))); - Map expectedRoleMap = new HashMap<>(BASE_ROLE_MAP); + Map expectedRoleMap = new HashMap<>(baseRoleMap); expectedRoleMap.put("druidRole", new BasicAuthorizerRole("druidRole", ImmutableList.of())); Map actualGroupMappingMap = BasicAuthUtils.deserializeAuthorizerGroupMappingMap( @@ -391,7 +418,7 @@ public void testUnassignInvalidRoleAssignmentToUserFails() Map expectedUserMap = new HashMap<>(BASE_USER_MAP); expectedUserMap.put("druid", new BasicAuthorizerUser("druid", ImmutableSet.of())); - Map expectedRoleMap = new HashMap<>(BASE_ROLE_MAP); + Map expectedRoleMap = new HashMap<>(baseRoleMap); expectedRoleMap.put("druidRole", new BasicAuthorizerRole("druidRole", ImmutableList.of())); Map actualUserMap = BasicAuthUtils.deserializeAuthorizerUserMap( @@ -423,7 +450,7 @@ public void testUnassignInvalidRoleAssignmentToGroupMappingFails() Map expectedGroupMappingMap = new HashMap<>(); expectedGroupMappingMap.put("druid", new BasicAuthorizerGroupMapping("druid", "CN=test", null)); - Map expectedRoleMap = new HashMap<>(BASE_ROLE_MAP); + Map expectedRoleMap = new HashMap<>(baseRoleMap); expectedRoleMap.put("druidRole", new BasicAuthorizerRole("druidRole", ImmutableList.of())); Map actualGroupMappingMap = BasicAuthUtils.deserializeAuthorizerGroupMappingMap( @@ -463,7 +490,7 @@ public void testSetRolePermissions() Map expectedUserMap = new HashMap<>(BASE_USER_MAP); expectedUserMap.put("druid", new BasicAuthorizerUser("druid", ImmutableSet.of("druidRole"))); - Map expectedRoleMap = new HashMap<>(BASE_ROLE_MAP); + Map expectedRoleMap = new HashMap<>(baseRoleMap); expectedRoleMap.put( "druidRole", new BasicAuthorizerRole("druidRole", BasicAuthorizerPermission.makePermissionList(permsToAdd)) diff --git a/extensions-core/druid-basic-security/src/test/java/org/apache/druid/security/authorization/CoordinatorBasicAuthorizerResourceTest.java b/extensions-core/druid-basic-security/src/test/java/org/apache/druid/security/authorization/CoordinatorBasicAuthorizerResourceTest.java index 746d3339f974..3d6f22487f1e 100644 --- a/extensions-core/druid-basic-security/src/test/java/org/apache/druid/security/authorization/CoordinatorBasicAuthorizerResourceTest.java +++ b/extensions-core/druid-basic-security/src/test/java/org/apache/druid/security/authorization/CoordinatorBasicAuthorizerResourceTest.java @@ -135,7 +135,7 @@ public void setUp() null, null ) - ) + ), null ); storageUpdater = new CoordinatorBasicAuthorizerMetadataStorageUpdater( diff --git a/extensions-core/druid-ranger-security/src/test/java/org/apache/druid/security/ranger/authorizer/RangerAuthorizerTest.java b/extensions-core/druid-ranger-security/src/test/java/org/apache/druid/security/ranger/authorizer/RangerAuthorizerTest.java index 46fef6ca77c8..3a6bb7372a34 100644 --- a/extensions-core/druid-ranger-security/src/test/java/org/apache/druid/security/ranger/authorizer/RangerAuthorizerTest.java +++ b/extensions-core/druid-ranger-security/src/test/java/org/apache/druid/security/ranger/authorizer/RangerAuthorizerTest.java @@ -20,6 +20,7 @@ package org.apache.druid.security.ranger.authorizer; import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthConfig; import org.apache.druid.server.security.AuthenticationResult; import org.apache.druid.server.security.Resource; import org.apache.druid.server.security.ResourceType; @@ -48,13 +49,13 @@ public static void setupBeforeClass() @Test public void testOperations() { - Assert.assertTrue(rangerAuthorizer.authorize(alice, aliceDatasource, Action.READ).isAllowed()); - Assert.assertTrue(rangerAuthorizer.authorize(alice, aliceDatasource, Action.READ).isAllowed()); - Assert.assertTrue(rangerAuthorizer.authorize(alice, aliceConfig, Action.READ).isAllowed()); - Assert.assertTrue(rangerAuthorizer.authorize(alice, aliceConfig, Action.WRITE).isAllowed()); - Assert.assertTrue(rangerAuthorizer.authorize(alice, aliceState, Action.READ).isAllowed()); - Assert.assertTrue(rangerAuthorizer.authorize(alice, aliceState, Action.WRITE).isAllowed()); - - Assert.assertFalse(rangerAuthorizer.authorize(bob, aliceDatasource, Action.READ).isAllowed()); + Assert.assertTrue(rangerAuthorizer.authorize(alice, aliceDatasource, Action.READ, AuthConfig.AUTH_VERSION_1).isAllowed()); + Assert.assertTrue(rangerAuthorizer.authorize(alice, aliceDatasource, Action.READ, AuthConfig.AUTH_VERSION_1).isAllowed()); + Assert.assertTrue(rangerAuthorizer.authorize(alice, aliceConfig, Action.READ, AuthConfig.AUTH_VERSION_1).isAllowed()); + Assert.assertTrue(rangerAuthorizer.authorize(alice, aliceConfig, Action.WRITE, AuthConfig.AUTH_VERSION_1).isAllowed()); + Assert.assertTrue(rangerAuthorizer.authorize(alice, aliceState, Action.READ, AuthConfig.AUTH_VERSION_1).isAllowed()); + Assert.assertTrue(rangerAuthorizer.authorize(alice, aliceState, Action.WRITE, AuthConfig.AUTH_VERSION_1).isAllowed()); + + Assert.assertFalse(rangerAuthorizer.authorize(bob, aliceDatasource, Action.READ, AuthConfig.AUTH_VERSION_1).isAllowed()); } } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/hrtr/HttpRemoteTaskRunnerResource.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/hrtr/HttpRemoteTaskRunnerResource.java index fc2c5ced2542..f9a9676bb196 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/hrtr/HttpRemoteTaskRunnerResource.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/hrtr/HttpRemoteTaskRunnerResource.java @@ -24,6 +24,7 @@ import com.sun.jersey.spi.container.ResourceFilters; import org.apache.druid.indexing.overlord.TaskMaster; import org.apache.druid.indexing.overlord.TaskRunner; +import org.apache.druid.server.http.security.InternalInternalResourceFilter; import org.apache.druid.server.http.security.StateResourceFilter; import javax.ws.rs.GET; @@ -38,7 +39,7 @@ * {@link org.apache.druid.indexing.overlord.http.OverlordResource} */ @Path("/druid-internal/v1/httpRemoteTaskRunner") -@ResourceFilters(StateResourceFilter.class) +@ResourceFilters({ StateResourceFilter.class, InternalInternalResourceFilter.class }) public class HttpRemoteTaskRunnerResource { private final TaskMaster taskMaster; diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/OverlordResource.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/OverlordResource.java index bc01e55fe847..ab79ce453643 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/OverlordResource.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/OverlordResource.java @@ -44,7 +44,9 @@ import org.apache.druid.indexer.TaskStatusPlus; import org.apache.druid.indexing.common.actions.TaskActionClient; import org.apache.druid.indexing.common.actions.TaskActionHolder; +import org.apache.druid.indexing.common.task.IndexTask; import org.apache.druid.indexing.common.task.Task; +import org.apache.druid.indexing.input.DruidInputSource; import org.apache.druid.indexing.overlord.IndexerMetadataStorageAdapter; import org.apache.druid.indexing.overlord.TaskMaster; import org.apache.druid.indexing.overlord.TaskQueue; @@ -64,9 +66,13 @@ import org.apache.druid.server.http.HttpMediaType; import org.apache.druid.server.http.security.ConfigResourceFilter; import org.apache.druid.server.http.security.DatasourceResourceFilter; +import org.apache.druid.server.http.security.InternalInternalResourceFilter; +import org.apache.druid.server.http.security.ServerServerResourceFilter; +import org.apache.druid.server.http.security.ServerStatusResourceFilter; import org.apache.druid.server.http.security.StateResourceFilter; import org.apache.druid.server.security.Access; import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthConfig; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.ForbiddenException; @@ -160,14 +166,25 @@ public OverlordResource( public Response taskPost(final Task task, @Context final HttpServletRequest req) { final String dataSource = task.getDataSource(); - final ResourceAction resourceAction = new ResourceAction( + final List resourceActions = new ArrayList<>(); + + resourceActions.add(new ResourceAction( new Resource(dataSource, ResourceType.DATASOURCE), Action.WRITE - ); + )); + + // if its a reindex task from druid, make sure the user has read permissions on the source druid datasource + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2) && task instanceof IndexTask + && ((IndexTask) task).getIngestionSchema().getIOConfig().getInputSource() instanceof DruidInputSource) { + final String readFromDataSource = ((DruidInputSource) ((IndexTask) task).getIngestionSchema() + .getIOConfig() + .getInputSource()).getDataSource(); + resourceActions.add(new ResourceAction(new Resource(readFromDataSource, ResourceType.DATASOURCE), Action.READ)); + } - Access authResult = AuthorizationUtils.authorizeResourceAction( + final Access authResult = AuthorizationUtils.authorizeAllResourceActions( req, - resourceAction, + resourceActions, authorizerMapper ); @@ -203,7 +220,7 @@ public Response apply(TaskQueue taskQueue) @GET @Path("/leader") - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, ServerStatusResourceFilter.class }) @Produces(MediaType.APPLICATION_JSON) public Response getLeader() { @@ -377,7 +394,7 @@ public Response apply(TaskQueue taskQueue) @POST @Path("/taskStatus") @Produces(MediaType.APPLICATION_JSON) - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, InternalInternalResourceFilter.class }) public Response getMultipleTaskStatuses(Set taskIds) { if (taskIds == null || taskIds.size() == 0) { @@ -398,7 +415,7 @@ public Response getMultipleTaskStatuses(Set taskIds) @GET @Path("/worker") @Produces(MediaType.APPLICATION_JSON) - @ResourceFilters(ConfigResourceFilter.class) + @ResourceFilters({ ConfigResourceFilter.class, ServerServerResourceFilter.class }) public Response getWorkerConfig() { if (workerConfigRef == null) { @@ -412,7 +429,7 @@ public Response getWorkerConfig() @POST @Path("/worker") @Consumes(MediaType.APPLICATION_JSON) - @ResourceFilters(ConfigResourceFilter.class) + @ResourceFilters({ ConfigResourceFilter.class, ServerServerResourceFilter.class }) public Response setWorkerConfig( final WorkerBehaviorConfig workerBehaviorConfig, @HeaderParam(AuditManager.X_DRUID_AUTHOR) @DefaultValue("") final String author, @@ -437,7 +454,7 @@ public Response setWorkerConfig( @GET @Path("/worker/history") @Produces(MediaType.APPLICATION_JSON) - @ResourceFilters(ConfigResourceFilter.class) + @ResourceFilters({ ConfigResourceFilter.class, ServerServerResourceFilter.class }) public Response getWorkerConfigHistory( @QueryParam("interval") final String interval, @QueryParam("count") final Integer count @@ -470,7 +487,7 @@ public Response getWorkerConfigHistory( @POST @Path("/action") @Produces(MediaType.APPLICATION_JSON) - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, InternalInternalResourceFilter.class }) public Response doAction(final TaskActionHolder holder) { return asLeaderWith( @@ -711,7 +728,7 @@ public Response killPendingSegments( @GET @Path("/workers") @Produces(MediaType.APPLICATION_JSON) - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, ServerServerResourceFilter.class }) public Response getWorkers() { return asLeaderWith( @@ -741,7 +758,7 @@ public Response apply(TaskRunner taskRunner) @POST @Path("/worker/{host}/enable") @Produces(MediaType.APPLICATION_JSON) - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, ServerServerResourceFilter.class }) public Response enableWorker(@PathParam("host") final String host) { return changeWorkerStatus(host, WorkerTaskRunner.ActionType.ENABLE); @@ -750,7 +767,7 @@ public Response enableWorker(@PathParam("host") final String host) @POST @Path("/worker/{host}/disable") @Produces(MediaType.APPLICATION_JSON) - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, ServerServerResourceFilter.class }) public Response disableWorker(@PathParam("host") final String host) { return changeWorkerStatus(host, WorkerTaskRunner.ActionType.DISABLE); @@ -782,7 +799,7 @@ private Response changeWorkerStatus(String host, WorkerTaskRunner.ActionType act @GET @Path("/scaling") @Produces(MediaType.APPLICATION_JSON) - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, InternalInternalResourceFilter.class }) public Response getScalingState() { // Don't use asLeaderWith, since we want to return 200 instead of 503 when missing an autoscaler. diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/sampler/IndexTaskSamplerSpec.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/sampler/IndexTaskSamplerSpec.java index be73cc7b5509..6282a93b7ecb 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/sampler/IndexTaskSamplerSpec.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/sampler/IndexTaskSamplerSpec.java @@ -93,4 +93,9 @@ public SamplerResponse sample() { return inputSourceSampler.sample(inputSource, inputFormat, dataSchema, samplerConfig); } + + public InputSource getInputSource() + { + return inputSource; + } } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/sampler/SamplerResource.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/sampler/SamplerResource.java index 3b8276cd47ee..4939950b2f61 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/sampler/SamplerResource.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/sampler/SamplerResource.java @@ -20,24 +20,67 @@ package org.apache.druid.indexing.overlord.sampler; import com.google.common.base.Preconditions; +import com.google.inject.Inject; import com.sun.jersey.spi.container.ResourceFilters; +import org.apache.druid.indexing.input.DruidInputSource; import org.apache.druid.server.http.security.StateResourceFilter; +import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthConfig; +import org.apache.druid.server.security.AuthorizationUtils; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.server.security.ForbiddenException; +import org.apache.druid.server.security.Resource; +import org.apache.druid.server.security.ResourceAction; +import org.apache.druid.server.security.ResourceType; +import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; +import java.util.ArrayList; +import java.util.List; @Path("/druid/indexer/v1/sampler") public class SamplerResource { + private final AuthorizerMapper authorizerMapper; + + @Inject + public SamplerResource(AuthorizerMapper authorizerMapper) + { + this.authorizerMapper = authorizerMapper; + } + @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @ResourceFilters(StateResourceFilter.class) - public SamplerResponse post(final SamplerSpec sampler) + public SamplerResponse post(final SamplerSpec sampler, @Context final HttpServletRequest req) { + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + final List resourceActions = new ArrayList<>(); + resourceActions.add(new ResourceAction(Resource.SERVER_USER_RESOURCE, Action.WRITE)); + // make sure the user has read permissions on the druid datasource + if (sampler instanceof IndexTaskSamplerSpec + && ((IndexTaskSamplerSpec) sampler).getInputSource() != null + && ((IndexTaskSamplerSpec) sampler).getInputSource() instanceof DruidInputSource) { + final String dataSource = ((DruidInputSource) ((IndexTaskSamplerSpec) sampler).getInputSource()) + .getDataSource(); + resourceActions.add(new ResourceAction(new Resource(dataSource, ResourceType.DATASOURCE), Action.READ)); + } + final Access authResult = AuthorizationUtils.authorizeAllResourceActions( + req, + resourceActions, + authorizerMapper + ); + if (!authResult.isAllowed()) { + throw new ForbiddenException(authResult.getMessage()); + } + } return Preconditions.checkNotNull(sampler, "Request body cannot be empty").sample(); } } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/worker/http/TaskManagementResource.java b/indexing-service/src/main/java/org/apache/druid/indexing/worker/http/TaskManagementResource.java index 57046ff80a03..e22048dad2e4 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/worker/http/TaskManagementResource.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/worker/http/TaskManagementResource.java @@ -35,6 +35,7 @@ import org.apache.druid.java.util.emitter.EmittingLogger; import org.apache.druid.server.coordination.ChangeRequestHistory; import org.apache.druid.server.coordination.ChangeRequestsSnapshot; +import org.apache.druid.server.http.security.InternalInternalResourceFilter; import org.apache.druid.server.http.security.StateResourceFilter; import javax.servlet.AsyncContext; @@ -57,7 +58,7 @@ * Endpoints used by Overlord to Manage tasks on this Middle Manager. */ @Path("/druid-internal/v1/worker/") -@ResourceFilters(StateResourceFilter.class) +@ResourceFilters({ StateResourceFilter.class, InternalInternalResourceFilter.class }) public class TaskManagementResource { protected static final EmittingLogger log = new EmittingLogger(TaskManagementResource.class); diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/worker/http/WorkerResource.java b/indexing-service/src/main/java/org/apache/druid/indexing/worker/http/WorkerResource.java index 06c414b008dd..2e5a4f2bc842 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/worker/http/WorkerResource.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/worker/http/WorkerResource.java @@ -39,6 +39,7 @@ import org.apache.druid.java.util.common.logger.Logger; import org.apache.druid.server.http.HttpMediaType; import org.apache.druid.server.http.security.ConfigResourceFilter; +import org.apache.druid.server.http.security.InternalInternalResourceFilter; import org.apache.druid.server.http.security.StateResourceFilter; import org.apache.druid.tasklogs.TaskLogStreamer; @@ -95,7 +96,7 @@ public WorkerResource( @POST @Path("/disable") @Produces(MediaType.APPLICATION_JSON) - @ResourceFilters(ConfigResourceFilter.class) + @ResourceFilters({ ConfigResourceFilter.class, InternalInternalResourceFilter.class }) public Response doDisable() { try { @@ -121,7 +122,7 @@ public Response doDisable() @POST @Path("/enable") @Produces(MediaType.APPLICATION_JSON) - @ResourceFilters(ConfigResourceFilter.class) + @ResourceFilters({ ConfigResourceFilter.class, InternalInternalResourceFilter.class }) public Response doEnable() { try { @@ -139,7 +140,7 @@ public Response doEnable() @GET @Path("/enabled") @Produces(MediaType.APPLICATION_JSON) - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, InternalInternalResourceFilter.class }) public Response isEnabled() { try { @@ -153,7 +154,7 @@ public Response isEnabled() @GET @Path("/tasks") @Produces(MediaType.APPLICATION_JSON) - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, InternalInternalResourceFilter.class }) public Response getTasks() { try { @@ -181,7 +182,7 @@ public String apply(TaskRunnerWorkItem input) @POST @Path("/task/{taskid}/shutdown") @Produces(MediaType.APPLICATION_JSON) - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, InternalInternalResourceFilter.class }) public Response doShutdown(@PathParam("taskid") String taskid) { try { @@ -197,7 +198,7 @@ public Response doShutdown(@PathParam("taskid") String taskid) @GET @Path("/task/{taskid}/log") @Produces(HttpMediaType.TEXT_PLAIN_UTF8) - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, InternalInternalResourceFilter.class }) public Response doGetLog( @PathParam("taskid") String taskId, @QueryParam("offset") @DefaultValue("0") long offset diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/worker/shuffle/ShuffleResource.java b/indexing-service/src/main/java/org/apache/druid/indexing/worker/shuffle/ShuffleResource.java index dd885a2ab0d6..4b1064a106a8 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/worker/shuffle/ShuffleResource.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/worker/shuffle/ShuffleResource.java @@ -25,6 +25,7 @@ import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.StringUtils; import org.apache.druid.java.util.common.logger.Logger; +import org.apache.druid.server.http.security.InternalInternalResourceFilter; import org.apache.druid.server.http.security.StateResourceFilter; import org.joda.time.Interval; @@ -54,7 +55,7 @@ * We could develop a new ResourceFileter in the future if needed. */ @Path("/druid/worker/v1/shuffle") -@ResourceFilters(StateResourceFilter.class) +@ResourceFilters({ StateResourceFilter.class, InternalInternalResourceFilter.class }) public class ShuffleResource { private static final Logger log = new Logger(ShuffleResource.class); diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/common/TestUtils.java b/indexing-service/src/test/java/org/apache/druid/indexing/common/TestUtils.java index c36a072b34eb..a831d70ad5d8 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/common/TestUtils.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/common/TestUtils.java @@ -95,7 +95,7 @@ public TestUtils() .addValue(RowIngestionMetersFactory.class, rowIngestionMetersFactory) .addValue(PruneSpecsHolder.class, PruneSpecsHolder.DEFAULT) .addValue(IndexingServiceClient.class, INDEXING_SERVICE_CLIENT) - .addValue(AuthorizerMapper.class, new AuthorizerMapper(ImmutableMap.of())) + .addValue(AuthorizerMapper.class, new AuthorizerMapper(ImmutableMap.of(), null)) .addValue(AppenderatorsManager.class, APPENDERATORS_MANAGER) .addValue(LocalDataSegmentPuller.class, new LocalDataSegmentPuller()) .addValue(IndexTaskClientFactory.class, TASK_CLIENT_FACTORY) diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/batch/parallel/AbstractParallelIndexSupervisorTaskTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/batch/parallel/AbstractParallelIndexSupervisorTaskTest.java index 45b0f383b110..caab0d6e9e04 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/batch/parallel/AbstractParallelIndexSupervisorTaskTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/batch/parallel/AbstractParallelIndexSupervisorTaskTest.java @@ -524,7 +524,7 @@ public void prepareObjectMapper(ObjectMapper objectMapper, IndexIO indexIO) .addValue(AuthorizerMapper.class, null) .addValue(RowIngestionMetersFactory.class, new DropwizardRowIngestionMetersFactory()) .addValue(DataSegment.PruneSpecsHolder.class, DataSegment.PruneSpecsHolder.DEFAULT) - .addValue(AuthorizerMapper.class, new AuthorizerMapper(ImmutableMap.of())) + .addValue(AuthorizerMapper.class, new AuthorizerMapper(ImmutableMap.of(), null)) .addValue(AppenderatorsManager.class, TestUtils.APPENDERATORS_MANAGER) .addValue(LocalDataSegmentPuller.class, new LocalDataSegmentPuller()) .addValue(CoordinatorClient.class, coordinatorClient) diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/OverlordResourceTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/OverlordResourceTest.java index e41a6c9b462e..4d39db6059fe 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/OverlordResourceTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/OverlordResourceTest.java @@ -22,6 +22,7 @@ import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; import com.google.common.util.concurrent.ListenableFuture; import org.apache.druid.indexer.RunnerTaskState; import org.apache.druid.indexer.TaskInfo; @@ -33,8 +34,11 @@ import org.apache.druid.indexing.common.actions.TaskActionClient; import org.apache.druid.indexing.common.config.TaskConfig; import org.apache.druid.indexing.common.task.AbstractTask; +import org.apache.druid.indexing.common.task.IndexTask; import org.apache.druid.indexing.common.task.NoopTask; import org.apache.druid.indexing.common.task.Task; +import org.apache.druid.indexing.common.task.TaskResource; +import org.apache.druid.indexing.input.DruidInputSource; import org.apache.druid.indexing.overlord.IndexerMetadataStorageAdapter; import org.apache.druid.indexing.overlord.TaskMaster; import org.apache.druid.indexing.overlord.TaskQueue; @@ -43,8 +47,10 @@ import org.apache.druid.indexing.overlord.TaskStorageQueryAdapter; import org.apache.druid.indexing.overlord.WorkerTaskRunnerQueryAdapter; import org.apache.druid.java.util.common.DateTimes; +import org.apache.druid.java.util.common.ISE; import org.apache.druid.java.util.common.RE; import org.apache.druid.segment.TestHelper; +import org.apache.druid.segment.indexing.DataSchema; import org.apache.druid.server.security.Access; import org.apache.druid.server.security.Action; import org.apache.druid.server.security.AuthConfig; @@ -53,6 +59,8 @@ import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.ForbiddenException; import org.apache.druid.server.security.Resource; +import org.apache.druid.server.security.ResourceAction; +import org.apache.druid.server.security.ResourceType; import org.easymock.EasyMock; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import org.joda.time.DateTime; @@ -66,15 +74,20 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; +@RunWith(Parameterized.class) public class OverlordResourceTest { private OverlordResource overlordResource; @@ -88,21 +101,18 @@ public class OverlordResourceTest @Rule public ExpectedException expectedException = ExpectedException.none(); - @Before - public void setUp() + @Parameterized.Parameters(name = "{index}: authVersion={0}") + public static Iterable data() { - taskRunner = EasyMock.createMock(TaskRunner.class); - taskMaster = EasyMock.createStrictMock(TaskMaster.class); - taskStorageQueryAdapter = EasyMock.createStrictMock(TaskStorageQueryAdapter.class); - indexerMetadataStorageAdapter = EasyMock.createStrictMock(IndexerMetadataStorageAdapter.class); - req = EasyMock.createStrictMock(HttpServletRequest.class); - workerTaskRunnerQueryAdapter = EasyMock.createStrictMock(WorkerTaskRunnerQueryAdapter.class); + return Arrays.asList(AuthConfig.AUTH_VERSION_1, AuthConfig.AUTH_VERSION_2); + } - EasyMock.expect(taskMaster.getTaskRunner()).andReturn( - Optional.of(taskRunner) - ).anyTimes(); + private AuthorizerMapper authMapper; + private List actualResourceActions = new ArrayList<>(); - AuthorizerMapper authMapper = new AuthorizerMapper(null) + public OverlordResourceTest(String authVersion) + { + authMapper = new AuthorizerMapper(null, authVersion) { @Override public Authorizer getAuthorizer(String name) @@ -112,6 +122,10 @@ public Authorizer getAuthorizer(String name) @Override public Access authorize(AuthenticationResult authenticationResult, Resource resource, Action action) { + if (authVersion.equals(AuthConfig.AUTH_VERSION_2)) { + throw new ISE("Unexpected call"); + } + actualResourceActions.add(new ResourceAction(resource, action)); if (resource.getName().equals("allow")) { return new Access(true); } else { @@ -119,9 +133,37 @@ public Access authorize(AuthenticationResult authenticationResult, Resource reso } } + @Override + public Access authorizeV2(AuthenticationResult authenticationResult, Resource resource, Action action) + { + if (authVersion.equals(AuthConfig.AUTH_VERSION_1)) { + throw new ISE("Unexpected call"); + } + actualResourceActions.add(new ResourceAction(resource, action)); + if (resource.getName().equals("allow")) { + return new Access(true); + } else { + return new Access(false); + } + } }; } }; + } + + @Before + public void setUp() + { + taskRunner = EasyMock.createMock(TaskRunner.class); + taskMaster = EasyMock.createStrictMock(TaskMaster.class); + taskStorageQueryAdapter = EasyMock.createStrictMock(TaskStorageQueryAdapter.class); + indexerMetadataStorageAdapter = EasyMock.createStrictMock(IndexerMetadataStorageAdapter.class); + req = EasyMock.createStrictMock(HttpServletRequest.class); + workerTaskRunnerQueryAdapter = EasyMock.createStrictMock(WorkerTaskRunnerQueryAdapter.class); + + EasyMock.expect(taskMaster.getTaskRunner()).andReturn( + Optional.of(taskRunner) + ).anyTimes(); overlordResource = new OverlordResource( taskMaster, @@ -922,6 +964,52 @@ public void testSecuredTaskPost() overlordResource.taskPost(task, req); } + @Test + public void testSecuredReindexingTaskPost() + { + expectedException.expect(ForbiddenException.class); + expectAuthorizationTokenCheck(); + + DataSchema dataSchema = EasyMock.createMock(DataSchema.class); + DruidInputSource druidInputSource = EasyMock.createMock(DruidInputSource.class); + + EasyMock.expect(dataSchema.getParserMap()).andReturn(null).anyTimes(); + EasyMock.expect(dataSchema.getDataSource()).andReturn("destination").anyTimes(); + EasyMock.expect(druidInputSource.needsFormat()).andReturn(false).anyTimes(); + EasyMock.expect(druidInputSource.getDataSource()).andReturn("source").anyTimes(); + + EasyMock.replay( + taskRunner, + taskMaster, + taskStorageQueryAdapter, + indexerMetadataStorageAdapter, + req, + workerTaskRunnerQueryAdapter, + dataSchema, + druidInputSource + ); + + Task task = new IndexTask( + "id", + new TaskResource("", 1), + new IndexTask.IndexIngestionSpec( + dataSchema, + new IndexTask.IndexIOConfig(null, druidInputSource, null, false), + null + ), + ImmutableMap.of() + ); + final List expectedResourceActions = Lists.newArrayList( + new ResourceAction(new Resource("destination", ResourceType.DATASOURCE), Action.WRITE), + new ResourceAction(new Resource("source", ResourceType.DATASOURCE), Action.READ) + ); + + overlordResource.taskPost(task, req); + Assert.assertEquals(expectedResourceActions, actualResourceActions); + + EasyMock.verify(dataSchema, druidInputSource); + } + @Test public void testKillPendingSegments() { diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/security/OverlordSecurityResourceFilterTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/security/OverlordSecurityResourceFilterTest.java index e603a553e244..80a7fcefc44d 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/security/OverlordSecurityResourceFilterTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/http/security/OverlordSecurityResourceFilterTest.java @@ -52,9 +52,11 @@ public class OverlordSecurityResourceFilterTest extends ResourceFilterTestHelper { private static final Pattern WORD = Pattern.compile("\\w+"); - @Parameterized.Parameters(name = "{index}: requestPath={0}, requestMethod={1}, resourceFilter={2}") + @Parameterized.Parameters(name = "{index}: requestPath={0}, requestMethod={1}, resourceFilter={2}, authVersion={4}, performAuth={5}") public static Collection data() { + ResourceFilterTestHelper.currentFilters.add(TaskResourceFilter.class); + ResourceFilterTestHelper.currentFilters.add(SupervisorResourceFilter.class); return ImmutableList.copyOf( Iterables.concat( getRequestPaths(OverlordResource.class, ImmutableList.of( @@ -79,6 +81,8 @@ public static Collection data() private final ResourceFilter resourceFilter; private final Injector injector; private final Task noopTask = NoopTask.create(); + private final String authVersion; + private final boolean performAuth; private static boolean mockedOnceTsqa; private static boolean mockedOnceSM; @@ -89,13 +93,17 @@ public OverlordSecurityResourceFilterTest( String requestPath, String requestMethod, ResourceFilter resourceFilter, - Injector injector + Injector injector, + String authVersion, + boolean performAuth ) { this.requestPath = requestPath; this.requestMethod = requestMethod; this.resourceFilter = resourceFilter; this.injector = injector; + this.authVersion = authVersion; + this.performAuth = performAuth; } @Before @@ -174,7 +182,7 @@ public String getSource() @Test public void testResourcesFilteringAccess() { - setUpMockExpectations(requestPath, true, requestMethod); + setUpMockExpectations(requestPath, true, requestMethod, authVersion, performAuth); EasyMock.expect(request.getEntity(Task.class)).andReturn(noopTask).anyTimes(); // As request object is a strict mock the ordering of expected calls matters // therefore adding the expectation below again as getEntity is called before getMethod @@ -186,11 +194,15 @@ public void testResourcesFilteringAccess() @Test(expected = ForbiddenException.class) public void testDatasourcesResourcesFilteringNoAccess() { - setUpMockExpectations(requestPath, false, requestMethod); + setUpMockExpectations(requestPath, false, requestMethod, authVersion, performAuth); EasyMock.expect(request.getEntity(Task.class)).andReturn(noopTask).anyTimes(); EasyMock.replay(req, request, authorizerMapper); try { resourceFilter.getRequestFilter().filter(request); + if (!performAuth) { + // if auth does not need to be performed, exception will not be thrown so do it manually + throw new ForbiddenException(); + } } catch (ForbiddenException e) { throw e; diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/sampler/SamplerResourceTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/sampler/SamplerResourceTest.java new file mode 100644 index 000000000000..47c97dc26d41 --- /dev/null +++ b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/sampler/SamplerResourceTest.java @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.druid.indexing.overlord.sampler; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import org.apache.druid.indexing.input.DruidInputSource; +import org.apache.druid.java.util.common.ISE; +import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthConfig; +import org.apache.druid.server.security.AuthenticationResult; +import org.apache.druid.server.security.Authorizer; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.server.security.Resource; +import org.apache.druid.server.security.ResourceAction; +import org.apache.druid.server.security.ResourceType; +import org.easymock.EasyMock; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SamplerResourceTest +{ + private AuthorizerMapper authorizerMapper; + private List actualResourceActions; + private SamplerResource samplerResource; + private IndexTaskSamplerSpec samplerSpec; + private HttpServletRequest request; + + @Before + public void setUp() + { + actualResourceActions = new ArrayList<>(); + authorizerMapper = new AuthorizerMapper( + ImmutableMap.of( + "auth1", + new Authorizer() + { + @Override + public Access authorize(AuthenticationResult authenticationResult, Resource resource, Action action) + { + throw new ISE("Unexpected call"); + } + + @Override + public Access authorizeV2(AuthenticationResult authenticationResult, Resource resource, Action action) + { + actualResourceActions.add(new ResourceAction(resource, action)); + return new Access(true); + } + } + ), AuthConfig.AUTH_VERSION_2); + samplerResource = new SamplerResource(authorizerMapper); + + request = EasyMock.createMock(HttpServletRequest.class); + EasyMock.expect(request.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH)).andReturn(null).once(); + EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).once(); + EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)) + .andReturn(new AuthenticationResult("", "auth1", "", null)).once(); + request.setAttribute(EasyMock.anyString(), EasyMock.anyObject()); + EasyMock.expectLastCall().once(); + EasyMock.replay(request); + } + + @After + public void tearDown() + { + EasyMock.verify(samplerSpec, request); + } + + @Test + public void testPost() + { + samplerSpec = EasyMock.createMock(IndexTaskSamplerSpec.class); + EasyMock.expect(samplerSpec.getInputSource()).andReturn(null).once(); + EasyMock.expect(samplerSpec.sample()).andReturn(new SamplerResponse(0, 0, null)).once(); + EasyMock.replay(samplerSpec); + List expectedResourceActions = Collections + .singletonList(new ResourceAction(Resource.SERVER_USER_RESOURCE, Action.WRITE)); + samplerResource.post(samplerSpec, request); + Assert.assertEquals(expectedResourceActions, actualResourceActions); + } + + @Test + public void testPostReindexSampler() + { + samplerSpec = EasyMock.createMock(IndexTaskSamplerSpec.class); + DruidInputSource inputSource = EasyMock.createMock(DruidInputSource.class); + EasyMock.expect(samplerSpec.getInputSource()).andReturn(inputSource).anyTimes(); + EasyMock.expect(inputSource.getDataSource()).andReturn("source").once(); + EasyMock.expect(samplerSpec.sample()).andReturn(new SamplerResponse(0, 0, null)).once(); + EasyMock.replay(samplerSpec, inputSource); + List expectedResourceActions = Lists.newArrayList( + new ResourceAction(Resource.SERVER_USER_RESOURCE, Action.WRITE), + new ResourceAction(new Resource("source", ResourceType.DATASOURCE), Action.READ) + ); + samplerResource.post(samplerSpec, request); + Assert.assertEquals(expectedResourceActions, actualResourceActions); + EasyMock.verify(inputSource); + } +} diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResourceTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResourceTest.java index 1390e7acc98c..710791f217e1 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResourceTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResourceTest.java @@ -86,7 +86,7 @@ public void setUp() { supervisorResource = new SupervisorResource( taskMaster, - new AuthorizerMapper(null) + new AuthorizerMapper(null, null) { @Override public Authorizer getAuthorizer(String name) diff --git a/server/src/main/java/org/apache/druid/client/HttpServerInventoryViewResource.java b/server/src/main/java/org/apache/druid/client/HttpServerInventoryViewResource.java index cda761e6f2bc..b40c64b5d5af 100644 --- a/server/src/main/java/org/apache/druid/client/HttpServerInventoryViewResource.java +++ b/server/src/main/java/org/apache/druid/client/HttpServerInventoryViewResource.java @@ -21,6 +21,7 @@ import com.google.inject.Inject; import com.sun.jersey.spi.container.ResourceFilters; +import org.apache.druid.server.http.security.InternalInternalResourceFilter; import org.apache.druid.server.http.security.StateResourceFilter; import javax.ws.rs.GET; @@ -33,7 +34,7 @@ * Collection of http endpoits to introspect state of HttpServerInventoryView instance for debugging. */ @Path("/druid-internal/v1/httpServerInventoryView") -@ResourceFilters(StateResourceFilter.class) +@ResourceFilters({ StateResourceFilter.class, InternalInternalResourceFilter.class }) public class HttpServerInventoryViewResource { private final HttpServerInventoryView httpServerInventoryView; diff --git a/server/src/main/java/org/apache/druid/query/lookup/LookupIntrospectionResource.java b/server/src/main/java/org/apache/druid/query/lookup/LookupIntrospectionResource.java index 8ccce127f0c0..1d409d8b14d8 100644 --- a/server/src/main/java/org/apache/druid/query/lookup/LookupIntrospectionResource.java +++ b/server/src/main/java/org/apache/druid/query/lookup/LookupIntrospectionResource.java @@ -23,7 +23,16 @@ import com.sun.jersey.spi.container.ResourceFilters; import org.apache.druid.java.util.common.logger.Logger; import org.apache.druid.server.http.security.ConfigResourceFilter; +import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthConfig; +import org.apache.druid.server.security.AuthorizationUtils; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.server.security.Resource; +import org.apache.druid.server.security.ResourceAction; +import org.apache.druid.server.security.ResourceType; +import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.core.Context; @@ -37,18 +46,31 @@ public class LookupIntrospectionResource private static final Logger LOGGER = new Logger(LookupIntrospectionResource.class); private final LookupExtractorFactoryContainerProvider lookupExtractorFactoryContainerProvider; + private AuthorizerMapper authorizerMapper; @Inject public LookupIntrospectionResource( - @Context LookupExtractorFactoryContainerProvider lookupExtractorFactoryContainerProvider + @Context LookupExtractorFactoryContainerProvider lookupExtractorFactoryContainerProvider, + AuthorizerMapper authorizerMapper ) { this.lookupExtractorFactoryContainerProvider = lookupExtractorFactoryContainerProvider; + this.authorizerMapper = authorizerMapper; } @Path("/{lookupId}") - public Object introspectLookup(@PathParam("lookupId") final String lookupId) + public Object introspectLookup(@PathParam("lookupId") final String lookupId, @Context HttpServletRequest request) { + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + final Access access = AuthorizationUtils.authorizeResourceAction(request, + new ResourceAction(new Resource(lookupId, ResourceType.LOOKUP), Action.READ), + authorizerMapper + ); + if (!access.isAllowed()) { + return Response.status(Response.Status.FORBIDDEN).entity(access.getMessage()).build(); + } + } + final Optional maybeContainer = lookupExtractorFactoryContainerProvider.get(lookupId); diff --git a/server/src/main/java/org/apache/druid/query/lookup/LookupListeningResource.java b/server/src/main/java/org/apache/druid/query/lookup/LookupListeningResource.java index dd3f880583c6..5eb967764467 100644 --- a/server/src/main/java/org/apache/druid/query/lookup/LookupListeningResource.java +++ b/server/src/main/java/org/apache/druid/query/lookup/LookupListeningResource.java @@ -29,6 +29,7 @@ import org.apache.druid.guice.annotations.Smile; import org.apache.druid.java.util.common.logger.Logger; import org.apache.druid.server.http.security.ConfigResourceFilter; +import org.apache.druid.server.http.security.InternalInternalResourceFilter; import org.apache.druid.server.listener.resource.AbstractListenerHandler; import org.apache.druid.server.listener.resource.ListenerResource; import org.apache.druid.server.lookup.cache.LookupCoordinatorManager; @@ -41,7 +42,7 @@ import java.util.Map; @Path(ListenerResource.BASE_PATH + "/" + LookupCoordinatorManager.LOOKUP_LISTEN_ANNOUNCE_KEY) -@ResourceFilters(ConfigResourceFilter.class) +@ResourceFilters({ ConfigResourceFilter.class, InternalInternalResourceFilter.class }) class LookupListeningResource extends ListenerResource { private static final Logger LOG = new Logger(LookupListeningResource.class); diff --git a/server/src/main/java/org/apache/druid/server/BrokerQueryResource.java b/server/src/main/java/org/apache/druid/server/BrokerQueryResource.java index 9ef86af87330..bc7d80f592d1 100644 --- a/server/src/main/java/org/apache/druid/server/BrokerQueryResource.java +++ b/server/src/main/java/org/apache/druid/server/BrokerQueryResource.java @@ -29,6 +29,7 @@ import org.apache.druid.guice.annotations.Self; import org.apache.druid.guice.annotations.Smile; import org.apache.druid.query.Query; +import org.apache.druid.server.http.security.InternalInternalResourceFilter; import org.apache.druid.server.http.security.StateResourceFilter; import org.apache.druid.server.security.AuthConfig; import org.apache.druid.server.security.AuthorizerMapper; @@ -83,7 +84,7 @@ public BrokerQueryResource( @Path("/candidates") @Produces({MediaType.APPLICATION_JSON, SmileMediaTypes.APPLICATION_JACKSON_SMILE}) @Consumes({MediaType.APPLICATION_JSON, SmileMediaTypes.APPLICATION_JACKSON_SMILE, APPLICATION_SMILE}) - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, InternalInternalResourceFilter.class }) public Response getQueryTargets( InputStream in, @QueryParam("pretty") String pretty, diff --git a/server/src/main/java/org/apache/druid/server/QueryLifecycle.java b/server/src/main/java/org/apache/druid/server/QueryLifecycle.java index cf6cde5081c9..23e76663340e 100644 --- a/server/src/main/java/org/apache/druid/server/QueryLifecycle.java +++ b/server/src/main/java/org/apache/druid/server/QueryLifecycle.java @@ -35,6 +35,7 @@ import org.apache.druid.query.DefaultQueryConfig; import org.apache.druid.query.DruidMetrics; import org.apache.druid.query.GenericQueryMetricsFactory; +import org.apache.druid.query.LookupDataSource; import org.apache.druid.query.Query; import org.apache.druid.query.QueryInterruptedException; import org.apache.druid.query.QueryMetrics; @@ -45,12 +46,18 @@ import org.apache.druid.query.context.ResponseContext; import org.apache.druid.server.log.RequestLogger; import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthConfig; import org.apache.druid.server.security.AuthenticationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.server.security.Resource; +import org.apache.druid.server.security.ResourceAction; +import org.apache.druid.server.security.ResourceType; import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; @@ -82,6 +89,7 @@ public class QueryLifecycle private final DefaultQueryConfig defaultQueryConfig; private final long startMs; private final long startNs; + private final AuthConfig authConfig; private State state = State.NEW; private AuthenticationResult authenticationResult; @@ -97,7 +105,8 @@ public QueryLifecycle( final AuthorizerMapper authorizerMapper, final DefaultQueryConfig defaultQueryConfig, final long startMs, - final long startNs + final long startNs, + final AuthConfig authConfig ) { this.warehouse = warehouse; @@ -109,6 +118,7 @@ public QueryLifecycle( this.defaultQueryConfig = defaultQueryConfig; this.startMs = startMs; this.startNs = startNs; + this.authConfig = authConfig; } /** @@ -196,6 +206,23 @@ public void initialize(final Query baseQuery) public Access authorize(final AuthenticationResult authenticationResult) { transition(State.INITIALIZED, State.AUTHORIZING); + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + return doAuthorize( + authenticationResult, + AuthorizationUtils.authorizeAllResourceActions( + authenticationResult, + baseQuery.getDataSource() instanceof LookupDataSource ? + Collections.singleton(new ResourceAction(new Resource( + ((LookupDataSource) baseQuery.getDataSource()).getLookupName(), + ResourceType.LOOKUP + ), Action.READ)) : Iterables.transform( + baseQuery.getDataSource().getTableNames(), + AuthorizationUtils.DATASOURCE_READ_RA_GENERATOR + ), + authorizerMapper + ) + ); + } return doAuthorize( authenticationResult, AuthorizationUtils.authorizeAllResourceActions( diff --git a/server/src/main/java/org/apache/druid/server/QueryLifecycleFactory.java b/server/src/main/java/org/apache/druid/server/QueryLifecycleFactory.java index 106ffe9e5e9c..3b001f49f3e0 100644 --- a/server/src/main/java/org/apache/druid/server/QueryLifecycleFactory.java +++ b/server/src/main/java/org/apache/druid/server/QueryLifecycleFactory.java @@ -41,6 +41,7 @@ public class QueryLifecycleFactory private final RequestLogger requestLogger; private final AuthorizerMapper authorizerMapper; private final DefaultQueryConfig defaultQueryConfig; + private final AuthConfig authConfig; @Inject public QueryLifecycleFactory( @@ -61,6 +62,7 @@ public QueryLifecycleFactory( this.requestLogger = requestLogger; this.authorizerMapper = authorizerMapper; this.defaultQueryConfig = queryConfigSupplier.get(); + this.authConfig = authConfig; } public QueryLifecycle factorize() @@ -74,7 +76,8 @@ public QueryLifecycle factorize() authorizerMapper, defaultQueryConfig, System.currentTimeMillis(), - System.nanoTime() + System.nanoTime(), + authConfig ); } } diff --git a/server/src/main/java/org/apache/druid/server/StatusResource.java b/server/src/main/java/org/apache/druid/server/StatusResource.java index cf457f14ee3b..21bde41aad93 100644 --- a/server/src/main/java/org/apache/druid/server/StatusResource.java +++ b/server/src/main/java/org/apache/druid/server/StatusResource.java @@ -28,6 +28,8 @@ import org.apache.druid.initialization.Initialization; import org.apache.druid.java.util.common.StringUtils; import org.apache.druid.server.http.security.ConfigResourceFilter; +import org.apache.druid.server.http.security.ServerServerResourceFilter; +import org.apache.druid.server.http.security.ServerStatusResourceFilter; import org.apache.druid.server.http.security.StateResourceFilter; import org.apache.druid.utils.JvmUtils; import org.apache.druid.utils.RuntimeInfo; @@ -64,7 +66,7 @@ public StatusResource(Properties properties, DruidServerConfig druidServerConfig @GET @Path("/properties") - @ResourceFilters(ConfigResourceFilter.class) + @ResourceFilters({ ConfigResourceFilter.class, ServerServerResourceFilter.class }) @Produces(MediaType.APPLICATION_JSON) public Map getProperties() { @@ -74,7 +76,7 @@ public Map getProperties() } @GET - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, ServerStatusResourceFilter.class }) @Produces(MediaType.APPLICATION_JSON) public Status doGet( @Context final HttpServletRequest req diff --git a/server/src/main/java/org/apache/druid/server/http/BrokerResource.java b/server/src/main/java/org/apache/druid/server/http/BrokerResource.java index e50b04edf8f0..b45154adae85 100644 --- a/server/src/main/java/org/apache/druid/server/http/BrokerResource.java +++ b/server/src/main/java/org/apache/druid/server/http/BrokerResource.java @@ -23,6 +23,7 @@ import com.google.inject.Inject; import com.sun.jersey.spi.container.ResourceFilters; import org.apache.druid.client.BrokerServerView; +import org.apache.druid.server.http.security.ServerStatusResourceFilter; import org.apache.druid.server.http.security.StateResourceFilter; import javax.ws.rs.GET; @@ -44,7 +45,7 @@ public BrokerResource(BrokerServerView brokerServerView) @GET @Path("/loadstatus") - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, ServerStatusResourceFilter.class }) @Produces(MediaType.APPLICATION_JSON) public Response getLoadStatus() { diff --git a/server/src/main/java/org/apache/druid/server/http/ClusterResource.java b/server/src/main/java/org/apache/druid/server/http/ClusterResource.java index eabf51ec115e..779cc96939f8 100644 --- a/server/src/main/java/org/apache/druid/server/http/ClusterResource.java +++ b/server/src/main/java/org/apache/druid/server/http/ClusterResource.java @@ -31,6 +31,7 @@ import org.apache.druid.discovery.NodeRole; import org.apache.druid.guice.LazySingleton; import org.apache.druid.server.DruidNode; +import org.apache.druid.server.http.security.InternalInternalResourceFilter; import org.apache.druid.server.http.security.StateResourceFilter; import javax.ws.rs.GET; @@ -47,7 +48,7 @@ */ @Path("/druid/coordinator/v1/cluster") @LazySingleton -@ResourceFilters(StateResourceFilter.class) +@ResourceFilters({ StateResourceFilter.class, InternalInternalResourceFilter.class }) public class ClusterResource { private final DruidNodeDiscoveryProvider druidNodeDiscoveryProvider; diff --git a/server/src/main/java/org/apache/druid/server/http/CompactionResource.java b/server/src/main/java/org/apache/druid/server/http/CompactionResource.java index 8c995144d8f9..1da42fb2b201 100644 --- a/server/src/main/java/org/apache/druid/server/http/CompactionResource.java +++ b/server/src/main/java/org/apache/druid/server/http/CompactionResource.java @@ -27,28 +27,45 @@ import org.apache.druid.server.coordinator.AutoCompactionSnapshot; import org.apache.druid.server.coordinator.DruidCoordinator; import org.apache.druid.server.http.security.ConfigResourceFilter; +import org.apache.druid.server.http.security.InternalInternalResourceFilter; import org.apache.druid.server.http.security.StateResourceFilter; +import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthConfig; +import org.apache.druid.server.security.AuthorizationUtils; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.server.security.ForbiddenException; +import org.apache.druid.server.security.Resource; +import org.apache.druid.server.security.ResourceAction; +import org.apache.druid.server.security.ResourceType; +import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; @Path("/druid/coordinator/v1/compaction") public class CompactionResource { private final DruidCoordinator coordinator; + private final AuthorizerMapper authorizerMapper; @Inject public CompactionResource( - DruidCoordinator coordinator + DruidCoordinator coordinator, + AuthorizerMapper authorizerMapper ) { this.coordinator = coordinator; + this.authorizerMapper = authorizerMapper; } /** @@ -56,7 +73,7 @@ public CompactionResource( */ @POST @Path("/compact") - @ResourceFilters(ConfigResourceFilter.class) + @ResourceFilters({ ConfigResourceFilter.class, InternalInternalResourceFilter.class }) @VisibleForTesting public Response forceTriggerCompaction() { @@ -69,9 +86,22 @@ public Response forceTriggerCompaction() @Produces(MediaType.APPLICATION_JSON) @ResourceFilters(StateResourceFilter.class) public Response getCompactionProgress( - @QueryParam("dataSource") String dataSource + @QueryParam("dataSource") String dataSource, + @Context HttpServletRequest request ) { + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + final Access authResult = AuthorizationUtils.authorizeResourceAction( + request, + new ResourceAction(new Resource(dataSource, ResourceType.DATASOURCE), Action.READ), + authorizerMapper + ); + + if (!authResult.isAllowed()) { + throw new ForbiddenException(authResult.getMessage()); + } + } + final Long notCompactedSegmentSizeBytes = coordinator.getTotalSizeOfSegmentsAwaitingCompaction(dataSource); if (notCompactedSegmentSizeBytes == null) { return Response.status(Response.Status.BAD_REQUEST).entity(ImmutableMap.of("error", "unknown dataSource")).build(); @@ -85,13 +115,37 @@ public Response getCompactionProgress( @Produces(MediaType.APPLICATION_JSON) @ResourceFilters(StateResourceFilter.class) public Response getCompactionSnapshotForDataSource( - @QueryParam("dataSource") String dataSource + @QueryParam("dataSource") String dataSource, + @Context HttpServletRequest request ) { final Collection snapshots; if (dataSource == null || dataSource.isEmpty()) { - snapshots = coordinator.getAutoCompactionSnapshot().values(); + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + snapshots = new HashSet<>(); + AuthorizationUtils.filterAuthorizedResources( + request, + coordinator.getAutoCompactionSnapshot().values(), + input -> Collections.singleton( + new ResourceAction(new Resource(input.getDataSource(), ResourceType.DATASOURCE), Action.READ)), + authorizerMapper + ).forEach(snapshots::add); + } else { + snapshots = coordinator.getAutoCompactionSnapshot().values(); + } } else { + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + final Access authResult = AuthorizationUtils.authorizeResourceAction( + request, + new ResourceAction(new Resource(dataSource, ResourceType.DATASOURCE), Action.READ), + authorizerMapper + ); + + if (!authResult.isAllowed()) { + throw new ForbiddenException(authResult.getMessage()); + } + } + AutoCompactionSnapshot autoCompactionSnapshot = coordinator.getAutoCompactionSnapshotForDataSource(dataSource); if (autoCompactionSnapshot == null) { return Response.status(Response.Status.BAD_REQUEST).entity(ImmutableMap.of("error", "unknown dataSource")).build(); diff --git a/server/src/main/java/org/apache/druid/server/http/CoordinatorCompactionConfigsResource.java b/server/src/main/java/org/apache/druid/server/http/CoordinatorCompactionConfigsResource.java index da66b333fcb8..eac1d32d67bb 100644 --- a/server/src/main/java/org/apache/druid/server/http/CoordinatorCompactionConfigsResource.java +++ b/server/src/main/java/org/apache/druid/server/http/CoordinatorCompactionConfigsResource.java @@ -21,6 +21,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; import com.google.inject.Inject; import com.sun.jersey.spi.container.ResourceFilters; import org.apache.druid.audit.AuditInfo; @@ -30,6 +31,14 @@ import org.apache.druid.server.coordinator.CoordinatorCompactionConfig; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.apache.druid.server.http.security.ConfigResourceFilter; +import org.apache.druid.server.http.security.ServerServerResourceFilter; +import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthConfig; +import org.apache.druid.server.security.AuthorizationUtils; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.server.security.Resource; +import org.apache.druid.server.security.ResourceAction; +import org.apache.druid.server.security.ResourceType; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.Consumes; @@ -45,32 +54,45 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.util.Collections; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; @Path("/druid/coordinator/v1/config/compaction") -@ResourceFilters(ConfigResourceFilter.class) public class CoordinatorCompactionConfigsResource { private final JacksonConfigManager manager; + private final AuthorizerMapper authorizerMapper; @Inject - public CoordinatorCompactionConfigsResource(JacksonConfigManager manager) + public CoordinatorCompactionConfigsResource( + JacksonConfigManager manager, + AuthorizerMapper authorizerMapper + ) { this.manager = manager; + this.authorizerMapper = authorizerMapper; } @GET @Produces(MediaType.APPLICATION_JSON) - public Response getCompactionConfig() + @ResourceFilters(ConfigResourceFilter.class) + public Response getCompactionConfig(@Context HttpServletRequest request) { - return Response.ok(CoordinatorCompactionConfig.current(manager)).build(); + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + return Response + .ok(getFilteredConfigsByDatasource(CoordinatorCompactionConfig.current(manager), request, Action.READ)) + .build(); + } else { + return Response.ok(CoordinatorCompactionConfig.current(manager)).build(); + } } @POST @Path("/taskslots") @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters({ ConfigResourceFilter.class, ServerServerResourceFilter.class }) public Response setCompactionTaskLimit( @QueryParam("ratio") Double compactionTaskSlotRatio, @QueryParam("max") Integer maxCompactionTaskSlots, @@ -104,6 +126,7 @@ public Response setCompactionTaskLimit( @POST @Consumes(MediaType.APPLICATION_JSON) + @ResourceFilters(ConfigResourceFilter.class) public Response addOrUpdateCompactionConfig( final DataSourceCompactionConfig newConfig, @HeaderParam(AuditManager.X_DRUID_AUTHOR) @DefaultValue("") final String author, @@ -111,6 +134,12 @@ public Response addOrUpdateCompactionConfig( @Context HttpServletRequest req ) { + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + if (!AuthorizationUtils.authorizeResourceAction(req, AuthorizationUtils.DATASOURCE_WRITE_RA_GENERATOR.apply( + newConfig.getDataSource()), authorizerMapper).isAllowed()) { + return Response.status(Response.Status.FORBIDDEN).build(); + } + } final CoordinatorCompactionConfig current = CoordinatorCompactionConfig.current(manager); final CoordinatorCompactionConfig newCompactionConfig; final Map newConfigs = current @@ -136,8 +165,18 @@ public Response addOrUpdateCompactionConfig( @GET @Path("/{dataSource}") @Produces(MediaType.APPLICATION_JSON) - public Response getCompactionConfig(@PathParam("dataSource") String dataSource) + @ResourceFilters(ConfigResourceFilter.class) + public Response getCompactionConfig(@PathParam("dataSource") String dataSource, @Context HttpServletRequest req) { + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + if (!AuthorizationUtils.authorizeResourceAction( + req, + AuthorizationUtils.DATASOURCE_READ_RA_GENERATOR.apply(dataSource), + authorizerMapper + ).isAllowed()) { + return Response.status(Response.Status.FORBIDDEN).build(); + } + } final CoordinatorCompactionConfig current = CoordinatorCompactionConfig.current(manager); final Map configs = current .getCompactionConfigs() @@ -155,6 +194,7 @@ public Response getCompactionConfig(@PathParam("dataSource") String dataSource) @DELETE @Path("/{dataSource}") @Produces(MediaType.APPLICATION_JSON) + @ResourceFilters(ConfigResourceFilter.class) public Response deleteCompactionConfig( @PathParam("dataSource") String dataSource, @HeaderParam(AuditManager.X_DRUID_AUTHOR) @DefaultValue("") final String author, @@ -162,6 +202,15 @@ public Response deleteCompactionConfig( @Context HttpServletRequest req ) { + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + if (!AuthorizationUtils.authorizeResourceAction( + req, + AuthorizationUtils.DATASOURCE_WRITE_RA_GENERATOR.apply(dataSource), + authorizerMapper + ).isAllowed()) { + return Response.status(Response.Status.FORBIDDEN).build(); + } + } final CoordinatorCompactionConfig current = CoordinatorCompactionConfig.current(manager); final Map configs = current .getCompactionConfigs() @@ -185,4 +234,28 @@ public Response deleteCompactionConfig( return Response.status(Response.Status.BAD_REQUEST).build(); } } + + private CoordinatorCompactionConfig getFilteredConfigsByDatasource( + CoordinatorCompactionConfig compactionConfig, + HttpServletRequest request, + Action action + ) + { + final Iterable dataSourceCompactionConfigs = AuthorizationUtils + .filterAuthorizedResources(request, compactionConfig.getCompactionConfigs(), + input -> Collections + .singleton(new ResourceAction(new Resource(input.getDataSource(), ResourceType.DATASOURCE), action)), + authorizerMapper + ); + // add base compaction configs, if user has server server permissions + if (AuthorizationUtils + .authorizeAllResourceActions( + AuthorizationUtils.authenticationResultFromRequest(request), + Collections.singleton(new ResourceAction(Resource.SERVER_SERVER_RESOURCE, action)), + authorizerMapper + ).isAllowed()) { + return CoordinatorCompactionConfig.from(compactionConfig, Lists.newArrayList(dataSourceCompactionConfigs)); + } + return CoordinatorCompactionConfig.from(Lists.newArrayList(dataSourceCompactionConfigs)); + } } diff --git a/server/src/main/java/org/apache/druid/server/http/CoordinatorDynamicConfigsResource.java b/server/src/main/java/org/apache/druid/server/http/CoordinatorDynamicConfigsResource.java index 488b0e705722..ae24fac2ee77 100644 --- a/server/src/main/java/org/apache/druid/server/http/CoordinatorDynamicConfigsResource.java +++ b/server/src/main/java/org/apache/druid/server/http/CoordinatorDynamicConfigsResource.java @@ -28,6 +28,7 @@ import org.apache.druid.java.util.common.Intervals; import org.apache.druid.server.coordinator.CoordinatorDynamicConfig; import org.apache.druid.server.http.security.ConfigResourceFilter; +import org.apache.druid.server.http.security.ServerServerResourceFilter; import org.joda.time.Interval; import javax.inject.Inject; @@ -47,7 +48,7 @@ /** */ @Path("/druid/coordinator/v1/config") -@ResourceFilters(ConfigResourceFilter.class) +@ResourceFilters({ ConfigResourceFilter.class, ServerServerResourceFilter.class }) public class CoordinatorDynamicConfigsResource { private final JacksonConfigManager manager; diff --git a/server/src/main/java/org/apache/druid/server/http/CoordinatorResource.java b/server/src/main/java/org/apache/druid/server/http/CoordinatorResource.java index 92543e6a9c75..960c98ebc763 100644 --- a/server/src/main/java/org/apache/druid/server/http/CoordinatorResource.java +++ b/server/src/main/java/org/apache/druid/server/http/CoordinatorResource.java @@ -27,6 +27,8 @@ import com.sun.jersey.spi.container.ResourceFilters; import org.apache.druid.server.coordinator.DruidCoordinator; import org.apache.druid.server.coordinator.LoadQueuePeon; +import org.apache.druid.server.http.security.ServerServerResourceFilter; +import org.apache.druid.server.http.security.ServerStatusResourceFilter; import org.apache.druid.server.http.security.StateResourceFilter; import org.apache.druid.timeline.DataSegment; @@ -55,7 +57,7 @@ public CoordinatorResource( @GET @Path("/leader") - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, ServerStatusResourceFilter.class }) @Produces(MediaType.APPLICATION_JSON) public Response getLeader() { @@ -81,7 +83,7 @@ public Response isLeader() @GET @Path("/loadstatus") - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, ServerServerResourceFilter.class }) @Produces(MediaType.APPLICATION_JSON) public Response getLoadStatus( @QueryParam("simple") String simple, @@ -100,7 +102,7 @@ public Response getLoadStatus( @GET @Path("/loadqueue") - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, ServerServerResourceFilter.class }) @Produces(MediaType.APPLICATION_JSON) public Response getLoadQueue( @QueryParam("simple") String simple, diff --git a/server/src/main/java/org/apache/druid/server/http/HistoricalResource.java b/server/src/main/java/org/apache/druid/server/http/HistoricalResource.java index 4bc48f444df1..d95e7aa9de93 100644 --- a/server/src/main/java/org/apache/druid/server/http/HistoricalResource.java +++ b/server/src/main/java/org/apache/druid/server/http/HistoricalResource.java @@ -22,6 +22,7 @@ import com.google.common.collect.ImmutableMap; import com.sun.jersey.spi.container.ResourceFilters; import org.apache.druid.server.coordination.SegmentLoadDropHandler; +import org.apache.druid.server.http.security.ServerStatusResourceFilter; import org.apache.druid.server.http.security.StateResourceFilter; import javax.inject.Inject; @@ -46,7 +47,7 @@ public HistoricalResource( @GET @Path("/loadstatus") - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, ServerStatusResourceFilter.class }) @Produces(MediaType.APPLICATION_JSON) public Response getLoadStatus() { diff --git a/server/src/main/java/org/apache/druid/server/http/LookupCoordinatorResource.java b/server/src/main/java/org/apache/druid/server/http/LookupCoordinatorResource.java index 8bc21caed54a..23bc1df5ca41 100644 --- a/server/src/main/java/org/apache/druid/server/http/LookupCoordinatorResource.java +++ b/server/src/main/java/org/apache/druid/server/http/LookupCoordinatorResource.java @@ -39,8 +39,18 @@ import org.apache.druid.java.util.common.logger.Logger; import org.apache.druid.query.lookup.LookupsState; import org.apache.druid.server.http.security.ConfigResourceFilter; +import org.apache.druid.server.http.security.ServerServerResourceFilter; +import org.apache.druid.server.http.security.ServerUserResourceFilter; import org.apache.druid.server.lookup.cache.LookupCoordinatorManager; import org.apache.druid.server.lookup.cache.LookupExtractorFactoryMapContainer; +import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthConfig; +import org.apache.druid.server.security.AuthorizationUtils; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.server.security.Resource; +import org.apache.druid.server.security.ResourceAction; +import org.apache.druid.server.security.ResourceType; import javax.annotation.Nullable; import javax.servlet.http.HttpServletRequest; @@ -61,44 +71,47 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.BiFunction; import java.util.stream.Collectors; /** * Contains information about lookups exposed through the coordinator */ @Path("/druid/coordinator/v1/lookups") -@ResourceFilters(ConfigResourceFilter.class) public class LookupCoordinatorResource { private static final Logger LOG = new Logger(LookupCoordinatorResource.class); private final LookupCoordinatorManager lookupCoordinatorManager; private final ObjectMapper smileMapper; private final ObjectMapper jsonMapper; + private final AuthorizerMapper authorizerMapper; @Inject public LookupCoordinatorResource( final LookupCoordinatorManager lookupCoordinatorManager, final @Smile ObjectMapper smileMapper, - final @Json ObjectMapper jsonMapper + final @Json ObjectMapper jsonMapper, + AuthorizerMapper authorizerMapper ) { this.smileMapper = smileMapper; this.jsonMapper = jsonMapper; this.lookupCoordinatorManager = lookupCoordinatorManager; + this.authorizerMapper = authorizerMapper; } @GET @Path("/config") @Produces({MediaType.APPLICATION_JSON, SmileMediaTypes.APPLICATION_JACKSON_SMILE}) - public Response getTiers( - @DefaultValue("false") @QueryParam("discover") boolean discover - ) + @ResourceFilters({ ConfigResourceFilter.class, ServerUserResourceFilter.class }) + public Response getTiers(@DefaultValue("false") @QueryParam("discover") boolean discover) { try { final Map> knownLookups = @@ -126,14 +139,18 @@ public Response getTiers( @GET @Produces({MediaType.APPLICATION_JSON}) @Path("/config/all") - public Response getAllLookupSpecs() + @ResourceFilters(ConfigResourceFilter.class) + public Response getAllLookupSpecs(@Context HttpServletRequest request) { try { - final Map> knownLookups = lookupCoordinatorManager + Map> knownLookups = lookupCoordinatorManager .getKnownLookups(); if (knownLookups == null) { return Response.status(Response.Status.NOT_FOUND).build(); } else { + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + knownLookups = filterByLookupAccess(knownLookups, request, authorizerMapper, Action.READ); + } return Response.ok().entity(knownLookups).build(); } } @@ -147,6 +164,7 @@ public Response getAllLookupSpecs() @Path("/config") @Produces({MediaType.APPLICATION_JSON, SmileMediaTypes.APPLICATION_JACKSON_SMILE}) @Consumes({MediaType.APPLICATION_JSON, SmileMediaTypes.APPLICATION_JACKSON_SMILE}) + @ResourceFilters(ConfigResourceFilter.class) public Response updateAllLookups( InputStream in, @HeaderParam(AuditManager.X_DRUID_AUTHOR) @DefaultValue("") final String author, @@ -166,6 +184,13 @@ public Response updateAllLookups( catch (IOException e) { return Response.status(Response.Status.BAD_REQUEST).entity(ServletResourceUtils.sanitizeException(e)).build(); } + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + final Access access = AuthorizationUtils + .authorizeAllResourceActions(req, TIER_LOOKUP_RA_GENERATOR.apply(map, Action.WRITE), authorizerMapper); + if (!access.isAllowed()) { + return Response.status(Response.Status.FORBIDDEN).entity(access.getMessage()).build(); + } + } if (lookupCoordinatorManager.updateLookups(map, new AuditInfo(author, comment, req.getRemoteAddr()))) { return Response.status(Response.Status.ACCEPTED).entity(map).build(); } else { @@ -181,6 +206,7 @@ public Response updateAllLookups( @DELETE @Produces({MediaType.APPLICATION_JSON, SmileMediaTypes.APPLICATION_JACKSON_SMILE}) @Path("/config/{tier}") + @ResourceFilters(ConfigResourceFilter.class) public Response deleteTier( @PathParam("tier") String tier, @HeaderParam(AuditManager.X_DRUID_AUTHOR) @DefaultValue("") final String author, @@ -195,6 +221,15 @@ public Response deleteTier( .build(); } + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + Map tierLookups = lookupCoordinatorManager.getTierLookups(tier); + final Access access = AuthorizationUtils + .authorizeAllResourceActions(req, LOOKUP_RA_GENERATOR.apply(tierLookups, Action.WRITE), authorizerMapper); + if (!access.isAllowed()) { + return Response.status(Response.Status.FORBIDDEN).entity(access.getMessage()).build(); + } + } + if (lookupCoordinatorManager.deleteTier(tier, new AuditInfo(author, comment, req.getRemoteAddr()))) { return Response.status(Response.Status.ACCEPTED).build(); } else { @@ -210,6 +245,7 @@ public Response deleteTier( @DELETE @Produces({MediaType.APPLICATION_JSON, SmileMediaTypes.APPLICATION_JACKSON_SMILE}) @Path("/config/{tier}/{lookup}") + @ResourceFilters(ConfigResourceFilter.class) public Response deleteLookup( @PathParam("tier") String tier, @PathParam("lookup") String lookup, @@ -231,6 +267,14 @@ public Response deleteLookup( .build(); } + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + final Access access = AuthorizationUtils + .authorizeResourceAction(req, new ResourceAction(new Resource(lookup, ResourceType.LOOKUP), Action.WRITE), authorizerMapper); + if (!access.isAllowed()) { + return Response.status(Response.Status.FORBIDDEN).entity(access.getMessage()).build(); + } + } + if (lookupCoordinatorManager.deleteLookup(tier, lookup, new AuditInfo(author, comment, req.getRemoteAddr()))) { return Response.status(Response.Status.ACCEPTED).build(); } else { @@ -246,6 +290,7 @@ public Response deleteLookup( @POST @Produces({MediaType.APPLICATION_JSON, SmileMediaTypes.APPLICATION_JACKSON_SMILE}) @Path("/config/{tier}/{lookup}") + @ResourceFilters(ConfigResourceFilter.class) public Response createOrUpdateLookup( @PathParam("tier") String tier, @PathParam("lookup") String lookup, @@ -267,6 +312,15 @@ public Response createOrUpdateLookup( .entity(ServletResourceUtils.sanitizeException(new IAE("`lookup` required"))) .build(); } + + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + final Access access = AuthorizationUtils + .authorizeResourceAction(req, new ResourceAction(new Resource(lookup, ResourceType.LOOKUP), Action.WRITE), authorizerMapper); + if (!access.isAllowed()) { + return Response.status(Response.Status.FORBIDDEN).entity(access.getMessage()).build(); + } + } + final boolean isSmile = SmileMediaTypes.APPLICATION_JACKSON_SMILE.equals(req.getContentType()); final ObjectMapper mapper = isSmile ? smileMapper : jsonMapper; final LookupExtractorFactoryMapContainer lookupSpec; @@ -296,9 +350,11 @@ public Response createOrUpdateLookup( @GET @Produces({MediaType.APPLICATION_JSON, SmileMediaTypes.APPLICATION_JACKSON_SMILE}) @Path("/config/{tier}/{lookup}") + @ResourceFilters(ConfigResourceFilter.class) public Response getSpecificLookup( @PathParam("tier") String tier, - @PathParam("lookup") String lookup + @PathParam("lookup") String lookup, + @Context HttpServletRequest req ) { try { @@ -312,6 +368,15 @@ public Response getSpecificLookup( .entity(ServletResourceUtils.sanitizeException(new NullPointerException("`lookup` required"))) .build(); } + + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + final Access access = AuthorizationUtils + .authorizeResourceAction(req, new ResourceAction(new Resource(lookup, ResourceType.LOOKUP), Action.READ), authorizerMapper); + if (!access.isAllowed()) { + return Response.status(Response.Status.FORBIDDEN).entity(access.getMessage()).build(); + } + } + final LookupExtractorFactoryMapContainer map = lookupCoordinatorManager.getLookup(tier, lookup); if (map == null) { return Response.status(Response.Status.NOT_FOUND) @@ -329,10 +394,11 @@ public Response getSpecificLookup( @GET @Produces({MediaType.APPLICATION_JSON, SmileMediaTypes.APPLICATION_JACKSON_SMILE}) @Path("/config/{tier}") + @ResourceFilters(ConfigResourceFilter.class) public Response getSpecificTier( @PathParam("tier") String tier, - @DefaultValue("false") @QueryParam("detailed") boolean detailed - + @DefaultValue("false") @QueryParam("detailed") boolean detailed, + @Context HttpServletRequest request ) { try { @@ -341,12 +407,15 @@ public Response getSpecificTier( .entity(ServletResourceUtils.sanitizeException(new NullPointerException("`tier` required"))) .build(); } - final Map> map = lookupCoordinatorManager.getKnownLookups(); + Map> map = lookupCoordinatorManager.getKnownLookups(); if (map == null) { return Response.status(Response.Status.NOT_FOUND) .entity(ServletResourceUtils.sanitizeException(new RE("No lookups found"))) .build(); } + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + map = filterByLookupAccess(map, request, authorizerMapper, Action.READ); + } final Map tierLookups = map.get(tier); if (tierLookups == null) { return Response.status(Response.Status.NOT_FOUND) @@ -368,8 +437,10 @@ public Response getSpecificTier( @GET @Produces({MediaType.APPLICATION_JSON}) @Path("/status") + @ResourceFilters(ConfigResourceFilter.class) public Response getAllLookupsStatus( - @QueryParam("detailed") boolean detailed + @QueryParam("detailed") boolean detailed, + @Context HttpServletRequest request ) { try { @@ -380,6 +451,9 @@ public Response getAllLookupsStatus( .entity(ServletResourceUtils.jsonize("No lookups found")) .build(); } + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + configuredLookups = filterByLookupAccess(configuredLookups, request, authorizerMapper, Action.READ); + } Map> lookupsStateOnNodes = lookupCoordinatorManager .getLastKnownLookupsStateOnNodes(); @@ -417,9 +491,11 @@ public Response getAllLookupsStatus( @GET @Produces({MediaType.APPLICATION_JSON}) @Path("/status/{tier}") + @ResourceFilters(ConfigResourceFilter.class) public Response getLookupStatusForTier( @PathParam("tier") String tier, - @QueryParam("detailed") boolean detailed + @QueryParam("detailed") boolean detailed, + @Context HttpServletRequest request ) { try { @@ -430,6 +506,9 @@ public Response getLookupStatusForTier( .entity(ServletResourceUtils.jsonize("No lookups found")) .build(); } + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + configuredLookups = filterByLookupAccess(configuredLookups, request, authorizerMapper, Action.READ); + } Map tierLookups = configuredLookups.get(tier); if (tierLookups == null) { @@ -463,10 +542,12 @@ public Response getLookupStatusForTier( @GET @Produces({MediaType.APPLICATION_JSON}) @Path("/status/{tier}/{lookup}") + @ResourceFilters(ConfigResourceFilter.class) public Response getSpecificLookupStatus( @PathParam("tier") String tier, @PathParam("lookup") String lookup, - @QueryParam("detailed") boolean detailed + @QueryParam("detailed") boolean detailed, + @Context HttpServletRequest request ) { try { @@ -477,6 +558,13 @@ public Response getSpecificLookupStatus( .entity(ServletResourceUtils.jsonize("No lookups found")) .build(); } + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + final Access access = AuthorizationUtils + .authorizeResourceAction(request, new ResourceAction(new Resource(lookup, ResourceType.LOOKUP), Action.READ), authorizerMapper); + if (!access.isAllowed()) { + return Response.status(Response.Status.FORBIDDEN).entity(access.getMessage()).build(); + } + } Map tierLookups = configuredLookups.get(tier); if (tierLookups == null) { @@ -541,6 +629,7 @@ LookupStatus getLookupStatus( @GET @Produces({MediaType.APPLICATION_JSON}) @Path("/nodeStatus") + @ResourceFilters({ ConfigResourceFilter.class, ServerServerResourceFilter.class }) public Response getAllNodesStatus( @QueryParam("discover") boolean discover, @QueryParam("detailed") @Nullable Boolean detailed @@ -584,6 +673,7 @@ public Response getAllNodesStatus( @GET @Produces({MediaType.APPLICATION_JSON}) @Path("/nodeStatus/{tier}") + @ResourceFilters({ ConfigResourceFilter.class, ServerServerResourceFilter.class }) public Response getNodesStatusInTier( @PathParam("tier") String tier ) @@ -616,6 +706,7 @@ public Response getNodesStatusInTier( @GET @Produces({MediaType.APPLICATION_JSON}) @Path("/nodeStatus/{tier}/{hostAndPort}") + @ResourceFilters({ ConfigResourceFilter.class, ServerServerResourceFilter.class }) public Response getSpecificNodeStatus( @PathParam("tier") String tier, @PathParam("hostAndPort") HostAndPort hostAndPort @@ -753,4 +844,42 @@ public int hashCode() return Objects.hash(loaded, pendingNodes); } } + + private Map> filterByLookupAccess( + Map> lookups, + HttpServletRequest request, + AuthorizerMapper authorizerMapper, + Action action + ) + { + final Map> filteredLookups = new HashMap<>(lookups); + lookups.keySet().forEach(tier -> filteredLookups.compute(tier, (tier1, lookupFactory) -> { + final Map filteredMap = new HashMap<>(lookupFactory); + lookupFactory.keySet().forEach(loookupId -> { + final Access access = AuthorizationUtils.authorizeAllResourceActions( + request, + Collections.singleton(new ResourceAction(new Resource(loookupId, ResourceType.LOOKUP), action)), + authorizerMapper + ); + if (!access.isAllowed()) { + filteredMap.remove(loookupId); + } + }); + return filteredMap; + } + )); + // We're filtering, so having access to none of the objects isn't an authorization failure (in terms of whether + // to send an error response or not.) + request.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, true); + return filteredLookups; + } + + private BiFunction>, Action, Iterable> TIER_LOOKUP_RA_GENERATOR = (input, action) -> input + .values().stream().flatMap(lookupMap -> lookupMap.keySet().stream() + .map(lookupId -> new ResourceAction(new Resource(lookupId, ResourceType.LOOKUP), action))) + .collect(Collectors.toList()); + + private BiFunction, Action, Iterable> LOOKUP_RA_GENERATOR = (input, action) -> input + .keySet().stream().map(lookupId -> new ResourceAction(new Resource(lookupId, ResourceType.LOOKUP), action)) + .collect(Collectors.toList()); } diff --git a/server/src/main/java/org/apache/druid/server/http/RouterResource.java b/server/src/main/java/org/apache/druid/server/http/RouterResource.java index 994d8b5e9b98..40cdbe73f03f 100644 --- a/server/src/main/java/org/apache/druid/server/http/RouterResource.java +++ b/server/src/main/java/org/apache/druid/server/http/RouterResource.java @@ -23,6 +23,7 @@ import com.google.inject.Inject; import com.sun.jersey.spi.container.ResourceFilters; import org.apache.druid.client.selector.Server; +import org.apache.druid.server.http.security.InternalInternalResourceFilter; import org.apache.druid.server.http.security.StateResourceFilter; import org.apache.druid.server.router.TieredBrokerHostSelector; @@ -49,7 +50,7 @@ public RouterResource(TieredBrokerHostSelector tieredBrokerHostSelector) @GET @Path("/brokers") - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, InternalInternalResourceFilter.class }) @Produces(MediaType.APPLICATION_JSON) public Map> getBrokers() { diff --git a/server/src/main/java/org/apache/druid/server/http/RulesResource.java b/server/src/main/java/org/apache/druid/server/http/RulesResource.java index fdb5896c4a20..ec78e6e5327a 100644 --- a/server/src/main/java/org/apache/druid/server/http/RulesResource.java +++ b/server/src/main/java/org/apache/druid/server/http/RulesResource.java @@ -29,7 +29,11 @@ import org.apache.druid.metadata.MetadataRuleManager; import org.apache.druid.server.coordinator.rules.Rule; import org.apache.druid.server.http.security.RulesResourceFilter; +import org.apache.druid.server.http.security.ServerServerResourceFilter; import org.apache.druid.server.http.security.StateResourceFilter; +import org.apache.druid.server.security.AuthConfig; +import org.apache.druid.server.security.AuthorizationUtils; +import org.apache.druid.server.security.AuthorizerMapper; import org.joda.time.Interval; import javax.servlet.http.HttpServletRequest; @@ -45,7 +49,10 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** */ @@ -53,26 +60,42 @@ public class RulesResource { public static final String RULES_ENDPOINT = "/druid/coordinator/v1/rules"; + public static final String DEFAULT_RULE_KEY = "_default"; private final MetadataRuleManager databaseRuleManager; private final AuditManager auditManager; + private final AuthorizerMapper authorizerMapper; @Inject public RulesResource( MetadataRuleManager databaseRuleManager, - AuditManager auditManager + AuditManager auditManager, + AuthorizerMapper authorizerMapper ) { this.databaseRuleManager = databaseRuleManager; this.auditManager = auditManager; + this.authorizerMapper = authorizerMapper; } @GET @Produces(MediaType.APPLICATION_JSON) @ResourceFilters(StateResourceFilter.class) - public Response getRules() + public Response getRules(@Context HttpServletRequest request) { - return Response.ok(databaseRuleManager.getAllRules()).build(); + final Map> rules = databaseRuleManager.getAllRules(); + + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + final Map> filteredRules = new HashMap<>(); + AuthorizationUtils.filterAuthorizedResources( + request, + rules.keySet(), + datasource -> Collections.singletonList(AuthorizationUtils.DATASOURCE_READ_RA_GENERATOR.apply(datasource)), + authorizerMapper + ).forEach(datasource -> filteredRules.put(datasource, rules.get(datasource))); + return Response.ok(filteredRules).build(); + } + return Response.ok(rules).build(); } @GET @@ -138,7 +161,7 @@ public Response getDatasourceRuleHistory( @GET @Path("/history") @Produces(MediaType.APPLICATION_JSON) - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, ServerServerResourceFilter.class }) public Response getDatasourceRuleHistory( @QueryParam("interval") final String interval, @QueryParam("count") final Integer count diff --git a/server/src/main/java/org/apache/druid/server/http/SegmentListerResource.java b/server/src/main/java/org/apache/druid/server/http/SegmentListerResource.java index 45dae0af0aa9..648910a1bad7 100644 --- a/server/src/main/java/org/apache/druid/server/http/SegmentListerResource.java +++ b/server/src/main/java/org/apache/druid/server/http/SegmentListerResource.java @@ -36,6 +36,7 @@ import org.apache.druid.server.coordination.DataSegmentChangeRequest; import org.apache.druid.server.coordination.SegmentLoadDropHandler; import org.apache.druid.server.coordinator.HttpLoadQueuePeon; +import org.apache.druid.server.http.security.InternalInternalResourceFilter; import org.apache.druid.server.http.security.StateResourceFilter; import javax.annotation.Nullable; @@ -59,7 +60,7 @@ * Endpoints exposed here are to be used only for druid internal management of segments by Coordinators, Brokers etc. */ @Path("/druid-internal/v1/segments/") -@ResourceFilters(StateResourceFilter.class) +@ResourceFilters({ StateResourceFilter.class, InternalInternalResourceFilter.class }) public class SegmentListerResource { protected static final EmittingLogger log = new EmittingLogger(SegmentListerResource.class); diff --git a/server/src/main/java/org/apache/druid/server/http/SelfDiscoveryResource.java b/server/src/main/java/org/apache/druid/server/http/SelfDiscoveryResource.java index 13b7bded496e..cb3a2b93c745 100644 --- a/server/src/main/java/org/apache/druid/server/http/SelfDiscoveryResource.java +++ b/server/src/main/java/org/apache/druid/server/http/SelfDiscoveryResource.java @@ -28,6 +28,7 @@ import org.apache.druid.guice.annotations.Self; import org.apache.druid.java.util.common.lifecycle.Lifecycle; import org.apache.druid.server.DruidNode; +import org.apache.druid.server.http.security.InternalInternalResourceFilter; import org.apache.druid.server.http.security.StateResourceFilter; import org.eclipse.jetty.http.HttpStatus; @@ -88,7 +89,7 @@ public void stop() @GET @Path("/status") @Produces(MediaType.APPLICATION_JSON) - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, InternalInternalResourceFilter.class }) public Response getSelfDiscoveredStatus() { return Response.ok(Collections.singletonMap("selfDiscovered", isDiscoveredAllRoles())).build(); @@ -97,7 +98,7 @@ public Response getSelfDiscoveredStatus() /** See the description of this endpoint in api-reference.md. */ @GET @Produces(MediaType.APPLICATION_JSON) - @ResourceFilters(StateResourceFilter.class) + @ResourceFilters({ StateResourceFilter.class, InternalInternalResourceFilter.class }) public Response getSelfDiscovered() { if (isDiscoveredAllRoles()) { diff --git a/server/src/main/java/org/apache/druid/server/http/ServersResource.java b/server/src/main/java/org/apache/druid/server/http/ServersResource.java index c6b105ad16c6..39bd827d51a5 100644 --- a/server/src/main/java/org/apache/druid/server/http/ServersResource.java +++ b/server/src/main/java/org/apache/druid/server/http/ServersResource.java @@ -27,6 +27,7 @@ import com.sun.jersey.spi.container.ResourceFilters; import org.apache.druid.client.DruidServer; import org.apache.druid.client.InventoryView; +import org.apache.druid.server.http.security.ServerServerResourceFilter; import org.apache.druid.server.http.security.StateResourceFilter; import org.apache.druid.timeline.DataSegment; import org.apache.druid.timeline.SegmentId; @@ -47,7 +48,7 @@ /** */ @Path("/druid/coordinator/v1/servers") -@ResourceFilters(StateResourceFilter.class) +@ResourceFilters({ StateResourceFilter.class, ServerServerResourceFilter.class }) public class ServersResource { private static Map makeSimpleServer(DruidServer input) diff --git a/server/src/main/java/org/apache/druid/server/http/TiersResource.java b/server/src/main/java/org/apache/druid/server/http/TiersResource.java index 51a0fdad1d80..97641132b3de 100644 --- a/server/src/main/java/org/apache/druid/server/http/TiersResource.java +++ b/server/src/main/java/org/apache/druid/server/http/TiersResource.java @@ -24,17 +24,27 @@ import org.apache.druid.client.DruidDataSource; import org.apache.druid.client.DruidServer; import org.apache.druid.client.InventoryView; +import org.apache.druid.server.http.security.ServerUserResourceFilter; import org.apache.druid.server.http.security.StateResourceFilter; +import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthorizationUtils; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.server.security.Resource; +import org.apache.druid.server.security.ResourceAction; +import org.apache.druid.server.security.ResourceType; import org.apache.druid.timeline.DataSegment; import org.joda.time.Interval; +import javax.servlet.http.HttpServletRequest; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.Map; @@ -44,15 +54,17 @@ /** */ @Path("/druid/coordinator/v1/tiers") -@ResourceFilters(StateResourceFilter.class) +@ResourceFilters({ StateResourceFilter.class, ServerUserResourceFilter.class }) public class TiersResource { private final InventoryView serverInventoryView; + private final AuthorizerMapper authorizerMapper; @Inject - public TiersResource(InventoryView serverInventoryView) + public TiersResource(InventoryView serverInventoryView, AuthorizerMapper authorizerMapper) { this.serverInventoryView = serverInventoryView; + this.authorizerMapper = authorizerMapper; } private enum TierMetadataKeys @@ -98,13 +110,24 @@ private enum IntervalProperties @GET @Path("/{tierName}") @Produces(MediaType.APPLICATION_JSON) - public Response getTierDataSources(@PathParam("tierName") String tierName, @QueryParam("simple") String simple) + public Response getTierDataSources( + @PathParam("tierName") String tierName, + @QueryParam("simple") String simple, + @Context HttpServletRequest request + ) { if (simple != null) { Map>> tierToStatsPerInterval = new HashMap<>(); for (DruidServer druidServer : serverInventoryView.getInventory()) { if (druidServer.getTier().equalsIgnoreCase(tierName)) { - for (DataSegment dataSegment : druidServer.iterateAllSegments()) { + Iterable filteredSegments = AuthorizationUtils.filterAuthorizedResources( + AuthorizationUtils.authenticationResultFromRequest(request), + druidServer.iterateAllSegments(), + input -> Collections.singleton( + new ResourceAction(new Resource(input.getDataSource(), ResourceType.DATASOURCE), Action.READ)), + authorizerMapper + ); + for (DataSegment dataSegment : filteredSegments) { Map properties = tierToStatsPerInterval .computeIfAbsent(dataSegment.getDataSource(), dsName -> new HashMap<>()) .computeIfAbsent(dataSegment.getInterval(), interval -> new EnumMap<>(IntervalProperties.class)); diff --git a/server/src/main/java/org/apache/druid/server/http/security/AbstractResourceFilter.java b/server/src/main/java/org/apache/druid/server/http/security/AbstractResourceFilter.java index 4119e0b9b747..c8087c2346a2 100644 --- a/server/src/main/java/org/apache/druid/server/http/security/AbstractResourceFilter.java +++ b/server/src/main/java/org/apache/druid/server/http/security/AbstractResourceFilter.java @@ -39,9 +39,7 @@ public abstract class AbstractResourceFilter implements ResourceFilter, Containe private AuthorizerMapper authorizerMapper; @Inject - public AbstractResourceFilter( - AuthorizerMapper authorizerMapper - ) + public AbstractResourceFilter(AuthorizerMapper authorizerMapper) { this.authorizerMapper = authorizerMapper; } @@ -73,6 +71,11 @@ public void setAuthorizerMapper(AuthorizerMapper authorizerMapper) this.authorizerMapper = authorizerMapper; } + public String getAuthVersion() + { + return authorizerMapper.getAuthVersion(); + } + public AbstractResourceFilter setReq(HttpServletRequest req) { this.req = req; diff --git a/server/src/main/java/org/apache/druid/server/http/security/ConfigResourceFilter.java b/server/src/main/java/org/apache/druid/server/http/security/ConfigResourceFilter.java index 7a45ca1d5bbb..960cd5a37bb0 100644 --- a/server/src/main/java/org/apache/druid/server/http/security/ConfigResourceFilter.java +++ b/server/src/main/java/org/apache/druid/server/http/security/ConfigResourceFilter.java @@ -22,6 +22,7 @@ import com.google.inject.Inject; import com.sun.jersey.spi.container.ContainerRequest; import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.AuthConfig; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.ForbiddenException; @@ -37,13 +38,14 @@ * - druid/coordinator/v1/config * Note - Currently the resource name for all end points is set to "CONFIG" however if more fine grained access control * is required the resource name can be set to specific config properties. + * + * @deprecated Used only with {@link AuthConfig#AUTH_VERSION_1} */ +@Deprecated public class ConfigResourceFilter extends AbstractResourceFilter { @Inject - public ConfigResourceFilter( - AuthorizerMapper authorizerMapper - ) + public ConfigResourceFilter(AuthorizerMapper authorizerMapper) { super(authorizerMapper); } @@ -51,21 +53,22 @@ public ConfigResourceFilter( @Override public ContainerRequest filter(ContainerRequest request) { - final ResourceAction resourceAction = new ResourceAction( - new Resource("CONFIG", ResourceType.CONFIG), - getAction(request) - ); + if (getAuthVersion().equals(AuthConfig.AUTH_VERSION_1)) { + final ResourceAction resourceAction = new ResourceAction( + new Resource("CONFIG", ResourceType.CONFIG), + getAction(request) + ); - final Access authResult = AuthorizationUtils.authorizeResourceAction( - getReq(), - resourceAction, - getAuthorizerMapper() - ); + final Access authResult = AuthorizationUtils.authorizeResourceAction( + getReq(), + resourceAction, + getAuthorizerMapper() + ); - if (!authResult.isAllowed()) { - throw new ForbiddenException(authResult.toString()); + if (!authResult.isAllowed()) { + throw new ForbiddenException(authResult.toString()); + } } - return request; } } diff --git a/server/src/main/java/org/apache/druid/server/http/security/DatasourceResourceFilter.java b/server/src/main/java/org/apache/druid/server/http/security/DatasourceResourceFilter.java index e32446cfe5cb..64ba228adbba 100644 --- a/server/src/main/java/org/apache/druid/server/http/security/DatasourceResourceFilter.java +++ b/server/src/main/java/org/apache/druid/server/http/security/DatasourceResourceFilter.java @@ -44,9 +44,7 @@ public class DatasourceResourceFilter extends AbstractResourceFilter { @Inject - public DatasourceResourceFilter( - AuthorizerMapper authorizerMapper - ) + public DatasourceResourceFilter(AuthorizerMapper authorizerMapper) { super(authorizerMapper); } diff --git a/server/src/main/java/org/apache/druid/server/http/security/InternalInternalResourceFilter.java b/server/src/main/java/org/apache/druid/server/http/security/InternalInternalResourceFilter.java new file mode 100644 index 000000000000..f5f855700b7e --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/http/security/InternalInternalResourceFilter.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.druid.server.http.security; + +import com.google.inject.Inject; +import com.sun.jersey.spi.container.ContainerRequest; +import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.AuthConfig; +import org.apache.druid.server.security.AuthorizationUtils; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.server.security.ForbiddenException; +import org.apache.druid.server.security.Resource; +import org.apache.druid.server.security.ResourceAction; + +public class InternalInternalResourceFilter extends AbstractResourceFilter +{ + @Inject + public InternalInternalResourceFilter(AuthorizerMapper authorizerMapper) + { + super(authorizerMapper); + } + + @Override + public ContainerRequest filter(ContainerRequest request) + { + if (getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + final ResourceAction resourceAction = new ResourceAction( + Resource.INTERNAL_INTERNAL_RESOURCE, + getAction(request) + ); + + final Access authResult = AuthorizationUtils.authorizeResourceAction( + getReq(), + resourceAction, + getAuthorizerMapper() + ); + + if (!authResult.isAllowed()) { + throw new ForbiddenException(authResult.toString()); + } + } + return request; + } +} diff --git a/server/src/main/java/org/apache/druid/server/http/security/RulesResourceFilter.java b/server/src/main/java/org/apache/druid/server/http/security/RulesResourceFilter.java index f314c77d7431..dc5ddecfb3c8 100644 --- a/server/src/main/java/org/apache/druid/server/http/security/RulesResourceFilter.java +++ b/server/src/main/java/org/apache/druid/server/http/security/RulesResourceFilter.java @@ -44,9 +44,7 @@ public class RulesResourceFilter extends AbstractResourceFilter { @Inject - public RulesResourceFilter( - AuthorizerMapper authorizerMapper - ) + public RulesResourceFilter(AuthorizerMapper authorizerMapper) { super(authorizerMapper); } diff --git a/server/src/main/java/org/apache/druid/server/http/security/ServerServerResourceFilter.java b/server/src/main/java/org/apache/druid/server/http/security/ServerServerResourceFilter.java new file mode 100644 index 000000000000..c1799278ca8d --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/http/security/ServerServerResourceFilter.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.druid.server.http.security; + +import com.google.inject.Inject; +import com.sun.jersey.spi.container.ContainerRequest; +import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.AuthConfig; +import org.apache.druid.server.security.AuthorizationUtils; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.server.security.ForbiddenException; +import org.apache.druid.server.security.Resource; +import org.apache.druid.server.security.ResourceAction; + +public class ServerServerResourceFilter extends AbstractResourceFilter +{ + @Inject + public ServerServerResourceFilter(AuthorizerMapper authorizerMapper) + { + super(authorizerMapper); + } + + @Override + public ContainerRequest filter(ContainerRequest request) + { + if (getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + final ResourceAction resourceAction = new ResourceAction( + Resource.SERVER_SERVER_RESOURCE, + getAction(request) + ); + + final Access authResult = AuthorizationUtils.authorizeResourceAction( + getReq(), + resourceAction, + getAuthorizerMapper() + ); + + if (!authResult.isAllowed()) { + throw new ForbiddenException(authResult.toString()); + } + } + return request; + } +} diff --git a/server/src/main/java/org/apache/druid/server/http/security/ServerStatusResourceFilter.java b/server/src/main/java/org/apache/druid/server/http/security/ServerStatusResourceFilter.java new file mode 100644 index 000000000000..eb260771884e --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/http/security/ServerStatusResourceFilter.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.druid.server.http.security; + +import com.google.inject.Inject; +import com.sun.jersey.spi.container.ContainerRequest; +import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.AuthConfig; +import org.apache.druid.server.security.AuthorizationUtils; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.server.security.ForbiddenException; +import org.apache.druid.server.security.Resource; +import org.apache.druid.server.security.ResourceAction; + +public class ServerStatusResourceFilter extends AbstractResourceFilter +{ + @Inject + public ServerStatusResourceFilter(AuthorizerMapper authorizerMapper) + { + super(authorizerMapper); + } + + @Override + public ContainerRequest filter(ContainerRequest request) + { + if (getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + final ResourceAction resourceAction = new ResourceAction( + Resource.SERVER_STATUS_RESOURCE, + getAction(request) + ); + + final Access authResult = AuthorizationUtils.authorizeResourceAction( + getReq(), + resourceAction, + getAuthorizerMapper() + ); + + if (!authResult.isAllowed()) { + throw new ForbiddenException(authResult.toString()); + } + } + return request; + } +} diff --git a/server/src/main/java/org/apache/druid/server/http/security/ServerUserResourceFilter.java b/server/src/main/java/org/apache/druid/server/http/security/ServerUserResourceFilter.java new file mode 100644 index 000000000000..d4331c287c6a --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/http/security/ServerUserResourceFilter.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.druid.server.http.security; + +import com.google.inject.Inject; +import com.sun.jersey.spi.container.ContainerRequest; +import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.AuthConfig; +import org.apache.druid.server.security.AuthorizationUtils; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.server.security.ForbiddenException; +import org.apache.druid.server.security.Resource; +import org.apache.druid.server.security.ResourceAction; + +public class ServerUserResourceFilter extends AbstractResourceFilter +{ + @Inject + public ServerUserResourceFilter(AuthorizerMapper authorizerMapper) + { + super(authorizerMapper); + } + + @Override + public ContainerRequest filter(ContainerRequest request) + { + if (getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + final ResourceAction resourceAction = new ResourceAction( + Resource.SERVER_USER_RESOURCE, + getAction(request) + ); + + final Access authResult = AuthorizationUtils.authorizeResourceAction( + getReq(), + resourceAction, + getAuthorizerMapper() + ); + + if (!authResult.isAllowed()) { + throw new ForbiddenException(authResult.toString()); + } + } + return request; + } +} diff --git a/server/src/main/java/org/apache/druid/server/http/security/StateResourceFilter.java b/server/src/main/java/org/apache/druid/server/http/security/StateResourceFilter.java index 3a2d0e3bf83c..28abe2f438e8 100644 --- a/server/src/main/java/org/apache/druid/server/http/security/StateResourceFilter.java +++ b/server/src/main/java/org/apache/druid/server/http/security/StateResourceFilter.java @@ -22,6 +22,7 @@ import com.google.inject.Inject; import com.sun.jersey.spi.container.ContainerRequest; import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.AuthConfig; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.ForbiddenException; @@ -42,7 +43,10 @@ * - status * Note - Currently the resource name for all end points is set to "STATE" however if more fine grained access control * is required the resource name can be set to specific state properties. + * + * @deprecated Used only with {@link AuthConfig#AUTH_VERSION_1} */ +@Deprecated public class StateResourceFilter extends AbstractResourceFilter { @Inject @@ -54,21 +58,22 @@ public StateResourceFilter(AuthorizerMapper authorizerMapper) @Override public ContainerRequest filter(ContainerRequest request) { - final ResourceAction resourceAction = new ResourceAction( - Resource.STATE_RESOURCE, - getAction(request) - ); + if (getAuthVersion().equals(AuthConfig.AUTH_VERSION_1)) { + final ResourceAction resourceAction = new ResourceAction( + Resource.STATE_RESOURCE, + getAction(request) + ); - final Access authResult = AuthorizationUtils.authorizeResourceAction( - getReq(), - resourceAction, - getAuthorizerMapper() - ); + final Access authResult = AuthorizationUtils.authorizeResourceAction( + getReq(), + resourceAction, + getAuthorizerMapper() + ); - if (!authResult.isAllowed()) { - throw new ForbiddenException(authResult.toString()); + if (!authResult.isAllowed()) { + throw new ForbiddenException(authResult.toString()); + } } - return request; } } diff --git a/server/src/main/java/org/apache/druid/server/initialization/AuthorizerMapperModule.java b/server/src/main/java/org/apache/druid/server/initialization/AuthorizerMapperModule.java index f547d14c5f33..cef8480faa25 100644 --- a/server/src/main/java/org/apache/druid/server/initialization/AuthorizerMapperModule.java +++ b/server/src/main/java/org/apache/druid/server/initialization/AuthorizerMapperModule.java @@ -97,7 +97,7 @@ public AuthorizerMapper get() AllowAllAuthorizer allowAllAuthorizer = new AllowAllAuthorizer(); authorizerMap.put(AuthConfig.ALLOW_ALL_NAME, allowAllAuthorizer); - return new AuthorizerMapper(null) + return new AuthorizerMapper(null, authConfig.getAuthVersion()) { @Override public Authorizer getAuthorizer(String name) @@ -138,7 +138,7 @@ public Map getAuthorizerMap() authorizerMap.put(authorizerName, authorizer); } - return new AuthorizerMapper(authorizerMap); + return new AuthorizerMapper(authorizerMap, authConfig.getAuthVersion()); } } diff --git a/server/src/main/java/org/apache/druid/server/lookup/cache/LookupCoordinatorManager.java b/server/src/main/java/org/apache/druid/server/lookup/cache/LookupCoordinatorManager.java index 7526ecbcc10f..c8f8b7a1fb76 100644 --- a/server/src/main/java/org/apache/druid/server/lookup/cache/LookupCoordinatorManager.java +++ b/server/src/main/java/org/apache/druid/server/lookup/cache/LookupCoordinatorManager.java @@ -361,6 +361,29 @@ public LookupExtractorFactoryMapContainer getLookup(final String tier, final Str return tierLookups.get(lookupName); } + /** + * Try to get all lookups for the specified tier. + * + * @param tier tier + * + * @return The lookups for a tier if found or null if not found or if no lookups at all are found + */ + @Nullable + public Map getTierLookups(final String tier) + { + final Map> prior = getKnownLookups(); + if (prior == null) { + LOG.warn("Requested tier [%s]. But no lookups exist!", tier); + return null; + } + final Map tierLookups = prior.get(tier); + if (tierLookups == null) { + LOG.warn("Tier [%s] does not exist", tier); + return null; + } + return tierLookups; + } + public boolean isStarted() { return lifecycleLock.isStarted(); diff --git a/server/src/main/java/org/apache/druid/server/security/AllowAllAuthorizer.java b/server/src/main/java/org/apache/druid/server/security/AllowAllAuthorizer.java index 6271228b33de..b04a6367455b 100644 --- a/server/src/main/java/org/apache/druid/server/security/AllowAllAuthorizer.java +++ b/server/src/main/java/org/apache/druid/server/security/AllowAllAuthorizer.java @@ -26,4 +26,10 @@ public Access authorize(AuthenticationResult authenticationResult, Resource reso { return Access.OK; } + + @Override + public Access authorizeV2(AuthenticationResult authenticationResult, Resource resource, Action action) + { + return Access.OK; + } } diff --git a/server/src/main/java/org/apache/druid/server/security/AuthConfig.java b/server/src/main/java/org/apache/druid/server/security/AuthConfig.java index f588101a5b84..143ceb4e70cd 100644 --- a/server/src/main/java/org/apache/druid/server/security/AuthConfig.java +++ b/server/src/main/java/org/apache/druid/server/security/AuthConfig.java @@ -21,6 +21,8 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import org.apache.druid.java.util.common.StringUtils; import java.util.Collections; import java.util.List; @@ -28,6 +30,8 @@ public class AuthConfig { + public static final String AUTH_VERSION_1 = "v1"; + public static final String AUTH_VERSION_2 = "v2"; /** * HTTP attribute that holds an AuthenticationResult, with info about a successful authentication check. */ @@ -48,7 +52,7 @@ public class AuthConfig public AuthConfig() { - this(null, null, null, false); + this(null, null, null, false, AUTH_VERSION_1); } @JsonCreator @@ -56,13 +60,17 @@ public AuthConfig( @JsonProperty("authenticatorChain") List authenticatorChain, @JsonProperty("authorizers") List authorizers, @JsonProperty("unsecuredPaths") List unsecuredPaths, - @JsonProperty("allowUnauthenticatedHttpOptions") boolean allowUnauthenticatedHttpOptions + @JsonProperty("allowUnauthenticatedHttpOptions") boolean allowUnauthenticatedHttpOptions, + @JsonProperty("authVersion") String authVersion ) { this.authenticatorChain = authenticatorChain; this.authorizers = authorizers; this.unsecuredPaths = unsecuredPaths == null ? Collections.emptyList() : unsecuredPaths; this.allowUnauthenticatedHttpOptions = allowUnauthenticatedHttpOptions; + Preconditions.checkArgument(authVersion == null || (authVersion.equalsIgnoreCase(AUTH_VERSION_1) || authVersion + .equalsIgnoreCase(AUTH_VERSION_2)), "Unknown auth version [%s]", authVersion); + this.authVersion = authVersion == null ? AUTH_VERSION_1 : StringUtils.toLowerCase(authVersion); } @JsonProperty @@ -77,6 +85,9 @@ public AuthConfig( @JsonProperty private final boolean allowUnauthenticatedHttpOptions; + @JsonProperty + private final String authVersion; + public List getAuthenticatorChain() { return authenticatorChain; @@ -97,6 +108,11 @@ public boolean isAllowUnauthenticatedHttpOptions() return allowUnauthenticatedHttpOptions; } + public String getAuthVersion() + { + return authVersion; + } + @Override public boolean equals(Object o) { @@ -108,9 +124,10 @@ public boolean equals(Object o) } AuthConfig that = (AuthConfig) o; return isAllowUnauthenticatedHttpOptions() == that.isAllowUnauthenticatedHttpOptions() && - Objects.equals(getAuthenticatorChain(), that.getAuthenticatorChain()) && - Objects.equals(getAuthorizers(), that.getAuthorizers()) && - Objects.equals(getUnsecuredPaths(), that.getUnsecuredPaths()); + Objects.equals(getAuthenticatorChain(), that.getAuthenticatorChain()) && + Objects.equals(getAuthorizers(), that.getAuthorizers()) && + Objects.equals(getUnsecuredPaths(), that.getUnsecuredPaths()) && + Objects.equals(getAuthVersion(), that.getAuthVersion()); } @Override @@ -120,7 +137,8 @@ public int hashCode() getAuthenticatorChain(), getAuthorizers(), getUnsecuredPaths(), - isAllowUnauthenticatedHttpOptions() + isAllowUnauthenticatedHttpOptions(), + getAuthVersion() ); } @@ -132,6 +150,7 @@ public String toString() ", authorizers=" + authorizers + ", unsecuredPaths=" + unsecuredPaths + ", allowUnauthenticatedHttpOptions=" + allowUnauthenticatedHttpOptions + + ", authVersion=" + authVersion + '}'; } } diff --git a/server/src/main/java/org/apache/druid/server/security/AuthTestUtils.java b/server/src/main/java/org/apache/druid/server/security/AuthTestUtils.java index 5922a5d234c4..9a9b35c8a30e 100644 --- a/server/src/main/java/org/apache/druid/server/security/AuthTestUtils.java +++ b/server/src/main/java/org/apache/druid/server/security/AuthTestUtils.java @@ -32,7 +32,7 @@ public class AuthTestUtils defaultMap.put(AuthConfig.ALLOW_ALL_NAME, new AllowAllAuthenticator()); TEST_AUTHENTICATOR_MAPPER = new AuthenticatorMapper(defaultMap); - TEST_AUTHORIZER_MAPPER = new AuthorizerMapper(null) { + TEST_AUTHORIZER_MAPPER = new AuthorizerMapper(null, null) { @Override public Authorizer getAuthorizer(String name) { diff --git a/server/src/main/java/org/apache/druid/server/security/AuthorizationUtils.java b/server/src/main/java/org/apache/druid/server/security/AuthorizationUtils.java index afb62eb8e3bb..f0e360bf5480 100644 --- a/server/src/main/java/org/apache/druid/server/security/AuthorizationUtils.java +++ b/server/src/main/java/org/apache/druid/server/security/AuthorizationUtils.java @@ -121,7 +121,8 @@ public static Access authorizeAllResourceActions( final Access access = authorizer.authorize( authenticationResult, resourceAction.getResource(), - resourceAction.getAction() + resourceAction.getAction(), + authorizerMapper.getAuthVersion() ); if (!access.isAllowed()) { return access; @@ -277,7 +278,8 @@ public static Iterable filterAuthorizedResources( ra -> authorizer.authorize( authenticationResult, ra.getResource(), - ra.getAction() + ra.getAction(), + authorizerMapper.getAuthVersion() ) ); if (!access.isAllowed()) { diff --git a/server/src/main/java/org/apache/druid/server/security/Authorizer.java b/server/src/main/java/org/apache/druid/server/security/Authorizer.java index 8929c9045111..9c1e47dd6d2f 100644 --- a/server/src/main/java/org/apache/druid/server/security/Authorizer.java +++ b/server/src/main/java/org/apache/druid/server/security/Authorizer.java @@ -21,6 +21,9 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.apache.druid.java.util.common.IAE; +import org.apache.druid.java.util.common.ISE; +import org.apache.druid.java.util.common.StringUtils; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes(value = { @@ -40,13 +43,49 @@ public interface Authorizer { /** - * Check if the entity represented by {@code identity} is authorized to perform {@code action} on {@code resource}. + * Method to decide whether to use v1 or v2 model for authorization * - * @param authenticationResult The authentication result of the request - * @param resource The resource to be accessed - * @param action The action to perform on the resource + * @param authenticationResult The authentication result of the request + * @param resource The resource to be accessed + * @param action The action to perform on the resource + * @param authVersion Auth version to use + * @return An Access object representing the result of the authorization check. Must not be null. + */ + default Access authorize(AuthenticationResult authenticationResult, Resource resource, Action action, String authVersion) + { + switch (StringUtils.toLowerCase(authVersion)) { + case AuthConfig.AUTH_VERSION_1: + return authorize(authenticationResult, resource, action); + case AuthConfig.AUTH_VERSION_2: + return authorizeV2(authenticationResult, resource, action); + default: + throw new IAE("No such auth version [%s]", authVersion); + } + } + + /** + * Check if the entity represented by {@code identity} is authorized to perform {@code action} on {@code resource}. * + * @param authenticationResult The authentication result of the request + * @param resource The resource to be accessed + * @param action The action to perform on the resource * @return An Access object representing the result of the authorization check. Must not be null. */ + @Deprecated Access authorize(AuthenticationResult authenticationResult, Resource resource, Action action); + + /** + * Check if the entity represented by {@code identity} is authorized to perform {@code action} on {@code resource}. + * This method will use Auth V2 model for resource name and types. + * + * @param authenticationResult The authentication result of the request + * @param resource The resource to be accessed + * @param action The action to perform on the resource + * @return An Access object representing the result of the authorization check. Must not be null. + */ + default Access authorizeV2(AuthenticationResult authenticationResult, Resource resource, Action action) + { + throw new ISE("Authorizer does not support V2 implementation, please implement it"); + } + } diff --git a/server/src/main/java/org/apache/druid/server/security/AuthorizerMapper.java b/server/src/main/java/org/apache/druid/server/security/AuthorizerMapper.java index e888cc14f9df..709bb655575a 100644 --- a/server/src/main/java/org/apache/druid/server/security/AuthorizerMapper.java +++ b/server/src/main/java/org/apache/druid/server/security/AuthorizerMapper.java @@ -27,12 +27,12 @@ public class AuthorizerMapper { private Map authorizerMap; + private String authVersion; - public AuthorizerMapper( - Map authorizerMap - ) + public AuthorizerMapper(Map authorizerMap, String authVersion) { this.authorizerMap = authorizerMap; + this.authVersion = authVersion == null ? AuthConfig.AUTH_VERSION_1 : authVersion; } public Authorizer getAuthorizer(String name) @@ -44,4 +44,9 @@ public Map getAuthorizerMap() { return authorizerMap; } + + public String getAuthVersion() + { + return authVersion; + } } diff --git a/server/src/main/java/org/apache/druid/server/security/Resource.java b/server/src/main/java/org/apache/druid/server/security/Resource.java index 6770bdaf5cbc..c99eb5d71895 100644 --- a/server/src/main/java/org/apache/druid/server/security/Resource.java +++ b/server/src/main/java/org/apache/druid/server/security/Resource.java @@ -26,6 +26,11 @@ public class Resource { public static final Resource STATE_RESOURCE = new Resource("STATE", ResourceType.STATE); + public static final Resource INTERNAL_INTERNAL_RESOURCE = new Resource("INTERNAL", ResourceType.INTERNAL); + public static final Resource SERVER_SERVER_RESOURCE = new Resource("SERVER", ResourceType.SERVER); + public static final Resource SERVER_USER_RESOURCE = new Resource("USER", ResourceType.SERVER); + public static final Resource SERVER_STATUS_RESOURCE = new Resource("STATUS", ResourceType.SERVER); + private final String name; private final ResourceType type; diff --git a/server/src/main/java/org/apache/druid/server/security/ResourceType.java b/server/src/main/java/org/apache/druid/server/security/ResourceType.java index 5debab216158..3304fc5e8d99 100644 --- a/server/src/main/java/org/apache/druid/server/security/ResourceType.java +++ b/server/src/main/java/org/apache/druid/server/security/ResourceType.java @@ -25,8 +25,11 @@ public enum ResourceType { DATASOURCE, - CONFIG, - STATE; + INTERNAL, + LOOKUP, + SERVER, + @Deprecated CONFIG, + @Deprecated STATE; @JsonCreator public static ResourceType fromString(String name) diff --git a/server/src/test/java/org/apache/druid/query/lookup/LookupIntrospectionResourceTest.java b/server/src/test/java/org/apache/druid/query/lookup/LookupIntrospectionResourceTest.java index dd8c84a96dcd..bc85ba379a31 100644 --- a/server/src/test/java/org/apache/druid/query/lookup/LookupIntrospectionResourceTest.java +++ b/server/src/test/java/org/apache/druid/query/lookup/LookupIntrospectionResourceTest.java @@ -26,6 +26,9 @@ import com.sun.jersey.api.client.config.DefaultClientConfig; import org.apache.druid.query.extraction.MapLookupExtractor; import org.apache.druid.server.WebserverTestUtils; +import org.apache.druid.server.security.AllowAllAuthorizer; +import org.apache.druid.server.security.AuthConfig; +import org.apache.druid.server.security.AuthorizerMapper; import org.easymock.EasyMock; import org.glassfish.grizzly.http.server.HttpServer; import org.junit.After; @@ -33,6 +36,7 @@ import org.junit.Before; import org.junit.Test; +import javax.servlet.http.HttpServletRequest; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.net.URI; @@ -49,10 +53,16 @@ public class LookupIntrospectionResourceTest EasyMock.createMock(LookupIntrospectHandler.class); private LookupIntrospectionResource lookupIntrospectionResource = - new LookupIntrospectionResource(mockLookupExtractorFactoryContainerProvider); + new LookupIntrospectionResource(mockLookupExtractorFactoryContainerProvider, + new AuthorizerMapper( + ImmutableMap.of(AuthConfig.ALLOW_ALL_NAME, new AllowAllAuthorizer()), + null + ) + ); private URI baseUri; private HttpServer server; + private HttpServletRequest request; @Before public void setup() throws Exception @@ -98,6 +108,7 @@ public void setup() throws Exception } ); server.start(); + request = EasyMock.createMock(HttpServletRequest.class); } @After @@ -118,7 +129,7 @@ public void testNotImplementedIntrospectLookup() EasyMock.replay(mockLookupExtractorFactory); Assert.assertEquals( Response.status(Response.Status.NOT_FOUND).build().getStatus(), - ((Response) lookupIntrospectionResource.introspectLookup("lookupId")).getStatus() + ((Response) lookupIntrospectionResource.introspectLookup("lookupId", request)).getStatus() ); } @@ -127,7 +138,7 @@ public void testNotExistingLookup() { Assert.assertEquals( Response.status(Response.Status.NOT_FOUND).build().getStatus(), - ((Response) lookupIntrospectionResource.introspectLookup("not there")).getStatus() + ((Response) lookupIntrospectionResource.introspectLookup("not there", request)).getStatus() ); } @@ -139,7 +150,7 @@ public void testExistingLookup() .andReturn(new MapLookupExtractor(ImmutableMap.of(), false)) .anyTimes(); EasyMock.replay(mockLookupExtractorFactory); - Assert.assertEquals(mockLookupIntrospectHandler, lookupIntrospectionResource.introspectLookup("lookupId")); + Assert.assertEquals(mockLookupIntrospectHandler, lookupIntrospectionResource.introspectLookup("lookupId", request)); } @Test diff --git a/server/src/test/java/org/apache/druid/server/AsyncQueryForwardingServletTest.java b/server/src/test/java/org/apache/druid/server/AsyncQueryForwardingServletTest.java index 066088c0eac4..beb4577daadb 100644 --- a/server/src/test/java/org/apache/druid/server/AsyncQueryForwardingServletTest.java +++ b/server/src/test/java/org/apache/druid/server/AsyncQueryForwardingServletTest.java @@ -132,7 +132,7 @@ public void configure(Binder binder) ); binder.bind(JettyServerInitializer.class).to(ProxyJettyServerInit.class).in(LazySingleton.class); binder.bind(AuthorizerMapper.class).toInstance( - new AuthorizerMapper(null) + new AuthorizerMapper(null, null) { @Override diff --git a/server/src/test/java/org/apache/druid/server/QueryResourceTest.java b/server/src/test/java/org/apache/druid/server/QueryResourceTest.java index 0a24b0edb8ad..2ed826640c46 100644 --- a/server/src/test/java/org/apache/druid/server/QueryResourceTest.java +++ b/server/src/test/java/org/apache/druid/server/QueryResourceTest.java @@ -550,7 +550,7 @@ public void testSecuredQuery() throws Exception EasyMock.replay(testServletRequest); - AuthorizerMapper authMapper = new AuthorizerMapper(null) + AuthorizerMapper authMapper = new AuthorizerMapper(null, null) { @Override public Authorizer getAuthorizer(String name) @@ -651,7 +651,7 @@ public void testSecuredCancelQuery() throws Exception EasyMock.replay(testServletRequest); - AuthorizerMapper authMapper = new AuthorizerMapper(null) + AuthorizerMapper authMapper = new AuthorizerMapper(null, null) { @Override public Authorizer getAuthorizer(String name) @@ -781,7 +781,7 @@ public void testDenySecuredCancelQuery() throws Exception EasyMock.replay(testServletRequest); - AuthorizerMapper authMapper = new AuthorizerMapper(null) + AuthorizerMapper authMapper = new AuthorizerMapper(null, null) { @Override public Authorizer getAuthorizer(String name) diff --git a/server/src/test/java/org/apache/druid/server/http/CompactionResourceTest.java b/server/src/test/java/org/apache/druid/server/http/CompactionResourceTest.java index 9e7cfd601eae..c3a2a8627d31 100644 --- a/server/src/test/java/org/apache/druid/server/http/CompactionResourceTest.java +++ b/server/src/test/java/org/apache/druid/server/http/CompactionResourceTest.java @@ -23,15 +23,27 @@ import com.google.common.collect.ImmutableMap; import org.apache.druid.server.coordinator.AutoCompactionSnapshot; import org.apache.druid.server.coordinator.DruidCoordinator; +import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthConfig; +import org.apache.druid.server.security.AuthenticationResult; +import org.apache.druid.server.security.Authorizer; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.server.security.Resource; import org.easymock.EasyMock; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import javax.servlet.http.HttpServletRequest; import javax.ws.rs.core.Response; +import java.util.Arrays; import java.util.Map; +@RunWith(Parameterized.class) public class CompactionResourceTest { private DruidCoordinator mock; @@ -49,6 +61,47 @@ public class CompactionResourceTest 1, 1 ); + private AuthorizerMapper authorizerMapper; + private HttpServletRequest request; + + @Parameterized.Parameters(name = "{index}: authVersion={0}") + public static Iterable data() + { + return Arrays.asList(AuthConfig.AUTH_VERSION_1, AuthConfig.AUTH_VERSION_2); + } + + public CompactionResourceTest(String authVersion) + { + authorizerMapper = new AuthorizerMapper( + ImmutableMap.of("auth1", + new Authorizer() + { + @Override + public Access authorize(AuthenticationResult authenticationResult, Resource resource, Action action) + { + return new Access(true); + } + + @Override + public Access authorizeV2(AuthenticationResult authenticationResult, Resource resource, Action action) + { + return new Access(true); + } + } + ), + authVersion + ); + request = EasyMock.createMock(HttpServletRequest.class); + if (authVersion.equals(AuthConfig.AUTH_VERSION_2)) { + EasyMock.expect(request.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH)).andReturn(null).once(); + EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).once(); + EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)) + .andReturn(new AuthenticationResult("", "auth1", "", null)).once(); + request.setAttribute(EasyMock.anyString(), EasyMock.anyObject()); + EasyMock.expectLastCall().once(); + } + EasyMock.replay(request); + } @Before public void setUp() @@ -59,7 +112,7 @@ public void setUp() @After public void tearDown() { - EasyMock.verify(mock); + EasyMock.verify(mock, request); } @Test @@ -73,7 +126,7 @@ public void testGetCompactionSnapshotForDataSourceWithEmptyQueryParameter() EasyMock.expect(mock.getAutoCompactionSnapshot()).andReturn(expected).once(); EasyMock.replay(mock); - final Response response = new CompactionResource(mock).getCompactionSnapshotForDataSource(""); + final Response response = new CompactionResource(mock, authorizerMapper).getCompactionSnapshotForDataSource("", request); Assert.assertEquals(ImmutableMap.of("latestStatus", expected.values()), response.getEntity()); Assert.assertEquals(200, response.getStatus()); } @@ -90,7 +143,7 @@ public void testGetCompactionSnapshotForDataSourceWithNullQueryParameter() EasyMock.expect(mock.getAutoCompactionSnapshot()).andReturn(expected).once(); EasyMock.replay(mock); - final Response response = new CompactionResource(mock).getCompactionSnapshotForDataSource(null); + final Response response = new CompactionResource(mock, authorizerMapper).getCompactionSnapshotForDataSource(null, request); Assert.assertEquals(ImmutableMap.of("latestStatus", expected.values()), response.getEntity()); Assert.assertEquals(200, response.getStatus()); } @@ -103,7 +156,7 @@ public void testGetCompactionSnapshotForDataSourceWithValidQueryParameter() EasyMock.expect(mock.getAutoCompactionSnapshotForDataSource(dataSourceName)).andReturn(expectedSnapshot).once(); EasyMock.replay(mock); - final Response response = new CompactionResource(mock).getCompactionSnapshotForDataSource(dataSourceName); + final Response response = new CompactionResource(mock, authorizerMapper).getCompactionSnapshotForDataSource(dataSourceName, request); Assert.assertEquals(ImmutableMap.of("latestStatus", ImmutableList.of(expectedSnapshot)), response.getEntity()); Assert.assertEquals(200, response.getStatus()); } @@ -116,7 +169,7 @@ public void testGetCompactionSnapshotForDataSourceWithInvalidQueryParameter() EasyMock.expect(mock.getAutoCompactionSnapshotForDataSource(dataSourceName)).andReturn(null).once(); EasyMock.replay(mock); - final Response response = new CompactionResource(mock).getCompactionSnapshotForDataSource(dataSourceName); + final Response response = new CompactionResource(mock, authorizerMapper).getCompactionSnapshotForDataSource(dataSourceName, request); Assert.assertEquals(400, response.getStatus()); } } diff --git a/server/src/test/java/org/apache/druid/server/http/DataSourcesResourceTest.java b/server/src/test/java/org/apache/druid/server/http/DataSourcesResourceTest.java index 02747a354970..5c3e88821ce9 100644 --- a/server/src/test/java/org/apache/druid/server/http/DataSourcesResourceTest.java +++ b/server/src/test/java/org/apache/druid/server/http/DataSourcesResourceTest.java @@ -234,7 +234,7 @@ public void testSecuredGetFullQueryableDataSources() EasyMock.expect(inventoryView.getInventory()).andReturn(ImmutableList.of(server)).once(); EasyMock.replay(inventoryView, server, request); - AuthorizerMapper authMapper = new AuthorizerMapper(null) { + AuthorizerMapper authMapper = new AuthorizerMapper(null, null) { @Override public Authorizer getAuthorizer(String name) { diff --git a/server/src/test/java/org/apache/druid/server/http/LookupCoordinatorResourceTest.java b/server/src/test/java/org/apache/druid/server/http/LookupCoordinatorResourceTest.java index 365eac6888ed..560dd1a05e32 100644 --- a/server/src/test/java/org/apache/druid/server/http/LookupCoordinatorResourceTest.java +++ b/server/src/test/java/org/apache/druid/server/http/LookupCoordinatorResourceTest.java @@ -27,14 +27,25 @@ import com.google.common.net.HostAndPort; import org.apache.druid.audit.AuditInfo; import org.apache.druid.jackson.DefaultObjectMapper; +import org.apache.druid.java.util.common.ISE; import org.apache.druid.java.util.common.StringUtils; import org.apache.druid.query.lookup.LookupsState; import org.apache.druid.server.lookup.cache.LookupCoordinatorManager; import org.apache.druid.server.lookup.cache.LookupExtractorFactoryMapContainer; +import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthConfig; +import org.apache.druid.server.security.AuthenticationResult; +import org.apache.druid.server.security.Authorizer; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.server.security.Resource; import org.easymock.Capture; import org.easymock.EasyMock; +import org.junit.After; import org.junit.Assert; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.core.MediaType; @@ -43,10 +54,12 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Set; +@RunWith(Parameterized.class) public class LookupCoordinatorResourceTest { private static final ObjectMapper MAPPER = new DefaultObjectMapper(); @@ -90,6 +103,68 @@ public InputStream openStream() throws IOException private static final Map> NODES_LOOKUP_STATE = ImmutableMap.of(LOOKUP_NODE, LOOKUP_STATE); + @Parameterized.Parameters(name = "{index}: authVersion={0}") + public static Iterable data() + { + return Arrays.asList(AuthConfig.AUTH_VERSION_1, AuthConfig.AUTH_VERSION_2); + } + + private AuthorizerMapper authorizerMapper; + private HttpServletRequest request; + private boolean verifyRequestMock = false; + + public LookupCoordinatorResourceTest(String authVersion) + { + this.request = EasyMock.createMock(HttpServletRequest.class); + + this.authorizerMapper = new AuthorizerMapper( + ImmutableMap.of("auth1", + new Authorizer() + { + @Override + public Access authorize(AuthenticationResult authenticationResult, Resource resource, Action action) + { + if (authVersion.equals(AuthConfig.AUTH_VERSION_2)) { + throw new ISE("Unexpected call"); + } + return new Access(true); + } + + @Override + public Access authorizeV2(AuthenticationResult authenticationResult, Resource resource, Action action) + { + if (authVersion.equals(AuthConfig.AUTH_VERSION_1)) { + throw new ISE("Unexpected call"); + } + return new Access(true); + } + } + ), + authVersion + ); + } + + private void setupRequestExpectation() + { + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + EasyMock.expect(request.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH)).andReturn(null).anyTimes(); + EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).anyTimes(); + EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)) + .andReturn(new AuthenticationResult("", "auth1", "", null)).anyTimes(); + request.setAttribute(EasyMock.anyString(), EasyMock.anyObject()); + EasyMock.expectLastCall().anyTimes(); + } + verifyRequestMock = true; + } + + @After + public void tearDown() + { + if (verifyRequestMock) { + EasyMock.verify(request); + } + } + @Test public void testSimpleGet() { @@ -101,7 +176,8 @@ public void testSimpleGet() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); final Response response = lookupCoordinatorResource.getTiers(false); Assert.assertEquals(200, response.getStatus()); @@ -119,7 +195,8 @@ public void testMissingGet() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); final Response response = lookupCoordinatorResource.getTiers(false); Assert.assertEquals(404, response.getStatus()); @@ -137,7 +214,8 @@ public void testExceptionalGet() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); final Response response = lookupCoordinatorResource.getTiers(false); Assert.assertEquals(500, response.getStatus()); @@ -157,7 +235,8 @@ public void testDiscoveryGet() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); final Response response = lookupCoordinatorResource.getTiers(true); Assert.assertEquals(200, response.getStatus()); @@ -179,7 +258,8 @@ public void testDiscoveryExceptionalGet() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); final Response response = lookupCoordinatorResource.getTiers(true); Assert.assertEquals(500, response.getStatus()); @@ -203,9 +283,12 @@ public void testSimpleGetLookup() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); - final Response response = lookupCoordinatorResource.getSpecificLookup(LOOKUP_TIER, LOOKUP_NAME); + setupRequestExpectation(); + EasyMock.replay(request); + final Response response = lookupCoordinatorResource.getSpecificLookup(LOOKUP_TIER, LOOKUP_NAME, request); Assert.assertEquals(200, response.getStatus()); Assert.assertEquals(container, response.getEntity()); EasyMock.verify(lookupCoordinatorManager); @@ -220,9 +303,12 @@ public void testDetailedGetLookup() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); - final Response response = lookupCoordinatorResource.getSpecificTier(LOOKUP_TIER, true); + setupRequestExpectation(); + EasyMock.replay(request); + final Response response = lookupCoordinatorResource.getSpecificTier(LOOKUP_TIER, true, request); Assert.assertEquals(200, response.getStatus()); Assert.assertEquals(SINGLE_TIER_MAP.get(LOOKUP_TIER), response.getEntity()); EasyMock.verify(lookupCoordinatorManager); @@ -240,9 +326,12 @@ public void testMissingGetLookup() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); - final Response response = lookupCoordinatorResource.getSpecificLookup(LOOKUP_TIER, LOOKUP_NAME); + setupRequestExpectation(); + EasyMock.replay(request); + final Response response = lookupCoordinatorResource.getSpecificLookup(LOOKUP_TIER, LOOKUP_NAME, request); Assert.assertEquals(404, response.getStatus()); EasyMock.verify(lookupCoordinatorManager); } @@ -256,12 +345,15 @@ public void testInvalidGetLookup() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER - ); - Assert.assertEquals(400, lookupCoordinatorResource.getSpecificLookup("foo", null).getStatus()); - Assert.assertEquals(400, lookupCoordinatorResource.getSpecificLookup("foo", "").getStatus()); - Assert.assertEquals(400, lookupCoordinatorResource.getSpecificLookup("", "foo").getStatus()); - Assert.assertEquals(400, lookupCoordinatorResource.getSpecificLookup(null, "foo").getStatus()); + MAPPER, + authorizerMapper + ); + setupRequestExpectation(); + EasyMock.replay(request); + Assert.assertEquals(400, lookupCoordinatorResource.getSpecificLookup("foo", null, request).getStatus()); + Assert.assertEquals(400, lookupCoordinatorResource.getSpecificLookup("foo", "", request).getStatus()); + Assert.assertEquals(400, lookupCoordinatorResource.getSpecificLookup("", "foo", request).getStatus()); + Assert.assertEquals(400, lookupCoordinatorResource.getSpecificLookup(null, "foo", request).getStatus()); EasyMock.verify(lookupCoordinatorManager); } @@ -278,9 +370,12 @@ public void testExceptionalGetLookup() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); - final Response response = lookupCoordinatorResource.getSpecificLookup(LOOKUP_TIER, LOOKUP_NAME); + setupRequestExpectation(); + EasyMock.replay(request); + final Response response = lookupCoordinatorResource.getSpecificLookup(LOOKUP_TIER, LOOKUP_NAME, request); Assert.assertEquals(500, response.getStatus()); Assert.assertEquals(ImmutableMap.of("error", errMsg), response.getEntity()); EasyMock.verify(lookupCoordinatorManager); @@ -293,12 +388,16 @@ public void testSimpleDeleteTier() final String comment = "some comment"; final String ip = "127.0.0.1"; - final HttpServletRequest request = EasyMock.createStrictMock(HttpServletRequest.class); + setupRequestExpectation(); EasyMock.expect(request.getRemoteAddr()).andReturn(ip).once(); final Capture auditInfoCapture = Capture.newInstance(); final LookupCoordinatorManager lookupCoordinatorManager = EasyMock.createStrictMock( LookupCoordinatorManager.class); + if (authorizerMapper.getAuthVersion().equals(AuthConfig.AUTH_VERSION_2)) { + // in auth v2 write access to all lookups are checked before deleting the tier + EasyMock.expect(lookupCoordinatorManager.getTierLookups(LOOKUP_TIER)).andReturn(SINGLE_LOOKUP_MAP).once(); + } EasyMock.expect(lookupCoordinatorManager.deleteTier( EasyMock.eq(LOOKUP_TIER), EasyMock.capture(auditInfoCapture) @@ -309,7 +408,8 @@ public void testSimpleDeleteTier() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); final Response response = lookupCoordinatorResource.deleteTier( LOOKUP_TIER, @@ -335,7 +435,7 @@ public void testSimpleDelete() final String comment = "some comment"; final String ip = "127.0.0.1"; - final HttpServletRequest request = EasyMock.createStrictMock(HttpServletRequest.class); + setupRequestExpectation(); EasyMock.expect(request.getRemoteAddr()).andReturn(ip).once(); final Capture auditInfoCapture = Capture.newInstance(); @@ -352,7 +452,8 @@ public void testSimpleDelete() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); final Response response = lookupCoordinatorResource.deleteLookup( LOOKUP_TIER, @@ -380,7 +481,7 @@ public void testMissingDelete() final String comment = "some comment"; final String ip = "127.0.0.1"; - final HttpServletRequest request = EasyMock.createStrictMock(HttpServletRequest.class); + setupRequestExpectation(); EasyMock.expect(request.getRemoteAddr()).andReturn(ip).once(); final Capture auditInfoCapture = Capture.newInstance(); @@ -397,7 +498,8 @@ public void testMissingDelete() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); final Response response = lookupCoordinatorResource.deleteLookup( LOOKUP_TIER, @@ -426,7 +528,7 @@ public void testExceptionalDelete() final String ip = "127.0.0.1"; final String errMsg = "some error"; - final HttpServletRequest request = EasyMock.createStrictMock(HttpServletRequest.class); + setupRequestExpectation(); EasyMock.expect(request.getRemoteAddr()).andReturn(ip).once(); final Capture auditInfoCapture = Capture.newInstance(); @@ -443,7 +545,8 @@ public void testExceptionalDelete() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); final Response response = lookupCoordinatorResource.deleteLookup( LOOKUP_TIER, @@ -473,13 +576,16 @@ public void testInvalidDelete() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER - ); - Assert.assertEquals(400, lookupCoordinatorResource.deleteLookup("foo", null, null, null, null).getStatus()); - Assert.assertEquals(400, lookupCoordinatorResource.deleteLookup(null, null, null, null, null).getStatus()); - Assert.assertEquals(400, lookupCoordinatorResource.deleteLookup(null, "foo", null, null, null).getStatus()); - Assert.assertEquals(400, lookupCoordinatorResource.deleteLookup("foo", "", null, null, null).getStatus()); - Assert.assertEquals(400, lookupCoordinatorResource.deleteLookup("", "foo", null, null, null).getStatus()); + MAPPER, + authorizerMapper + ); + setupRequestExpectation(); + EasyMock.replay(request); + Assert.assertEquals(400, lookupCoordinatorResource.deleteLookup("foo", null, null, null, request).getStatus()); + Assert.assertEquals(400, lookupCoordinatorResource.deleteLookup(null, null, null, null, request).getStatus()); + Assert.assertEquals(400, lookupCoordinatorResource.deleteLookup(null, "foo", null, null, request).getStatus()); + Assert.assertEquals(400, lookupCoordinatorResource.deleteLookup("foo", "", null, null, request).getStatus()); + Assert.assertEquals(400, lookupCoordinatorResource.deleteLookup("", "foo", null, null, request).getStatus()); EasyMock.verify(lookupCoordinatorManager); } @@ -489,8 +595,7 @@ public void testSimpleNew() throws Exception final String author = "some author"; final String comment = "some comment"; final String ip = "127.0.0.1"; - - final HttpServletRequest request = EasyMock.createStrictMock(HttpServletRequest.class); + setupRequestExpectation(); EasyMock.expect(request.getContentType()).andReturn(MediaType.APPLICATION_JSON).once(); EasyMock.expect(request.getRemoteAddr()).andReturn(ip).once(); final Capture auditInfoCapture = Capture.newInstance(); @@ -505,7 +610,8 @@ public void testSimpleNew() throws Exception final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); final Response response = lookupCoordinatorResource.updateAllLookups( SINGLE_TIER_MAP_SOURCE.openStream(), @@ -532,7 +638,7 @@ public void testExceptionalNew() throws Exception final String ip = "127.0.0.1"; final String errMsg = "some error"; - final HttpServletRequest request = EasyMock.createStrictMock(HttpServletRequest.class); + setupRequestExpectation(); EasyMock.expect(request.getContentType()).andReturn(MediaType.APPLICATION_JSON).once(); EasyMock.expect(request.getRemoteAddr()).andReturn(ip).once(); @@ -549,7 +655,8 @@ public void testExceptionalNew() throws Exception final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); final Response response = lookupCoordinatorResource.updateAllLookups( SINGLE_TIER_MAP_SOURCE.openStream(), @@ -576,7 +683,7 @@ public void testFailedNew() throws Exception final String comment = "some comment"; final String ip = "127.0.0.1"; - final HttpServletRequest request = EasyMock.createStrictMock(HttpServletRequest.class); + setupRequestExpectation(); EasyMock.expect(request.getContentType()).andReturn(MediaType.APPLICATION_JSON).once(); EasyMock.expect(request.getRemoteAddr()).andReturn(ip).once(); @@ -593,7 +700,8 @@ public void testFailedNew() throws Exception final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); final Response response = lookupCoordinatorResource.updateAllLookups( SINGLE_TIER_MAP_SOURCE.openStream(), @@ -620,7 +728,7 @@ public void testSimpleNewLookup() throws Exception final String comment = "some comment"; final String ip = "127.0.0.1"; - final HttpServletRequest request = EasyMock.createStrictMock(HttpServletRequest.class); + setupRequestExpectation(); EasyMock.expect(request.getContentType()).andReturn(MediaType.APPLICATION_JSON).once(); EasyMock.expect(request.getRemoteAddr()).andReturn(ip).once(); @@ -639,7 +747,8 @@ public void testSimpleNewLookup() throws Exception final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); final Response response = lookupCoordinatorResource.createOrUpdateLookup( LOOKUP_TIER, @@ -668,7 +777,7 @@ public void testDBErrNewLookup() throws Exception final String comment = "some comment"; final String ip = "127.0.0.1"; - final HttpServletRequest request = EasyMock.createStrictMock(HttpServletRequest.class); + setupRequestExpectation(); EasyMock.expect(request.getContentType()).andReturn(MediaType.APPLICATION_JSON).once(); EasyMock.expect(request.getRemoteAddr()).andReturn(ip).once(); @@ -687,7 +796,8 @@ public void testDBErrNewLookup() throws Exception final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); final Response response = lookupCoordinatorResource.createOrUpdateLookup( LOOKUP_TIER, @@ -717,7 +827,7 @@ public void testExceptionalNewLookup() throws Exception final String comment = "some comment"; final String ip = "127.0.0.1"; - final HttpServletRequest request = EasyMock.createStrictMock(HttpServletRequest.class); + setupRequestExpectation(); EasyMock.expect(request.getContentType()).andReturn(MediaType.APPLICATION_JSON).once(); EasyMock.expect(request.getRemoteAddr()).andReturn(ip).once(); @@ -736,7 +846,8 @@ public void testExceptionalNewLookup() throws Exception final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); final Response response = lookupCoordinatorResource.createOrUpdateLookup( LOOKUP_TIER, @@ -774,7 +885,8 @@ public void testNullValsNewLookup() throws Exception final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); EasyMock.replay(lookupCoordinatorManager, request); @@ -825,9 +937,12 @@ public void testSimpleGetTier() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); - final Response response = lookupCoordinatorResource.getSpecificTier(LOOKUP_TIER, false); + setupRequestExpectation(); + EasyMock.replay(request); + final Response response = lookupCoordinatorResource.getSpecificTier(LOOKUP_TIER, false, request); Assert.assertEquals(200, response.getStatus()); Assert.assertEquals(SINGLE_TIER_MAP.get(LOOKUP_TIER).keySet(), response.getEntity()); EasyMock.verify(lookupCoordinatorManager); @@ -845,9 +960,12 @@ public void testMissingGetTier() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); - final Response response = lookupCoordinatorResource.getSpecificTier(tier, false); + setupRequestExpectation(); + EasyMock.replay(request); + final Response response = lookupCoordinatorResource.getSpecificTier(tier, false, request); Assert.assertEquals(404, response.getStatus()); EasyMock.verify(lookupCoordinatorManager); } @@ -861,9 +979,12 @@ public void testNullGetTier() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); - final Response response = lookupCoordinatorResource.getSpecificTier(tier, false); + setupRequestExpectation(); + EasyMock.replay(request); + final Response response = lookupCoordinatorResource.getSpecificTier(tier, false, request); Assert.assertEquals(400, response.getStatus()); Assert.assertEquals(ImmutableMap.of("error", "`tier` required"), response.getEntity()); EasyMock.verify(lookupCoordinatorManager); @@ -879,9 +1000,12 @@ public void testNullLookupsGetTier() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); - final Response response = lookupCoordinatorResource.getSpecificTier(tier, false); + setupRequestExpectation(); + EasyMock.replay(request); + final Response response = lookupCoordinatorResource.getSpecificTier(tier, false, request); Assert.assertEquals(404, response.getStatus()); Assert.assertEquals(ImmutableMap.of("error", "No lookups found"), response.getEntity()); EasyMock.verify(lookupCoordinatorManager); @@ -898,9 +1022,12 @@ public void testExceptionalGetTier() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); - final Response response = lookupCoordinatorResource.getSpecificTier(tier, false); + setupRequestExpectation(); + EasyMock.replay(request); + final Response response = lookupCoordinatorResource.getSpecificTier(tier, false, request); Assert.assertEquals(500, response.getStatus()); Assert.assertEquals(ImmutableMap.of("error", errMsg), response.getEntity()); EasyMock.verify(lookupCoordinatorManager); @@ -921,10 +1048,12 @@ public void testGetAllLookupsStatus() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); - - final Response response = lookupCoordinatorResource.getAllLookupsStatus(false); + setupRequestExpectation(); + EasyMock.replay(request); + final Response response = lookupCoordinatorResource.getAllLookupsStatus(false, request); Assert.assertEquals(200, response.getStatus()); Assert.assertEquals( ImmutableMap.of( @@ -954,10 +1083,12 @@ public void testGetLookupStatusForTier() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); - - final Response response = lookupCoordinatorResource.getLookupStatusForTier(LOOKUP_TIER, false); + setupRequestExpectation(); + EasyMock.replay(request); + final Response response = lookupCoordinatorResource.getLookupStatusForTier(LOOKUP_TIER, false, request); Assert.assertEquals(200, response.getStatus()); Assert.assertEquals( ImmutableMap.of( @@ -984,10 +1115,12 @@ public void testGetSpecificLookupStatus() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); - - final Response response = lookupCoordinatorResource.getSpecificLookupStatus(LOOKUP_TIER, LOOKUP_NAME, false); + setupRequestExpectation(); + EasyMock.replay(request); + final Response response = lookupCoordinatorResource.getSpecificLookupStatus(LOOKUP_TIER, LOOKUP_NAME, false, request); Assert.assertEquals(200, response.getStatus()); Assert.assertEquals( new LookupCoordinatorResource.LookupStatus(true, null), response.getEntity() @@ -1002,7 +1135,8 @@ public void testGetLookupStatusDetailedTrue() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( EasyMock.createStrictMock(LookupCoordinatorManager.class), MAPPER, - MAPPER + MAPPER, + authorizerMapper ); HostAndPort newNode = HostAndPort.fromParts("localhost", 4352); @@ -1024,7 +1158,8 @@ public void testGetLookupStatusDetailedFalse() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( EasyMock.createStrictMock(LookupCoordinatorManager.class), MAPPER, - MAPPER + MAPPER, + authorizerMapper ); HostAndPort newNode = HostAndPort.fromParts("localhost", 4352); @@ -1055,7 +1190,8 @@ public void testGetAllNodesStatus() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); final Response response = lookupCoordinatorResource.getAllNodesStatus(false, null); @@ -1089,7 +1225,8 @@ public void testGetAllNodesStatusDetailedFalse() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); final Response response = lookupCoordinatorResource.getAllNodesStatus(false, false); @@ -1124,7 +1261,8 @@ public void testGetNodesStatusInTier() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); final Response response = lookupCoordinatorResource.getNodesStatusInTier(LOOKUP_TIER); @@ -1152,7 +1290,8 @@ public void testGetSpecificNodeStatus() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); final Response response = lookupCoordinatorResource.getSpecificNodeStatus(LOOKUP_TIER, LOOKUP_NODE); @@ -1198,9 +1337,12 @@ public void testGetAllLookupSpecs() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); - final Response response = lookupCoordinatorResource.getAllLookupSpecs(); + setupRequestExpectation(); + EasyMock.replay(request); + final Response response = lookupCoordinatorResource.getAllLookupSpecs(request); Assert.assertEquals(Status.OK.getStatusCode(), response.getStatus()); Assert.assertEquals(lookups, response.getEntity()); EasyMock.verify(lookupCoordinatorManager); @@ -1219,9 +1361,12 @@ public void testGetEmptyAllLookupSpecs() final LookupCoordinatorResource lookupCoordinatorResource = new LookupCoordinatorResource( lookupCoordinatorManager, MAPPER, - MAPPER + MAPPER, + authorizerMapper ); - final Response response = lookupCoordinatorResource.getAllLookupSpecs(); + setupRequestExpectation(); + EasyMock.replay(request); + final Response response = lookupCoordinatorResource.getAllLookupSpecs(request); Assert.assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus()); EasyMock.verify(lookupCoordinatorManager); } diff --git a/server/src/test/java/org/apache/druid/server/http/RulesResourceTest.java b/server/src/test/java/org/apache/druid/server/http/RulesResourceTest.java index 5b52e3d30ee9..ca3edfa7bac5 100644 --- a/server/src/test/java/org/apache/druid/server/http/RulesResourceTest.java +++ b/server/src/test/java/org/apache/druid/server/http/RulesResourceTest.java @@ -20,18 +20,29 @@ package org.apache.druid.server.http; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import org.apache.druid.audit.AuditEntry; import org.apache.druid.audit.AuditInfo; import org.apache.druid.audit.AuditManager; import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.Intervals; import org.apache.druid.metadata.MetadataRuleManager; +import org.apache.druid.server.coordinator.rules.ForeverLoadRule; +import org.apache.druid.server.coordinator.rules.Rule; +import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthConfig; +import org.apache.druid.server.security.AuthenticationResult; +import org.apache.druid.server.security.Authorizer; +import org.apache.druid.server.security.AuthorizerMapper; +import org.apache.druid.server.security.Resource; import org.easymock.EasyMock; import org.joda.time.Interval; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import javax.servlet.http.HttpServletRequest; import javax.ws.rs.core.Response; import java.util.List; import java.util.Map; @@ -48,6 +59,62 @@ public void setUp() auditManager = EasyMock.createStrictMock(AuditManager.class); } + @Test + public void testGetRules() + { + final String authorizerName = "testAuthorizer"; + + EasyMock.expect(databaseRuleManager.getAllRules()).andReturn( + ImmutableMap.of( + "ds1", + ImmutableList.of(new ForeverLoadRule(null)), + "ds2", + ImmutableList.of(new ForeverLoadRule(null)) + ) + ).once(); + + RulesResource rulesResource = new RulesResource(databaseRuleManager, auditManager, new AuthorizerMapper( + ImmutableMap.of(authorizerName, + new Authorizer() + { + @Override + public Access authorize(AuthenticationResult authenticationResult, Resource resource, + Action action + ) + { + return null; + } + + @Override + public Access authorizeV2(AuthenticationResult authenticationResult, Resource resource, Action action) + { + if (resource.getName().equals("ds1")) { + return new Access(true); + } + return new Access(false); + } + } + ), AuthConfig.AUTH_VERSION_2)); + + final HttpServletRequest request = EasyMock.createMock(HttpServletRequest.class); + EasyMock.expect(request.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH)).andReturn(null).once(); + EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).once(); + EasyMock.expect(request.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)) + .andReturn(new AuthenticationResult("", authorizerName, "", null)).once(); + request.setAttribute(EasyMock.anyString(), EasyMock.anyObject()); + EasyMock.expectLastCall().once(); + + EasyMock.replay(databaseRuleManager, request); + + Response response = rulesResource.getRules(request); + final Map> rules = (Map>) response.getEntity(); + Assert.assertEquals(1, rules.size()); + Assert.assertEquals(1, rules.get("ds1").size()); + Assert.assertEquals(new ForeverLoadRule(null), rules.get("ds1").get(0)); + + EasyMock.verify(databaseRuleManager, request); + } + @Test public void testGetDatasourceRuleHistoryWithCount() { @@ -78,7 +145,7 @@ public void testGetDatasourceRuleHistoryWithCount() .once(); EasyMock.replay(auditManager); - RulesResource rulesResource = new RulesResource(databaseRuleManager, auditManager); + RulesResource rulesResource = new RulesResource(databaseRuleManager, auditManager, new AuthorizerMapper(null, null)); Response response = rulesResource.getDatasourceRuleHistory("datasource1", null, 2); List rulesHistory = (List) response.getEntity(); @@ -121,7 +188,7 @@ public void testGetDatasourceRuleHistoryWithInterval() .once(); EasyMock.replay(auditManager); - RulesResource rulesResource = new RulesResource(databaseRuleManager, auditManager); + RulesResource rulesResource = new RulesResource(databaseRuleManager, auditManager, new AuthorizerMapper(null, null)); Response response = rulesResource.getDatasourceRuleHistory("datasource1", interval, null); List rulesHistory = (List) response.getEntity(); @@ -140,7 +207,7 @@ public void testGetDatasourceRuleHistoryWithWrongCount() .once(); EasyMock.replay(auditManager); - RulesResource rulesResource = new RulesResource(databaseRuleManager, auditManager); + RulesResource rulesResource = new RulesResource(databaseRuleManager, auditManager, new AuthorizerMapper(null, null)); Response response = rulesResource.getDatasourceRuleHistory("datasource1", null, -1); Map rulesHistory = (Map) response.getEntity(); @@ -181,7 +248,7 @@ public void testGetAllDatasourcesRuleHistoryWithCount() .once(); EasyMock.replay(auditManager); - RulesResource rulesResource = new RulesResource(databaseRuleManager, auditManager); + RulesResource rulesResource = new RulesResource(databaseRuleManager, auditManager, new AuthorizerMapper(null, null)); Response response = rulesResource.getDatasourceRuleHistory(null, 2); List rulesHistory = (List) response.getEntity(); @@ -224,7 +291,7 @@ public void testGetAllDatasourcesRuleHistoryWithInterval() .once(); EasyMock.replay(auditManager); - RulesResource rulesResource = new RulesResource(databaseRuleManager, auditManager); + RulesResource rulesResource = new RulesResource(databaseRuleManager, auditManager, new AuthorizerMapper(null, null)); Response response = rulesResource.getDatasourceRuleHistory(interval, null); List rulesHistory = (List) response.getEntity(); @@ -243,7 +310,7 @@ public void testGetAllDatasourcesRuleHistoryWithWrongCount() .once(); EasyMock.replay(auditManager); - RulesResource rulesResource = new RulesResource(databaseRuleManager, auditManager); + RulesResource rulesResource = new RulesResource(databaseRuleManager, auditManager, new AuthorizerMapper(null, null)); Response response = rulesResource.getDatasourceRuleHistory(null, -1); Map rulesHistory = (Map) response.getEntity(); diff --git a/server/src/test/java/org/apache/druid/server/http/security/ResourceFilterTestHelper.java b/server/src/test/java/org/apache/druid/server/http/security/ResourceFilterTestHelper.java index 95a0cc29e31a..ca0aaa534f11 100644 --- a/server/src/test/java/org/apache/druid/server/http/security/ResourceFilterTestHelper.java +++ b/server/src/test/java/org/apache/druid/server/http/security/ResourceFilterTestHelper.java @@ -23,7 +23,9 @@ import com.google.common.base.Predicate; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; import com.google.inject.Binder; import com.google.inject.Guice; import com.google.inject.Injector; @@ -32,6 +34,7 @@ import com.sun.jersey.spi.container.ContainerRequest; import com.sun.jersey.spi.container.ResourceFilter; import com.sun.jersey.spi.container.ResourceFilters; +import org.apache.druid.java.util.common.Pair; import org.apache.druid.java.util.common.StringUtils; import org.apache.druid.server.security.Access; import org.apache.druid.server.security.Action; @@ -55,9 +58,29 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; public class ResourceFilterTestHelper { + private static final Set> LEGACY_FILTERS = ImmutableSet.of( + ConfigResourceFilter.class, + StateResourceFilter.class + ); + + public static Set> currentFilters = Sets.newHashSet( + DatasourceResourceFilter.class, + RulesResourceFilter.class + ); + + private static final Set> NEW_FILTERS = ImmutableSet.of( + InternalInternalResourceFilter.class, + ServerServerResourceFilter.class, + ServerStatusResourceFilter.class, + ServerUserResourceFilter.class + ); + public HttpServletRequest req; public AuthorizerMapper authorizerMapper; public ContainerRequest request; @@ -66,7 +89,7 @@ public void setUp(ResourceFilter resourceFilter) { req = EasyMock.createStrictMock(HttpServletRequest.class); request = EasyMock.createStrictMock(ContainerRequest.class); - authorizerMapper = EasyMock.createStrictMock(AuthorizerMapper.class); + authorizerMapper = EasyMock.createMock(AuthorizerMapper.class); // Memory barrier synchronized (this) { @@ -78,7 +101,9 @@ public void setUp(ResourceFilter resourceFilter) public void setUpMockExpectations( String requestPath, boolean authCheckResult, - String requestMethod + String requestMethod, + String authVersion, + boolean performAuth ) { EasyMock.expect(request.getPath()).andReturn(requestPath).anyTimes(); @@ -114,24 +139,32 @@ public MultivaluedMap getMatrixParameters() EasyMock.expect(req.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH)).andReturn(null).anyTimes(); EasyMock.expect(req.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED)).andReturn(null).anyTimes(); AuthenticationResult authenticationResult = new AuthenticationResult("druid", "druid", null, null); - EasyMock.expect(req.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)) - .andReturn(authenticationResult) - .atLeastOnce(); - req.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, authCheckResult); - EasyMock.expectLastCall().anyTimes(); - EasyMock.expect(authorizerMapper.getAuthorizer( - EasyMock.anyString() - )).andReturn( - new Authorizer() - { - @Override - public Access authorize(AuthenticationResult authenticationResult1, Resource resource, Action action) + + EasyMock.expect(authorizerMapper.getAuthVersion()).andReturn(authVersion).anyTimes(); + if (performAuth) { + EasyMock.expect(req.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)) + .andReturn(authenticationResult) + .atLeastOnce(); + EasyMock.expect(authorizerMapper.getAuthorizer( + EasyMock.anyString() + )).andReturn( + new Authorizer() { - return new Access(authCheckResult); + @Override + public Access authorize(AuthenticationResult authenticationResult1, Resource resource, Action action) + { + return new Access(authCheckResult); + } + @Override + public Access authorizeV2(AuthenticationResult authenticationResult1, Resource resource, Action action) + { + return new Access(authCheckResult); + } } - - } - ).atLeastOnce(); + ).atLeastOnce(); + req.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, authCheckResult); + EasyMock.expectLastCall().anyTimes(); + } } public static Collection getRequestPathsWithAuthorizer(final AnnotatedElement classOrMethod) @@ -229,28 +262,38 @@ public Collection apply(final Method method) { final List> resourceFilters = method.getAnnotation(ResourceFilters.class) == null ? baseResourceFilters : - ImmutableList.copyOf(method.getAnnotation(ResourceFilters.class).value()); - + ImmutableList.copyOf(method.getAnnotation(ResourceFilters.class).value()); + final List, String>> filterVersionPairs = resourceFilters + .stream().flatMap( + (java.util.function.Function, Stream, String>>>) filter -> + Stream.of( + new Pair<>(filter, AuthConfig.AUTH_VERSION_1), + new Pair<>(filter, AuthConfig.AUTH_VERSION_2) + )).collect(Collectors.toList()); return Collections2.transform( - resourceFilters, - new Function, Object[]>() + filterVersionPairs, + new Function, String>, Object[]>() { @Override - public Object[] apply(Class input) + public Object[] apply(Pair, String> filterVersionPair) { if (method.getAnnotation(Path.class) != null) { - return new Object[]{ + return new Object[] { StringUtils.format("%s%s", basepath, method.getAnnotation(Path.class).value()), - httpMethodFromAnnotation(input, method), - injector.getInstance(input), - injector + httpMethodFromAnnotation(filterVersionPair.lhs, method), + injector.getInstance(filterVersionPair.lhs), + injector, + filterVersionPair.rhs, + performAuth(filterVersionPair.lhs, filterVersionPair.rhs) }; } else { - return new Object[]{ + return new Object[] { basepath, - httpMethodFromAnnotation(input, method), - injector.getInstance(input), - injector + httpMethodFromAnnotation(filterVersionPair.lhs, method), + injector.getInstance(filterVersionPair.lhs), + injector, + filterVersionPair.rhs, + performAuth(filterVersionPair.lhs, filterVersionPair.rhs) }; } } @@ -271,4 +314,18 @@ private static String httpMethodFromAnnotation(Class i return method.getAnnotation(DELETE.class) != null ? "DELETE" : "POST"; } } + + // the new filters will perform authorization only if auth version is v2 + // current filters mostly datasource realted ones will work same for both v1 and v2 + // legacy filters will perform authorization only if auth version is v1 + public static boolean performAuth(Class filter, String authVersion) + { + if (NEW_FILTERS.contains(filter) && authVersion.equals(AuthConfig.AUTH_VERSION_2)) { + return true; + } else if (currentFilters.contains(filter)) { + return true; + } else { + return LEGACY_FILTERS.contains(filter) && authVersion.equals(AuthConfig.AUTH_VERSION_1); + } + } } diff --git a/server/src/test/java/org/apache/druid/server/http/security/SecurityResourceFilterTest.java b/server/src/test/java/org/apache/druid/server/http/security/SecurityResourceFilterTest.java index c0b83145d352..0bbdc63223c7 100644 --- a/server/src/test/java/org/apache/druid/server/http/security/SecurityResourceFilterTest.java +++ b/server/src/test/java/org/apache/druid/server/http/security/SecurityResourceFilterTest.java @@ -53,7 +53,7 @@ @RunWith(Parameterized.class) public class SecurityResourceFilterTest extends ResourceFilterTestHelper { - @Parameterized.Parameters(name = "{index}: requestPath={0}, requestMethod={1}, resourceFilter={2}") + @Parameterized.Parameters(name = "{index}: requestPath={0}, requestMethod={1}, resourceFilter={2}, authVersion={4}, performAuth={5}") public static Collection data() { return ImmutableList.copyOf( @@ -83,18 +83,24 @@ public static Collection data() private final String requestMethod; private final ResourceFilter resourceFilter; private final Injector injector; + private final String authVersion; + private final boolean performAuth; public SecurityResourceFilterTest( String requestPath, String requestMethod, ResourceFilter resourceFilter, - Injector injector + Injector injector, + String authVersion, + boolean performAuth ) { this.requestPath = requestPath; this.requestMethod = requestMethod; this.resourceFilter = resourceFilter; this.injector = injector; + this.authVersion = authVersion; + this.performAuth = performAuth; } @Before @@ -106,7 +112,7 @@ public void setUp() @Test public void testResourcesFilteringAccess() { - setUpMockExpectations(requestPath, true, requestMethod); + setUpMockExpectations(requestPath, true, requestMethod, authVersion, performAuth); EasyMock.replay(req, request, authorizerMapper); resourceFilter.getRequestFilter().filter(request); EasyMock.verify(req, request, authorizerMapper); @@ -115,10 +121,14 @@ public void testResourcesFilteringAccess() @Test(expected = ForbiddenException.class) public void testResourcesFilteringNoAccess() { - setUpMockExpectations(requestPath, false, requestMethod); + setUpMockExpectations(requestPath, false, requestMethod, authVersion, performAuth); EasyMock.replay(req, request, authorizerMapper); try { resourceFilter.getRequestFilter().filter(request); + if (!performAuth) { + // if auth does not need to be performed, exception will not be thrown so do it manually + throw new ForbiddenException(); + } Assert.fail(); } catch (ForbiddenException e) { diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java index 19c6b22d7b98..5bf22e0e2c77 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java @@ -70,6 +70,7 @@ import org.apache.druid.server.DruidNode; import org.apache.druid.server.security.Access; import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthConfig; import org.apache.druid.server.security.AuthenticationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; @@ -209,7 +210,8 @@ public SystemSchema( final @Coordinator DruidLeaderClient coordinatorDruidLeaderClient, final @IndexingService DruidLeaderClient overlordDruidLeaderClient, final DruidNodeDiscoveryProvider druidNodeDiscoveryProvider, - final ObjectMapper jsonMapper + final ObjectMapper jsonMapper, + final AuthConfig authConfig ) { Preconditions.checkNotNull(serverView, "serverView"); @@ -511,7 +513,7 @@ public Enumerable scan(DataContext root) root.get(PlannerContext.DATA_CTX_AUTHENTICATION_RESULT), "authenticationResult in dataContext" ); - checkStateReadAccessForServers(authenticationResult, authorizerMapper); + checkStateReadAccessForServers(authenticationResult, authorizerMapper, authorizerMapper.getAuthVersion()); final FluentIterable results = FluentIterable .from(() -> druidServers) @@ -636,7 +638,7 @@ private static Iterator getDruidServers(DruidNodeDiscoveryPr static class ServerSegmentsTable extends AbstractTable implements ScannableTable { private final TimelineServerView serverView; - final AuthorizerMapper authorizerMapper; + private final AuthorizerMapper authorizerMapper; public ServerSegmentsTable(TimelineServerView serverView, AuthorizerMapper authorizerMapper) { @@ -663,7 +665,7 @@ public Enumerable scan(DataContext root) root.get(PlannerContext.DATA_CTX_AUTHENTICATION_RESULT), "authenticationResult in dataContext" ); - checkStateReadAccessForServers(authenticationResult, authorizerMapper); + checkStateReadAccessForServers(authenticationResult, authorizerMapper, authorizerMapper.getAuthVersion()); final List rows = new ArrayList<>(); final List druidServers = serverView.getDruidServers(); @@ -1083,12 +1085,17 @@ private static String toStringOrNull(@Nullable final Object object) */ private static void checkStateReadAccessForServers( AuthenticationResult authenticationResult, - AuthorizerMapper authorizerMapper + AuthorizerMapper authorizerMapper, + String authVersion ) { final Access stateAccess = AuthorizationUtils.authorizeAllResourceActions( authenticationResult, - Collections.singletonList(new ResourceAction(Resource.STATE_RESOURCE, Action.READ)), + Collections.singletonList( + authVersion.equals(AuthConfig.AUTH_VERSION_2) ? new ResourceAction(Resource.SERVER_SERVER_RESOURCE, + Action.READ + ) : new ResourceAction(Resource.STATE_RESOURCE, Action.READ) + ), authorizerMapper ); if (!stateAccess.isAllowed()) { diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaTest.java index 46aa3cca6785..bdb4413f68ca 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/schema/SystemSchemaTest.java @@ -24,6 +24,8 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; import junitparams.converters.Nullable; import org.apache.calcite.DataContext; import org.apache.calcite.adapter.java.JavaTypeFactory; @@ -79,9 +81,15 @@ import org.apache.druid.server.coordination.ServerType; import org.apache.druid.server.coordinator.BytesAccumulatingResponseHandler; import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthConfig; +import org.apache.druid.server.security.AuthenticationResult; import org.apache.druid.server.security.Authorizer; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.NoopEscalator; +import org.apache.druid.server.security.Resource; +import org.apache.druid.server.security.ResourceAction; +import org.apache.druid.server.security.ResourceType; import org.apache.druid.sql.calcite.planner.PlannerConfig; import org.apache.druid.sql.calcite.schema.SystemSchema.SegmentsTable; import org.apache.druid.sql.calcite.table.RowSignatures; @@ -108,6 +116,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; import java.io.File; import java.io.IOException; @@ -121,6 +130,7 @@ import java.util.Map; import java.util.Set; +@RunWith(JUnitParamsRunner.class) public class SystemSchemaTest extends CalciteTestBase { private static final PlannerConfig PLANNER_CONFIG_DEFAULT = new PlannerConfig(); @@ -191,7 +201,7 @@ public void setUp() throws Exception .addMockedMethod("getStatus") .createMock(); request = EasyMock.createMock(Request.class); - authMapper = new AuthorizerMapper(null) + authMapper = new AuthorizerMapper(null, null) { @Override public Authorizer getAuthorizer(String name) @@ -268,7 +278,8 @@ public Authorizer getAuthorizer(String name) client, client, druidNodeDiscoveryProvider, - mapper + mapper, + new AuthConfig() ); } @@ -729,8 +740,33 @@ private void verifyRow( } @Test - public void testServersTable() + @Parameters({ AuthConfig.AUTH_VERSION_1, AuthConfig.AUTH_VERSION_2 }) + public void testServersTable(String authVersion) { + final List expectedResourceActions = new ArrayList<>(); + authMapper = new AuthorizerMapper(null, authVersion) + { + @Override + public Authorizer getAuthorizer(String name) + { + return new Authorizer() + { + @Override + public Access authorize(AuthenticationResult authenticationResult, Resource resource, Action action) + { + expectedResourceActions.add(new ResourceAction(resource, action)); + return new Access(true); + } + + @Override + public Access authorizeV2(AuthenticationResult authenticationResult, Resource resource, Action action) + { + expectedResourceActions.add(new ResourceAction(resource, action)); + return new Access(true); + } + }; + } + }; SystemSchema.ServersTable serversTable = EasyMock.createMockBuilder(SystemSchema.ServersTable.class) .withConstructor( @@ -942,6 +978,10 @@ public Object get(String name) Assert.assertArrayEquals(expectedRows.get(i), rows.get(i)); } + final List actualResourceActions = authVersion.equals(AuthConfig.AUTH_VERSION_2) ? + ImmutableList.of(new ResourceAction(Resource.SERVER_SERVER_RESOURCE, Action.READ)) : + ImmutableList.of(new ResourceAction(Resource.STATE_RESOURCE, Action.READ)); + Assert.assertEquals(expectedResourceActions, actualResourceActions); // Verify value types. verifyTypes(rows, SystemSchema.SERVERS_SIGNATURE); } @@ -982,8 +1022,34 @@ private Object[] createExpectedRow( } @Test - public void testServerSegmentsTable() + @Parameters({ AuthConfig.AUTH_VERSION_1, AuthConfig.AUTH_VERSION_2 }) + public void testServerSegmentsTable(String authVersion) { + final List expectedResourceActions = new ArrayList<>(); + authMapper = new AuthorizerMapper(null, authVersion) + { + @Override + public Authorizer getAuthorizer(String name) + { + return new Authorizer() + { + @Override + public Access authorize(AuthenticationResult authenticationResult, Resource resource, Action action) + { + expectedResourceActions.add(new ResourceAction(resource, action)); + return new Access(true); + } + + @Override + public Access authorizeV2(AuthenticationResult authenticationResult, Resource resource, Action action) + { + expectedResourceActions.add(new ResourceAction(resource, action)); + return new Access(true); + } + }; + } + }; + SystemSchema.ServerSegmentsTable serverSegmentsTable = EasyMock .createMockBuilder(SystemSchema.ServerSegmentsTable.class) .withConstructor(serverView, authMapper) @@ -1051,6 +1117,24 @@ public Object get(String name) Assert.assertEquals("server2:1234", row4[0]); Assert.assertEquals("test5_2015-01-01T00:00:00.000Z_2016-01-01T00:00:00.000Z_version5", row4[1].toString()); + final List actualResourceActions = authVersion.equals(AuthConfig.AUTH_VERSION_2) ? + ImmutableList.of( + new ResourceAction(Resource.SERVER_SERVER_RESOURCE, Action.READ), + new ResourceAction(new Resource("test1", ResourceType.DATASOURCE), Action.READ), + new ResourceAction(new Resource("test2", ResourceType.DATASOURCE), Action.READ), + new ResourceAction(new Resource("test3", ResourceType.DATASOURCE), Action.READ), + new ResourceAction(new Resource("test4", ResourceType.DATASOURCE), Action.READ), + new ResourceAction(new Resource("test5", ResourceType.DATASOURCE), Action.READ) + ) : + ImmutableList.of( + new ResourceAction(Resource.STATE_RESOURCE, Action.READ), + new ResourceAction(new Resource("test1", ResourceType.DATASOURCE), Action.READ), + new ResourceAction(new Resource("test2", ResourceType.DATASOURCE), Action.READ), + new ResourceAction(new Resource("test3", ResourceType.DATASOURCE), Action.READ), + new ResourceAction(new Resource("test4", ResourceType.DATASOURCE), Action.READ), + new ResourceAction(new Resource("test5", ResourceType.DATASOURCE), Action.READ) + ); + Assert.assertEquals(expectedResourceActions, actualResourceActions); // Verify value types. verifyTypes(rows, SystemSchema.SERVER_SEGMENTS_SIGNATURE); } diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/util/CalciteTests.java b/sql/src/test/java/org/apache/druid/sql/calcite/util/CalciteTests.java index d8088ae7ed87..6139ba796c82 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/util/CalciteTests.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/util/CalciteTests.java @@ -165,7 +165,7 @@ public class CalciteTests public static final String LOOKUP_SCHEMA_NAME = "lookup"; public static final String TEST_SUPERUSER_NAME = "testSuperuser"; - public static final AuthorizerMapper TEST_AUTHORIZER_MAPPER = new AuthorizerMapper(null) + public static final AuthorizerMapper TEST_AUTHORIZER_MAPPER = new AuthorizerMapper(null, null) { @Override public Authorizer getAuthorizer(String name) @@ -1029,7 +1029,8 @@ NodeRole.COORDINATOR, new FakeDruidNodeDiscovery(ImmutableMap.of(NodeRole.COORDI druidLeaderClient, druidLeaderClient, provider, - getJsonMapper() + getJsonMapper(), + new AuthConfig() ); } diff --git a/website/.spelling b/website/.spelling index 096b3009e459..8abd2d9d19d4 100644 --- a/website/.spelling +++ b/website/.spelling @@ -444,6 +444,8 @@ HttpClient allowAll authenticatorChain defaultUser +- ../docs/design/auth-model.md +INFORMATION_SCHEMA - ../docs/design/coordinator.md inputSegmentSizeBytes skipOffsetFromLatest @@ -648,7 +650,6 @@ metricColumns nominalEntries numberOfValues - ../docs/development/extensions-core/druid-basic-security.md -INFORMATION_SCHEMA MyBasicAuthenticator MyBasicAuthorizer authenticatorName