From 3eb0b66dfd760f22a4cf476b7e57613d4c1bf18a Mon Sep 17 00:00:00 2001 From: cecemei Date: Thu, 12 Dec 2024 13:01:03 -0800 Subject: [PATCH 01/32] add row-level security filter --- .../apache/druid/grpc/server/QueryDriver.java | 3 +- .../basic/BasicSecurityResourceFilter.java | 6 +- .../druid/catalog/http/CatalogResource.java | 76 ++-- .../dart/controller/http/DartSqlResource.java | 6 +- .../druid/msq/rpc/MSQResourceUtils.java | 19 +- .../sql/resources/SqlStatementResource.java | 30 +- .../indexing/common/task/IndexTaskUtils.java | 9 +- .../overlord/http/OverlordResource.java | 25 +- .../security/SupervisorResourceFilter.java | 7 +- .../http/security/TaskResourceFilter.java | 7 +- .../overlord/sampler/SamplerResource.java | 7 +- .../supervisor/SupervisorResource.java | 29 +- .../SeekableStreamIndexTaskRunner.java | 50 +-- .../supervisor/SupervisorResourceTest.java | 4 +- .../org/apache/druid/query/DataSource.java | 32 +- .../apache/druid/query/JoinDataSource.java | 8 +- .../java/org/apache/druid/query/Query.java | 11 + .../druid/query/RestrictedDataSource.java | 227 +++++++++++ .../apache/druid/query/TableDataSource.java | 19 + .../druid/query/filter/NotDimFilter.java | 1 - .../metadata/SegmentMetadataQuery.java | 41 +- .../segment/RestrictedCursorFactory.java | 74 ++++ .../druid/segment/RestrictedSegment.java | 52 +++ .../apache/druid/query/DataSourceTest.java | 84 ++++ .../druid/query/JoinDataSourceTest.java | 20 + .../druid/query/RestrictedDataSourceTest.java | 154 +++++++ .../metadata/SegmentMetadataQueryTest.java | 25 ++ .../AbstractSegmentMetadataCache.java | 5 +- .../SegmentMetadataQuerySegmentWalker.java | 18 +- .../druid/segment/realtime/ChatHandlers.java | 9 +- .../apache/druid/server/QueryLifecycle.java | 51 ++- .../apache/druid/server/QueryResource.java | 11 +- .../http/security/ConfigResourceFilter.java | 8 +- .../security/DatasourceResourceFilter.java | 7 +- .../http/security/RulesResourceFilter.java | 11 +- .../http/security/StateResourceFilter.java | 8 +- .../apache/druid/server/security/Access.java | 60 ++- .../druid/server/security/AuthConfig.java | 35 +- .../server/security/AuthorizationResult.java | 136 +++++++ .../server/security/AuthorizationUtils.java | 155 ++++---- .../CoordinatorSegmentMetadataCacheTest.java | 14 +- .../druid/server/QueryLifecycleTest.java | 376 ++++++++++++++++-- .../security/ForbiddenExceptionTest.java | 21 +- .../apache/druid/sql/AbstractStatement.java | 20 +- .../org/apache/druid/sql/HttpStatement.java | 18 +- .../druid/sql/SqlExecutionReporter.java | 2 +- .../sql/calcite/planner/DruidPlanner.java | 48 +-- .../sql/calcite/planner/PlannerContext.java | 12 +- .../sql/calcite/planner/PlannerFactory.java | 10 +- .../sql/calcite/run/NativeQueryMaker.java | 10 +- .../sql/calcite/schema/SystemSchema.java | 30 +- .../apache/druid/sql/http/SqlResource.java | 8 +- .../sql/calcite/CalciteJoinQueryTest.java | 4 +- .../BrokerSegmentMetadataCacheTest.java | 123 ++++-- .../druid/sql/calcite/util/CalciteTests.java | 10 +- .../druid/sql/http/SqlResourceTest.java | 4 +- 56 files changed, 1786 insertions(+), 464 deletions(-) create mode 100644 processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java create mode 100644 processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java create mode 100644 processing/src/main/java/org/apache/druid/segment/RestrictedSegment.java create mode 100644 processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java create mode 100644 server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java diff --git a/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java b/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java index 096a1439a4fd..442563d305be 100644 --- a/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java +++ b/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java @@ -49,6 +49,7 @@ import org.apache.druid.server.QueryLifecycleFactory; import org.apache.druid.server.security.Access; import org.apache.druid.server.security.AuthenticationResult; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.ForbiddenException; import org.apache.druid.sql.DirectStatement; import org.apache.druid.sql.DirectStatement.ResultSet; @@ -146,7 +147,7 @@ private QueryResponse runNativeQuery(QueryRequest request, AuthenticationResult final String currThreadName = Thread.currentThread().getName(); try { queryLifecycle.initialize(query); - Access authorizationResult = queryLifecycle.authorize(authResult); + AuthorizationResult authorizationResult = queryLifecycle.authorize(authResult); if (!authorizationResult.isAllowed()) { throw new ForbiddenException(Access.DEFAULT_ERROR_MESSAGE); } 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 bcb4ec053457..1c7d374da308 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 @@ -23,7 +23,7 @@ import com.sun.jersey.spi.container.ContainerRequest; 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.AuthorizationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.Resource; @@ -54,7 +54,7 @@ public ContainerRequest filter(ContainerRequest request) getAction(request) ); - final Access authResult = AuthorizationUtils.authorizeResourceAction( + final AuthorizationResult authResult = AuthorizationUtils.authorizeResourceAction( getReq(), resourceAction, getAuthorizerMapper() @@ -64,7 +64,7 @@ public ContainerRequest filter(ContainerRequest request) throw new WebApplicationException( Response.status(Response.Status.FORBIDDEN) .type(MediaType.TEXT_PLAIN) - .entity(StringUtils.format("Access-Check-Result: %s", authResult.toString())) + .entity(StringUtils.format("Access-Check-Result: %s", authResult.getFailureMessage())) .build() ); } diff --git a/extensions-core/druid-catalog/src/main/java/org/apache/druid/catalog/http/CatalogResource.java b/extensions-core/druid-catalog/src/main/java/org/apache/druid/catalog/http/CatalogResource.java index 2590f57c8b38..0eb9ebf58867 100644 --- a/extensions-core/druid-catalog/src/main/java/org/apache/druid/catalog/http/CatalogResource.java +++ b/extensions-core/druid-catalog/src/main/java/org/apache/druid/catalog/http/CatalogResource.java @@ -34,8 +34,8 @@ import org.apache.druid.java.util.common.IAE; 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; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.ForbiddenException; @@ -56,10 +56,10 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; - import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; /** @@ -108,17 +108,17 @@ public CatalogResource( * * * @param schemaName The name of the Druid schema, which must be writable - * and the user must have at least read access. - * @param tableName The name of the table definition to modify. The user must - * have write access to the table. - * @param spec The new table definition. - * @param version the expected version of an existing table. The version must - * match. If not (or if the table does not exist), returns an error. - * @param overwrite if {@code true}, then overwrites any existing table. - * If {@code false}, then the operation fails if the table already exists. - * Ignored if a version is specified. - * @param req the HTTP request used for authorization. - */ + * and the user must have at least read access. + * @param tableName The name of the table definition to modify. The user must + * have write access to the table. + * @param spec The new table definition. + * @param version the expected version of an existing table. The version must + * match. If not (or if the table does not exist), returns an error. + * @param overwrite if {@code true}, then overwrites any existing table. + * If {@code false}, then the operation fails if the table already exists. + * Ignored if a version is specified. + * @param req the HTTP request used for authorization. + */ @POST @Path("/schemas/{schema}/tables/{name}") @Consumes(MediaType.APPLICATION_JSON) @@ -181,9 +181,9 @@ public Response postTable( * the definition is created before the datasource itself.) * * @param schemaName The Druid schema. The user must have read access. - * @param tableName The name of the table within the schema. The user must have - * read access. - * @param req the HTTP request used for authorization. + * @param tableName The name of the table within the schema. The user must have + * read access. + * @param req the HTTP request used for authorization. * @return the definition for the table, if any. */ @GET @@ -211,8 +211,8 @@ public Response getTable( * for the given schema and table. * * @param schemaName The name of the schema that holds the table. - * @param tableName The name of the table definition to delete. The user must have - * write access. + * @param tableName The name of the table definition to delete. The user must have + * write access. */ @DELETE @Path("/schemas/{schema}/tables/{name}") @@ -247,9 +247,9 @@ public Response deleteTable( * the table spec changed between the time it was retrieve and the edit operation * is submitted. * - * @param schemaName The name of the schema that holds the table. - * @param tableName The name of the table definition to delete. The user must have - * write access. + * @param schemaName The name of the schema that holds the table. + * @param tableName The name of the table definition to delete. The user must have + * write access. * @param editRequest The operation to perform. See the classes for details. */ @POST @@ -281,7 +281,7 @@ public Response editTable( * Retrieves the list of all Druid schema names. * * @param format the format of the response. See the code for the - * available formats + * available formats */ @GET @Path("/schemas") @@ -318,9 +318,9 @@ public Response getSchemas( * the read-only schemas, there will be no table definitions. * * @param schemaName The name of the Druid schema to query. The user must - * have read access. - * @param format the format of the response. See the code for the - * available formats + * have read access. + * @param format the format of the response. See the code for the + * available formats */ @GET @Path("/schemas/{schema}/tables") @@ -360,7 +360,7 @@ public Response getSchemaTables( * table definitions known to the catalog. Used to prime a cache on first access. * After that, the Coordinator will push updates to Brokers. Returns the full * list of table details. - * + *

* It is expected that the number of table definitions will be of small or moderate * size, so no provision is made to handle very large lists. */ @@ -467,9 +467,9 @@ private Response listAllTableMetadata(final HttpServletRequest req) List> tables = new ArrayList<>(); for (SchemaSpec schema : catalog.schemaRegistry().schemas()) { tables.addAll(catalog.tables().tablesInSchema(schema.name()) - .stream() - .map(table -> Pair.of(schema, table)) - .collect(Collectors.toList())); + .stream() + .map(table -> Pair.of(schema, table)) + .collect(Collectors.toList())); } Iterable> filtered = AuthorizationUtils.filterAuthorizedResources( @@ -483,9 +483,9 @@ private Response listAllTableMetadata(final HttpServletRequest req) ); List metadata = Lists.newArrayList(filtered) - .stream() - .map(pair -> pair.rhs) - .collect(Collectors.toList()); + .stream() + .map(pair -> pair.rhs) + .collect(Collectors.toList()); return Response.ok().entity(metadata).build(); } @@ -499,9 +499,9 @@ private Response tableNamesInSchema( req, tables, name -> - Collections.singletonList( - resourceAction(schema, name, Action.READ)), - authorizerMapper + Collections.singletonList( + resourceAction(schema, name, Action.READ)), + authorizerMapper ); return Response.ok().entity(Lists.newArrayList(filtered)).build(); } @@ -581,13 +581,13 @@ private void authorizeTable( private void authorize(String resource, String key, Action action, HttpServletRequest request) { - final Access authResult = authorizeAccess(resource, key, action, request); + final AuthorizationResult authResult = authorizeAccess(resource, key, action, request); if (!authResult.isAllowed()) { - throw new ForbiddenException(authResult.toString()); + throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); } } - private Access authorizeAccess(String resource, String key, Action action, HttpServletRequest request) + private AuthorizationResult authorizeAccess(String resource, String key, Action action, HttpServletRequest request) { return AuthorizationUtils.authorizeResourceAction( request, diff --git a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/http/DartSqlResource.java b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/http/DartSqlResource.java index 65d770a29c55..97a1f32f66a9 100644 --- a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/http/DartSqlResource.java +++ b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/http/DartSqlResource.java @@ -36,9 +36,9 @@ import org.apache.druid.server.DruidNode; import org.apache.druid.server.ResponseContextConfig; import org.apache.druid.server.initialization.ServerConfig; -import org.apache.druid.server.security.Access; import org.apache.druid.server.security.Action; import org.apache.druid.server.security.AuthenticationResult; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.Resource; @@ -144,7 +144,7 @@ public GetQueriesResponse doGetRunningQueries( ) { final AuthenticationResult authenticationResult = AuthorizationUtils.authenticationResultFromRequest(req); - final Access stateReadAccess = AuthorizationUtils.authorizeAllResourceActions( + final AuthorizationResult stateReadAccess = AuthorizationUtils.authorizeAllResourceActions( authenticationResult, Collections.singletonList(new ResourceAction(Resource.STATE_RESOURCE, Action.READ)), authorizerMapper @@ -245,7 +245,7 @@ public Response cancelQuery( return Response.status(Response.Status.ACCEPTED).build(); } - final Access access = authorizeCancellation(req, cancelables); + final AuthorizationResult access = authorizeCancellation(req, cancelables); if (access.isAllowed()) { sqlLifecycleManager.removeAll(sqlQueryId, cancelables); diff --git a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/rpc/MSQResourceUtils.java b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/rpc/MSQResourceUtils.java index 8820b4ead5a0..7d9779b723f5 100644 --- a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/rpc/MSQResourceUtils.java +++ b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/rpc/MSQResourceUtils.java @@ -19,7 +19,7 @@ package org.apache.druid.msq.rpc; -import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.ForbiddenException; @@ -27,6 +27,7 @@ import javax.servlet.http.HttpServletRequest; import java.util.List; +import java.util.Objects; /** * Utility methods for MSQ resources such as {@link ControllerResource}. @@ -41,10 +42,14 @@ public static void authorizeAdminRequest( { final List resourceActions = permissionMapper.getAdminPermissions(); - Access access = AuthorizationUtils.authorizeAllResourceActions(request, resourceActions, authorizerMapper); + AuthorizationResult access = AuthorizationUtils.authorizeAllResourceActions( + request, + resourceActions, + authorizerMapper + ); if (!access.isAllowed()) { - throw new ForbiddenException(access.toString()); + throw new ForbiddenException(Objects.requireNonNull(access.getFailureMessage())); } } @@ -57,10 +62,14 @@ public static void authorizeQueryRequest( { final List resourceActions = permissionMapper.getQueryPermissions(queryId); - Access access = AuthorizationUtils.authorizeAllResourceActions(request, resourceActions, authorizerMapper); + AuthorizationResult access = AuthorizationUtils.authorizeAllResourceActions( + request, + resourceActions, + authorizerMapper + ); if (!access.isAllowed()) { - throw new ForbiddenException(access.toString()); + throw new ForbiddenException(Objects.requireNonNull(access.getFailureMessage())); } } } diff --git a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlStatementResource.java b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlStatementResource.java index c92bfa955fb6..33527e6eb903 100644 --- a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlStatementResource.java +++ b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlStatementResource.java @@ -74,9 +74,9 @@ import org.apache.druid.rpc.HttpResponseException; import org.apache.druid.rpc.indexing.OverlordClient; import org.apache.druid.server.QueryResponse; -import org.apache.druid.server.security.Access; import org.apache.druid.server.security.Action; import org.apache.druid.server.security.AuthenticationResult; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.ForbiddenException; @@ -484,7 +484,13 @@ private Response buildTaskResponse(Sequence sequence, AuthenticationRe } String taskId = String.valueOf(firstRow[0]); - Optional statementResult = getStatementStatus(taskId, authenticationResult, true, Action.READ, false); + Optional statementResult = getStatementStatus( + taskId, + authenticationResult, + true, + Action.READ, + false + ); if (statementResult.isPresent()) { return Response.status(Response.Status.OK).entity(statementResult.get()).build(); @@ -585,7 +591,11 @@ private Optional getStatementStatus( } // since we need the controller payload for auth checks. - MSQControllerTask msqControllerTask = getMSQControllerTaskAndCheckPermission(queryId, authenticationResult, forAction); + MSQControllerTask msqControllerTask = getMSQControllerTaskAndCheckPermission( + queryId, + authenticationResult, + forAction + ); SqlStatementState sqlStatementState = SqlStatementResourceHelper.getSqlStatementState(statusPlus); MSQTaskReportPayload taskReportPayload = null; @@ -640,9 +650,9 @@ private Optional getStatementStatus( * necessary permissions. A user has the necessary permissions if one of the following criteria is satisfied: * 1. The user is the one who submitted the query * 2. The user belongs to a role containing the READ or WRITE permissions over the STATE resource. For endpoints like GET, - * the user should have READ permission for the STATE resource, while for endpoints like DELETE, the user should - * have WRITE permission for the STATE resource. (Note: POST API does not need to check the state permissions since - * the currentUser always equal to the queryUser) + * the user should have READ permission for the STATE resource, while for endpoints like DELETE, the user should + * have WRITE permission for the STATE resource. (Note: POST API does not need to check the state permissions since + * the currentUser always equal to the queryUser) */ private MSQControllerTask getMSQControllerTaskAndCheckPermission( String queryId, @@ -665,7 +675,7 @@ private MSQControllerTask getMSQControllerTaskAndCheckPermission( return msqControllerTask; } - Access access = AuthorizationUtils.authorizeAllResourceActions( + AuthorizationResult access = AuthorizationUtils.authorizeAllResourceActions( authenticationResult, Collections.singletonList(new ResourceAction(Resource.STATE_RESOURCE, forAction)), authorizerMapper @@ -990,7 +1000,11 @@ private T contactOverlord(final ListenableFuture future, String queryId) private static DruidException queryNotFoundException(String queryId) { - return NotFound.exception("Query [%s] was not found. The query details are no longer present or might not be of the type [%s]. Verify that the id is correct.", queryId, MSQControllerTask.TYPE); + return NotFound.exception( + "Query [%s] was not found. The query details are no longer present or might not be of the type [%s]. Verify that the id is correct.", + queryId, + MSQControllerTask.TYPE + ); } } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/common/task/IndexTaskUtils.java b/indexing-service/src/main/java/org/apache/druid/indexing/common/task/IndexTaskUtils.java index 79a3e8993a8c..70b1b50e2daf 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/common/task/IndexTaskUtils.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/common/task/IndexTaskUtils.java @@ -27,8 +27,8 @@ import org.apache.druid.java.util.emitter.service.ServiceMetricEvent; import org.apache.druid.query.DruidMetrics; import org.apache.druid.segment.incremental.ParseExceptionReport; -import org.apache.druid.server.security.Access; import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.ForbiddenException; @@ -43,6 +43,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; public class IndexTaskUtils { @@ -67,7 +68,7 @@ public static List getReportListFromSavedParseExceptions( * * @return authorization result */ - public static Access datasourceAuthorizationCheck( + public static AuthorizationResult datasourceAuthorizationCheck( final HttpServletRequest req, Action action, String datasource, @@ -79,9 +80,9 @@ public static Access datasourceAuthorizationCheck( action ); - Access access = AuthorizationUtils.authorizeResourceAction(req, resourceAction, authorizerMapper); + AuthorizationResult access = AuthorizationUtils.authorizeResourceAction(req, resourceAction, authorizerMapper); if (!access.isAllowed()) { - throw new ForbiddenException(access.toString()); + throw new ForbiddenException(Objects.requireNonNull(access.getFailureMessage())); } return access; 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 84bf260b6f99..b432132e0f27 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 @@ -66,9 +66,9 @@ import org.apache.druid.server.http.security.ConfigResourceFilter; import org.apache.druid.server.http.security.DatasourceResourceFilter; 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.AuthorizationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.ForbiddenException; @@ -101,6 +101,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; /** @@ -177,14 +178,14 @@ public Response taskPost( .build(); } - Access authResult = AuthorizationUtils.authorizeAllResourceActions( + AuthorizationResult authResult = AuthorizationUtils.authorizeAllResourceActions( req, resourceActions, authorizerMapper ); if (!authResult.isAllowed()) { - throw new ForbiddenException(authResult.getMessage()); + throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); } return asLeaderWith( @@ -609,7 +610,7 @@ public Response getTasks( new Resource(dataSource, ResourceType.DATASOURCE), Action.READ ); - final Access authResult = AuthorizationUtils.authorizeResourceAction( + final AuthorizationResult authResult = AuthorizationUtils.authorizeResourceAction( req, resourceAction, authorizerMapper @@ -618,7 +619,10 @@ public Response getTasks( throw new WebApplicationException( Response.status(Response.Status.FORBIDDEN) .type(MediaType.TEXT_PLAIN) - .entity(StringUtils.format("Access-Check-Result: %s", authResult.toString())) + .entity(StringUtils.format( + "Access-Check-Result: %s", + Objects.requireNonNull(authResult.getFailureMessage()) + )) .build() ); } @@ -654,7 +658,7 @@ public Response killPendingSegments( { final Interval deleteInterval = Intervals.of(deleteIntervalString); // check auth for dataSource - final Access authResult = AuthorizationUtils.authorizeAllResourceActions( + final AuthorizationResult authResult = AuthorizationUtils.authorizeAllResourceActions( request, ImmutableList.of( new ResourceAction(new Resource(dataSource, ResourceType.DATASOURCE), Action.READ), @@ -664,7 +668,7 @@ public Response killPendingSegments( ); if (!authResult.isAllowed()) { - throw new ForbiddenException(authResult.getMessage()); + throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); } if (overlord.isLeader()) { @@ -678,7 +682,12 @@ public Response killPendingSegments( .build(); } catch (Exception e) { - log.warn(e, "Failed to delete pending segments for datasource[%s] and interval[%s].", dataSource, deleteInterval); + log.warn( + e, + "Failed to delete pending segments for datasource[%s] and interval[%s].", + dataSource, + deleteInterval + ); return Response.status(Status.INTERNAL_SERVER_ERROR) .entity(ImmutableMap.of("error", e.getMessage())) .build(); diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/SupervisorResourceFilter.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/SupervisorResourceFilter.java index 7e9aaa927407..b0647623c509 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/SupervisorResourceFilter.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/SupervisorResourceFilter.java @@ -30,8 +30,8 @@ import org.apache.druid.indexing.overlord.supervisor.SupervisorSpec; 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.Action; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.ForbiddenException; @@ -41,6 +41,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.PathSegment; import javax.ws.rs.core.Response; +import java.util.Objects; public class SupervisorResourceFilter extends AbstractResourceFilter { @@ -97,14 +98,14 @@ public boolean apply(PathSegment input) AuthorizationUtils.DATASOURCE_READ_RA_GENERATOR : AuthorizationUtils.DATASOURCE_WRITE_RA_GENERATOR; - Access authResult = AuthorizationUtils.authorizeAllResourceActions( + AuthorizationResult authResult = AuthorizationUtils.authorizeAllResourceActions( getReq(), Iterables.transform(spec.getDataSources(), resourceActionFunction), getAuthorizerMapper() ); if (!authResult.isAllowed()) { - throw new ForbiddenException(authResult.toString()); + throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); } return request; diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/TaskResourceFilter.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/TaskResourceFilter.java index a9f66ce30e72..bd63c1197814 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/TaskResourceFilter.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/TaskResourceFilter.java @@ -29,7 +29,7 @@ import org.apache.druid.indexing.overlord.TaskQueryTool; 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.AuthorizationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.ForbiddenException; @@ -40,6 +40,7 @@ import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.util.Objects; /** * Use this ResourceFilter when the datasource information is present after "task" segment in the request Path @@ -92,14 +93,14 @@ public ContainerRequest filter(ContainerRequest request) getAction(request) ); - final Access authResult = AuthorizationUtils.authorizeResourceAction( + final AuthorizationResult authResult = AuthorizationUtils.authorizeResourceAction( getReq(), resourceAction, getAuthorizerMapper() ); if (!authResult.isAllowed()) { - throw new ForbiddenException(authResult.toString()); + throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); } return request; 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 75618ddae42a..2b4fa1565328 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 @@ -23,9 +23,9 @@ import com.google.inject.Inject; import org.apache.druid.client.indexing.SamplerResponse; import org.apache.druid.client.indexing.SamplerSpec; -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.AuthorizationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.ForbiddenException; @@ -40,6 +40,7 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import java.util.HashSet; +import java.util.Objects; import java.util.Set; @Path("/druid/indexer/v1/sampler") @@ -72,14 +73,14 @@ public SamplerResponse post(final SamplerSpec sampler, @Context final HttpServle resourceActions.addAll(sampler.getInputSourceResources()); } - Access authResult = AuthorizationUtils.authorizeAllResourceActions( + AuthorizationResult authResult = AuthorizationUtils.authorizeAllResourceActions( req, resourceActions, authorizerMapper ); if (!authResult.isAllowed()) { - throw new ForbiddenException(authResult.getMessage()); + throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); } return sampler.sample(); } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java index 130f617d59d1..dc1bc41bbebd 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java @@ -41,9 +41,9 @@ import org.apache.druid.java.util.common.StringUtils; import org.apache.druid.java.util.common.UOE; import org.apache.druid.segment.incremental.ParseExceptionReport; -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.AuthorizationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.ForbiddenException; @@ -142,14 +142,14 @@ public Response specPost(final SupervisorSpec spec, @Context final HttpServletRe .build(); } - Access authResult = AuthorizationUtils.authorizeAllResourceActions( + AuthorizationResult authResult = AuthorizationUtils.authorizeAllResourceActions( req, resourceActions, authorizerMapper ); if (!authResult.isAllowed()) { - throw new ForbiddenException(authResult.toString()); + throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); } manager.createOrUpdateAndStartSupervisor(spec); @@ -410,13 +410,16 @@ public Response shutdown(@PathParam("id") final String id) @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @ResourceFilters(SupervisorResourceFilter.class) - public Response handoffTaskGroups(@PathParam("id") final String id, @Nonnull final HandoffTaskGroupsRequest handoffTaskGroupsRequest) + public Response handoffTaskGroups( + @PathParam("id") final String id, + @Nonnull final HandoffTaskGroupsRequest handoffTaskGroupsRequest + ) { List taskGroupIds = handoffTaskGroupsRequest.getTaskGroupIds(); if (CollectionUtils.isNullOrEmpty(taskGroupIds)) { return Response.status(Response.Status.BAD_REQUEST) - .entity(ImmutableMap.of("error", "List of task groups to handoff can't be empty")) - .build(); + .entity(ImmutableMap.of("error", "List of task groups to handoff can't be empty")) + .build(); } return asLeaderWithSupervisorManager( @@ -426,14 +429,20 @@ public Response handoffTaskGroups(@PathParam("id") final String id, @Nonnull fin return Response.ok().build(); } else { return Response.status(Response.Status.NOT_FOUND) - .entity(ImmutableMap.of("error", StringUtils.format("Supervisor was not found [%s]", id))) - .build(); + .entity(ImmutableMap.of("error", StringUtils.format("Supervisor was not found [%s]", id))) + .build(); } } catch (NotImplementedException e) { return Response.status(Response.Status.BAD_REQUEST) - .entity(ImmutableMap.of("error", StringUtils.format("Supervisor [%s] does not support early handoff", id))) - .build(); + .entity(ImmutableMap.of( + "error", + StringUtils.format( + "Supervisor [%s] does not support early handoff", + id + ) + )) + .build(); } } ); diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/seekablestream/SeekableStreamIndexTaskRunner.java b/indexing-service/src/main/java/org/apache/druid/indexing/seekablestream/SeekableStreamIndexTaskRunner.java index a2db12d005e9..7ee3dcb6f9da 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/seekablestream/SeekableStreamIndexTaskRunner.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/seekablestream/SeekableStreamIndexTaskRunner.java @@ -89,8 +89,8 @@ import org.apache.druid.segment.realtime.appenderator.SegmentsAndCommitMetadata; import org.apache.druid.segment.realtime.appenderator.StreamAppenderator; import org.apache.druid.segment.realtime.appenderator.StreamAppenderatorDriver; -import org.apache.druid.server.security.Access; import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.timeline.DataSegment; import org.apache.druid.utils.CollectionUtils; @@ -144,7 +144,8 @@ * @param Sequence Number Type */ @SuppressWarnings("CheckReturnValue") -public abstract class SeekableStreamIndexTaskRunner implements ChatHandler +public abstract class SeekableStreamIndexTaskRunner + implements ChatHandler { private static final String CTX_KEY_LOOKUP_TIER = "lookupTier"; @@ -278,12 +279,11 @@ public SeekableStreamIndexTaskRunner( rejectionPeriodUpdaterExec = Execs.scheduledSingleThreaded("RejectionPeriodUpdater-Exec--%d"); if (ioConfig.getRefreshRejectionPeriodsInMinutes() != null) { - rejectionPeriodUpdaterExec - .scheduleWithFixedDelay( - this::refreshMinMaxMessageTime, - ioConfig.getRefreshRejectionPeriodsInMinutes(), - ioConfig.getRefreshRejectionPeriodsInMinutes(), - TimeUnit.MINUTES); + rejectionPeriodUpdaterExec.scheduleWithFixedDelay(this::refreshMinMaxMessageTime, + ioConfig.getRefreshRejectionPeriodsInMinutes(), + ioConfig.getRefreshRejectionPeriodsInMinutes(), + TimeUnit.MINUTES + ); } resetNextCheckpointTime(); } @@ -759,10 +759,18 @@ public void onFailure(Throwable t) if (System.currentTimeMillis() > nextCheckpointTime) { sequenceToCheckpoint = getLastSequenceMetadata(); - log.info("Next checkpoint time, updating sequenceToCheckpoint, SequenceToCheckpoint: [%s]", sequenceToCheckpoint); + log.info( + "Next checkpoint time, updating sequenceToCheckpoint, SequenceToCheckpoint: [%s]", + sequenceToCheckpoint + ); } if (pushTriggeringAddResult != null) { - log.info("Hit the row limit updating sequenceToCheckpoint, SequenceToCheckpoint: [%s], rowInSegment: [%s], TotalRows: [%s]", sequenceToCheckpoint, pushTriggeringAddResult.getNumRowsInSegment(), pushTriggeringAddResult.getTotalNumRowsInAppenderator()); + log.info( + "Hit the row limit updating sequenceToCheckpoint, SequenceToCheckpoint: [%s], rowInSegment: [%s], TotalRows: [%s]", + sequenceToCheckpoint, + pushTriggeringAddResult.getNumRowsInSegment(), + pushTriggeringAddResult.getTotalNumRowsInAppenderator() + ); } if (sequenceToCheckpoint != null && stillReading) { @@ -1128,14 +1136,14 @@ private synchronized void persistSequences() throws IOException /** * Return a map of reports for the task. - * + *

* A successfull task should always have a null errorMsg. Segments availability is inherently confirmed * if the task was succesful. - * + *

* A falied task should always have a non-null errorMsg. Segment availability is never confirmed if the task * was not successful. * - * @param errorMsg Nullable error message for the task. null if task succeeded. + * @param errorMsg Nullable error message for the task. null if task succeeded. * @param handoffWaitMs Milliseconds waited for segments to be handed off. * @return Map of reports for the task. */ @@ -1446,7 +1454,7 @@ protected void sendResetRequestAndWait( * * @return authorization result */ - private Access authorizationCheck(final HttpServletRequest req, Action action) + private AuthorizationResult authorizationCheck(final HttpServletRequest req, Action action) { return IndexTaskUtils.datasourceAuthorizationCheck(req, action, task.getDataSource(), authorizerMapper); } @@ -2023,9 +2031,7 @@ private boolean verifyRecordInRange( * * @param toolbox task toolbox * @param checkpointsString the json-serialized checkpoint string - * * @return checkpoint - * * @throws IOException jsonProcessingException */ @Nullable @@ -2039,7 +2045,6 @@ protected abstract TreeMap> ge * This is what would become the start offsets of the next reader, if we stopped reading now. * * @param sequenceNumber the sequence number that has already been processed - * * @return next sequence number to be stored */ protected abstract SequenceOffsetType getNextStartOffset(SequenceOffsetType sequenceNumber); @@ -2049,7 +2054,6 @@ protected abstract TreeMap> ge * * @param mapper json objectMapper * @param object metadata - * * @return SeekableStreamEndSequenceNumbers */ protected abstract SeekableStreamEndSequenceNumbers deserializePartitionsFromMetadata( @@ -2063,9 +2067,7 @@ protected abstract SeekableStreamEndSequenceNumbers createDataSourceMetadata( @@ -2089,7 +2090,6 @@ protected abstract SeekableStreamDataSourceMetadata createSequenceNumber(SequenceOffsetType sequenceNumber); @@ -2117,7 +2117,11 @@ private void refreshMinMaxMessageTime() minMessageTime = minMessageTime.plusMinutes(ioConfig.getRefreshRejectionPeriodsInMinutes().intValue()); maxMessageTime = maxMessageTime.plusMinutes(ioConfig.getRefreshRejectionPeriodsInMinutes().intValue()); - log.info(StringUtils.format("Updated min and max messsage times to %s and %s respectively.", minMessageTime, maxMessageTime)); + log.info(StringUtils.format( + "Updated min and max messsage times to %s and %s respectively.", + minMessageTime, + maxMessageTime + )); } public boolean withinMinMaxRecordTime(final InputRow row) 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 1fd7af69e123..f07c6c13ab88 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 @@ -114,13 +114,13 @@ public Authorizer getAuthorizer(String name) } else { if (resource.getType().equals(ResourceType.DATASOURCE)) { if (resource.getName().equals("datasource2")) { - return new Access(false, "not authorized."); + return Access.deny("not authorized."); } else { return Access.OK; } } else if (resource.getType().equals(ResourceType.EXTERNAL)) { if (resource.getName().equals("test")) { - return new Access(false, "not authorized."); + return Access.deny("not authorized."); } else { return Access.OK; } diff --git a/processing/src/main/java/org/apache/druid/query/DataSource.java b/processing/src/main/java/org/apache/druid/query/DataSource.java index 360c339627f9..439aeaddfd44 100644 --- a/processing/src/main/java/org/apache/druid/query/DataSource.java +++ b/processing/src/main/java/org/apache/druid/query/DataSource.java @@ -21,14 +21,18 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.planning.DataSourceAnalysis; import org.apache.druid.query.planning.PreJoinableClause; import org.apache.druid.segment.SegmentReference; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; +import java.util.stream.Collectors; /** * Represents a source... of data... for a query. Analogous to the "FROM" clause in SQL. @@ -43,7 +47,8 @@ @JsonSubTypes.Type(value = InlineDataSource.class, name = "inline"), @JsonSubTypes.Type(value = GlobalTableDataSource.class, name = "globalTable"), @JsonSubTypes.Type(value = UnnestDataSource.class, name = "unnest"), - @JsonSubTypes.Type(value = FilteredDataSource.class, name = "filter") + @JsonSubTypes.Type(value = FilteredDataSource.class, name = "filter"), + @JsonSubTypes.Type(value = RestrictedDataSource.class, name = "restrict") }) public interface DataSource { @@ -88,11 +93,11 @@ public interface DataSource /** * Returns true if this datasource can be the base datasource of query processing. - * + *

* Base datasources drive query processing. If the base datasource is {@link TableDataSource}, for example, queries * are processed in parallel on data servers. If the base datasource is {@link InlineDataSource}, queries are * processed on the Broker. See {@link DataSourceAnalysis#getBaseDataSource()} for further discussion. - * + *

* Datasources that are *not* concrete must be pre-processed in some way before they can be processed by the main * query stack. For example, {@link QueryDataSource} must be executed first and substituted with its results. * @@ -118,6 +123,27 @@ public interface DataSource */ DataSource withUpdatedDataSource(DataSource newSource); + default DataSource mapWithRestriction(Map> rowFilters) + { + return mapWithRestriction(rowFilters, true); + } + + /** + * Returns an updated datasource based on the policy restrictions on tables. If this datasource contains no table, no + * changes should occur. + * + * @param rowFilters a mapping of table names to row filters, every table in the datasource tree must have an entry + * @return the updated datasource, with restrictions applied in the datasource tree + */ + default DataSource mapWithRestriction(Map> rowFilters, boolean enableStrictPolicyCheck) + { + List children = this.getChildren() + .stream() + .map(child -> child.mapWithRestriction(rowFilters, enableStrictPolicyCheck)) + .collect(Collectors.toList()); + return this.withChildren(children); + } + /** * Compute a cache key prefix for a data source. This includes the data sources that participate in the RHS of a * join as well as any query specific constructs associated with join data source such as base table filter. This key prefix diff --git a/processing/src/main/java/org/apache/druid/query/JoinDataSource.java b/processing/src/main/java/org/apache/druid/query/JoinDataSource.java index 220f18a94855..65ec304c8b8e 100644 --- a/processing/src/main/java/org/apache/druid/query/JoinDataSource.java +++ b/processing/src/main/java/org/apache/druid/query/JoinDataSource.java @@ -529,7 +529,10 @@ private static Triple> flattenJoi // Will need an instanceof check here // A future work should look into if the flattenJoin // can be refactored to omit these instanceof checks - while (current instanceof JoinDataSource || current instanceof UnnestDataSource || current instanceof FilteredDataSource) { + while (current instanceof JoinDataSource + || current instanceof UnnestDataSource + || current instanceof FilteredDataSource + || current instanceof RestrictedDataSource) { if (current instanceof JoinDataSource) { final JoinDataSource joinDataSource = (JoinDataSource) current; current = joinDataSource.getLeft(); @@ -545,6 +548,9 @@ private static Triple> flattenJoi } else if (current instanceof UnnestDataSource) { final UnnestDataSource unnestDataSource = (UnnestDataSource) current; current = unnestDataSource.getBase(); + } else if (current instanceof RestrictedDataSource) { + final RestrictedDataSource restrictedDataSource = (RestrictedDataSource) current; + current = restrictedDataSource.getBase(); } else { final FilteredDataSource filteredDataSource = (FilteredDataSource) current; current = filteredDataSource.getBase(); diff --git a/processing/src/main/java/org/apache/druid/query/Query.java b/processing/src/main/java/org/apache/druid/query/Query.java index 3ed1dcbe0ead..17c84b0ca64c 100644 --- a/processing/src/main/java/org/apache/druid/query/Query.java +++ b/processing/src/main/java/org/apache/druid/query/Query.java @@ -54,6 +54,7 @@ import javax.annotation.Nullable; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -242,6 +243,16 @@ default String getMostSpecificId() Query withDataSource(DataSource dataSource); + default Query withPolicyRestrictions(Map> restrictions) + { + return this.withPolicyRestrictions(restrictions, true); + } + + default Query withPolicyRestrictions(Map> restrictions, boolean enableStrictPolicyCheck) + { + return this.withDataSource(this.getDataSource().mapWithRestriction(restrictions, enableStrictPolicyCheck)); + } + default Query optimizeForSegment(PerSegmentQueryOptimizationContext optimizationContext) { return this; diff --git a/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java b/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java new file mode 100644 index 000000000000..8685006c4971 --- /dev/null +++ b/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java @@ -0,0 +1,227 @@ +/* + * 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.query; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import org.apache.druid.error.DruidException; +import org.apache.druid.java.util.common.IAE; +import org.apache.druid.java.util.common.ISE; +import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.filter.TrueDimFilter; +import org.apache.druid.query.planning.DataSourceAnalysis; +import org.apache.druid.segment.RestrictedSegment; +import org.apache.druid.segment.SegmentReference; +import org.apache.druid.utils.JvmUtils; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; + +/** + * Reperesents a TableDataSource with row-level restriction. + *

+ * A RestrictedDataSource means the base TableDataSource has restriction imposed. A table without any restriction should + * never be transformed to a RestrictedDataSource. Druid internal system and admin users would have a null rowFilter, + * while external users would have a rowFilter based on the applied restriction. + */ +public class RestrictedDataSource implements DataSource +{ + private final TableDataSource base; + @Nullable + private final DimFilter rowFilter; + + @JsonProperty("base") + public TableDataSource getBase() + { + return base; + } + + /** + * Returns true if the row-level filter imposes no restrictions. + */ + public boolean allowAll() + { + return Objects.isNull(rowFilter) || rowFilter.equals(TrueDimFilter.instance()); + } + + @Nullable + @JsonProperty("filter") + public DimFilter getFilter() + { + return rowFilter; + } + + RestrictedDataSource(TableDataSource base, @Nullable DimFilter rowFilter) + { + this.base = base; + this.rowFilter = rowFilter; + } + + @JsonCreator + public static RestrictedDataSource create( + @JsonProperty("base") DataSource base, + @Nullable @JsonProperty("filter") DimFilter rowFilter + ) + { + if (!(base instanceof TableDataSource)) { + throw new IAE("Expected a TableDataSource, got [%s]", base.getClass()); + } + return new RestrictedDataSource((TableDataSource) base, rowFilter); + } + + @Override + public Set getTableNames() + { + return base.getTableNames(); + } + + @Override + public List getChildren() + { + return ImmutableList.of(base); + } + + @Override + public DataSource withChildren(List children) + { + if (children.size() != 1) { + throw new IAE("Expected [1] child, got [%d]", children.size()); + } + + return RestrictedDataSource.create(children.get(0), rowFilter); + } + + @Override + public boolean isCacheable(boolean isBroker) + { + return false; + } + + @Override + public boolean isGlobal() + { + return base.isGlobal(); + } + + @Override + public boolean isConcrete() + { + return base.isConcrete(); + } + + @Override + public Function createSegmentMapFunction( + Query query, + AtomicLong cpuTimeAccumulator + ) + { + return JvmUtils.safeAccumulateThreadCpuTime( + cpuTimeAccumulator, + () -> base.createSegmentMapFunction( + query, + cpuTimeAccumulator + ).andThen((segment) -> (new RestrictedSegment(segment, rowFilter))) + ); + } + + @Override + public DataSource withUpdatedDataSource(DataSource newSource) + { + return RestrictedDataSource.create(newSource, rowFilter); + } + + @Override + public DataSource mapWithRestriction(Map> rowFilters, boolean enableStrictPolicyCheck) + { + if (!rowFilters.containsKey(this.base.getName()) && enableStrictPolicyCheck) { + throw DruidException.defensive("Missing row filter for table [%s]", this.base.getName()); + } + + Optional newFilter = rowFilters.getOrDefault(this.base.getName(), Optional.empty()); + if (!newFilter.isPresent()) { + throw DruidException.defensive( + "No restriction found on table [%s], but had %s before.", + this.base.getName(), + this.rowFilter + ); + } + if (newFilter.get().equals(TrueDimFilter.instance())) { + // The internal druid_system always has a TrueDimFilter, whic can be applied in conjunction with an external user's filter. + return this; + } else if (newFilter.get().equals(rowFilter)) { + // This likely occurs when we perform an authentication check for the same user more than once, which is not ideal. + return this; + } else { + throw new ISE("Incompatible restrictions on [%s]: %s and %s", this.base.getName(), rowFilter, newFilter.get()); + } + } + + @Override + public String toString() + { + try { + return "RestrictedDataSource{" + + "base=" + base + + ", filter='" + rowFilter + '}'; + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public byte[] getCacheKey() + { + return new byte[0]; + } + + @Override + public DataSourceAnalysis getAnalysis() + { + final DataSource current = this.getBase(); + return current.getAnalysis(); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RestrictedDataSource that = (RestrictedDataSource) o; + return Objects.equals(base, that.base) && Objects.equals(rowFilter, that.rowFilter); + } + + @Override + public int hashCode() + { + return Objects.hash(base, rowFilter); + } +} diff --git a/processing/src/main/java/org/apache/druid/query/TableDataSource.java b/processing/src/main/java/org/apache/druid/query/TableDataSource.java index fe9cf46e37b9..9680e2d061ca 100644 --- a/processing/src/main/java/org/apache/druid/query/TableDataSource.java +++ b/processing/src/main/java/org/apache/druid/query/TableDataSource.java @@ -23,13 +23,18 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeName; import com.google.common.base.Preconditions; +import org.apache.druid.error.DruidException; import org.apache.druid.java.util.common.IAE; +import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.filter.TrueDimFilter; import org.apache.druid.query.planning.DataSourceAnalysis; import org.apache.druid.segment.SegmentReference; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; @@ -112,6 +117,20 @@ public DataSource withUpdatedDataSource(DataSource newSource) return newSource; } + @Override + public DataSource mapWithRestriction(Map> rowFilters, boolean enableStrictPolicyCheck) + { + if (!rowFilters.containsKey(this.name) && enableStrictPolicyCheck) { + throw DruidException.defensive("Need to check row-level policy for all tables, missing [%s]", this.name); + } + Optional filter = rowFilters.getOrDefault(this.name, Optional.empty()); + if (!filter.isPresent()) { + // Skip adding restriction on table if there's no policy restriction found. + return this; + } + return RestrictedDataSource.create(this, filter.get().equals(TrueDimFilter.instance()) ? null : filter.get()); + } + @Override public byte[] getCacheKey() { diff --git a/processing/src/main/java/org/apache/druid/query/filter/NotDimFilter.java b/processing/src/main/java/org/apache/druid/query/filter/NotDimFilter.java index 8314e71308f2..8b2b13e4aece 100644 --- a/processing/src/main/java/org/apache/druid/query/filter/NotDimFilter.java +++ b/processing/src/main/java/org/apache/druid/query/filter/NotDimFilter.java @@ -40,7 +40,6 @@ public static NotDimFilter of(DimFilter field) return new NotDimFilter(field); } - private final DimFilter field; @JsonCreator diff --git a/processing/src/main/java/org/apache/druid/query/metadata/metadata/SegmentMetadataQuery.java b/processing/src/main/java/org/apache/druid/query/metadata/metadata/SegmentMetadataQuery.java index f2d434bab8a5..0faa5be7168b 100644 --- a/processing/src/main/java/org/apache/druid/query/metadata/metadata/SegmentMetadataQuery.java +++ b/processing/src/main/java/org/apache/druid/query/metadata/metadata/SegmentMetadataQuery.java @@ -30,6 +30,7 @@ import org.apache.druid.query.DataSource; import org.apache.druid.query.Druids; import org.apache.druid.query.Query; +import org.apache.druid.query.RestrictedDataSource; import org.apache.druid.query.TableDataSource; import org.apache.druid.query.UnionDataSource; import org.apache.druid.query.filter.DimFilter; @@ -74,7 +75,7 @@ public static AnalysisType fromString(String name) @Override public byte[] getCacheKey() { - return new byte[] {(byte) this.ordinal()}; + return new byte[]{(byte) this.ordinal()}; } } @@ -116,9 +117,12 @@ public SegmentMetadataQuery( // of truth for consumers of this class variable. The defaults are to preserve backwards compatibility. // In a future release, 28.0+, we can remove the deprecated property lenientAggregatorMerge. if (lenientAggregatorMerge != null && aggregatorMergeStrategy != null) { - throw InvalidInput.exception("Both lenientAggregatorMerge [%s] and aggregatorMergeStrategy [%s] parameters cannot be set." - + " Consider using aggregatorMergeStrategy since lenientAggregatorMerge is deprecated.", - lenientAggregatorMerge, aggregatorMergeStrategy); + throw InvalidInput.exception( + "Both lenientAggregatorMerge [%s] and aggregatorMergeStrategy [%s] parameters cannot be set." + + " Consider using aggregatorMergeStrategy since lenientAggregatorMerge is deprecated.", + lenientAggregatorMerge, + aggregatorMergeStrategy + ); } if (lenientAggregatorMerge != null) { this.aggregatorMergeStrategy = lenientAggregatorMerge @@ -220,6 +224,11 @@ public Query withQuerySegmentSpec(QuerySegmentSpec spec) @Override public Query withDataSource(DataSource dataSource) { + if (dataSource instanceof RestrictedDataSource && ((RestrictedDataSource) dataSource).allowAll()) { + return Druids.SegmentMetadataQueryBuilder.copy(this) + .dataSource(((RestrictedDataSource) dataSource).getBase()) + .build(); + } return Druids.SegmentMetadataQueryBuilder.copy(this).dataSource(dataSource).build(); } @@ -249,14 +258,14 @@ public List getIntervals() public String toString() { return "SegmentMetadataQuery{" + - "dataSource='" + getDataSource() + '\'' + - ", querySegmentSpec=" + getQuerySegmentSpec() + - ", toInclude=" + toInclude + - ", merge=" + merge + - ", usingDefaultInterval=" + usingDefaultInterval + - ", analysisTypes=" + analysisTypes + - ", aggregatorMergeStrategy=" + aggregatorMergeStrategy + - '}'; + "dataSource='" + getDataSource() + '\'' + + ", querySegmentSpec=" + getQuerySegmentSpec() + + ", toInclude=" + toInclude + + ", merge=" + merge + + ", usingDefaultInterval=" + usingDefaultInterval + + ", analysisTypes=" + analysisTypes + + ", aggregatorMergeStrategy=" + aggregatorMergeStrategy + + '}'; } @Override @@ -273,10 +282,10 @@ public boolean equals(Object o) } SegmentMetadataQuery that = (SegmentMetadataQuery) o; return merge == that.merge && - usingDefaultInterval == that.usingDefaultInterval && - Objects.equals(toInclude, that.toInclude) && - Objects.equals(analysisTypes, that.analysisTypes) && - Objects.equals(aggregatorMergeStrategy, that.aggregatorMergeStrategy); + usingDefaultInterval == that.usingDefaultInterval && + Objects.equals(toInclude, that.toInclude) && + Objects.equals(analysisTypes, that.analysisTypes) && + Objects.equals(aggregatorMergeStrategy, that.aggregatorMergeStrategy); } @Override diff --git a/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java b/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java new file mode 100644 index 000000000000..f29fce0780d5 --- /dev/null +++ b/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java @@ -0,0 +1,74 @@ +/* + * 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.segment; + +import com.google.common.collect.ImmutableList; +import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.filter.Filter; +import org.apache.druid.segment.column.ColumnCapabilities; +import org.apache.druid.segment.column.RowSignature; +import org.apache.druid.segment.filter.AndFilter; + +import javax.annotation.Nullable; + +public class RestrictedCursorFactory implements CursorFactory +{ + private final CursorFactory delegate; + @Nullable + private final DimFilter filter; + + public RestrictedCursorFactory( + CursorFactory delegate, + @Nullable DimFilter filter + ) + { + this.delegate = delegate; + this.filter = filter; + } + + @Override + public CursorHolder makeCursorHolder(CursorBuildSpec spec) + { + if (filter == null) { + return delegate.makeCursorHolder(spec); + } + + final CursorBuildSpec.CursorBuildSpecBuilder buildSpecBuilder = CursorBuildSpec.builder(spec); + final Filter newFilter = spec.getFilter() == null + ? filter.toFilter() + : new AndFilter(ImmutableList.of(spec.getFilter(), filter.toFilter())); + buildSpecBuilder.setFilter(newFilter); + + return delegate.makeCursorHolder(buildSpecBuilder.build()); + } + + @Override + public RowSignature getRowSignature() + { + return delegate.getRowSignature(); + } + + @Nullable + @Override + public ColumnCapabilities getColumnCapabilities(String column) + { + return delegate.getColumnCapabilities(column); + } +} diff --git a/processing/src/main/java/org/apache/druid/segment/RestrictedSegment.java b/processing/src/main/java/org/apache/druid/segment/RestrictedSegment.java new file mode 100644 index 000000000000..9935186da446 --- /dev/null +++ b/processing/src/main/java/org/apache/druid/segment/RestrictedSegment.java @@ -0,0 +1,52 @@ +/* + * 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.segment; + +import org.apache.druid.query.filter.DimFilter; + +import javax.annotation.Nullable; + +public class RestrictedSegment extends WrappedSegmentReference +{ + @Nullable + private final DimFilter filter; + + public RestrictedSegment( + SegmentReference delegate, + @Nullable DimFilter filter + ) + { + super(delegate); + this.filter = filter; + } + + @Override + public CursorFactory asCursorFactory() + { + return new RestrictedCursorFactory(delegate.asCursorFactory(), filter); + } + + @Nullable + @Override + public QueryableIndex asQueryableIndex() + { + throw new RuntimeException("Can't get a queryable index from restricted segment."); + } +} diff --git a/processing/src/test/java/org/apache/druid/query/DataSourceTest.java b/processing/src/test/java/org/apache/druid/query/DataSourceTest.java index e7850953a609..aa83cdbf23d6 100644 --- a/processing/src/test/java/org/apache/druid/query/DataSourceTest.java +++ b/processing/src/test/java/org/apache/druid/query/DataSourceTest.java @@ -20,21 +20,34 @@ package org.apache.druid.query; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; +import org.apache.druid.common.config.NullHandling; import org.apache.druid.query.aggregation.LongSumAggregatorFactory; import org.apache.druid.query.dimension.DefaultDimensionSpec; +import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.filter.NullFilter; +import org.apache.druid.query.filter.TrueDimFilter; import org.apache.druid.query.groupby.GroupByQuery; import org.apache.druid.segment.TestHelper; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import java.io.IOException; +import java.util.Optional; public class DataSourceTest { private static final ObjectMapper JSON_MAPPER = TestHelper.makeJsonMapper(); + @Before + public void setUp() + { + NullHandling.initializeForTests(); // Needed for loading QueryRunnerTestHelper static variables. + } + @Test public void testSerialization() throws IOException { @@ -61,6 +74,16 @@ public void testTableDataSource() throws IOException Assert.assertEquals(new TableDataSource("somedatasource"), dataSource); } + @Test + public void testRestrictedDataSource() throws IOException + { + DataSource dataSource = JSON_MAPPER.readValue( + "{\"type\":\"restrict\",\"base\":{\"type\":\"table\",\"name\":\"somedatasource\"},\"filter\":null}", + DataSource.class + ); + Assert.assertEquals(RestrictedDataSource.create(TableDataSource.create("somedatasource"), null), dataSource); + } + @Test public void testQueryDataSource() throws IOException { @@ -99,4 +122,65 @@ public void testUnionDataSource() throws Exception final DataSource serde = JSON_MAPPER.readValue(JSON_MAPPER.writeValueAsString(dataSource), DataSource.class); Assert.assertEquals(dataSource, serde); } + + @Test + public void testMapWithRestriction() throws Exception + { + TableDataSource table1 = TableDataSource.create("table1"); + TableDataSource table2 = TableDataSource.create("table2"); + TableDataSource table3 = TableDataSource.create("table3"); + UnionDataSource unionDataSource = new UnionDataSource(Lists.newArrayList(table1, table2, table3)); + ImmutableMap> restrictions = ImmutableMap.of( + "table1", + Optional.of(TrueDimFilter.instance()), + "table2", + Optional.empty(), + "table3", + Optional.of(new NullFilter( + "some-column", + null + )) + ); + + Assert.assertEquals( + unionDataSource.mapWithRestriction(restrictions), + new UnionDataSource(Lists.newArrayList( + RestrictedDataSource.create(table1, null), + table2, + RestrictedDataSource.create(table3, new NullFilter("some-column", null)) + )) + ); + } + + @Test + public void testMapWithRestrictionThrowsWhenMissingRestriction() throws Exception + { + TableDataSource table1 = TableDataSource.create("table1"); + TableDataSource table2 = TableDataSource.create("table2"); + UnionDataSource unionDataSource = new UnionDataSource(Lists.newArrayList(table1, table2)); + ImmutableMap> restrictions = ImmutableMap.of( + "table1", + Optional.of(TrueDimFilter.instance()) + ); + + Exception e = Assert.assertThrows(RuntimeException.class, () -> unionDataSource.mapWithRestriction(restrictions)); + Assert.assertEquals(e.getMessage(), "Need to check row-level policy for all tables, missing [table2]"); + } + + @Test + public void testMapWithRestrictionThrowsWithIncompatibleRestriction() throws Exception + { + RestrictedDataSource restrictedDataSource = RestrictedDataSource.create(TableDataSource.create("table1"), null); + ImmutableMap> restrictions = ImmutableMap.of( + "table1", + Optional.of(new NullFilter("some-column", null)) + ); + + Assert.assertThrows(RuntimeException.class, () -> restrictedDataSource.mapWithRestriction(restrictions)); + Assert.assertThrows(RuntimeException.class, () -> restrictedDataSource.mapWithRestriction(ImmutableMap.of())); + Assert.assertThrows( + RuntimeException.class, + () -> restrictedDataSource.mapWithRestriction(ImmutableMap.of("table1", Optional.empty())) + ); + } } diff --git a/processing/src/test/java/org/apache/druid/query/JoinDataSourceTest.java b/processing/src/test/java/org/apache/druid/query/JoinDataSourceTest.java index b821bc49c4e7..382a85ba5a94 100644 --- a/processing/src/test/java/org/apache/druid/query/JoinDataSourceTest.java +++ b/processing/src/test/java/org/apache/druid/query/JoinDataSourceTest.java @@ -481,6 +481,26 @@ public void testGetAnalysisWithFilteredDS() Assert.assertEquals("table1", analysis.getBaseDataSource().getTableNames().iterator().next()); } + @Test + public void testGetAnalysisWithRestrictedDS() + { + JoinDataSource dataSource = JoinDataSource.create( + RestrictedDataSource.create( + new TableDataSource("table1"), + TrueDimFilter.instance() + ), + new TableDataSource("table2"), + "j.", + "x == \"j.x\"", + JoinType.LEFT, + null, + ExprMacroTable.nil(), + null + ); + DataSourceAnalysis analysis = dataSource.getAnalysis(); + Assert.assertEquals("table1", analysis.getBaseDataSource().getTableNames().iterator().next()); + } + @Test public void test_computeJoinDataSourceCacheKey_keyChangesWithBaseFilter() { diff --git a/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java b/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java new file mode 100644 index 000000000000..6239729d20cd --- /dev/null +++ b/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java @@ -0,0 +1,154 @@ +/* + * 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.query; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; +import nl.jqno.equalsverifier.EqualsVerifier; +import org.apache.druid.segment.TestHelper; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.Collections; + +public class RestrictedDataSourceTest +{ + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private final TableDataSource fooDataSource = new TableDataSource("foo"); + private final TableDataSource barDataSource = new TableDataSource("bar"); + private final RestrictedDataSource restrictedFooDataSource = RestrictedDataSource.create(fooDataSource, null); + private final RestrictedDataSource restrictedBarDataSource = RestrictedDataSource.create(barDataSource, null); + + @Test + public void test_getTableNames() + { + Assert.assertEquals(Collections.singleton("foo"), restrictedFooDataSource.getTableNames()); + Assert.assertEquals(Collections.singleton("bar"), restrictedBarDataSource.getTableNames()); + } + + @Test + public void test_getChildren() + { + Assert.assertEquals(Collections.singletonList(fooDataSource), restrictedFooDataSource.getChildren()); + Assert.assertEquals(Collections.singletonList(barDataSource), restrictedBarDataSource.getChildren()); + } + + @Test + public void test_isCacheable() + { + Assert.assertFalse(restrictedFooDataSource.isCacheable(true)); + } + + @Test + public void test_isGlobal() + { + Assert.assertFalse(restrictedFooDataSource.isGlobal()); + } + + @Test + public void test_isConcrete() + { + Assert.assertTrue(restrictedFooDataSource.isConcrete()); + } + + @Test + public void test_withChildren() + { + IllegalArgumentException exception = Assert.assertThrows( + IllegalArgumentException.class, + () -> restrictedFooDataSource.withChildren(Collections.emptyList()) + ); + Assert.assertEquals(exception.getMessage(), "Expected [1] child, got [0]"); + + IllegalArgumentException exception2 = Assert.assertThrows( + IllegalArgumentException.class, + () -> restrictedFooDataSource.withChildren(ImmutableList.of(fooDataSource, barDataSource)) + ); + Assert.assertEquals(exception2.getMessage(), "Expected [1] child, got [2]"); + + RestrictedDataSource newRestrictedDataSource = (RestrictedDataSource) restrictedFooDataSource.withChildren( + ImmutableList.of(barDataSource)); + Assert.assertEquals(newRestrictedDataSource.getBase(), barDataSource); + } + + @Test + public void test_withUpdatedDataSource() + { + RestrictedDataSource newRestrictedDataSource = (RestrictedDataSource) restrictedFooDataSource.withUpdatedDataSource( + new TableDataSource("bar")); + Assert.assertEquals(newRestrictedDataSource.getBase(), barDataSource); + } + + @Test + public void test_withAnalysis() + { + Assert.assertEquals(restrictedFooDataSource.getAnalysis(), fooDataSource.getAnalysis()); + Assert.assertEquals(restrictedBarDataSource.getAnalysis(), barDataSource.getAnalysis()); + } + + @Test + public void test_equals() + { + EqualsVerifier.forClass(RestrictedDataSource.class).usingGetClass().withNonnullFields("base").verify(); + } + + @Test + public void test_serde_roundTrip() throws Exception + { + final ObjectMapper jsonMapper = TestHelper.makeJsonMapper(); + final RestrictedDataSource deserialized = (RestrictedDataSource) jsonMapper.readValue( + jsonMapper.writeValueAsString(restrictedFooDataSource), + DataSource.class + ); + + Assert.assertEquals(restrictedFooDataSource, deserialized); + } + + @Test + public void test_deserialize_fromObject() throws Exception + { + final ObjectMapper jsonMapper = TestHelper.makeJsonMapper(); + final RestrictedDataSource deserializedRestrictedDataSource = jsonMapper.readValue( + "{\"type\":\"restrict\",\"base\":{\"type\":\"table\",\"name\":\"foo\"},\"filter\":null}", + RestrictedDataSource.class + ); + + Assert.assertEquals(restrictedFooDataSource, deserializedRestrictedDataSource); + } + + @Test + public void test_serialize() throws Exception + { + final ObjectMapper jsonMapper = TestHelper.makeJsonMapper(); + final String s = jsonMapper.writeValueAsString(restrictedFooDataSource); + + Assert.assertEquals("{\"type\":\"restrict\",\"base\":{\"type\":\"table\",\"name\":\"foo\"},\"filter\":null}", s); + } + + @Test + public void testStringRep() + { + Assert.assertNotEquals(restrictedFooDataSource.toString(), restrictedBarDataSource.toString()); + } +} diff --git a/processing/src/test/java/org/apache/druid/query/metadata/SegmentMetadataQueryTest.java b/processing/src/test/java/org/apache/druid/query/metadata/SegmentMetadataQueryTest.java index af17379367ed..8c6380270fa5 100644 --- a/processing/src/test/java/org/apache/druid/query/metadata/SegmentMetadataQueryTest.java +++ b/processing/src/test/java/org/apache/druid/query/metadata/SegmentMetadataQueryTest.java @@ -51,6 +51,9 @@ import org.apache.druid.query.Result; import org.apache.druid.query.TableDataSource; import org.apache.druid.query.aggregation.AggregatorFactory; +import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.filter.NullFilter; +import org.apache.druid.query.filter.TrueDimFilter; import org.apache.druid.query.metadata.metadata.AggregatorMergeStrategy; import org.apache.druid.query.metadata.metadata.ColumnAnalysis; import org.apache.druid.query.metadata.metadata.ListColumnIncluderator; @@ -85,6 +88,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -328,6 +332,27 @@ public void testSegmentMetadataQuery() Assert.assertEquals(Collections.singletonList(expectedSegmentAnalysis1), results); } + @Test + public void testSegmentMetadataQueryWorksWithRestrictions() throws Exception + { + ImmutableMap> noRestriction = ImmutableMap.of(DATASOURCE, Optional.empty()); + ImmutableMap> alwaysTrueRestriction = ImmutableMap.of(DATASOURCE, Optional.of( + TrueDimFilter.instance())); + ImmutableMap> withRestriction = ImmutableMap.of(DATASOURCE, Optional.of( + new NullFilter("some-column", null))); + List results1 = runner1.run(QueryPlus.wrap(testQuery.withPolicyRestrictions(noRestriction))) + .toList(); + List results2 = runner1.run(QueryPlus.wrap(testQuery.withPolicyRestrictions(alwaysTrueRestriction))) + .toList(); + + Assert.assertEquals(Collections.singletonList(expectedSegmentAnalysis1), results1); + Assert.assertEquals(Collections.singletonList(expectedSegmentAnalysis1), results2); + Assert.assertThrows( + RuntimeException.class, + () -> runner1.run(QueryPlus.wrap(testQuery.withPolicyRestrictions(withRestriction))) + ); + } + @Test public void testSegmentMetadataQueryWithRollupMerge() { diff --git a/server/src/main/java/org/apache/druid/segment/metadata/AbstractSegmentMetadataCache.java b/server/src/main/java/org/apache/druid/segment/metadata/AbstractSegmentMetadataCache.java index 99d965ec643e..bf96eec0114c 100644 --- a/server/src/main/java/org/apache/druid/segment/metadata/AbstractSegmentMetadataCache.java +++ b/server/src/main/java/org/apache/druid/segment/metadata/AbstractSegmentMetadataCache.java @@ -57,7 +57,7 @@ import org.apache.druid.server.QueryLifecycleFactory; import org.apache.druid.server.coordination.DruidServerMetadata; import org.apache.druid.server.coordination.ServerType; -import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.Escalator; import org.apache.druid.timeline.DataSegment; import org.apache.druid.timeline.SegmentId; @@ -975,7 +975,8 @@ public Sequence runSegmentMetadataQuery( return queryLifecycleFactory .factorize() - .runSimple(segmentMetadataQuery, escalator.createEscalatedAuthenticationResult(), Access.OK).getResults(); + .runSimple(segmentMetadataQuery, escalator.createEscalatedAuthenticationResult(), AuthorizationResult.ALLOW_ALL) + .getResults(); } @VisibleForTesting diff --git a/server/src/main/java/org/apache/druid/segment/metadata/SegmentMetadataQuerySegmentWalker.java b/server/src/main/java/org/apache/druid/segment/metadata/SegmentMetadataQuerySegmentWalker.java index 68f264cebb48..bc66fbf56d69 100644 --- a/server/src/main/java/org/apache/druid/segment/metadata/SegmentMetadataQuerySegmentWalker.java +++ b/server/src/main/java/org/apache/druid/segment/metadata/SegmentMetadataQuerySegmentWalker.java @@ -142,7 +142,11 @@ private Sequence run( final TimelineLookup timelineLookup = timelineConverter.apply(timeline); QueryToolChest> toolChest = conglomerate.getToolChest(query); - Set> segmentAndServers = computeSegmentsToQuery(timelineLookup, query, toolChest); + Set> segmentAndServers = computeSegmentsToQuery( + timelineLookup, + query, + toolChest + ); queryPlus = queryPlus.withQueryMetrics(toolChest); queryPlus.getQueryMetrics().reportQueriedSegmentCount(segmentAndServers.size()).emit(emitter); @@ -181,7 +185,8 @@ Sequence getServerResults( QueryPlus queryPlus, ResponseContext responseContext, long maxQueuedBytesPerServer, - List segmentDescriptors) + List segmentDescriptors + ) { return serverRunner.run( queryPlus.withQuery( @@ -207,7 +212,10 @@ private Set> computeSegmentsToQuery List> timelineObjectHolders = intervals.stream().flatMap(i -> lookupFn.apply(i).stream()).collect(Collectors.toList()); - final List> serversLookup = toolChest.filterSegments(query, timelineObjectHolders); + final List> serversLookup = toolChest.filterSegments( + query, + timelineObjectHolders + ); Set> segmentAndServers = new HashSet<>(); for (TimelineObjectHolder holder : serversLookup) { @@ -252,8 +260,6 @@ private SortedMap> groupSegmentsByServer( private Sequence merge(Query query, List> sequencesByInterval) { - return Sequences - .simple(sequencesByInterval) - .flatMerge(seq -> seq, query.getResultOrdering()); + return Sequences.simple(sequencesByInterval).flatMerge(seq -> seq, query.getResultOrdering()); } } diff --git a/server/src/main/java/org/apache/druid/segment/realtime/ChatHandlers.java b/server/src/main/java/org/apache/druid/segment/realtime/ChatHandlers.java index 4d971db81e08..bcfff65b1852 100644 --- a/server/src/main/java/org/apache/druid/segment/realtime/ChatHandlers.java +++ b/server/src/main/java/org/apache/druid/segment/realtime/ChatHandlers.java @@ -19,8 +19,8 @@ package org.apache.druid.segment.realtime; -import org.apache.druid.server.security.Access; import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.ForbiddenException; @@ -29,6 +29,7 @@ import org.apache.druid.server.security.ResourceType; import javax.servlet.http.HttpServletRequest; +import java.util.Objects; public class ChatHandlers { @@ -37,7 +38,7 @@ public class ChatHandlers * * @return authorization result */ - public static Access authorizationCheck( + public static AuthorizationResult authorizationCheck( HttpServletRequest req, Action action, String dataSource, @@ -49,9 +50,9 @@ public static Access authorizationCheck( action ); - Access access = AuthorizationUtils.authorizeResourceAction(req, resourceAction, authorizerMapper); + AuthorizationResult access = AuthorizationUtils.authorizeResourceAction(req, resourceAction, authorizerMapper); if (!access.isAllowed()) { - throw new ForbiddenException(access.toString()); + throw new ForbiddenException(Objects.requireNonNull(access.getFailureMessage())); } return access; 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 c4ead8bedce7..c314a47554f6 100644 --- a/server/src/main/java/org/apache/druid/server/QueryLifecycle.java +++ b/server/src/main/java/org/apache/druid/server/QueryLifecycle.java @@ -24,6 +24,7 @@ import com.google.common.base.Strings; import com.google.common.collect.Iterables; import org.apache.druid.client.DirectDruidClient; +import org.apache.druid.error.DruidException; import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.ISE; import org.apache.druid.java.util.common.StringUtils; @@ -49,10 +50,10 @@ import org.apache.druid.query.context.ResponseContext; import org.apache.druid.server.QueryResource.ResourceIOReaderWriter; 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.AuthorizationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.Resource; @@ -65,6 +66,7 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -79,7 +81,7 @@ *

  • Execution ({@link #execute()}
  • *
  • Logging ({@link #emitLogsAndMetrics(Throwable, String, long)}
  • * - * + *

    * This object is not thread-safe. */ public class QueryLifecycle @@ -136,16 +138,15 @@ public QueryLifecycle( * does it all in one call. Logs and metrics are emitted when the Sequence is either fully iterated or throws an * exception. * - * @param query the query - * @param authenticationResult authentication result indicating identity of the requester - * @param authorizationResult authorization result of requester - * + * @param query the query + * @param authenticationResult authentication result indicating identity of the requester + * @param authorizationResult authorization result of requester * @return results */ public QueryResponse runSimple( final Query query, final AuthenticationResult authenticationResult, - final Access authorizationResult + final AuthorizationResult authorizationResult ) { initialize(query); @@ -156,7 +157,7 @@ public QueryResponse runSimple( try { preAuthorized(authenticationResult, authorizationResult); if (!authorizationResult.isAllowed()) { - throw new ISE(Access.DEFAULT_ERROR_MESSAGE); + throw new ISE(Objects.requireNonNull(authorizationResult.getFailureMessage())); } queryResponse = execute(); @@ -204,7 +205,10 @@ public void initialize(final Query baseQuery) queryId = UUID.randomUUID().toString(); } - Map mergedUserAndConfigContext = QueryContexts.override(defaultQueryConfig.getContext(), baseQuery.getContext()); + Map mergedUserAndConfigContext = QueryContexts.override( + defaultQueryConfig.getContext(), + baseQuery.getContext() + ); mergedUserAndConfigContext.put(BaseQuery.QUERY_ID, queryId); this.baseQuery = baseQuery.withOverriddenContext(mergedUserAndConfigContext); this.toolChest = conglomerate.getToolChest(this.baseQuery); @@ -215,10 +219,9 @@ public void initialize(final Query baseQuery) * * @param req HTTP request object of the request. If provided, the auth-related fields in the HTTP request * will be automatically set. - * * @return authorization result */ - public Access authorize(HttpServletRequest req) + public AuthorizationResult authorize(HttpServletRequest req) { transition(State.INITIALIZED, State.AUTHORIZING); final Iterable resourcesToAuthorize = Iterables.concat( @@ -249,7 +252,7 @@ public Access authorize(HttpServletRequest req) * @param authenticationResult authentication result indicating identity of the requester * @return authorization result of requester */ - public Access authorize(AuthenticationResult authenticationResult) + public AuthorizationResult authorize(AuthenticationResult authenticationResult) { transition(State.INITIALIZED, State.AUTHORIZING); final Iterable resourcesToAuthorize = Iterables.concat( @@ -272,14 +275,20 @@ public Access authorize(AuthenticationResult authenticationResult) ); } - private void preAuthorized(final AuthenticationResult authenticationResult, final Access access) + private void preAuthorized( + final AuthenticationResult authenticationResult, + final AuthorizationResult authorizationResult + ) { // gotta transition those states, even if we are already authorized transition(State.INITIALIZED, State.AUTHORIZING); - doAuthorize(authenticationResult, access); + doAuthorize(authenticationResult, authorizationResult); } - private Access doAuthorize(final AuthenticationResult authenticationResult, final Access authorizationResult) + private AuthorizationResult doAuthorize( + final AuthenticationResult authenticationResult, + final AuthorizationResult authorizationResult + ) { Preconditions.checkNotNull(authenticationResult, "authenticationResult"); Preconditions.checkNotNull(authorizationResult, "authorizationResult"); @@ -289,6 +298,12 @@ private Access doAuthorize(final AuthenticationResult authenticationResult, fina transition(State.AUTHORIZING, State.UNAUTHORIZED); } else { transition(State.AUTHORIZING, State.AUTHORIZED); + if (!authorizationResult.equals(AuthorizationResult.ALLOW_ALL)) { + this.baseQuery = this.baseQuery.withPolicyRestrictions( + authorizationResult.getPolicyFilters(), + authConfig.isEnableStrictPolicyCheck() + ); + } } this.authenticationResult = authenticationResult; @@ -311,8 +326,8 @@ public QueryResponse execute() @SuppressWarnings("unchecked") final Sequence res = QueryPlus.wrap((Query) baseQuery) - .withIdentity(authenticationResult.getIdentity()) - .run(texasRanger, responseContext); + .withIdentity(authenticationResult.getIdentity()) + .run(texasRanger, responseContext); return new QueryResponse(res == null ? Sequences.empty() : res, responseContext); } @@ -455,7 +470,7 @@ public QueryToolChest getToolChest() private void transition(final State from, final State to) { if (state != from) { - throw new ISE("Cannot transition from[%s] to[%s].", from, to); + throw DruidException.defensive("Cannot transition from[%s] to[%s], current state[%s].", from, to, state); } state = to; diff --git a/server/src/main/java/org/apache/druid/server/QueryResource.java b/server/src/main/java/org/apache/druid/server/QueryResource.java index 06104000b1ca..94f2d1f1f57e 100644 --- a/server/src/main/java/org/apache/druid/server/QueryResource.java +++ b/server/src/main/java/org/apache/druid/server/QueryResource.java @@ -49,8 +49,8 @@ import org.apache.druid.query.context.ResponseContext; import org.apache.druid.query.context.ResponseContext.Keys; import org.apache.druid.server.metrics.QueryCountStatsProvider; -import org.apache.druid.server.security.Access; import org.apache.druid.server.security.AuthConfig; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.ForbiddenException; @@ -74,6 +74,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.Objects; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicLong; @@ -152,14 +153,14 @@ public Response cancelQuery(@PathParam("id") String queryId, @Context final Http datasources = new TreeSet<>(); } - Access authResult = AuthorizationUtils.authorizeAllResourceActions( + AuthorizationResult authResult = AuthorizationUtils.authorizeAllResourceActions( req, Iterables.transform(datasources, AuthorizationUtils.DATASOURCE_WRITE_RA_GENERATOR), authorizerMapper ); if (!authResult.isAllowed()) { - throw new ForbiddenException(authResult.toString()); + throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); } queryScheduler.cancelQuery(queryId); @@ -198,7 +199,7 @@ public Response doPost( log.debug("Got query [%s]", queryLifecycle.getQuery()); } - final Access authResult; + final AuthorizationResult authResult; try { authResult = queryLifecycle.authorize(req); } @@ -215,7 +216,7 @@ public Response doPost( } if (!authResult.isAllowed()) { - throw new ForbiddenException(authResult.toString()); + throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); } final QueryResourceQueryResultPusher pusher = new QueryResourceQueryResultPusher(req, queryLifecycle, io); 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..8184b165df4f 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 @@ -21,7 +21,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.AuthorizationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.ForbiddenException; @@ -29,6 +29,8 @@ import org.apache.druid.server.security.ResourceAction; import org.apache.druid.server.security.ResourceType; +import java.util.Objects; + /** * Use this ResourceFilter at end points where Druid Cluster configuration is read or written * Here are some example paths where this filter is used - @@ -56,14 +58,14 @@ public ContainerRequest filter(ContainerRequest request) getAction(request) ); - final Access authResult = AuthorizationUtils.authorizeResourceAction( + final AuthorizationResult authResult = AuthorizationUtils.authorizeResourceAction( getReq(), resourceAction, getAuthorizerMapper() ); if (!authResult.isAllowed()) { - throw new ForbiddenException(authResult.toString()); + throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); } 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 2e84e5bd1f38..970ba60223d8 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 @@ -23,7 +23,7 @@ import com.google.common.collect.Iterables; 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.AuthorizationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.ForbiddenException; @@ -33,6 +33,7 @@ import javax.ws.rs.core.PathSegment; import java.util.List; +import java.util.Objects; /** * Use this resource filter for API endpoints that contain {@link #DATASOURCES_PATH_SEGMENT} in their request path. @@ -57,14 +58,14 @@ public ContainerRequest filter(ContainerRequest request) getAction(request) ); - final Access authResult = AuthorizationUtils.authorizeResourceAction( + final AuthorizationResult authResult = AuthorizationUtils.authorizeResourceAction( getReq(), resourceAction, getAuthorizerMapper() ); if (!authResult.isAllowed()) { - throw new ForbiddenException(authResult.toString()); + throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); } 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..b90d967a91d7 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 @@ -24,7 +24,7 @@ import com.google.common.collect.Iterables; 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.AuthorizationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.ForbiddenException; @@ -33,13 +33,14 @@ import org.apache.druid.server.security.ResourceType; import javax.ws.rs.core.PathSegment; +import java.util.Objects; /** * Use this ResourceFilter when the datasource information is present after "rules" segment in the request Path * Here are some example paths where this filter is used - - * - druid/coordinator/v1/rules/ - * */ + * - druid/coordinator/v1/rules/ + */ public class RulesResourceFilter extends AbstractResourceFilter { @@ -75,14 +76,14 @@ public boolean apply(PathSegment input) getAction(request) ); - final Access authResult = AuthorizationUtils.authorizeResourceAction( + final AuthorizationResult authResult = AuthorizationUtils.authorizeResourceAction( getReq(), resourceAction, getAuthorizerMapper() ); if (!authResult.isAllowed()) { - throw new ForbiddenException(authResult.toString()); + throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); } 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..890e37452386 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 @@ -21,13 +21,15 @@ 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.AuthorizationResult; 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 java.util.Objects; + /** * Use this ResourceFilter at end points where Druid Cluster State is read or written * Here are some example paths where this filter is used - @@ -59,14 +61,14 @@ public ContainerRequest filter(ContainerRequest request) getAction(request) ); - final Access authResult = AuthorizationUtils.authorizeResourceAction( + final AuthorizationResult authResult = AuthorizationUtils.authorizeResourceAction( getReq(), resourceAction, getAuthorizerMapper() ); if (!authResult.isAllowed()) { - throw new ForbiddenException(authResult.toString()); + throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); } return request; diff --git a/server/src/main/java/org/apache/druid/server/security/Access.java b/server/src/main/java/org/apache/druid/server/security/Access.java index 706a78329062..0dff2d3d64ef 100644 --- a/server/src/main/java/org/apache/druid/server/security/Access.java +++ b/server/src/main/java/org/apache/druid/server/security/Access.java @@ -21,26 +21,55 @@ import com.google.common.base.Strings; import org.apache.druid.java.util.common.StringUtils; +import org.apache.druid.query.filter.DimFilter; + +import javax.annotation.Nullable; +import java.util.Objects; +import java.util.Optional; public class Access { public static final String DEFAULT_ERROR_MESSAGE = "Unauthorized"; + public static final String DEFAULT_AUTHORIZED_MESSAGE = "Authorized"; - public static final Access OK = new Access(true); - public static final Access DENIED = new Access(false); + public static final Access OK = Access.allow(); + public static final Access DENIED = Access.deny(""); private final boolean allowed; private final String message; + // A row-level policy filter on top of table-level read access. It should be empty if there are no policy restrictions + // or if access is requested for an action other than reading the table. + private final Optional rowFilter; + /** + * @deprecated use {@link #allow()} or {@link #deny(String)} instead + */ + @Deprecated public Access(boolean allowed) { - this(allowed, ""); + this(allowed, "", Optional.empty()); } - public Access(boolean allowed, String message) + Access(boolean allowed, String message, Optional rowFilter) { this.allowed = allowed; this.message = message; + this.rowFilter = rowFilter; + } + + public static Access allow() + { + return new Access(true, "", Optional.empty()); + } + + public static Access deny(@Nullable String message) + { + return new Access(false, Objects.isNull(message) ? "" : message, Optional.empty()); + } + + public static Access allowWithRestriction(Optional rowFilter) + { + return new Access(true, "", rowFilter); } public boolean isAllowed() @@ -48,25 +77,30 @@ public boolean isAllowed() return allowed; } - public String getMessage() + public Optional getRowFilter() { - return message; + return rowFilter; } - public String toMessage() + public String getMessage() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(allowed ? DEFAULT_AUTHORIZED_MESSAGE : DEFAULT_ERROR_MESSAGE); if (!Strings.isNullOrEmpty(message)) { - return toString(); - } else if (allowed) { - return "Authorized"; - } else { - return DEFAULT_ERROR_MESSAGE; + stringBuilder.append(", "); + stringBuilder.append(message); + } + if (allowed && rowFilter.isPresent()) { + stringBuilder.append(", with restriction "); + stringBuilder.append(rowFilter.get()); } + return stringBuilder.toString(); } @Override public String toString() { - return StringUtils.format("Allowed:%s, Message:%s", allowed, message); + return StringUtils.format("Allowed:%s, Message:%s, Row filter: %s", allowed, message, rowFilter); } + } 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 8413155a8f3c..84817b8322c0 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 @@ -63,7 +63,7 @@ public class AuthConfig public AuthConfig() { - this(null, null, null, false, false, null, null, false); + this(null, null, null, false, false, null, null, false, false); } @JsonProperty @@ -100,6 +100,9 @@ public AuthConfig() @JsonProperty private final boolean enableInputSourceSecurity; + @JsonProperty + private final boolean enableStrictPolicyCheck; + @JsonCreator public AuthConfig( @JsonProperty("authenticatorChain") List authenticatorChain, @@ -109,7 +112,8 @@ public AuthConfig( @JsonProperty("authorizeQueryContextParams") boolean authorizeQueryContextParams, @JsonProperty("unsecuredContextKeys") Set unsecuredContextKeys, @JsonProperty("securedContextKeys") Set securedContextKeys, - @JsonProperty("enableInputSourceSecurity") boolean enableInputSourceSecurity + @JsonProperty("enableInputSourceSecurity") boolean enableInputSourceSecurity, + @JsonProperty("enableStrictPolicyCheck") boolean enableStrictPolicyCheck ) { this.authenticatorChain = authenticatorChain; @@ -118,10 +122,11 @@ public AuthConfig( this.allowUnauthenticatedHttpOptions = allowUnauthenticatedHttpOptions; this.authorizeQueryContextParams = authorizeQueryContextParams; this.unsecuredContextKeys = unsecuredContextKeys == null - ? Collections.emptySet() - : unsecuredContextKeys; + ? Collections.emptySet() + : unsecuredContextKeys; this.securedContextKeys = securedContextKeys; this.enableInputSourceSecurity = enableInputSourceSecurity; + this.enableStrictPolicyCheck = enableStrictPolicyCheck; } public List getAuthenticatorChain() @@ -154,6 +159,11 @@ public boolean isEnableInputSourceSecurity() return enableInputSourceSecurity; } + public boolean isEnableStrictPolicyCheck() + { + return enableStrictPolicyCheck; + } + /** * Filter the user-supplied context keys based on the context key security * rules. If context key security is disabled, then allow all keys. Else, @@ -201,7 +211,8 @@ public boolean equals(Object o) && Objects.equals(unsecuredPaths, that.unsecuredPaths) && Objects.equals(unsecuredContextKeys, that.unsecuredContextKeys) && Objects.equals(securedContextKeys, that.securedContextKeys) - && Objects.equals(enableInputSourceSecurity, that.enableInputSourceSecurity); + && Objects.equals(enableInputSourceSecurity, that.enableInputSourceSecurity) + && Objects.equals(enableStrictPolicyCheck, that.enableStrictPolicyCheck); } @Override @@ -215,7 +226,8 @@ public int hashCode() authorizeQueryContextParams, unsecuredContextKeys, securedContextKeys, - enableInputSourceSecurity + enableInputSourceSecurity, + enableStrictPolicyCheck ); } @@ -231,6 +243,7 @@ public String toString() ", unsecuredContextKeys=" + unsecuredContextKeys + ", securedContextKeys=" + securedContextKeys + ", enableInputSourceSecurity=" + enableInputSourceSecurity + + ", enableStrictPolicyCheck=" + enableStrictPolicyCheck + '}'; } @@ -252,6 +265,7 @@ public static class Builder private Set unsecuredContextKeys; private Set securedContextKeys; private boolean enableInputSourceSecurity; + private boolean enableStrictPolicyCheck; public Builder setAuthenticatorChain(List authenticatorChain) { @@ -301,6 +315,12 @@ public Builder setEnableInputSourceSecurity(boolean enableInputSourceSecurity) return this; } + public Builder setEnableStrictPolicyCheck(boolean enableStrictPolicyCheck) + { + this.enableStrictPolicyCheck = enableStrictPolicyCheck; + return this; + } + public AuthConfig build() { return new AuthConfig( @@ -311,7 +331,8 @@ public AuthConfig build() authorizeQueryContextParams, unsecuredContextKeys, securedContextKeys, - enableInputSourceSecurity + enableInputSourceSecurity, + enableStrictPolicyCheck ); } } diff --git a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java new file mode 100644 index 000000000000..d4c16615daeb --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java @@ -0,0 +1,136 @@ +/* + * 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.security; + +import com.google.common.collect.ImmutableMap; +import org.apache.druid.query.filter.DimFilter; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +public class AuthorizationResult +{ + /** + * Provides unrestricted access to all resources. This should be limited to Druid internal systems or superusers, + * except in cases where ACL considerations are not a priority. + */ + public static final AuthorizationResult ALLOW_ALL = new AuthorizationResult( + true, + null, + Collections.emptyMap(), + null, + null + ); + + /** + * Provides a default deny access result. + */ + public static final AuthorizationResult DENY = new AuthorizationResult( + false, + Access.DENIED.getMessage(), + Collections.emptyMap(), + null, + null + ); + + private final boolean isAllowed; + + @Nullable + private final String failureMessage; + + private final Map> policyFilters; + + @Nullable + private final Set sqlResourceActions; + + @Nullable + private final Set allResourceActions; + + AuthorizationResult( + boolean isAllowed, + @Nullable String failureMessage, + Map> policyFilters, + @Nullable Set sqlResourceActions, + @Nullable Set allResourceActions + ) + { + this.isAllowed = isAllowed; + this.failureMessage = failureMessage; + this.policyFilters = policyFilters; + this.sqlResourceActions = sqlResourceActions; + this.allResourceActions = allResourceActions; + } + + public static AuthorizationResult deny(@Nonnull String failureMessage) + { + return new AuthorizationResult(false, failureMessage, Collections.emptyMap(), null, null); + } + + public static AuthorizationResult allowWithRestriction(Map> policyFilters) + { + return new AuthorizationResult(true, null, policyFilters, null, null); + } + + public AuthorizationResult withResourceActions( + Set sqlResourceActions, + Set allResourceActions + ) + { + return new AuthorizationResult( + isAllowed(), + getFailureMessage(), + ImmutableMap.copyOf(getPolicyFilters()), + sqlResourceActions, + allResourceActions + ); + } + + public boolean isAllowed() + { + return isAllowed; + } + + @Nullable + public String getFailureMessage() + { + return failureMessage; + } + + public Map> getPolicyFilters() + { + return policyFilters; + } + + @Nullable + public Set getSqlResourceActions() + { + return sqlResourceActions; + } + + @Nullable + public Set getAllResourceActions() + { + return allResourceActions; + } +} 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 431819da8a42..5fbac8a5b668 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 @@ -20,6 +20,7 @@ package org.apache.druid.server.security; import com.google.common.base.Function; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import org.apache.druid.audit.AuditInfo; @@ -27,6 +28,7 @@ import org.apache.druid.audit.RequestInfo; import org.apache.druid.error.DruidException; import org.apache.druid.java.util.common.ISE; +import org.apache.druid.query.filter.DimFilter; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; @@ -35,6 +37,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; /** @@ -42,22 +45,23 @@ */ public class AuthorizationUtils { + static final ImmutableSet RESTRICTION_APPLICABLE_RESOURCE_TYPES = ImmutableSet.of( + ResourceType.DATASOURCE, + ResourceType.VIEW + ); + /** - * Check a resource-action using the authorization fields from the request. - * - * Otherwise, if the resource-actions is authorized, return ACCESS_OK. - * - * This function will set the DRUID_AUTHORIZATION_CHECKED attribute in the request. - * - * If this attribute is already set when this function is called, an exception is thrown. + * Performs authorization check on a single resource-action based on the authentication fields from the request. + *

    + * This function will set the DRUID_AUTHORIZATION_CHECKED attribute in the request. If this attribute is already set + * when this function is called, an exception is thrown. * * @param request HTTP request to be authorized * @param resourceAction A resource identifier and the action to be taken the resource. * @param authorizerMapper The singleton AuthorizerMapper instance - * - * @return ACCESS_OK or the failed Access object returned by the Authorizer that checked the request. + * @return AuthorizationResult containing allow/deny access to the resource action, along with policy restrictions. */ - public static Access authorizeResourceAction( + public static AuthorizationResult authorizeResourceAction( final HttpServletRequest request, final ResourceAction resourceAction, final AuthorizerMapper authorizerMapper @@ -74,9 +78,7 @@ public static Access authorizeResourceAction( * Returns the authentication information for a request. * * @param request http request - * * @return authentication result - * * @throws IllegalStateException if the request was not authenticated */ public static AuthenticationResult authenticationResultFromRequest(final HttpServletRequest request) @@ -145,19 +147,15 @@ public static RequestInfo buildRequestInfo(String service, HttpServletRequest re } /** - * Check a list of resource-actions to be performed by the identity represented by authenticationResult. - * - * If one of the resource-actions fails the authorization check, this method returns the failed - * Access object from the check. - * - * Otherwise, return ACCESS_OK if all resource-actions were successfully authorized. + * Performs authorization check on a list of resource-actions based on the authenticationResult. + *

    + * If one of the resource-actions denys access, returns deny access immediately. * * @param authenticationResult Authentication result representing identity of requester * @param resourceActions An Iterable of resource-actions to authorize - * - * @return ACCESS_OK or the Access object from the first failed check + * @return AuthorizationResult containing allow/deny access to the resource actions, along with policy restrictions. */ - public static Access authorizeAllResourceActions( + public static AuthorizationResult authorizeAllResourceActions( final AuthenticationResult authenticationResult, final Iterable resourceActions, final AuthorizerMapper authorizerMapper @@ -170,6 +168,7 @@ public static Access authorizeAllResourceActions( // this method returns on first failure, so only successful Access results are kept in the cache final Set resultCache = new HashSet<>(); + final Map> policyFilters = new HashMap<>(); for (ResourceAction resourceAction : resourceActions) { if (resultCache.contains(resourceAction)) { @@ -181,47 +180,54 @@ public static Access authorizeAllResourceActions( resourceAction.getAction() ); if (!access.isAllowed()) { - return access; + return AuthorizationResult.deny(access.getMessage()); } else { resultCache.add(resourceAction); + if (resourceAction.getAction().equals(Action.READ) + && RESTRICTION_APPLICABLE_RESOURCE_TYPES.contains(resourceAction.getResource().getType())) { + policyFilters.put(resourceAction.getResource().getName(), access.getRowFilter()); + } else if (access.getRowFilter().isPresent()) { + throw DruidException.defensive( + "Row policy should only present when reading a table, but was present for %s", + resourceAction + ); + } else { + // Not a read table action, access doesn't have a filter, do nothing. + } } } - return Access.OK; + return AuthorizationResult.allowWithRestriction(policyFilters); } + /** - * Check a list of resource-actions to be performed as a result of an HTTP request. - * - * If one of the resource-actions fails the authorization check, this method returns the failed - * Access object from the check. - * - * Otherwise, return ACCESS_OK if all resource-actions were successfully authorized. - * - * This function will set the DRUID_AUTHORIZATION_CHECKED attribute in the request. - * - * If this attribute is already set when this function is called, an exception is thrown. + * Performs authorization check on a list of resource-actions based on the authentication fields from the request. + *

    + * If one of the resource-actions denys access, returns deny access immediately. + *

    + * This function will set the DRUID_AUTHORIZATION_CHECKED attribute in the request. If this attribute is already set + * when this function is called, an exception is thrown. * * @param request HTTP request to be authorized * @param resourceActions An Iterable of resource-actions to authorize - * - * @return ACCESS_OK or the Access object from the first failed check + * @return AuthorizationResult containing allow/deny access to the resource actions, along with policy restrictions. */ - public static Access authorizeAllResourceActions( + public static AuthorizationResult authorizeAllResourceActions( final HttpServletRequest request, final Iterable resourceActions, final AuthorizerMapper authorizerMapper ) { if (request.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH) != null) { - return Access.OK; + return AuthorizationResult.ALLOW_ALL; } if (request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED) != null) { throw new ISE("Request already had authorization check."); } - Access access = authorizeAllResourceActions( + AuthorizationResult access = authorizeAllResourceActions( authenticationResultFromRequest(request), resourceActions, authorizerMapper @@ -249,28 +255,22 @@ public static void setRequestAuthorizationAttributeIfNeeded(final HttpServletReq } /** - * Filter a collection of resources by applying the resourceActionGenerator to each resource, return an iterable - * containing the filtered resources. - * - * The resourceActionGenerator returns an Iterable for each resource. - * - * If every resource-action in the iterable is authorized, the resource will be added to the filtered resources. - * - * If there is an authorization failure for one of the resource-actions, the resource will not be - * added to the returned filtered resources.. - * - * If the resourceActionGenerator returns null for a resource, that resource will not be added to the filtered - * resources. - * - * This function will set the DRUID_AUTHORIZATION_CHECKED attribute in the request. - * - * If this attribute is already set when this function is called, an exception is thrown. + * Return an iterable of authorized resources, by filtering the input resources with authorization checks based on the + * authentication fields from the request. This method does: + *

  • + * For every resource, resourceActionGenerator generates an Iterable of ResourceAction or null. + *
  • + * If null, continue with next resource. If any resource-action in the iterable has deny-access, continue with next + * resource. Only when every resource-action has allow-access, add the resource to the result. + *
  • + *

    + * This function will set the DRUID_AUTHORIZATION_CHECKED attribute in the request. If this attribute is already set + * when this function is called, an exception is thrown. * * @param request HTTP request to be authorized * @param resources resources to be processed into resource-actions * @param resourceActionGenerator Function that creates an iterable of resource-actions from a resource * @param authorizerMapper authorizer mapper - * * @return Iterable containing resources that were authorized */ public static Iterable filterAuthorizedResources( @@ -305,24 +305,18 @@ public static Iterable filterAuthorizedResources( } /** - * Filter a collection of resources by applying the resourceActionGenerator to each resource, return an iterable - * containing the filtered resources. - * - * The resourceActionGenerator returns an Iterable for each resource. - * - * If every resource-action in the iterable is authorized, the resource will be added to the filtered resources. - * - * If there is an authorization failure for one of the resource-actions, the resource will not be - * added to the returned filtered resources.. - * - * If the resourceActionGenerator returns null for a resource, that resource will not be added to the filtered - * resources. + * Return an iterable of authorized resources, by filtering the input resources with authorization checks based on + * authenticationResult. This method does: + *

  • + * For every resource, resourceActionGenerator generates an Iterable of ResourceAction or null. + *
  • + * If null, continue with next resource. If any resource-action in the iterable has deny-access, continue with next + * resource. Only when every resource-action has allow-access, add the resource to the result. * * @param authenticationResult Authentication result representing identity of requester * @param resources resources to be processed into resource-actions * @param resourceActionGenerator Function that creates an iterable of resource-actions from a resource * @param authorizerMapper authorizer mapper - * * @return Iterable containing resources that were authorized */ public static Iterable filterAuthorizedResources( @@ -369,23 +363,22 @@ public static Iterable filterAuthorizedResources( } /** - * Given a map of resource lists, filter each resources list by applying the resource action generator to each - * item in each resource list. - * - * The resourceActionGenerator returns an Iterable for each resource. - * - * If a resource list is null or has no authorized items after filtering, it will not be included in the returned - * map. - * - * This function will set the DRUID_AUTHORIZATION_CHECKED attribute in the request. - * - * If this attribute is already set when this function is called, an exception is thrown. + * Return a map of authorized resources, by filtering the input resources with authorization checks based on the + * authentication fields from the request. This method does: + *
  • + * For every resource, resourceActionGenerator generates an Iterable of ResourceAction or null. + *
  • + * If null, continue with next resource. If any resource-action in the iterable has deny-access, continue with next + * resource. Only when every resource-action has allow-access, add the resource to the result. + *
  • + *

    + * This function will set the DRUID_AUTHORIZATION_CHECKED attribute in the request. If this attribute is already set + * when this function is called, an exception is thrown. * * @param request HTTP request to be authorized * @param unfilteredResources Map of resource lists to be filtered * @param resourceActionGenerator Function that creates an iterable of resource-actions from a resource * @param authorizerMapper authorizer mapper - * * @return Map containing lists of resources that were authorized */ public static Map> filterAuthorizedResources( @@ -437,7 +430,7 @@ public static Map> filterAuthorizedRes * This method constructs a 'superuser' set of permissions composed of {@link Action#READ} and {@link Action#WRITE} * permissions for all known {@link ResourceType#knownTypes()} for any {@link Authorizer} implementation which is * built on pattern matching with a regex. - * + *

    * Note that if any {@link Resource} exist that use custom types not registered with * {@link ResourceType#registerResourceType}, those permissions will not be included in this list and will need to * be added manually. diff --git a/server/src/test/java/org/apache/druid/segment/metadata/CoordinatorSegmentMetadataCacheTest.java b/server/src/test/java/org/apache/druid/segment/metadata/CoordinatorSegmentMetadataCacheTest.java index 22b0890e855e..edbf3eec0e6f 100644 --- a/server/src/test/java/org/apache/druid/segment/metadata/CoordinatorSegmentMetadataCacheTest.java +++ b/server/src/test/java/org/apache/druid/segment/metadata/CoordinatorSegmentMetadataCacheTest.java @@ -78,8 +78,8 @@ import org.apache.druid.server.coordinator.loading.SegmentReplicaCount; import org.apache.druid.server.coordinator.loading.SegmentReplicationStatus; import org.apache.druid.server.metrics.NoopServiceEmitter; -import org.apache.druid.server.security.Access; import org.apache.druid.server.security.AllowAllAuthenticator; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.NoopEscalator; import org.apache.druid.timeline.DataSegment; import org.apache.druid.timeline.SegmentId; @@ -1064,7 +1064,11 @@ public void testRunSegmentMetadataQueryWithContext() throws Exception EasyMock.expect(factoryMock.factorize()).andReturn(lifecycleMock).once(); // This is the mat of the test, making sure that the query created by the method under test matches the expected query, specifically the operator configured context - EasyMock.expect(lifecycleMock.runSimple(expectedMetadataQuery, AllowAllAuthenticator.ALLOW_ALL_RESULT, Access.OK)) + EasyMock.expect(lifecycleMock.runSimple( + expectedMetadataQuery, + AllowAllAuthenticator.ALLOW_ALL_RESULT, + AuthorizationResult.ALLOW_ALL + )) .andReturn(QueryResponse.withEmptyContext(Sequences.empty())); EasyMock.replay(factoryMock, lifecycleMock); @@ -2299,7 +2303,11 @@ public void testTombstoneSegmentIsNotRefreshed() throws IOException ); EasyMock.expect(factoryMock.factorize()).andReturn(lifecycleMock).once(); - EasyMock.expect(lifecycleMock.runSimple(expectedMetadataQuery, AllowAllAuthenticator.ALLOW_ALL_RESULT, Access.OK)) + EasyMock.expect(lifecycleMock.runSimple( + expectedMetadataQuery, + AllowAllAuthenticator.ALLOW_ALL_RESULT, + AuthorizationResult.ALLOW_ALL + )) .andReturn(QueryResponse.withEmptyContext(Sequences.empty())).once(); EasyMock.replay(factoryMock, lifecycleMock); diff --git a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java index 8bc436ed405c..e745def91bd6 100644 --- a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java +++ b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java @@ -26,38 +26,48 @@ import org.apache.druid.java.util.common.Intervals; import org.apache.druid.java.util.common.guava.Sequences; import org.apache.druid.java.util.emitter.service.ServiceEmitter; +import org.apache.druid.query.DataSource; import org.apache.druid.query.DefaultQueryConfig; import org.apache.druid.query.Druids; import org.apache.druid.query.GenericQueryMetricsFactory; +import org.apache.druid.query.Query; import org.apache.druid.query.QueryContextTest; import org.apache.druid.query.QueryMetrics; import org.apache.druid.query.QueryRunner; import org.apache.druid.query.QueryRunnerFactoryConglomerate; import org.apache.druid.query.QuerySegmentWalker; import org.apache.druid.query.QueryToolChest; +import org.apache.druid.query.RestrictedDataSource; +import org.apache.druid.query.TableDataSource; import org.apache.druid.query.aggregation.CountAggregatorFactory; +import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.filter.NullFilter; +import org.apache.druid.query.filter.TrueDimFilter; import org.apache.druid.query.timeseries.TimeseriesQuery; 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.AuthorizationResult; 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.ResourceType; import org.easymock.EasyMock; +import org.easymock.IArgumentMatcher; import org.junit.After; import org.junit.Assert; +import org.junit.Assume; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import javax.servlet.http.HttpServletRequest; - import java.util.HashMap; import java.util.Map; +import java.util.Optional; public class QueryLifecycleTest { @@ -157,7 +167,7 @@ public void testRunSimplePreauthorized() replayAll(); QueryLifecycle lifecycle = createLifecycle(new AuthConfig()); - lifecycle.runSimple(query, authenticationResult, Access.OK); + lifecycle.runSimple(query, authenticationResult, AuthorizationResult.ALLOW_ALL); } @Test @@ -178,7 +188,238 @@ public void testRunSimpleUnauthorized() replayAll(); QueryLifecycle lifecycle = createLifecycle(new AuthConfig()); - lifecycle.runSimple(query, authenticationResult, new Access(false)); + lifecycle.runSimple(query, authenticationResult, AuthorizationResult.DENY); + } + + @Test + public void testAuthorizedWithNoPolicyRestriction() + { + EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); + EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); + EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); + EasyMock.expect(authorizer.authorize( + authenticationResult, + new Resource(DATASOURCE, ResourceType.DATASOURCE), + Action.READ + )) + .andReturn(Access.allowWithRestriction(Optional.empty())).once(); + EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) + .andReturn(toolChest).times(2); + EasyMock.expect(texasRanger.getQueryRunnerForIntervals( + queryMatchDataSource(TableDataSource.create(DATASOURCE)), + EasyMock.anyObject() + )) + .andReturn(runner).times(2); + EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).times(2); + replayAll(); + + final TimeseriesQuery query = Druids.newTimeseriesQueryBuilder() + .dataSource(DATASOURCE) + .intervals(ImmutableList.of(Intervals.ETERNITY)) + .aggregators(new CountAggregatorFactory("chocula")) + .build(); + AuthConfig authConfig = AuthConfig.newBuilder() + .setAuthorizeQueryContextParams(true) + .setEnableStrictPolicyCheck(true) + .build(); + QueryLifecycle lifecycle = createLifecycle(authConfig); + lifecycle.initialize(query); + Assert.assertTrue(lifecycle.authorize(authenticationResult).isAllowed()); + lifecycle.execute(); + + lifecycle = createLifecycle(authConfig); + lifecycle.runSimple(query, authenticationResult, AuthorizationResult.ALLOW_ALL); + } + + @Test + public void testAuthorizedWithAlwaysTruePolicyRestriction() + { + Optional alwaysTrueFilter = Optional.of(TrueDimFilter.instance()); + EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); + EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); + EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); + EasyMock.expect(authorizer.authorize( + authenticationResult, + new Resource(DATASOURCE, ResourceType.DATASOURCE), + Action.READ + )) + .andReturn(Access.allowWithRestriction(alwaysTrueFilter)).once(); + EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) + .andReturn(toolChest).times(2); + EasyMock.expect(texasRanger.getQueryRunnerForIntervals(queryMatchDataSource(RestrictedDataSource.create( + TableDataSource.create(DATASOURCE), + null + )), EasyMock.anyObject())) + .andReturn(runner).times(2); + EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).times(2); + replayAll(); + + final TimeseriesQuery query = Druids.newTimeseriesQueryBuilder() + .dataSource(DATASOURCE) + .intervals(ImmutableList.of(Intervals.ETERNITY)) + .aggregators(new CountAggregatorFactory("chocula")) + .build(); + AuthConfig authConfig = AuthConfig.newBuilder() + .setAuthorizeQueryContextParams(true) + .setEnableStrictPolicyCheck(true) + .build(); + QueryLifecycle lifecycle = createLifecycle(authConfig); + lifecycle.initialize(query); + Assert.assertTrue(lifecycle.authorize(authenticationResult).isAllowed()); + lifecycle.execute(); + + lifecycle = createLifecycle(authConfig); + lifecycle.runSimple( + query, + authenticationResult, + AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, alwaysTrueFilter)) + ); + } + + @Test + public void testAuthorizedWithOnePolicyRestriction() + { + Optional rowFilter = Optional.of(new NullFilter("some-column", null)); + final TimeseriesQuery query = Druids.newTimeseriesQueryBuilder() + .dataSource(DATASOURCE) + .intervals(ImmutableList.of(Intervals.ETERNITY)) + .aggregators(new CountAggregatorFactory("chocula")) + .build(); + EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); + EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); + EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); + EasyMock.expect(authorizer.authorize( + authenticationResult, + new Resource(DATASOURCE, ResourceType.DATASOURCE), + Action.READ + )) + .andReturn(Access.allowWithRestriction(rowFilter)).times(1); + EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) + .andReturn(toolChest).times(2); + EasyMock.expect(texasRanger.getQueryRunnerForIntervals(queryMatchDataSource(RestrictedDataSource.create( + TableDataSource.create(DATASOURCE), + rowFilter.get() + )), EasyMock.anyObject())) + .andReturn(runner).times(2); + EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).times(2); + replayAll(); + + AuthConfig authConfig = AuthConfig.newBuilder() + .setAuthorizeQueryContextParams(true) + .setEnableStrictPolicyCheck(true) + .build(); + QueryLifecycle lifecycle = createLifecycle(authConfig); + lifecycle.initialize(query); + Assert.assertTrue(lifecycle.authorize(authenticationResult).isAllowed()); + lifecycle.execute(); + + lifecycle = createLifecycle(authConfig); + lifecycle.runSimple( + query, + authenticationResult, + AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, rowFilter)) + ); + } + + + @Test + public void testAuthorizedMissingPolicyRestriction() + { + final TimeseriesQuery query = Druids.newTimeseriesQueryBuilder() + .dataSource(DATASOURCE) + .intervals(ImmutableList.of(Intervals.ETERNITY)) + .aggregators(new CountAggregatorFactory("chocula")) + .build(); + final TimeseriesQuery queryOnRestrictedDS = (TimeseriesQuery) query.withPolicyRestrictions(ImmutableMap.of( + DATASOURCE, + Optional.of(TrueDimFilter.instance()) + )); + Assume.assumeTrue(queryOnRestrictedDS.getDataSource() instanceof RestrictedDataSource); + EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); + EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())).andReturn(toolChest).anyTimes(); + EasyMock.expect(toolChest.makeMetrics(EasyMock.anyObject())).andReturn(metrics).anyTimes(); + replayAll(); + + AuthConfig authConfig = AuthConfig.newBuilder() + .setAuthorizeQueryContextParams(true) + .setEnableStrictPolicyCheck(true) + .build(); + QueryLifecycle lifecycle = createLifecycle(authConfig); + RuntimeException e = Assert.assertThrows(RuntimeException.class, () -> + lifecycle.runSimple( + query, + authenticationResult, + AuthorizationResult.allowWithRestriction(ImmutableMap.of()) + )); + Assert.assertEquals("Need to check row-level policy for all tables, missing [some_datasource]", e.getMessage()); + + QueryLifecycle lifecycle2 = createLifecycle(authConfig); + RuntimeException e2 = Assert.assertThrows(RuntimeException.class, () -> + lifecycle2.runSimple( + queryOnRestrictedDS, + authenticationResult, + AuthorizationResult.allowWithRestriction(ImmutableMap.of()) + )); + Assert.assertEquals("Missing row filter for table [some_datasource]", e2.getMessage()); + } + + @Test + public void testAuthorizedMultiplePolicyRestrictions() + { + Optional trueFilter = Optional.of(TrueDimFilter.instance()); + Optional columnFilter = Optional.of(new NullFilter("some-column", null)); + Optional columnFilter2 = Optional.of(new NullFilter("some-column2", null)); + + final TimeseriesQuery query = Druids.newTimeseriesQueryBuilder() + .dataSource(RestrictedDataSource.create( + TableDataSource.create(DATASOURCE), + columnFilter.get() + )) + .intervals(ImmutableList.of(Intervals.ETERNITY)) + .aggregators(new CountAggregatorFactory("chocula")) + .build(); + EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); + EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); + EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); + EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())).andReturn(toolChest).anyTimes(); + EasyMock.expect(toolChest.makeMetrics(EasyMock.anyObject())).andReturn(metrics).anyTimes(); + EasyMock.expect(texasRanger.getQueryRunnerForIntervals(EasyMock.anyObject(), EasyMock.anyObject())) + .andReturn(runner).times(2); + EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).times(2); + replayAll(); + + AuthConfig authConfig = AuthConfig.newBuilder() + .setAuthorizeQueryContextParams(true) + .setEnableStrictPolicyCheck(true) + .build(); + + QueryLifecycle lifecycle = createLifecycle(authConfig); + RuntimeException e = Assert.assertThrows(RuntimeException.class, () -> + lifecycle.runSimple( + query, + authenticationResult, + AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, columnFilter2)) + )); + Assert.assertEquals( + "Incompatible restrictions on [some_datasource]: some-column IS NULL and some-column2 IS NULL", + e.getMessage() + ); + + QueryLifecycle lifecycle2 = createLifecycle(authConfig); + // trueFilter is a compatible restriction + lifecycle2.runSimple( + query, + authenticationResult, + AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, trueFilter)) + ); + + lifecycle2 = createLifecycle(authConfig); + // the same filter, compatible + lifecycle2.runSimple( + query, + authenticationResult, + AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, columnFilter)) + ); } @Test @@ -187,11 +428,23 @@ public void testAuthorizeQueryContext_authorized() EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); - EasyMock.expect(authorizer.authorize(authenticationResult, new Resource(DATASOURCE, ResourceType.DATASOURCE), Action.READ)) + EasyMock.expect(authorizer.authorize( + authenticationResult, + new Resource(DATASOURCE, ResourceType.DATASOURCE), + Action.READ + )) .andReturn(Access.OK).times(2); - EasyMock.expect(authorizer.authorize(authenticationResult, new Resource("foo", ResourceType.QUERY_CONTEXT), Action.WRITE)) + EasyMock.expect(authorizer.authorize( + authenticationResult, + new Resource("foo", ResourceType.QUERY_CONTEXT), + Action.WRITE + )) .andReturn(Access.OK).times(2); - EasyMock.expect(authorizer.authorize(authenticationResult, new Resource("baz", ResourceType.QUERY_CONTEXT), Action.WRITE)) + EasyMock.expect(authorizer.authorize( + authenticationResult, + new Resource("baz", ResourceType.QUERY_CONTEXT), + Action.WRITE + )) .andReturn(Access.OK).times(2); EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) @@ -209,8 +462,8 @@ public void testAuthorizeQueryContext_authorized() .build(); AuthConfig authConfig = AuthConfig.newBuilder() - .setAuthorizeQueryContextParams(true) - .build(); + .setAuthorizeQueryContextParams(true) + .build(); QueryLifecycle lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); @@ -235,10 +488,18 @@ public void testAuthorizeQueryContext_notAuthorized() EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); - EasyMock.expect(authorizer.authorize(authenticationResult, new Resource(DATASOURCE, ResourceType.DATASOURCE), Action.READ)) + EasyMock.expect(authorizer.authorize( + authenticationResult, + new Resource(DATASOURCE, ResourceType.DATASOURCE), + Action.READ + )) .andReturn(Access.OK) .times(2); - EasyMock.expect(authorizer.authorize(authenticationResult, new Resource("foo", ResourceType.QUERY_CONTEXT), Action.WRITE)) + EasyMock.expect(authorizer.authorize( + authenticationResult, + new Resource("foo", ResourceType.QUERY_CONTEXT), + Action.WRITE + )) .andReturn(Access.DENIED) .times(2); @@ -256,8 +517,8 @@ public void testAuthorizeQueryContext_notAuthorized() .build(); AuthConfig authConfig = AuthConfig.newBuilder() - .setAuthorizeQueryContextParams(true) - .build(); + .setAuthorizeQueryContextParams(true) + .build(); QueryLifecycle lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); Assert.assertFalse(lifecycle.authorize(mockRequest()).isAllowed()); @@ -273,7 +534,11 @@ public void testAuthorizeQueryContext_unsecuredKeys() EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); - EasyMock.expect(authorizer.authorize(authenticationResult, new Resource(DATASOURCE, ResourceType.DATASOURCE), Action.READ)) + EasyMock.expect(authorizer.authorize( + authenticationResult, + new Resource(DATASOURCE, ResourceType.DATASOURCE), + Action.READ + )) .andReturn(Access.OK) .times(2); @@ -292,9 +557,9 @@ public void testAuthorizeQueryContext_unsecuredKeys() .build(); AuthConfig authConfig = AuthConfig.newBuilder() - .setAuthorizeQueryContextParams(true) - .setUnsecuredContextKeys(ImmutableSet.of("foo", "baz")) - .build(); + .setAuthorizeQueryContextParams(true) + .setUnsecuredContextKeys(ImmutableSet.of("foo", "baz")) + .build(); QueryLifecycle lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); @@ -319,7 +584,11 @@ public void testAuthorizeQueryContext_securedKeys() EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); - EasyMock.expect(authorizer.authorize(authenticationResult, new Resource(DATASOURCE, ResourceType.DATASOURCE), Action.READ)) + EasyMock.expect(authorizer.authorize( + authenticationResult, + new Resource(DATASOURCE, ResourceType.DATASOURCE), + Action.READ + )) .andReturn(Access.OK) .times(2); @@ -338,10 +607,10 @@ public void testAuthorizeQueryContext_securedKeys() .build(); AuthConfig authConfig = AuthConfig.newBuilder() - .setAuthorizeQueryContextParams(true) - // We have secured keys, just not what the user gave. - .setSecuredContextKeys(ImmutableSet.of("foo2", "baz2")) - .build(); + .setAuthorizeQueryContextParams(true) + // We have secured keys, just not what the user gave. + .setSecuredContextKeys(ImmutableSet.of("foo2", "baz2")) + .build(); QueryLifecycle lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); @@ -366,10 +635,18 @@ public void testAuthorizeQueryContext_securedKeysNotAuthorized() EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); - EasyMock.expect(authorizer.authorize(authenticationResult, new Resource(DATASOURCE, ResourceType.DATASOURCE), Action.READ)) + EasyMock.expect(authorizer.authorize( + authenticationResult, + new Resource(DATASOURCE, ResourceType.DATASOURCE), + Action.READ + )) .andReturn(Access.OK) .times(2); - EasyMock.expect(authorizer.authorize(authenticationResult, new Resource("foo", ResourceType.QUERY_CONTEXT), Action.WRITE)) + EasyMock.expect(authorizer.authorize( + authenticationResult, + new Resource("foo", ResourceType.QUERY_CONTEXT), + Action.WRITE + )) .andReturn(Access.DENIED) .times(2); @@ -388,10 +665,10 @@ public void testAuthorizeQueryContext_securedKeysNotAuthorized() .build(); AuthConfig authConfig = AuthConfig.newBuilder() - .setAuthorizeQueryContextParams(true) - // We have secured keys. User used one of them. - .setSecuredContextKeys(ImmutableSet.of("foo", "baz2")) - .build(); + .setAuthorizeQueryContextParams(true) + // We have secured keys. User used one of them. + .setSecuredContextKeys(ImmutableSet.of("foo", "baz2")) + .build(); QueryLifecycle lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); Assert.assertFalse(lifecycle.authorize(mockRequest()).isAllowed()); @@ -407,13 +684,25 @@ public void testAuthorizeLegacyQueryContext_authorized() EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); - EasyMock.expect(authorizer.authorize(authenticationResult, new Resource("fake", ResourceType.DATASOURCE), Action.READ)) + EasyMock.expect(authorizer.authorize( + authenticationResult, + new Resource("fake", ResourceType.DATASOURCE), + Action.READ + )) .andReturn(Access.OK) .times(2); - EasyMock.expect(authorizer.authorize(authenticationResult, new Resource("foo", ResourceType.QUERY_CONTEXT), Action.WRITE)) + EasyMock.expect(authorizer.authorize( + authenticationResult, + new Resource("foo", ResourceType.QUERY_CONTEXT), + Action.WRITE + )) .andReturn(Access.OK) .times(2); - EasyMock.expect(authorizer.authorize(authenticationResult, new Resource("baz", ResourceType.QUERY_CONTEXT), Action.WRITE)) + EasyMock.expect(authorizer.authorize( + authenticationResult, + new Resource("baz", ResourceType.QUERY_CONTEXT), + Action.WRITE + )) .andReturn(Access.OK) .times(2); @@ -423,7 +712,12 @@ public void testAuthorizeLegacyQueryContext_authorized() replayAll(); - final QueryContextTest.LegacyContextQuery query = new QueryContextTest.LegacyContextQuery(ImmutableMap.of("foo", "bar", "baz", "qux")); + final QueryContextTest.LegacyContextQuery query = new QueryContextTest.LegacyContextQuery(ImmutableMap.of( + "foo", + "bar", + "baz", + "qux" + )); AuthConfig authConfig = AuthConfig.newBuilder() .setAuthorizeQueryContextParams(true) @@ -444,6 +738,26 @@ public void testAuthorizeLegacyQueryContext_authorized() Assert.assertTrue(lifecycle.authorize(mockRequest()).isAllowed()); } + public static Query queryMatchDataSource(DataSource dataSource) + { + EasyMock.reportMatcher(new IArgumentMatcher() + { + @Override + public boolean matches(Object query) + { + return query instanceof Query + && ((Query) query).getDataSource().equals(dataSource); + } + + @Override + public void appendTo(StringBuffer buffer) + { + buffer.append("dataSource(\"").append(dataSource).append("\")"); + } + }); + return null; + } + private HttpServletRequest mockRequest() { HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class); diff --git a/server/src/test/java/org/apache/druid/server/security/ForbiddenExceptionTest.java b/server/src/test/java/org/apache/druid/server/security/ForbiddenExceptionTest.java index dc3bc9144485..a818603e91ad 100644 --- a/server/src/test/java/org/apache/druid/server/security/ForbiddenExceptionTest.java +++ b/server/src/test/java/org/apache/druid/server/security/ForbiddenExceptionTest.java @@ -66,22 +66,19 @@ public void testSanitizeWithTransformFunctionReturningNewString() @Test public void testAccess() { - Access access = new Access(false); + Access access = Access.deny(null); Assert.assertFalse(access.isAllowed()); - Assert.assertEquals("", access.getMessage()); - Assert.assertEquals("Allowed:false, Message:", access.toString()); - Assert.assertEquals(Access.DEFAULT_ERROR_MESSAGE, access.toMessage()); + Assert.assertEquals("Allowed:false, Message:, Row filter: Optional.empty", access.toString()); + Assert.assertEquals(Access.DEFAULT_ERROR_MESSAGE, access.getMessage()); - access = new Access(true); + access = Access.allow(); Assert.assertTrue(access.isAllowed()); - Assert.assertEquals("", access.getMessage()); - Assert.assertEquals("Allowed:true, Message:", access.toString()); - Assert.assertEquals("Authorized", access.toMessage()); + Assert.assertEquals("Allowed:true, Message:, Row filter: Optional.empty", access.toString()); + Assert.assertEquals("Authorized", access.getMessage()); - access = new Access(false, "oops"); + access = Access.deny("oops"); Assert.assertFalse(access.isAllowed()); - Assert.assertEquals("oops", access.getMessage()); - Assert.assertEquals("Allowed:false, Message:oops", access.toString()); - Assert.assertEquals("Allowed:false, Message:oops", access.toMessage()); + Assert.assertEquals("Allowed:false, Message:oops, Row filter: Optional.empty", access.toString()); + Assert.assertEquals("Unauthorized, oops", access.getMessage()); } } diff --git a/sql/src/main/java/org/apache/druid/sql/AbstractStatement.java b/sql/src/main/java/org/apache/druid/sql/AbstractStatement.java index bfa95c5d5562..b3f04bf0fdda 100644 --- a/sql/src/main/java/org/apache/druid/sql/AbstractStatement.java +++ b/sql/src/main/java/org/apache/druid/sql/AbstractStatement.java @@ -21,8 +21,8 @@ import org.apache.druid.java.util.common.logger.Logger; import org.apache.druid.query.QueryContexts; -import org.apache.druid.server.security.Access; import org.apache.druid.server.security.Action; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.ForbiddenException; import org.apache.druid.server.security.Resource; @@ -36,6 +36,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.function.Function; @@ -69,7 +70,7 @@ public abstract class AbstractStatement implements Closeable */ protected final Map queryContext; protected PlannerContext plannerContext; - protected DruidPlanner.AuthResult authResult; + protected AuthorizationResult authResult; protected PlannerHook hook; public AbstractStatement( @@ -137,7 +138,7 @@ protected void validate(final DruidPlanner planner) */ protected void authorize( final DruidPlanner planner, - final Function, Access> authorizer + final Function, AuthorizationResult> authorizer ) { Set securedKeys = this.sqlToolbox.plannerFactory.getAuthConfig() @@ -150,16 +151,15 @@ protected void authorize( // Authentication is done by the planner using the function provided // here. The planner ensures that this step is done before planning. authResult = planner.authorize(authorizer, contextResources); - if (!authResult.authorizationResult.isAllowed()) { - throw new ForbiddenException(authResult.authorizationResult.toMessage()); + if (!authResult.isAllowed()) { + throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); } } /** - * Resource authorizer based on the authentication result - * provided earlier. + * Returns an authorizer that can provide authorization result given a set of required resource actions and authentication result. */ - protected Function, Access> authorizer() + protected Function, AuthorizationResult> authorizer() { return resourceActions -> AuthorizationUtils.authorizeAllResourceActions( @@ -175,12 +175,12 @@ protected Function, Access> authorizer() */ public Set resources() { - return authResult.sqlResourceActions; + return Objects.requireNonNull(authResult.getSqlResourceActions()); } public Set allResources() { - return authResult.allResourceActions; + return Objects.requireNonNull(authResult.getAllResourceActions()); } public SqlQueryPlus query() diff --git a/sql/src/main/java/org/apache/druid/sql/HttpStatement.java b/sql/src/main/java/org/apache/druid/sql/HttpStatement.java index 52bef0a04f07..d02f8c6b444d 100644 --- a/sql/src/main/java/org/apache/druid/sql/HttpStatement.java +++ b/sql/src/main/java/org/apache/druid/sql/HttpStatement.java @@ -19,7 +19,7 @@ package org.apache.druid.sql; -import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.ResourceAction; import org.apache.druid.sql.http.SqlQuery; @@ -51,21 +51,21 @@ public HttpStatement( super( lifecycleToolbox, SqlQueryPlus.builder(sqlQuery) - .auth(AuthorizationUtils.authenticationResultFromRequest(req)) - .build(), + .auth(AuthorizationUtils.authenticationResultFromRequest(req)) + .build(), req.getRemoteAddr() ); this.req = req; } @Override - protected Function, Access> authorizer() + protected Function, AuthorizationResult> authorizer() { return resourceActions -> - AuthorizationUtils.authorizeAllResourceActions( - req, - resourceActions, - sqlToolbox.plannerFactory.getAuthorizerMapper() - ); + AuthorizationUtils.authorizeAllResourceActions( + req, + resourceActions, + sqlToolbox.plannerFactory.getAuthorizerMapper() + ); } } diff --git a/sql/src/main/java/org/apache/druid/sql/SqlExecutionReporter.java b/sql/src/main/java/org/apache/druid/sql/SqlExecutionReporter.java index f55f2f3f1233..224c67acfa7f 100644 --- a/sql/src/main/java/org/apache/druid/sql/SqlExecutionReporter.java +++ b/sql/src/main/java/org/apache/druid/sql/SqlExecutionReporter.java @@ -103,7 +103,7 @@ public void emit() // datasources. metricBuilder.setDimension( "dataSource", - stmt.authResult.sqlResourceActions + stmt.authResult.getSqlResourceActions() .stream() .map(action -> action.getResource().getName()) .collect(Collectors.toList()) diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidPlanner.java b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidPlanner.java index 03ef94656c5f..d236a3bc2213 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidPlanner.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidPlanner.java @@ -35,7 +35,7 @@ import org.apache.druid.error.DruidException; import org.apache.druid.error.InvalidSqlInput; import org.apache.druid.query.QueryContext; -import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.Resource; import org.apache.druid.server.security.ResourceAction; import org.apache.druid.sql.calcite.parser.DruidSqlInsert; @@ -72,35 +72,6 @@ public enum State START, VALIDATED, PREPARED, PLANNED } - public static class AuthResult - { - public final Access authorizationResult; - - /** - * Resource actions used with authorizing a cancellation request. These actions - * include only the data-level actions (e.g. the datasource.) - */ - public final Set sqlResourceActions; - - /** - * Full resource actions authorized as part of this request. Used when logging - * resource actions. Includes query context keys, if query context authorization - * is enabled. - */ - public final Set allResourceActions; - - public AuthResult( - final Access authorizationResult, - final Set sqlResourceActions, - final Set allResourceActions - ) - { - this.authorizationResult = authorizationResult; - this.sqlResourceActions = sqlResourceActions; - this.allResourceActions = allResourceActions; - } - } - private final FrameworkConfig frameworkConfig; private final CalcitePlanner planner; private final PlannerContext plannerContext; @@ -183,7 +154,7 @@ private SqlStatementHandler createHandler(final SqlNode node) /** * Uses {@link SqlParameterizerShuttle} to rewrite {@link SqlNode} to swap out any * {@link org.apache.calcite.sql.SqlDynamicParam} early for their {@link org.apache.calcite.sql.SqlLiteral} - * replacement. + * replacement. * * @return a rewritten {@link SqlNode} with any dynamic parameters rewritten in the provided {@code original} node, * if they were present. @@ -226,14 +197,14 @@ public PrepareResult prepare() * Authorizes the statement. Done within the planner to enforce the authorization * step within the planner's state machine. * - * @param authorizer a function from resource actions to a {@link Access} result. + * @param authorizer a function produces {@link AuthorizationResult} based on resource actions. * @param extraActions set of additional resource actions beyond those inferred * from the query itself. Specifically, the set of context keys to * authorize. * @return the return value from the authorizer */ - public AuthResult authorize( - final Function, Access> authorizer, + public AuthorizationResult authorize( + final Function, AuthorizationResult> authorizer, final Set extraActions ) { @@ -241,14 +212,17 @@ public AuthResult authorize( Set sqlResourceActions = plannerContext.getResourceActions(); Set allResourceActions = new HashSet<>(sqlResourceActions); allResourceActions.addAll(extraActions); - Access access = authorizer.apply(allResourceActions); - plannerContext.setAuthorizationResult(access); + AuthorizationResult authorizationResult = authorizer.apply(allResourceActions); + plannerContext.setAuthorizationResult(authorizationResult); // Authorization is done as a flag, not a state, alas. // Views prepare without authorization, Avatica does authorize, then prepare, // so the only constraint is that authorization be done before planning. authorized = true; - return new AuthResult(access, sqlResourceActions, allResourceActions); + return authorizationResult.withResourceActions( + sqlResourceActions, + allResourceActions + ); } /** diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerContext.java b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerContext.java index 51be1f93502a..c55023c5fc72 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerContext.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerContext.java @@ -47,8 +47,8 @@ import org.apache.druid.query.lookup.RegisteredLookupExtractionFn; import org.apache.druid.segment.join.JoinableFactoryWrapper; import org.apache.druid.server.lookup.cache.LookupLoadingSpec; -import org.apache.druid.server.security.Access; import org.apache.druid.server.security.AuthenticationResult; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.ResourceAction; import org.apache.druid.sql.calcite.expression.SqlOperatorConversion; import org.apache.druid.sql.calcite.expression.builtin.QueryLookupOperatorConversion; @@ -146,7 +146,7 @@ public class PlannerContext // set of datasources and views which must be authorized, initialized to null so we can detect if it has been set. private Set resourceActions; // result of authorizing set of resources against authentication identity - private Access authorizationResult; + private AuthorizationResult authorizationResult; // error messages encountered while planning the query @Nullable private String planningError; @@ -411,7 +411,7 @@ public boolean isStringifyArrays() * Whether we should use {@link org.apache.druid.query.filter.BoundDimFilter} and * {@link org.apache.druid.query.filter.SelectorDimFilter} (true) or {@link org.apache.druid.query.filter.RangeFilter}, * {@link org.apache.druid.query.filter.EqualityFilter}, and {@link org.apache.druid.query.filter.NullFilter} (false). - * + *

    * Typically true when {@link NullHandling#replaceWithDefault()} and false when {@link NullHandling#sqlCompatible()}. * Can be overriden by the context parameter {@link #CTX_SQL_USE_BOUNDS_AND_SELECTORS}. */ @@ -575,7 +575,7 @@ public Object get(final String name) } - public Access getAuthorizationResult() + public AuthorizationResult getAuthorizationResult() { return Preconditions.checkNotNull(authorizationResult, "Authorization result not available"); } @@ -595,7 +595,7 @@ public void setAuthenticationResult(AuthenticationResult authenticationResult) this.authenticationResult = Preconditions.checkNotNull(authenticationResult, "authenticationResult"); } - public void setAuthorizationResult(Access access) + public void setAuthorizationResult(AuthorizationResult access) { if (this.authorizationResult != null) { // It's a bug if this happens, because setAuthorizationResult should be called exactly once. @@ -637,7 +637,7 @@ public SqlEngine getEngine() /** * Checks if the current {@link SqlEngine} supports a particular feature. - * + *

    * When executing a specific query, use this method instead of {@link SqlEngine#featureAvailable(EngineFeature)} * because it also verifies feature flags. */ diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java index c21f6408b52f..eeb0dee10640 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java @@ -41,8 +41,8 @@ import org.apache.druid.math.expr.ExprMacroTable; import org.apache.druid.query.QueryContexts; import org.apache.druid.segment.join.JoinableFactoryWrapper; -import org.apache.druid.server.security.Access; import org.apache.druid.server.security.AuthConfig; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.NoopEscalator; import org.apache.druid.sql.calcite.parser.DruidSqlParserImplFactory; @@ -127,13 +127,17 @@ public DruidPlanner createPlanner( * and ready to go authorization result. */ @VisibleForTesting - public DruidPlanner createPlannerForTesting(final SqlEngine engine, final String sql, final Map queryContext) + public DruidPlanner createPlannerForTesting( + final SqlEngine engine, + final String sql, + final Map queryContext + ) { final DruidPlanner thePlanner = createPlanner(engine, sql, queryContext, null); thePlanner.getPlannerContext() .setAuthenticationResult(NoopEscalator.getInstance().createEscalatedAuthenticationResult()); thePlanner.validate(); - thePlanner.authorize(ra -> Access.OK, ImmutableSet.of()); + thePlanner.authorize(ra -> AuthorizationResult.ALLOW_ALL, ImmutableSet.of()); return thePlanner; } diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/run/NativeQueryMaker.java b/sql/src/main/java/org/apache/druid/sql/calcite/run/NativeQueryMaker.java index 7b1e1ec7091d..836404319c95 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/run/NativeQueryMaker.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/run/NativeQueryMaker.java @@ -45,8 +45,8 @@ import org.apache.druid.server.QueryLifecycle; import org.apache.druid.server.QueryLifecycleFactory; import org.apache.druid.server.QueryResponse; -import org.apache.druid.server.security.Access; import org.apache.druid.server.security.AuthenticationResult; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.sql.calcite.planner.PlannerConfig; import org.apache.druid.sql.calcite.planner.PlannerContext; import org.apache.druid.sql.calcite.rel.CannotBuildQueryException; @@ -185,14 +185,18 @@ private QueryResponse execute( query = query.withSqlQueryId(plannerContext.getSqlQueryId()); final AuthenticationResult authenticationResult = plannerContext.getAuthenticationResult(); - final Access authorizationResult = plannerContext.getAuthorizationResult(); + final AuthorizationResult authorizationResult = plannerContext.getAuthorizationResult(); final QueryLifecycle queryLifecycle = queryLifecycleFactory.factorize(); // After calling "runSimple" the query will start running. We need to do this before reading the toolChest, since // otherwise it won't yet be initialized. (A bummer, since ideally, we'd verify the toolChest exists and can do // array-based results before starting the query; but in practice we don't expect this to happen since we keep // tight control over which query types we generate in the SQL layer. They all support array-based results.) - final QueryResponse results = queryLifecycle.runSimple((Query) query, authenticationResult, authorizationResult); + final QueryResponse results = queryLifecycle.runSimple( + (Query) query, + authenticationResult, + authorizationResult + ); return mapResultSequence( results, 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 0af8c19ca064..fe2e54fef83f 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 @@ -74,9 +74,9 @@ import org.apache.druid.segment.column.ValueType; import org.apache.druid.segment.metadata.AvailableSegmentMetadata; 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.AuthenticationResult; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.ForbiddenException; @@ -245,11 +245,22 @@ public SystemSchema( { Preconditions.checkNotNull(serverView, "serverView"); this.tableMap = ImmutableMap.of( - SEGMENTS_TABLE, new SegmentsTable(druidSchema, metadataView, jsonMapper, authorizerMapper), - SERVERS_TABLE, new ServersTable(druidNodeDiscoveryProvider, serverInventoryView, authorizerMapper, overlordClient, coordinatorDruidLeaderClient), - SERVER_SEGMENTS_TABLE, new ServerSegmentsTable(serverView, authorizerMapper), - TASKS_TABLE, new TasksTable(overlordClient, authorizerMapper), - SUPERVISOR_TABLE, new SupervisorsTable(overlordClient, authorizerMapper) + SEGMENTS_TABLE, + new SegmentsTable(druidSchema, metadataView, jsonMapper, authorizerMapper), + SERVERS_TABLE, + new ServersTable( + druidNodeDiscoveryProvider, + serverInventoryView, + authorizerMapper, + overlordClient, + coordinatorDruidLeaderClient + ), + SERVER_SEGMENTS_TABLE, + new ServerSegmentsTable(serverView, authorizerMapper), + TASKS_TABLE, + new TasksTable(overlordClient, authorizerMapper), + SUPERVISOR_TABLE, + new SupervisorsTable(overlordClient, authorizerMapper) ); } @@ -1136,19 +1147,20 @@ private static void checkStateReadAccessForServers( AuthorizerMapper authorizerMapper ) { - final Access stateAccess = AuthorizationUtils.authorizeAllResourceActions( + final AuthorizationResult stateAccess = AuthorizationUtils.authorizeAllResourceActions( authenticationResult, Collections.singletonList(new ResourceAction(Resource.STATE_RESOURCE, Action.READ)), authorizerMapper ); if (!stateAccess.isAllowed()) { - throw new ForbiddenException("Insufficient permission to view servers: " + stateAccess.toMessage()); + throw new ForbiddenException("Insufficient permission to view servers: " + + Objects.requireNonNull(stateAccess.getFailureMessage())); } } /** * Project a row using "projects" from {@link SegmentsTable#scan(DataContext, List, int[])}. - * + *

    * Also, fix up types so {@link ColumnType#STRING} are transformed to Strings if they aren't yet. This defers * computation of {@link ObjectMapper#writeValueAsString(Object)} or {@link Object#toString()} until we know we * actually need it. diff --git a/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java b/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java index d957e7155b5e..9be9c3077129 100644 --- a/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java +++ b/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java @@ -33,7 +33,7 @@ import org.apache.druid.server.QueryResultPusher; import org.apache.druid.server.ResponseContextConfig; import org.apache.druid.server.initialization.ServerConfig; -import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.AuthorizationUtils; import org.apache.druid.server.security.AuthorizerMapper; import org.apache.druid.server.security.ResourceAction; @@ -140,7 +140,7 @@ public Response cancelQuery( return Response.status(Status.NOT_FOUND).build(); } - final Access access = authorizeCancellation(req, lifecycles); + final AuthorizationResult access = authorizeCancellation(req, lifecycles); if (access.isAllowed()) { // should remove only the lifecycles in the snapshot. @@ -332,11 +332,11 @@ public void writeException(Exception ex, OutputStream out) throws IOException /** * Authorize a query cancellation operation. - * + *

    * Considers only datasource and table resources; not context key resources when checking permissions. This means * that a user's permission to cancel a query depends on the datasource, not the context variables used in the query. */ - public Access authorizeCancellation(final HttpServletRequest req, final List cancelables) + public AuthorizationResult authorizeCancellation(final HttpServletRequest req, final List cancelables) { Set resources = cancelables .stream() diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteJoinQueryTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteJoinQueryTest.java index 02a1a3fe2816..6950393831f8 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteJoinQueryTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteJoinQueryTest.java @@ -83,7 +83,7 @@ import org.apache.druid.segment.join.JoinType; import org.apache.druid.segment.virtual.ListFilteredVirtualColumn; import org.apache.druid.server.QueryLifecycle; -import org.apache.druid.server.security.Access; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.sql.calcite.DecoupledTestConfig.IgnoreQueriesReason; import org.apache.druid.sql.calcite.DecoupledTestConfig.QuidemTestCaseReason; import org.apache.druid.sql.calcite.NotYetSupported.Modes; @@ -5187,7 +5187,7 @@ public void testGroupByJoinAsNativeQueryWithUnoptimizedFilter(Map results = seq.toList(); Assert.assertEquals( ImmutableList.of(ResultRow.of("def")), diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/schema/BrokerSegmentMetadataCacheTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/schema/BrokerSegmentMetadataCacheTest.java index b613c602f633..9bcc55c799f2 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/schema/BrokerSegmentMetadataCacheTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/schema/BrokerSegmentMetadataCacheTest.java @@ -69,8 +69,8 @@ import org.apache.druid.server.coordination.DruidServerMetadata; import org.apache.druid.server.coordination.ServerType; import org.apache.druid.server.metrics.NoopServiceEmitter; -import org.apache.druid.server.security.Access; import org.apache.druid.server.security.AllowAllAuthenticator; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.NoopEscalator; import org.apache.druid.sql.calcite.table.DatasourceTable; import org.apache.druid.sql.calcite.table.DruidTable; @@ -141,7 +141,10 @@ public BrokerSegmentMetadataCache buildSchemaMarkAndTableLatch() throws Interrup return buildSchemaMarkAndTableLatch(SEGMENT_CACHE_CONFIG_DEFAULT, new NoopCoordinatorClient()); } - public BrokerSegmentMetadataCache buildSchemaMarkAndTableLatch(BrokerSegmentMetadataCacheConfig config, CoordinatorClient coordinatorClient) throws InterruptedException + public BrokerSegmentMetadataCache buildSchemaMarkAndTableLatch( + BrokerSegmentMetadataCacheConfig config, + CoordinatorClient coordinatorClient + ) throws InterruptedException { Preconditions.checkState(runningSchema == null); runningSchema = new BrokerSegmentMetadataCache( @@ -203,7 +206,8 @@ public void markDataSourceAsNeedRebuild(String datasource) @VisibleForTesting public void refresh( final Set segmentsToRefresh, - final Set dataSourcesToRebuild) throws IOException + final Set dataSourcesToRebuild + ) throws IOException { super.refresh(segmentsToRefresh, dataSourcesToRebuild); refreshLatch.countDown(); @@ -226,14 +230,18 @@ public void testCoordinatorReturnsAllDSSchema() throws InterruptedException final RowSignature someDataSourceRowSignature = new QueryableIndexCursorFactory(indexAuto1).getRowSignature(); final RowSignature foo3RowSignature = new QueryableIndexCursorFactory(indexAuto2).getRowSignature(); - NoopCoordinatorClient coordinatorClient = new NoopCoordinatorClient() { + NoopCoordinatorClient coordinatorClient = new NoopCoordinatorClient() + { @Override public ListenableFuture> fetchDataSourceInformation(Set datasources) { Map dataSourceInformationMap = new HashMap<>(); dataSourceInformationMap.put(DATASOURCE1, new DataSourceInformation(DATASOURCE1, dataSource1RowSignature)); dataSourceInformationMap.put(DATASOURCE2, new DataSourceInformation(DATASOURCE2, dataSource2RowSignature)); - dataSourceInformationMap.put(SOME_DATASOURCE, new DataSourceInformation(SOME_DATASOURCE, someDataSourceRowSignature)); + dataSourceInformationMap.put( + SOME_DATASOURCE, + new DataSourceInformation(SOME_DATASOURCE, someDataSourceRowSignature) + ); dataSourceInformationMap.put("foo3", new DataSourceInformation("foo3", foo3RowSignature)); return Futures.immediateFuture(new ArrayList<>(dataSourceInformationMap.values())); @@ -258,7 +266,12 @@ public ListenableFuture> fetchDataSourceInformation( schema.start(); schema.awaitInitialization(); final Set tableNames = schema.getDatasourceNames(); - Assert.assertEquals(ImmutableSet.of(CalciteTests.DATASOURCE1, CalciteTests.DATASOURCE2, CalciteTests.SOME_DATASOURCE, "foo3"), tableNames); + Assert.assertEquals(ImmutableSet.of( + CalciteTests.DATASOURCE1, + CalciteTests.DATASOURCE2, + CalciteTests.SOME_DATASOURCE, + "foo3" + ), tableNames); Assert.assertEquals(dataSource1RowSignature, schema.getDatasource(DATASOURCE1).getRowSignature()); Assert.assertEquals(dataSource2RowSignature, schema.getDatasource(DATASOURCE2).getRowSignature()); @@ -277,14 +290,18 @@ public void testCoordinatorReturnsFewDSSchema() throws InterruptedException final RowSignature dataSource2RowSignature = new QueryableIndexCursorFactory(index2).getRowSignature(); final RowSignature someDataSourceRowSignature = new QueryableIndexCursorFactory(indexAuto1).getRowSignature(); - NoopCoordinatorClient coordinatorClient = new NoopCoordinatorClient() { + NoopCoordinatorClient coordinatorClient = new NoopCoordinatorClient() + { @Override public ListenableFuture> fetchDataSourceInformation(Set datasources) { Map dataSourceInformationMap = new HashMap<>(); dataSourceInformationMap.put(DATASOURCE1, new DataSourceInformation(DATASOURCE1, dataSource1RowSignature)); dataSourceInformationMap.put(DATASOURCE2, new DataSourceInformation(DATASOURCE2, dataSource2RowSignature)); - dataSourceInformationMap.put(SOME_DATASOURCE, new DataSourceInformation(SOME_DATASOURCE, someDataSourceRowSignature)); + dataSourceInformationMap.put( + SOME_DATASOURCE, + new DataSourceInformation(SOME_DATASOURCE, someDataSourceRowSignature) + ); return Futures.immediateFuture(new ArrayList<>(dataSourceInformationMap.values())); } }; @@ -304,7 +321,11 @@ public ListenableFuture> fetchDataSourceInformation( QueryLifecycleFactory factoryMock = EasyMock.createMock(QueryLifecycleFactory.class); QueryLifecycle lifecycleMock = EasyMock.createMock(QueryLifecycle.class); EasyMock.expect(factoryMock.factorize()).andReturn(lifecycleMock).once(); - EasyMock.expect(lifecycleMock.runSimple(expectedMetadataQuery, AllowAllAuthenticator.ALLOW_ALL_RESULT, Access.OK)) + EasyMock.expect(lifecycleMock.runSimple( + expectedMetadataQuery, + AllowAllAuthenticator.ALLOW_ALL_RESULT, + AuthorizationResult.ALLOW_ALL + )) .andReturn(QueryResponse.withEmptyContext(Sequences.empty())); BrokerSegmentMetadataCache schema = new BrokerSegmentMetadataCache( @@ -335,7 +356,8 @@ public void testBrokerPollsAllDSSchema() throws InterruptedException { ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(Set.class); CoordinatorClient coordinatorClient = Mockito.mock(CoordinatorClient.class); - Mockito.when(coordinatorClient.fetchDataSourceInformation(argumentCaptor.capture())).thenReturn(Futures.immediateFuture(null)); + Mockito.when(coordinatorClient.fetchDataSourceInformation(argumentCaptor.capture())) + .thenReturn(Futures.immediateFuture(null)); Set datsources = Sets.newHashSet(DATASOURCE1, DATASOURCE2, DATASOURCE3, SOME_DATASOURCE, "xyz", "coldDS"); Mockito.when(coordinatorClient.fetchDataSourcesWithUsedSegments()).thenReturn(Futures.immediateFuture(datsources)); @@ -386,7 +408,8 @@ public void testRefreshOnEachCycleCentralizedDatasourceSchemaEnabled() throws In new PhysicalDatasourceMetadataFactory(globalTableJoinable, segmentManager), new NoopCoordinatorClient(), config - ) { + ) + { @Override public void refresh(Set segmentsToRefresh, Set dataSourcesToRebuild) throws IOException @@ -425,7 +448,8 @@ public void testRefreshOnEachCycleCentralizedDatasourceSchemaDisabled() throws I new PhysicalDatasourceMetadataFactory(globalTableJoinable, segmentManager), new NoopCoordinatorClient(), CentralizedDatasourceSchemaConfig.create() - ) { + ) + { @Override public void refresh(Set segmentsToRefresh, Set dataSourcesToRebuild) throws IOException @@ -449,7 +473,11 @@ public void refresh(Set segmentsToRefresh, Set dataSourcesToR public void testGetTableMap() throws InterruptedException { BrokerSegmentMetadataCache schema = buildSchemaMarkAndTableLatch(); - Assert.assertEquals(ImmutableSet.of(CalciteTests.DATASOURCE1, CalciteTests.DATASOURCE2, CalciteTests.SOME_DATASOURCE), schema.getDatasourceNames()); + Assert.assertEquals(ImmutableSet.of( + CalciteTests.DATASOURCE1, + CalciteTests.DATASOURCE2, + CalciteTests.SOME_DATASOURCE + ), schema.getDatasourceNames()); } @Test @@ -509,7 +537,8 @@ public void testGetTableMapSomeTable() throws InterruptedException // using 'newest first' column type merge strategy, the types are expected to be the types defined in the newer // segment, except for json, which is special handled BrokerSegmentMetadataCache schema = buildSchemaMarkAndTableLatch( - new BrokerSegmentMetadataCacheConfig() { + new BrokerSegmentMetadataCacheConfig() + { @Override public AbstractSegmentMetadataCache.ColumnTypeMergePolicy getMetadataColumnTypeMergePolicy() { @@ -603,6 +632,7 @@ public void testGetTableMapSomeTableLeastRestrictiveTypeMerge() throws Interrupt * This tests that {@link AvailableSegmentMetadata#getNumRows()} is correct in case * of multiple replicas i.e. when {@link AbstractSegmentMetadataCache#addSegment(DruidServerMetadata, DataSegment)} * is called more than once for same segment + * * @throws InterruptedException */ @Test @@ -720,7 +750,8 @@ public void markDataSourceAsNeedRebuild(String datasource) @VisibleForTesting public void refresh( final Set segmentsToRefresh, - final Set dataSourcesToRebuild) throws IOException + final Set dataSourcesToRebuild + ) throws IOException { super.refresh(segmentsToRefresh, dataSourcesToRebuild); } @@ -731,9 +762,9 @@ public void refresh( final Map segmentMetadatas = schema.getSegmentMetadataSnapshot(); List segments = segmentMetadatas.values() - .stream() - .map(AvailableSegmentMetadata::getSegment) - .collect(Collectors.toList()); + .stream() + .map(AvailableSegmentMetadata::getSegment) + .collect(Collectors.toList()); Assert.assertEquals(6, segments.size()); // verify that dim3 column isn't present in the schema for foo @@ -769,20 +800,20 @@ public void refresh( ); QueryableIndex index = IndexBuilder.create() - .tmpDir(new File(tmpDir, "1")) - .segmentWriteOutMediumFactory(OffHeapMemorySegmentWriteOutMediumFactory.instance()) - .schema( - new IncrementalIndexSchema.Builder() - .withMetrics( - new CountAggregatorFactory("cnt"), - new DoubleSumAggregatorFactory("m1", "m1"), - new HyperUniquesAggregatorFactory("unique_dim1", "dim1") - ) - .withRollup(false) - .build() - ) - .rows(rows) - .buildMMappedIndex(); + .tmpDir(new File(tmpDir, "1")) + .segmentWriteOutMediumFactory(OffHeapMemorySegmentWriteOutMediumFactory.instance()) + .schema( + new IncrementalIndexSchema.Builder() + .withMetrics( + new CountAggregatorFactory("cnt"), + new DoubleSumAggregatorFactory("m1", "m1"), + new HyperUniquesAggregatorFactory("unique_dim1", "dim1") + ) + .withRollup(false) + .build() + ) + .rows(rows) + .buildMMappedIndex(); walker.add(newSegment, index); serverView.addSegment(newSegment, ServerType.HISTORICAL); @@ -839,11 +870,11 @@ public void testNullAvailableSegmentMetadata() throws IOException, InterruptedEx /** * Test actions on the cache. The current design of the cache makes testing far harder * than it should be. - * + *

    * - The cache is refreshed on a schedule. * - Datasources are added to the refresh queue via an unsynchronized thread. * - The refresh loop always refreshes since one of the segments is dynamic. - * + *

    * The use of latches tries to keep things synchronized, but there are many * moving parts. A simpler technique is sorely needed. */ @@ -1038,7 +1069,11 @@ public void testRunSegmentMetadataQueryWithContext() throws Exception EasyMock.expect(factoryMock.factorize()).andReturn(lifecycleMock).once(); // This is the mat of the test, making sure that the query created by the method under test matches the expected query, specifically the operator configured context - EasyMock.expect(lifecycleMock.runSimple(expectedMetadataQuery, AllowAllAuthenticator.ALLOW_ALL_RESULT, Access.OK)) + EasyMock.expect(lifecycleMock.runSimple( + expectedMetadataQuery, + AllowAllAuthenticator.ALLOW_ALL_RESULT, + AuthorizationResult.ALLOW_ALL + )) .andReturn(QueryResponse.withEmptyContext(Sequences.empty())); EasyMock.replay(factoryMock, lifecycleMock); @@ -1130,9 +1165,9 @@ public void testNoDatasourceSchemaWhenNoSegmentMetadata() throws InterruptedExce schema.awaitInitialization(); List segments = schema.getSegmentMetadataSnapshot().values() - .stream() - .map(AvailableSegmentMetadata::getSegment) - .collect(Collectors.toList()); + .stream() + .map(AvailableSegmentMetadata::getSegment) + .collect(Collectors.toList()); schema.refresh(segments.stream().map(DataSegment::getId).collect(Collectors.toSet()), Collections.singleton("foo")); @@ -1186,9 +1221,9 @@ public void testTombstoneSegmentIsNotRefreshed() throws IOException .build(); final ImmutableDruidServer historicalServer = druidServers.stream() - .filter(s -> s.getType().equals(ServerType.HISTORICAL)) - .findAny() - .orElse(null); + .filter(s -> s.getType().equals(ServerType.HISTORICAL)) + .findAny() + .orElse(null); Assert.assertNotNull(historicalServer); final DruidServerMetadata historicalServerMetadata = historicalServer.getMetadata(); @@ -1217,7 +1252,11 @@ public void testTombstoneSegmentIsNotRefreshed() throws IOException ); EasyMock.expect(factoryMock.factorize()).andReturn(lifecycleMock).once(); - EasyMock.expect(lifecycleMock.runSimple(expectedMetadataQuery, AllowAllAuthenticator.ALLOW_ALL_RESULT, Access.OK)) + EasyMock.expect(lifecycleMock.runSimple( + expectedMetadataQuery, + AllowAllAuthenticator.ALLOW_ALL_RESULT, + AuthorizationResult.ALLOW_ALL + )) .andReturn(QueryResponse.withEmptyContext(Sequences.empty())); EasyMock.replay(factoryMock, lifecycleMock); 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 05c267a00bbe..0202e8a52b15 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 @@ -146,13 +146,13 @@ public Authorizer getAuthorizer(String name) switch (resource.getType()) { case ResourceType.DATASOURCE: if (FORBIDDEN_DATASOURCE.equals(resource.getName())) { - return new Access(false); + return Access.DENIED; } else { return Access.OK; } case ResourceType.VIEW: if ("forbiddenView".equals(resource.getName())) { - return new Access(false); + return Access.DENIED; } else { return Access.OK; } @@ -161,14 +161,14 @@ public Authorizer getAuthorizer(String name) case ResourceType.EXTERNAL: if (Action.WRITE.equals(action)) { if (FORBIDDEN_DESTINATION.equals(resource.getName())) { - return new Access(false); + return Access.DENIED; } else { return Access.OK; } } - return new Access(false); + return Access.DENIED; default: - return new Access(false); + return Access.DENIED; } }; } diff --git a/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java b/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java index df1d94e6f206..194d970ed280 100644 --- a/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java +++ b/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java @@ -77,9 +77,9 @@ import org.apache.druid.server.mocks.MockHttpServletResponse; import org.apache.druid.server.scheduling.HiLoQueryLaningStrategy; import org.apache.druid.server.scheduling.ManualQueryPrioritizationStrategy; -import org.apache.druid.server.security.Access; import org.apache.druid.server.security.AuthConfig; import org.apache.druid.server.security.AuthenticationResult; +import org.apache.druid.server.security.AuthorizationResult; import org.apache.druid.server.security.ForbiddenException; import org.apache.druid.server.security.ResourceAction; import org.apache.druid.sql.DirectStatement; @@ -2184,7 +2184,7 @@ private TestHttpStatement( @Override protected void authorize( DruidPlanner planner, - Function, Access> authorizer + Function, AuthorizationResult> authorizer ) { if (validateAndAuthorizeLatchSupplier.get() != null) { From dd251c2c57409361d362df95ea685d2b2ed44114 Mon Sep 17 00:00:00 2001 From: cecemei Date: Tue, 17 Dec 2024 15:40:07 -0800 Subject: [PATCH 02/32] Add getPermissionErrorMessage method to AuthorizationResult, and use it as the default interface for checking permission --- .../apache/druid/grpc/server/QueryDriver.java | 2 +- .../basic/BasicSecurityResourceFilter.java | 6 ++-- .../druid/catalog/http/CatalogResource.java | 7 ++--- .../dart/controller/http/DartSqlResource.java | 7 +++-- .../dart/controller/sql/DartQueryMaker.java | 4 +++ .../druid/msq/rpc/MSQResourceUtils.java | 17 +++++------ .../druid/msq/sql/MSQTaskQueryMaker.java | 4 +++ .../sql/resources/SqlStatementResource.java | 18 +++++------ .../indexing/common/task/IndexTaskUtils.java | 12 ++++---- .../overlord/http/OverlordResource.java | 24 +++++++-------- .../security/SupervisorResourceFilter.java | 7 ++--- .../http/security/TaskResourceFilter.java | 7 ++--- .../overlord/sampler/SamplerResource.java | 7 ++--- .../supervisor/SupervisorResource.java | 6 ++-- .../druid/segment/realtime/ChatHandlers.java | 11 ++++--- .../apache/druid/server/QueryLifecycle.java | 13 ++++---- .../apache/druid/server/QueryResource.java | 13 ++++---- .../http/security/ConfigResourceFilter.java | 8 ++--- .../security/DatasourceResourceFilter.java | 7 ++--- .../http/security/RulesResourceFilter.java | 7 ++--- .../http/security/StateResourceFilter.java | 7 ++--- .../server/security/AuthorizationResult.java | 27 ++++++++++------- .../server/security/AuthorizationUtils.java | 9 ++++-- .../druid/server/QueryLifecycleTest.java | 30 +++++++++---------- .../apache/druid/sql/AbstractStatement.java | 6 ++-- .../sql/calcite/schema/SystemSchema.java | 10 +++---- .../apache/druid/sql/http/SqlResource.java | 4 +-- 27 files changed, 140 insertions(+), 140 deletions(-) diff --git a/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java b/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java index 442563d305be..10559026af54 100644 --- a/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java +++ b/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java @@ -148,7 +148,7 @@ private QueryResponse runNativeQuery(QueryRequest request, AuthenticationResult try { queryLifecycle.initialize(query); AuthorizationResult authorizationResult = queryLifecycle.authorize(authResult); - if (!authorizationResult.isAllowed()) { + if (!authorizationResult.getPermissionErrorMessage(true).isPresent()) { throw new ForbiddenException(Access.DEFAULT_ERROR_MESSAGE); } queryResponse = queryLifecycle.execute(); 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 1c7d374da308..98046bc931e1 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 @@ -60,14 +60,14 @@ public ContainerRequest filter(ContainerRequest request) getAuthorizerMapper() ); - if (!authResult.isAllowed()) { + authResult.getPermissionErrorMessage(true).ifPresent(error -> { throw new WebApplicationException( Response.status(Response.Status.FORBIDDEN) .type(MediaType.TEXT_PLAIN) - .entity(StringUtils.format("Access-Check-Result: %s", authResult.getFailureMessage())) + .entity(StringUtils.format("Access-Check-Result: %s", error)) .build() ); - } + }); return request; } diff --git a/extensions-core/druid-catalog/src/main/java/org/apache/druid/catalog/http/CatalogResource.java b/extensions-core/druid-catalog/src/main/java/org/apache/druid/catalog/http/CatalogResource.java index 0eb9ebf58867..0435ef9e2f8d 100644 --- a/extensions-core/druid-catalog/src/main/java/org/apache/druid/catalog/http/CatalogResource.java +++ b/extensions-core/druid-catalog/src/main/java/org/apache/druid/catalog/http/CatalogResource.java @@ -59,7 +59,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.stream.Collectors; /** @@ -582,9 +581,9 @@ private void authorizeTable( private void authorize(String resource, String key, Action action, HttpServletRequest request) { final AuthorizationResult authResult = authorizeAccess(resource, key, action, request); - if (!authResult.isAllowed()) { - throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); - } + authResult.getPermissionErrorMessage(true).ifPresent(error -> { + throw new ForbiddenException(error); + }); } private AuthorizationResult authorizeAccess(String resource, String key, Action action, HttpServletRequest request) diff --git a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/http/DartSqlResource.java b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/http/DartSqlResource.java index 97a1f32f66a9..3630e2c1d056 100644 --- a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/http/DartSqlResource.java +++ b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/http/DartSqlResource.java @@ -175,7 +175,8 @@ public GetQueriesResponse doGetRunningQueries( queries.sort(Comparator.comparing(DartQueryInfo::getStartTime).thenComparing(DartQueryInfo::getDartQueryId)); final GetQueriesResponse response; - if (stateReadAccess.isAllowed()) { + boolean hasFullPermission = !stateReadAccess.getPermissionErrorMessage(true).isPresent(); + if (hasFullPermission) { // User can READ STATE, so they can see all running queries, as well as authentication details. response = new GetQueriesResponse(queries); } else { @@ -245,9 +246,9 @@ public Response cancelQuery( return Response.status(Response.Status.ACCEPTED).build(); } - final AuthorizationResult access = authorizeCancellation(req, cancelables); + final AuthorizationResult authResult = authorizeCancellation(req, cancelables); - if (access.isAllowed()) { + if (!authResult.getPermissionErrorMessage(true).isPresent()) { sqlLifecycleManager.removeAll(sqlQueryId, cancelables); // Don't call cancel() on the cancelables. That just cancels native queries, which is useless here. Instead, diff --git a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/sql/DartQueryMaker.java b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/sql/DartQueryMaker.java index 66686f7640f9..ba0c503f1d76 100644 --- a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/sql/DartQueryMaker.java +++ b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/sql/DartQueryMaker.java @@ -52,6 +52,7 @@ import org.apache.druid.msq.sql.MSQTaskQueryMaker; import org.apache.druid.segment.column.ColumnType; import org.apache.druid.server.QueryResponse; +import org.apache.druid.server.security.ForbiddenException; import org.apache.druid.sql.calcite.planner.PlannerContext; import org.apache.druid.sql.calcite.rel.DruidQuery; import org.apache.druid.sql.calcite.run.QueryMaker; @@ -127,6 +128,9 @@ public DartQueryMaker( @Override public QueryResponse runQuery(DruidQuery druidQuery) { + plannerContext.getAuthorizationResult().getPermissionErrorMessage(true).ifPresent(error -> { + throw new ForbiddenException(error); + }); final MSQSpec querySpec = MSQTaskQueryMaker.makeQuerySpec( null, druidQuery, diff --git a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/rpc/MSQResourceUtils.java b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/rpc/MSQResourceUtils.java index 7d9779b723f5..795f0fbdaa85 100644 --- a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/rpc/MSQResourceUtils.java +++ b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/rpc/MSQResourceUtils.java @@ -27,7 +27,6 @@ import javax.servlet.http.HttpServletRequest; import java.util.List; -import java.util.Objects; /** * Utility methods for MSQ resources such as {@link ControllerResource}. @@ -42,15 +41,15 @@ public static void authorizeAdminRequest( { final List resourceActions = permissionMapper.getAdminPermissions(); - AuthorizationResult access = AuthorizationUtils.authorizeAllResourceActions( + AuthorizationResult authResult = AuthorizationUtils.authorizeAllResourceActions( request, resourceActions, authorizerMapper ); - if (!access.isAllowed()) { - throw new ForbiddenException(Objects.requireNonNull(access.getFailureMessage())); - } + authResult.getPermissionErrorMessage(true).ifPresent(error -> { + throw new ForbiddenException(error); + }); } public static void authorizeQueryRequest( @@ -62,14 +61,14 @@ public static void authorizeQueryRequest( { final List resourceActions = permissionMapper.getQueryPermissions(queryId); - AuthorizationResult access = AuthorizationUtils.authorizeAllResourceActions( + AuthorizationResult authResult = AuthorizationUtils.authorizeAllResourceActions( request, resourceActions, authorizerMapper ); - if (!access.isAllowed()) { - throw new ForbiddenException(Objects.requireNonNull(access.getFailureMessage())); - } + authResult.getPermissionErrorMessage(true).ifPresent(error -> { + throw new ForbiddenException(error); + }); } } diff --git a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/MSQTaskQueryMaker.java b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/MSQTaskQueryMaker.java index 5462b9917376..e6ecd5a5785f 100644 --- a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/MSQTaskQueryMaker.java +++ b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/MSQTaskQueryMaker.java @@ -54,6 +54,7 @@ import org.apache.druid.segment.column.ColumnType; import org.apache.druid.server.QueryResponse; import org.apache.druid.server.lookup.cache.LookupLoadingSpec; +import org.apache.druid.server.security.ForbiddenException; import org.apache.druid.sql.calcite.parser.DruidSqlIngest; import org.apache.druid.sql.calcite.parser.DruidSqlInsert; import org.apache.druid.sql.calcite.parser.DruidSqlReplace; @@ -116,6 +117,9 @@ public class MSQTaskQueryMaker implements QueryMaker @Override public QueryResponse runQuery(final DruidQuery druidQuery) { + plannerContext.getAuthorizationResult().getPermissionErrorMessage(true).ifPresent(error -> { + throw new ForbiddenException(error); + }); Hook.QUERY_PLAN.run(druidQuery.getQuery()); plannerContext.dispatchHook(DruidHook.NATIVE_PLAN, druidQuery.getQuery()); diff --git a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlStatementResource.java b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlStatementResource.java index 33527e6eb903..0bf82ea97cb9 100644 --- a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlStatementResource.java +++ b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlStatementResource.java @@ -675,21 +675,21 @@ private MSQControllerTask getMSQControllerTaskAndCheckPermission( return msqControllerTask; } - AuthorizationResult access = AuthorizationUtils.authorizeAllResourceActions( + AuthorizationResult authResult = AuthorizationUtils.authorizeAllResourceActions( authenticationResult, Collections.singletonList(new ResourceAction(Resource.STATE_RESOURCE, forAction)), authorizerMapper ); - if (access.isAllowed()) { - return msqControllerTask; - } + authResult.getPermissionErrorMessage(true).ifPresent(error -> { + throw new ForbiddenException(StringUtils.format( + "The current user[%s] cannot view query id[%s] since the query is owned by another user", + currentUser, + queryId + )); + }); - throw new ForbiddenException(StringUtils.format( - "The current user[%s] cannot view query id[%s] since the query is owned by another user", - currentUser, - queryId - )); + return msqControllerTask; } /** diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/common/task/IndexTaskUtils.java b/indexing-service/src/main/java/org/apache/druid/indexing/common/task/IndexTaskUtils.java index 70b1b50e2daf..b1bd4af65b33 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/common/task/IndexTaskUtils.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/common/task/IndexTaskUtils.java @@ -43,7 +43,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Objects; public class IndexTaskUtils { @@ -80,12 +79,11 @@ public static AuthorizationResult datasourceAuthorizationCheck( action ); - AuthorizationResult access = AuthorizationUtils.authorizeResourceAction(req, resourceAction, authorizerMapper); - if (!access.isAllowed()) { - throw new ForbiddenException(Objects.requireNonNull(access.getFailureMessage())); - } - - return access; + AuthorizationResult authResult = AuthorizationUtils.authorizeResourceAction(req, resourceAction, authorizerMapper); + authResult.getPermissionErrorMessage(true).ifPresent(error -> { + throw new ForbiddenException(error); + }); + return authResult; } public static void setTaskDimensions(final ServiceMetricEvent.Builder metricBuilder, final Task task) 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 b432132e0f27..33d6cc70fbe3 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 @@ -101,7 +101,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; /** @@ -183,10 +182,9 @@ public Response taskPost( resourceActions, authorizerMapper ); - - if (!authResult.isAllowed()) { - throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); - } + authResult.getPermissionErrorMessage(true).ifPresent(error -> { + throw new ForbiddenException(error); + }); return asLeaderWith( taskMaster.getTaskQueue(), @@ -615,17 +613,15 @@ public Response getTasks( resourceAction, authorizerMapper ); - if (!authResult.isAllowed()) { + + authResult.getPermissionErrorMessage(true).ifPresent(error -> { throw new WebApplicationException( Response.status(Response.Status.FORBIDDEN) .type(MediaType.TEXT_PLAIN) - .entity(StringUtils.format( - "Access-Check-Result: %s", - Objects.requireNonNull(authResult.getFailureMessage()) - )) + .entity(StringUtils.format("Access-Check-Result: %s", error)) .build() ); - } + }); } return asLeaderWith( @@ -667,9 +663,9 @@ public Response killPendingSegments( authorizerMapper ); - if (!authResult.isAllowed()) { - throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); - } + authResult.getPermissionErrorMessage(true).ifPresent(error -> { + throw new ForbiddenException(error); + }); if (overlord.isLeader()) { try { diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/SupervisorResourceFilter.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/SupervisorResourceFilter.java index b0647623c509..ddca13c24222 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/SupervisorResourceFilter.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/SupervisorResourceFilter.java @@ -41,7 +41,6 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.PathSegment; import javax.ws.rs.core.Response; -import java.util.Objects; public class SupervisorResourceFilter extends AbstractResourceFilter { @@ -104,9 +103,9 @@ public boolean apply(PathSegment input) getAuthorizerMapper() ); - if (!authResult.isAllowed()) { - throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); - } + authResult.getPermissionErrorMessage(true).ifPresent(error -> { + throw new ForbiddenException(error); + }); return request; } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/TaskResourceFilter.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/TaskResourceFilter.java index bd63c1197814..7e431dca8797 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/TaskResourceFilter.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/TaskResourceFilter.java @@ -40,7 +40,6 @@ import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import java.util.Objects; /** * Use this ResourceFilter when the datasource information is present after "task" segment in the request Path @@ -99,9 +98,9 @@ public ContainerRequest filter(ContainerRequest request) getAuthorizerMapper() ); - if (!authResult.isAllowed()) { - throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); - } + authResult.getPermissionErrorMessage(true).ifPresent(error -> { + throw new ForbiddenException(error); + }); return request; } 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 2b4fa1565328..de2f26c3923c 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 @@ -40,7 +40,6 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import java.util.HashSet; -import java.util.Objects; import java.util.Set; @Path("/druid/indexer/v1/sampler") @@ -79,9 +78,9 @@ public SamplerResponse post(final SamplerSpec sampler, @Context final HttpServle authorizerMapper ); - if (!authResult.isAllowed()) { - throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); - } + authResult.getPermissionErrorMessage(true).ifPresent(error -> { + throw new ForbiddenException(error); + }); return sampler.sample(); } } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java index dc1bc41bbebd..487dbd4eeaa3 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java @@ -148,9 +148,9 @@ public Response specPost(final SupervisorSpec spec, @Context final HttpServletRe authorizerMapper ); - if (!authResult.isAllowed()) { - throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); - } + authResult.getPermissionErrorMessage(true).ifPresent(error -> { + throw new ForbiddenException(error); + }); manager.createOrUpdateAndStartSupervisor(spec); diff --git a/server/src/main/java/org/apache/druid/segment/realtime/ChatHandlers.java b/server/src/main/java/org/apache/druid/segment/realtime/ChatHandlers.java index bcfff65b1852..28074d037b4b 100644 --- a/server/src/main/java/org/apache/druid/segment/realtime/ChatHandlers.java +++ b/server/src/main/java/org/apache/druid/segment/realtime/ChatHandlers.java @@ -29,7 +29,6 @@ import org.apache.druid.server.security.ResourceType; import javax.servlet.http.HttpServletRequest; -import java.util.Objects; public class ChatHandlers { @@ -50,11 +49,11 @@ public static AuthorizationResult authorizationCheck( action ); - AuthorizationResult access = AuthorizationUtils.authorizeResourceAction(req, resourceAction, authorizerMapper); - if (!access.isAllowed()) { - throw new ForbiddenException(Objects.requireNonNull(access.getFailureMessage())); - } + AuthorizationResult authResult = AuthorizationUtils.authorizeResourceAction(req, resourceAction, authorizerMapper); + authResult.getPermissionErrorMessage(true).ifPresent(error -> { + throw new ForbiddenException(error); + }); - return access; + return authResult; } } 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 c314a47554f6..a0e1f0eded14 100644 --- a/server/src/main/java/org/apache/druid/server/QueryLifecycle.java +++ b/server/src/main/java/org/apache/druid/server/QueryLifecycle.java @@ -66,7 +66,6 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -156,10 +155,6 @@ public QueryResponse runSimple( final QueryResponse queryResponse; try { preAuthorized(authenticationResult, authorizationResult); - if (!authorizationResult.isAllowed()) { - throw new ISE(Objects.requireNonNull(authorizationResult.getFailureMessage())); - } - queryResponse = execute(); results = queryResponse.getResults(); } @@ -280,9 +275,13 @@ private void preAuthorized( final AuthorizationResult authorizationResult ) { - // gotta transition those states, even if we are already authorized + // The authorization have already been checked previously (or skipped). This just follows the state transition + // process, should not throw unauthorized error. transition(State.INITIALIZED, State.AUTHORIZING); doAuthorize(authenticationResult, authorizationResult); + if (!state.equals(State.AUTHORIZED)) { + throw DruidException.defensive("Unexpected state [%s], expecting [%s].", state, State.AUTHORIZED); + } } private AuthorizationResult doAuthorize( @@ -293,7 +292,7 @@ private AuthorizationResult doAuthorize( Preconditions.checkNotNull(authenticationResult, "authenticationResult"); Preconditions.checkNotNull(authorizationResult, "authorizationResult"); - if (!authorizationResult.isAllowed()) { + if (authorizationResult.getPermissionErrorMessage(false).isPresent()) { // Not authorized; go straight to Jail, do not pass Go. transition(State.AUTHORIZING, State.UNAUTHORIZED); } else { diff --git a/server/src/main/java/org/apache/druid/server/QueryResource.java b/server/src/main/java/org/apache/druid/server/QueryResource.java index 94f2d1f1f57e..c7c0a6e1ae0b 100644 --- a/server/src/main/java/org/apache/druid/server/QueryResource.java +++ b/server/src/main/java/org/apache/druid/server/QueryResource.java @@ -74,7 +74,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.util.Objects; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicLong; @@ -159,9 +158,9 @@ public Response cancelQuery(@PathParam("id") String queryId, @Context final Http authorizerMapper ); - if (!authResult.isAllowed()) { - throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); - } + authResult.getPermissionErrorMessage(true).ifPresent(error -> { + throw new ForbiddenException(error); + }); queryScheduler.cancelQuery(queryId); return Response.status(Response.Status.ACCEPTED).build(); @@ -215,9 +214,9 @@ public Response doPost( return io.getResponseWriter().buildNonOkResponse(qe.getFailType().getExpectedStatus(), qe); } - if (!authResult.isAllowed()) { - throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); - } + authResult.getPermissionErrorMessage(true).ifPresent(error -> { + throw new ForbiddenException(error); + }); final QueryResourceQueryResultPusher pusher = new QueryResourceQueryResultPusher(req, queryLifecycle, io); return pusher.push(); 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 8184b165df4f..6c357900da2a 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 @@ -29,8 +29,6 @@ import org.apache.druid.server.security.ResourceAction; import org.apache.druid.server.security.ResourceType; -import java.util.Objects; - /** * Use this ResourceFilter at end points where Druid Cluster configuration is read or written * Here are some example paths where this filter is used - @@ -64,9 +62,9 @@ public ContainerRequest filter(ContainerRequest request) getAuthorizerMapper() ); - if (!authResult.isAllowed()) { - throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); - } + authResult.getPermissionErrorMessage(true).ifPresent(error -> { + throw new ForbiddenException(error); + }); 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 970ba60223d8..4c239a902486 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 @@ -33,7 +33,6 @@ import javax.ws.rs.core.PathSegment; import java.util.List; -import java.util.Objects; /** * Use this resource filter for API endpoints that contain {@link #DATASOURCES_PATH_SEGMENT} in their request path. @@ -64,9 +63,9 @@ public ContainerRequest filter(ContainerRequest request) getAuthorizerMapper() ); - if (!authResult.isAllowed()) { - throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); - } + authResult.getPermissionErrorMessage(true).ifPresent(error -> { + throw new ForbiddenException(error); + }); 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 b90d967a91d7..04478d4cdac8 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 @@ -33,7 +33,6 @@ import org.apache.druid.server.security.ResourceType; import javax.ws.rs.core.PathSegment; -import java.util.Objects; /** @@ -82,9 +81,9 @@ public boolean apply(PathSegment input) getAuthorizerMapper() ); - if (!authResult.isAllowed()) { - throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); - } + authResult.getPermissionErrorMessage(true).ifPresent(error -> { + throw new ForbiddenException(error); + }); 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 890e37452386..600fd4f4ebe9 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 @@ -28,7 +28,6 @@ import org.apache.druid.server.security.Resource; import org.apache.druid.server.security.ResourceAction; -import java.util.Objects; /** * Use this ResourceFilter at end points where Druid Cluster State is read or written @@ -67,9 +66,9 @@ public ContainerRequest filter(ContainerRequest request) getAuthorizerMapper() ); - if (!authResult.isAllowed()) { - throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); - } + authResult.getPermissionErrorMessage(true).ifPresent(error -> { + throw new ForbiddenException(error); + }); return request; } diff --git a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java index d4c16615daeb..ed95dfbf9618 100644 --- a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java +++ b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java @@ -21,11 +21,13 @@ import com.google.common.collect.ImmutableMap; import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.filter.TrueDimFilter; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Collections; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -98,23 +100,28 @@ public AuthorizationResult withResourceActions( ) { return new AuthorizationResult( - isAllowed(), - getFailureMessage(), + isAllowed, + failureMessage, ImmutableMap.copyOf(getPolicyFilters()), sqlResourceActions, allResourceActions ); } - public boolean isAllowed() + public Optional getPermissionErrorMessage(boolean policyFilterNotPermitted) { - return isAllowed; - } - - @Nullable - public String getFailureMessage() - { - return failureMessage; + if (!isAllowed) { + return Optional.of(Objects.requireNonNull(failureMessage)); + } + + if (policyFilterNotPermitted && policyFilters.values() + .stream() + .flatMap(Optional::stream) + .anyMatch(filter -> !(filter instanceof TrueDimFilter))) { + return Optional.of(Access.DEFAULT_ERROR_MESSAGE); + } + + return Optional.empty(); } public Map> getPolicyFilters() 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 5fbac8a5b668..47b7d0a84959 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 @@ -227,14 +227,17 @@ public static AuthorizationResult authorizeAllResourceActions( throw new ISE("Request already had authorization check."); } - AuthorizationResult access = authorizeAllResourceActions( + AuthorizationResult authResult = authorizeAllResourceActions( authenticationResultFromRequest(request), resourceActions, authorizerMapper ); - request.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, access.isAllowed()); - return access; + request.setAttribute( + AuthConfig.DRUID_AUTHORIZATION_CHECKED, + !authResult.getPermissionErrorMessage(false).isPresent() + ); + return authResult; } /** diff --git a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java index e745def91bd6..a816345fed7b 100644 --- a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java +++ b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java @@ -224,7 +224,7 @@ public void testAuthorizedWithNoPolicyRestriction() .build(); QueryLifecycle lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertTrue(lifecycle.authorize(authenticationResult).isAllowed()); + Assert.assertTrue(lifecycle.authorize(authenticationResult).getPermissionErrorMessage(false).isEmpty()); lifecycle.execute(); lifecycle = createLifecycle(authConfig); @@ -265,7 +265,7 @@ public void testAuthorizedWithAlwaysTruePolicyRestriction() .build(); QueryLifecycle lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertTrue(lifecycle.authorize(authenticationResult).isAllowed()); + Assert.assertTrue(lifecycle.authorize(authenticationResult).getPermissionErrorMessage(false).isEmpty()); lifecycle.execute(); lifecycle = createLifecycle(authConfig); @@ -310,7 +310,7 @@ public void testAuthorizedWithOnePolicyRestriction() .build(); QueryLifecycle lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertTrue(lifecycle.authorize(authenticationResult).isAllowed()); + Assert.assertTrue(lifecycle.authorize(authenticationResult).getPermissionErrorMessage(false).isEmpty()); lifecycle.execute(); lifecycle = createLifecycle(authConfig); @@ -475,11 +475,11 @@ public void testAuthorizeQueryContext_authorized() revisedContext ); - Assert.assertTrue(lifecycle.authorize(mockRequest()).isAllowed()); + Assert.assertTrue(lifecycle.authorize(mockRequest()).getPermissionErrorMessage(false).isEmpty()); lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertTrue(lifecycle.authorize(authenticationResult).isAllowed()); + Assert.assertTrue(lifecycle.authorize(authenticationResult).getPermissionErrorMessage(false).isEmpty()); } @Test @@ -521,11 +521,11 @@ public void testAuthorizeQueryContext_notAuthorized() .build(); QueryLifecycle lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertFalse(lifecycle.authorize(mockRequest()).isAllowed()); + Assert.assertFalse(lifecycle.authorize(mockRequest()).getPermissionErrorMessage(false).isEmpty()); lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertFalse(lifecycle.authorize(authenticationResult).isAllowed()); + Assert.assertFalse(lifecycle.authorize(authenticationResult).getPermissionErrorMessage(false).isEmpty()); } @Test @@ -571,11 +571,11 @@ public void testAuthorizeQueryContext_unsecuredKeys() revisedContext ); - Assert.assertTrue(lifecycle.authorize(mockRequest()).isAllowed()); + Assert.assertTrue(lifecycle.authorize(mockRequest()).getPermissionErrorMessage(false).isEmpty()); lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertTrue(lifecycle.authorize(authenticationResult).isAllowed()); + Assert.assertTrue(lifecycle.authorize(authenticationResult).getPermissionErrorMessage(false).isEmpty()); } @Test @@ -622,11 +622,11 @@ public void testAuthorizeQueryContext_securedKeys() revisedContext ); - Assert.assertTrue(lifecycle.authorize(mockRequest()).isAllowed()); + Assert.assertTrue(lifecycle.authorize(mockRequest()).getPermissionErrorMessage(false).isEmpty()); lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertTrue(lifecycle.authorize(authenticationResult).isAllowed()); + Assert.assertTrue(lifecycle.authorize(authenticationResult).getPermissionErrorMessage(false).isEmpty()); } @Test @@ -671,11 +671,11 @@ public void testAuthorizeQueryContext_securedKeysNotAuthorized() .build(); QueryLifecycle lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertFalse(lifecycle.authorize(mockRequest()).isAllowed()); + Assert.assertFalse(lifecycle.authorize(mockRequest()).getPermissionErrorMessage(false).isEmpty()); lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertFalse(lifecycle.authorize(authenticationResult).isAllowed()); + Assert.assertFalse(lifecycle.authorize(authenticationResult).getPermissionErrorMessage(false).isEmpty()); } @Test @@ -731,11 +731,11 @@ public void testAuthorizeLegacyQueryContext_authorized() Assert.assertTrue(revisedContext.containsKey("baz")); Assert.assertTrue(revisedContext.containsKey("queryId")); - Assert.assertTrue(lifecycle.authorize(mockRequest()).isAllowed()); + Assert.assertTrue(lifecycle.authorize(mockRequest()).getPermissionErrorMessage(false).isEmpty()); lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertTrue(lifecycle.authorize(mockRequest()).isAllowed()); + Assert.assertTrue(lifecycle.authorize(mockRequest()).getPermissionErrorMessage(false).isEmpty()); } public static Query queryMatchDataSource(DataSource dataSource) diff --git a/sql/src/main/java/org/apache/druid/sql/AbstractStatement.java b/sql/src/main/java/org/apache/druid/sql/AbstractStatement.java index b3f04bf0fdda..90962fa05716 100644 --- a/sql/src/main/java/org/apache/druid/sql/AbstractStatement.java +++ b/sql/src/main/java/org/apache/druid/sql/AbstractStatement.java @@ -151,9 +151,9 @@ protected void authorize( // Authentication is done by the planner using the function provided // here. The planner ensures that this step is done before planning. authResult = planner.authorize(authorizer, contextResources); - if (!authResult.isAllowed()) { - throw new ForbiddenException(Objects.requireNonNull(authResult.getFailureMessage())); - } + authResult.getPermissionErrorMessage(false).ifPresent(error -> { + throw new ForbiddenException(error); + }); } /** 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 fe2e54fef83f..ac4c645560bc 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 @@ -1147,15 +1147,15 @@ private static void checkStateReadAccessForServers( AuthorizerMapper authorizerMapper ) { - final AuthorizationResult stateAccess = AuthorizationUtils.authorizeAllResourceActions( + final AuthorizationResult authResult = AuthorizationUtils.authorizeAllResourceActions( authenticationResult, Collections.singletonList(new ResourceAction(Resource.STATE_RESOURCE, Action.READ)), authorizerMapper ); - if (!stateAccess.isAllowed()) { - throw new ForbiddenException("Insufficient permission to view servers: " - + Objects.requireNonNull(stateAccess.getFailureMessage())); - } + + authResult.getPermissionErrorMessage(true).ifPresent(error -> { + throw new ForbiddenException("Insufficient permission to view servers: " + error); + }); } /** diff --git a/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java b/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java index 9be9c3077129..b93de5ce9653 100644 --- a/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java +++ b/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java @@ -140,9 +140,9 @@ public Response cancelQuery( return Response.status(Status.NOT_FOUND).build(); } - final AuthorizationResult access = authorizeCancellation(req, lifecycles); + final AuthorizationResult authResult = authorizeCancellation(req, lifecycles); - if (access.isAllowed()) { + if (!authResult.getPermissionErrorMessage(true).isPresent()) { // should remove only the lifecycles in the snapshot. sqlLifecycleManager.removeAll(sqlQueryId, lifecycles); lifecycles.forEach(Cancelable::cancel); From b19728b6bffe7cf2462d9b6f431e1e476fe8b887 Mon Sep 17 00:00:00 2001 From: cecemei Date: Tue, 17 Dec 2024 20:14:01 -0800 Subject: [PATCH 03/32] policy change --- .../java/org/apache/druid/server/Policy.java | 26 +++++++++++++++++++ .../apache/druid/server/security/Access.java | 21 ++++++++------- .../server/security/AuthorizationResult.java | 23 +++++++++++----- .../server/security/AuthorizationUtils.java | 7 ++--- .../druid/server/QueryLifecycleTest.java | 6 ++--- 5 files changed, 61 insertions(+), 22 deletions(-) create mode 100644 server/src/main/java/org/apache/druid/server/Policy.java diff --git a/server/src/main/java/org/apache/druid/server/Policy.java b/server/src/main/java/org/apache/druid/server/Policy.java new file mode 100644 index 000000000000..ea36839eca00 --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/Policy.java @@ -0,0 +1,26 @@ +package org.apache.druid.server; + +import org.apache.druid.query.filter.DimFilter; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Optional; + +public class Policy +{ + public static final Policy NO_RESTRICTION = new Policy(null); + + private final Optional rowFilter; + + public Policy(@Nullable DimFilter rowFilter) { + this.rowFilter = Optional.ofNullable(rowFilter); + } + + public static Policy fromRowFilter(@Nonnull DimFilter rowFilter) { + return new Policy(rowFilter); + } + + public Optional getRowFilter() { + return rowFilter; + } +} diff --git a/server/src/main/java/org/apache/druid/server/security/Access.java b/server/src/main/java/org/apache/druid/server/security/Access.java index 0dff2d3d64ef..979ce989dc2a 100644 --- a/server/src/main/java/org/apache/druid/server/security/Access.java +++ b/server/src/main/java/org/apache/druid/server/security/Access.java @@ -22,6 +22,7 @@ import com.google.common.base.Strings; import org.apache.druid.java.util.common.StringUtils; import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.server.Policy; import javax.annotation.Nullable; import java.util.Objects; @@ -39,7 +40,7 @@ public class Access private final String message; // A row-level policy filter on top of table-level read access. It should be empty if there are no policy restrictions // or if access is requested for an action other than reading the table. - private final Optional rowFilter; + private final Policy rowFilter; /** * @deprecated use {@link #allow()} or {@link #deny(String)} instead @@ -47,10 +48,10 @@ public class Access @Deprecated public Access(boolean allowed) { - this(allowed, "", Optional.empty()); + this(allowed, "", Policy.NO_RESTRICTION); } - Access(boolean allowed, String message, Optional rowFilter) + Access(boolean allowed, String message, Policy rowFilter) { this.allowed = allowed; this.message = message; @@ -59,17 +60,17 @@ public Access(boolean allowed) public static Access allow() { - return new Access(true, "", Optional.empty()); + return new Access(true, "", Policy.NO_RESTRICTION); } public static Access deny(@Nullable String message) { - return new Access(false, Objects.isNull(message) ? "" : message, Optional.empty()); + return new Access(false, Objects.isNull(message) ? "" : message, null); } - public static Access allowWithRestriction(Optional rowFilter) + public static Access allowWithRestriction(Policy policy) { - return new Access(true, "", rowFilter); + return new Access(true, "", policy); } public boolean isAllowed() @@ -77,7 +78,7 @@ public boolean isAllowed() return allowed; } - public Optional getRowFilter() + public Policy getPolicy() { return rowFilter; } @@ -90,9 +91,9 @@ public String getMessage() stringBuilder.append(", "); stringBuilder.append(message); } - if (allowed && rowFilter.isPresent()) { + if (allowed && rowFilter.getRowFilter().isPresent()) { stringBuilder.append(", with restriction "); - stringBuilder.append(rowFilter.get()); + stringBuilder.append(rowFilter.getRowFilter().get()); } return stringBuilder.toString(); } diff --git a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java index ed95dfbf9618..394a264ee132 100644 --- a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java +++ b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java @@ -22,10 +22,12 @@ import com.google.common.collect.ImmutableMap; import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.TrueDimFilter; +import org.apache.druid.server.Policy; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -61,7 +63,7 @@ public class AuthorizationResult @Nullable private final String failureMessage; - private final Map> policyFilters; + private final Map policyFilters; @Nullable private final Set sqlResourceActions; @@ -72,7 +74,7 @@ public class AuthorizationResult AuthorizationResult( boolean isAllowed, @Nullable String failureMessage, - Map> policyFilters, + Map policyFilters, @Nullable Set sqlResourceActions, @Nullable Set allResourceActions ) @@ -89,7 +91,7 @@ public static AuthorizationResult deny(@Nonnull String failureMessage) return new AuthorizationResult(false, failureMessage, Collections.emptyMap(), null, null); } - public static AuthorizationResult allowWithRestriction(Map> policyFilters) + public static AuthorizationResult allowWithRestriction(Map policyFilters) { return new AuthorizationResult(true, null, policyFilters, null, null); } @@ -102,7 +104,7 @@ public AuthorizationResult withResourceActions( return new AuthorizationResult( isAllowed, failureMessage, - ImmutableMap.copyOf(getPolicyFilters()), + ImmutableMap.copyOf(getPolicy()), sqlResourceActions, allResourceActions ); @@ -115,7 +117,7 @@ public Optional getPermissionErrorMessage(boolean policyFilterNotPermitt } if (policyFilterNotPermitted && policyFilters.values() - .stream() + .stream().map(Policy::getRowFilter) .flatMap(Optional::stream) .anyMatch(filter -> !(filter instanceof TrueDimFilter))) { return Optional.of(Access.DEFAULT_ERROR_MESSAGE); @@ -124,11 +126,20 @@ public Optional getPermissionErrorMessage(boolean policyFilterNotPermitt return Optional.empty(); } - public Map> getPolicyFilters() + public Map getPolicy() { return policyFilters; } + public Map> getPolicyFilters() + { + Map> filters = new HashMap<>(); + for (Map.Entry entry : policyFilters.entrySet()) { + filters.put(entry.getKey(), entry.getValue().getRowFilter()); + } + return filters; + } + @Nullable public Set getSqlResourceActions() { 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 47b7d0a84959..4e143c4ae693 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 @@ -29,6 +29,7 @@ import org.apache.druid.error.DruidException; import org.apache.druid.java.util.common.ISE; import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.server.Policy; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; @@ -168,7 +169,7 @@ public static AuthorizationResult authorizeAllResourceActions( // this method returns on first failure, so only successful Access results are kept in the cache final Set resultCache = new HashSet<>(); - final Map> policyFilters = new HashMap<>(); + final Map policyFilters = new HashMap<>(); for (ResourceAction resourceAction : resourceActions) { if (resultCache.contains(resourceAction)) { @@ -185,8 +186,8 @@ public static AuthorizationResult authorizeAllResourceActions( resultCache.add(resourceAction); if (resourceAction.getAction().equals(Action.READ) && RESTRICTION_APPLICABLE_RESOURCE_TYPES.contains(resourceAction.getResource().getType())) { - policyFilters.put(resourceAction.getResource().getName(), access.getRowFilter()); - } else if (access.getRowFilter().isPresent()) { + policyFilters.put(resourceAction.getResource().getName(), access.getPolicy()); + } else if (access.getPolicy().getRowFilter().isPresent()) { throw DruidException.defensive( "Row policy should only present when reading a table, but was present for %s", resourceAction diff --git a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java index a816345fed7b..558bffce4d24 100644 --- a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java +++ b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java @@ -202,7 +202,7 @@ public void testAuthorizedWithNoPolicyRestriction() new Resource(DATASOURCE, ResourceType.DATASOURCE), Action.READ )) - .andReturn(Access.allowWithRestriction(Optional.empty())).once(); + .andReturn(Access.allowWithRestriction(Policy.NO_RESTRICTION)).once(); EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) .andReturn(toolChest).times(2); EasyMock.expect(texasRanger.getQueryRunnerForIntervals( @@ -243,7 +243,7 @@ public void testAuthorizedWithAlwaysTruePolicyRestriction() new Resource(DATASOURCE, ResourceType.DATASOURCE), Action.READ )) - .andReturn(Access.allowWithRestriction(alwaysTrueFilter)).once(); + .andReturn(Access.allowWithRestriction(Policy.fromRowFilter(alwaysTrueFilter.get()))).once(); EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) .andReturn(toolChest).times(2); EasyMock.expect(texasRanger.getQueryRunnerForIntervals(queryMatchDataSource(RestrictedDataSource.create( @@ -293,7 +293,7 @@ public void testAuthorizedWithOnePolicyRestriction() new Resource(DATASOURCE, ResourceType.DATASOURCE), Action.READ )) - .andReturn(Access.allowWithRestriction(rowFilter)).times(1); + .andReturn(Access.allowWithRestriction(Policy.fromRowFilter(rowFilter.get()))).times(1); EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) .andReturn(toolChest).times(2); EasyMock.expect(texasRanger.getQueryRunnerForIntervals(queryMatchDataSource(RestrictedDataSource.create( From aae0bb17273f65b991bbc089523a77b2d8dc841e Mon Sep 17 00:00:00 2001 From: cecemei Date: Tue, 17 Dec 2024 21:10:04 -0800 Subject: [PATCH 04/32] minor change, Optional.stream is not supported java8 --- .../org/apache/druid/server/security/AuthorizationResult.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java index ed95dfbf9618..782d1f9a204c 100644 --- a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java +++ b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java @@ -116,7 +116,7 @@ public Optional getPermissionErrorMessage(boolean policyFilterNotPermitt if (policyFilterNotPermitted && policyFilters.values() .stream() - .flatMap(Optional::stream) + .map(v -> v.orElse(null)) .anyMatch(filter -> !(filter instanceof TrueDimFilter))) { return Optional.of(Access.DEFAULT_ERROR_MESSAGE); } From af51747b278f7314d5092d6bcefebf894b5f8077 Mon Sep 17 00:00:00 2001 From: cecemei Date: Tue, 17 Dec 2024 21:24:26 -0800 Subject: [PATCH 05/32] update test case for JoinDataSource since it has a new JoinAlgorithm field now --- .../java/org/apache/druid/query/RestrictedDataSource.java | 4 ++-- .../test/java/org/apache/druid/query/JoinDataSourceTest.java | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java b/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java index 8685006c4971..e6884e9d9b9d 100644 --- a/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java +++ b/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java @@ -112,7 +112,7 @@ public DataSource withChildren(List children) throw new IAE("Expected [1] child, got [%d]", children.size()); } - return RestrictedDataSource.create(children.get(0), rowFilter); + return create(children.get(0), rowFilter); } @Override @@ -151,7 +151,7 @@ public Function createSegmentMapFunction( @Override public DataSource withUpdatedDataSource(DataSource newSource) { - return RestrictedDataSource.create(newSource, rowFilter); + return create(newSource, rowFilter); } @Override diff --git a/processing/src/test/java/org/apache/druid/query/JoinDataSourceTest.java b/processing/src/test/java/org/apache/druid/query/JoinDataSourceTest.java index 62807cf7e3b3..95c9936c340a 100644 --- a/processing/src/test/java/org/apache/druid/query/JoinDataSourceTest.java +++ b/processing/src/test/java/org/apache/druid/query/JoinDataSourceTest.java @@ -516,7 +516,8 @@ public void testGetAnalysisWithRestrictedDS() JoinType.LEFT, null, ExprMacroTable.nil(), - null + null, + JoinAlgorithm.BROADCAST ); DataSourceAnalysis analysis = dataSource.getAnalysis(); Assert.assertEquals("table1", analysis.getBaseDataSource().getTableNames().iterator().next()); From 92077edb4ef44eeb5a6b5b029166ec65a39f01fa Mon Sep 17 00:00:00 2001 From: cecemei Date: Wed, 18 Dec 2024 00:11:08 -0800 Subject: [PATCH 06/32] fix a bug in AuthorizationResult --- .../apache/druid/server/security/AuthorizationResult.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java index 782d1f9a204c..f4322bfad66e 100644 --- a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java +++ b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java @@ -30,6 +30,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Stream; public class AuthorizationResult { @@ -116,7 +117,9 @@ public Optional getPermissionErrorMessage(boolean policyFilterNotPermitt if (policyFilterNotPermitted && policyFilters.values() .stream() - .map(v -> v.orElse(null)) + .flatMap(v -> v.isPresent() + ? Stream.of(v.get()) + : Stream.empty()) // Can be replaced by Optional:stream in Java 11 .anyMatch(filter -> !(filter instanceof TrueDimFilter))) { return Optional.of(Access.DEFAULT_ERROR_MESSAGE); } From a470236d666c276c35db6d39e2292448ffcbd04d Mon Sep 17 00:00:00 2001 From: cecemei Date: Wed, 18 Dec 2024 09:57:02 -0800 Subject: [PATCH 07/32] fix style and bug --- .../main/java/org/apache/druid/grpc/server/QueryDriver.java | 2 +- .../druid/metadata/SQLMetadataStorageActionHandler.java | 3 +-- .../main/java/org/apache/druid/server/security/Access.java | 4 ++-- .../java/org/apache/druid/server/QueryLifecycleTest.java | 5 +++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java b/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java index 10559026af54..50c426a70b52 100644 --- a/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java +++ b/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java @@ -148,7 +148,7 @@ private QueryResponse runNativeQuery(QueryRequest request, AuthenticationResult try { queryLifecycle.initialize(query); AuthorizationResult authorizationResult = queryLifecycle.authorize(authResult); - if (!authorizationResult.getPermissionErrorMessage(true).isPresent()) { + if (authorizationResult.getPermissionErrorMessage(true).isPresent()) { throw new ForbiddenException(Access.DEFAULT_ERROR_MESSAGE); } queryResponse = queryLifecycle.execute(); diff --git a/server/src/main/java/org/apache/druid/metadata/SQLMetadataStorageActionHandler.java b/server/src/main/java/org/apache/druid/metadata/SQLMetadataStorageActionHandler.java index eaf2efc82aaf..622191138e9f 100644 --- a/server/src/main/java/org/apache/druid/metadata/SQLMetadataStorageActionHandler.java +++ b/server/src/main/java/org/apache/druid/metadata/SQLMetadataStorageActionHandler.java @@ -119,8 +119,7 @@ public SQLMetadataStorageActionHandler( this.connector = connector; //fully qualified references required below due to identical package names across project modules. //noinspection UnnecessaryFullyQualifiedName - this.jsonMapper = jsonMapper.copy().addMixIn(org.apache.druid.metadata.PasswordProvider.class, - org.apache.druid.metadata.PasswordProviderRedactionMixIn.class); + this.jsonMapper = jsonMapper.copy().addMixIn(PasswordProvider.class,PasswordProviderRedactionMixIn.class); this.entryType = types.getEntryType(); this.statusType = types.getStatusType(); this.lockType = types.getLockType(); diff --git a/server/src/main/java/org/apache/druid/server/security/Access.java b/server/src/main/java/org/apache/druid/server/security/Access.java index 0dff2d3d64ef..5cd4ddd298da 100644 --- a/server/src/main/java/org/apache/druid/server/security/Access.java +++ b/server/src/main/java/org/apache/druid/server/security/Access.java @@ -32,8 +32,8 @@ public class Access public static final String DEFAULT_ERROR_MESSAGE = "Unauthorized"; public static final String DEFAULT_AUTHORIZED_MESSAGE = "Authorized"; - public static final Access OK = Access.allow(); - public static final Access DENIED = Access.deny(""); + public static final Access OK = allow(); + public static final Access DENIED = deny(""); private final boolean allowed; private final String message; diff --git a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java index a816345fed7b..92c57ce82fe6 100644 --- a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java +++ b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java @@ -22,6 +22,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import org.apache.druid.error.DruidException; import org.apache.druid.java.util.common.ISE; import org.apache.druid.java.util.common.Intervals; import org.apache.druid.java.util.common.guava.Sequences; @@ -173,8 +174,8 @@ public void testRunSimplePreauthorized() @Test public void testRunSimpleUnauthorized() { - expectedException.expect(ISE.class); - expectedException.expectMessage(Access.DEFAULT_ERROR_MESSAGE); + expectedException.expect(DruidException.class); + expectedException.expectMessage("Unexpected state [UNAUTHORIZED], expecting [AUTHORIZED]"); EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); From e7ebcf8f22c4d09ef3285254767b50154ee814d1 Mon Sep 17 00:00:00 2001 From: cecemei Date: Wed, 18 Dec 2024 18:06:58 -0800 Subject: [PATCH 08/32] Added a Policy class to wrap the filter, updated some Javadoc --- .../org/apache/druid/query/DataSource.java | 26 ++--- .../java/org/apache/druid/query/Query.java | 18 ++-- .../druid/query/RestrictedDataSource.java | 80 ++++++---------- .../apache/druid/query/TableDataSource.java | 17 ++-- .../metadata/SegmentMetadataQuery.java | 3 +- .../org/apache/druid/query/policy/Policy.java | 95 +++++++++++++++++++ .../apache/druid/query/DataSourceTest.java | 35 ++++--- .../druid/query/JoinDataSourceTest.java | 3 +- .../druid/query/RestrictedDataSourceTest.java | 33 ++++++- .../metadata/SegmentMetadataQueryTest.java | 22 +++-- .../SQLMetadataStorageActionHandler.java | 2 +- .../java/org/apache/druid/server/Policy.java | 26 ----- .../apache/druid/server/QueryLifecycle.java | 57 ++++++++--- .../apache/druid/server/security/Access.java | 43 ++++++--- .../druid/server/security/AuthConfig.java | 4 + .../server/security/AuthorizationResult.java | 64 ++++++++----- .../server/security/AuthorizationUtils.java | 9 +- .../druid/server/QueryLifecycleTest.java | 35 +++---- 18 files changed, 360 insertions(+), 212 deletions(-) create mode 100644 processing/src/main/java/org/apache/druid/query/policy/Policy.java delete mode 100644 server/src/main/java/org/apache/druid/server/Policy.java diff --git a/processing/src/main/java/org/apache/druid/query/DataSource.java b/processing/src/main/java/org/apache/druid/query/DataSource.java index 439aeaddfd44..288d8abd33fc 100644 --- a/processing/src/main/java/org/apache/druid/query/DataSource.java +++ b/processing/src/main/java/org/apache/druid/query/DataSource.java @@ -21,9 +21,9 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.planning.DataSourceAnalysis; import org.apache.druid.query.planning.PreJoinableClause; +import org.apache.druid.query.policy.Policy; import org.apache.druid.segment.SegmentReference; import java.util.List; @@ -123,23 +123,27 @@ public interface DataSource */ DataSource withUpdatedDataSource(DataSource newSource); - default DataSource mapWithRestriction(Map> rowFilters) - { - return mapWithRestriction(rowFilters, true); - } - /** - * Returns an updated datasource based on the policy restrictions on tables. If this datasource contains no table, no - * changes should occur. + * Returns an updated datasource based on the policy restrictions on tables. + *

    + * If this datasource contains no table, no changes should occur. If {@code enableStrictPolicyCheck}, every table must + * have an entry in the {@code policyMap}, the value could be {@code Optional.empty()} meaning no restriction is + * enforced on this table. * - * @param rowFilters a mapping of table names to row filters, every table in the datasource tree must have an entry + * @param policyMap a mapping of table names to policy restrictions, every table in the datasource tree must have an entry + * @param enableStrictPolicyCheck a boolean denoting that, every table should have an entry in the policies map. * @return the updated datasource, with restrictions applied in the datasource tree + * @throws IllegalStateException in one of following conditions: + *

    */ - default DataSource mapWithRestriction(Map> rowFilters, boolean enableStrictPolicyCheck) + default DataSource mapWithRestriction(Map> policyMap, boolean enableStrictPolicyCheck) { List children = this.getChildren() .stream() - .map(child -> child.mapWithRestriction(rowFilters, enableStrictPolicyCheck)) + .map(child -> child.mapWithRestriction(policyMap, enableStrictPolicyCheck)) .collect(Collectors.toList()); return this.withChildren(children); } diff --git a/processing/src/main/java/org/apache/druid/query/Query.java b/processing/src/main/java/org/apache/druid/query/Query.java index 17c84b0ca64c..328d6b1daaab 100644 --- a/processing/src/main/java/org/apache/druid/query/Query.java +++ b/processing/src/main/java/org/apache/druid/query/Query.java @@ -34,6 +34,7 @@ import org.apache.druid.query.metadata.metadata.SegmentMetadataQuery; import org.apache.druid.query.operator.WindowOperatorQuery; import org.apache.druid.query.planning.DataSourceAnalysis; +import org.apache.druid.query.policy.Policy; import org.apache.druid.query.scan.ScanQuery; import org.apache.druid.query.search.SearchQuery; import org.apache.druid.query.select.SelectQuery; @@ -127,7 +128,7 @@ default QueryContext context() /** * Get context value and cast to ContextType in an unsafe way. - * + *

    * For safe conversion, it's recommended to use following methods instead: *

    * {@link QueryContext#getBoolean(String)}
    @@ -179,7 +180,7 @@ default HumanReadableBytes getContextHumanReadableBytes(String key, HumanReadabl * {@link QueryRunnerFactory#mergeRunners(QueryProcessingPool, Iterable)} calls. This is used to combine streams of * results from different sources; for example, it's used by historicals to combine streams from different segments, * and it's used by the broker to combine streams from different historicals. - * + *

    * Important note: sometimes, this ordering is used in a type-unsafe way to order @{code Result} * objects. Because of this, implementations should fall back to {@code Ordering.natural()} when they are given an * object that is not of type T. @@ -190,7 +191,7 @@ default HumanReadableBytes getContextHumanReadableBytes(String key, HumanReadabl /** * Returns a new query, identical to this one, but with a different associated {@link QuerySegmentSpec}. - * + *

    * This often changes the behavior of {@link #getRunner(QuerySegmentWalker)}, since most queries inherit that method * from {@link BaseQuery}, which implements it by calling {@link QuerySegmentSpec#lookup}. */ @@ -243,12 +244,7 @@ default String getMostSpecificId() Query withDataSource(DataSource dataSource); - default Query withPolicyRestrictions(Map> restrictions) - { - return this.withPolicyRestrictions(restrictions, true); - } - - default Query withPolicyRestrictions(Map> restrictions, boolean enableStrictPolicyCheck) + default Query withPolicyRestrictions(Map> restrictions, boolean enableStrictPolicyCheck) { return this.withDataSource(this.getDataSource().mapWithRestriction(restrictions, enableStrictPolicyCheck)); } @@ -275,12 +271,12 @@ default VirtualColumns getVirtualColumns() /** * Returns the set of columns that this query will need to access out of its datasource. - * + *

    * This method does not "look into" what the datasource itself is doing. For example, if a query is built on a * {@link QueryDataSource}, this method will not return the columns used by that subquery. As another example, if a * query is built on a {@link JoinDataSource}, this method will not return the columns from the underlying datasources * that are used by the join condition, unless those columns are also used by this query in other ways. - * + *

    * Returns null if the set of required columns cannot be known ahead of time. */ @Nullable diff --git a/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java b/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java index e6884e9d9b9d..542c8f6bb62e 100644 --- a/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java +++ b/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java @@ -22,17 +22,14 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.collect.ImmutableList; -import org.apache.druid.error.DruidException; import org.apache.druid.java.util.common.IAE; import org.apache.druid.java.util.common.ISE; -import org.apache.druid.query.filter.DimFilter; -import org.apache.druid.query.filter.TrueDimFilter; import org.apache.druid.query.planning.DataSourceAnalysis; +import org.apache.druid.query.policy.Policy; import org.apache.druid.segment.RestrictedSegment; import org.apache.druid.segment.SegmentReference; import org.apache.druid.utils.JvmUtils; -import javax.annotation.Nullable; import java.util.List; import java.util.Map; import java.util.Objects; @@ -51,8 +48,7 @@ public class RestrictedDataSource implements DataSource { private final TableDataSource base; - @Nullable - private final DimFilter rowFilter; + private final Policy policy; @JsonProperty("base") public TableDataSource getBase() @@ -60,37 +56,31 @@ public TableDataSource getBase() return base; } - /** - * Returns true if the row-level filter imposes no restrictions. - */ - public boolean allowAll() + @JsonProperty("policy") + public Policy getPolicy() { - return Objects.isNull(rowFilter) || rowFilter.equals(TrueDimFilter.instance()); + return policy; } - @Nullable - @JsonProperty("filter") - public DimFilter getFilter() - { - return rowFilter; - } - - RestrictedDataSource(TableDataSource base, @Nullable DimFilter rowFilter) + RestrictedDataSource(TableDataSource base, Policy policy) { this.base = base; - this.rowFilter = rowFilter; + this.policy = policy; } @JsonCreator public static RestrictedDataSource create( @JsonProperty("base") DataSource base, - @Nullable @JsonProperty("filter") DimFilter rowFilter + @JsonProperty("policy") Policy policy ) { if (!(base instanceof TableDataSource)) { throw new IAE("Expected a TableDataSource, got [%s]", base.getClass()); } - return new RestrictedDataSource((TableDataSource) base, rowFilter); + if (Objects.isNull(policy)) { + throw new IAE("Policy can't be null for RestrictedDataSource"); + } + return new RestrictedDataSource((TableDataSource) base, policy); } @Override @@ -112,7 +102,7 @@ public DataSource withChildren(List children) throw new IAE("Expected [1] child, got [%d]", children.size()); } - return create(children.get(0), rowFilter); + return create(children.get(0), policy); } @Override @@ -144,53 +134,45 @@ public Function createSegmentMapFunction( () -> base.createSegmentMapFunction( query, cpuTimeAccumulator - ).andThen((segment) -> (new RestrictedSegment(segment, rowFilter))) + ).andThen((segment) -> (new RestrictedSegment(segment, policy.getRowFilter()))) ); } @Override public DataSource withUpdatedDataSource(DataSource newSource) { - return create(newSource, rowFilter); + return create(newSource, policy); } @Override - public DataSource mapWithRestriction(Map> rowFilters, boolean enableStrictPolicyCheck) + public DataSource mapWithRestriction(Map> policies, boolean enableStrictPolicyCheck) { - if (!rowFilters.containsKey(this.base.getName()) && enableStrictPolicyCheck) { - throw DruidException.defensive("Missing row filter for table [%s]", this.base.getName()); + if (!policies.containsKey(base.getName()) && enableStrictPolicyCheck) { + throw new ISE("Missing policy check result for table [%s]", base.getName()); } - Optional newFilter = rowFilters.getOrDefault(this.base.getName(), Optional.empty()); - if (!newFilter.isPresent()) { - throw DruidException.defensive( + Optional newPolicy = policies.getOrDefault(base.getName(), Optional.empty()); + if (!newPolicy.isPresent()) { + throw new ISE( "No restriction found on table [%s], but had %s before.", - this.base.getName(), - this.rowFilter + base.getName(), + policy ); } - if (newFilter.get().equals(TrueDimFilter.instance())) { - // The internal druid_system always has a TrueDimFilter, whic can be applied in conjunction with an external user's filter. - return this; - } else if (newFilter.get().equals(rowFilter)) { - // This likely occurs when we perform an authentication check for the same user more than once, which is not ideal. + if (newPolicy.get().hasNoRestriction()) { + // The internal druid_system could use NO_RESTRICTION policy. return this; } else { - throw new ISE("Incompatible restrictions on [%s]: %s and %s", this.base.getName(), rowFilter, newFilter.get()); + throw new ISE("Incompatible restrictions on [%s]: %s and %s", base.getName(), policy, newPolicy.get()); } } @Override public String toString() { - try { - return "RestrictedDataSource{" + - "base=" + base + - ", filter='" + rowFilter + '}'; - } - catch (Exception e) { - throw new RuntimeException(e); - } + return "RestrictedDataSource{" + + "base=" + base + + ", policy='" + policy + '}'; } @Override @@ -216,12 +198,12 @@ public boolean equals(Object o) return false; } RestrictedDataSource that = (RestrictedDataSource) o; - return Objects.equals(base, that.base) && Objects.equals(rowFilter, that.rowFilter); + return Objects.equals(base, that.base) && Objects.equals(policy, that.policy); } @Override public int hashCode() { - return Objects.hash(base, rowFilter); + return Objects.hash(base, policy); } } diff --git a/processing/src/main/java/org/apache/druid/query/TableDataSource.java b/processing/src/main/java/org/apache/druid/query/TableDataSource.java index 9680e2d061ca..cf6e1f00405d 100644 --- a/processing/src/main/java/org/apache/druid/query/TableDataSource.java +++ b/processing/src/main/java/org/apache/druid/query/TableDataSource.java @@ -23,11 +23,10 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeName; import com.google.common.base.Preconditions; -import org.apache.druid.error.DruidException; import org.apache.druid.java.util.common.IAE; -import org.apache.druid.query.filter.DimFilter; -import org.apache.druid.query.filter.TrueDimFilter; +import org.apache.druid.java.util.common.ISE; import org.apache.druid.query.planning.DataSourceAnalysis; +import org.apache.druid.query.policy.Policy; import org.apache.druid.segment.SegmentReference; import java.util.Collections; @@ -118,17 +117,17 @@ public DataSource withUpdatedDataSource(DataSource newSource) } @Override - public DataSource mapWithRestriction(Map> rowFilters, boolean enableStrictPolicyCheck) + public DataSource mapWithRestriction(Map> policyMap, boolean enableStrictPolicyCheck) { - if (!rowFilters.containsKey(this.name) && enableStrictPolicyCheck) { - throw DruidException.defensive("Need to check row-level policy for all tables, missing [%s]", this.name); + if (!policyMap.containsKey(name) && enableStrictPolicyCheck) { + throw new ISE("Need to check row-level policy for all tables, missing [%s]", name); } - Optional filter = rowFilters.getOrDefault(this.name, Optional.empty()); - if (!filter.isPresent()) { + Optional policy = policyMap.getOrDefault(name, Optional.empty()); + if (!policy.isPresent()) { // Skip adding restriction on table if there's no policy restriction found. return this; } - return RestrictedDataSource.create(this, filter.get().equals(TrueDimFilter.instance()) ? null : filter.get()); + return RestrictedDataSource.create(this, policy.get()); } @Override diff --git a/processing/src/main/java/org/apache/druid/query/metadata/metadata/SegmentMetadataQuery.java b/processing/src/main/java/org/apache/druid/query/metadata/metadata/SegmentMetadataQuery.java index 2d9620f22704..31c3055dafb9 100644 --- a/processing/src/main/java/org/apache/druid/query/metadata/metadata/SegmentMetadataQuery.java +++ b/processing/src/main/java/org/apache/druid/query/metadata/metadata/SegmentMetadataQuery.java @@ -224,7 +224,8 @@ public Query withQuerySegmentSpec(QuerySegmentSpec spec) @Override public Query withDataSource(DataSource dataSource) { - if (dataSource instanceof RestrictedDataSource && ((RestrictedDataSource) dataSource).allowAll()) { + if (dataSource instanceof RestrictedDataSource && ((RestrictedDataSource) dataSource).getPolicy() + .hasNoRestriction()) { return Druids.SegmentMetadataQueryBuilder.copy(this) .dataSource(((RestrictedDataSource) dataSource).getBase()) .build(); diff --git a/processing/src/main/java/org/apache/druid/query/policy/Policy.java b/processing/src/main/java/org/apache/druid/query/policy/Policy.java new file mode 100644 index 000000000000..8c0d0dfd1437 --- /dev/null +++ b/processing/src/main/java/org/apache/druid/query/policy/Policy.java @@ -0,0 +1,95 @@ +/* + * 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.query.policy; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.filter.TrueDimFilter; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * Represents a granular-level (e.x. row filter) restriction on read-table access. + */ +public class Policy +{ + public static final Policy NO_RESTRICTION = new Policy(null); + + @JsonProperty("rowFilter") + private final DimFilter rowFilter; + + @JsonCreator + Policy(@Nullable @JsonProperty("rowFilter") DimFilter rowFilter) + { + this.rowFilter = rowFilter; + } + + public static Policy fromRowFilter(@Nonnull DimFilter rowFilter) + { + return new Policy(rowFilter); + } + + @Nullable + public DimFilter getRowFilter() + { + return rowFilter; + } + + /** + * Returns true if the policy imposes no restrictions. + */ + public boolean hasNoRestriction() + { + if (NO_RESTRICTION.equals(this)) { + return true; + } else if (rowFilter == null || rowFilter instanceof TrueDimFilter) { + return true; + } + return false; + } + + @Override + public String toString() + { + return "Policy{" + "rowFilter=" + rowFilter + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Policy that = (Policy) o; + return Objects.equals(rowFilter, that.rowFilter); + } + + @Override + public int hashCode() + { + return Objects.hash(rowFilter); + } +} diff --git a/processing/src/test/java/org/apache/druid/query/DataSourceTest.java b/processing/src/test/java/org/apache/druid/query/DataSourceTest.java index aa83cdbf23d6..f2aead098530 100644 --- a/processing/src/test/java/org/apache/druid/query/DataSourceTest.java +++ b/processing/src/test/java/org/apache/druid/query/DataSourceTest.java @@ -26,10 +26,10 @@ import org.apache.druid.common.config.NullHandling; import org.apache.druid.query.aggregation.LongSumAggregatorFactory; import org.apache.druid.query.dimension.DefaultDimensionSpec; -import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.NullFilter; import org.apache.druid.query.filter.TrueDimFilter; import org.apache.druid.query.groupby.GroupByQuery; +import org.apache.druid.query.policy.Policy; import org.apache.druid.segment.TestHelper; import org.junit.Assert; import org.junit.Before; @@ -130,24 +130,25 @@ public void testMapWithRestriction() throws Exception TableDataSource table2 = TableDataSource.create("table2"); TableDataSource table3 = TableDataSource.create("table3"); UnionDataSource unionDataSource = new UnionDataSource(Lists.newArrayList(table1, table2, table3)); - ImmutableMap> restrictions = ImmutableMap.of( + ImmutableMap> restrictions = ImmutableMap.of( "table1", - Optional.of(TrueDimFilter.instance()), + Optional.of(Policy.NO_RESTRICTION), "table2", Optional.empty(), "table3", - Optional.of(new NullFilter( + Optional.of(Policy.fromRowFilter(new NullFilter( "some-column", null - )) + ))) ); Assert.assertEquals( - unionDataSource.mapWithRestriction(restrictions), + unionDataSource.mapWithRestriction(restrictions, true), new UnionDataSource(Lists.newArrayList( RestrictedDataSource.create(table1, null), table2, - RestrictedDataSource.create(table3, new NullFilter("some-column", null)) + RestrictedDataSource.create(table3, Policy.fromRowFilter(new NullFilter("some-column", null)) + ) )) ); } @@ -158,12 +159,15 @@ public void testMapWithRestrictionThrowsWhenMissingRestriction() throws Exceptio TableDataSource table1 = TableDataSource.create("table1"); TableDataSource table2 = TableDataSource.create("table2"); UnionDataSource unionDataSource = new UnionDataSource(Lists.newArrayList(table1, table2)); - ImmutableMap> restrictions = ImmutableMap.of( + ImmutableMap> restrictions = ImmutableMap.of( "table1", - Optional.of(TrueDimFilter.instance()) + Optional.of(Policy.fromRowFilter(TrueDimFilter.instance())) ); - Exception e = Assert.assertThrows(RuntimeException.class, () -> unionDataSource.mapWithRestriction(restrictions)); + Exception e = Assert.assertThrows( + RuntimeException.class, + () -> unionDataSource.mapWithRestriction(restrictions, true) + ); Assert.assertEquals(e.getMessage(), "Need to check row-level policy for all tables, missing [table2]"); } @@ -171,16 +175,17 @@ public void testMapWithRestrictionThrowsWhenMissingRestriction() throws Exceptio public void testMapWithRestrictionThrowsWithIncompatibleRestriction() throws Exception { RestrictedDataSource restrictedDataSource = RestrictedDataSource.create(TableDataSource.create("table1"), null); - ImmutableMap> restrictions = ImmutableMap.of( + ImmutableMap> restrictions = ImmutableMap.of( "table1", - Optional.of(new NullFilter("some-column", null)) + Optional.of(Policy.fromRowFilter(new NullFilter("some-column", null))) ); - Assert.assertThrows(RuntimeException.class, () -> restrictedDataSource.mapWithRestriction(restrictions)); - Assert.assertThrows(RuntimeException.class, () -> restrictedDataSource.mapWithRestriction(ImmutableMap.of())); + Assert.assertThrows(RuntimeException.class, () -> restrictedDataSource.mapWithRestriction(restrictions, true)); + Assert.assertThrows(RuntimeException.class, () -> restrictedDataSource.mapWithRestriction(restrictions, false)); + Assert.assertThrows(RuntimeException.class, () -> restrictedDataSource.mapWithRestriction(ImmutableMap.of(), true)); Assert.assertThrows( RuntimeException.class, - () -> restrictedDataSource.mapWithRestriction(ImmutableMap.of("table1", Optional.empty())) + () -> restrictedDataSource.mapWithRestriction(ImmutableMap.of("table1", Optional.empty()), true) ); } } diff --git a/processing/src/test/java/org/apache/druid/query/JoinDataSourceTest.java b/processing/src/test/java/org/apache/druid/query/JoinDataSourceTest.java index 95c9936c340a..b7def71060ca 100644 --- a/processing/src/test/java/org/apache/druid/query/JoinDataSourceTest.java +++ b/processing/src/test/java/org/apache/druid/query/JoinDataSourceTest.java @@ -30,6 +30,7 @@ import org.apache.druid.query.filter.InDimFilter; import org.apache.druid.query.filter.TrueDimFilter; import org.apache.druid.query.planning.DataSourceAnalysis; +import org.apache.druid.query.policy.Policy; import org.apache.druid.segment.TestHelper; import org.apache.druid.segment.column.ColumnType; import org.apache.druid.segment.join.JoinConditionAnalysis; @@ -508,7 +509,7 @@ public void testGetAnalysisWithRestrictedDS() JoinDataSource dataSource = JoinDataSource.create( RestrictedDataSource.create( new TableDataSource("table1"), - TrueDimFilter.instance() + Policy.NO_RESTRICTION ), new TableDataSource("table2"), "j.", diff --git a/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java b/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java index 6239729d20cd..559f7cd717d8 100644 --- a/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java +++ b/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java @@ -22,6 +22,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableList; import nl.jqno.equalsverifier.EqualsVerifier; +import org.apache.druid.java.util.common.IAE; +import org.apache.druid.query.filter.TrueDimFilter; +import org.apache.druid.query.policy.Policy; import org.apache.druid.segment.TestHelper; import org.junit.Assert; import org.junit.Rule; @@ -37,8 +40,21 @@ public class RestrictedDataSourceTest private final TableDataSource fooDataSource = new TableDataSource("foo"); private final TableDataSource barDataSource = new TableDataSource("bar"); - private final RestrictedDataSource restrictedFooDataSource = RestrictedDataSource.create(fooDataSource, null); - private final RestrictedDataSource restrictedBarDataSource = RestrictedDataSource.create(barDataSource, null); + private final RestrictedDataSource restrictedFooDataSource = RestrictedDataSource.create( + fooDataSource, + Policy.NO_RESTRICTION + ); + private final RestrictedDataSource restrictedBarDataSource = RestrictedDataSource.create( + barDataSource, + Policy.NO_RESTRICTION + ); + + @Test + public void test_creation_failWithNullPolicy() + { + IAE e = Assert.assertThrows(IAE.class, () -> RestrictedDataSource.create(fooDataSource, null)); + Assert.assertEquals(e.getMessage(), "Policy can't be null for RestrictedDataSource"); + } @Test public void test_getTableNames() @@ -130,20 +146,27 @@ public void test_deserialize_fromObject() throws Exception { final ObjectMapper jsonMapper = TestHelper.makeJsonMapper(); final RestrictedDataSource deserializedRestrictedDataSource = jsonMapper.readValue( - "{\"type\":\"restrict\",\"base\":{\"type\":\"table\",\"name\":\"foo\"},\"filter\":null}", + "{\"type\":\"restrict\",\"base\":{\"type\":\"table\",\"name\":\"foo\"},\"policy\":{\"rowFilter\":{\"type\":\"true\"}}}", RestrictedDataSource.class ); - Assert.assertEquals(restrictedFooDataSource, deserializedRestrictedDataSource); + Assert.assertEquals( + deserializedRestrictedDataSource, + RestrictedDataSource.create(fooDataSource, Policy.fromRowFilter(TrueDimFilter.instance())) + ); } + @Test public void test_serialize() throws Exception { final ObjectMapper jsonMapper = TestHelper.makeJsonMapper(); final String s = jsonMapper.writeValueAsString(restrictedFooDataSource); - Assert.assertEquals("{\"type\":\"restrict\",\"base\":{\"type\":\"table\",\"name\":\"foo\"},\"filter\":null}", s); + Assert.assertEquals( + "{\"type\":\"restrict\",\"base\":{\"type\":\"table\",\"name\":\"foo\"},\"policy\":{\"rowFilter\":null}}", + s + ); } @Test diff --git a/processing/src/test/java/org/apache/druid/query/metadata/SegmentMetadataQueryTest.java b/processing/src/test/java/org/apache/druid/query/metadata/SegmentMetadataQueryTest.java index 31ea774894ea..7eac7a0eaedd 100644 --- a/processing/src/test/java/org/apache/druid/query/metadata/SegmentMetadataQueryTest.java +++ b/processing/src/test/java/org/apache/druid/query/metadata/SegmentMetadataQueryTest.java @@ -52,14 +52,13 @@ import org.apache.druid.query.Result; import org.apache.druid.query.TableDataSource; import org.apache.druid.query.aggregation.AggregatorFactory; -import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.NullFilter; -import org.apache.druid.query.filter.TrueDimFilter; import org.apache.druid.query.metadata.metadata.AggregatorMergeStrategy; import org.apache.druid.query.metadata.metadata.ColumnAnalysis; import org.apache.druid.query.metadata.metadata.ListColumnIncluderator; import org.apache.druid.query.metadata.metadata.SegmentAnalysis; import org.apache.druid.query.metadata.metadata.SegmentMetadataQuery; +import org.apache.druid.query.policy.Policy; import org.apache.druid.query.spec.LegacySegmentSpec; import org.apache.druid.segment.IncrementalIndexSegment; import org.apache.druid.segment.QueryableIndex; @@ -336,21 +335,24 @@ public void testSegmentMetadataQuery() @Test public void testSegmentMetadataQueryWorksWithRestrictions() throws Exception { - ImmutableMap> noRestriction = ImmutableMap.of(DATASOURCE, Optional.empty()); - ImmutableMap> alwaysTrueRestriction = ImmutableMap.of(DATASOURCE, Optional.of( - TrueDimFilter.instance())); - ImmutableMap> withRestriction = ImmutableMap.of(DATASOURCE, Optional.of( - new NullFilter("some-column", null))); - List results1 = runner1.run(QueryPlus.wrap(testQuery.withPolicyRestrictions(noRestriction))) + ImmutableMap> noRestriction = ImmutableMap.of(DATASOURCE, Optional.empty()); + ImmutableMap> alwaysTrueRestriction = ImmutableMap.of(DATASOURCE, Optional.of( + Policy.NO_RESTRICTION)); + ImmutableMap> withRestriction = ImmutableMap.of( + DATASOURCE, + Optional.of(Policy.fromRowFilter( + new NullFilter("some-column", null))) + ); + List results1 = runner1.run(QueryPlus.wrap(testQuery.withPolicyRestrictions(noRestriction, true))) .toList(); - List results2 = runner1.run(QueryPlus.wrap(testQuery.withPolicyRestrictions(alwaysTrueRestriction))) + List results2 = runner1.run(QueryPlus.wrap(testQuery.withPolicyRestrictions(alwaysTrueRestriction, true))) .toList(); Assert.assertEquals(Collections.singletonList(expectedSegmentAnalysis1), results1); Assert.assertEquals(Collections.singletonList(expectedSegmentAnalysis1), results2); Assert.assertThrows( RuntimeException.class, - () -> runner1.run(QueryPlus.wrap(testQuery.withPolicyRestrictions(withRestriction))) + () -> runner1.run(QueryPlus.wrap(testQuery.withPolicyRestrictions(withRestriction, true))) ); } diff --git a/server/src/main/java/org/apache/druid/metadata/SQLMetadataStorageActionHandler.java b/server/src/main/java/org/apache/druid/metadata/SQLMetadataStorageActionHandler.java index 622191138e9f..910e2e1b1633 100644 --- a/server/src/main/java/org/apache/druid/metadata/SQLMetadataStorageActionHandler.java +++ b/server/src/main/java/org/apache/druid/metadata/SQLMetadataStorageActionHandler.java @@ -119,7 +119,7 @@ public SQLMetadataStorageActionHandler( this.connector = connector; //fully qualified references required below due to identical package names across project modules. //noinspection UnnecessaryFullyQualifiedName - this.jsonMapper = jsonMapper.copy().addMixIn(PasswordProvider.class,PasswordProviderRedactionMixIn.class); + this.jsonMapper = jsonMapper.copy().addMixIn(PasswordProvider.class, PasswordProviderRedactionMixIn.class); this.entryType = types.getEntryType(); this.statusType = types.getStatusType(); this.lockType = types.getLockType(); diff --git a/server/src/main/java/org/apache/druid/server/Policy.java b/server/src/main/java/org/apache/druid/server/Policy.java deleted file mode 100644 index ea36839eca00..000000000000 --- a/server/src/main/java/org/apache/druid/server/Policy.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.apache.druid.server; - -import org.apache.druid.query.filter.DimFilter; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.Optional; - -public class Policy -{ - public static final Policy NO_RESTRICTION = new Policy(null); - - private final Optional rowFilter; - - public Policy(@Nullable DimFilter rowFilter) { - this.rowFilter = Optional.ofNullable(rowFilter); - } - - public static Policy fromRowFilter(@Nonnull DimFilter rowFilter) { - return new Policy(rowFilter); - } - - public Optional getRowFilter() { - return rowFilter; - } -} 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 37012adc7948..224ae296a687 100644 --- a/server/src/main/java/org/apache/druid/server/QueryLifecycle.java +++ b/server/src/main/java/org/apache/druid/server/QueryLifecycle.java @@ -80,6 +80,9 @@ *

  • Execution ({@link #execute()}
  • *
  • Logging ({@link #emitLogsAndMetrics(Throwable, String, long)}
  • * + * Alternatively, if the request is already authenticated and authorized, just call + * {@link #runSimple(Query, AuthenticationResult, AuthorizationResult)}. + * *

    * This object is not thread-safe. */ @@ -136,11 +139,14 @@ public QueryLifecycle( * For callers who have already authorized their query, and where simplicity is desired over flexibility. This method * does it all in one call. Logs and metrics are emitted when the Sequence is either fully iterated or throws an * exception. + *

    + * The {@code state} transitions from NEW, to INITIALIZED, to AUTHORIZING, to AUTHORIZED, to EXECUTING, then DONE. * * @param query the query * @param authenticationResult authentication result indicating identity of the requester * @param authorizationResult authorization result of requester * @return results + * @throws DruidException if the given authorizationResult deny access, which indicates a bug */ public QueryResponse runSimple( final Query query, @@ -187,8 +193,11 @@ public void after(final boolean isDone, final Throwable thrown) /** * Initializes this object to execute a specific query. Does not actually execute the query. + *

    + * The {@code state} transitions from NEW, to INITIALIZED. * * @param baseQuery the query + * @throws DruidException if the current state is not NEW, which indicates a bug */ public void initialize(final Query baseQuery) { @@ -210,11 +219,18 @@ public void initialize(final Query baseQuery) } /** - * Authorize the query. Will return an Access object denoting whether the query is authorized or not. + * Returns {@link AuthorizationResult} based on {@code DRUID_AUTHENTICATION_RESULT} in the given request, base query + * would be transformed with restrictions on the AuthorizationResult. + *

    + * The {@code state} transitions from INITIALIZED, to AUTHORIZING, then to AUTHORIZED or UNAUTHORIZED. + *

    + * Note this won't throw exception if authorization deny access or impose policy restrictions. It is the caller's + * responsibility to throw exception on denial and impose policy restriction. * - * @param req HTTP request object of the request. If provided, the auth-related fields in the HTTP request - * will be automatically set. - * @return authorization result + * @param req HTTP request to be authorized. The auth-related fields in the HTTP request will be set. + * @return authorization result denoting whether the query is authorized or not, along with policy restrictions + * @throws IllegalStateException if the request was not authenticated + * @throws DruidException if the current state is not INITIALIZED, which indicates a bug */ public AuthorizationResult authorize(HttpServletRequest req) { @@ -240,12 +256,19 @@ public AuthorizationResult authorize(HttpServletRequest req) } /** - * Authorize the query using the authentication result. - * Will return an Access object denoting whether the query is authorized or not. + * Returns {@link AuthorizationResult} based on the given {@link AuthenticationResult}, base query would be + * transformed with restrictions on the AuthorizationResult. + *

    + * The {@code state} transitions from INITIALIZED, to AUTHORIZING, then to AUTHORIZED or UNAUTHORIZED. + *

    + * Note this won't throw exception if authorization deny access or impose policy restrictions. It is the caller's + * responsibility to throw exception on denial and impose policy restriction. + *

    * This method is to be used by the grpc-query-extension. * * @param authenticationResult authentication result indicating identity of the requester - * @return authorization result of requester + * @return authorization result denoting whether the query is authorized or not, along with policy restrictions. + * @throws DruidException if the current state is not INITIALIZED, which indicates a bug */ public AuthorizationResult authorize(AuthenticationResult authenticationResult) { @@ -299,23 +322,27 @@ private AuthorizationResult doAuthorize( transition(State.AUTHORIZING, State.AUTHORIZED); if (!authorizationResult.equals(AuthorizationResult.ALLOW_ALL)) { this.baseQuery = this.baseQuery.withPolicyRestrictions( - authorizationResult.getPolicyFilters(), + authorizationResult.getPolicy(), authConfig.isEnableStrictPolicyCheck() ); } } this.authenticationResult = authenticationResult; - return authorizationResult; } /** - * Execute the query. Can only be called if the query has been authorized. Note that query logs and metrics will - * not be emitted automatically when the Sequence is fully iterated. It is the caller's responsibility to call - * {@link #emitLogsAndMetrics(Throwable, String, long)} to emit logs and metrics. + * Executes the query. + *

    + * Note that query logs and metrics will not be emitted automatically when the Sequence is fully iterated withou. It + * is the caller's responsibility to call {@link #emitLogsAndMetrics(Throwable, String, long)} to emit logs and + * metrics. + *

    + * The {@code state} transitions from AUTHORIZED, to EXECUTING. * * @return result sequence and response context + * @throws DruidException if the current state is not AUTHORIZED, which indicates a bug */ public QueryResponse execute() { @@ -332,7 +359,11 @@ public QueryResponse execute() } /** - * Emit logs and metrics for this query. + * Emits logs and metrics for this query. + *

    + * The {@code state} transitions to DONE. The initial state can be anything, but it likely shouldn't be set to DONE. + *

    + * If {@code baseQuery} is null, likely because {@link #initialize(Query)} was never call, do nothing. * * @param e exception that occurred while processing this query * @param remoteAddress remote address, for logging; or null if unknown diff --git a/server/src/main/java/org/apache/druid/server/security/Access.java b/server/src/main/java/org/apache/druid/server/security/Access.java index 72f46a5f7a96..01dfd3a3a538 100644 --- a/server/src/main/java/org/apache/druid/server/security/Access.java +++ b/server/src/main/java/org/apache/druid/server/security/Access.java @@ -21,11 +21,15 @@ import com.google.common.base.Strings; import org.apache.druid.java.util.common.StringUtils; -import org.apache.druid.server.Policy; +import org.apache.druid.query.policy.Policy; import javax.annotation.Nullable; -import java.util.Objects; +import java.util.Optional; +/** + * Represents the outcome of verifying permissions to perform an {@link Action} on a {@link Resource}, along with any + * policy restrictions. + */ public class Access { public static final String DEFAULT_ERROR_MESSAGE = "Unauthorized"; @@ -36,9 +40,9 @@ public class Access private final boolean allowed; private final String message; - // A row-level policy filter on top of table-level read access. It should be empty if there are no policy restrictions + // A policy restriction on top of table-level read access. It should be empty if there are no policy restrictions // or if access is requested for an action other than reading the table. - private final Policy rowFilter; + private final Optional policy; /** * @deprecated use {@link #allow()} or {@link #deny(String)} instead @@ -46,29 +50,38 @@ public class Access @Deprecated public Access(boolean allowed) { - this(allowed, "", Policy.NO_RESTRICTION); + this(allowed, "", Optional.empty()); + } + + /** + * @deprecated use {@link #allow()} or {@link #deny(String)} instead + */ + @Deprecated + public Access(boolean allowed, String message) + { + this(allowed, message, Optional.empty()); } - Access(boolean allowed, String message, Policy rowFilter) + Access(boolean allowed, String message, Optional policy) { this.allowed = allowed; this.message = message; - this.rowFilter = rowFilter; + this.policy = policy; } public static Access allow() { - return new Access(true, "", Policy.NO_RESTRICTION); + return new Access(true, "", Optional.empty()); } public static Access deny(@Nullable String message) { - return new Access(false, Objects.isNull(message) ? "" : message, null); + return new Access(false, StringUtils.nullToEmptyNonDruidDataString(message), null); } public static Access allowWithRestriction(Policy policy) { - return new Access(true, "", policy); + return new Access(true, "", Optional.of(policy)); } public boolean isAllowed() @@ -76,9 +89,9 @@ public boolean isAllowed() return allowed; } - public Policy getPolicy() + public Optional getPolicy() { - return rowFilter; + return policy; } public String getMessage() @@ -89,9 +102,9 @@ public String getMessage() stringBuilder.append(", "); stringBuilder.append(message); } - if (allowed && rowFilter.getRowFilter().isPresent()) { + if (allowed && policy.isPresent()) { stringBuilder.append(", with restriction "); - stringBuilder.append(rowFilter.getRowFilter().get()); + stringBuilder.append(policy.get()); } return stringBuilder.toString(); } @@ -99,7 +112,7 @@ public String getMessage() @Override public String toString() { - return StringUtils.format("Allowed:%s, Message:%s, Row filter: %s", allowed, message, rowFilter); + return StringUtils.format("Allowed:%s, Message:%s, Policy: %s", allowed, message, policy); } } 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 84817b8322c0..2498607ad3d8 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 @@ -159,6 +159,10 @@ public boolean isEnableInputSourceSecurity() return enableInputSourceSecurity; } + /** + * When enabled, {@link org.apache.druid.server.QueryLifecycle} checks a policy entry in {@link AuthorizationResult#getPolicy()} + * for all tables in the query, and throws exception when there's no entry. + */ public boolean isEnableStrictPolicyCheck() { return enableStrictPolicyCheck; diff --git a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java index 7e008f6ce70a..95622a9a714e 100644 --- a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java +++ b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java @@ -20,18 +20,27 @@ package org.apache.druid.server.security; import com.google.common.collect.ImmutableMap; -import org.apache.druid.query.filter.DimFilter; -import org.apache.druid.server.Policy; +import org.apache.druid.query.policy.Policy; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Collections; -import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; - +import java.util.stream.Stream; + +/** + * Represents the outcoming of performing authorization check on required resource accesses on a query or http requests. + * It contains: + *

      + *
    • a boolean allow or deny access results for checking permissions on a list of resource actions. + *
    • a failure message if deny access. It's null when access is allowed. + *
    • a map of table name with optional {@link Policy} restriction. An empty value means there's no restriction + * enforced on the table. + *
    + */ public class AuthorizationResult { /** @@ -62,7 +71,7 @@ public class AuthorizationResult @Nullable private final String failureMessage; - private final Map policyFilters; + private final Map> policyRestrictions; @Nullable private final Set sqlResourceActions; @@ -73,14 +82,14 @@ public class AuthorizationResult AuthorizationResult( boolean isAllowed, @Nullable String failureMessage, - Map policyFilters, + Map> policyRestrictions, @Nullable Set sqlResourceActions, @Nullable Set allResourceActions ) { this.isAllowed = isAllowed; this.failureMessage = failureMessage; - this.policyFilters = policyFilters; + this.policyRestrictions = policyRestrictions; this.sqlResourceActions = sqlResourceActions; this.allResourceActions = allResourceActions; } @@ -90,9 +99,9 @@ public static AuthorizationResult deny(@Nonnull String failureMessage) return new AuthorizationResult(false, failureMessage, Collections.emptyMap(), null, null); } - public static AuthorizationResult allowWithRestriction(Map policyFilters) + public static AuthorizationResult allowWithRestriction(Map> policyRestrictions) { - return new AuthorizationResult(true, null, policyFilters, null, null); + return new AuthorizationResult(true, null, policyRestrictions, null, null); } public AuthorizationResult withResourceActions( @@ -109,33 +118,40 @@ public AuthorizationResult withResourceActions( ); } - public Optional getPermissionErrorMessage(boolean policyFilterNotPermitted) + /** + * Returns a permission error string if the AuthorizationResult doesn't permit all requried access. Otherwise, returns + * empty. When {@code policyRestrictionsNotPermitted} set to true, it requests unrestricted full access. The caller + * can use this method to retrieve the error string, and throw a {@link ForbiddenException} with the error message. + *

    + * It first checks if all permissions (e.x. {@link org.apache.druid.security.basic.authorization.entity.BasicAuthorizerPermission}) + * have been granted access. If not, returns the {@code failureMessage}. Then if {@code policyRestrictionsNotPermitted}, + * it checks for 'actual' policy restrictions (i.e. {@link Policy#hasNoRestriction} returns false). If 'actual' policy + * restrictions exist, returns {@link Access#DEFAULT_ERROR_MESSAGE}. + * + * @param policyRestrictionsNotPermitted true if policy restrictions are considered as not permitted + * @return optional permission error message + */ + public Optional getPermissionErrorMessage(boolean policyRestrictionsNotPermitted) { if (!isAllowed) { return Optional.of(Objects.requireNonNull(failureMessage)); } - if (policyFilterNotPermitted && policyFilters.values() - .stream() - .anyMatch(Policy.NO_RESTRICTION::equals)) { + if (policyRestrictionsNotPermitted && policyRestrictions.values() + .stream() + .flatMap(policy -> policy.isPresent() + ? Stream.of(policy.get()) + : Stream.empty()) // Can be replaced by Optional.stream after Java 11 + .anyMatch(Policy::hasNoRestriction)) { return Optional.of(Access.DEFAULT_ERROR_MESSAGE); } return Optional.empty(); } - public Map getPolicy() + public Map> getPolicy() { - return policyFilters; - } - - public Map> getPolicyFilters() - { - Map> filters = new HashMap<>(); - for (Map.Entry entry : policyFilters.entrySet()) { - filters.put(entry.getKey(), entry.getValue().getRowFilter()); - } - return filters; + return policyRestrictions; } @Nullable 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 21fb71565df2..7712d9c94e59 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 @@ -28,7 +28,7 @@ import org.apache.druid.audit.RequestInfo; import org.apache.druid.error.DruidException; import org.apache.druid.java.util.common.ISE; -import org.apache.druid.server.Policy; +import org.apache.druid.query.policy.Policy; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; @@ -37,6 +37,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; /** @@ -167,7 +168,7 @@ public static AuthorizationResult authorizeAllResourceActions( // this method returns on first failure, so only successful Access results are kept in the cache final Set resultCache = new HashSet<>(); - final Map policyFilters = new HashMap<>(); + final Map> policyFilters = new HashMap<>(); for (ResourceAction resourceAction : resourceActions) { if (resultCache.contains(resourceAction)) { @@ -185,9 +186,9 @@ public static AuthorizationResult authorizeAllResourceActions( if (resourceAction.getAction().equals(Action.READ) && RESTRICTION_APPLICABLE_RESOURCE_TYPES.contains(resourceAction.getResource().getType())) { policyFilters.put(resourceAction.getResource().getName(), access.getPolicy()); - } else if (access.getPolicy().getRowFilter().isPresent()) { + } else if (access.getPolicy().isPresent()) { throw DruidException.defensive( - "Row policy should only present when reading a table, but was present for %s", + "Policy should only present when reading a table, but was present for %s", resourceAction ); } else { diff --git a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java index 5fe6d75762e8..523d1269831a 100644 --- a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java +++ b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java @@ -42,6 +42,7 @@ import org.apache.druid.query.aggregation.CountAggregatorFactory; import org.apache.druid.query.filter.NullFilter; import org.apache.druid.query.filter.TrueDimFilter; +import org.apache.druid.query.policy.Policy; import org.apache.druid.query.timeseries.TimeseriesQuery; import org.apache.druid.server.log.RequestLogger; import org.apache.druid.server.security.Access; @@ -233,7 +234,7 @@ public void testAuthorizedWithNoPolicyRestriction() @Test public void testAuthorizedWithAlwaysTruePolicyRestriction() { - Policy alwaysTrueFilter = Policy.fromRowFilter(TrueDimFilter.instance()); + Policy alwaysTruePolicy = Policy.fromRowFilter(TrueDimFilter.instance()); EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); @@ -242,7 +243,7 @@ public void testAuthorizedWithAlwaysTruePolicyRestriction() new Resource(DATASOURCE, ResourceType.DATASOURCE), Action.READ )) - .andReturn(Access.allowWithRestriction(alwaysTrueFilter)).once(); + .andReturn(Access.allowWithRestriction(alwaysTruePolicy)).once(); EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) .andReturn(toolChest).times(2); EasyMock.expect(texasRanger.getQueryRunnerForIntervals(queryMatchDataSource(RestrictedDataSource.create( @@ -271,14 +272,14 @@ public void testAuthorizedWithAlwaysTruePolicyRestriction() lifecycle.runSimple( query, authenticationResult, - AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, alwaysTrueFilter)) + AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, Optional.of(alwaysTruePolicy))) ); } @Test public void testAuthorizedWithOnePolicyRestriction() { - Policy rowFilter = Policy.fromRowFilter(new NullFilter("some-column", null)); + Policy rowFilterPolicy = Policy.fromRowFilter(new NullFilter("some-column", null)); final TimeseriesQuery query = Druids.newTimeseriesQueryBuilder() .dataSource(DATASOURCE) .intervals(ImmutableList.of(Intervals.ETERNITY)) @@ -292,12 +293,12 @@ public void testAuthorizedWithOnePolicyRestriction() new Resource(DATASOURCE, ResourceType.DATASOURCE), Action.READ )) - .andReturn(Access.allowWithRestriction(rowFilter)).times(1); + .andReturn(Access.allowWithRestriction(rowFilterPolicy)).times(1); EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) .andReturn(toolChest).times(2); EasyMock.expect(texasRanger.getQueryRunnerForIntervals(queryMatchDataSource(RestrictedDataSource.create( TableDataSource.create(DATASOURCE), - rowFilter.getRowFilter().get() + rowFilterPolicy )), EasyMock.anyObject())) .andReturn(runner).times(2); EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).times(2); @@ -316,7 +317,7 @@ public void testAuthorizedWithOnePolicyRestriction() lifecycle.runSimple( query, authenticationResult, - AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, rowFilter)) + AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, Optional.of(rowFilterPolicy))) ); } @@ -331,8 +332,8 @@ public void testAuthorizedMissingPolicyRestriction() .build(); final TimeseriesQuery queryOnRestrictedDS = (TimeseriesQuery) query.withPolicyRestrictions(ImmutableMap.of( DATASOURCE, - Optional.of(TrueDimFilter.instance()) - )); + Optional.of(Policy.NO_RESTRICTION) + ), true); Assume.assumeTrue(queryOnRestrictedDS.getDataSource() instanceof RestrictedDataSource); EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())).andReturn(toolChest).anyTimes(); @@ -365,14 +366,14 @@ public void testAuthorizedMissingPolicyRestriction() @Test public void testAuthorizedMultiplePolicyRestrictions() { - Policy trueFilter = Policy.fromRowFilter(TrueDimFilter.instance()); - Policy columnFilter = Policy.fromRowFilter(new NullFilter("some-column", null)); - Policy columnFilter2 = Policy.fromRowFilter(new NullFilter("some-column2", null)); + Policy alwaysTruePolicy = Policy.fromRowFilter(TrueDimFilter.instance()); + Policy filterPolicy = Policy.fromRowFilter(new NullFilter("some-column", null)); + Policy filterPolicy2 = Policy.fromRowFilter(new NullFilter("some-column2", null)); final TimeseriesQuery query = Druids.newTimeseriesQueryBuilder() .dataSource(RestrictedDataSource.create( TableDataSource.create(DATASOURCE), - columnFilter.getRowFilter().get() + filterPolicy )) .intervals(ImmutableList.of(Intervals.ETERNITY)) .aggregators(new CountAggregatorFactory("chocula")) @@ -397,7 +398,7 @@ public void testAuthorizedMultiplePolicyRestrictions() lifecycle.runSimple( query, authenticationResult, - AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, columnFilter2)) + AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, Optional.of(filterPolicy2))) )); Assert.assertEquals( "Incompatible restrictions on [some_datasource]: some-column IS NULL and some-column2 IS NULL", @@ -405,11 +406,11 @@ public void testAuthorizedMultiplePolicyRestrictions() ); QueryLifecycle lifecycle2 = createLifecycle(authConfig); - // trueFilter is a compatible restriction + // alwaysTruePolicy is a compatible restriction lifecycle2.runSimple( query, authenticationResult, - AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, trueFilter)) + AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, Optional.of(alwaysTruePolicy))) ); lifecycle2 = createLifecycle(authConfig); @@ -417,7 +418,7 @@ public void testAuthorizedMultiplePolicyRestrictions() lifecycle2.runSimple( query, authenticationResult, - AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, columnFilter)) + AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, Optional.of(filterPolicy))) ); } From 51993548bb5f44a93661d4bf12a78d028d1e992c Mon Sep 17 00:00:00 2001 From: cecemei Date: Wed, 18 Dec 2024 20:07:15 -0800 Subject: [PATCH 09/32] fix test --- .../org/apache/druid/query/DataSourceTest.java | 14 ++++++++++---- .../apache/druid/server/QueryLifecycleTest.java | 17 +++++++++-------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/processing/src/test/java/org/apache/druid/query/DataSourceTest.java b/processing/src/test/java/org/apache/druid/query/DataSourceTest.java index f2aead098530..e2681d552855 100644 --- a/processing/src/test/java/org/apache/druid/query/DataSourceTest.java +++ b/processing/src/test/java/org/apache/druid/query/DataSourceTest.java @@ -78,10 +78,13 @@ public void testTableDataSource() throws IOException public void testRestrictedDataSource() throws IOException { DataSource dataSource = JSON_MAPPER.readValue( - "{\"type\":\"restrict\",\"base\":{\"type\":\"table\",\"name\":\"somedatasource\"},\"filter\":null}", + "{\"type\":\"restrict\",\"base\":{\"type\":\"table\",\"name\":\"somedatasource\"},\"policy\":{\"rowFilter\":null}}", DataSource.class ); - Assert.assertEquals(RestrictedDataSource.create(TableDataSource.create("somedatasource"), null), dataSource); + Assert.assertEquals( + RestrictedDataSource.create(TableDataSource.create("somedatasource"), Policy.NO_RESTRICTION), + dataSource + ); } @Test @@ -145,7 +148,7 @@ public void testMapWithRestriction() throws Exception Assert.assertEquals( unionDataSource.mapWithRestriction(restrictions, true), new UnionDataSource(Lists.newArrayList( - RestrictedDataSource.create(table1, null), + RestrictedDataSource.create(table1, Policy.NO_RESTRICTION), table2, RestrictedDataSource.create(table3, Policy.fromRowFilter(new NullFilter("some-column", null)) ) @@ -174,7 +177,10 @@ public void testMapWithRestrictionThrowsWhenMissingRestriction() throws Exceptio @Test public void testMapWithRestrictionThrowsWithIncompatibleRestriction() throws Exception { - RestrictedDataSource restrictedDataSource = RestrictedDataSource.create(TableDataSource.create("table1"), null); + RestrictedDataSource restrictedDataSource = RestrictedDataSource.create( + TableDataSource.create("table1"), + Policy.NO_RESTRICTION + ); ImmutableMap> restrictions = ImmutableMap.of( "table1", Optional.of(Policy.fromRowFilter(new NullFilter("some-column", null))) diff --git a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java index 523d1269831a..81a59c987725 100644 --- a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java +++ b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java @@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import org.apache.druid.error.DruidException; +import org.apache.druid.java.util.common.ISE; import org.apache.druid.java.util.common.Intervals; import org.apache.druid.java.util.common.guava.Sequences; import org.apache.druid.java.util.emitter.service.ServiceEmitter; @@ -192,7 +193,7 @@ public void testRunSimpleUnauthorized() } @Test - public void testAuthorizedWithNoPolicyRestriction() + public void testAuthorizedWithNoPolicyOnTable() { EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); @@ -202,7 +203,7 @@ public void testAuthorizedWithNoPolicyRestriction() new Resource(DATASOURCE, ResourceType.DATASOURCE), Action.READ )) - .andReturn(Access.allowWithRestriction(Policy.NO_RESTRICTION)).once(); + .andReturn(Access.allow()).once(); EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) .andReturn(toolChest).times(2); EasyMock.expect(texasRanger.getQueryRunnerForIntervals( @@ -248,7 +249,7 @@ public void testAuthorizedWithAlwaysTruePolicyRestriction() .andReturn(toolChest).times(2); EasyMock.expect(texasRanger.getQueryRunnerForIntervals(queryMatchDataSource(RestrictedDataSource.create( TableDataSource.create(DATASOURCE), - null + alwaysTruePolicy )), EasyMock.anyObject())) .andReturn(runner).times(2); EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).times(2); @@ -360,7 +361,7 @@ public void testAuthorizedMissingPolicyRestriction() authenticationResult, AuthorizationResult.allowWithRestriction(ImmutableMap.of()) )); - Assert.assertEquals("Missing row filter for table [some_datasource]", e2.getMessage()); + Assert.assertEquals("Missing policy check result for table [some_datasource]", e2.getMessage()); } @Test @@ -394,14 +395,14 @@ public void testAuthorizedMultiplePolicyRestrictions() .build(); QueryLifecycle lifecycle = createLifecycle(authConfig); - RuntimeException e = Assert.assertThrows(RuntimeException.class, () -> + ISE e = Assert.assertThrows(ISE.class, () -> lifecycle.runSimple( query, authenticationResult, AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, Optional.of(filterPolicy2))) )); Assert.assertEquals( - "Incompatible restrictions on [some_datasource]: some-column IS NULL and some-column2 IS NULL", + "Incompatible restrictions on [some_datasource]: Policy{rowFilter=some-column IS NULL} and Policy{rowFilter=some-column2 IS NULL}", e.getMessage() ); @@ -414,11 +415,11 @@ public void testAuthorizedMultiplePolicyRestrictions() ); lifecycle2 = createLifecycle(authConfig); - // the same filter, compatible + // no restriction, compatible lifecycle2.runSimple( query, authenticationResult, - AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, Optional.of(filterPolicy))) + AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, Optional.of(Policy.NO_RESTRICTION))) ); } From 390caac6354087939de92bbc15160ce825940f5a Mon Sep 17 00:00:00 2001 From: cecemei Date: Thu, 19 Dec 2024 10:33:12 -0800 Subject: [PATCH 10/32] remove VIEW from restricted applicable resource types, add tests for AuthorizationResult class, dart sql, msq sql, fix bug, added restricted data source to calcite test data --- .../controller/http/DartSqlResourceTest.java | 63 +++++++- .../apache/druid/msq/exec/MSQSelectTest.java | 15 ++ .../apache/druid/msq/test/MSQTestBase.java | 1 + .../java/org/apache/druid/query/Query.java | 20 ++- .../server/security/AuthorizationResult.java | 41 +++++- .../server/security/AuthorizationUtils.java | 3 +- .../AuthorizationResultTest.java | 135 ++++++++++++++++++ .../security/ForbiddenExceptionTest.java | 21 ++- .../druid/sql/calcite/CalciteQueryTest.java | 2 + .../sql/calcite/CalciteSelectQueryTest.java | 40 ++++++ .../druid/sql/calcite/util/CalciteTests.java | 47 +++--- .../sql/calcite/util/TestDataBuilder.java | 9 ++ 12 files changed, 366 insertions(+), 31 deletions(-) create mode 100644 server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java diff --git a/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/http/DartSqlResourceTest.java b/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/http/DartSqlResourceTest.java index 981f96fbe2a8..f1978bd8c39d 100644 --- a/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/http/DartSqlResourceTest.java +++ b/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/http/DartSqlResourceTest.java @@ -509,6 +509,61 @@ public void test_doPost_regularUser_forbidden() ); } + @Test + public void test_doPost_regularUser_restricted_throwsForbidden() + { + final MockAsyncContext asyncContext = new MockAsyncContext(); + final MockHttpServletResponse asyncResponse = new MockHttpServletResponse(); + asyncContext.response = asyncResponse; + + Mockito.when(httpServletRequest.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)) + .thenReturn(makeAuthenticationResult(REGULAR_USER_NAME)); + Mockito.when(httpServletRequest.startAsync()) + .thenReturn(asyncContext); + + final SqlQuery sqlQuery = new SqlQuery( + StringUtils.format("SELECT * FROM \"%s\"", CalciteTests.RESTRICTED_DATASOURCE), + ResultFormat.ARRAY, + false, + false, + false, + Collections.emptyMap(), + Collections.emptyList() + ); + + ForbiddenException e = Assertions.assertThrows( + ForbiddenException.class, + () -> sqlResource.doPost(sqlQuery, httpServletRequest) + ); + Assertions.assertEquals("Unauthorized", e.getMessage()); + } + + @Test + public void test_doPost_superUser_restricted_throwsServerError() + { + final MockAsyncContext asyncContext = new MockAsyncContext(); + final MockHttpServletResponse asyncResponse = new MockHttpServletResponse(); + asyncContext.response = asyncResponse; + + Mockito.when(httpServletRequest.getAttribute(AuthConfig.DRUID_AUTHENTICATION_RESULT)) + .thenReturn(makeAuthenticationResult(CalciteTests.TEST_SUPERUSER_NAME)); + Mockito.when(httpServletRequest.startAsync()) + .thenReturn(asyncContext); + + final SqlQuery sqlQuery = new SqlQuery( + StringUtils.format("SELECT * FROM \"%s\"", CalciteTests.RESTRICTED_DATASOURCE), + ResultFormat.ARRAY, + false, + false, + false, + Collections.emptyMap(), + Collections.emptyList() + ); + Assertions.assertNull(sqlResource.doPost(sqlQuery, httpServletRequest)); + // Super user can run a dart query, but we don't support it yet. + Assertions.assertEquals(Response.Status.INTERNAL_SERVER_ERROR, asyncResponse.getStatus()); + } + @Test public void test_doPost_regularUser_runtimeError() throws IOException { @@ -571,7 +626,9 @@ public void test_doPost_regularUser_fullReport() throws Exception final List> reportMaps = objectMapper.readValue( asyncResponse.baos.toByteArray(), - new TypeReference<>() {} + new TypeReference<>() + { + } ); Assertions.assertEquals(1, reportMaps.size()); @@ -610,7 +667,9 @@ public void test_doPost_regularUser_runtimeError_fullReport() throws Exception final List> reportMaps = objectMapper.readValue( asyncResponse.baos.toByteArray(), - new TypeReference<>() {} + new TypeReference<>() + { + } ); Assertions.assertEquals(1, reportMaps.size()); diff --git a/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQSelectTest.java b/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQSelectTest.java index 6b904e5f1495..bc424ec82dd8 100644 --- a/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQSelectTest.java +++ b/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/exec/MSQSelectTest.java @@ -77,6 +77,7 @@ import org.apache.druid.segment.join.JoinType; import org.apache.druid.segment.virtual.ExpressionVirtualColumn; import org.apache.druid.server.lookup.cache.LookupLoadingSpec; +import org.apache.druid.server.security.ForbiddenException; import org.apache.druid.sql.calcite.expression.DruidExpression; import org.apache.druid.sql.calcite.external.ExternalDataSource; import org.apache.druid.sql.calcite.filtration.Filtration; @@ -798,6 +799,20 @@ public void testSelectWithGroupByLimit(String contextName, Map c } + @MethodSource("data") + @ParameterizedTest(name = "{index}:with context {0}") + public void testSelectRestricted(String contextName, Map context) + { + testSelectQuery() + .setSql("select count(*) from druid.restrictedDatasource_m1_is_6") + .setQueryContext(context) + .setExpectedExecutionErrorMatcher(CoreMatchers.allOf( + CoreMatchers.instanceOf(ForbiddenException.class), + ThrowableMessageMatcher.hasMessage(CoreMatchers.containsString("Unauthorized")) + )) + .verifyExecutionError(); + } + @MethodSource("data") @ParameterizedTest(name = "{index}:with context {0}") public void testSelectLookup(String contextName, Map context) diff --git a/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/test/MSQTestBase.java b/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/test/MSQTestBase.java index 060f8499e3eb..0f7959682653 100644 --- a/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/test/MSQTestBase.java +++ b/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/test/MSQTestBase.java @@ -236,6 +236,7 @@ import static org.apache.druid.sql.calcite.util.CalciteTests.DATASOURCE1; import static org.apache.druid.sql.calcite.util.CalciteTests.DATASOURCE2; +import static org.apache.druid.sql.calcite.util.CalciteTests.RESTRICTED_DATASOURCE; import static org.apache.druid.sql.calcite.util.CalciteTests.WIKIPEDIA; import static org.apache.druid.sql.calcite.util.TestDataBuilder.ROWS1; import static org.apache.druid.sql.calcite.util.TestDataBuilder.ROWS2; diff --git a/processing/src/main/java/org/apache/druid/query/Query.java b/processing/src/main/java/org/apache/druid/query/Query.java index 328d6b1daaab..648bfe8daf91 100644 --- a/processing/src/main/java/org/apache/druid/query/Query.java +++ b/processing/src/main/java/org/apache/druid/query/Query.java @@ -244,9 +244,25 @@ default String getMostSpecificId() Query withDataSource(DataSource dataSource); - default Query withPolicyRestrictions(Map> restrictions, boolean enableStrictPolicyCheck) + /** + * Returns the query with an updated datasource based on the policy restrictions on tables. + *

    + * If this datasource contains no table, no changes should occur. If {@code enableStrictPolicyCheck}, every table must + * have an entry in the {@code policyMap}, the value could be {@code Optional.empty()} meaning no restriction is + * enforced on this table. + * + * @param policyMap a mapping of table names to policy restrictions, every table in the datasource tree must have an entry + * @param enableStrictPolicyCheck a boolean denoting that, every table should have an entry in the policies map. + * @return the updated datasource, with restrictions applied in the datasource tree + * @throws IllegalStateException in one of following conditions: + *

      + *
    • table doesn't exist in {@code policyMap} and {@code enableStrictPolicyCheck} + *
    • the policy the policyMap is not compatible with existing policy, see {@link RestrictedDataSource#mapWithRestriction(Map, boolean)} + *
    + */ + default Query withPolicyRestrictions(Map> policyMap, boolean enableStrictPolicyCheck) { - return this.withDataSource(this.getDataSource().mapWithRestriction(restrictions, enableStrictPolicyCheck)); + return this.withDataSource(this.getDataSource().mapWithRestriction(policyMap, enableStrictPolicyCheck)); } default Query optimizeForSegment(PerSegmentQueryOptimizationContext optimizationContext) diff --git a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java index 95622a9a714e..e62837f7fac9 100644 --- a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java +++ b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java @@ -142,7 +142,7 @@ public Optional getPermissionErrorMessage(boolean policyRestrictionsNotP .flatMap(policy -> policy.isPresent() ? Stream.of(policy.get()) : Stream.empty()) // Can be replaced by Optional.stream after Java 11 - .anyMatch(Policy::hasNoRestriction)) { + .anyMatch(p -> !p.hasNoRestriction())) { return Optional.of(Access.DEFAULT_ERROR_MESSAGE); } @@ -165,4 +165,43 @@ public Set getAllResourceActions() { return allResourceActions; } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AuthorizationResult that = (AuthorizationResult) o; + return Objects.equals(isAllowed, that.isAllowed) && + Objects.equals(failureMessage, that.failureMessage) && + Objects.equals(policyRestrictions, that.policyRestrictions) && + Objects.equals(sqlResourceActions, that.sqlResourceActions) && + Objects.equals(allResourceActions, that.allResourceActions); + } + + @Override + public int hashCode() + { + return Objects.hash(isAllowed, failureMessage, policyRestrictions, sqlResourceActions, allResourceActions); + } + + @Override + public String toString() + { + return "AuthorizationResult [isAllowed=" + + isAllowed + + ", failureMessage=" + + failureMessage + + ", policyRestrictions=" + + policyRestrictions + + ", sqlResourceActions=" + + sqlResourceActions + + ", allResourceActions=" + + allResourceActions + + "]"; + } } 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 b3621b439c0d..bab2c1809a9c 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 @@ -46,8 +46,7 @@ public class AuthorizationUtils { static final ImmutableSet RESTRICTION_APPLICABLE_RESOURCE_TYPES = ImmutableSet.of( - ResourceType.DATASOURCE, - ResourceType.VIEW + ResourceType.DATASOURCE ); /** diff --git a/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java b/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java new file mode 100644 index 000000000000..42afda89b11f --- /dev/null +++ b/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java @@ -0,0 +1,135 @@ +/* + * 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.initialization; + +import com.google.common.collect.ImmutableMap; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import nl.jqno.equalsverifier.EqualsVerifier; +import org.apache.druid.common.config.NullHandling; +import org.apache.druid.query.filter.EqualityFilter; +import org.apache.druid.query.filter.TrueDimFilter; +import org.apache.druid.query.policy.Policy; +import org.apache.druid.segment.column.ColumnType; +import org.apache.druid.server.security.AuthorizationResult; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +@RunWith(JUnitParamsRunner.class) +public class AuthorizationResultTest +{ + @Before + public void setUp() + { + NullHandling.initializeForTests(); + } + + @Test + public void testEquals() + { + EqualsVerifier.forClass(AuthorizationResult.class) + .usingGetClass() + .verify(); + } + + @Test + public void testToString() + { + AuthorizationResult result = AuthorizationResult.allowWithRestriction( + ImmutableMap.of( + "table1", + Optional.of(Policy.NO_RESTRICTION), + "table2", + Optional.of( + Policy.fromRowFilter(new EqualityFilter("column1", ColumnType.STRING, "val1", null))) + ) + ); + assertEquals( + "AuthorizationResult [isAllowed=true, failureMessage=null, policyRestrictions={table1=Optional[Policy{rowFilter=null}], table2=Optional[Policy{rowFilter=column1 = val1}]}, sqlResourceActions=null, allResourceActions=null]", + result.toString() + ); + } + + @Test + @Parameters({"true", "false"}) + public void testFailedAccess_withPermissionError(boolean policyRestrictionsNotPermitted) + { + AuthorizationResult result = AuthorizationResult.deny("this data source is not permitted"); + assertEquals( + Optional.of("this data source is not permitted"), + result.getPermissionErrorMessage(policyRestrictionsNotPermitted) + ); + } + + @Test + @Parameters({"true", "false"}) + public void testFullAccess_noPermissionError(boolean policyRestrictionsNotPermitted) + { + AuthorizationResult result = AuthorizationResult.allowWithRestriction(ImmutableMap.of()); + AuthorizationResult resultWithEmptyPolicy = AuthorizationResult.allowWithRestriction(ImmutableMap.of( + "table1", + Optional.empty() + )); + AuthorizationResult resultWithNoRestrictionPolicy = AuthorizationResult.allowWithRestriction(ImmutableMap.of( + "table1", + Optional.of(Policy.NO_RESTRICTION) + )); + AuthorizationResult resultWithTrueFilterRestrictionPolicy = AuthorizationResult.allowWithRestriction(ImmutableMap.of( + "table1", + Optional.of(Policy.fromRowFilter(TrueDimFilter.instance())) + )); + + assertEquals(Optional.empty(), result.getPermissionErrorMessage(policyRestrictionsNotPermitted)); + assertEquals(Optional.empty(), resultWithEmptyPolicy.getPermissionErrorMessage(policyRestrictionsNotPermitted)); + assertEquals( + Optional.empty(), + resultWithNoRestrictionPolicy.getPermissionErrorMessage(policyRestrictionsNotPermitted) + ); + assertEquals( + Optional.empty(), + resultWithTrueFilterRestrictionPolicy.getPermissionErrorMessage(policyRestrictionsNotPermitted) + ); + } + + @Test + @Parameters({ + "true, Optional[Unauthorized]", + "false, Optional.empty" + }) + public void testRestrictedAccess_noPermissionError(boolean policyRestrictionsNotPermitted, String error) + { + AuthorizationResult result = AuthorizationResult.allowWithRestriction(ImmutableMap.of( + "table1", + Optional.of(Policy.fromRowFilter(new EqualityFilter( + "col", + ColumnType.STRING, + "val1", + null + ))) + )); + assertEquals(error, result.getPermissionErrorMessage(policyRestrictionsNotPermitted).toString()); + } +} diff --git a/server/src/test/java/org/apache/druid/server/security/ForbiddenExceptionTest.java b/server/src/test/java/org/apache/druid/server/security/ForbiddenExceptionTest.java index a818603e91ad..c7586a3616f0 100644 --- a/server/src/test/java/org/apache/druid/server/security/ForbiddenExceptionTest.java +++ b/server/src/test/java/org/apache/druid/server/security/ForbiddenExceptionTest.java @@ -19,6 +19,7 @@ package org.apache.druid.server.security; +import org.apache.druid.query.policy.Policy; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -53,7 +54,8 @@ public void testSanitizeWithTransformFunctionReturningNull() @Test public void testSanitizeWithTransformFunctionReturningNewString() { - Mockito.when(trasformFunction.apply(ArgumentMatchers.eq(ERROR_MESSAGE_ORIGINAL))).thenReturn(ERROR_MESSAGE_TRANSFORMED); + Mockito.when(trasformFunction.apply(ArgumentMatchers.eq(ERROR_MESSAGE_ORIGINAL))) + .thenReturn(ERROR_MESSAGE_TRANSFORMED); ForbiddenException forbiddenException = new ForbiddenException(ERROR_MESSAGE_ORIGINAL); ForbiddenException actual = forbiddenException.sanitize(trasformFunction); Assert.assertNotNull(actual); @@ -68,17 +70,22 @@ public void testAccess() { Access access = Access.deny(null); Assert.assertFalse(access.isAllowed()); - Assert.assertEquals("Allowed:false, Message:, Row filter: Optional.empty", access.toString()); + Assert.assertEquals("Allowed:false, Message:, Policy: null", access.toString()); Assert.assertEquals(Access.DEFAULT_ERROR_MESSAGE, access.getMessage()); + access = Access.deny("oops"); + Assert.assertFalse(access.isAllowed()); + Assert.assertEquals("Allowed:false, Message:oops, Policy: null", access.toString()); + Assert.assertEquals("Unauthorized, oops", access.getMessage()); + access = Access.allow(); Assert.assertTrue(access.isAllowed()); - Assert.assertEquals("Allowed:true, Message:, Row filter: Optional.empty", access.toString()); + Assert.assertEquals("Allowed:true, Message:, Policy: Optional.empty", access.toString()); Assert.assertEquals("Authorized", access.getMessage()); - access = Access.deny("oops"); - Assert.assertFalse(access.isAllowed()); - Assert.assertEquals("Allowed:false, Message:oops, Row filter: Optional.empty", access.toString()); - Assert.assertEquals("Unauthorized, oops", access.getMessage()); + access = Access.allowWithRestriction(Policy.NO_RESTRICTION); + Assert.assertTrue(access.isAllowed()); + Assert.assertEquals("Allowed:true, Message:, Policy: Optional[Policy{rowFilter=null}]", access.toString()); + Assert.assertEquals("Authorized, with restriction Policy{rowFilter=null}", access.getMessage()); } } diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java index ed5fbe7c2069..d91d23525bb3 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java @@ -195,6 +195,7 @@ public void testInformationSchemaTables() .add(new Object[]{"druid", CalciteTests.DATASOURCE4, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.DATASOURCE5, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.DATASOURCE3, "TABLE", "NO", "NO"}) + .add(new Object[]{"druid", CalciteTests.RESTRICTED_DATASOURCE, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.SOME_DATASOURCE, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.SOMEXDATASOURCE, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.USERVISITDATASOURCE, "TABLE", "NO", "NO"}) @@ -234,6 +235,7 @@ public void testInformationSchemaTables() .add(new Object[]{"druid", CalciteTests.DATASOURCE1, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.DATASOURCE2, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.DATASOURCE4, "TABLE", "NO", "NO"}) + .add(new Object[]{"druid", CalciteTests.RESTRICTED_DATASOURCE, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.FORBIDDEN_DATASOURCE, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.DATASOURCE5, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.DATASOURCE3, "TABLE", "NO", "NO"}) diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteSelectQueryTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteSelectQueryTest.java index 7f11e07dcbda..487ab3a4bd4c 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteSelectQueryTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteSelectQueryTest.java @@ -1165,6 +1165,46 @@ public void testSelectStar() ); } + @Test + public void testCountRestrictedTable_shouldFilterOnPolicy() + { + testQuery( + PLANNER_CONFIG_DEFAULT, + "SELECT COUNT(*) FROM druid.restrictedDatasource_m1_is_6", + CalciteTests.SUPER_USER_AUTH_RESULT, + ImmutableList.of( + Druids.newTimeseriesQueryBuilder() + .dataSource(CalciteTests.RESTRICTED_DATASOURCE) + .intervals(querySegmentSpec(Filtration.eternity())) + .granularity(Granularities.ALL) + .aggregators(aggregators(new CountAggregatorFactory("a0"))) + .context(QUERY_CONTEXT_DEFAULT) + .build() + ), + ImmutableList.of( + new Object[]{6L} // superuser can see all records + ) + ); + + testQuery( + PLANNER_CONFIG_DEFAULT, + "SELECT COUNT(*) FROM druid.restrictedDatasource_m1_is_6", + CalciteTests.REGULAR_USER_AUTH_RESULT, + ImmutableList.of( + Druids.newTimeseriesQueryBuilder() + .dataSource(CalciteTests.RESTRICTED_DATASOURCE) + .intervals(querySegmentSpec(Filtration.eternity())) + .granularity(Granularities.ALL) + .aggregators(aggregators(new CountAggregatorFactory("a0"))) + .context(QUERY_CONTEXT_DEFAULT) + .build() + ), + ImmutableList.of( + new Object[]{1L} // regular user can only see 1 record based on the policy + ) + ); + } + @Test public void testSelectStarOnForbiddenTable() { 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 92c47958e6e7..f1a39640fcd2 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 @@ -53,6 +53,7 @@ import org.apache.druid.math.expr.ExprMacroTable; import org.apache.druid.query.QueryRunnerFactoryConglomerate; import org.apache.druid.query.QuerySegmentWalker; +import org.apache.druid.query.policy.Policy; import org.apache.druid.rpc.indexing.OverlordClient; import org.apache.druid.segment.join.JoinableFactory; import org.apache.druid.segment.join.JoinableFactoryWrapper; @@ -75,6 +76,7 @@ import org.apache.druid.server.security.NoopEscalator; import org.apache.druid.server.security.ResourceType; import org.apache.druid.sql.SqlStatementFactory; +import org.apache.druid.sql.calcite.BaseCalciteQueryTest; import org.apache.druid.sql.calcite.aggregation.SqlAggregationModule; import org.apache.druid.sql.calcite.planner.DruidOperatorTable; import org.apache.druid.sql.calcite.planner.PlannerConfig; @@ -117,6 +119,7 @@ public class CalciteTests public static final String ARRAYS_DATASOURCE = "arrays"; public static final String BROADCAST_DATASOURCE = "broadcast"; public static final String FORBIDDEN_DATASOURCE = "forbiddenDatasource"; + public static final String RESTRICTED_DATASOURCE = "restrictedDatasource_m1_is_6"; public static final String FORBIDDEN_DESTINATION = "forbiddenDestination"; public static final String SOME_DATASOURCE = "some_datasource"; public static final String SOME_DATSOURCE_ESCAPED = "some\\_datasource"; @@ -133,22 +136,27 @@ public class CalciteTests public static final String BENCHMARK_DATASOURCE = "benchmark_ds"; public static final String TEST_SUPERUSER_NAME = "testSuperuser"; + public static final Policy POLICY_NO_RESTRICTION_SUPERUSER = Policy.NO_RESTRICTION; + public static final Policy POLICY_RESTRICTION = Policy.fromRowFilter(BaseCalciteQueryTest.numericSelector("m1", "6")); public static final AuthorizerMapper TEST_AUTHORIZER_MAPPER = new AuthorizerMapper(null) { @Override public Authorizer getAuthorizer(String name) { return (authenticationResult, resource, action) -> { + boolean isRestrictedTable = resource.getName().equals(RESTRICTED_DATASOURCE); + if (TEST_SUPERUSER_NAME.equals(authenticationResult.getIdentity())) { - return Access.OK; + return isRestrictedTable ? Access.allowWithRestriction(POLICY_NO_RESTRICTION_SUPERUSER) : Access.OK; } switch (resource.getType()) { case ResourceType.DATASOURCE: - if (FORBIDDEN_DATASOURCE.equals(resource.getName())) { - return Access.DENIED; - } else { - return Access.OK; + switch (resource.getName()) { + case FORBIDDEN_DATASOURCE: + return Access.DENIED; + default: + return isRestrictedTable ? Access.allowWithRestriction(POLICY_RESTRICTION) : Access.OK; } case ResourceType.VIEW: if ("forbiddenView".equals(resource.getName())) { @@ -180,20 +188,22 @@ public Authorizer getAuthorizer(String name) public Authorizer getAuthorizer(String name) { return (authenticationResult, resource, action) -> { + boolean isRestrictedTable = resource.getName().equals(RESTRICTED_DATASOURCE); + if (TEST_SUPERUSER_NAME.equals(authenticationResult.getIdentity())) { - return Access.OK; + return isRestrictedTable ? Access.allowWithRestriction(POLICY_NO_RESTRICTION_SUPERUSER) : Access.OK; } switch (resource.getType()) { case ResourceType.DATASOURCE: if (FORBIDDEN_DATASOURCE.equals(resource.getName())) { - return new Access(false); + return Access.DENIED; } else { - return Access.OK; + return isRestrictedTable ? Access.allowWithRestriction(POLICY_RESTRICTION) : Access.OK; } case ResourceType.VIEW: if ("forbiddenView".equals(resource.getName())) { - return new Access(false); + return Access.DENIED; } else { return Access.OK; } @@ -201,7 +211,7 @@ public Authorizer getAuthorizer(String name) case ResourceType.EXTERNAL: return Access.OK; default: - return new Access(false); + return Access.DENIED; } }; } @@ -254,10 +264,10 @@ public AuthenticationResult createEscalatedAuthenticationResult() ); public static final Injector INJECTOR = QueryStackTests.defaultInjectorBuilder() - .addModule(new LookylooModule()) - .addModule(new SqlAggregationModule()) - .addModule(new CalciteTestOperatorModule()) - .build(); + .addModule(new LookylooModule()) + .addModule(new SqlAggregationModule()) + .addModule(new CalciteTestOperatorModule()) + .build(); private CalciteTests() { @@ -398,7 +408,8 @@ public static SystemSchema createMockSystemSchema( provider, NodeRole.COORDINATOR, "/simple/leader" - ) { + ) + { @Override public String findCurrentLeader() { @@ -406,7 +417,8 @@ public String findCurrentLeader() } }; - final OverlordClient overlordClient = new NoopOverlordClient() { + final OverlordClient overlordClient = new NoopOverlordClient() + { @Override public ListenableFuture findCurrentLeader() { @@ -481,7 +493,8 @@ public static DruidSchemaCatalog createMockRootSchema( conglomerate, walker, plannerConfig, - authorizerMapper); + authorizerMapper + ); } /** diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/util/TestDataBuilder.java b/sql/src/test/java/org/apache/druid/sql/calcite/util/TestDataBuilder.java index 383c740811e1..8093e36dfc38 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/util/TestDataBuilder.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/util/TestDataBuilder.java @@ -878,6 +878,15 @@ public static SpecificSegmentsQuerySegmentWalker createMockWalker( .build(), index2 ).add( + DataSegment.builder() + .dataSource(CalciteTests.RESTRICTED_DATASOURCE) + .interval(index1.getDataInterval()) + .version("1") + .shardSpec(new LinearShardSpec(0)) + .size(0) + .build(), + index1 + ).add( DataSegment.builder() .dataSource(CalciteTests.FORBIDDEN_DATASOURCE) .interval(forbiddenIndex.getDataInterval()) From e81a25db93a0feb8945055d4e657a8e37c600717 Mon Sep 17 00:00:00 2001 From: cecemei Date: Thu, 19 Dec 2024 15:57:31 -0800 Subject: [PATCH 11/32] add Permission enum in AuthorizationResult class, add TablePolicySecurityLevel enum in Policy class, updated a bunch of tests --- .../apache/druid/msq/test/MSQTestBase.java | 1 - .../org/apache/druid/query/DataSource.java | 23 +- .../java/org/apache/druid/query/Query.java | 21 +- .../druid/query/RestrictedDataSource.java | 15 +- .../apache/druid/query/TableDataSource.java | 17 +- .../org/apache/druid/query/policy/Policy.java | 40 ++ .../apache/druid/query/DataSourceTest.java | 114 ++++- .../metadata/SegmentMetadataQueryTest.java | 6 +- .../AbstractSegmentMetadataCache.java | 2 +- .../apache/druid/server/QueryLifecycle.java | 5 +- .../druid/server/security/AuthConfig.java | 28 +- .../server/security/AuthorizationResult.java | 97 +++-- .../server/security/AuthorizationUtils.java | 2 +- .../AuthorizationResultTest.java | 2 +- .../CoordinatorSegmentMetadataCacheTest.java | 4 +- .../druid/server/QueryLifecycleTest.java | 395 +++++++++++------- .../sql/calcite/planner/PlannerFactory.java | 2 +- .../sql/avatica/DruidAvaticaHandlerTest.java | 12 + .../sql/calcite/CalciteJoinQueryTest.java | 2 +- .../druid/sql/calcite/CalciteQueryTest.java | 2 +- .../BrokerSegmentMetadataCacheTest.java | 6 +- 21 files changed, 523 insertions(+), 273 deletions(-) diff --git a/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/test/MSQTestBase.java b/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/test/MSQTestBase.java index 0f7959682653..060f8499e3eb 100644 --- a/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/test/MSQTestBase.java +++ b/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/test/MSQTestBase.java @@ -236,7 +236,6 @@ import static org.apache.druid.sql.calcite.util.CalciteTests.DATASOURCE1; import static org.apache.druid.sql.calcite.util.CalciteTests.DATASOURCE2; -import static org.apache.druid.sql.calcite.util.CalciteTests.RESTRICTED_DATASOURCE; import static org.apache.druid.sql.calcite.util.CalciteTests.WIKIPEDIA; import static org.apache.druid.sql.calcite.util.TestDataBuilder.ROWS1; import static org.apache.druid.sql.calcite.util.TestDataBuilder.ROWS2; diff --git a/processing/src/main/java/org/apache/druid/query/DataSource.java b/processing/src/main/java/org/apache/druid/query/DataSource.java index 288d8abd33fc..040b0a5c6133 100644 --- a/processing/src/main/java/org/apache/druid/query/DataSource.java +++ b/processing/src/main/java/org/apache/druid/query/DataSource.java @@ -124,26 +124,23 @@ public interface DataSource DataSource withUpdatedDataSource(DataSource newSource); /** - * Returns an updated datasource based on the policy restrictions on tables. + * Returns the query with an updated datasource based on the policy restrictions on tables. *

    - * If this datasource contains no table, no changes should occur. If {@code enableStrictPolicyCheck}, every table must - * have an entry in the {@code policyMap}, the value could be {@code Optional.empty()} meaning no restriction is - * enforced on this table. + * If this datasource contains no table, no changes should occur. * - * @param policyMap a mapping of table names to policy restrictions, every table in the datasource tree must have an entry - * @param enableStrictPolicyCheck a boolean denoting that, every table should have an entry in the policies map. + * @param policyMap a mapping of table names to policy restrictions + * @param tablePolicySecurityLevel an enum denoting that, how strict we need to enforce the policy on tables * @return the updated datasource, with restrictions applied in the datasource tree - * @throws IllegalStateException in one of following conditions: - *

      - *
    • table doesn't exist in {@code policyMap} and {@code enableStrictPolicyCheck} - *
    • the policy the policyMap is not compatible with existing policy, see {@link RestrictedDataSource#mapWithRestriction(Map, boolean)} - *
    + * @throws IllegalStateException if {@code policyMap} is not compatible with {@code tablePolicySecurityLevel} */ - default DataSource mapWithRestriction(Map> policyMap, boolean enableStrictPolicyCheck) + default DataSource mapWithRestriction( + Map> policyMap, + Policy.TablePolicySecurityLevel tablePolicySecurityLevel + ) { List children = this.getChildren() .stream() - .map(child -> child.mapWithRestriction(policyMap, enableStrictPolicyCheck)) + .map(child -> child.mapWithRestriction(policyMap, tablePolicySecurityLevel)) .collect(Collectors.toList()); return this.withChildren(children); } diff --git a/processing/src/main/java/org/apache/druid/query/Query.java b/processing/src/main/java/org/apache/druid/query/Query.java index 648bfe8daf91..29086153196a 100644 --- a/processing/src/main/java/org/apache/druid/query/Query.java +++ b/processing/src/main/java/org/apache/druid/query/Query.java @@ -247,22 +247,19 @@ default String getMostSpecificId() /** * Returns the query with an updated datasource based on the policy restrictions on tables. *

    - * If this datasource contains no table, no changes should occur. If {@code enableStrictPolicyCheck}, every table must - * have an entry in the {@code policyMap}, the value could be {@code Optional.empty()} meaning no restriction is - * enforced on this table. + * If this datasource contains no table, no changes should occur. * - * @param policyMap a mapping of table names to policy restrictions, every table in the datasource tree must have an entry - * @param enableStrictPolicyCheck a boolean denoting that, every table should have an entry in the policies map. + * @param policyMap a mapping of table names to policy restrictions + * @param tablePolicySecurityLevel an enum denoting that, how strict we need to enforce the policy on tables * @return the updated datasource, with restrictions applied in the datasource tree - * @throws IllegalStateException in one of following conditions: - *

      - *
    • table doesn't exist in {@code policyMap} and {@code enableStrictPolicyCheck} - *
    • the policy the policyMap is not compatible with existing policy, see {@link RestrictedDataSource#mapWithRestriction(Map, boolean)} - *
    + * @throws IllegalStateException if {@code policyMap} is not compatible with {@code tablePolicySecurityLevel} */ - default Query withPolicyRestrictions(Map> policyMap, boolean enableStrictPolicyCheck) + default Query withPolicyRestrictions( + Map> policyMap, + Policy.TablePolicySecurityLevel tablePolicySecurityLevel + ) { - return this.withDataSource(this.getDataSource().mapWithRestriction(policyMap, enableStrictPolicyCheck)); + return this.withDataSource(this.getDataSource().mapWithRestriction(policyMap, tablePolicySecurityLevel)); } default Query optimizeForSegment(PerSegmentQueryOptimizationContext optimizationContext) diff --git a/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java b/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java index 542c8f6bb62e..8b63db737869 100644 --- a/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java +++ b/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java @@ -145,9 +145,13 @@ public DataSource withUpdatedDataSource(DataSource newSource) } @Override - public DataSource mapWithRestriction(Map> policies, boolean enableStrictPolicyCheck) + public DataSource mapWithRestriction( + Map> policies, + Policy.TablePolicySecurityLevel tablePolicySecurityLevel + ) { - if (!policies.containsKey(base.getName()) && enableStrictPolicyCheck) { + // This method always throws, since we should only put restrictions once. When query is being passed by druid-system, it should use SUPERUSER AuthorizationResults. + if (!policies.containsKey(base.getName())) { throw new ISE("Missing policy check result for table [%s]", base.getName()); } @@ -159,12 +163,7 @@ public DataSource mapWithRestriction(Map> policies, boo policy ); } - if (newPolicy.get().hasNoRestriction()) { - // The internal druid_system could use NO_RESTRICTION policy. - return this; - } else { - throw new ISE("Incompatible restrictions on [%s]: %s and %s", base.getName(), policy, newPolicy.get()); - } + throw new ISE("Multiple restrictions on [%s]: %s and %s", base.getName(), policy, newPolicy.get()); } @Override diff --git a/processing/src/main/java/org/apache/druid/query/TableDataSource.java b/processing/src/main/java/org/apache/druid/query/TableDataSource.java index cf6e1f00405d..b8dcba9060a5 100644 --- a/processing/src/main/java/org/apache/druid/query/TableDataSource.java +++ b/processing/src/main/java/org/apache/druid/query/TableDataSource.java @@ -117,15 +117,22 @@ public DataSource withUpdatedDataSource(DataSource newSource) } @Override - public DataSource mapWithRestriction(Map> policyMap, boolean enableStrictPolicyCheck) + public DataSource mapWithRestriction( + Map> policyMap, + Policy.TablePolicySecurityLevel tablePolicySecurityLevel + ) { - if (!policyMap.containsKey(name) && enableStrictPolicyCheck) { - throw new ISE("Need to check row-level policy for all tables, missing [%s]", name); + if (!policyMap.containsKey(name) && tablePolicySecurityLevel.policyMustBeCheckedOnAllTables()) { + throw new ISE("Need to check row-level policy for all tables missing [%s]", name); } Optional policy = policyMap.getOrDefault(name, Optional.empty()); if (!policy.isPresent()) { - // Skip adding restriction on table if there's no policy restriction found. - return this; + if (tablePolicySecurityLevel.policyMustBeCheckedAndExistOnAllTables()) { + throw new ISE("Every table must have a policy restriction attached missing [%s]", name); + } else { + // Skip adding restriction on table if there's no policy restriction found. + return this; + } } return RestrictedDataSource.create(this, policy.get()); } diff --git a/processing/src/main/java/org/apache/druid/query/policy/Policy.java b/processing/src/main/java/org/apache/druid/query/policy/Policy.java index 8c0d0dfd1437..c0d498a3d210 100644 --- a/processing/src/main/java/org/apache/druid/query/policy/Policy.java +++ b/processing/src/main/java/org/apache/druid/query/policy/Policy.java @@ -33,6 +33,46 @@ */ public class Policy { + /** + * Defines how strict we want to enforce the policy on tables during query execution process. + *
      + *
    1. {@code APPLY_WHEN_APPLICABLE}, the most basic level, restriction is applied whenever seen fit. + *
    2. {@code POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY}, every table must have been checked on the policy. + *
    3. {@code POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST}, every table must have a policy when requests come from external users. + *
    + */ + public enum TablePolicySecurityLevel + { + APPLY_WHEN_APPLICABLE(0), + POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY(1), + POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST(2); + + private final int securityLevel; + + TablePolicySecurityLevel(int securityLevel) + { + this.securityLevel = securityLevel; + } + + /** + * Returns true if the security level requires that, every table must have an entry in the policy map during query + * execution stage. + */ + public boolean policyMustBeCheckedOnAllTables() + { + return securityLevel >= 1; + } + + /** + * Returns true if the security level requires that, every table must have a policy during query execution stage, + * this means the table must have a non-empty value in the policy map. + */ + public boolean policyMustBeCheckedAndExistOnAllTables() + { + return securityLevel >= 2; + } + } + public static final Policy NO_RESTRICTION = new Policy(null); @JsonProperty("rowFilter") diff --git a/processing/src/test/java/org/apache/druid/query/DataSourceTest.java b/processing/src/test/java/org/apache/druid/query/DataSourceTest.java index e2681d552855..9fb098569474 100644 --- a/processing/src/test/java/org/apache/druid/query/DataSourceTest.java +++ b/processing/src/test/java/org/apache/druid/query/DataSourceTest.java @@ -23,7 +23,10 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; import org.apache.druid.common.config.NullHandling; +import org.apache.druid.java.util.common.ISE; import org.apache.druid.query.aggregation.LongSumAggregatorFactory; import org.apache.druid.query.dimension.DefaultDimensionSpec; import org.apache.druid.query.filter.NullFilter; @@ -34,10 +37,12 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; import java.io.IOException; import java.util.Optional; +@RunWith(JUnitParamsRunner.class) public class DataSourceTest { private static final ObjectMapper JSON_MAPPER = TestHelper.makeJsonMapper(); @@ -127,7 +132,12 @@ public void testUnionDataSource() throws Exception } @Test - public void testMapWithRestriction() throws Exception + @Parameters({ + "APPLY_WHEN_APPLICABLE", + "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY", + "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST" + }) + public void testMapWithRestriction(Policy.TablePolicySecurityLevel securityLevel) { TableDataSource table1 = TableDataSource.create("table1"); TableDataSource table2 = TableDataSource.create("table2"); @@ -137,7 +147,7 @@ public void testMapWithRestriction() throws Exception "table1", Optional.of(Policy.NO_RESTRICTION), "table2", - Optional.empty(), + Optional.of(Policy.NO_RESTRICTION), "table3", Optional.of(Policy.fromRowFilter(new NullFilter( "some-column", @@ -146,10 +156,10 @@ public void testMapWithRestriction() throws Exception ); Assert.assertEquals( - unionDataSource.mapWithRestriction(restrictions, true), + unionDataSource.mapWithRestriction(restrictions, securityLevel), new UnionDataSource(Lists.newArrayList( RestrictedDataSource.create(table1, Policy.NO_RESTRICTION), - table2, + RestrictedDataSource.create(table2, Policy.NO_RESTRICTION), RestrictedDataSource.create(table3, Policy.fromRowFilter(new NullFilter("some-column", null)) ) )) @@ -157,7 +167,15 @@ public void testMapWithRestriction() throws Exception } @Test - public void testMapWithRestrictionThrowsWhenMissingRestriction() throws Exception + @Parameters({ + "APPLY_WHEN_APPLICABLE, ", + "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY, Need to check row-level policy for all tables missing [table2]", + "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST, Need to check row-level policy for all tables missing [table2]" + }) + public void testMapWithRestriction_tableMissingRestriction( + Policy.TablePolicySecurityLevel securityLevel, + String error + ) { TableDataSource table1 = TableDataSource.create("table1"); TableDataSource table2 = TableDataSource.create("table2"); @@ -167,31 +185,93 @@ public void testMapWithRestrictionThrowsWhenMissingRestriction() throws Exceptio Optional.of(Policy.fromRowFilter(TrueDimFilter.instance())) ); - Exception e = Assert.assertThrows( - RuntimeException.class, - () -> unionDataSource.mapWithRestriction(restrictions, true) + if (error.isEmpty()) { + unionDataSource.mapWithRestriction(restrictions, securityLevel); + } else { + ISE e = Assert.assertThrows(ISE.class, () -> unionDataSource.mapWithRestriction(restrictions, securityLevel)); + Assert.assertEquals(e.getMessage(), error); + } + } + + @Test + @Parameters({ + "APPLY_WHEN_APPLICABLE, ", + "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY, ", + "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST, Every table must have a policy restriction attached missing [table2]" + }) + public void testMapWithRestriction_tableWithEmptyPolicy(Policy.TablePolicySecurityLevel securityLevel, String error) + { + TableDataSource table1 = TableDataSource.create("table1"); + TableDataSource table2 = TableDataSource.create("table2"); + UnionDataSource unionDataSource = new UnionDataSource(Lists.newArrayList(table1, table2)); + ImmutableMap> restrictions = ImmutableMap.of( + "table1", + Optional.of(Policy.NO_RESTRICTION), + "table2", + Optional.empty() ); - Assert.assertEquals(e.getMessage(), "Need to check row-level policy for all tables, missing [table2]"); + + if (error.isEmpty()) { + Assert.assertEquals( + unionDataSource.mapWithRestriction(restrictions, securityLevel), + new UnionDataSource(Lists.newArrayList(RestrictedDataSource.create(table1, Policy.NO_RESTRICTION), table2)) + ); + } else { + ISE e = Assert.assertThrows( + ISE.class, + () -> unionDataSource.mapWithRestriction( + restrictions, + securityLevel + ) + ); + Assert.assertEquals(e.getMessage(), error); + } } @Test - public void testMapWithRestrictionThrowsWithIncompatibleRestriction() throws Exception + @Parameters({ + "APPLY_WHEN_APPLICABLE", + "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY", + "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST" + }) + public void testMapWithRestriction_onRestrictedDataSource_alwaysThrows(Policy.TablePolicySecurityLevel securityLevel) { RestrictedDataSource restrictedDataSource = RestrictedDataSource.create( TableDataSource.create("table1"), Policy.NO_RESTRICTION ); - ImmutableMap> restrictions = ImmutableMap.of( + ImmutableMap> anotherRestrictions = ImmutableMap.of( "table1", Optional.of(Policy.fromRowFilter(new NullFilter("some-column", null))) ); + ImmutableMap> noRestrictions = ImmutableMap.of("table1", Optional.empty()); + ImmutableMap> emptyPolicyMap = ImmutableMap.of(); + + ISE e = Assert.assertThrows( + ISE.class, + () -> restrictedDataSource.mapWithRestriction(anotherRestrictions, securityLevel) + ); + Assert.assertEquals( + "Multiple restrictions on [table1]: Policy{rowFilter=null} and Policy{rowFilter=some-column IS NULL}", + e.getMessage() + ); - Assert.assertThrows(RuntimeException.class, () -> restrictedDataSource.mapWithRestriction(restrictions, true)); - Assert.assertThrows(RuntimeException.class, () -> restrictedDataSource.mapWithRestriction(restrictions, false)); - Assert.assertThrows(RuntimeException.class, () -> restrictedDataSource.mapWithRestriction(ImmutableMap.of(), true)); - Assert.assertThrows( - RuntimeException.class, - () -> restrictedDataSource.mapWithRestriction(ImmutableMap.of("table1", Optional.empty()), true) + ISE e2 = Assert.assertThrows( + ISE.class, + () -> restrictedDataSource.mapWithRestriction(noRestrictions, securityLevel) + ); + Assert.assertEquals( + "No restriction found on table [table1], but had Policy{rowFilter=null} before.", + e2.getMessage() + ); + + ISE e3 = Assert.assertThrows( + ISE.class, + () -> restrictedDataSource.mapWithRestriction(emptyPolicyMap, securityLevel) + ); + Assert.assertEquals( + "Missing policy check result for table [table1]", + e3.getMessage() ); } } diff --git a/processing/src/test/java/org/apache/druid/query/metadata/SegmentMetadataQueryTest.java b/processing/src/test/java/org/apache/druid/query/metadata/SegmentMetadataQueryTest.java index 7eac7a0eaedd..1e5160e2ed83 100644 --- a/processing/src/test/java/org/apache/druid/query/metadata/SegmentMetadataQueryTest.java +++ b/processing/src/test/java/org/apache/druid/query/metadata/SegmentMetadataQueryTest.java @@ -343,16 +343,16 @@ public void testSegmentMetadataQueryWorksWithRestrictions() throws Exception Optional.of(Policy.fromRowFilter( new NullFilter("some-column", null))) ); - List results1 = runner1.run(QueryPlus.wrap(testQuery.withPolicyRestrictions(noRestriction, true))) + List results1 = runner1.run(QueryPlus.wrap(testQuery.withPolicyRestrictions(noRestriction, Policy.TablePolicySecurityLevel.POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST))) .toList(); - List results2 = runner1.run(QueryPlus.wrap(testQuery.withPolicyRestrictions(alwaysTrueRestriction, true))) + List results2 = runner1.run(QueryPlus.wrap(testQuery.withPolicyRestrictions(alwaysTrueRestriction, Policy.TablePolicySecurityLevel.POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST))) .toList(); Assert.assertEquals(Collections.singletonList(expectedSegmentAnalysis1), results1); Assert.assertEquals(Collections.singletonList(expectedSegmentAnalysis1), results2); Assert.assertThrows( RuntimeException.class, - () -> runner1.run(QueryPlus.wrap(testQuery.withPolicyRestrictions(withRestriction, true))) + () -> runner1.run(QueryPlus.wrap(testQuery.withPolicyRestrictions(withRestriction, Policy.TablePolicySecurityLevel.POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST))) ); } diff --git a/server/src/main/java/org/apache/druid/segment/metadata/AbstractSegmentMetadataCache.java b/server/src/main/java/org/apache/druid/segment/metadata/AbstractSegmentMetadataCache.java index bf96eec0114c..49277c2ff19e 100644 --- a/server/src/main/java/org/apache/druid/segment/metadata/AbstractSegmentMetadataCache.java +++ b/server/src/main/java/org/apache/druid/segment/metadata/AbstractSegmentMetadataCache.java @@ -975,7 +975,7 @@ public Sequence runSegmentMetadataQuery( return queryLifecycleFactory .factorize() - .runSimple(segmentMetadataQuery, escalator.createEscalatedAuthenticationResult(), AuthorizationResult.ALLOW_ALL) + .runSimple(segmentMetadataQuery, escalator.createEscalatedAuthenticationResult(), AuthorizationResult.SUPERUSER) .getResults(); } 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 224ae296a687..bf627fb08a1b 100644 --- a/server/src/main/java/org/apache/druid/server/QueryLifecycle.java +++ b/server/src/main/java/org/apache/druid/server/QueryLifecycle.java @@ -320,10 +320,11 @@ private AuthorizationResult doAuthorize( transition(State.AUTHORIZING, State.UNAUTHORIZED); } else { transition(State.AUTHORIZING, State.AUTHORIZED); - if (!authorizationResult.equals(AuthorizationResult.ALLOW_ALL)) { + if (!AuthorizationResult.SUPERUSER.equals(authorizationResult)) { + // Unless this request comes from superuser or druid-internal, we need to map the query with restrictions. this.baseQuery = this.baseQuery.withPolicyRestrictions( authorizationResult.getPolicy(), - authConfig.isEnableStrictPolicyCheck() + authConfig.getTablePolicySecurityLevel() ); } } 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 2498607ad3d8..2ebc71fce749 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 @@ -23,11 +23,13 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.collect.ImmutableSet; import org.apache.druid.query.QueryContexts; +import org.apache.druid.query.policy.Policy; import org.apache.druid.utils.CollectionUtils; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; public class AuthConfig @@ -63,7 +65,7 @@ public class AuthConfig public AuthConfig() { - this(null, null, null, false, false, null, null, false, false); + this(null, null, null, false, false, null, null, false, Policy.TablePolicySecurityLevel.APPLY_WHEN_APPLICABLE); } @JsonProperty @@ -101,7 +103,7 @@ public AuthConfig() private final boolean enableInputSourceSecurity; @JsonProperty - private final boolean enableStrictPolicyCheck; + private final Policy.TablePolicySecurityLevel tablePolicySecurityLevel; @JsonCreator public AuthConfig( @@ -113,7 +115,7 @@ public AuthConfig( @JsonProperty("unsecuredContextKeys") Set unsecuredContextKeys, @JsonProperty("securedContextKeys") Set securedContextKeys, @JsonProperty("enableInputSourceSecurity") boolean enableInputSourceSecurity, - @JsonProperty("enableStrictPolicyCheck") boolean enableStrictPolicyCheck + @JsonProperty("tablePolicySecurityLevel") Policy.TablePolicySecurityLevel tablePolicySecurityLevel ) { this.authenticatorChain = authenticatorChain; @@ -126,7 +128,7 @@ public AuthConfig( : unsecuredContextKeys; this.securedContextKeys = securedContextKeys; this.enableInputSourceSecurity = enableInputSourceSecurity; - this.enableStrictPolicyCheck = enableStrictPolicyCheck; + this.tablePolicySecurityLevel = tablePolicySecurityLevel; } public List getAuthenticatorChain() @@ -163,9 +165,9 @@ public boolean isEnableInputSourceSecurity() * When enabled, {@link org.apache.druid.server.QueryLifecycle} checks a policy entry in {@link AuthorizationResult#getPolicy()} * for all tables in the query, and throws exception when there's no entry. */ - public boolean isEnableStrictPolicyCheck() + public Policy.TablePolicySecurityLevel getTablePolicySecurityLevel() { - return enableStrictPolicyCheck; + return tablePolicySecurityLevel; } /** @@ -216,7 +218,7 @@ public boolean equals(Object o) && Objects.equals(unsecuredContextKeys, that.unsecuredContextKeys) && Objects.equals(securedContextKeys, that.securedContextKeys) && Objects.equals(enableInputSourceSecurity, that.enableInputSourceSecurity) - && Objects.equals(enableStrictPolicyCheck, that.enableStrictPolicyCheck); + && Objects.equals(tablePolicySecurityLevel, that.tablePolicySecurityLevel); } @Override @@ -231,7 +233,7 @@ public int hashCode() unsecuredContextKeys, securedContextKeys, enableInputSourceSecurity, - enableStrictPolicyCheck + tablePolicySecurityLevel ); } @@ -247,7 +249,7 @@ public String toString() ", unsecuredContextKeys=" + unsecuredContextKeys + ", securedContextKeys=" + securedContextKeys + ", enableInputSourceSecurity=" + enableInputSourceSecurity + - ", enableStrictPolicyCheck=" + enableStrictPolicyCheck + + ", tablePolicySecurityLevel=" + tablePolicySecurityLevel + '}'; } @@ -269,7 +271,7 @@ public static class Builder private Set unsecuredContextKeys; private Set securedContextKeys; private boolean enableInputSourceSecurity; - private boolean enableStrictPolicyCheck; + private Policy.TablePolicySecurityLevel tablePolicySecurityLevel; public Builder setAuthenticatorChain(List authenticatorChain) { @@ -319,9 +321,9 @@ public Builder setEnableInputSourceSecurity(boolean enableInputSourceSecurity) return this; } - public Builder setEnableStrictPolicyCheck(boolean enableStrictPolicyCheck) + public Builder setTablePolicySecurityLevel(Policy.TablePolicySecurityLevel tablePolicySecurityLevel) { - this.enableStrictPolicyCheck = enableStrictPolicyCheck; + this.tablePolicySecurityLevel = tablePolicySecurityLevel; return this; } @@ -336,7 +338,7 @@ public AuthConfig build() unsecuredContextKeys, securedContextKeys, enableInputSourceSecurity, - enableStrictPolicyCheck + Optional.ofNullable(tablePolicySecurityLevel).orElse(Policy.TablePolicySecurityLevel.APPLY_WHEN_APPLICABLE) ); } } diff --git a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java index e62837f7fac9..79f0f5e283f2 100644 --- a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java +++ b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java @@ -20,6 +20,7 @@ package org.apache.druid.server.security; import com.google.common.collect.ImmutableMap; +import org.apache.druid.error.DruidException; import org.apache.druid.query.policy.Policy; import javax.annotation.Nonnull; @@ -44,11 +45,22 @@ public class AuthorizationResult { /** - * Provides unrestricted access to all resources. This should be limited to Druid internal systems or superusers, - * except in cases where ACL considerations are not a priority. + * Provides unrestricted superuser access to all resources. This should be limited to Druid internal systems or + * superusers, except in cases where ACL considerations are not a priority. */ - public static final AuthorizationResult ALLOW_ALL = new AuthorizationResult( - true, + public static final AuthorizationResult SUPERUSER = new AuthorizationResult( + PERMISSION.SUPERUSER, + null, + Collections.emptyMap(), + null, + null + ); + + /** + * Provides access with no restrictions to all resources. + */ + public static final AuthorizationResult ALLOW_NO_RESTRICTION = new AuthorizationResult( + PERMISSION.ALLOW_NO_RESTRICTION, null, Collections.emptyMap(), null, @@ -59,14 +71,22 @@ public class AuthorizationResult * Provides a default deny access result. */ public static final AuthorizationResult DENY = new AuthorizationResult( - false, + PERMISSION.DENY, Access.DENIED.getMessage(), Collections.emptyMap(), null, null ); - private final boolean isAllowed; + enum PERMISSION + { + SUPERUSER, + ALLOW_NO_RESTRICTION, + ALLOW_WITH_RESTRICTION, + DENY + } + + private final PERMISSION permission; @Nullable private final String failureMessage; @@ -80,28 +100,44 @@ public class AuthorizationResult private final Set allResourceActions; AuthorizationResult( - boolean isAllowed, + PERMISSION permission, @Nullable String failureMessage, Map> policyRestrictions, @Nullable Set sqlResourceActions, @Nullable Set allResourceActions ) { - this.isAllowed = isAllowed; + this.permission = permission; this.failureMessage = failureMessage; this.policyRestrictions = policyRestrictions; this.sqlResourceActions = sqlResourceActions; this.allResourceActions = allResourceActions; + + // sanity check + if (failureMessage != null && !PERMISSION.DENY.equals(permission)) { + throw DruidException.defensive("Failure message should only be set for DENY permission"); + } else if (PERMISSION.DENY.equals(permission) && failureMessage == null) { + throw DruidException.defensive("Failure message must be set for DENY permission"); + } + + if (!policyRestrictions.isEmpty() && !PERMISSION.ALLOW_WITH_RESTRICTION.equals(permission)) { + throw DruidException.defensive("Policy restrictions should only be set for ALLOW_WITH_RESTRICTION permission"); + } else if (PERMISSION.ALLOW_WITH_RESTRICTION.equals(permission) && policyRestrictions.isEmpty()) { + throw DruidException.defensive("Policy restrictions must be set for ALLOW_WITH_RESTRICTION permission"); + } } public static AuthorizationResult deny(@Nonnull String failureMessage) { - return new AuthorizationResult(false, failureMessage, Collections.emptyMap(), null, null); + return new AuthorizationResult(PERMISSION.DENY, failureMessage, Collections.emptyMap(), null, null); } public static AuthorizationResult allowWithRestriction(Map> policyRestrictions) { - return new AuthorizationResult(true, null, policyRestrictions, null, null); + if (policyRestrictions.isEmpty()) { + return ALLOW_NO_RESTRICTION; + } + return new AuthorizationResult(PERMISSION.ALLOW_WITH_RESTRICTION, null, policyRestrictions, null, null); } public AuthorizationResult withResourceActions( @@ -110,7 +146,7 @@ public AuthorizationResult withResourceActions( ) { return new AuthorizationResult( - isAllowed, + permission, failureMessage, ImmutableMap.copyOf(getPolicy()), sqlResourceActions, @@ -133,20 +169,25 @@ public AuthorizationResult withResourceActions( */ public Optional getPermissionErrorMessage(boolean policyRestrictionsNotPermitted) { - if (!isAllowed) { - return Optional.of(Objects.requireNonNull(failureMessage)); - } - - if (policyRestrictionsNotPermitted && policyRestrictions.values() - .stream() - .flatMap(policy -> policy.isPresent() - ? Stream.of(policy.get()) - : Stream.empty()) // Can be replaced by Optional.stream after Java 11 - .anyMatch(p -> !p.hasNoRestriction())) { - return Optional.of(Access.DEFAULT_ERROR_MESSAGE); + switch (permission) { + case SUPERUSER: + case ALLOW_NO_RESTRICTION: + return Optional.empty(); + case DENY: + return Optional.of(Objects.requireNonNull(failureMessage)); + case ALLOW_WITH_RESTRICTION: + if (policyRestrictionsNotPermitted && policyRestrictions.values() + .stream() + .flatMap(policy -> policy.isPresent() + ? Stream.of(policy.get()) + : Stream.empty()) // Can be replaced by Optional.stream after Java 11 + .anyMatch(p -> !p.hasNoRestriction())) { + return Optional.of(Access.DEFAULT_ERROR_MESSAGE); + } + return Optional.empty(); + default: + throw DruidException.defensive("unreachable"); } - - return Optional.empty(); } public Map> getPolicy() @@ -176,7 +217,7 @@ public boolean equals(Object o) return false; } AuthorizationResult that = (AuthorizationResult) o; - return Objects.equals(isAllowed, that.isAllowed) && + return Objects.equals(permission, that.permission) && Objects.equals(failureMessage, that.failureMessage) && Objects.equals(policyRestrictions, that.policyRestrictions) && Objects.equals(sqlResourceActions, that.sqlResourceActions) && @@ -186,14 +227,14 @@ public boolean equals(Object o) @Override public int hashCode() { - return Objects.hash(isAllowed, failureMessage, policyRestrictions, sqlResourceActions, allResourceActions); + return Objects.hash(permission, failureMessage, policyRestrictions, sqlResourceActions, allResourceActions); } @Override public String toString() { - return "AuthorizationResult [isAllowed=" - + isAllowed + return "AuthorizationResult [permission=" + + permission + ", failureMessage=" + failureMessage + ", policyRestrictions=" 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 bab2c1809a9c..102eb657e422 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 @@ -219,7 +219,7 @@ public static AuthorizationResult authorizeAllResourceActions( ) { if (request.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH) != null) { - return AuthorizationResult.ALLOW_ALL; + return AuthorizationResult.SUPERUSER; } if (request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED) != null) { diff --git a/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java b/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java index 42afda89b11f..bcdfb23350cc 100644 --- a/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java +++ b/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java @@ -68,7 +68,7 @@ public void testToString() ) ); assertEquals( - "AuthorizationResult [isAllowed=true, failureMessage=null, policyRestrictions={table1=Optional[Policy{rowFilter=null}], table2=Optional[Policy{rowFilter=column1 = val1}]}, sqlResourceActions=null, allResourceActions=null]", + "AuthorizationResult [permission=ALLOW_WITH_RESTRICTION, failureMessage=null, policyRestrictions={table1=Optional[Policy{rowFilter=null}], table2=Optional[Policy{rowFilter=column1 = val1}]}, sqlResourceActions=null, allResourceActions=null]", result.toString() ); } diff --git a/server/src/test/java/org/apache/druid/segment/metadata/CoordinatorSegmentMetadataCacheTest.java b/server/src/test/java/org/apache/druid/segment/metadata/CoordinatorSegmentMetadataCacheTest.java index edbf3eec0e6f..8ec08b56d1d3 100644 --- a/server/src/test/java/org/apache/druid/segment/metadata/CoordinatorSegmentMetadataCacheTest.java +++ b/server/src/test/java/org/apache/druid/segment/metadata/CoordinatorSegmentMetadataCacheTest.java @@ -1067,7 +1067,7 @@ public void testRunSegmentMetadataQueryWithContext() throws Exception EasyMock.expect(lifecycleMock.runSimple( expectedMetadataQuery, AllowAllAuthenticator.ALLOW_ALL_RESULT, - AuthorizationResult.ALLOW_ALL + AuthorizationResult.SUPERUSER )) .andReturn(QueryResponse.withEmptyContext(Sequences.empty())); @@ -2306,7 +2306,7 @@ public void testTombstoneSegmentIsNotRefreshed() throws IOException EasyMock.expect(lifecycleMock.runSimple( expectedMetadataQuery, AllowAllAuthenticator.ALLOW_ALL_RESULT, - AuthorizationResult.ALLOW_ALL + AuthorizationResult.SUPERUSER )) .andReturn(QueryResponse.withEmptyContext(Sequences.empty())).once(); diff --git a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java index 81a59c987725..1c0e5601ebde 100644 --- a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java +++ b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java @@ -22,6 +22,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; import org.apache.druid.error.DruidException; import org.apache.druid.java.util.common.ISE; import org.apache.druid.java.util.common.Intervals; @@ -41,8 +43,8 @@ import org.apache.druid.query.RestrictedDataSource; import org.apache.druid.query.TableDataSource; import org.apache.druid.query.aggregation.CountAggregatorFactory; +import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.NullFilter; -import org.apache.druid.query.filter.TrueDimFilter; import org.apache.druid.query.policy.Policy; import org.apache.druid.query.timeseries.TimeseriesQuery; import org.apache.druid.server.log.RequestLogger; @@ -59,23 +61,26 @@ import org.easymock.IArgumentMatcher; import org.junit.After; import org.junit.Assert; -import org.junit.Assume; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; import java.util.Optional; +@RunWith(JUnitParamsRunner.class) public class QueryLifecycleTest { private static final String DATASOURCE = "some_datasource"; private static final String IDENTITY = "some_identity"; private static final String AUTHORIZER = "some_authorizer"; + private static final Resource RESOURCE = new Resource(DATASOURCE, ResourceType.DATASOURCE); + private final TimeseriesQuery query = Druids.newTimeseriesQueryBuilder() .dataSource(DATASOURCE) .intervals(ImmutableList.of(Intervals.ETERNITY)) @@ -153,7 +158,12 @@ public void teardown() } @Test - public void testRunSimplePreauthorized() + @Parameters({ + "APPLY_WHEN_APPLICABLE", + "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY", + "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST" + }) + public void testRunSimple_preauthorizedAsSuperuser(Policy.TablePolicySecurityLevel securityLevel) { EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); @@ -164,11 +174,12 @@ public void testRunSimplePreauthorized() .andReturn(runner) .once(); EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).once(); - replayAll(); - QueryLifecycle lifecycle = createLifecycle(new AuthConfig()); - lifecycle.runSimple(query, authenticationResult, AuthorizationResult.ALLOW_ALL); + QueryLifecycle lifecycle = createLifecycle(AuthConfig.newBuilder() + .setTablePolicySecurityLevel(securityLevel) + .build()); + lifecycle.runSimple(query, authenticationResult, AuthorizationResult.SUPERUSER); } @Test @@ -182,10 +193,7 @@ public void testRunSimpleUnauthorized() EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) .andReturn(toolChest) .once(); - EasyMock.expect(toolChest.makeMetrics(EasyMock.anyObject())).andReturn(metrics).anyTimes(); - - replayAll(); QueryLifecycle lifecycle = createLifecycle(new AuthConfig()); @@ -193,234 +201,305 @@ public void testRunSimpleUnauthorized() } @Test - public void testAuthorizedWithNoPolicyOnTable() + @Parameters({ + "APPLY_WHEN_APPLICABLE", + "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY", + "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST" + }) + public void testRunSimple_withPolicyRestriction(Policy.TablePolicySecurityLevel securityLevel) { + Policy rowFilterPolicy = Policy.fromRowFilter(new NullFilter("some-column", null)); + Access access = Access.allowWithRestriction(rowFilterPolicy); + AuthorizationResult authorizationResult = AuthorizationResult.allowWithRestriction(ImmutableMap.of( + DATASOURCE, + Optional.of(rowFilterPolicy) + )); + DataSource expectedDataSource = RestrictedDataSource.create(TableDataSource.create(DATASOURCE), rowFilterPolicy); + + final TimeseriesQuery query = Druids.newTimeseriesQueryBuilder() + .dataSource(DATASOURCE) + .intervals(ImmutableList.of(Intervals.ETERNITY)) + .aggregators(new CountAggregatorFactory("chocula")) + .build(); EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); - EasyMock.expect(authorizer.authorize( - authenticationResult, - new Resource(DATASOURCE, ResourceType.DATASOURCE), - Action.READ - )) - .andReturn(Access.allow()).once(); + EasyMock.expect(authorizer.authorize(authenticationResult, RESOURCE, Action.READ)).andReturn(access).anyTimes(); EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) - .andReturn(toolChest).times(2); + .andReturn(toolChest).once(); + // We're expecting the data source in the query to be transformed to a RestrictedDataSource, with policy. + // Any other DataSource would throw AssertionError. EasyMock.expect(texasRanger.getQueryRunnerForIntervals( - queryMatchDataSource(TableDataSource.create(DATASOURCE)), - EasyMock.anyObject() - )) - .andReturn(runner).times(2); - EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).times(2); + queryMatchDataSource(expectedDataSource), + EasyMock.anyObject() + )).andReturn(runner).anyTimes(); + EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).anyTimes(); replayAll(); - final TimeseriesQuery query = Druids.newTimeseriesQueryBuilder() - .dataSource(DATASOURCE) - .intervals(ImmutableList.of(Intervals.ETERNITY)) - .aggregators(new CountAggregatorFactory("chocula")) - .build(); AuthConfig authConfig = AuthConfig.newBuilder() .setAuthorizeQueryContextParams(true) - .setEnableStrictPolicyCheck(true) + .setTablePolicySecurityLevel(securityLevel) .build(); QueryLifecycle lifecycle = createLifecycle(authConfig); - lifecycle.initialize(query); - Assert.assertTrue(lifecycle.authorize(authenticationResult).getPermissionErrorMessage(false).isEmpty()); - lifecycle.execute(); + lifecycle.runSimple(query, authenticationResult, authorizationResult); + } - lifecycle = createLifecycle(authConfig); - lifecycle.runSimple(query, authenticationResult, AuthorizationResult.ALLOW_ALL); + @Test + @Parameters({ + "APPLY_WHEN_APPLICABLE, ", + "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY, Need to check row-level policy for all tables missing [some_datasource]", + "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST, Need to check row-level policy for all tables missing [some_datasource]" + }) + public void testRunSimple_withNoRestriction( + Policy.TablePolicySecurityLevel securityLevel, + String error + ) + { + // When AuthorizationResult is ALLOW_NO_RESTRICTION, this means policy restriction has never been checked. + if (!error.isEmpty()) { + expectedException.expect(ISE.class); + expectedException.expectMessage(error); + } + + EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); + EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); + EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) + .andReturn(toolChest) + .once(); + EasyMock.expect(texasRanger.getQueryRunnerForIntervals(EasyMock.anyObject(), EasyMock.anyObject())) + .andReturn(runner) + .anyTimes(); + EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).anyTimes(); + EasyMock.expect(toolChest.makeMetrics(EasyMock.anyObject())).andReturn(metrics).anyTimes(); + replayAll(); + + QueryLifecycle lifecycle = createLifecycle(AuthConfig.newBuilder() + .setTablePolicySecurityLevel(securityLevel) + .build()); + lifecycle.runSimple(query, authenticationResult, AuthorizationResult.ALLOW_NO_RESTRICTION); } @Test - public void testAuthorizedWithAlwaysTruePolicyRestriction() + @Parameters({ + "APPLY_WHEN_APPLICABLE, ", + "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY, ", + "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST, Every table must have a policy restriction attached missing [some_datasource]" + }) + public void testRunSimple_withPolicyNotExist(Policy.TablePolicySecurityLevel securityLevel, String error) { - Policy alwaysTruePolicy = Policy.fromRowFilter(TrueDimFilter.instance()); + if (!error.isEmpty()) { + expectedException.expect(ISE.class); + expectedException.expectMessage(error); + } + // AuthorizationResult indicates there's no policy restriction on the table. + AuthorizationResult authorizationResult = AuthorizationResult.allowWithRestriction(ImmutableMap.of( + DATASOURCE, + Optional.empty() + )); + EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); - EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); - EasyMock.expect(authorizer.authorize( - authenticationResult, - new Resource(DATASOURCE, ResourceType.DATASOURCE), - Action.READ - )) - .andReturn(Access.allowWithRestriction(alwaysTruePolicy)).once(); EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) - .andReturn(toolChest).times(2); - EasyMock.expect(texasRanger.getQueryRunnerForIntervals(queryMatchDataSource(RestrictedDataSource.create( - TableDataSource.create(DATASOURCE), - alwaysTruePolicy - )), EasyMock.anyObject())) - .andReturn(runner).times(2); - EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).times(2); + .andReturn(toolChest) + .once(); + EasyMock.expect(texasRanger.getQueryRunnerForIntervals(EasyMock.anyObject(), EasyMock.anyObject())) + .andReturn(runner) + .anyTimes(); + EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).anyTimes(); + EasyMock.expect(toolChest.makeMetrics(EasyMock.anyObject())).andReturn(metrics).anyTimes(); replayAll(); + QueryLifecycle lifecycle = createLifecycle(AuthConfig.newBuilder() + .setTablePolicySecurityLevel(securityLevel) + .build()); + lifecycle.runSimple(query, authenticationResult, authorizationResult); + } + + @Test + @Parameters({ + "APPLY_WHEN_APPLICABLE", + "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY", + "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST" + }) + public void testRunSimple_foundMultiplePolicyRestrictions(Policy.TablePolicySecurityLevel securityLevel) + { + expectedException.expect(ISE.class); + expectedException.expectMessage( + "Multiple restrictions on [some_datasource]: Policy{rowFilter=some-column IS NULL} and Policy{rowFilter=some-column2 IS NULL}"); + + DimFilter originalFilterOnRDS = new NullFilter("some-column", null); + Policy originalFilterPolicy = Policy.fromRowFilter(originalFilterOnRDS); + + Policy newFilterPolicy = Policy.fromRowFilter(new NullFilter("some-column2", null)); + AuthorizationResult authorizationResult = AuthorizationResult.allowWithRestriction(ImmutableMap.of( + DATASOURCE, + Optional.of(newFilterPolicy) + )); + final TimeseriesQuery query = Druids.newTimeseriesQueryBuilder() - .dataSource(DATASOURCE) + .dataSource(RestrictedDataSource.create( + TableDataSource.create(DATASOURCE), + originalFilterPolicy + )) .intervals(ImmutableList.of(Intervals.ETERNITY)) .aggregators(new CountAggregatorFactory("chocula")) .build(); + EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); + EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); + EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); + EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())).andReturn(toolChest).anyTimes(); + EasyMock.expect(toolChest.makeMetrics(EasyMock.anyObject())).andReturn(metrics).anyTimes(); + EasyMock.expect(texasRanger.getQueryRunnerForIntervals(EasyMock.anyObject(), EasyMock.anyObject())) + .andReturn(runner).anyTimes(); + EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).anyTimes(); + replayAll(); + AuthConfig authConfig = AuthConfig.newBuilder() .setAuthorizeQueryContextParams(true) - .setEnableStrictPolicyCheck(true) + .setTablePolicySecurityLevel(securityLevel) .build(); - QueryLifecycle lifecycle = createLifecycle(authConfig); - lifecycle.initialize(query); - Assert.assertTrue(lifecycle.authorize(authenticationResult).getPermissionErrorMessage(false).isEmpty()); - lifecycle.execute(); - lifecycle = createLifecycle(authConfig); - lifecycle.runSimple( - query, - authenticationResult, - AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, Optional.of(alwaysTruePolicy))) - ); + QueryLifecycle lifecycle = createLifecycle(authConfig); + lifecycle.runSimple(query, authenticationResult, authorizationResult); } @Test - public void testAuthorizedWithOnePolicyRestriction() + @Parameters({ + "APPLY_WHEN_APPLICABLE", + "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY", + "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST" + }) + public void testRunSimple_queryWithRestrictedDataSource_policyRestrictionMightHaveBeenRemoved(Policy.TablePolicySecurityLevel securityLevel) { - Policy rowFilterPolicy = Policy.fromRowFilter(new NullFilter("some-column", null)); + expectedException.expect(ISE.class); + expectedException.expectMessage( + "No restriction found on table [some_datasource], but had Policy{rowFilter=some-column IS NULL} before."); + + DimFilter originalFilterOnRDS = new NullFilter("some-column", null); + Policy originalFilterPolicy = Policy.fromRowFilter(originalFilterOnRDS); + DataSource restrictedDataSource = RestrictedDataSource.create( + TableDataSource.create(DATASOURCE), + originalFilterPolicy + ); + + // The query is built on a restricted data source, but we didn't find any policy, which could be one of: + // 1. policy restriction might have been been removed + // 2. some bug in the system + // In this case, we throw an exception to be safe. + AuthorizationResult authorizationResult = AuthorizationResult.allowWithRestriction(ImmutableMap.of( + DATASOURCE, + Optional.empty() + )); + final TimeseriesQuery query = Druids.newTimeseriesQueryBuilder() - .dataSource(DATASOURCE) + .dataSource(restrictedDataSource) .intervals(ImmutableList.of(Intervals.ETERNITY)) .aggregators(new CountAggregatorFactory("chocula")) .build(); EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); - EasyMock.expect(authorizer.authorize( - authenticationResult, - new Resource(DATASOURCE, ResourceType.DATASOURCE), - Action.READ - )) - .andReturn(Access.allowWithRestriction(rowFilterPolicy)).times(1); - EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) - .andReturn(toolChest).times(2); - EasyMock.expect(texasRanger.getQueryRunnerForIntervals(queryMatchDataSource(RestrictedDataSource.create( - TableDataSource.create(DATASOURCE), - rowFilterPolicy - )), EasyMock.anyObject())) - .andReturn(runner).times(2); - EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).times(2); + EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())).andReturn(toolChest).anyTimes(); + EasyMock.expect(toolChest.makeMetrics(EasyMock.anyObject())).andReturn(metrics).anyTimes(); + EasyMock.expect(texasRanger.getQueryRunnerForIntervals(EasyMock.anyObject(), EasyMock.anyObject())) + .andReturn(runner).anyTimes(); + EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).anyTimes(); replayAll(); AuthConfig authConfig = AuthConfig.newBuilder() .setAuthorizeQueryContextParams(true) - .setEnableStrictPolicyCheck(true) + .setTablePolicySecurityLevel(securityLevel) .build(); - QueryLifecycle lifecycle = createLifecycle(authConfig); - lifecycle.initialize(query); - Assert.assertTrue(lifecycle.authorize(authenticationResult).getPermissionErrorMessage(false).isEmpty()); - lifecycle.execute(); - lifecycle = createLifecycle(authConfig); - lifecycle.runSimple( - query, - authenticationResult, - AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, Optional.of(rowFilterPolicy))) - ); + QueryLifecycle lifecycle = createLifecycle(authConfig); + lifecycle.runSimple(query, authenticationResult, authorizationResult); } - @Test - public void testAuthorizedMissingPolicyRestriction() + @Parameters({ + "APPLY_WHEN_APPLICABLE", + "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY", + "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST" + }) + public void testRunSimple_queryWithRestrictedDataSource_runWithSuperUserPermission(Policy.TablePolicySecurityLevel securityLevel) { + DimFilter originalFilterOnRDS = new NullFilter("some-column", null); + Policy originalFilterPolicy = Policy.fromRowFilter(originalFilterOnRDS); + DataSource restrictedDataSource = RestrictedDataSource.create( + TableDataSource.create(DATASOURCE), + originalFilterPolicy + ); + + // Internal druid system could be passing the query on restricted data source (e.x. broker calling historical), this must be allowed + AuthorizationResult authorizationResult = AuthorizationResult.SUPERUSER; + final TimeseriesQuery query = Druids.newTimeseriesQueryBuilder() - .dataSource(DATASOURCE) + .dataSource(restrictedDataSource) .intervals(ImmutableList.of(Intervals.ETERNITY)) .aggregators(new CountAggregatorFactory("chocula")) .build(); - final TimeseriesQuery queryOnRestrictedDS = (TimeseriesQuery) query.withPolicyRestrictions(ImmutableMap.of( - DATASOURCE, - Optional.of(Policy.NO_RESTRICTION) - ), true); - Assume.assumeTrue(queryOnRestrictedDS.getDataSource() instanceof RestrictedDataSource); EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); + EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); + EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())).andReturn(toolChest).anyTimes(); EasyMock.expect(toolChest.makeMetrics(EasyMock.anyObject())).andReturn(metrics).anyTimes(); + EasyMock.expect(texasRanger.getQueryRunnerForIntervals(EasyMock.anyObject(), EasyMock.anyObject())) + .andReturn(runner).anyTimes(); + EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).anyTimes(); replayAll(); AuthConfig authConfig = AuthConfig.newBuilder() .setAuthorizeQueryContextParams(true) - .setEnableStrictPolicyCheck(true) + .setTablePolicySecurityLevel(securityLevel) .build(); + QueryLifecycle lifecycle = createLifecycle(authConfig); - RuntimeException e = Assert.assertThrows(RuntimeException.class, () -> - lifecycle.runSimple( - query, - authenticationResult, - AuthorizationResult.allowWithRestriction(ImmutableMap.of()) - )); - Assert.assertEquals("Need to check row-level policy for all tables, missing [some_datasource]", e.getMessage()); - - QueryLifecycle lifecycle2 = createLifecycle(authConfig); - RuntimeException e2 = Assert.assertThrows(RuntimeException.class, () -> - lifecycle2.runSimple( - queryOnRestrictedDS, - authenticationResult, - AuthorizationResult.allowWithRestriction(ImmutableMap.of()) - )); - Assert.assertEquals("Missing policy check result for table [some_datasource]", e2.getMessage()); + lifecycle.runSimple(query, authenticationResult, authorizationResult); } @Test - public void testAuthorizedMultiplePolicyRestrictions() + @Parameters({ + "APPLY_WHEN_APPLICABLE", + "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY", + "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST" + }) + public void testAuthorized_withPolicyRestriction(Policy.TablePolicySecurityLevel securityLevel) { - Policy alwaysTruePolicy = Policy.fromRowFilter(TrueDimFilter.instance()); - Policy filterPolicy = Policy.fromRowFilter(new NullFilter("some-column", null)); - Policy filterPolicy2 = Policy.fromRowFilter(new NullFilter("some-column2", null)); + Policy rowFilterPolicy = Policy.fromRowFilter(new NullFilter("some-column", null)); + Access access = Access.allowWithRestriction(rowFilterPolicy); + + DataSource expectedDataSource = RestrictedDataSource.create(TableDataSource.create(DATASOURCE), rowFilterPolicy); final TimeseriesQuery query = Druids.newTimeseriesQueryBuilder() - .dataSource(RestrictedDataSource.create( - TableDataSource.create(DATASOURCE), - filterPolicy - )) + .dataSource(DATASOURCE) .intervals(ImmutableList.of(Intervals.ETERNITY)) .aggregators(new CountAggregatorFactory("chocula")) .build(); EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); - EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())).andReturn(toolChest).anyTimes(); - EasyMock.expect(toolChest.makeMetrics(EasyMock.anyObject())).andReturn(metrics).anyTimes(); - EasyMock.expect(texasRanger.getQueryRunnerForIntervals(EasyMock.anyObject(), EasyMock.anyObject())) - .andReturn(runner).times(2); - EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).times(2); + EasyMock.expect(authorizer.authorize(authenticationResult, RESOURCE, Action.READ)) + .andReturn(access).anyTimes(); + EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) + .andReturn(toolChest).anyTimes(); + // We're expecting the data source in the query to be transformed to a RestrictedDataSource, with policy. + // Any other DataSource would throw AssertionError. + EasyMock.expect(texasRanger.getQueryRunnerForIntervals( + queryMatchDataSource(expectedDataSource), + EasyMock.anyObject() + )) + .andReturn(runner).anyTimes(); + EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).anyTimes(); replayAll(); AuthConfig authConfig = AuthConfig.newBuilder() .setAuthorizeQueryContextParams(true) - .setEnableStrictPolicyCheck(true) + .setTablePolicySecurityLevel(securityLevel) .build(); - QueryLifecycle lifecycle = createLifecycle(authConfig); - ISE e = Assert.assertThrows(ISE.class, () -> - lifecycle.runSimple( - query, - authenticationResult, - AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, Optional.of(filterPolicy2))) - )); - Assert.assertEquals( - "Incompatible restrictions on [some_datasource]: Policy{rowFilter=some-column IS NULL} and Policy{rowFilter=some-column2 IS NULL}", - e.getMessage() - ); - - QueryLifecycle lifecycle2 = createLifecycle(authConfig); - // alwaysTruePolicy is a compatible restriction - lifecycle2.runSimple( - query, - authenticationResult, - AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, Optional.of(alwaysTruePolicy))) - ); - - lifecycle2 = createLifecycle(authConfig); - // no restriction, compatible - lifecycle2.runSimple( - query, - authenticationResult, - AuthorizationResult.allowWithRestriction(ImmutableMap.of(DATASOURCE, Optional.of(Policy.NO_RESTRICTION))) - ); + lifecycle.initialize(query); + Assert.assertTrue(lifecycle.authorize(authenticationResult).getPermissionErrorMessage(false).isEmpty()); + lifecycle.execute(); } @Test @@ -535,11 +614,7 @@ public void testAuthorizeQueryContext_unsecuredKeys() EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); - EasyMock.expect(authorizer.authorize( - authenticationResult, - new Resource(DATASOURCE, ResourceType.DATASOURCE), - Action.READ - )) + EasyMock.expect(authorizer.authorize(authenticationResult, RESOURCE, Action.READ)) .andReturn(Access.OK) .times(2); diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java index eeb0dee10640..3eaa9c617093 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java @@ -137,7 +137,7 @@ public DruidPlanner createPlannerForTesting( thePlanner.getPlannerContext() .setAuthenticationResult(NoopEscalator.getInstance().createEscalatedAuthenticationResult()); thePlanner.validate(); - thePlanner.authorize(ra -> AuthorizationResult.ALLOW_ALL, ImmutableSet.of()); + thePlanner.authorize(ra -> AuthorizationResult.SUPERUSER, ImmutableSet.of()); return thePlanner; } diff --git a/sql/src/test/java/org/apache/druid/sql/avatica/DruidAvaticaHandlerTest.java b/sql/src/test/java/org/apache/druid/sql/avatica/DruidAvaticaHandlerTest.java index 8ee486701a27..a6bbcf72f3d4 100644 --- a/sql/src/test/java/org/apache/druid/sql/avatica/DruidAvaticaHandlerTest.java +++ b/sql/src/test/java/org/apache/druid/sql/avatica/DruidAvaticaHandlerTest.java @@ -562,6 +562,12 @@ public void testDatabaseMetaDataTables() throws SQLException Pair.of("TABLE_SCHEM", "druid"), Pair.of("TABLE_TYPE", "TABLE") ), + row( + Pair.of("TABLE_CAT", "druid"), + Pair.of("TABLE_NAME", CalciteTests.RESTRICTED_DATASOURCE), + Pair.of("TABLE_SCHEM", "druid"), + Pair.of("TABLE_TYPE", "TABLE") + ), row( Pair.of("TABLE_CAT", "druid"), Pair.of("TABLE_NAME", CalciteTests.SOME_DATASOURCE), @@ -654,6 +660,12 @@ public void testDatabaseMetaDataTablesAsSuperuser() throws SQLException Pair.of("TABLE_SCHEM", "druid"), Pair.of("TABLE_TYPE", "TABLE") ), + row( + Pair.of("TABLE_CAT", "druid"), + Pair.of("TABLE_NAME", CalciteTests.RESTRICTED_DATASOURCE), + Pair.of("TABLE_SCHEM", "druid"), + Pair.of("TABLE_TYPE", "TABLE") + ), row( Pair.of("TABLE_CAT", "druid"), Pair.of("TABLE_NAME", CalciteTests.SOME_DATASOURCE), diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteJoinQueryTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteJoinQueryTest.java index 85d46281ff4f..a4dfc309f3fa 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteJoinQueryTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteJoinQueryTest.java @@ -5191,7 +5191,7 @@ public void testGroupByJoinAsNativeQueryWithUnoptimizedFilter(Map results = seq.toList(); Assert.assertEquals( ImmutableList.of(ResultRow.of("def")), diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java index d91d23525bb3..6cd13b08380f 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java @@ -235,10 +235,10 @@ public void testInformationSchemaTables() .add(new Object[]{"druid", CalciteTests.DATASOURCE1, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.DATASOURCE2, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.DATASOURCE4, "TABLE", "NO", "NO"}) - .add(new Object[]{"druid", CalciteTests.RESTRICTED_DATASOURCE, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.FORBIDDEN_DATASOURCE, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.DATASOURCE5, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.DATASOURCE3, "TABLE", "NO", "NO"}) + .add(new Object[]{"druid", CalciteTests.RESTRICTED_DATASOURCE, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.SOME_DATASOURCE, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.SOMEXDATASOURCE, "TABLE", "NO", "NO"}) .add(new Object[]{"druid", CalciteTests.USERVISITDATASOURCE, "TABLE", "NO", "NO"}) diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/schema/BrokerSegmentMetadataCacheTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/schema/BrokerSegmentMetadataCacheTest.java index 9bcc55c799f2..560856b690a8 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/schema/BrokerSegmentMetadataCacheTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/schema/BrokerSegmentMetadataCacheTest.java @@ -324,7 +324,7 @@ public ListenableFuture> fetchDataSourceInformation( EasyMock.expect(lifecycleMock.runSimple( expectedMetadataQuery, AllowAllAuthenticator.ALLOW_ALL_RESULT, - AuthorizationResult.ALLOW_ALL + AuthorizationResult.SUPERUSER )) .andReturn(QueryResponse.withEmptyContext(Sequences.empty())); @@ -1072,7 +1072,7 @@ public void testRunSegmentMetadataQueryWithContext() throws Exception EasyMock.expect(lifecycleMock.runSimple( expectedMetadataQuery, AllowAllAuthenticator.ALLOW_ALL_RESULT, - AuthorizationResult.ALLOW_ALL + AuthorizationResult.SUPERUSER )) .andReturn(QueryResponse.withEmptyContext(Sequences.empty())); @@ -1255,7 +1255,7 @@ public void testTombstoneSegmentIsNotRefreshed() throws IOException EasyMock.expect(lifecycleMock.runSimple( expectedMetadataQuery, AllowAllAuthenticator.ALLOW_ALL_RESULT, - AuthorizationResult.ALLOW_ALL + AuthorizationResult.SUPERUSER )) .andReturn(QueryResponse.withEmptyContext(Sequences.empty())); From ec6e3b5af1018e204d4dfb79808f4d3f28d6cad4 Mon Sep 17 00:00:00 2001 From: cecemei Date: Thu, 19 Dec 2024 21:09:00 -0800 Subject: [PATCH 12/32] reverted changes in SegmentMetadataQuery, since now query from druid-internal won't be restricted --- .../controller/http/DartSqlResourceTest.java | 2 +- .../druid/query/RestrictedDataSource.java | 14 ++- .../metadata/SegmentMetadataQuery.java | 7 -- .../org/apache/druid/query/policy/Policy.java | 7 +- .../apache/druid/query/DataSourceTest.java | 38 +++++-- .../AbstractSegmentMetadataCache.java | 2 +- .../apache/druid/server/QueryLifecycle.java | 6 +- .../druid/server/security/AuthConfig.java | 7 +- .../server/security/AuthorizationResult.java | 27 ++--- .../server/security/AuthorizationUtils.java | 2 +- .../apache/druid/server/security/Policy.java | 106 ++++++++++++++++++ .../AuthorizationResultTest.java | 9 -- .../CoordinatorSegmentMetadataCacheTest.java | 4 +- .../druid/server/QueryLifecycleTest.java | 72 +++++++----- .../sql/calcite/planner/PlannerFactory.java | 2 +- .../sql/calcite/CalciteJoinQueryTest.java | 2 +- .../BrokerSegmentMetadataCacheTest.java | 6 +- 17 files changed, 220 insertions(+), 93 deletions(-) create mode 100644 server/src/main/java/org/apache/druid/server/security/Policy.java diff --git a/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/http/DartSqlResourceTest.java b/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/http/DartSqlResourceTest.java index f1978bd8c39d..10b5f20e4187 100644 --- a/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/http/DartSqlResourceTest.java +++ b/extensions-core/multi-stage-query/src/test/java/org/apache/druid/msq/dart/controller/http/DartSqlResourceTest.java @@ -561,7 +561,7 @@ public void test_doPost_superUser_restricted_throwsServerError() ); Assertions.assertNull(sqlResource.doPost(sqlQuery, httpServletRequest)); // Super user can run a dart query, but we don't support it yet. - Assertions.assertEquals(Response.Status.INTERNAL_SERVER_ERROR, asyncResponse.getStatus()); + Assertions.assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), asyncResponse.getStatus()); } @Test diff --git a/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java b/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java index 8b63db737869..10d14a112dd9 100644 --- a/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java +++ b/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java @@ -146,16 +146,15 @@ public DataSource withUpdatedDataSource(DataSource newSource) @Override public DataSource mapWithRestriction( - Map> policies, + Map> policyMap, Policy.TablePolicySecurityLevel tablePolicySecurityLevel ) { - // This method always throws, since we should only put restrictions once. When query is being passed by druid-system, it should use SUPERUSER AuthorizationResults. - if (!policies.containsKey(base.getName())) { + if (!policyMap.containsKey(base.getName())) { throw new ISE("Missing policy check result for table [%s]", base.getName()); } - Optional newPolicy = policies.getOrDefault(base.getName(), Optional.empty()); + Optional newPolicy = policyMap.getOrDefault(base.getName(), Optional.empty()); if (!newPolicy.isPresent()) { throw new ISE( "No restriction found on table [%s], but had %s before.", @@ -163,7 +162,12 @@ public DataSource mapWithRestriction( policy ); } - throw new ISE("Multiple restrictions on [%s]: %s and %s", base.getName(), policy, newPolicy.get()); + if (!Policy.NO_RESTRICTION.equals(newPolicy.get())) { + throw new ISE("Multiple restrictions on [%s]: %s and %s", base.getName(), policy, newPolicy.get()); + } + // The only happy path is, newPolicy is Policy.NO_RESTRICTION, which means this comes from an anthenticated and + // authorized druid-internal request. + return this; } @Override diff --git a/processing/src/main/java/org/apache/druid/query/metadata/metadata/SegmentMetadataQuery.java b/processing/src/main/java/org/apache/druid/query/metadata/metadata/SegmentMetadataQuery.java index 3ef563d9854b..09881b87e123 100644 --- a/processing/src/main/java/org/apache/druid/query/metadata/metadata/SegmentMetadataQuery.java +++ b/processing/src/main/java/org/apache/druid/query/metadata/metadata/SegmentMetadataQuery.java @@ -30,7 +30,6 @@ import org.apache.druid.query.DataSource; import org.apache.druid.query.Druids; import org.apache.druid.query.Query; -import org.apache.druid.query.RestrictedDataSource; import org.apache.druid.query.TableDataSource; import org.apache.druid.query.UnionDataSource; import org.apache.druid.query.filter.DimFilter; @@ -224,12 +223,6 @@ public Query withQuerySegmentSpec(QuerySegmentSpec spec) @Override public Query withDataSource(DataSource dataSource) { - if (dataSource instanceof RestrictedDataSource && ((RestrictedDataSource) dataSource).getPolicy() - .hasNoRestriction()) { - return Druids.SegmentMetadataQueryBuilder.copy(this) - .dataSource(((RestrictedDataSource) dataSource).getBase()) - .build(); - } return Druids.SegmentMetadataQueryBuilder.copy(this).dataSource(dataSource).build(); } diff --git a/processing/src/main/java/org/apache/druid/query/policy/Policy.java b/processing/src/main/java/org/apache/druid/query/policy/Policy.java index c0d498a3d210..516f6d85a631 100644 --- a/processing/src/main/java/org/apache/druid/query/policy/Policy.java +++ b/processing/src/main/java/org/apache/druid/query/policy/Policy.java @@ -22,7 +22,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import org.apache.druid.query.filter.DimFilter; -import org.apache.druid.query.filter.TrueDimFilter; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -73,6 +72,10 @@ public boolean policyMustBeCheckedAndExistOnAllTables() } } + /** + * A special kind of policy restriction, indicating that this table is restricted, but doesn't impose any restriction + * to a user. + */ public static final Policy NO_RESTRICTION = new Policy(null); @JsonProperty("rowFilter") @@ -102,8 +105,6 @@ public boolean hasNoRestriction() { if (NO_RESTRICTION.equals(this)) { return true; - } else if (rowFilter == null || rowFilter instanceof TrueDimFilter) { - return true; } return false; } diff --git a/processing/src/test/java/org/apache/druid/query/DataSourceTest.java b/processing/src/test/java/org/apache/druid/query/DataSourceTest.java index 9fb098569474..71b898daa3b9 100644 --- a/processing/src/test/java/org/apache/druid/query/DataSourceTest.java +++ b/processing/src/test/java/org/apache/druid/query/DataSourceTest.java @@ -228,6 +228,30 @@ public void testMapWithRestriction_tableWithEmptyPolicy(Policy.TablePolicySecuri } } + @Test + @Parameters({ + "APPLY_WHEN_APPLICABLE", + "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY", + "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST" + }) + public void testMapWithRestriction_onRestrictedDataSource_fromDruidSystem(Policy.TablePolicySecurityLevel securityLevel) + { + RestrictedDataSource restrictedDataSource = RestrictedDataSource.create( + TableDataSource.create("table1"), + Policy.fromRowFilter(new NullFilter("some-column", null)) + ); + // The druid-system should get a NO_RESTRICTION policy attached on a table. + ImmutableMap> noRestrictionPolicy = ImmutableMap.of( + "table1", + Optional.of(Policy.NO_RESTRICTION) + ); + + Assert.assertEquals( + restrictedDataSource, + restrictedDataSource.mapWithRestriction(noRestrictionPolicy, securityLevel) + ); + } + @Test @Parameters({ "APPLY_WHEN_APPLICABLE", @@ -238,36 +262,36 @@ public void testMapWithRestriction_onRestrictedDataSource_alwaysThrows(Policy.Ta { RestrictedDataSource restrictedDataSource = RestrictedDataSource.create( TableDataSource.create("table1"), - Policy.NO_RESTRICTION + Policy.fromRowFilter(new NullFilter("random-column", null)) ); ImmutableMap> anotherRestrictions = ImmutableMap.of( "table1", Optional.of(Policy.fromRowFilter(new NullFilter("some-column", null))) ); - ImmutableMap> noRestrictions = ImmutableMap.of("table1", Optional.empty()); - ImmutableMap> emptyPolicyMap = ImmutableMap.of(); + ImmutableMap> noPolicyFound = ImmutableMap.of("table1", Optional.empty()); + ImmutableMap> policyWasNotChecked = ImmutableMap.of(); ISE e = Assert.assertThrows( ISE.class, () -> restrictedDataSource.mapWithRestriction(anotherRestrictions, securityLevel) ); Assert.assertEquals( - "Multiple restrictions on [table1]: Policy{rowFilter=null} and Policy{rowFilter=some-column IS NULL}", + "Multiple restrictions on [table1]: Policy{rowFilter=random-column IS NULL} and Policy{rowFilter=some-column IS NULL}", e.getMessage() ); ISE e2 = Assert.assertThrows( ISE.class, - () -> restrictedDataSource.mapWithRestriction(noRestrictions, securityLevel) + () -> restrictedDataSource.mapWithRestriction(noPolicyFound, securityLevel) ); Assert.assertEquals( - "No restriction found on table [table1], but had Policy{rowFilter=null} before.", + "No restriction found on table [table1], but had Policy{rowFilter=random-column IS NULL} before.", e2.getMessage() ); ISE e3 = Assert.assertThrows( ISE.class, - () -> restrictedDataSource.mapWithRestriction(emptyPolicyMap, securityLevel) + () -> restrictedDataSource.mapWithRestriction(policyWasNotChecked, securityLevel) ); Assert.assertEquals( "Missing policy check result for table [table1]", diff --git a/server/src/main/java/org/apache/druid/segment/metadata/AbstractSegmentMetadataCache.java b/server/src/main/java/org/apache/druid/segment/metadata/AbstractSegmentMetadataCache.java index 49277c2ff19e..589af8322b44 100644 --- a/server/src/main/java/org/apache/druid/segment/metadata/AbstractSegmentMetadataCache.java +++ b/server/src/main/java/org/apache/druid/segment/metadata/AbstractSegmentMetadataCache.java @@ -975,7 +975,7 @@ public Sequence runSegmentMetadataQuery( return queryLifecycleFactory .factorize() - .runSimple(segmentMetadataQuery, escalator.createEscalatedAuthenticationResult(), AuthorizationResult.SUPERUSER) + .runSimple(segmentMetadataQuery, escalator.createEscalatedAuthenticationResult(), AuthorizationResult.ALLOW_NO_RESTRICTION) .getResults(); } 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 bf627fb08a1b..8068079e903c 100644 --- a/server/src/main/java/org/apache/druid/server/QueryLifecycle.java +++ b/server/src/main/java/org/apache/druid/server/QueryLifecycle.java @@ -48,6 +48,7 @@ import org.apache.druid.query.QueryTimeoutException; import org.apache.druid.query.QueryToolChest; import org.apache.druid.query.context.ResponseContext; +import org.apache.druid.query.metadata.metadata.SegmentMetadataQuery; import org.apache.druid.server.QueryResource.ResourceIOReaderWriter; import org.apache.druid.server.log.RequestLogger; import org.apache.druid.server.security.Action; @@ -320,8 +321,9 @@ private AuthorizationResult doAuthorize( transition(State.AUTHORIZING, State.UNAUTHORIZED); } else { transition(State.AUTHORIZING, State.AUTHORIZED); - if (!AuthorizationResult.SUPERUSER.equals(authorizationResult)) { - // Unless this request comes from superuser or druid-internal, we need to map the query with restrictions. + if (this.baseQuery instanceof SegmentMetadataQuery && authorizationResult.isUserWithNoRestriction()) { + // skip restrictions mapping for SegmentMetadataQuery from user with no restriction + } else { this.baseQuery = this.baseQuery.withPolicyRestrictions( authorizationResult.getPolicy(), authConfig.getTablePolicySecurityLevel() 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 2ebc71fce749..187cc83d6d3c 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 @@ -29,7 +29,6 @@ import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.Optional; import java.util.Set; public class AuthConfig @@ -65,7 +64,7 @@ public class AuthConfig public AuthConfig() { - this(null, null, null, false, false, null, null, false, Policy.TablePolicySecurityLevel.APPLY_WHEN_APPLICABLE); + this(null, null, null, false, false, null, null, false, null); } @JsonProperty @@ -128,7 +127,7 @@ public AuthConfig( : unsecuredContextKeys; this.securedContextKeys = securedContextKeys; this.enableInputSourceSecurity = enableInputSourceSecurity; - this.tablePolicySecurityLevel = tablePolicySecurityLevel; + this.tablePolicySecurityLevel = tablePolicySecurityLevel == null ? Policy.TablePolicySecurityLevel.APPLY_WHEN_APPLICABLE : tablePolicySecurityLevel; } public List getAuthenticatorChain() @@ -338,7 +337,7 @@ public AuthConfig build() unsecuredContextKeys, securedContextKeys, enableInputSourceSecurity, - Optional.ofNullable(tablePolicySecurityLevel).orElse(Policy.TablePolicySecurityLevel.APPLY_WHEN_APPLICABLE) + tablePolicySecurityLevel ); } } diff --git a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java index 79f0f5e283f2..8304812a7d3a 100644 --- a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java +++ b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java @@ -45,19 +45,8 @@ public class AuthorizationResult { /** - * Provides unrestricted superuser access to all resources. This should be limited to Druid internal systems or - * superusers, except in cases where ACL considerations are not a priority. - */ - public static final AuthorizationResult SUPERUSER = new AuthorizationResult( - PERMISSION.SUPERUSER, - null, - Collections.emptyMap(), - null, - null - ); - - /** - * Provides access with no restrictions to all resources. + * Provides access with no restrictions to all resources.This should be limited to Druid internal systems or + * superusers, except in cases where granular ACL considerations are not a priority. */ public static final AuthorizationResult ALLOW_NO_RESTRICTION = new AuthorizationResult( PERMISSION.ALLOW_NO_RESTRICTION, @@ -80,7 +69,6 @@ public class AuthorizationResult enum PERMISSION { - SUPERUSER, ALLOW_NO_RESTRICTION, ALLOW_WITH_RESTRICTION, DENY @@ -154,6 +142,16 @@ public AuthorizationResult withResourceActions( ); } + public boolean isUserWithNoRestriction() + { + return policyRestrictions.values() + .stream() + .flatMap(policy -> policy.isPresent() + ? Stream.of(policy.get()) + : Stream.empty()) // Can be replaced by Optional.stream after Java 11 + .allMatch(Policy::hasNoRestriction); + } + /** * Returns a permission error string if the AuthorizationResult doesn't permit all requried access. Otherwise, returns * empty. When {@code policyRestrictionsNotPermitted} set to true, it requests unrestricted full access. The caller @@ -170,7 +168,6 @@ public AuthorizationResult withResourceActions( public Optional getPermissionErrorMessage(boolean policyRestrictionsNotPermitted) { switch (permission) { - case SUPERUSER: case ALLOW_NO_RESTRICTION: return Optional.empty(); case DENY: 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 102eb657e422..b2fefda7f51d 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 @@ -219,7 +219,7 @@ public static AuthorizationResult authorizeAllResourceActions( ) { if (request.getAttribute(AuthConfig.DRUID_ALLOW_UNSECURED_PATH) != null) { - return AuthorizationResult.SUPERUSER; + return AuthorizationResult.ALLOW_NO_RESTRICTION; } if (request.getAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED) != null) { diff --git a/server/src/main/java/org/apache/druid/server/security/Policy.java b/server/src/main/java/org/apache/druid/server/security/Policy.java new file mode 100644 index 000000000000..c925ce6f67bd --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/security/Policy.java @@ -0,0 +1,106 @@ +/* + * 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.security; + +import com.google.common.collect.Iterables; +import org.apache.druid.error.DruidException; +import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.filter.EqualityFilter; +import org.apache.druid.query.filter.TrueDimFilter; +import org.apache.druid.segment.column.ColumnType; + +import java.util.Collection; +import java.util.Objects; + +public class Policy +{ + public static class RowPolicy + { + public static RowPolicy ALLOW_ALL = createPermissivePolicy(TrueDimFilter.instance()); + + + public enum Type + { + // A permissive row policy gives permission, multiple permissive policies can be combined with 'Or' + Permissive, + // A restrictive row policy restricts permission, multiple restrictive policies can be combined with 'And' + Restrictive; + } + + private final Type type; + private final DimFilter filter; + + public RowPolicy(Type type, DimFilter filter) + { + this.type = type; + this.filter = filter; + } + + public static RowPolicy createRestrictiveTenantIdPolicy(String columnName, String tenantIdValue) + { + return new RowPolicy(Type.Restrictive, new EqualityFilter(columnName, ColumnType.STRING, tenantIdValue, null)); + } + + public static RowPolicy createPermissivePolicy(DimFilter filter) + { + return new RowPolicy(Type.Permissive, filter); + } + + public boolean isPermissive() + { + return type.equals(Type.Permissive); + } + + public DimFilter getFilter() + { + return this.filter; + } + + public static DimFilter combines(Collection policies) + { + assert !policies.isEmpty(); + if (policies.size() == 1) { + return Iterables.getOnlyElement(policies).getFilter(); + } + + throw DruidException.defensive("Multiple policies are not expected."); + } + + @Override + public boolean equals(Object object) + { + if (this == object) { + return true; + } + if (object == null || getClass() != object.getClass()) { + return false; + } + RowPolicy rowPolicy = (RowPolicy) object; + return type == rowPolicy.type && Objects.equals(filter, rowPolicy.filter); + } + + @Override + public int hashCode() + { + return Objects.hash(type, filter); + } + + } +} diff --git a/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java b/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java index bcdfb23350cc..d6eeb69d15eb 100644 --- a/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java +++ b/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java @@ -25,7 +25,6 @@ import nl.jqno.equalsverifier.EqualsVerifier; import org.apache.druid.common.config.NullHandling; import org.apache.druid.query.filter.EqualityFilter; -import org.apache.druid.query.filter.TrueDimFilter; import org.apache.druid.query.policy.Policy; import org.apache.druid.segment.column.ColumnType; import org.apache.druid.server.security.AuthorizationResult; @@ -97,10 +96,6 @@ public void testFullAccess_noPermissionError(boolean policyRestrictionsNotPermit "table1", Optional.of(Policy.NO_RESTRICTION) )); - AuthorizationResult resultWithTrueFilterRestrictionPolicy = AuthorizationResult.allowWithRestriction(ImmutableMap.of( - "table1", - Optional.of(Policy.fromRowFilter(TrueDimFilter.instance())) - )); assertEquals(Optional.empty(), result.getPermissionErrorMessage(policyRestrictionsNotPermitted)); assertEquals(Optional.empty(), resultWithEmptyPolicy.getPermissionErrorMessage(policyRestrictionsNotPermitted)); @@ -108,10 +103,6 @@ public void testFullAccess_noPermissionError(boolean policyRestrictionsNotPermit Optional.empty(), resultWithNoRestrictionPolicy.getPermissionErrorMessage(policyRestrictionsNotPermitted) ); - assertEquals( - Optional.empty(), - resultWithTrueFilterRestrictionPolicy.getPermissionErrorMessage(policyRestrictionsNotPermitted) - ); } @Test diff --git a/server/src/test/java/org/apache/druid/segment/metadata/CoordinatorSegmentMetadataCacheTest.java b/server/src/test/java/org/apache/druid/segment/metadata/CoordinatorSegmentMetadataCacheTest.java index 8ec08b56d1d3..d56db2ea4418 100644 --- a/server/src/test/java/org/apache/druid/segment/metadata/CoordinatorSegmentMetadataCacheTest.java +++ b/server/src/test/java/org/apache/druid/segment/metadata/CoordinatorSegmentMetadataCacheTest.java @@ -1067,7 +1067,7 @@ public void testRunSegmentMetadataQueryWithContext() throws Exception EasyMock.expect(lifecycleMock.runSimple( expectedMetadataQuery, AllowAllAuthenticator.ALLOW_ALL_RESULT, - AuthorizationResult.SUPERUSER + AuthorizationResult.ALLOW_NO_RESTRICTION )) .andReturn(QueryResponse.withEmptyContext(Sequences.empty())); @@ -2306,7 +2306,7 @@ public void testTombstoneSegmentIsNotRefreshed() throws IOException EasyMock.expect(lifecycleMock.runSimple( expectedMetadataQuery, AllowAllAuthenticator.ALLOW_ALL_RESULT, - AuthorizationResult.SUPERUSER + AuthorizationResult.ALLOW_NO_RESTRICTION )) .andReturn(QueryResponse.withEmptyContext(Sequences.empty())).once(); diff --git a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java index 1c0e5601ebde..a23fa34af0fc 100644 --- a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java +++ b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java @@ -158,13 +158,10 @@ public void teardown() } @Test - @Parameters({ - "APPLY_WHEN_APPLICABLE", - "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY", - "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST" - }) + @Parameters({"APPLY_WHEN_APPLICABLE"}) public void testRunSimple_preauthorizedAsSuperuser(Policy.TablePolicySecurityLevel securityLevel) { + // A simple path with the lowest security level configed. EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) @@ -179,7 +176,7 @@ public void testRunSimple_preauthorizedAsSuperuser(Policy.TablePolicySecurityLev QueryLifecycle lifecycle = createLifecycle(AuthConfig.newBuilder() .setTablePolicySecurityLevel(securityLevel) .build()); - lifecycle.runSimple(query, authenticationResult, AuthorizationResult.SUPERUSER); + lifecycle.runSimple(query, authenticationResult, AuthorizationResult.ALLOW_NO_RESTRICTION); } @Test @@ -208,6 +205,7 @@ public void testRunSimpleUnauthorized() }) public void testRunSimple_withPolicyRestriction(Policy.TablePolicySecurityLevel securityLevel) { + // Test the path when an external client send a sql query to broker, through runSimple. Policy rowFilterPolicy = Policy.fromRowFilter(new NullFilter("some-column", null)); Access access = Access.allowWithRestriction(rowFilterPolicy); AuthorizationResult authorizationResult = AuthorizationResult.allowWithRestriction(ImmutableMap.of( @@ -256,11 +254,14 @@ public void testRunSimple_withNoRestriction( ) { // When AuthorizationResult is ALLOW_NO_RESTRICTION, this means policy restriction has never been checked. + // Either it's calling from a mis-behave druid node, or somehow the path has bypassed the policy checks. + // This is only allowed at APPLY_WHEN_APPLICABLE, the lowest security level. if (!error.isEmpty()) { expectedException.expect(ISE.class); expectedException.expectMessage(error); } + AuthorizationResult authorizationResult = AuthorizationResult.ALLOW_NO_RESTRICTION; EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) @@ -276,7 +277,7 @@ public void testRunSimple_withNoRestriction( QueryLifecycle lifecycle = createLifecycle(AuthConfig.newBuilder() .setTablePolicySecurityLevel(securityLevel) .build()); - lifecycle.runSimple(query, authenticationResult, AuthorizationResult.ALLOW_NO_RESTRICTION); + lifecycle.runSimple(query, authenticationResult, authorizationResult); } @Test @@ -291,7 +292,8 @@ public void testRunSimple_withPolicyNotExist(Policy.TablePolicySecurityLevel sec expectedException.expect(ISE.class); expectedException.expectMessage(error); } - // AuthorizationResult indicates there's no policy restriction on the table. + // AuthorizationResult indicates there's no policy restriction on the table + // This is not allowed at the highest security level AuthorizationResult authorizationResult = AuthorizationResult.allowWithRestriction(ImmutableMap.of( DATASOURCE, Optional.empty() @@ -323,6 +325,8 @@ public void testRunSimple_withPolicyNotExist(Policy.TablePolicySecurityLevel sec }) public void testRunSimple_foundMultiplePolicyRestrictions(Policy.TablePolicySecurityLevel securityLevel) { + // Multiple policy restrictions indicates most likely the system is trying to double-authorizing the request + // This is not allowed in any case. expectedException.expect(ISE.class); expectedException.expectMessage( "Multiple restrictions on [some_datasource]: Policy{rowFilter=some-column IS NULL} and Policy{rowFilter=some-column2 IS NULL}"); @@ -383,7 +387,7 @@ public void testRunSimple_queryWithRestrictedDataSource_policyRestrictionMightHa ); // The query is built on a restricted data source, but we didn't find any policy, which could be one of: - // 1. policy restriction might have been been removed + // 1. policy restriction might have been removed // 2. some bug in the system // In this case, we throw an exception to be safe. AuthorizationResult authorizationResult = AuthorizationResult.allowWithRestriction(ImmutableMap.of( @@ -421,29 +425,33 @@ public void testRunSimple_queryWithRestrictedDataSource_policyRestrictionMightHa "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY", "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST" }) - public void testRunSimple_queryWithRestrictedDataSource_runWithSuperUserPermission(Policy.TablePolicySecurityLevel securityLevel) + + public void testAuthorized_withPolicyRestriction(Policy.TablePolicySecurityLevel securityLevel) { - DimFilter originalFilterOnRDS = new NullFilter("some-column", null); - Policy originalFilterPolicy = Policy.fromRowFilter(originalFilterOnRDS); - DataSource restrictedDataSource = RestrictedDataSource.create( - TableDataSource.create(DATASOURCE), - originalFilterPolicy - ); + // Test the path broker receives a native json query from external client, should add restriction on data source + Policy rowFilterPolicy = Policy.fromRowFilter(new NullFilter("some-column", null)); + Access access = Access.allowWithRestriction(rowFilterPolicy); - // Internal druid system could be passing the query on restricted data source (e.x. broker calling historical), this must be allowed - AuthorizationResult authorizationResult = AuthorizationResult.SUPERUSER; + DataSource expectedDataSource = RestrictedDataSource.create(TableDataSource.create(DATASOURCE), rowFilterPolicy); final TimeseriesQuery query = Druids.newTimeseriesQueryBuilder() - .dataSource(restrictedDataSource) + .dataSource(DATASOURCE) .intervals(ImmutableList.of(Intervals.ETERNITY)) .aggregators(new CountAggregatorFactory("chocula")) .build(); EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); - EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())).andReturn(toolChest).anyTimes(); - EasyMock.expect(toolChest.makeMetrics(EasyMock.anyObject())).andReturn(metrics).anyTimes(); - EasyMock.expect(texasRanger.getQueryRunnerForIntervals(EasyMock.anyObject(), EasyMock.anyObject())) + EasyMock.expect(authorizer.authorize(authenticationResult, RESOURCE, Action.READ)) + .andReturn(access).anyTimes(); + EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) + .andReturn(toolChest).anyTimes(); + // We're expecting the data source in the query to be transformed to a RestrictedDataSource, with policy. + // Any other DataSource would throw AssertionError. + EasyMock.expect(texasRanger.getQueryRunnerForIntervals( + queryMatchDataSource(expectedDataSource), + EasyMock.anyObject() + )) .andReturn(runner).anyTimes(); EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).anyTimes(); replayAll(); @@ -452,9 +460,10 @@ public void testRunSimple_queryWithRestrictedDataSource_runWithSuperUserPermissi .setAuthorizeQueryContextParams(true) .setTablePolicySecurityLevel(securityLevel) .build(); - QueryLifecycle lifecycle = createLifecycle(authConfig); - lifecycle.runSimple(query, authenticationResult, authorizationResult); + lifecycle.initialize(query); + Assert.assertTrue(lifecycle.authorize(authenticationResult).getPermissionErrorMessage(false).isEmpty()); + lifecycle.execute(); } @Test @@ -463,15 +472,17 @@ public void testRunSimple_queryWithRestrictedDataSource_runWithSuperUserPermissi "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY", "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST" }) - public void testAuthorized_withPolicyRestriction(Policy.TablePolicySecurityLevel securityLevel) + public void testAuthorized_queryWithRestrictedDataSource_runWithSuperUserPermission(Policy.TablePolicySecurityLevel securityLevel) { + // Test the path historical receives a native json query from broker, query already has restriction on data source Policy rowFilterPolicy = Policy.fromRowFilter(new NullFilter("some-column", null)); - Access access = Access.allowWithRestriction(rowFilterPolicy); + // Internal druid system would get a NO_RESTRICTION on a restricted data source. + Access access = Access.allowWithRestriction(Policy.NO_RESTRICTION); - DataSource expectedDataSource = RestrictedDataSource.create(TableDataSource.create(DATASOURCE), rowFilterPolicy); + DataSource restrictedDataSource = RestrictedDataSource.create(TableDataSource.create(DATASOURCE), rowFilterPolicy); final TimeseriesQuery query = Druids.newTimeseriesQueryBuilder() - .dataSource(DATASOURCE) + .dataSource(restrictedDataSource) .intervals(ImmutableList.of(Intervals.ETERNITY)) .aggregators(new CountAggregatorFactory("chocula")) .build(); @@ -482,10 +493,9 @@ public void testAuthorized_withPolicyRestriction(Policy.TablePolicySecurityLevel .andReturn(access).anyTimes(); EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) .andReturn(toolChest).anyTimes(); - // We're expecting the data source in the query to be transformed to a RestrictedDataSource, with policy. - // Any other DataSource would throw AssertionError. + // We're expecting the data source in the query to be the same RestrictedDataSource. EasyMock.expect(texasRanger.getQueryRunnerForIntervals( - queryMatchDataSource(expectedDataSource), + queryMatchDataSource(restrictedDataSource), EasyMock.anyObject() )) .andReturn(runner).anyTimes(); diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java index 3eaa9c617093..3e69d275471f 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java @@ -137,7 +137,7 @@ public DruidPlanner createPlannerForTesting( thePlanner.getPlannerContext() .setAuthenticationResult(NoopEscalator.getInstance().createEscalatedAuthenticationResult()); thePlanner.validate(); - thePlanner.authorize(ra -> AuthorizationResult.SUPERUSER, ImmutableSet.of()); + thePlanner.authorize(ra -> AuthorizationResult.ALLOW_NO_RESTRICTION, ImmutableSet.of()); return thePlanner; } diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteJoinQueryTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteJoinQueryTest.java index a4dfc309f3fa..ad0e334fcd5a 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteJoinQueryTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteJoinQueryTest.java @@ -5191,7 +5191,7 @@ public void testGroupByJoinAsNativeQueryWithUnoptimizedFilter(Map results = seq.toList(); Assert.assertEquals( ImmutableList.of(ResultRow.of("def")), diff --git a/sql/src/test/java/org/apache/druid/sql/calcite/schema/BrokerSegmentMetadataCacheTest.java b/sql/src/test/java/org/apache/druid/sql/calcite/schema/BrokerSegmentMetadataCacheTest.java index 560856b690a8..3aef6d84061f 100644 --- a/sql/src/test/java/org/apache/druid/sql/calcite/schema/BrokerSegmentMetadataCacheTest.java +++ b/sql/src/test/java/org/apache/druid/sql/calcite/schema/BrokerSegmentMetadataCacheTest.java @@ -324,7 +324,7 @@ public ListenableFuture> fetchDataSourceInformation( EasyMock.expect(lifecycleMock.runSimple( expectedMetadataQuery, AllowAllAuthenticator.ALLOW_ALL_RESULT, - AuthorizationResult.SUPERUSER + AuthorizationResult.ALLOW_NO_RESTRICTION )) .andReturn(QueryResponse.withEmptyContext(Sequences.empty())); @@ -1072,7 +1072,7 @@ public void testRunSegmentMetadataQueryWithContext() throws Exception EasyMock.expect(lifecycleMock.runSimple( expectedMetadataQuery, AllowAllAuthenticator.ALLOW_ALL_RESULT, - AuthorizationResult.SUPERUSER + AuthorizationResult.ALLOW_NO_RESTRICTION )) .andReturn(QueryResponse.withEmptyContext(Sequences.empty())); @@ -1255,7 +1255,7 @@ public void testTombstoneSegmentIsNotRefreshed() throws IOException EasyMock.expect(lifecycleMock.runSimple( expectedMetadataQuery, AllowAllAuthenticator.ALLOW_ALL_RESULT, - AuthorizationResult.SUPERUSER + AuthorizationResult.ALLOW_NO_RESTRICTION )) .andReturn(QueryResponse.withEmptyContext(Sequences.empty())); From f1aa7918d59fac5bf94c1b74a6ce89ce42cc5647 Mon Sep 17 00:00:00 2001 From: cecemei Date: Thu, 19 Dec 2024 23:33:22 -0800 Subject: [PATCH 13/32] revert change in test as well --- .../metadata/SegmentMetadataQueryTest.java | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/processing/src/test/java/org/apache/druid/query/metadata/SegmentMetadataQueryTest.java b/processing/src/test/java/org/apache/druid/query/metadata/SegmentMetadataQueryTest.java index 1e5160e2ed83..1abe8ec08c29 100644 --- a/processing/src/test/java/org/apache/druid/query/metadata/SegmentMetadataQueryTest.java +++ b/processing/src/test/java/org/apache/druid/query/metadata/SegmentMetadataQueryTest.java @@ -52,13 +52,11 @@ import org.apache.druid.query.Result; import org.apache.druid.query.TableDataSource; import org.apache.druid.query.aggregation.AggregatorFactory; -import org.apache.druid.query.filter.NullFilter; import org.apache.druid.query.metadata.metadata.AggregatorMergeStrategy; import org.apache.druid.query.metadata.metadata.ColumnAnalysis; import org.apache.druid.query.metadata.metadata.ListColumnIncluderator; import org.apache.druid.query.metadata.metadata.SegmentAnalysis; import org.apache.druid.query.metadata.metadata.SegmentMetadataQuery; -import org.apache.druid.query.policy.Policy; import org.apache.druid.query.spec.LegacySegmentSpec; import org.apache.druid.segment.IncrementalIndexSegment; import org.apache.druid.segment.QueryableIndex; @@ -88,7 +86,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -332,30 +329,6 @@ public void testSegmentMetadataQuery() Assert.assertEquals(Collections.singletonList(expectedSegmentAnalysis1), results); } - @Test - public void testSegmentMetadataQueryWorksWithRestrictions() throws Exception - { - ImmutableMap> noRestriction = ImmutableMap.of(DATASOURCE, Optional.empty()); - ImmutableMap> alwaysTrueRestriction = ImmutableMap.of(DATASOURCE, Optional.of( - Policy.NO_RESTRICTION)); - ImmutableMap> withRestriction = ImmutableMap.of( - DATASOURCE, - Optional.of(Policy.fromRowFilter( - new NullFilter("some-column", null))) - ); - List results1 = runner1.run(QueryPlus.wrap(testQuery.withPolicyRestrictions(noRestriction, Policy.TablePolicySecurityLevel.POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST))) - .toList(); - List results2 = runner1.run(QueryPlus.wrap(testQuery.withPolicyRestrictions(alwaysTrueRestriction, Policy.TablePolicySecurityLevel.POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST))) - .toList(); - - Assert.assertEquals(Collections.singletonList(expectedSegmentAnalysis1), results1); - Assert.assertEquals(Collections.singletonList(expectedSegmentAnalysis1), results2); - Assert.assertThrows( - RuntimeException.class, - () -> runner1.run(QueryPlus.wrap(testQuery.withPolicyRestrictions(withRestriction, Policy.TablePolicySecurityLevel.POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST))) - ); - } - @Test public void testSegmentMetadataQueryWithRollupMerge() { From a1b3b06313f15207df8ab6c58bb02fbde4179176 Mon Sep 17 00:00:00 2001 From: cecemei Date: Fri, 20 Dec 2024 12:06:08 -0800 Subject: [PATCH 14/32] clean up a bit --- .../msq/dart/controller/http/DartSqlResource.java | 3 +-- .../druid/server/security/AuthorizationResult.java | 12 ++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/http/DartSqlResource.java b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/http/DartSqlResource.java index 3630e2c1d056..7cbdc55e59f2 100644 --- a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/http/DartSqlResource.java +++ b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/http/DartSqlResource.java @@ -175,8 +175,7 @@ public GetQueriesResponse doGetRunningQueries( queries.sort(Comparator.comparing(DartQueryInfo::getStartTime).thenComparing(DartQueryInfo::getDartQueryId)); final GetQueriesResponse response; - boolean hasFullPermission = !stateReadAccess.getPermissionErrorMessage(true).isPresent(); - if (hasFullPermission) { + if (stateReadAccess.isUserWithNoRestriction()) { // User can READ STATE, so they can see all running queries, as well as authentication details. response = new GetQueriesResponse(queries); } else { diff --git a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java index 8304812a7d3a..fb0156600619 100644 --- a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java +++ b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java @@ -142,6 +142,11 @@ public AuthorizationResult withResourceActions( ); } + /** + * Returns true if the policy restrictions indicates that all resources are one of the following: + *
  • no policy found + *
  • the user has a no-restriction policy + */ public boolean isUserWithNoRestriction() { return policyRestrictions.values() @@ -173,12 +178,7 @@ public Optional getPermissionErrorMessage(boolean policyRestrictionsNotP case DENY: return Optional.of(Objects.requireNonNull(failureMessage)); case ALLOW_WITH_RESTRICTION: - if (policyRestrictionsNotPermitted && policyRestrictions.values() - .stream() - .flatMap(policy -> policy.isPresent() - ? Stream.of(policy.get()) - : Stream.empty()) // Can be replaced by Optional.stream after Java 11 - .anyMatch(p -> !p.hasNoRestriction())) { + if (policyRestrictionsNotPermitted && !isUserWithNoRestriction()) { return Optional.of(Access.DEFAULT_ERROR_MESSAGE); } return Optional.empty(); From 17313d95e469d76c6266ca6a1862ce9ae9adad27 Mon Sep 17 00:00:00 2001 From: cecemei Date: Fri, 20 Dec 2024 14:12:56 -0800 Subject: [PATCH 15/32] updated the AuthorizationResult class a bit, along with some tests --- .../server/security/AuthorizationResult.java | 78 ++++++++++++++----- .../AuthorizationResultTest.java | 9 +++ 2 files changed, 69 insertions(+), 18 deletions(-) diff --git a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java index fb0156600619..e8ab7d409aa1 100644 --- a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java +++ b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java @@ -19,6 +19,8 @@ package org.apache.druid.server.security; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import org.apache.druid.error.DruidException; import org.apache.druid.query.policy.Policy; @@ -30,7 +32,6 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.stream.Stream; /** * Represents the outcoming of performing authorization check on required resource accesses on a query or http requests. @@ -102,16 +103,21 @@ enum PERMISSION this.allResourceActions = allResourceActions; // sanity check - if (failureMessage != null && !PERMISSION.DENY.equals(permission)) { - throw DruidException.defensive("Failure message should only be set for DENY permission"); - } else if (PERMISSION.DENY.equals(permission) && failureMessage == null) { - throw DruidException.defensive("Failure message must be set for DENY permission"); - } - - if (!policyRestrictions.isEmpty() && !PERMISSION.ALLOW_WITH_RESTRICTION.equals(permission)) { - throw DruidException.defensive("Policy restrictions should only be set for ALLOW_WITH_RESTRICTION permission"); - } else if (PERMISSION.ALLOW_WITH_RESTRICTION.equals(permission) && policyRestrictions.isEmpty()) { - throw DruidException.defensive("Policy restrictions must be set for ALLOW_WITH_RESTRICTION permission"); + switch (permission) { + case DENY: + validateFailureMessageIsSet(); + validatePolicyRestrictionEmpty(); + return; + case ALLOW_WITH_RESTRICTION: + validateFailureMessageNull(); + validatePolicyRestrictionNonEmpty(); + return; + case ALLOW_NO_RESTRICTION: + validateFailureMessageNull(); + validatePolicyRestrictionEmpty(); + return; + default: + throw DruidException.defensive("unreachable"); } } @@ -143,18 +149,18 @@ public AuthorizationResult withResourceActions( } /** - * Returns true if the policy restrictions indicates that all resources are one of the following: + * Returns true if user has the correct permission, and the policy restrictions indicates one of the following: *
  • no policy found *
  • the user has a no-restriction policy */ public boolean isUserWithNoRestriction() { - return policyRestrictions.values() - .stream() - .flatMap(policy -> policy.isPresent() - ? Stream.of(policy.get()) - : Stream.empty()) // Can be replaced by Optional.stream after Java 11 - .allMatch(Policy::hasNoRestriction); + return PERMISSION.ALLOW_NO_RESTRICTION.equals(permission) || (PERMISSION.ALLOW_WITH_RESTRICTION.equals(permission) + && policyRestrictions.values() + .stream() + .map(p -> p.orElse(null)) + .filter(Objects::nonNull) // Can be replaced by Optional::stream after java 11 + .allMatch(Policy::hasNoRestriction)); } /** @@ -242,4 +248,40 @@ public String toString() + allResourceActions + "]"; } + + private void validateFailureMessageIsSet() + { + Preconditions.checkArgument( + !Strings.isNullOrEmpty(failureMessage), + "Failure message must be set for permission[%s]", + permission + ); + } + + private void validateFailureMessageNull() + { + Preconditions.checkArgument( + failureMessage == null, + "Failure message must be null for permission[%s]", + permission + ); + } + + private void validatePolicyRestrictionEmpty() + { + Preconditions.checkArgument( + policyRestrictions.isEmpty(), + "Policy restrictions not allowed for permission[%s]", + permission + ); + } + + private void validatePolicyRestrictionNonEmpty() + { + Preconditions.checkArgument( + !policyRestrictions.isEmpty(), + "Policy restrictions must exist for permission[%s]", + permission + ); + } } diff --git a/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java b/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java index d6eeb69d15eb..0d988180672d 100644 --- a/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java +++ b/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java @@ -35,6 +35,8 @@ import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; @RunWith(JUnitParamsRunner.class) @@ -81,6 +83,7 @@ public void testFailedAccess_withPermissionError(boolean policyRestrictionsNotPe Optional.of("this data source is not permitted"), result.getPermissionErrorMessage(policyRestrictionsNotPermitted) ); + assertFalse(result.isUserWithNoRestriction()); } @Test @@ -98,11 +101,16 @@ public void testFullAccess_noPermissionError(boolean policyRestrictionsNotPermit )); assertEquals(Optional.empty(), result.getPermissionErrorMessage(policyRestrictionsNotPermitted)); + assertTrue(result.isUserWithNoRestriction()); + assertEquals(Optional.empty(), resultWithEmptyPolicy.getPermissionErrorMessage(policyRestrictionsNotPermitted)); + assertTrue(resultWithEmptyPolicy.isUserWithNoRestriction()); + assertEquals( Optional.empty(), resultWithNoRestrictionPolicy.getPermissionErrorMessage(policyRestrictionsNotPermitted) ); + assertTrue(resultWithNoRestrictionPolicy.isUserWithNoRestriction()); } @Test @@ -122,5 +130,6 @@ public void testRestrictedAccess_noPermissionError(boolean policyRestrictionsNot ))) )); assertEquals(error, result.getPermissionErrorMessage(policyRestrictionsNotPermitted).toString()); + assertFalse(result.isUserWithNoRestriction()); } } From 2eaf63aa3ca1dbd0de02899297ce25b346e02f3b Mon Sep 17 00:00:00 2001 From: cecemei Date: Mon, 6 Jan 2025 11:49:15 -0800 Subject: [PATCH 16/32] update on RestrictedSegment to implement SegmentReference directly. --- .../druid/segment/RestrictedSegment.java | 74 ++++++++++++++++++- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/processing/src/main/java/org/apache/druid/segment/RestrictedSegment.java b/processing/src/main/java/org/apache/druid/segment/RestrictedSegment.java index 9935186da446..ed3f6586a25b 100644 --- a/processing/src/main/java/org/apache/druid/segment/RestrictedSegment.java +++ b/processing/src/main/java/org/apache/druid/segment/RestrictedSegment.java @@ -20,11 +20,22 @@ package org.apache.druid.segment; import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.timeline.SegmentId; +import org.joda.time.Interval; +import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.io.Closeable; +import java.io.IOException; +import java.util.Optional; -public class RestrictedSegment extends WrappedSegmentReference +/** + * A wrapped {@link SegmentReference} with a {@link DimFilter} restriction. The restriction must be applied on the + * segment. + */ +public class RestrictedSegment implements SegmentReference { + protected final SegmentReference delegate; @Nullable private final DimFilter filter; @@ -33,10 +44,28 @@ public RestrictedSegment( @Nullable DimFilter filter ) { - super(delegate); + this.delegate = delegate; this.filter = filter; } + @Override + public Optional acquireReferences() + { + return delegate.acquireReferences(); + } + + @Override + public SegmentId getId() + { + return delegate.getId(); + } + + @Override + public Interval getDataInterval() + { + return delegate.getDataInterval(); + } + @Override public CursorFactory asCursorFactory() { @@ -47,6 +76,45 @@ public CursorFactory asCursorFactory() @Override public QueryableIndex asQueryableIndex() { - throw new RuntimeException("Can't get a queryable index from restricted segment."); + return null; + } + + @Nullable + @Override + public T as(@Nonnull Class clazz) + { + if (CursorFactory.class.equals(clazz)) { + return (T) asCursorFactory(); + } else if (QueryableIndex.class.equals(clazz)) { + return null; + } else if (TimeBoundaryInspector.class.equals(clazz)) { + return (T) WrappedTimeBoundaryInspector.create(delegate.as(TimeBoundaryInspector.class)); + } else if (TopNOptimizationInspector.class.equals(clazz)) { + return (T) new SimpleTopNOptimizationInspector(filter == null); + } + + // Unless we know there's no restriction, it's dangerous to return the implementation of a particular interface. + if (filter == null) { + return delegate.as(clazz); + } + return null; + } + + @Override + public boolean isTombstone() + { + return delegate.isTombstone(); + } + + @Override + public void close() throws IOException + { + delegate.close(); + } + + @Override + public String asString() + { + return delegate.asString(); } } From 64074790a67dfc7f7c6744f635af8e825cb07310 Mon Sep 17 00:00:00 2001 From: cecemei Date: Tue, 7 Jan 2025 13:39:32 -0800 Subject: [PATCH 17/32] Update Policy inteface and added NoRestrictionPolicy and RowFilterPolicy class. Updated AuthorizationResult class as well. --- .../apache/druid/grpc/server/QueryDriver.java | 2 +- .../basic/BasicSecurityResourceFilter.java | 6 +- .../druid/catalog/http/CatalogResource.java | 6 +- .../dart/controller/http/DartSqlResource.java | 2 +- .../dart/controller/sql/DartQueryMaker.java | 6 +- .../druid/msq/rpc/MSQResourceUtils.java | 12 +-- .../druid/msq/sql/MSQTaskQueryMaker.java | 6 +- .../sql/resources/SqlStatementResource.java | 4 +- .../indexing/common/task/IndexTaskUtils.java | 6 +- .../overlord/http/OverlordResource.java | 18 ++--- .../security/SupervisorResourceFilter.java | 6 +- .../http/security/TaskResourceFilter.java | 6 +- .../overlord/sampler/SamplerResource.java | 6 +- .../supervisor/SupervisorResource.java | 6 +- .../druid/query/RestrictedDataSource.java | 20 +++-- .../query/policy/NoRestrictionPolicy.java | 60 ++++++++++++++ .../org/apache/druid/query/policy/Policy.java | 80 +++---------------- .../druid/query/policy/RowFilterPolicy.java | 79 ++++++++++++++++++ .../segment/BypassRestrictedSegment.java | 41 ++++++++++ .../segment/RestrictedCursorFactory.java | 32 +++++--- .../druid/segment/RestrictedSegment.java | 18 +++-- .../apache/druid/query/DataSourceTest.java | 40 ++++++---- .../druid/query/JoinDataSourceTest.java | 4 +- .../druid/query/RestrictedDataSourceTest.java | 13 +-- .../druid/segment/realtime/ChatHandlers.java | 6 +- .../apache/druid/server/QueryLifecycle.java | 2 +- .../apache/druid/server/QueryResource.java | 12 +-- .../http/security/ConfigResourceFilter.java | 6 +- .../security/DatasourceResourceFilter.java | 6 +- .../http/security/RulesResourceFilter.java | 6 +- .../http/security/StateResourceFilter.java | 6 +- .../apache/druid/server/security/Access.java | 3 +- .../server/security/AuthorizationResult.java | 35 ++++---- .../server/security/AuthorizationUtils.java | 8 +- .../AuthorizationResultTest.java | 55 +++++++------ .../druid/server/QueryLifecycleTest.java | 62 +++++++------- .../security/ForbiddenExceptionTest.java | 8 +- .../apache/druid/sql/AbstractStatement.java | 6 +- .../sql/calcite/schema/SystemSchema.java | 6 +- .../apache/druid/sql/http/SqlResource.java | 2 +- .../druid/sql/calcite/util/CalciteTests.java | 6 +- 41 files changed, 424 insertions(+), 290 deletions(-) create mode 100644 processing/src/main/java/org/apache/druid/query/policy/NoRestrictionPolicy.java create mode 100644 processing/src/main/java/org/apache/druid/query/policy/RowFilterPolicy.java create mode 100644 processing/src/main/java/org/apache/druid/segment/BypassRestrictedSegment.java diff --git a/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java b/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java index 50c426a70b52..12e6c470baaf 100644 --- a/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java +++ b/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java @@ -148,7 +148,7 @@ private QueryResponse runNativeQuery(QueryRequest request, AuthenticationResult try { queryLifecycle.initialize(query); AuthorizationResult authorizationResult = queryLifecycle.authorize(authResult); - if (authorizationResult.getPermissionErrorMessage(true).isPresent()) { + if (!authorizationResult.isUserWithNoRestriction()) { throw new ForbiddenException(Access.DEFAULT_ERROR_MESSAGE); } queryResponse = queryLifecycle.execute(); 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 98046bc931e1..3a5651e09fcf 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 @@ -60,14 +60,14 @@ public ContainerRequest filter(ContainerRequest request) getAuthorizerMapper() ); - authResult.getPermissionErrorMessage(true).ifPresent(error -> { + if (!authResult.isUserWithNoRestriction()) { throw new WebApplicationException( Response.status(Response.Status.FORBIDDEN) .type(MediaType.TEXT_PLAIN) - .entity(StringUtils.format("Access-Check-Result: %s", error)) + .entity(StringUtils.format("Access-Check-Result: %s", authResult.getErrorMessage())) .build() ); - }); + } return request; } diff --git a/extensions-core/druid-catalog/src/main/java/org/apache/druid/catalog/http/CatalogResource.java b/extensions-core/druid-catalog/src/main/java/org/apache/druid/catalog/http/CatalogResource.java index 0435ef9e2f8d..10fc797b0ef9 100644 --- a/extensions-core/druid-catalog/src/main/java/org/apache/druid/catalog/http/CatalogResource.java +++ b/extensions-core/druid-catalog/src/main/java/org/apache/druid/catalog/http/CatalogResource.java @@ -581,9 +581,9 @@ private void authorizeTable( private void authorize(String resource, String key, Action action, HttpServletRequest request) { final AuthorizationResult authResult = authorizeAccess(resource, key, action, request); - authResult.getPermissionErrorMessage(true).ifPresent(error -> { - throw new ForbiddenException(error); - }); + if (!authResult.isUserWithNoRestriction()) { + throw new ForbiddenException(authResult.getErrorMessage()); + } } private AuthorizationResult authorizeAccess(String resource, String key, Action action, HttpServletRequest request) diff --git a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/http/DartSqlResource.java b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/http/DartSqlResource.java index 7cbdc55e59f2..59b4e8e81b41 100644 --- a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/http/DartSqlResource.java +++ b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/http/DartSqlResource.java @@ -247,7 +247,7 @@ public Response cancelQuery( final AuthorizationResult authResult = authorizeCancellation(req, cancelables); - if (!authResult.getPermissionErrorMessage(true).isPresent()) { + if (authResult.isUserWithNoRestriction()) { sqlLifecycleManager.removeAll(sqlQueryId, cancelables); // Don't call cancel() on the cancelables. That just cancels native queries, which is useless here. Instead, diff --git a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/sql/DartQueryMaker.java b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/sql/DartQueryMaker.java index e0a5d018a90a..6f63de0f5668 100644 --- a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/sql/DartQueryMaker.java +++ b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/sql/DartQueryMaker.java @@ -128,9 +128,9 @@ public DartQueryMaker( @Override public QueryResponse runQuery(DruidQuery druidQuery) { - plannerContext.getAuthorizationResult().getPermissionErrorMessage(true).ifPresent(error -> { - throw new ForbiddenException(error); - }); + if (!plannerContext.getAuthorizationResult().isUserWithNoRestriction()) { + throw new ForbiddenException(plannerContext.getAuthorizationResult().getErrorMessage()); + } final MSQSpec querySpec = MSQTaskQueryMaker.makeQuerySpec( null, druidQuery, diff --git a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/rpc/MSQResourceUtils.java b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/rpc/MSQResourceUtils.java index 795f0fbdaa85..cf74413d244b 100644 --- a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/rpc/MSQResourceUtils.java +++ b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/rpc/MSQResourceUtils.java @@ -47,9 +47,9 @@ public static void authorizeAdminRequest( authorizerMapper ); - authResult.getPermissionErrorMessage(true).ifPresent(error -> { - throw new ForbiddenException(error); - }); + if (!authResult.isUserWithNoRestriction()) { + throw new ForbiddenException(authResult.getErrorMessage()); + } } public static void authorizeQueryRequest( @@ -67,8 +67,8 @@ public static void authorizeQueryRequest( authorizerMapper ); - authResult.getPermissionErrorMessage(true).ifPresent(error -> { - throw new ForbiddenException(error); - }); + if (!authResult.isUserWithNoRestriction()) { + throw new ForbiddenException(authResult.getErrorMessage()); + } } } diff --git a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/MSQTaskQueryMaker.java b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/MSQTaskQueryMaker.java index e6ecd5a5785f..8c201808bc70 100644 --- a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/MSQTaskQueryMaker.java +++ b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/MSQTaskQueryMaker.java @@ -117,9 +117,9 @@ public class MSQTaskQueryMaker implements QueryMaker @Override public QueryResponse runQuery(final DruidQuery druidQuery) { - plannerContext.getAuthorizationResult().getPermissionErrorMessage(true).ifPresent(error -> { - throw new ForbiddenException(error); - }); + if (!plannerContext.getAuthorizationResult().isUserWithNoRestriction()) { + throw new ForbiddenException(plannerContext.getAuthorizationResult().getErrorMessage()); + } Hook.QUERY_PLAN.run(druidQuery.getQuery()); plannerContext.dispatchHook(DruidHook.NATIVE_PLAN, druidQuery.getQuery()); diff --git a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlStatementResource.java b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlStatementResource.java index 0bf82ea97cb9..918dd8a1da0c 100644 --- a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlStatementResource.java +++ b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlStatementResource.java @@ -681,13 +681,13 @@ private MSQControllerTask getMSQControllerTaskAndCheckPermission( authorizerMapper ); - authResult.getPermissionErrorMessage(true).ifPresent(error -> { + if (!authResult.isUserWithNoRestriction()) { throw new ForbiddenException(StringUtils.format( "The current user[%s] cannot view query id[%s] since the query is owned by another user", currentUser, queryId )); - }); + } return msqControllerTask; } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/common/task/IndexTaskUtils.java b/indexing-service/src/main/java/org/apache/druid/indexing/common/task/IndexTaskUtils.java index b1bd4af65b33..aa29a22129c2 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/common/task/IndexTaskUtils.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/common/task/IndexTaskUtils.java @@ -80,9 +80,9 @@ public static AuthorizationResult datasourceAuthorizationCheck( ); AuthorizationResult authResult = AuthorizationUtils.authorizeResourceAction(req, resourceAction, authorizerMapper); - authResult.getPermissionErrorMessage(true).ifPresent(error -> { - throw new ForbiddenException(error); - }); + if (!authResult.isUserWithNoRestriction()) { + throw new ForbiddenException(authResult.getErrorMessage()); + } return authResult; } 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 f72df2febc92..a0e608b4b6ef 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 @@ -182,9 +182,9 @@ public Response taskPost( resourceActions, authorizerMapper ); - authResult.getPermissionErrorMessage(true).ifPresent(error -> { - throw new ForbiddenException(error); - }); + if (!authResult.isUserWithNoRestriction()) { + throw new ForbiddenException(authResult.getErrorMessage()); + } return asLeaderWith( taskMaster.getTaskQueue(), @@ -614,14 +614,14 @@ public Response getTasks( authorizerMapper ); - authResult.getPermissionErrorMessage(true).ifPresent(error -> { + if (!authResult.isUserWithNoRestriction()) { throw new WebApplicationException( Response.status(Response.Status.FORBIDDEN) .type(MediaType.TEXT_PLAIN) - .entity(StringUtils.format("Access-Check-Result: %s", error)) + .entity(StringUtils.format("Access-Check-Result: %s", authResult.getErrorMessage())) .build() ); - }); + } } return asLeaderWith( @@ -663,9 +663,9 @@ public Response killPendingSegments( authorizerMapper ); - authResult.getPermissionErrorMessage(true).ifPresent(error -> { - throw new ForbiddenException(error); - }); + if (!authResult.isUserWithNoRestriction()) { + throw new ForbiddenException(authResult.getErrorMessage()); + } if (overlord.isLeader()) { try { diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/SupervisorResourceFilter.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/SupervisorResourceFilter.java index 3cd8a3c993da..07e83946689f 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/SupervisorResourceFilter.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/SupervisorResourceFilter.java @@ -103,9 +103,9 @@ public boolean apply(PathSegment input) getAuthorizerMapper() ); - authResult.getPermissionErrorMessage(true).ifPresent(error -> { - throw new ForbiddenException(error); - }); + if (!authResult.isUserWithNoRestriction()) { + throw new ForbiddenException(authResult.getErrorMessage()); + } return request; } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/TaskResourceFilter.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/TaskResourceFilter.java index 7e431dca8797..c6f410b6634d 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/TaskResourceFilter.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/TaskResourceFilter.java @@ -98,9 +98,9 @@ public ContainerRequest filter(ContainerRequest request) getAuthorizerMapper() ); - authResult.getPermissionErrorMessage(true).ifPresent(error -> { - throw new ForbiddenException(error); - }); + if (!authResult.isUserWithNoRestriction()) { + throw new ForbiddenException(authResult.getErrorMessage()); + } return request; } 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 de2f26c3923c..3591c36f9a40 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 @@ -78,9 +78,9 @@ public SamplerResponse post(final SamplerSpec sampler, @Context final HttpServle authorizerMapper ); - authResult.getPermissionErrorMessage(true).ifPresent(error -> { - throw new ForbiddenException(error); - }); + if (!authResult.isUserWithNoRestriction()) { + throw new ForbiddenException(authResult.getErrorMessage()); + } return sampler.sample(); } } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java index 487dbd4eeaa3..934008926272 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java @@ -148,9 +148,9 @@ public Response specPost(final SupervisorSpec spec, @Context final HttpServletRe authorizerMapper ); - authResult.getPermissionErrorMessage(true).ifPresent(error -> { - throw new ForbiddenException(error); - }); + if (!authResult.isUserWithNoRestriction()) { + throw new ForbiddenException(authResult.getErrorMessage()); + } manager.createOrUpdateAndStartSupervisor(spec); diff --git a/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java b/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java index 10d14a112dd9..9c6a2bebba84 100644 --- a/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java +++ b/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java @@ -22,10 +22,13 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.collect.ImmutableList; +import org.apache.druid.error.DruidException; import org.apache.druid.java.util.common.IAE; import org.apache.druid.java.util.common.ISE; import org.apache.druid.query.planning.DataSourceAnalysis; +import org.apache.druid.query.policy.NoRestrictionPolicy; import org.apache.druid.query.policy.Policy; +import org.apache.druid.query.policy.RowFilterPolicy; import org.apache.druid.segment.RestrictedSegment; import org.apache.druid.segment.SegmentReference; import org.apache.druid.utils.JvmUtils; @@ -75,7 +78,7 @@ public static RestrictedDataSource create( ) { if (!(base instanceof TableDataSource)) { - throw new IAE("Expected a TableDataSource, got [%s]", base.getClass()); + throw new IAE("Expected a TableDataSource, got data source type [%s]", base.getClass()); } if (Objects.isNull(policy)) { throw new IAE("Policy can't be null for RestrictedDataSource"); @@ -134,7 +137,7 @@ public Function createSegmentMapFunction( () -> base.createSegmentMapFunction( query, cpuTimeAccumulator - ).andThen((segment) -> (new RestrictedSegment(segment, policy.getRowFilter()))) + ).andThen((segment) -> (new RestrictedSegment(segment, policy))) ); } @@ -157,13 +160,18 @@ public DataSource mapWithRestriction( Optional newPolicy = policyMap.getOrDefault(base.getName(), Optional.empty()); if (!newPolicy.isPresent()) { throw new ISE( - "No restriction found on table [%s], but had %s before.", + "No restriction found on table [%s], but had policy [%s] before.", base.getName(), policy ); } - if (!Policy.NO_RESTRICTION.equals(newPolicy.get())) { - throw new ISE("Multiple restrictions on [%s]: %s and %s", base.getName(), policy, newPolicy.get()); + if (!(newPolicy.get() instanceof NoRestrictionPolicy)) { + throw new ISE( + "Multiple restrictions on table [%s]: policy [%s] and policy [%s]", + base.getName(), + policy, + newPolicy.get() + ); } // The only happy path is, newPolicy is Policy.NO_RESTRICTION, which means this comes from an anthenticated and // authorized druid-internal request. @@ -175,7 +183,7 @@ public String toString() { return "RestrictedDataSource{" + "base=" + base + - ", policy='" + policy + '}'; + ", policy='" + policy + "'}"; } @Override diff --git a/processing/src/main/java/org/apache/druid/query/policy/NoRestrictionPolicy.java b/processing/src/main/java/org/apache/druid/query/policy/NoRestrictionPolicy.java new file mode 100644 index 000000000000..abe73349cb88 --- /dev/null +++ b/processing/src/main/java/org/apache/druid/query/policy/NoRestrictionPolicy.java @@ -0,0 +1,60 @@ +/* + * 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.query.policy; + +import com.fasterxml.jackson.annotation.JsonCreator; + +/** + * Represents a special kind of policy restriction, indicating that this table is restricted, but doesn't impose any restriction + * to a user. + */ +public class NoRestrictionPolicy implements Policy +{ + public static final NoRestrictionPolicy INSTANCE = new NoRestrictionPolicy(); + + @JsonCreator + NoRestrictionPolicy() + { + } + + @Override + public String toString() + { + return "NO_RESTRICTION"; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return true; + } + + @Override + public int hashCode() + { + return 0; + } +} diff --git a/processing/src/main/java/org/apache/druid/query/policy/Policy.java b/processing/src/main/java/org/apache/druid/query/policy/Policy.java index 516f6d85a631..ae74de46862b 100644 --- a/processing/src/main/java/org/apache/druid/query/policy/Policy.java +++ b/processing/src/main/java/org/apache/druid/query/policy/Policy.java @@ -19,18 +19,18 @@ package org.apache.druid.query.policy; -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import org.apache.druid.query.filter.DimFilter; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; /** * Represents a granular-level (e.x. row filter) restriction on read-table access. */ -public class Policy +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = RowFilterPolicy.class, name = "row"), + @JsonSubTypes.Type(value = NoRestrictionPolicy.class, name = "noRestriction") +}) +public interface Policy { /** * Defines how strict we want to enforce the policy on tables during query execution process. @@ -40,7 +40,7 @@ public class Policy *
  • {@code POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST}, every table must have a policy when requests come from external users. * */ - public enum TablePolicySecurityLevel + enum TablePolicySecurityLevel { APPLY_WHEN_APPLICABLE(0), POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY(1), @@ -71,66 +71,4 @@ public boolean policyMustBeCheckedAndExistOnAllTables() return securityLevel >= 2; } } - - /** - * A special kind of policy restriction, indicating that this table is restricted, but doesn't impose any restriction - * to a user. - */ - public static final Policy NO_RESTRICTION = new Policy(null); - - @JsonProperty("rowFilter") - private final DimFilter rowFilter; - - @JsonCreator - Policy(@Nullable @JsonProperty("rowFilter") DimFilter rowFilter) - { - this.rowFilter = rowFilter; - } - - public static Policy fromRowFilter(@Nonnull DimFilter rowFilter) - { - return new Policy(rowFilter); - } - - @Nullable - public DimFilter getRowFilter() - { - return rowFilter; - } - - /** - * Returns true if the policy imposes no restrictions. - */ - public boolean hasNoRestriction() - { - if (NO_RESTRICTION.equals(this)) { - return true; - } - return false; - } - - @Override - public String toString() - { - return "Policy{" + "rowFilter=" + rowFilter + '}'; - } - - @Override - public boolean equals(Object o) - { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Policy that = (Policy) o; - return Objects.equals(rowFilter, that.rowFilter); - } - - @Override - public int hashCode() - { - return Objects.hash(rowFilter); - } } diff --git a/processing/src/main/java/org/apache/druid/query/policy/RowFilterPolicy.java b/processing/src/main/java/org/apache/druid/query/policy/RowFilterPolicy.java new file mode 100644 index 000000000000..ca9c03419588 --- /dev/null +++ b/processing/src/main/java/org/apache/druid/query/policy/RowFilterPolicy.java @@ -0,0 +1,79 @@ +/* + * 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.query.policy; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.druid.query.filter.DimFilter; + +import javax.annotation.Nonnull; +import java.util.Objects; + +/** + * Represents a basic row filter policy restriction. + */ +public class RowFilterPolicy implements Policy +{ + @JsonProperty("rowFilter") + private final DimFilter rowFilter; + + @JsonCreator + RowFilterPolicy(@Nonnull @JsonProperty("rowFilter") DimFilter rowFilter) + { + this.rowFilter = rowFilter; + } + + public static RowFilterPolicy from(@Nonnull DimFilter rowFilter) + { + return new RowFilterPolicy(rowFilter); + } + + @Nonnull + public DimFilter getRowFilter() + { + return rowFilter; + } + + @Override + public String toString() + { + return "RowFilterPolicy{" + "rowFilter=" + rowFilter + '}'; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RowFilterPolicy that = (RowFilterPolicy) o; + return Objects.equals(rowFilter, that.rowFilter); + } + + @Override + public int hashCode() + { + return Objects.hash(rowFilter); + } + +} diff --git a/processing/src/main/java/org/apache/druid/segment/BypassRestrictedSegment.java b/processing/src/main/java/org/apache/druid/segment/BypassRestrictedSegment.java new file mode 100644 index 000000000000..ecd1d2614f3f --- /dev/null +++ b/processing/src/main/java/org/apache/druid/segment/BypassRestrictedSegment.java @@ -0,0 +1,41 @@ +package org.apache.druid.segment; + +import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.policy.Policy; + +/** + * A wrapped {@link SegmentReference} with a {@link DimFilter} restriction, and the policy restriction can be bypassed. + *

    + * In some methods, such as {@link #as(Class)}, {@link #asQueryableIndex()}, and {@link #asCursorFactory()}, the policy + * is ignored. + */ +class BypassRestrictedSegment extends RestrictedSegment +{ + public BypassRestrictedSegment(SegmentReference delegate, Policy policy) + { + super(delegate, policy); + } + + public Policy getPolicy() + { + return policy; + } + + @Override + public CursorFactory asCursorFactory() + { + return delegate.asCursorFactory(); + } + + @Override + public QueryableIndex asQueryableIndex() + { + return delegate.asQueryableIndex(); + } + + @Override + public T as(Class clazz) + { + return delegate.as(clazz); + } +} diff --git a/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java b/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java index f29fce0780d5..2ab5f41fd6ba 100644 --- a/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java +++ b/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java @@ -20,8 +20,12 @@ package org.apache.druid.segment; import com.google.common.collect.ImmutableList; +import org.apache.druid.error.DruidException; import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.Filter; +import org.apache.druid.query.policy.NoRestrictionPolicy; +import org.apache.druid.query.policy.Policy; +import org.apache.druid.query.policy.RowFilterPolicy; import org.apache.druid.segment.column.ColumnCapabilities; import org.apache.druid.segment.column.RowSignature; import org.apache.druid.segment.filter.AndFilter; @@ -31,32 +35,34 @@ public class RestrictedCursorFactory implements CursorFactory { private final CursorFactory delegate; - @Nullable - private final DimFilter filter; + private final Policy policy; public RestrictedCursorFactory( CursorFactory delegate, - @Nullable DimFilter filter + Policy policy ) { this.delegate = delegate; - this.filter = filter; + this.policy = policy; } @Override public CursorHolder makeCursorHolder(CursorBuildSpec spec) { - if (filter == null) { + if (policy instanceof NoRestrictionPolicy) { return delegate.makeCursorHolder(spec); - } + } else if (policy instanceof RowFilterPolicy) { + final Filter rowFilter = ((RowFilterPolicy) policy).getRowFilter().toFilter(); + final CursorBuildSpec.CursorBuildSpecBuilder buildSpecBuilder = CursorBuildSpec.builder(spec); + final Filter newFilter = spec.getFilter() == null + ? rowFilter + : new AndFilter(ImmutableList.of(spec.getFilter(), rowFilter)); + buildSpecBuilder.setFilter(newFilter); - final CursorBuildSpec.CursorBuildSpecBuilder buildSpecBuilder = CursorBuildSpec.builder(spec); - final Filter newFilter = spec.getFilter() == null - ? filter.toFilter() - : new AndFilter(ImmutableList.of(spec.getFilter(), filter.toFilter())); - buildSpecBuilder.setFilter(newFilter); - - return delegate.makeCursorHolder(buildSpecBuilder.build()); + return delegate.makeCursorHolder(buildSpecBuilder.build()); + } else { + throw DruidException.defensive("not supported policy type [%s]", policy.getClass()); + } } @Override diff --git a/processing/src/main/java/org/apache/druid/segment/RestrictedSegment.java b/processing/src/main/java/org/apache/druid/segment/RestrictedSegment.java index ed3f6586a25b..92e7b47bc6cd 100644 --- a/processing/src/main/java/org/apache/druid/segment/RestrictedSegment.java +++ b/processing/src/main/java/org/apache/druid/segment/RestrictedSegment.java @@ -20,6 +20,8 @@ package org.apache.druid.segment; import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.policy.NoRestrictionPolicy; +import org.apache.druid.query.policy.Policy; import org.apache.druid.timeline.SegmentId; import org.joda.time.Interval; @@ -36,16 +38,15 @@ public class RestrictedSegment implements SegmentReference { protected final SegmentReference delegate; - @Nullable - private final DimFilter filter; + protected final Policy policy; public RestrictedSegment( SegmentReference delegate, - @Nullable DimFilter filter + Policy policy ) { this.delegate = delegate; - this.filter = filter; + this.policy = policy; } @Override @@ -69,7 +70,7 @@ public Interval getDataInterval() @Override public CursorFactory asCursorFactory() { - return new RestrictedCursorFactory(delegate.asCursorFactory(), filter); + return new RestrictedCursorFactory(delegate.asCursorFactory(), policy); } @Nullable @@ -90,11 +91,14 @@ public T as(@Nonnull Class clazz) } else if (TimeBoundaryInspector.class.equals(clazz)) { return (T) WrappedTimeBoundaryInspector.create(delegate.as(TimeBoundaryInspector.class)); } else if (TopNOptimizationInspector.class.equals(clazz)) { - return (T) new SimpleTopNOptimizationInspector(filter == null); + return (T) new SimpleTopNOptimizationInspector(policy instanceof NoRestrictionPolicy); + } else if (BypassRestrictedSegment.class.equals(clazz)) { + // A backdoor solution to get the wrapped segment, effectively bypassing the policy. + return (T) new BypassRestrictedSegment(delegate, policy); } // Unless we know there's no restriction, it's dangerous to return the implementation of a particular interface. - if (filter == null) { + if (policy instanceof NoRestrictionPolicy) { return delegate.as(clazz); } return null; diff --git a/processing/src/test/java/org/apache/druid/query/DataSourceTest.java b/processing/src/test/java/org/apache/druid/query/DataSourceTest.java index 71b898daa3b9..fa907db41efe 100644 --- a/processing/src/test/java/org/apache/druid/query/DataSourceTest.java +++ b/processing/src/test/java/org/apache/druid/query/DataSourceTest.java @@ -32,7 +32,9 @@ import org.apache.druid.query.filter.NullFilter; import org.apache.druid.query.filter.TrueDimFilter; import org.apache.druid.query.groupby.GroupByQuery; +import org.apache.druid.query.policy.NoRestrictionPolicy; import org.apache.druid.query.policy.Policy; +import org.apache.druid.query.policy.RowFilterPolicy; import org.apache.druid.segment.TestHelper; import org.junit.Assert; import org.junit.Before; @@ -83,11 +85,12 @@ public void testTableDataSource() throws IOException public void testRestrictedDataSource() throws IOException { DataSource dataSource = JSON_MAPPER.readValue( - "{\"type\":\"restrict\",\"base\":{\"type\":\"table\",\"name\":\"somedatasource\"},\"policy\":{\"rowFilter\":null}}", + "{\"type\":\"restrict\",\"base\":{\"type\":\"table\",\"name\":\"somedatasource\"},\"policy\":{\"type\":\"noRestriction\"}}\n", DataSource.class ); + Assert.assertEquals( - RestrictedDataSource.create(TableDataSource.create("somedatasource"), Policy.NO_RESTRICTION), + RestrictedDataSource.create(TableDataSource.create("somedatasource"), NoRestrictionPolicy.INSTANCE), dataSource ); } @@ -145,11 +148,11 @@ public void testMapWithRestriction(Policy.TablePolicySecurityLevel securityLevel UnionDataSource unionDataSource = new UnionDataSource(Lists.newArrayList(table1, table2, table3)); ImmutableMap> restrictions = ImmutableMap.of( "table1", - Optional.of(Policy.NO_RESTRICTION), + Optional.of(NoRestrictionPolicy.INSTANCE), "table2", - Optional.of(Policy.NO_RESTRICTION), + Optional.of(NoRestrictionPolicy.INSTANCE), "table3", - Optional.of(Policy.fromRowFilter(new NullFilter( + Optional.of(RowFilterPolicy.from(new NullFilter( "some-column", null ))) @@ -158,9 +161,9 @@ public void testMapWithRestriction(Policy.TablePolicySecurityLevel securityLevel Assert.assertEquals( unionDataSource.mapWithRestriction(restrictions, securityLevel), new UnionDataSource(Lists.newArrayList( - RestrictedDataSource.create(table1, Policy.NO_RESTRICTION), - RestrictedDataSource.create(table2, Policy.NO_RESTRICTION), - RestrictedDataSource.create(table3, Policy.fromRowFilter(new NullFilter("some-column", null)) + RestrictedDataSource.create(table1, NoRestrictionPolicy.INSTANCE), + RestrictedDataSource.create(table2, NoRestrictionPolicy.INSTANCE), + RestrictedDataSource.create(table3, RowFilterPolicy.from(new NullFilter("some-column", null)) ) )) ); @@ -182,7 +185,7 @@ public void testMapWithRestriction_tableMissingRestriction( UnionDataSource unionDataSource = new UnionDataSource(Lists.newArrayList(table1, table2)); ImmutableMap> restrictions = ImmutableMap.of( "table1", - Optional.of(Policy.fromRowFilter(TrueDimFilter.instance())) + Optional.of(RowFilterPolicy.from(TrueDimFilter.instance())) ); if (error.isEmpty()) { @@ -206,7 +209,7 @@ public void testMapWithRestriction_tableWithEmptyPolicy(Policy.TablePolicySecuri UnionDataSource unionDataSource = new UnionDataSource(Lists.newArrayList(table1, table2)); ImmutableMap> restrictions = ImmutableMap.of( "table1", - Optional.of(Policy.NO_RESTRICTION), + Optional.of(NoRestrictionPolicy.INSTANCE), "table2", Optional.empty() ); @@ -214,7 +217,10 @@ public void testMapWithRestriction_tableWithEmptyPolicy(Policy.TablePolicySecuri if (error.isEmpty()) { Assert.assertEquals( unionDataSource.mapWithRestriction(restrictions, securityLevel), - new UnionDataSource(Lists.newArrayList(RestrictedDataSource.create(table1, Policy.NO_RESTRICTION), table2)) + new UnionDataSource(Lists.newArrayList( + RestrictedDataSource.create(table1, NoRestrictionPolicy.INSTANCE), + table2 + )) ); } else { ISE e = Assert.assertThrows( @@ -238,12 +244,12 @@ public void testMapWithRestriction_onRestrictedDataSource_fromDruidSystem(Policy { RestrictedDataSource restrictedDataSource = RestrictedDataSource.create( TableDataSource.create("table1"), - Policy.fromRowFilter(new NullFilter("some-column", null)) + RowFilterPolicy.from(new NullFilter("some-column", null)) ); // The druid-system should get a NO_RESTRICTION policy attached on a table. ImmutableMap> noRestrictionPolicy = ImmutableMap.of( "table1", - Optional.of(Policy.NO_RESTRICTION) + Optional.of(NoRestrictionPolicy.INSTANCE) ); Assert.assertEquals( @@ -262,11 +268,11 @@ public void testMapWithRestriction_onRestrictedDataSource_alwaysThrows(Policy.Ta { RestrictedDataSource restrictedDataSource = RestrictedDataSource.create( TableDataSource.create("table1"), - Policy.fromRowFilter(new NullFilter("random-column", null)) + RowFilterPolicy.from(new NullFilter("random-column", null)) ); ImmutableMap> anotherRestrictions = ImmutableMap.of( "table1", - Optional.of(Policy.fromRowFilter(new NullFilter("some-column", null))) + Optional.of(RowFilterPolicy.from(new NullFilter("some-column", null))) ); ImmutableMap> noPolicyFound = ImmutableMap.of("table1", Optional.empty()); ImmutableMap> policyWasNotChecked = ImmutableMap.of(); @@ -276,7 +282,7 @@ public void testMapWithRestriction_onRestrictedDataSource_alwaysThrows(Policy.Ta () -> restrictedDataSource.mapWithRestriction(anotherRestrictions, securityLevel) ); Assert.assertEquals( - "Multiple restrictions on [table1]: Policy{rowFilter=random-column IS NULL} and Policy{rowFilter=some-column IS NULL}", + "Multiple restrictions on table [table1]: policy [RowFilterPolicy{rowFilter=random-column IS NULL}] and policy [RowFilterPolicy{rowFilter=some-column IS NULL}]", e.getMessage() ); @@ -285,7 +291,7 @@ public void testMapWithRestriction_onRestrictedDataSource_alwaysThrows(Policy.Ta () -> restrictedDataSource.mapWithRestriction(noPolicyFound, securityLevel) ); Assert.assertEquals( - "No restriction found on table [table1], but had Policy{rowFilter=random-column IS NULL} before.", + "No restriction found on table [table1], but had policy [RowFilterPolicy{rowFilter=random-column IS NULL}] before.", e2.getMessage() ); diff --git a/processing/src/test/java/org/apache/druid/query/JoinDataSourceTest.java b/processing/src/test/java/org/apache/druid/query/JoinDataSourceTest.java index b7def71060ca..c4fe921a5d0c 100644 --- a/processing/src/test/java/org/apache/druid/query/JoinDataSourceTest.java +++ b/processing/src/test/java/org/apache/druid/query/JoinDataSourceTest.java @@ -30,7 +30,7 @@ import org.apache.druid.query.filter.InDimFilter; import org.apache.druid.query.filter.TrueDimFilter; import org.apache.druid.query.planning.DataSourceAnalysis; -import org.apache.druid.query.policy.Policy; +import org.apache.druid.query.policy.NoRestrictionPolicy; import org.apache.druid.segment.TestHelper; import org.apache.druid.segment.column.ColumnType; import org.apache.druid.segment.join.JoinConditionAnalysis; @@ -509,7 +509,7 @@ public void testGetAnalysisWithRestrictedDS() JoinDataSource dataSource = JoinDataSource.create( RestrictedDataSource.create( new TableDataSource("table1"), - Policy.NO_RESTRICTION + NoRestrictionPolicy.INSTANCE ), new TableDataSource("table2"), "j.", diff --git a/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java b/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java index 559f7cd717d8..676b0ce77524 100644 --- a/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java +++ b/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java @@ -24,7 +24,8 @@ import nl.jqno.equalsverifier.EqualsVerifier; import org.apache.druid.java.util.common.IAE; import org.apache.druid.query.filter.TrueDimFilter; -import org.apache.druid.query.policy.Policy; +import org.apache.druid.query.policy.NoRestrictionPolicy; +import org.apache.druid.query.policy.RowFilterPolicy; import org.apache.druid.segment.TestHelper; import org.junit.Assert; import org.junit.Rule; @@ -42,11 +43,11 @@ public class RestrictedDataSourceTest private final TableDataSource barDataSource = new TableDataSource("bar"); private final RestrictedDataSource restrictedFooDataSource = RestrictedDataSource.create( fooDataSource, - Policy.NO_RESTRICTION + RowFilterPolicy.from(TrueDimFilter.instance()) ); private final RestrictedDataSource restrictedBarDataSource = RestrictedDataSource.create( barDataSource, - Policy.NO_RESTRICTION + NoRestrictionPolicy.INSTANCE ); @Test @@ -146,13 +147,13 @@ public void test_deserialize_fromObject() throws Exception { final ObjectMapper jsonMapper = TestHelper.makeJsonMapper(); final RestrictedDataSource deserializedRestrictedDataSource = jsonMapper.readValue( - "{\"type\":\"restrict\",\"base\":{\"type\":\"table\",\"name\":\"foo\"},\"policy\":{\"rowFilter\":{\"type\":\"true\"}}}", + "{\"type\":\"restrict\",\"base\":{\"type\":\"table\",\"name\":\"foo\"},\"policy\":{\"type\":\"noRestriction\"}}", RestrictedDataSource.class ); Assert.assertEquals( deserializedRestrictedDataSource, - RestrictedDataSource.create(fooDataSource, Policy.fromRowFilter(TrueDimFilter.instance())) + RestrictedDataSource.create(fooDataSource, NoRestrictionPolicy.INSTANCE) ); } @@ -164,7 +165,7 @@ public void test_serialize() throws Exception final String s = jsonMapper.writeValueAsString(restrictedFooDataSource); Assert.assertEquals( - "{\"type\":\"restrict\",\"base\":{\"type\":\"table\",\"name\":\"foo\"},\"policy\":{\"rowFilter\":null}}", + "{\"type\":\"restrict\",\"base\":{\"type\":\"table\",\"name\":\"foo\"},\"policy\":{\"type\":\"row\",\"rowFilter\":{\"type\":\"true\"}}}", s ); } diff --git a/server/src/main/java/org/apache/druid/segment/realtime/ChatHandlers.java b/server/src/main/java/org/apache/druid/segment/realtime/ChatHandlers.java index 28074d037b4b..c5ef70f17cce 100644 --- a/server/src/main/java/org/apache/druid/segment/realtime/ChatHandlers.java +++ b/server/src/main/java/org/apache/druid/segment/realtime/ChatHandlers.java @@ -50,9 +50,9 @@ public static AuthorizationResult authorizationCheck( ); AuthorizationResult authResult = AuthorizationUtils.authorizeResourceAction(req, resourceAction, authorizerMapper); - authResult.getPermissionErrorMessage(true).ifPresent(error -> { - throw new ForbiddenException(error); - }); + if (!authResult.isUserWithNoRestriction()) { + throw new ForbiddenException(authResult.getErrorMessage()); + } return authResult; } 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 8068079e903c..e2ead4bf696e 100644 --- a/server/src/main/java/org/apache/druid/server/QueryLifecycle.java +++ b/server/src/main/java/org/apache/druid/server/QueryLifecycle.java @@ -316,7 +316,7 @@ private AuthorizationResult doAuthorize( Preconditions.checkNotNull(authenticationResult, "authenticationResult"); Preconditions.checkNotNull(authorizationResult, "authorizationResult"); - if (authorizationResult.getPermissionErrorMessage(false).isPresent()) { + if (!authorizationResult.allowBasicAccess()) { // Not authorized; go straight to Jail, do not pass Go. transition(State.AUTHORIZING, State.UNAUTHORIZED); } else { diff --git a/server/src/main/java/org/apache/druid/server/QueryResource.java b/server/src/main/java/org/apache/druid/server/QueryResource.java index c7c0a6e1ae0b..388253a9b2de 100644 --- a/server/src/main/java/org/apache/druid/server/QueryResource.java +++ b/server/src/main/java/org/apache/druid/server/QueryResource.java @@ -158,9 +158,9 @@ public Response cancelQuery(@PathParam("id") String queryId, @Context final Http authorizerMapper ); - authResult.getPermissionErrorMessage(true).ifPresent(error -> { - throw new ForbiddenException(error); - }); + if (!authResult.isUserWithNoRestriction()) { + throw new ForbiddenException(authResult.getErrorMessage()); + } queryScheduler.cancelQuery(queryId); return Response.status(Response.Status.ACCEPTED).build(); @@ -214,9 +214,9 @@ public Response doPost( return io.getResponseWriter().buildNonOkResponse(qe.getFailType().getExpectedStatus(), qe); } - authResult.getPermissionErrorMessage(true).ifPresent(error -> { - throw new ForbiddenException(error); - }); + if (!authResult.isUserWithNoRestriction()) { + throw new ForbiddenException(authResult.getErrorMessage()); + } final QueryResourceQueryResultPusher pusher = new QueryResourceQueryResultPusher(req, queryLifecycle, io); return pusher.push(); 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 6c357900da2a..dc4aee86b94b 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 @@ -62,9 +62,9 @@ public ContainerRequest filter(ContainerRequest request) getAuthorizerMapper() ); - authResult.getPermissionErrorMessage(true).ifPresent(error -> { - throw new ForbiddenException(error); - }); + if (!authResult.isUserWithNoRestriction()) { + throw new ForbiddenException(authResult.getErrorMessage()); + } 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 4c239a902486..08e9af1cc259 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 @@ -63,9 +63,9 @@ public ContainerRequest filter(ContainerRequest request) getAuthorizerMapper() ); - authResult.getPermissionErrorMessage(true).ifPresent(error -> { - throw new ForbiddenException(error); - }); + if (!authResult.isUserWithNoRestriction()) { + throw new ForbiddenException(authResult.getErrorMessage()); + } 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 db322a83f0e6..937db5a2daf8 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 @@ -81,9 +81,9 @@ public boolean apply(PathSegment input) getAuthorizerMapper() ); - authResult.getPermissionErrorMessage(true).ifPresent(error -> { - throw new ForbiddenException(error); - }); + if (!authResult.isUserWithNoRestriction()) { + throw new ForbiddenException(authResult.getErrorMessage()); + } 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 600fd4f4ebe9..f5e812d968d3 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 @@ -66,9 +66,9 @@ public ContainerRequest filter(ContainerRequest request) getAuthorizerMapper() ); - authResult.getPermissionErrorMessage(true).ifPresent(error -> { - throw new ForbiddenException(error); - }); + if (!authResult.isUserWithNoRestriction()) { + throw new ForbiddenException(authResult.getErrorMessage()); + } return request; } diff --git a/server/src/main/java/org/apache/druid/server/security/Access.java b/server/src/main/java/org/apache/druid/server/security/Access.java index 01dfd3a3a538..819b0319410b 100644 --- a/server/src/main/java/org/apache/druid/server/security/Access.java +++ b/server/src/main/java/org/apache/druid/server/security/Access.java @@ -103,8 +103,9 @@ public String getMessage() stringBuilder.append(message); } if (allowed && policy.isPresent()) { - stringBuilder.append(", with restriction "); + stringBuilder.append(", with restriction ["); stringBuilder.append(policy.get()); + stringBuilder.append("]"); } return stringBuilder.toString(); } diff --git a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java index e8ab7d409aa1..8c7090078eb3 100644 --- a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java +++ b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java @@ -23,6 +23,7 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableMap; import org.apache.druid.error.DruidException; +import org.apache.druid.query.policy.NoRestrictionPolicy; import org.apache.druid.query.policy.Policy; import javax.annotation.Nonnull; @@ -149,7 +150,14 @@ public AuthorizationResult withResourceActions( } /** - * Returns true if user has the correct permission, and the policy restrictions indicates one of the following: + * Returns true if user has basic access. + */ + public boolean allowBasicAccess() { + return PERMISSION.ALLOW_NO_RESTRICTION.equals(permission) || PERMISSION.ALLOW_WITH_RESTRICTION.equals(permission); + } + + /** + * Returns true if user has all required permission, and the policy restrictions indicates one of the following: *

  • no policy found *
  • the user has a no-restriction policy */ @@ -160,34 +168,21 @@ public boolean isUserWithNoRestriction() .stream() .map(p -> p.orElse(null)) .filter(Objects::nonNull) // Can be replaced by Optional::stream after java 11 - .allMatch(Policy::hasNoRestriction)); + .allMatch(p -> (p instanceof NoRestrictionPolicy))); } /** - * Returns a permission error string if the AuthorizationResult doesn't permit all requried access. Otherwise, returns - * empty. When {@code policyRestrictionsNotPermitted} set to true, it requests unrestricted full access. The caller - * can use this method to retrieve the error string, and throw a {@link ForbiddenException} with the error message. - *

    - * It first checks if all permissions (e.x. {@link org.apache.druid.security.basic.authorization.entity.BasicAuthorizerPermission}) - * have been granted access. If not, returns the {@code failureMessage}. Then if {@code policyRestrictionsNotPermitted}, - * it checks for 'actual' policy restrictions (i.e. {@link Policy#hasNoRestriction} returns false). If 'actual' policy - * restrictions exist, returns {@link Access#DEFAULT_ERROR_MESSAGE}. - * - * @param policyRestrictionsNotPermitted true if policy restrictions are considered as not permitted - * @return optional permission error message + * Returns an error string if the AuthorizationResult doesn't permit all requried access. */ - public Optional getPermissionErrorMessage(boolean policyRestrictionsNotPermitted) + public String getErrorMessage() { switch (permission) { - case ALLOW_NO_RESTRICTION: - return Optional.empty(); case DENY: - return Optional.of(Objects.requireNonNull(failureMessage)); + return Objects.requireNonNull(failureMessage); case ALLOW_WITH_RESTRICTION: - if (policyRestrictionsNotPermitted && !isUserWithNoRestriction()) { - return Optional.of(Access.DEFAULT_ERROR_MESSAGE); + if (!isUserWithNoRestriction()) { + return Access.DEFAULT_ERROR_MESSAGE; } - return Optional.empty(); default: throw DruidException.defensive("unreachable"); } 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 b2fefda7f51d..28543b1a5c86 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 @@ -184,10 +184,11 @@ public static AuthorizationResult authorizeAllResourceActions( resultCache.add(resourceAction); if (resourceAction.getAction().equals(Action.READ) && RESTRICTION_APPLICABLE_RESOURCE_TYPES.contains(resourceAction.getResource().getType())) { + // For every table read, we check on the policy returned from authorizer and add it to the map. policyFilters.put(resourceAction.getResource().getName(), access.getPolicy()); } else if (access.getPolicy().isPresent()) { throw DruidException.defensive( - "Policy should only present when reading a table, but was present for %s", + "Policy should only present when reading a table, but was present for a different kind of resource action [%s]", resourceAction ); } else { @@ -232,10 +233,7 @@ public static AuthorizationResult authorizeAllResourceActions( authorizerMapper ); - request.setAttribute( - AuthConfig.DRUID_AUTHORIZATION_CHECKED, - !authResult.getPermissionErrorMessage(false).isPresent() - ); + request.setAttribute(AuthConfig.DRUID_AUTHORIZATION_CHECKED, authResult.allowBasicAccess()); return authResult; } diff --git a/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java b/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java index 0d988180672d..c3cb9817bd8c 100644 --- a/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java +++ b/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java @@ -24,8 +24,10 @@ import junitparams.Parameters; import nl.jqno.equalsverifier.EqualsVerifier; import org.apache.druid.common.config.NullHandling; +import org.apache.druid.error.DruidException; import org.apache.druid.query.filter.EqualityFilter; -import org.apache.druid.query.policy.Policy; +import org.apache.druid.query.policy.NoRestrictionPolicy; +import org.apache.druid.query.policy.RowFilterPolicy; import org.apache.druid.segment.column.ColumnType; import org.apache.druid.server.security.AuthorizationResult; import org.junit.Before; @@ -36,9 +38,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; - @RunWith(JUnitParamsRunner.class) public class AuthorizationResultTest { @@ -62,55 +64,51 @@ public void testToString() AuthorizationResult result = AuthorizationResult.allowWithRestriction( ImmutableMap.of( "table1", - Optional.of(Policy.NO_RESTRICTION), + Optional.of(NoRestrictionPolicy.INSTANCE), "table2", Optional.of( - Policy.fromRowFilter(new EqualityFilter("column1", ColumnType.STRING, "val1", null))) + RowFilterPolicy.from(new EqualityFilter("column1", ColumnType.STRING, "val1", null))) ) ); assertEquals( - "AuthorizationResult [permission=ALLOW_WITH_RESTRICTION, failureMessage=null, policyRestrictions={table1=Optional[Policy{rowFilter=null}], table2=Optional[Policy{rowFilter=column1 = val1}]}, sqlResourceActions=null, allResourceActions=null]", + "AuthorizationResult [permission=ALLOW_WITH_RESTRICTION, failureMessage=null, policyRestrictions={table1=Optional[NO_RESTRICTION], table2=Optional[RowFilterPolicy{rowFilter=column1 = val1}]}, sqlResourceActions=null, allResourceActions=null]", result.toString() ); } @Test - @Parameters({"true", "false"}) - public void testFailedAccess_withPermissionError(boolean policyRestrictionsNotPermitted) + public void testNoAccess() { AuthorizationResult result = AuthorizationResult.deny("this data source is not permitted"); - assertEquals( - Optional.of("this data source is not permitted"), - result.getPermissionErrorMessage(policyRestrictionsNotPermitted) - ); + assertFalse(result.allowBasicAccess()); + assertFalse(result.isUserWithNoRestriction()); + assertEquals("this data source is not permitted", result.getErrorMessage()); assertFalse(result.isUserWithNoRestriction()); } @Test - @Parameters({"true", "false"}) - public void testFullAccess_noPermissionError(boolean policyRestrictionsNotPermitted) + public void testFullAccess() { AuthorizationResult result = AuthorizationResult.allowWithRestriction(ImmutableMap.of()); + assertTrue(result.allowBasicAccess()); + assertTrue(result.isUserWithNoRestriction()); + assertThrows(DruidException.class, result::getErrorMessage); + AuthorizationResult resultWithEmptyPolicy = AuthorizationResult.allowWithRestriction(ImmutableMap.of( "table1", Optional.empty() )); + assertTrue(resultWithEmptyPolicy.allowBasicAccess()); + assertTrue(resultWithEmptyPolicy.isUserWithNoRestriction()); + assertThrows(DruidException.class, resultWithEmptyPolicy::getErrorMessage); + AuthorizationResult resultWithNoRestrictionPolicy = AuthorizationResult.allowWithRestriction(ImmutableMap.of( "table1", - Optional.of(Policy.NO_RESTRICTION) + Optional.of(NoRestrictionPolicy.INSTANCE) )); - - assertEquals(Optional.empty(), result.getPermissionErrorMessage(policyRestrictionsNotPermitted)); - assertTrue(result.isUserWithNoRestriction()); - - assertEquals(Optional.empty(), resultWithEmptyPolicy.getPermissionErrorMessage(policyRestrictionsNotPermitted)); - assertTrue(resultWithEmptyPolicy.isUserWithNoRestriction()); - - assertEquals( - Optional.empty(), - resultWithNoRestrictionPolicy.getPermissionErrorMessage(policyRestrictionsNotPermitted) - ); + assertTrue(resultWithNoRestrictionPolicy.allowBasicAccess()); assertTrue(resultWithNoRestrictionPolicy.isUserWithNoRestriction()); + assertThrows(DruidException.class, resultWithNoRestrictionPolicy::getErrorMessage); } @Test @@ -118,18 +116,19 @@ public void testFullAccess_noPermissionError(boolean policyRestrictionsNotPermit "true, Optional[Unauthorized]", "false, Optional.empty" }) - public void testRestrictedAccess_noPermissionError(boolean policyRestrictionsNotPermitted, String error) + public void testRestrictedAccess(boolean policyRestrictionsNotPermitted, String error) { AuthorizationResult result = AuthorizationResult.allowWithRestriction(ImmutableMap.of( "table1", - Optional.of(Policy.fromRowFilter(new EqualityFilter( + Optional.of(RowFilterPolicy.from(new EqualityFilter( "col", ColumnType.STRING, "val1", null ))) )); - assertEquals(error, result.getPermissionErrorMessage(policyRestrictionsNotPermitted).toString()); + assertTrue(result.allowBasicAccess()); assertFalse(result.isUserWithNoRestriction()); + assertEquals("Unauthorized", result.getErrorMessage()); } } diff --git a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java index a23fa34af0fc..d8cdd1186ae3 100644 --- a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java +++ b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java @@ -45,7 +45,9 @@ import org.apache.druid.query.aggregation.CountAggregatorFactory; import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.NullFilter; +import org.apache.druid.query.policy.NoRestrictionPolicy; import org.apache.druid.query.policy.Policy; +import org.apache.druid.query.policy.RowFilterPolicy; import org.apache.druid.query.timeseries.TimeseriesQuery; import org.apache.druid.server.log.RequestLogger; import org.apache.druid.server.security.Access; @@ -158,10 +160,8 @@ public void teardown() } @Test - @Parameters({"APPLY_WHEN_APPLICABLE"}) - public void testRunSimple_preauthorizedAsSuperuser(Policy.TablePolicySecurityLevel securityLevel) + public void testRunSimple_preauthorizedAsSuperuser() { - // A simple path with the lowest security level configed. EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) @@ -171,11 +171,10 @@ public void testRunSimple_preauthorizedAsSuperuser(Policy.TablePolicySecurityLev .andReturn(runner) .once(); EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).once(); + replayAll(); - QueryLifecycle lifecycle = createLifecycle(AuthConfig.newBuilder() - .setTablePolicySecurityLevel(securityLevel) - .build()); + QueryLifecycle lifecycle = createLifecycle(new AuthConfig()); lifecycle.runSimple(query, authenticationResult, AuthorizationResult.ALLOW_NO_RESTRICTION); } @@ -206,7 +205,7 @@ public void testRunSimpleUnauthorized() public void testRunSimple_withPolicyRestriction(Policy.TablePolicySecurityLevel securityLevel) { // Test the path when an external client send a sql query to broker, through runSimple. - Policy rowFilterPolicy = Policy.fromRowFilter(new NullFilter("some-column", null)); + Policy rowFilterPolicy = RowFilterPolicy.from(new NullFilter("some-column", null)); Access access = Access.allowWithRestriction(rowFilterPolicy); AuthorizationResult authorizationResult = AuthorizationResult.allowWithRestriction(ImmutableMap.of( DATASOURCE, @@ -248,7 +247,7 @@ public void testRunSimple_withPolicyRestriction(Policy.TablePolicySecurityLevel "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY, Need to check row-level policy for all tables missing [some_datasource]", "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST, Need to check row-level policy for all tables missing [some_datasource]" }) - public void testRunSimple_withNoRestriction( + public void testRunSimple_withNoRestrictionPolicy( Policy.TablePolicySecurityLevel securityLevel, String error ) @@ -329,12 +328,12 @@ public void testRunSimple_foundMultiplePolicyRestrictions(Policy.TablePolicySecu // This is not allowed in any case. expectedException.expect(ISE.class); expectedException.expectMessage( - "Multiple restrictions on [some_datasource]: Policy{rowFilter=some-column IS NULL} and Policy{rowFilter=some-column2 IS NULL}"); + "Multiple restrictions on table [some_datasource]: policy [RowFilterPolicy{rowFilter=some-column IS NULL}] and policy [RowFilterPolicy{rowFilter=some-column2 IS NULL}]"); DimFilter originalFilterOnRDS = new NullFilter("some-column", null); - Policy originalFilterPolicy = Policy.fromRowFilter(originalFilterOnRDS); + Policy originalFilterPolicy = RowFilterPolicy.from(originalFilterOnRDS); - Policy newFilterPolicy = Policy.fromRowFilter(new NullFilter("some-column2", null)); + Policy newFilterPolicy = RowFilterPolicy.from(new NullFilter("some-column2", null)); AuthorizationResult authorizationResult = AuthorizationResult.allowWithRestriction(ImmutableMap.of( DATASOURCE, Optional.of(newFilterPolicy) @@ -359,7 +358,6 @@ public void testRunSimple_foundMultiplePolicyRestrictions(Policy.TablePolicySecu replayAll(); AuthConfig authConfig = AuthConfig.newBuilder() - .setAuthorizeQueryContextParams(true) .setTablePolicySecurityLevel(securityLevel) .build(); @@ -377,10 +375,10 @@ public void testRunSimple_queryWithRestrictedDataSource_policyRestrictionMightHa { expectedException.expect(ISE.class); expectedException.expectMessage( - "No restriction found on table [some_datasource], but had Policy{rowFilter=some-column IS NULL} before."); + "No restriction found on table [some_datasource], but had policy [RowFilterPolicy{rowFilter=some-column IS NULL}] before."); DimFilter originalFilterOnRDS = new NullFilter("some-column", null); - Policy originalFilterPolicy = Policy.fromRowFilter(originalFilterOnRDS); + Policy originalFilterPolicy = RowFilterPolicy.from(originalFilterOnRDS); DataSource restrictedDataSource = RestrictedDataSource.create( TableDataSource.create(DATASOURCE), originalFilterPolicy @@ -411,7 +409,6 @@ public void testRunSimple_queryWithRestrictedDataSource_policyRestrictionMightHa replayAll(); AuthConfig authConfig = AuthConfig.newBuilder() - .setAuthorizeQueryContextParams(true) .setTablePolicySecurityLevel(securityLevel) .build(); @@ -429,7 +426,7 @@ public void testRunSimple_queryWithRestrictedDataSource_policyRestrictionMightHa public void testAuthorized_withPolicyRestriction(Policy.TablePolicySecurityLevel securityLevel) { // Test the path broker receives a native json query from external client, should add restriction on data source - Policy rowFilterPolicy = Policy.fromRowFilter(new NullFilter("some-column", null)); + Policy rowFilterPolicy = RowFilterPolicy.from(new NullFilter("some-column", null)); Access access = Access.allowWithRestriction(rowFilterPolicy); DataSource expectedDataSource = RestrictedDataSource.create(TableDataSource.create(DATASOURCE), rowFilterPolicy); @@ -457,12 +454,11 @@ public void testAuthorized_withPolicyRestriction(Policy.TablePolicySecurityLevel replayAll(); AuthConfig authConfig = AuthConfig.newBuilder() - .setAuthorizeQueryContextParams(true) .setTablePolicySecurityLevel(securityLevel) .build(); QueryLifecycle lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertTrue(lifecycle.authorize(authenticationResult).getPermissionErrorMessage(false).isEmpty()); + Assert.assertTrue(lifecycle.authorize(authenticationResult).allowBasicAccess()); lifecycle.execute(); } @@ -475,9 +471,9 @@ public void testAuthorized_withPolicyRestriction(Policy.TablePolicySecurityLevel public void testAuthorized_queryWithRestrictedDataSource_runWithSuperUserPermission(Policy.TablePolicySecurityLevel securityLevel) { // Test the path historical receives a native json query from broker, query already has restriction on data source - Policy rowFilterPolicy = Policy.fromRowFilter(new NullFilter("some-column", null)); + Policy rowFilterPolicy = RowFilterPolicy.from(new NullFilter("some-column", null)); // Internal druid system would get a NO_RESTRICTION on a restricted data source. - Access access = Access.allowWithRestriction(Policy.NO_RESTRICTION); + Access access = Access.allowWithRestriction(NoRestrictionPolicy.INSTANCE); DataSource restrictedDataSource = RestrictedDataSource.create(TableDataSource.create(DATASOURCE), rowFilterPolicy); @@ -508,7 +504,7 @@ public void testAuthorized_queryWithRestrictedDataSource_runWithSuperUserPermiss .build(); QueryLifecycle lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertTrue(lifecycle.authorize(authenticationResult).getPermissionErrorMessage(false).isEmpty()); + Assert.assertTrue(lifecycle.authorize(authenticationResult).allowBasicAccess()); lifecycle.execute(); } @@ -565,11 +561,11 @@ public void testAuthorizeQueryContext_authorized() revisedContext ); - Assert.assertTrue(lifecycle.authorize(mockRequest()).getPermissionErrorMessage(false).isEmpty()); + Assert.assertTrue(lifecycle.authorize(mockRequest()).isUserWithNoRestriction()); lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertTrue(lifecycle.authorize(authenticationResult).getPermissionErrorMessage(false).isEmpty()); + Assert.assertTrue(lifecycle.authorize(authenticationResult).isUserWithNoRestriction()); } @Test @@ -611,11 +607,11 @@ public void testAuthorizeQueryContext_notAuthorized() .build(); QueryLifecycle lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertFalse(lifecycle.authorize(mockRequest()).getPermissionErrorMessage(false).isEmpty()); + Assert.assertFalse(lifecycle.authorize(mockRequest()).allowBasicAccess()); lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertFalse(lifecycle.authorize(authenticationResult).getPermissionErrorMessage(false).isEmpty()); + Assert.assertFalse(lifecycle.authorize(authenticationResult).allowBasicAccess()); } @Test @@ -657,11 +653,11 @@ public void testAuthorizeQueryContext_unsecuredKeys() revisedContext ); - Assert.assertTrue(lifecycle.authorize(mockRequest()).getPermissionErrorMessage(false).isEmpty()); + Assert.assertTrue(lifecycle.authorize(mockRequest()).isUserWithNoRestriction()); lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertTrue(lifecycle.authorize(authenticationResult).getPermissionErrorMessage(false).isEmpty()); + Assert.assertTrue(lifecycle.authorize(authenticationResult).isUserWithNoRestriction()); } @Test @@ -708,11 +704,11 @@ public void testAuthorizeQueryContext_securedKeys() revisedContext ); - Assert.assertTrue(lifecycle.authorize(mockRequest()).getPermissionErrorMessage(false).isEmpty()); + Assert.assertTrue(lifecycle.authorize(mockRequest()).allowBasicAccess()); lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertTrue(lifecycle.authorize(authenticationResult).getPermissionErrorMessage(false).isEmpty()); + Assert.assertTrue(lifecycle.authorize(authenticationResult).allowBasicAccess()); } @Test @@ -757,11 +753,11 @@ public void testAuthorizeQueryContext_securedKeysNotAuthorized() .build(); QueryLifecycle lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertFalse(lifecycle.authorize(mockRequest()).getPermissionErrorMessage(false).isEmpty()); + Assert.assertFalse(lifecycle.authorize(mockRequest()).allowBasicAccess()); lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertFalse(lifecycle.authorize(authenticationResult).getPermissionErrorMessage(false).isEmpty()); + Assert.assertFalse(lifecycle.authorize(authenticationResult).allowBasicAccess()); } @Test @@ -817,11 +813,11 @@ public void testAuthorizeLegacyQueryContext_authorized() Assert.assertTrue(revisedContext.containsKey("baz")); Assert.assertTrue(revisedContext.containsKey("queryId")); - Assert.assertTrue(lifecycle.authorize(mockRequest()).getPermissionErrorMessage(false).isEmpty()); + Assert.assertTrue(lifecycle.authorize(mockRequest()).allowBasicAccess()); lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertTrue(lifecycle.authorize(mockRequest()).getPermissionErrorMessage(false).isEmpty()); + Assert.assertTrue(lifecycle.authorize(mockRequest()).allowBasicAccess()); } public static Query queryMatchDataSource(DataSource dataSource) diff --git a/server/src/test/java/org/apache/druid/server/security/ForbiddenExceptionTest.java b/server/src/test/java/org/apache/druid/server/security/ForbiddenExceptionTest.java index c7586a3616f0..76b24e8a5243 100644 --- a/server/src/test/java/org/apache/druid/server/security/ForbiddenExceptionTest.java +++ b/server/src/test/java/org/apache/druid/server/security/ForbiddenExceptionTest.java @@ -19,7 +19,7 @@ package org.apache.druid.server.security; -import org.apache.druid.query.policy.Policy; +import org.apache.druid.query.policy.NoRestrictionPolicy; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -83,9 +83,9 @@ public void testAccess() Assert.assertEquals("Allowed:true, Message:, Policy: Optional.empty", access.toString()); Assert.assertEquals("Authorized", access.getMessage()); - access = Access.allowWithRestriction(Policy.NO_RESTRICTION); + access = Access.allowWithRestriction(NoRestrictionPolicy.INSTANCE); Assert.assertTrue(access.isAllowed()); - Assert.assertEquals("Allowed:true, Message:, Policy: Optional[Policy{rowFilter=null}]", access.toString()); - Assert.assertEquals("Authorized, with restriction Policy{rowFilter=null}", access.getMessage()); + Assert.assertEquals("Allowed:true, Message:, Policy: Optional[NO_RESTRICTION]", access.toString()); + Assert.assertEquals("Authorized, with restriction [NO_RESTRICTION]", access.getMessage()); } } diff --git a/sql/src/main/java/org/apache/druid/sql/AbstractStatement.java b/sql/src/main/java/org/apache/druid/sql/AbstractStatement.java index 90962fa05716..a9cef6430557 100644 --- a/sql/src/main/java/org/apache/druid/sql/AbstractStatement.java +++ b/sql/src/main/java/org/apache/druid/sql/AbstractStatement.java @@ -151,9 +151,9 @@ protected void authorize( // Authentication is done by the planner using the function provided // here. The planner ensures that this step is done before planning. authResult = planner.authorize(authorizer, contextResources); - authResult.getPermissionErrorMessage(false).ifPresent(error -> { - throw new ForbiddenException(error); - }); + if (!authResult.allowBasicAccess()) { + throw new ForbiddenException(authResult.getErrorMessage()); + } } /** 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 857219b31e2d..37d43073d6e4 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 @@ -1153,9 +1153,9 @@ private static void checkStateReadAccessForServers( authorizerMapper ); - authResult.getPermissionErrorMessage(true).ifPresent(error -> { - throw new ForbiddenException("Insufficient permission to view servers: " + error); - }); + if (!authResult.isUserWithNoRestriction()) { + throw new ForbiddenException("Insufficient permission to view servers: " + authResult.getErrorMessage()); + } } /** diff --git a/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java b/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java index b93de5ce9653..a5436ffc7b57 100644 --- a/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java +++ b/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java @@ -142,7 +142,7 @@ public Response cancelQuery( final AuthorizationResult authResult = authorizeCancellation(req, lifecycles); - if (!authResult.getPermissionErrorMessage(true).isPresent()) { + if (authResult.isUserWithNoRestriction()) { // should remove only the lifecycles in the snapshot. sqlLifecycleManager.removeAll(sqlQueryId, lifecycles); lifecycles.forEach(Cancelable::cancel); 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 6bdad517e0b7..ab40dc0abb4e 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 @@ -52,7 +52,9 @@ import org.apache.druid.math.expr.ExprMacroTable; import org.apache.druid.query.QueryRunnerFactoryConglomerate; import org.apache.druid.query.QuerySegmentWalker; +import org.apache.druid.query.policy.NoRestrictionPolicy; import org.apache.druid.query.policy.Policy; +import org.apache.druid.query.policy.RowFilterPolicy; import org.apache.druid.rpc.indexing.NoopOverlordClient; import org.apache.druid.rpc.indexing.OverlordClient; import org.apache.druid.segment.join.JoinableFactory; @@ -136,8 +138,8 @@ public class CalciteTests public static final String BENCHMARK_DATASOURCE = "benchmark_ds"; public static final String TEST_SUPERUSER_NAME = "testSuperuser"; - public static final Policy POLICY_NO_RESTRICTION_SUPERUSER = Policy.NO_RESTRICTION; - public static final Policy POLICY_RESTRICTION = Policy.fromRowFilter(BaseCalciteQueryTest.numericSelector("m1", "6")); + public static final Policy POLICY_NO_RESTRICTION_SUPERUSER = NoRestrictionPolicy.INSTANCE; + public static final Policy POLICY_RESTRICTION = RowFilterPolicy.from(BaseCalciteQueryTest.numericSelector("m1", "6")); public static final AuthorizerMapper TEST_AUTHORIZER_MAPPER = new AuthorizerMapper(null) { @Override From 009e8db936f9e4daef923f2101e4f21aa0815d89 Mon Sep 17 00:00:00 2001 From: cecemei Date: Tue, 7 Jan 2025 17:00:00 -0800 Subject: [PATCH 18/32] added back AuthResult class, and some javadoc --- .../druid/query/RestrictedDataSource.java | 6 +- .../segment/BypassRestrictedSegment.java | 19 ++++ .../segment/RestrictedCursorFactory.java | 1 - .../druid/segment/RestrictedSegment.java | 9 +- .../apache/druid/server/security/Access.java | 12 ++ .../server/security/AuthorizationResult.java | 65 ++--------- .../apache/druid/server/security/Policy.java | 106 ------------------ .../AuthorizationResultTest.java | 12 +- .../apache/druid/sql/AbstractStatement.java | 10 +- .../druid/sql/SqlExecutionReporter.java | 2 +- .../sql/calcite/planner/DruidPlanner.java | 37 +++++- 11 files changed, 88 insertions(+), 191 deletions(-) delete mode 100644 server/src/main/java/org/apache/druid/server/security/Policy.java diff --git a/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java b/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java index 9c6a2bebba84..a6eabd6476b2 100644 --- a/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java +++ b/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java @@ -22,13 +22,11 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.collect.ImmutableList; -import org.apache.druid.error.DruidException; import org.apache.druid.java.util.common.IAE; import org.apache.druid.java.util.common.ISE; import org.apache.druid.query.planning.DataSourceAnalysis; import org.apache.druid.query.policy.NoRestrictionPolicy; import org.apache.druid.query.policy.Policy; -import org.apache.druid.query.policy.RowFilterPolicy; import org.apache.druid.segment.RestrictedSegment; import org.apache.druid.segment.SegmentReference; import org.apache.druid.utils.JvmUtils; @@ -173,7 +171,7 @@ public DataSource mapWithRestriction( newPolicy.get() ); } - // The only happy path is, newPolicy is Policy.NO_RESTRICTION, which means this comes from an anthenticated and + // The only happy path is, newPolicy is NoRestrictionPolicy, which means this comes from an anthenticated and // authorized druid-internal request. return this; } @@ -183,7 +181,7 @@ public String toString() { return "RestrictedDataSource{" + "base=" + base + - ", policy='" + policy + "'}"; + ", policy=" + policy + "}"; } @Override diff --git a/processing/src/main/java/org/apache/druid/segment/BypassRestrictedSegment.java b/processing/src/main/java/org/apache/druid/segment/BypassRestrictedSegment.java index ecd1d2614f3f..6b74179b0539 100644 --- a/processing/src/main/java/org/apache/druid/segment/BypassRestrictedSegment.java +++ b/processing/src/main/java/org/apache/druid/segment/BypassRestrictedSegment.java @@ -1,3 +1,22 @@ +/* + * 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.segment; import org.apache.druid.query.filter.DimFilter; diff --git a/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java b/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java index 2ab5f41fd6ba..a91018f44aa7 100644 --- a/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java +++ b/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java @@ -21,7 +21,6 @@ import com.google.common.collect.ImmutableList; import org.apache.druid.error.DruidException; -import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.Filter; import org.apache.druid.query.policy.NoRestrictionPolicy; import org.apache.druid.query.policy.Policy; diff --git a/processing/src/main/java/org/apache/druid/segment/RestrictedSegment.java b/processing/src/main/java/org/apache/druid/segment/RestrictedSegment.java index 92e7b47bc6cd..e663d48c61e2 100644 --- a/processing/src/main/java/org/apache/druid/segment/RestrictedSegment.java +++ b/processing/src/main/java/org/apache/druid/segment/RestrictedSegment.java @@ -19,7 +19,6 @@ package org.apache.druid.segment; -import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.policy.NoRestrictionPolicy; import org.apache.druid.query.policy.Policy; import org.apache.druid.timeline.SegmentId; @@ -32,8 +31,12 @@ import java.util.Optional; /** - * A wrapped {@link SegmentReference} with a {@link DimFilter} restriction. The restriction must be applied on the - * segment. + * A wrapped {@link SegmentReference} with a {@link Policy} restriction. The policy must be applied when querying the + * wrapped segment, e.x {@link #asCursorFactory()} returns a policy-enforced {@link RestrictedCursorFactory}. The policy + * and wrapped SegmentReference (i.e. delegate) can't be accessed directly. + *

    + * There's a backdoor to get the wrapped SegmentReference through {@code as(BypassRestrictedSegment.class)}, returning + * a policy-aware (but not enforced) {@link BypassRestrictedSegment} instance. */ public class RestrictedSegment implements SegmentReference { diff --git a/server/src/main/java/org/apache/druid/server/security/Access.java b/server/src/main/java/org/apache/druid/server/security/Access.java index 819b0319410b..e7c582c91b77 100644 --- a/server/src/main/java/org/apache/druid/server/security/Access.java +++ b/server/src/main/java/org/apache/druid/server/security/Access.java @@ -69,21 +69,33 @@ public Access(boolean allowed, String message) this.policy = policy; } + /** + * Constructs {@link Access} instance with access allowed, with no policy restriction. + */ public static Access allow() { return new Access(true, "", Optional.empty()); } + /** + * Contructs {@link Access} instance with access denied. + */ public static Access deny(@Nullable String message) { return new Access(false, StringUtils.nullToEmptyNonDruidDataString(message), null); } + /** + * Contructs {@link Access} instance with access allowed, but with policy restriction. + */ public static Access allowWithRestriction(Policy policy) { return new Access(true, "", Optional.of(policy)); } + /** + * Returns true if access allowed, ignoring any policy restrictions. + */ public boolean isAllowed() { return allowed; diff --git a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java index 8c7090078eb3..7402e0fbc435 100644 --- a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java +++ b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java @@ -21,7 +21,6 @@ import com.google.common.base.Preconditions; import com.google.common.base.Strings; -import com.google.common.collect.ImmutableMap; import org.apache.druid.error.DruidException; import org.apache.druid.query.policy.NoRestrictionPolicy; import org.apache.druid.query.policy.Policy; @@ -32,7 +31,6 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.Set; /** * Represents the outcoming of performing authorization check on required resource accesses on a query or http requests. @@ -53,9 +51,7 @@ public class AuthorizationResult public static final AuthorizationResult ALLOW_NO_RESTRICTION = new AuthorizationResult( PERMISSION.ALLOW_NO_RESTRICTION, null, - Collections.emptyMap(), - null, - null + Collections.emptyMap() ); /** @@ -64,9 +60,7 @@ public class AuthorizationResult public static final AuthorizationResult DENY = new AuthorizationResult( PERMISSION.DENY, Access.DENIED.getMessage(), - Collections.emptyMap(), - null, - null + Collections.emptyMap() ); enum PERMISSION @@ -83,25 +77,15 @@ enum PERMISSION private final Map> policyRestrictions; - @Nullable - private final Set sqlResourceActions; - - @Nullable - private final Set allResourceActions; - AuthorizationResult( PERMISSION permission, @Nullable String failureMessage, - Map> policyRestrictions, - @Nullable Set sqlResourceActions, - @Nullable Set allResourceActions + Map> policyRestrictions ) { this.permission = permission; this.failureMessage = failureMessage; this.policyRestrictions = policyRestrictions; - this.sqlResourceActions = sqlResourceActions; - this.allResourceActions = allResourceActions; // sanity check switch (permission) { @@ -124,7 +108,7 @@ enum PERMISSION public static AuthorizationResult deny(@Nonnull String failureMessage) { - return new AuthorizationResult(PERMISSION.DENY, failureMessage, Collections.emptyMap(), null, null); + return new AuthorizationResult(PERMISSION.DENY, failureMessage, Collections.emptyMap()); } public static AuthorizationResult allowWithRestriction(Map> policyRestrictions) @@ -132,27 +116,14 @@ public static AuthorizationResult allowWithRestriction(Map sqlResourceActions, - Set allResourceActions - ) - { - return new AuthorizationResult( - permission, - failureMessage, - ImmutableMap.copyOf(getPolicy()), - sqlResourceActions, - allResourceActions - ); + return new AuthorizationResult(PERMISSION.ALLOW_WITH_RESTRICTION, null, policyRestrictions); } /** * Returns true if user has basic access. */ - public boolean allowBasicAccess() { + public boolean allowBasicAccess() + { return PERMISSION.ALLOW_NO_RESTRICTION.equals(permission) || PERMISSION.ALLOW_WITH_RESTRICTION.equals(permission); } @@ -193,18 +164,6 @@ public Map> getPolicy() return policyRestrictions; } - @Nullable - public Set getSqlResourceActions() - { - return sqlResourceActions; - } - - @Nullable - public Set getAllResourceActions() - { - return allResourceActions; - } - @Override public boolean equals(Object o) { @@ -217,15 +176,13 @@ public boolean equals(Object o) AuthorizationResult that = (AuthorizationResult) o; return Objects.equals(permission, that.permission) && Objects.equals(failureMessage, that.failureMessage) && - Objects.equals(policyRestrictions, that.policyRestrictions) && - Objects.equals(sqlResourceActions, that.sqlResourceActions) && - Objects.equals(allResourceActions, that.allResourceActions); + Objects.equals(policyRestrictions, that.policyRestrictions); } @Override public int hashCode() { - return Objects.hash(permission, failureMessage, policyRestrictions, sqlResourceActions, allResourceActions); + return Objects.hash(permission, failureMessage, policyRestrictions); } @Override @@ -237,10 +194,6 @@ public String toString() + failureMessage + ", policyRestrictions=" + policyRestrictions - + ", sqlResourceActions=" - + sqlResourceActions - + ", allResourceActions=" - + allResourceActions + "]"; } diff --git a/server/src/main/java/org/apache/druid/server/security/Policy.java b/server/src/main/java/org/apache/druid/server/security/Policy.java deleted file mode 100644 index c925ce6f67bd..000000000000 --- a/server/src/main/java/org/apache/druid/server/security/Policy.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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.security; - -import com.google.common.collect.Iterables; -import org.apache.druid.error.DruidException; -import org.apache.druid.query.filter.DimFilter; -import org.apache.druid.query.filter.EqualityFilter; -import org.apache.druid.query.filter.TrueDimFilter; -import org.apache.druid.segment.column.ColumnType; - -import java.util.Collection; -import java.util.Objects; - -public class Policy -{ - public static class RowPolicy - { - public static RowPolicy ALLOW_ALL = createPermissivePolicy(TrueDimFilter.instance()); - - - public enum Type - { - // A permissive row policy gives permission, multiple permissive policies can be combined with 'Or' - Permissive, - // A restrictive row policy restricts permission, multiple restrictive policies can be combined with 'And' - Restrictive; - } - - private final Type type; - private final DimFilter filter; - - public RowPolicy(Type type, DimFilter filter) - { - this.type = type; - this.filter = filter; - } - - public static RowPolicy createRestrictiveTenantIdPolicy(String columnName, String tenantIdValue) - { - return new RowPolicy(Type.Restrictive, new EqualityFilter(columnName, ColumnType.STRING, tenantIdValue, null)); - } - - public static RowPolicy createPermissivePolicy(DimFilter filter) - { - return new RowPolicy(Type.Permissive, filter); - } - - public boolean isPermissive() - { - return type.equals(Type.Permissive); - } - - public DimFilter getFilter() - { - return this.filter; - } - - public static DimFilter combines(Collection policies) - { - assert !policies.isEmpty(); - if (policies.size() == 1) { - return Iterables.getOnlyElement(policies).getFilter(); - } - - throw DruidException.defensive("Multiple policies are not expected."); - } - - @Override - public boolean equals(Object object) - { - if (this == object) { - return true; - } - if (object == null || getClass() != object.getClass()) { - return false; - } - RowPolicy rowPolicy = (RowPolicy) object; - return type == rowPolicy.type && Objects.equals(filter, rowPolicy.filter); - } - - @Override - public int hashCode() - { - return Objects.hash(type, filter); - } - - } -} diff --git a/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java b/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java index c3cb9817bd8c..8aa0a3772f35 100644 --- a/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java +++ b/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java @@ -20,8 +20,6 @@ package org.apache.druid.initialization; import com.google.common.collect.ImmutableMap; -import junitparams.JUnitParamsRunner; -import junitparams.Parameters; import nl.jqno.equalsverifier.EqualsVerifier; import org.apache.druid.common.config.NullHandling; import org.apache.druid.error.DruidException; @@ -32,7 +30,6 @@ import org.apache.druid.server.security.AuthorizationResult; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; import java.util.Optional; @@ -41,7 +38,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -@RunWith(JUnitParamsRunner.class) public class AuthorizationResultTest { @Before @@ -71,7 +67,7 @@ public void testToString() ) ); assertEquals( - "AuthorizationResult [permission=ALLOW_WITH_RESTRICTION, failureMessage=null, policyRestrictions={table1=Optional[NO_RESTRICTION], table2=Optional[RowFilterPolicy{rowFilter=column1 = val1}]}, sqlResourceActions=null, allResourceActions=null]", + "AuthorizationResult [permission=ALLOW_WITH_RESTRICTION, failureMessage=null, policyRestrictions={table1=Optional[NO_RESTRICTION], table2=Optional[RowFilterPolicy{rowFilter=column1 = val1}]}]", result.toString() ); } @@ -112,11 +108,7 @@ public void testFullAccess() } @Test - @Parameters({ - "true, Optional[Unauthorized]", - "false, Optional.empty" - }) - public void testRestrictedAccess(boolean policyRestrictionsNotPermitted, String error) + public void testRestrictedAccess() { AuthorizationResult result = AuthorizationResult.allowWithRestriction(ImmutableMap.of( "table1", diff --git a/sql/src/main/java/org/apache/druid/sql/AbstractStatement.java b/sql/src/main/java/org/apache/druid/sql/AbstractStatement.java index a9cef6430557..ee5cb290e9a5 100644 --- a/sql/src/main/java/org/apache/druid/sql/AbstractStatement.java +++ b/sql/src/main/java/org/apache/druid/sql/AbstractStatement.java @@ -70,7 +70,7 @@ public abstract class AbstractStatement implements Closeable */ protected final Map queryContext; protected PlannerContext plannerContext; - protected AuthorizationResult authResult; + protected DruidPlanner.AuthResult authResult; protected PlannerHook hook; public AbstractStatement( @@ -151,8 +151,8 @@ protected void authorize( // Authentication is done by the planner using the function provided // here. The planner ensures that this step is done before planning. authResult = planner.authorize(authorizer, contextResources); - if (!authResult.allowBasicAccess()) { - throw new ForbiddenException(authResult.getErrorMessage()); + if (!authResult.authorizationResult.allowBasicAccess()) { + throw new ForbiddenException(authResult.authorizationResult.getErrorMessage()); } } @@ -175,12 +175,12 @@ protected Function, AuthorizationResult> authorizer() */ public Set resources() { - return Objects.requireNonNull(authResult.getSqlResourceActions()); + return Objects.requireNonNull(authResult.sqlResourceActions); } public Set allResources() { - return Objects.requireNonNull(authResult.getAllResourceActions()); + return Objects.requireNonNull(authResult.allResourceActions); } public SqlQueryPlus query() diff --git a/sql/src/main/java/org/apache/druid/sql/SqlExecutionReporter.java b/sql/src/main/java/org/apache/druid/sql/SqlExecutionReporter.java index 75d4ee4dd24d..f531144b5f05 100644 --- a/sql/src/main/java/org/apache/druid/sql/SqlExecutionReporter.java +++ b/sql/src/main/java/org/apache/druid/sql/SqlExecutionReporter.java @@ -103,7 +103,7 @@ public void emit() // datasources. metricBuilder.setDimension( "dataSource", - stmt.authResult.getSqlResourceActions() + stmt.authResult.sqlResourceActions .stream() .map(action -> action.getResource().getName()) .collect(Collectors.toList()) diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidPlanner.java b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidPlanner.java index d236a3bc2213..44cc1848f923 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidPlanner.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidPlanner.java @@ -42,6 +42,7 @@ import org.apache.druid.sql.calcite.parser.DruidSqlReplace; import org.apache.druid.sql.calcite.parser.ParseException; import org.apache.druid.sql.calcite.parser.Token; +import org.apache.druid.sql.calcite.planner.DruidPlanner.AuthResult; import org.apache.druid.sql.calcite.run.SqlEngine; import org.joda.time.DateTimeZone; @@ -72,6 +73,35 @@ public enum State START, VALIDATED, PREPARED, PLANNED } + public static class AuthResult + { + public final AuthorizationResult authorizationResult; + + /** + * Resource actions used with authorizing a cancellation request. These actions + * include only the data-level actions (e.g. the datasource.) + */ + public final Set sqlResourceActions; + + /** + * Full resource actions authorized as part of this request. Used when logging + * resource actions. Includes query context keys, if query context authorization + * is enabled. + */ + public final Set allResourceActions; + + public AuthResult( + final AuthorizationResult authorizationResult, + final Set sqlResourceActions, + final Set allResourceActions + ) + { + this.authorizationResult = authorizationResult; + this.sqlResourceActions = sqlResourceActions; + this.allResourceActions = allResourceActions; + } + } + private final FrameworkConfig frameworkConfig; private final CalcitePlanner planner; private final PlannerContext plannerContext; @@ -203,7 +233,7 @@ public PrepareResult prepare() * authorize. * @return the return value from the authorizer */ - public AuthorizationResult authorize( + public AuthResult authorize( final Function, AuthorizationResult> authorizer, final Set extraActions ) @@ -219,10 +249,7 @@ public AuthorizationResult authorize( // Views prepare without authorization, Avatica does authorize, then prepare, // so the only constraint is that authorization be done before planning. authorized = true; - return authorizationResult.withResourceActions( - sqlResourceActions, - allResourceActions - ); + return new AuthResult(authorizationResult, sqlResourceActions, allResourceActions); } /** From cedf9bbfe366452760de77cfb5c7042430755b18 Mon Sep 17 00:00:00 2001 From: cecemei Date: Tue, 7 Jan 2025 18:10:35 -0800 Subject: [PATCH 19/32] minor update --- .../java/org/apache/druid/query/RestrictedDataSource.java | 7 +++---- .../org/apache/druid/query/RestrictedDataSourceTest.java | 3 --- .../org/apache/druid/sql/calcite/planner/DruidPlanner.java | 1 - 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java b/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java index a6eabd6476b2..25b9ff28fd97 100644 --- a/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java +++ b/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java @@ -40,11 +40,10 @@ import java.util.function.Function; /** - * Reperesents a TableDataSource with row-level restriction. + * Reperesents a TableDataSource with policy restriction. *

    - * A RestrictedDataSource means the base TableDataSource has restriction imposed. A table without any restriction should - * never be transformed to a RestrictedDataSource. Druid internal system and admin users would have a null rowFilter, - * while external users would have a rowFilter based on the applied restriction. + * A RestrictedDataSource means the base TableDataSource has policy imposed. A table without any policy should never be + * transformed to a RestrictedDataSource. Druid internal system and admin users would have a {@link NoRestrictionPolicy}. */ public class RestrictedDataSource implements DataSource { diff --git a/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java b/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java index 676b0ce77524..4e7473acaaf8 100644 --- a/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java +++ b/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java @@ -36,9 +36,6 @@ public class RestrictedDataSourceTest { - @Rule - public ExpectedException expectedException = ExpectedException.none(); - private final TableDataSource fooDataSource = new TableDataSource("foo"); private final TableDataSource barDataSource = new TableDataSource("bar"); private final RestrictedDataSource restrictedFooDataSource = RestrictedDataSource.create( diff --git a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidPlanner.java b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidPlanner.java index 44cc1848f923..07a632f9c82d 100644 --- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidPlanner.java +++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidPlanner.java @@ -42,7 +42,6 @@ import org.apache.druid.sql.calcite.parser.DruidSqlReplace; import org.apache.druid.sql.calcite.parser.ParseException; import org.apache.druid.sql.calcite.parser.Token; -import org.apache.druid.sql.calcite.planner.DruidPlanner.AuthResult; import org.apache.druid.sql.calcite.run.SqlEngine; import org.joda.time.DateTimeZone; From e275b1f74fbc42ddc0cf4ee627cc01266517d826 Mon Sep 17 00:00:00 2001 From: cecemei Date: Wed, 8 Jan 2025 11:03:18 -0800 Subject: [PATCH 20/32] add visit(CursorBuilderSpec) to policy interface, and add tests for policy subclasses --- .../query/policy/NoRestrictionPolicy.java | 7 +++ .../org/apache/druid/query/policy/Policy.java | 9 ++- .../druid/query/policy/RowFilterPolicy.java | 16 ++++- .../segment/RestrictedCursorFactory.java | 21 +------ .../query/policy/NoRestrictionPolicyTest.java | 34 ++++++++++ .../query/policy/RowFilterPolicyTest.java | 63 +++++++++++++++++++ 6 files changed, 126 insertions(+), 24 deletions(-) create mode 100644 processing/src/test/java/org/apache/druid/query/policy/NoRestrictionPolicyTest.java create mode 100644 processing/src/test/java/org/apache/druid/query/policy/RowFilterPolicyTest.java diff --git a/processing/src/main/java/org/apache/druid/query/policy/NoRestrictionPolicy.java b/processing/src/main/java/org/apache/druid/query/policy/NoRestrictionPolicy.java index abe73349cb88..28b5ae949b88 100644 --- a/processing/src/main/java/org/apache/druid/query/policy/NoRestrictionPolicy.java +++ b/processing/src/main/java/org/apache/druid/query/policy/NoRestrictionPolicy.java @@ -20,6 +20,7 @@ package org.apache.druid.query.policy; import com.fasterxml.jackson.annotation.JsonCreator; +import org.apache.druid.segment.CursorBuildSpec; /** * Represents a special kind of policy restriction, indicating that this table is restricted, but doesn't impose any restriction @@ -34,6 +35,12 @@ public class NoRestrictionPolicy implements Policy { } + @Override + public CursorBuildSpec visit(CursorBuildSpec spec) + { + return spec; + } + @Override public String toString() { diff --git a/processing/src/main/java/org/apache/druid/query/policy/Policy.java b/processing/src/main/java/org/apache/druid/query/policy/Policy.java index ae74de46862b..3dc2c3a15179 100644 --- a/processing/src/main/java/org/apache/druid/query/policy/Policy.java +++ b/processing/src/main/java/org/apache/druid/query/policy/Policy.java @@ -21,9 +21,10 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.apache.druid.segment.CursorBuildSpec; /** - * Represents a granular-level (e.x. row filter) restriction on read-table access. + * Extensible interface for a granular-level (e.x. row filter) restriction on read-table access. */ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @@ -32,6 +33,12 @@ }) public interface Policy { + /** + * Apply this policy to a {@link CursorBuildSpec} to seamlessly enforce policies for cursor-based queries. The + * application must encapsulate 100% of the requirements of this policy. + */ + CursorBuildSpec visit(CursorBuildSpec spec); + /** * Defines how strict we want to enforce the policy on tables during query execution process. *

      diff --git a/processing/src/main/java/org/apache/druid/query/policy/RowFilterPolicy.java b/processing/src/main/java/org/apache/druid/query/policy/RowFilterPolicy.java index ca9c03419588..eb67e1457901 100644 --- a/processing/src/main/java/org/apache/druid/query/policy/RowFilterPolicy.java +++ b/processing/src/main/java/org/apache/druid/query/policy/RowFilterPolicy.java @@ -21,7 +21,11 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.filter.Filter; +import org.apache.druid.segment.CursorBuildSpec; +import org.apache.druid.segment.filter.AndFilter; import javax.annotation.Nonnull; import java.util.Objects; @@ -45,10 +49,16 @@ public static RowFilterPolicy from(@Nonnull DimFilter rowFilter) return new RowFilterPolicy(rowFilter); } - @Nonnull - public DimFilter getRowFilter() + @Override + public CursorBuildSpec visit(CursorBuildSpec spec) { - return rowFilter; + CursorBuildSpec.CursorBuildSpecBuilder builder = CursorBuildSpec.builder(spec); + final Filter filter = spec.getFilter(); + final Filter policyFilter = this.rowFilter.toFilter(); + + final Filter newFilter = filter == null ? policyFilter : new AndFilter(ImmutableList.of(policyFilter, filter)); + builder.setFilter(newFilter); + return builder.build(); } @Override diff --git a/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java b/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java index a91018f44aa7..130fe59059d2 100644 --- a/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java +++ b/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java @@ -19,15 +19,9 @@ package org.apache.druid.segment; -import com.google.common.collect.ImmutableList; -import org.apache.druid.error.DruidException; -import org.apache.druid.query.filter.Filter; -import org.apache.druid.query.policy.NoRestrictionPolicy; import org.apache.druid.query.policy.Policy; -import org.apache.druid.query.policy.RowFilterPolicy; import org.apache.druid.segment.column.ColumnCapabilities; import org.apache.druid.segment.column.RowSignature; -import org.apache.druid.segment.filter.AndFilter; import javax.annotation.Nullable; @@ -48,20 +42,7 @@ public RestrictedCursorFactory( @Override public CursorHolder makeCursorHolder(CursorBuildSpec spec) { - if (policy instanceof NoRestrictionPolicy) { - return delegate.makeCursorHolder(spec); - } else if (policy instanceof RowFilterPolicy) { - final Filter rowFilter = ((RowFilterPolicy) policy).getRowFilter().toFilter(); - final CursorBuildSpec.CursorBuildSpecBuilder buildSpecBuilder = CursorBuildSpec.builder(spec); - final Filter newFilter = spec.getFilter() == null - ? rowFilter - : new AndFilter(ImmutableList.of(spec.getFilter(), rowFilter)); - buildSpecBuilder.setFilter(newFilter); - - return delegate.makeCursorHolder(buildSpecBuilder.build()); - } else { - throw DruidException.defensive("not supported policy type [%s]", policy.getClass()); - } + return delegate.makeCursorHolder(policy.visit(spec)); } @Override diff --git a/processing/src/test/java/org/apache/druid/query/policy/NoRestrictionPolicyTest.java b/processing/src/test/java/org/apache/druid/query/policy/NoRestrictionPolicyTest.java new file mode 100644 index 000000000000..a9cc1ab549f5 --- /dev/null +++ b/processing/src/test/java/org/apache/druid/query/policy/NoRestrictionPolicyTest.java @@ -0,0 +1,34 @@ +/* + * 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.query.policy; + +import org.apache.druid.segment.CursorBuildSpec; +import org.junit.Assert; +import org.junit.Test; + +public class NoRestrictionPolicyTest +{ + @Test + public void testVisit() + { + final NoRestrictionPolicy policy = NoRestrictionPolicy.INSTANCE; + Assert.assertEquals(CursorBuildSpec.FULL_SCAN, policy.visit(CursorBuildSpec.FULL_SCAN)); + } +} diff --git a/processing/src/test/java/org/apache/druid/query/policy/RowFilterPolicyTest.java b/processing/src/test/java/org/apache/druid/query/policy/RowFilterPolicyTest.java new file mode 100644 index 000000000000..8fa3112c8660 --- /dev/null +++ b/processing/src/test/java/org/apache/druid/query/policy/RowFilterPolicyTest.java @@ -0,0 +1,63 @@ +/* + * 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.query.policy; + +import com.google.common.collect.ImmutableList; +import org.apache.druid.common.config.NullHandling; +import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.filter.EqualityFilter; +import org.apache.druid.segment.CursorBuildSpec; +import org.apache.druid.segment.column.ColumnType; +import org.apache.druid.segment.filter.AndFilter; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.apache.druid.query.filter.Filter; + +public class RowFilterPolicyTest +{ + @Before + public void setup() + { + NullHandling.initializeForTests(); + } + + @Test + public void testVisit() + { + DimFilter policyFilter = new EqualityFilter("col", ColumnType.STRING, "val", null); + final RowFilterPolicy policy = RowFilterPolicy.from(policyFilter); + + Assert.assertEquals(policyFilter, policy.visit(CursorBuildSpec.FULL_SCAN).getFilter()); + } + + @Test + public void testVisit_combineFilters() + { + Filter filter = new EqualityFilter("col0", ColumnType.STRING, "val0", null); + CursorBuildSpec spec = CursorBuildSpec.builder().setFilter(filter).build(); + + DimFilter policyFilter = new EqualityFilter("col", ColumnType.STRING, "val", null); + final RowFilterPolicy policy = RowFilterPolicy.from(policyFilter); + + Filter expected = new AndFilter(ImmutableList.of(policyFilter.toFilter(), filter)); + Assert.assertEquals(expected, policy.visit(spec).getFilter()); + } +} From 0d26d74e892739663e5e0b0202bccdad0e781424 Mon Sep 17 00:00:00 2001 From: cecemei Date: Wed, 8 Jan 2025 12:49:57 -0800 Subject: [PATCH 21/32] format --- .../java/org/apache/druid/query/RestrictedDataSourceTest.java | 2 -- .../java/org/apache/druid/query/policy/RowFilterPolicyTest.java | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java b/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java index 4e7473acaaf8..0ed77c99be77 100644 --- a/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java +++ b/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java @@ -28,9 +28,7 @@ import org.apache.druid.query.policy.RowFilterPolicy; import org.apache.druid.segment.TestHelper; import org.junit.Assert; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import java.util.Collections; diff --git a/processing/src/test/java/org/apache/druid/query/policy/RowFilterPolicyTest.java b/processing/src/test/java/org/apache/druid/query/policy/RowFilterPolicyTest.java index 8fa3112c8660..b698b4394cd1 100644 --- a/processing/src/test/java/org/apache/druid/query/policy/RowFilterPolicyTest.java +++ b/processing/src/test/java/org/apache/druid/query/policy/RowFilterPolicyTest.java @@ -23,13 +23,13 @@ import org.apache.druid.common.config.NullHandling; import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.EqualityFilter; +import org.apache.druid.query.filter.Filter; import org.apache.druid.segment.CursorBuildSpec; import org.apache.druid.segment.column.ColumnType; import org.apache.druid.segment.filter.AndFilter; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.apache.druid.query.filter.Filter; public class RowFilterPolicyTest { From ab8eed184bcb210f94d8787baf9a53fff43e36d2 Mon Sep 17 00:00:00 2001 From: cecemei Date: Thu, 9 Jan 2025 09:51:23 -0800 Subject: [PATCH 22/32] Remove TablePolicySecurityLevel enum and add tests in QueryLifecycleTest for SegmentMetadataQuery. --- .../org/apache/druid/query/DataSource.java | 14 +- .../java/org/apache/druid/query/Query.java | 20 --- .../druid/query/RestrictedDataSource.java | 5 +- .../apache/druid/query/TableDataSource.java | 17 +- .../org/apache/druid/query/policy/Policy.java | 42 +---- .../apache/druid/query/DataSourceTest.java | 136 +++------------ .../apache/druid/server/QueryLifecycle.java | 6 +- .../druid/server/security/AuthConfig.java | 36 +--- .../druid/server/QueryLifecycleTest.java | 155 +++++++----------- 9 files changed, 98 insertions(+), 333 deletions(-) diff --git a/processing/src/main/java/org/apache/druid/query/DataSource.java b/processing/src/main/java/org/apache/druid/query/DataSource.java index 040b0a5c6133..339632838593 100644 --- a/processing/src/main/java/org/apache/druid/query/DataSource.java +++ b/processing/src/main/java/org/apache/druid/query/DataSource.java @@ -128,19 +128,17 @@ public interface DataSource *

      * If this datasource contains no table, no changes should occur. * - * @param policyMap a mapping of table names to policy restrictions - * @param tablePolicySecurityLevel an enum denoting that, how strict we need to enforce the policy on tables + * @param policyMap a mapping of table names to policy restrictions * @return the updated datasource, with restrictions applied in the datasource tree - * @throws IllegalStateException if {@code policyMap} is not compatible with {@code tablePolicySecurityLevel} + * @throws IllegalStateException when mapping a RestrictedDataSource, unless the table has a NoRestrictionPolicy in + * the policyMap (used by druid-internal). Missing policy or adding a + * non-NoRestrictionPolicy to RestrictedDataSource would throw. */ - default DataSource mapWithRestriction( - Map> policyMap, - Policy.TablePolicySecurityLevel tablePolicySecurityLevel - ) + default DataSource mapWithRestriction(Map> policyMap) { List children = this.getChildren() .stream() - .map(child -> child.mapWithRestriction(policyMap, tablePolicySecurityLevel)) + .map(child -> child.mapWithRestriction(policyMap)) .collect(Collectors.toList()); return this.withChildren(children); } diff --git a/processing/src/main/java/org/apache/druid/query/Query.java b/processing/src/main/java/org/apache/druid/query/Query.java index 29086153196a..3c085a6adede 100644 --- a/processing/src/main/java/org/apache/druid/query/Query.java +++ b/processing/src/main/java/org/apache/druid/query/Query.java @@ -34,7 +34,6 @@ import org.apache.druid.query.metadata.metadata.SegmentMetadataQuery; import org.apache.druid.query.operator.WindowOperatorQuery; import org.apache.druid.query.planning.DataSourceAnalysis; -import org.apache.druid.query.policy.Policy; import org.apache.druid.query.scan.ScanQuery; import org.apache.druid.query.search.SearchQuery; import org.apache.druid.query.select.SelectQuery; @@ -55,7 +54,6 @@ import javax.annotation.Nullable; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -244,24 +242,6 @@ default String getMostSpecificId() Query withDataSource(DataSource dataSource); - /** - * Returns the query with an updated datasource based on the policy restrictions on tables. - *

      - * If this datasource contains no table, no changes should occur. - * - * @param policyMap a mapping of table names to policy restrictions - * @param tablePolicySecurityLevel an enum denoting that, how strict we need to enforce the policy on tables - * @return the updated datasource, with restrictions applied in the datasource tree - * @throws IllegalStateException if {@code policyMap} is not compatible with {@code tablePolicySecurityLevel} - */ - default Query withPolicyRestrictions( - Map> policyMap, - Policy.TablePolicySecurityLevel tablePolicySecurityLevel - ) - { - return this.withDataSource(this.getDataSource().mapWithRestriction(policyMap, tablePolicySecurityLevel)); - } - default Query optimizeForSegment(PerSegmentQueryOptimizationContext optimizationContext) { return this; diff --git a/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java b/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java index 25b9ff28fd97..91637c181f9b 100644 --- a/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java +++ b/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java @@ -145,10 +145,7 @@ public DataSource withUpdatedDataSource(DataSource newSource) } @Override - public DataSource mapWithRestriction( - Map> policyMap, - Policy.TablePolicySecurityLevel tablePolicySecurityLevel - ) + public DataSource mapWithRestriction(Map> policyMap) { if (!policyMap.containsKey(base.getName())) { throw new ISE("Missing policy check result for table [%s]", base.getName()); diff --git a/processing/src/main/java/org/apache/druid/query/TableDataSource.java b/processing/src/main/java/org/apache/druid/query/TableDataSource.java index b8dcba9060a5..b2e0504d4ca0 100644 --- a/processing/src/main/java/org/apache/druid/query/TableDataSource.java +++ b/processing/src/main/java/org/apache/druid/query/TableDataSource.java @@ -24,7 +24,6 @@ import com.fasterxml.jackson.annotation.JsonTypeName; import com.google.common.base.Preconditions; import org.apache.druid.java.util.common.IAE; -import org.apache.druid.java.util.common.ISE; import org.apache.druid.query.planning.DataSourceAnalysis; import org.apache.druid.query.policy.Policy; import org.apache.druid.segment.SegmentReference; @@ -117,22 +116,12 @@ public DataSource withUpdatedDataSource(DataSource newSource) } @Override - public DataSource mapWithRestriction( - Map> policyMap, - Policy.TablePolicySecurityLevel tablePolicySecurityLevel - ) + public DataSource mapWithRestriction(Map> policyMap) { - if (!policyMap.containsKey(name) && tablePolicySecurityLevel.policyMustBeCheckedOnAllTables()) { - throw new ISE("Need to check row-level policy for all tables missing [%s]", name); - } Optional policy = policyMap.getOrDefault(name, Optional.empty()); if (!policy.isPresent()) { - if (tablePolicySecurityLevel.policyMustBeCheckedAndExistOnAllTables()) { - throw new ISE("Every table must have a policy restriction attached missing [%s]", name); - } else { - // Skip adding restriction on table if there's no policy restriction found. - return this; - } + // Skip adding restriction on table if there's no policy restriction found. + return this; } return RestrictedDataSource.create(this, policy.get()); } diff --git a/processing/src/main/java/org/apache/druid/query/policy/Policy.java b/processing/src/main/java/org/apache/druid/query/policy/Policy.java index 3dc2c3a15179..28d61ac0c1c5 100644 --- a/processing/src/main/java/org/apache/druid/query/policy/Policy.java +++ b/processing/src/main/java/org/apache/druid/query/policy/Policy.java @@ -24,7 +24,8 @@ import org.apache.druid.segment.CursorBuildSpec; /** - * Extensible interface for a granular-level (e.x. row filter) restriction on read-table access. + * Extensible interface for a granular-level (e.x. row filter) restriction on read-table access. Implementations must be + * Jackson-serializable. */ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes({ @@ -39,43 +40,4 @@ public interface Policy */ CursorBuildSpec visit(CursorBuildSpec spec); - /** - * Defines how strict we want to enforce the policy on tables during query execution process. - *

        - *
      1. {@code APPLY_WHEN_APPLICABLE}, the most basic level, restriction is applied whenever seen fit. - *
      2. {@code POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY}, every table must have been checked on the policy. - *
      3. {@code POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST}, every table must have a policy when requests come from external users. - *
      - */ - enum TablePolicySecurityLevel - { - APPLY_WHEN_APPLICABLE(0), - POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY(1), - POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST(2); - - private final int securityLevel; - - TablePolicySecurityLevel(int securityLevel) - { - this.securityLevel = securityLevel; - } - - /** - * Returns true if the security level requires that, every table must have an entry in the policy map during query - * execution stage. - */ - public boolean policyMustBeCheckedOnAllTables() - { - return securityLevel >= 1; - } - - /** - * Returns true if the security level requires that, every table must have a policy during query execution stage, - * this means the table must have a non-empty value in the policy map. - */ - public boolean policyMustBeCheckedAndExistOnAllTables() - { - return securityLevel >= 2; - } - } } diff --git a/processing/src/test/java/org/apache/druid/query/DataSourceTest.java b/processing/src/test/java/org/apache/druid/query/DataSourceTest.java index fa907db41efe..5f38fdbbeddc 100644 --- a/processing/src/test/java/org/apache/druid/query/DataSourceTest.java +++ b/processing/src/test/java/org/apache/druid/query/DataSourceTest.java @@ -23,14 +23,11 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; -import junitparams.JUnitParamsRunner; -import junitparams.Parameters; import org.apache.druid.common.config.NullHandling; import org.apache.druid.java.util.common.ISE; import org.apache.druid.query.aggregation.LongSumAggregatorFactory; import org.apache.druid.query.dimension.DefaultDimensionSpec; import org.apache.druid.query.filter.NullFilter; -import org.apache.druid.query.filter.TrueDimFilter; import org.apache.druid.query.groupby.GroupByQuery; import org.apache.druid.query.policy.NoRestrictionPolicy; import org.apache.druid.query.policy.Policy; @@ -39,12 +36,10 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; import java.io.IOException; import java.util.Optional; -@RunWith(JUnitParamsRunner.class) public class DataSourceTest { private static final ObjectMapper JSON_MAPPER = TestHelper.makeJsonMapper(); @@ -135,12 +130,7 @@ public void testUnionDataSource() throws Exception } @Test - @Parameters({ - "APPLY_WHEN_APPLICABLE", - "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY", - "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST" - }) - public void testMapWithRestriction(Policy.TablePolicySecurityLevel securityLevel) + public void testMapWithRestriction() { TableDataSource table1 = TableDataSource.create("table1"); TableDataSource table2 = TableDataSource.create("table2"); @@ -159,88 +149,29 @@ public void testMapWithRestriction(Policy.TablePolicySecurityLevel securityLevel ); Assert.assertEquals( - unionDataSource.mapWithRestriction(restrictions, securityLevel), + unionDataSource.mapWithRestriction(restrictions), new UnionDataSource(Lists.newArrayList( - RestrictedDataSource.create(table1, NoRestrictionPolicy.INSTANCE), - RestrictedDataSource.create(table2, NoRestrictionPolicy.INSTANCE), - RestrictedDataSource.create(table3, RowFilterPolicy.from(new NullFilter("some-column", null)) + RestrictedDataSource.create( + table1, + NoRestrictionPolicy.INSTANCE + ), + RestrictedDataSource.create( + table2, + NoRestrictionPolicy.INSTANCE + ), + RestrictedDataSource.create( + table3, + RowFilterPolicy.from(new NullFilter( + "some-column", + null + )) ) )) ); } @Test - @Parameters({ - "APPLY_WHEN_APPLICABLE, ", - "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY, Need to check row-level policy for all tables missing [table2]", - "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST, Need to check row-level policy for all tables missing [table2]" - }) - public void testMapWithRestriction_tableMissingRestriction( - Policy.TablePolicySecurityLevel securityLevel, - String error - ) - { - TableDataSource table1 = TableDataSource.create("table1"); - TableDataSource table2 = TableDataSource.create("table2"); - UnionDataSource unionDataSource = new UnionDataSource(Lists.newArrayList(table1, table2)); - ImmutableMap> restrictions = ImmutableMap.of( - "table1", - Optional.of(RowFilterPolicy.from(TrueDimFilter.instance())) - ); - - if (error.isEmpty()) { - unionDataSource.mapWithRestriction(restrictions, securityLevel); - } else { - ISE e = Assert.assertThrows(ISE.class, () -> unionDataSource.mapWithRestriction(restrictions, securityLevel)); - Assert.assertEquals(e.getMessage(), error); - } - } - - @Test - @Parameters({ - "APPLY_WHEN_APPLICABLE, ", - "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY, ", - "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST, Every table must have a policy restriction attached missing [table2]" - }) - public void testMapWithRestriction_tableWithEmptyPolicy(Policy.TablePolicySecurityLevel securityLevel, String error) - { - TableDataSource table1 = TableDataSource.create("table1"); - TableDataSource table2 = TableDataSource.create("table2"); - UnionDataSource unionDataSource = new UnionDataSource(Lists.newArrayList(table1, table2)); - ImmutableMap> restrictions = ImmutableMap.of( - "table1", - Optional.of(NoRestrictionPolicy.INSTANCE), - "table2", - Optional.empty() - ); - - if (error.isEmpty()) { - Assert.assertEquals( - unionDataSource.mapWithRestriction(restrictions, securityLevel), - new UnionDataSource(Lists.newArrayList( - RestrictedDataSource.create(table1, NoRestrictionPolicy.INSTANCE), - table2 - )) - ); - } else { - ISE e = Assert.assertThrows( - ISE.class, - () -> unionDataSource.mapWithRestriction( - restrictions, - securityLevel - ) - ); - Assert.assertEquals(e.getMessage(), error); - } - } - - @Test - @Parameters({ - "APPLY_WHEN_APPLICABLE", - "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY", - "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST" - }) - public void testMapWithRestriction_onRestrictedDataSource_fromDruidSystem(Policy.TablePolicySecurityLevel securityLevel) + public void testMapWithRestriction_onRestrictedDataSource_fromDruidSystem() { RestrictedDataSource restrictedDataSource = RestrictedDataSource.create( TableDataSource.create("table1"), @@ -252,19 +183,11 @@ public void testMapWithRestriction_onRestrictedDataSource_fromDruidSystem(Policy Optional.of(NoRestrictionPolicy.INSTANCE) ); - Assert.assertEquals( - restrictedDataSource, - restrictedDataSource.mapWithRestriction(noRestrictionPolicy, securityLevel) - ); + Assert.assertEquals(restrictedDataSource, restrictedDataSource.mapWithRestriction(noRestrictionPolicy)); } @Test - @Parameters({ - "APPLY_WHEN_APPLICABLE", - "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY", - "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST" - }) - public void testMapWithRestriction_onRestrictedDataSource_alwaysThrows(Policy.TablePolicySecurityLevel securityLevel) + public void testMapWithRestriction_onRestrictedDataSource_alwaysThrows() { RestrictedDataSource restrictedDataSource = RestrictedDataSource.create( TableDataSource.create("table1"), @@ -277,31 +200,18 @@ public void testMapWithRestriction_onRestrictedDataSource_alwaysThrows(Policy.Ta ImmutableMap> noPolicyFound = ImmutableMap.of("table1", Optional.empty()); ImmutableMap> policyWasNotChecked = ImmutableMap.of(); - ISE e = Assert.assertThrows( - ISE.class, - () -> restrictedDataSource.mapWithRestriction(anotherRestrictions, securityLevel) - ); + ISE e = Assert.assertThrows(ISE.class, () -> restrictedDataSource.mapWithRestriction(anotherRestrictions)); Assert.assertEquals( "Multiple restrictions on table [table1]: policy [RowFilterPolicy{rowFilter=random-column IS NULL}] and policy [RowFilterPolicy{rowFilter=some-column IS NULL}]", e.getMessage() ); - ISE e2 = Assert.assertThrows( - ISE.class, - () -> restrictedDataSource.mapWithRestriction(noPolicyFound, securityLevel) - ); + ISE e2 = Assert.assertThrows(ISE.class, () -> restrictedDataSource.mapWithRestriction(noPolicyFound)); Assert.assertEquals( "No restriction found on table [table1], but had policy [RowFilterPolicy{rowFilter=random-column IS NULL}] before.", e2.getMessage() ); - - ISE e3 = Assert.assertThrows( - ISE.class, - () -> restrictedDataSource.mapWithRestriction(policyWasNotChecked, securityLevel) - ); - Assert.assertEquals( - "Missing policy check result for table [table1]", - e3.getMessage() - ); + ISE e3 = Assert.assertThrows(ISE.class, () -> restrictedDataSource.mapWithRestriction(policyWasNotChecked)); + Assert.assertEquals("Missing policy check result for table [table1]", e3.getMessage()); } } 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 e2ead4bf696e..5dfdfc4357b0 100644 --- a/server/src/main/java/org/apache/druid/server/QueryLifecycle.java +++ b/server/src/main/java/org/apache/druid/server/QueryLifecycle.java @@ -324,10 +324,8 @@ private AuthorizationResult doAuthorize( if (this.baseQuery instanceof SegmentMetadataQuery && authorizationResult.isUserWithNoRestriction()) { // skip restrictions mapping for SegmentMetadataQuery from user with no restriction } else { - this.baseQuery = this.baseQuery.withPolicyRestrictions( - authorizationResult.getPolicy(), - authConfig.getTablePolicySecurityLevel() - ); + this.baseQuery = this.baseQuery.withDataSource(this.baseQuery.getDataSource() + .mapWithRestriction(authorizationResult.getPolicy())); } } 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 187cc83d6d3c..4ad6325d8e45 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 @@ -23,7 +23,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.collect.ImmutableSet; import org.apache.druid.query.QueryContexts; -import org.apache.druid.query.policy.Policy; import org.apache.druid.utils.CollectionUtils; import java.util.Collections; @@ -64,7 +63,7 @@ public class AuthConfig public AuthConfig() { - this(null, null, null, false, false, null, null, false, null); + this(null, null, null, false, false, null, null, false); } @JsonProperty @@ -101,9 +100,6 @@ public AuthConfig() @JsonProperty private final boolean enableInputSourceSecurity; - @JsonProperty - private final Policy.TablePolicySecurityLevel tablePolicySecurityLevel; - @JsonCreator public AuthConfig( @JsonProperty("authenticatorChain") List authenticatorChain, @@ -113,8 +109,7 @@ public AuthConfig( @JsonProperty("authorizeQueryContextParams") boolean authorizeQueryContextParams, @JsonProperty("unsecuredContextKeys") Set unsecuredContextKeys, @JsonProperty("securedContextKeys") Set securedContextKeys, - @JsonProperty("enableInputSourceSecurity") boolean enableInputSourceSecurity, - @JsonProperty("tablePolicySecurityLevel") Policy.TablePolicySecurityLevel tablePolicySecurityLevel + @JsonProperty("enableInputSourceSecurity") boolean enableInputSourceSecurity ) { this.authenticatorChain = authenticatorChain; @@ -127,7 +122,6 @@ public AuthConfig( : unsecuredContextKeys; this.securedContextKeys = securedContextKeys; this.enableInputSourceSecurity = enableInputSourceSecurity; - this.tablePolicySecurityLevel = tablePolicySecurityLevel == null ? Policy.TablePolicySecurityLevel.APPLY_WHEN_APPLICABLE : tablePolicySecurityLevel; } public List getAuthenticatorChain() @@ -160,15 +154,6 @@ public boolean isEnableInputSourceSecurity() return enableInputSourceSecurity; } - /** - * When enabled, {@link org.apache.druid.server.QueryLifecycle} checks a policy entry in {@link AuthorizationResult#getPolicy()} - * for all tables in the query, and throws exception when there's no entry. - */ - public Policy.TablePolicySecurityLevel getTablePolicySecurityLevel() - { - return tablePolicySecurityLevel; - } - /** * Filter the user-supplied context keys based on the context key security * rules. If context key security is disabled, then allow all keys. Else, @@ -216,8 +201,7 @@ public boolean equals(Object o) && Objects.equals(unsecuredPaths, that.unsecuredPaths) && Objects.equals(unsecuredContextKeys, that.unsecuredContextKeys) && Objects.equals(securedContextKeys, that.securedContextKeys) - && Objects.equals(enableInputSourceSecurity, that.enableInputSourceSecurity) - && Objects.equals(tablePolicySecurityLevel, that.tablePolicySecurityLevel); + && Objects.equals(enableInputSourceSecurity, that.enableInputSourceSecurity); } @Override @@ -231,8 +215,7 @@ public int hashCode() authorizeQueryContextParams, unsecuredContextKeys, securedContextKeys, - enableInputSourceSecurity, - tablePolicySecurityLevel + enableInputSourceSecurity ); } @@ -248,7 +231,6 @@ public String toString() ", unsecuredContextKeys=" + unsecuredContextKeys + ", securedContextKeys=" + securedContextKeys + ", enableInputSourceSecurity=" + enableInputSourceSecurity + - ", tablePolicySecurityLevel=" + tablePolicySecurityLevel + '}'; } @@ -270,7 +252,6 @@ public static class Builder private Set unsecuredContextKeys; private Set securedContextKeys; private boolean enableInputSourceSecurity; - private Policy.TablePolicySecurityLevel tablePolicySecurityLevel; public Builder setAuthenticatorChain(List authenticatorChain) { @@ -320,12 +301,6 @@ public Builder setEnableInputSourceSecurity(boolean enableInputSourceSecurity) return this; } - public Builder setTablePolicySecurityLevel(Policy.TablePolicySecurityLevel tablePolicySecurityLevel) - { - this.tablePolicySecurityLevel = tablePolicySecurityLevel; - return this; - } - public AuthConfig build() { return new AuthConfig( @@ -336,8 +311,7 @@ public AuthConfig build() authorizeQueryContextParams, unsecuredContextKeys, securedContextKeys, - enableInputSourceSecurity, - tablePolicySecurityLevel + enableInputSourceSecurity ); } } diff --git a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java index d8cdd1186ae3..675d7c8deab3 100644 --- a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java +++ b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java @@ -22,8 +22,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; -import junitparams.JUnitParamsRunner; -import junitparams.Parameters; import org.apache.druid.error.DruidException; import org.apache.druid.java.util.common.ISE; import org.apache.druid.java.util.common.Intervals; @@ -45,6 +43,7 @@ import org.apache.druid.query.aggregation.CountAggregatorFactory; import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.NullFilter; +import org.apache.druid.query.metadata.metadata.SegmentMetadataQuery; import org.apache.druid.query.policy.NoRestrictionPolicy; import org.apache.druid.query.policy.Policy; import org.apache.druid.query.policy.RowFilterPolicy; @@ -67,14 +66,12 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; import javax.servlet.http.HttpServletRequest; import java.util.HashMap; import java.util.Map; import java.util.Optional; -@RunWith(JUnitParamsRunner.class) public class QueryLifecycleTest { private static final String DATASOURCE = "some_datasource"; @@ -197,16 +194,10 @@ public void testRunSimpleUnauthorized() } @Test - @Parameters({ - "APPLY_WHEN_APPLICABLE", - "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY", - "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST" - }) - public void testRunSimple_withPolicyRestriction(Policy.TablePolicySecurityLevel securityLevel) + public void testRunSimple_withPolicyRestriction() { // Test the path when an external client send a sql query to broker, through runSimple. Policy rowFilterPolicy = RowFilterPolicy.from(new NullFilter("some-column", null)); - Access access = Access.allowWithRestriction(rowFilterPolicy); AuthorizationResult authorizationResult = AuthorizationResult.allowWithRestriction(ImmutableMap.of( DATASOURCE, Optional.of(rowFilterPolicy) @@ -221,7 +212,6 @@ public void testRunSimple_withPolicyRestriction(Policy.TablePolicySecurityLevel EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); - EasyMock.expect(authorizer.authorize(authenticationResult, RESOURCE, Action.READ)).andReturn(access).anyTimes(); EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) .andReturn(toolChest).once(); // We're expecting the data source in the query to be transformed to a RestrictedDataSource, with policy. @@ -229,75 +219,77 @@ public void testRunSimple_withPolicyRestriction(Policy.TablePolicySecurityLevel EasyMock.expect(texasRanger.getQueryRunnerForIntervals( queryMatchDataSource(expectedDataSource), EasyMock.anyObject() - )).andReturn(runner).anyTimes(); - EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).anyTimes(); + )).andReturn(runner).once(); + EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).once(); replayAll(); AuthConfig authConfig = AuthConfig.newBuilder() .setAuthorizeQueryContextParams(true) - .setTablePolicySecurityLevel(securityLevel) .build(); QueryLifecycle lifecycle = createLifecycle(authConfig); lifecycle.runSimple(query, authenticationResult, authorizationResult); } @Test - @Parameters({ - "APPLY_WHEN_APPLICABLE, ", - "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY, Need to check row-level policy for all tables missing [some_datasource]", - "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST, Need to check row-level policy for all tables missing [some_datasource]" - }) - public void testRunSimple_withNoRestrictionPolicy( - Policy.TablePolicySecurityLevel securityLevel, - String error - ) + public void testRunSimple_withPolicyRestriction_segmentMetadataQueryRunAsInternal() { - // When AuthorizationResult is ALLOW_NO_RESTRICTION, this means policy restriction has never been checked. - // Either it's calling from a mis-behave druid node, or somehow the path has bypassed the policy checks. - // This is only allowed at APPLY_WHEN_APPLICABLE, the lowest security level. - if (!error.isEmpty()) { - expectedException.expect(ISE.class); - expectedException.expectMessage(error); - } - - AuthorizationResult authorizationResult = AuthorizationResult.ALLOW_NO_RESTRICTION; + // Test the path when broker sends SegmentMetadataQuery to historical, through runSimple. + // The druid-internal gets a NoRestrictionPolicy. + AuthorizationResult authorizationResult = AuthorizationResult.allowWithRestriction(ImmutableMap.of( + DATASOURCE, + Optional.of(NoRestrictionPolicy.INSTANCE) + )); + final SegmentMetadataQuery query = Druids.newSegmentMetadataQueryBuilder() + .dataSource(DATASOURCE) + .intervals(ImmutableList.of(Intervals.ETERNITY)) + .build(); EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); + EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) - .andReturn(toolChest) - .once(); - EasyMock.expect(texasRanger.getQueryRunnerForIntervals(EasyMock.anyObject(), EasyMock.anyObject())) - .andReturn(runner) - .anyTimes(); - EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).anyTimes(); - EasyMock.expect(toolChest.makeMetrics(EasyMock.anyObject())).andReturn(metrics).anyTimes(); + .andReturn(toolChest).once(); + // We're expecting the data source in the query to still be TableDataSource. + // Any other DataSource would throw AssertionError. + EasyMock.expect(texasRanger.getQueryRunnerForIntervals( + queryMatchDataSource(TableDataSource.create(DATASOURCE)), + EasyMock.anyObject() + )).andReturn(runner).once(); + EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).once(); replayAll(); - QueryLifecycle lifecycle = createLifecycle(AuthConfig.newBuilder() - .setTablePolicySecurityLevel(securityLevel) - .build()); + QueryLifecycle lifecycle = createLifecycle(new AuthConfig()); lifecycle.runSimple(query, authenticationResult, authorizationResult); } + @Test - @Parameters({ - "APPLY_WHEN_APPLICABLE, ", - "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY, ", - "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST, Every table must have a policy restriction attached missing [some_datasource]" - }) - public void testRunSimple_withPolicyNotExist(Policy.TablePolicySecurityLevel securityLevel, String error) + public void testRunSimple_withPolicyRestriction_segmentMetadataQueryRunAsExternal() { - if (!error.isEmpty()) { - expectedException.expect(ISE.class); - expectedException.expectMessage(error); - } - // AuthorizationResult indicates there's no policy restriction on the table - // This is not allowed at the highest security level + Policy policy = RowFilterPolicy.from(new NullFilter("col", null)); AuthorizationResult authorizationResult = AuthorizationResult.allowWithRestriction(ImmutableMap.of( DATASOURCE, - Optional.empty() + Optional.of(policy) )); + final SegmentMetadataQuery query = Druids.newSegmentMetadataQueryBuilder() + .dataSource(DATASOURCE) + .intervals(ImmutableList.of(Intervals.ETERNITY)) + .build(); + EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); + EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); + EasyMock.expect(authenticationResult.getAuthorizerName()).andReturn(AUTHORIZER).anyTimes(); + EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) + .andReturn(toolChest).once(); + EasyMock.expect(toolChest.makeMetrics(EasyMock.anyObject())).andReturn(metrics).once(); + replayAll(); + + QueryLifecycle lifecycle = createLifecycle(new AuthConfig()); + Assert.assertThrows(Exception.class, () -> lifecycle.runSimple(query, authenticationResult, authorizationResult)); + } + @Test + public void testRunSimple_withoutPolicy() + { + AuthorizationResult authorizationResult = AuthorizationResult.ALLOW_NO_RESTRICTION; EasyMock.expect(queryConfig.getContext()).andReturn(ImmutableMap.of()).anyTimes(); EasyMock.expect(authenticationResult.getIdentity()).andReturn(IDENTITY).anyTimes(); EasyMock.expect(conglomerate.getToolChest(EasyMock.anyObject())) @@ -310,19 +302,12 @@ public void testRunSimple_withPolicyNotExist(Policy.TablePolicySecurityLevel sec EasyMock.expect(toolChest.makeMetrics(EasyMock.anyObject())).andReturn(metrics).anyTimes(); replayAll(); - QueryLifecycle lifecycle = createLifecycle(AuthConfig.newBuilder() - .setTablePolicySecurityLevel(securityLevel) - .build()); + QueryLifecycle lifecycle = createLifecycle(AuthConfig.newBuilder().build()); lifecycle.runSimple(query, authenticationResult, authorizationResult); } @Test - @Parameters({ - "APPLY_WHEN_APPLICABLE", - "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY", - "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST" - }) - public void testRunSimple_foundMultiplePolicyRestrictions(Policy.TablePolicySecurityLevel securityLevel) + public void testRunSimple_foundMultiplePolicyRestrictions() { // Multiple policy restrictions indicates most likely the system is trying to double-authorizing the request // This is not allowed in any case. @@ -357,21 +342,12 @@ public void testRunSimple_foundMultiplePolicyRestrictions(Policy.TablePolicySecu EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).anyTimes(); replayAll(); - AuthConfig authConfig = AuthConfig.newBuilder() - .setTablePolicySecurityLevel(securityLevel) - .build(); - - QueryLifecycle lifecycle = createLifecycle(authConfig); + QueryLifecycle lifecycle = createLifecycle(new AuthConfig()); lifecycle.runSimple(query, authenticationResult, authorizationResult); } @Test - @Parameters({ - "APPLY_WHEN_APPLICABLE", - "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY", - "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST" - }) - public void testRunSimple_queryWithRestrictedDataSource_policyRestrictionMightHaveBeenRemoved(Policy.TablePolicySecurityLevel securityLevel) + public void testRunSimple_queryWithRestrictedDataSource_policyRestrictionMightHaveBeenRemoved() { expectedException.expect(ISE.class); expectedException.expectMessage( @@ -408,22 +384,12 @@ public void testRunSimple_queryWithRestrictedDataSource_policyRestrictionMightHa EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).anyTimes(); replayAll(); - AuthConfig authConfig = AuthConfig.newBuilder() - .setTablePolicySecurityLevel(securityLevel) - .build(); - - QueryLifecycle lifecycle = createLifecycle(authConfig); + QueryLifecycle lifecycle = createLifecycle(new AuthConfig()); lifecycle.runSimple(query, authenticationResult, authorizationResult); } @Test - @Parameters({ - "APPLY_WHEN_APPLICABLE", - "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY", - "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST" - }) - - public void testAuthorized_withPolicyRestriction(Policy.TablePolicySecurityLevel securityLevel) + public void testAuthorized_withPolicyRestriction() { // Test the path broker receives a native json query from external client, should add restriction on data source Policy rowFilterPolicy = RowFilterPolicy.from(new NullFilter("some-column", null)); @@ -453,22 +419,14 @@ public void testAuthorized_withPolicyRestriction(Policy.TablePolicySecurityLevel EasyMock.expect(runner.run(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Sequences.empty()).anyTimes(); replayAll(); - AuthConfig authConfig = AuthConfig.newBuilder() - .setTablePolicySecurityLevel(securityLevel) - .build(); - QueryLifecycle lifecycle = createLifecycle(authConfig); + QueryLifecycle lifecycle = createLifecycle(new AuthConfig()); lifecycle.initialize(query); Assert.assertTrue(lifecycle.authorize(authenticationResult).allowBasicAccess()); lifecycle.execute(); } @Test - @Parameters({ - "APPLY_WHEN_APPLICABLE", - "POLICY_CHECKED_ON_ALL_TABLES_ALLOW_EMPTY", - "POLICY_CHECKED_ON_ALL_TABLES_POLICY_MUST_EXIST" - }) - public void testAuthorized_queryWithRestrictedDataSource_runWithSuperUserPermission(Policy.TablePolicySecurityLevel securityLevel) + public void testAuthorized_queryWithRestrictedDataSource_runWithSuperUserPermission() { // Test the path historical receives a native json query from broker, query already has restriction on data source Policy rowFilterPolicy = RowFilterPolicy.from(new NullFilter("some-column", null)); @@ -500,7 +458,6 @@ public void testAuthorized_queryWithRestrictedDataSource_runWithSuperUserPermiss AuthConfig authConfig = AuthConfig.newBuilder() .setAuthorizeQueryContextParams(true) - .setTablePolicySecurityLevel(securityLevel) .build(); QueryLifecycle lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); From 18c4828310c937082780f5c04aa212e2f84b134c Mon Sep 17 00:00:00 2001 From: cecemei Date: Thu, 9 Jan 2025 10:58:48 -0800 Subject: [PATCH 23/32] Update QueryResource to check for basic access, native json query should work with basic access. --- .../src/main/java/org/apache/druid/server/QueryResource.java | 2 +- .../test/java/org/apache/druid/server/QueryResourceTest.java | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/apache/druid/server/QueryResource.java b/server/src/main/java/org/apache/druid/server/QueryResource.java index 388253a9b2de..cf40c6356b14 100644 --- a/server/src/main/java/org/apache/druid/server/QueryResource.java +++ b/server/src/main/java/org/apache/druid/server/QueryResource.java @@ -214,7 +214,7 @@ public Response doPost( return io.getResponseWriter().buildNonOkResponse(qe.getFailType().getExpectedStatus(), qe); } - if (!authResult.isUserWithNoRestriction()) { + if (!authResult.allowBasicAccess()) { throw new ForbiddenException(authResult.getErrorMessage()); } 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 b5580f90dfec..516cd1b48205 100644 --- a/server/src/test/java/org/apache/druid/server/QueryResourceTest.java +++ b/server/src/test/java/org/apache/druid/server/QueryResourceTest.java @@ -64,6 +64,8 @@ import org.apache.druid.query.Result; import org.apache.druid.query.SegmentDescriptor; import org.apache.druid.query.TruncatedResponseContextException; +import org.apache.druid.query.filter.NullFilter; +import org.apache.druid.query.policy.RowFilterPolicy; import org.apache.druid.query.timeboundary.TimeBoundaryResultValue; import org.apache.druid.server.initialization.ServerConfig; import org.apache.druid.server.log.TestRequestLogger; @@ -822,7 +824,7 @@ public Authorizer getAuthorizer(String name) public Access authorize(AuthenticationResult authenticationResult, Resource resource, Action action) { if (resource.getName().equals("allow")) { - return new Access(true); + return Access.allowWithRestriction(RowFilterPolicy.from(new NullFilter("col",null))); } else { return new Access(false); } From 3ba7d353b320f2e98eb33eea694a8e9e19c9a65f Mon Sep 17 00:00:00 2001 From: cecemei Date: Thu, 9 Jan 2025 11:02:55 -0800 Subject: [PATCH 24/32] rename isUserWithNoRestriction to allowAccessWithNoRestriction --- .../apache/druid/grpc/server/QueryDriver.java | 2 +- .../basic/BasicSecurityResourceFilter.java | 2 +- .../druid/catalog/http/CatalogResource.java | 2 +- .../dart/controller/http/DartSqlResource.java | 4 +- .../dart/controller/sql/DartQueryMaker.java | 2 +- .../druid/msq/rpc/MSQResourceUtils.java | 4 +- .../druid/msq/sql/MSQTaskQueryMaker.java | 2 +- .../sql/resources/SqlStatementResource.java | 2 +- .../indexing/common/task/IndexTaskUtils.java | 2 +- .../overlord/http/OverlordResource.java | 6 +- .../security/SupervisorResourceFilter.java | 2 +- .../http/security/TaskResourceFilter.java | 2 +- .../overlord/sampler/SamplerResource.java | 2 +- .../supervisor/SupervisorResource.java | 2 +- .../druid/segment/realtime/ChatHandlers.java | 2 +- .../apache/druid/server/QueryLifecycle.java | 2 +- .../apache/druid/server/QueryResource.java | 2 +- .../http/security/ConfigResourceFilter.java | 2 +- .../security/DatasourceResourceFilter.java | 2 +- .../http/security/RulesResourceFilter.java | 2 +- .../http/security/StateResourceFilter.java | 2 +- .../server/security/AuthorizationResult.java | 4 +- .../AuthorizationResultTest.java | 12 +- .../druid/server/QueryLifecycleTest.java | 8 +- .../druid/server/QueryResourceTest.java | 129 ++++++++++-------- .../sql/calcite/schema/SystemSchema.java | 2 +- .../apache/druid/sql/http/SqlResource.java | 2 +- 27 files changed, 111 insertions(+), 96 deletions(-) diff --git a/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java b/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java index 12e6c470baaf..ca3aeb4e65ac 100644 --- a/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java +++ b/extensions-contrib/grpc-query/src/main/java/org/apache/druid/grpc/server/QueryDriver.java @@ -148,7 +148,7 @@ private QueryResponse runNativeQuery(QueryRequest request, AuthenticationResult try { queryLifecycle.initialize(query); AuthorizationResult authorizationResult = queryLifecycle.authorize(authResult); - if (!authorizationResult.isUserWithNoRestriction()) { + if (!authorizationResult.allowAccessWithNoRestriction()) { throw new ForbiddenException(Access.DEFAULT_ERROR_MESSAGE); } queryResponse = queryLifecycle.execute(); 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 3a5651e09fcf..eade42320c7d 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 @@ -60,7 +60,7 @@ public ContainerRequest filter(ContainerRequest request) getAuthorizerMapper() ); - if (!authResult.isUserWithNoRestriction()) { + if (!authResult.allowAccessWithNoRestriction()) { throw new WebApplicationException( Response.status(Response.Status.FORBIDDEN) .type(MediaType.TEXT_PLAIN) diff --git a/extensions-core/druid-catalog/src/main/java/org/apache/druid/catalog/http/CatalogResource.java b/extensions-core/druid-catalog/src/main/java/org/apache/druid/catalog/http/CatalogResource.java index 10fc797b0ef9..c206266f5f12 100644 --- a/extensions-core/druid-catalog/src/main/java/org/apache/druid/catalog/http/CatalogResource.java +++ b/extensions-core/druid-catalog/src/main/java/org/apache/druid/catalog/http/CatalogResource.java @@ -581,7 +581,7 @@ private void authorizeTable( private void authorize(String resource, String key, Action action, HttpServletRequest request) { final AuthorizationResult authResult = authorizeAccess(resource, key, action, request); - if (!authResult.isUserWithNoRestriction()) { + if (!authResult.allowAccessWithNoRestriction()) { throw new ForbiddenException(authResult.getErrorMessage()); } } diff --git a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/http/DartSqlResource.java b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/http/DartSqlResource.java index 59b4e8e81b41..a277d7d126ff 100644 --- a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/http/DartSqlResource.java +++ b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/http/DartSqlResource.java @@ -175,7 +175,7 @@ public GetQueriesResponse doGetRunningQueries( queries.sort(Comparator.comparing(DartQueryInfo::getStartTime).thenComparing(DartQueryInfo::getDartQueryId)); final GetQueriesResponse response; - if (stateReadAccess.isUserWithNoRestriction()) { + if (stateReadAccess.allowAccessWithNoRestriction()) { // User can READ STATE, so they can see all running queries, as well as authentication details. response = new GetQueriesResponse(queries); } else { @@ -247,7 +247,7 @@ public Response cancelQuery( final AuthorizationResult authResult = authorizeCancellation(req, cancelables); - if (authResult.isUserWithNoRestriction()) { + if (authResult.allowAccessWithNoRestriction()) { sqlLifecycleManager.removeAll(sqlQueryId, cancelables); // Don't call cancel() on the cancelables. That just cancels native queries, which is useless here. Instead, diff --git a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/sql/DartQueryMaker.java b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/sql/DartQueryMaker.java index 6f63de0f5668..4526754cf573 100644 --- a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/sql/DartQueryMaker.java +++ b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/dart/controller/sql/DartQueryMaker.java @@ -128,7 +128,7 @@ public DartQueryMaker( @Override public QueryResponse runQuery(DruidQuery druidQuery) { - if (!plannerContext.getAuthorizationResult().isUserWithNoRestriction()) { + if (!plannerContext.getAuthorizationResult().allowAccessWithNoRestriction()) { throw new ForbiddenException(plannerContext.getAuthorizationResult().getErrorMessage()); } final MSQSpec querySpec = MSQTaskQueryMaker.makeQuerySpec( diff --git a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/rpc/MSQResourceUtils.java b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/rpc/MSQResourceUtils.java index cf74413d244b..ade376066138 100644 --- a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/rpc/MSQResourceUtils.java +++ b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/rpc/MSQResourceUtils.java @@ -47,7 +47,7 @@ public static void authorizeAdminRequest( authorizerMapper ); - if (!authResult.isUserWithNoRestriction()) { + if (!authResult.allowAccessWithNoRestriction()) { throw new ForbiddenException(authResult.getErrorMessage()); } } @@ -67,7 +67,7 @@ public static void authorizeQueryRequest( authorizerMapper ); - if (!authResult.isUserWithNoRestriction()) { + if (!authResult.allowAccessWithNoRestriction()) { throw new ForbiddenException(authResult.getErrorMessage()); } } diff --git a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/MSQTaskQueryMaker.java b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/MSQTaskQueryMaker.java index 8c201808bc70..9f69396edcfc 100644 --- a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/MSQTaskQueryMaker.java +++ b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/MSQTaskQueryMaker.java @@ -117,7 +117,7 @@ public class MSQTaskQueryMaker implements QueryMaker @Override public QueryResponse runQuery(final DruidQuery druidQuery) { - if (!plannerContext.getAuthorizationResult().isUserWithNoRestriction()) { + if (!plannerContext.getAuthorizationResult().allowAccessWithNoRestriction()) { throw new ForbiddenException(plannerContext.getAuthorizationResult().getErrorMessage()); } Hook.QUERY_PLAN.run(druidQuery.getQuery()); diff --git a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlStatementResource.java b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlStatementResource.java index 918dd8a1da0c..e26969c67619 100644 --- a/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlStatementResource.java +++ b/extensions-core/multi-stage-query/src/main/java/org/apache/druid/msq/sql/resources/SqlStatementResource.java @@ -681,7 +681,7 @@ private MSQControllerTask getMSQControllerTaskAndCheckPermission( authorizerMapper ); - if (!authResult.isUserWithNoRestriction()) { + if (!authResult.allowAccessWithNoRestriction()) { throw new ForbiddenException(StringUtils.format( "The current user[%s] cannot view query id[%s] since the query is owned by another user", currentUser, diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/common/task/IndexTaskUtils.java b/indexing-service/src/main/java/org/apache/druid/indexing/common/task/IndexTaskUtils.java index aa29a22129c2..1d0f2622ce30 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/common/task/IndexTaskUtils.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/common/task/IndexTaskUtils.java @@ -80,7 +80,7 @@ public static AuthorizationResult datasourceAuthorizationCheck( ); AuthorizationResult authResult = AuthorizationUtils.authorizeResourceAction(req, resourceAction, authorizerMapper); - if (!authResult.isUserWithNoRestriction()) { + if (!authResult.allowAccessWithNoRestriction()) { throw new ForbiddenException(authResult.getErrorMessage()); } return authResult; 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 a0e608b4b6ef..74656dfdb5f1 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 @@ -182,7 +182,7 @@ public Response taskPost( resourceActions, authorizerMapper ); - if (!authResult.isUserWithNoRestriction()) { + if (!authResult.allowAccessWithNoRestriction()) { throw new ForbiddenException(authResult.getErrorMessage()); } @@ -614,7 +614,7 @@ public Response getTasks( authorizerMapper ); - if (!authResult.isUserWithNoRestriction()) { + if (!authResult.allowAccessWithNoRestriction()) { throw new WebApplicationException( Response.status(Response.Status.FORBIDDEN) .type(MediaType.TEXT_PLAIN) @@ -663,7 +663,7 @@ public Response killPendingSegments( authorizerMapper ); - if (!authResult.isUserWithNoRestriction()) { + if (!authResult.allowAccessWithNoRestriction()) { throw new ForbiddenException(authResult.getErrorMessage()); } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/SupervisorResourceFilter.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/SupervisorResourceFilter.java index 07e83946689f..0265d20c1dee 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/SupervisorResourceFilter.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/SupervisorResourceFilter.java @@ -103,7 +103,7 @@ public boolean apply(PathSegment input) getAuthorizerMapper() ); - if (!authResult.isUserWithNoRestriction()) { + if (!authResult.allowAccessWithNoRestriction()) { throw new ForbiddenException(authResult.getErrorMessage()); } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/TaskResourceFilter.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/TaskResourceFilter.java index c6f410b6634d..2d23c443125d 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/TaskResourceFilter.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/http/security/TaskResourceFilter.java @@ -98,7 +98,7 @@ public ContainerRequest filter(ContainerRequest request) getAuthorizerMapper() ); - if (!authResult.isUserWithNoRestriction()) { + if (!authResult.allowAccessWithNoRestriction()) { throw new ForbiddenException(authResult.getErrorMessage()); } 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 3591c36f9a40..9c2bf31a18d5 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 @@ -78,7 +78,7 @@ public SamplerResponse post(final SamplerSpec sampler, @Context final HttpServle authorizerMapper ); - if (!authResult.isUserWithNoRestriction()) { + if (!authResult.allowAccessWithNoRestriction()) { throw new ForbiddenException(authResult.getErrorMessage()); } return sampler.sample(); diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java index 934008926272..3190835c3e67 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java @@ -148,7 +148,7 @@ public Response specPost(final SupervisorSpec spec, @Context final HttpServletRe authorizerMapper ); - if (!authResult.isUserWithNoRestriction()) { + if (!authResult.allowAccessWithNoRestriction()) { throw new ForbiddenException(authResult.getErrorMessage()); } diff --git a/server/src/main/java/org/apache/druid/segment/realtime/ChatHandlers.java b/server/src/main/java/org/apache/druid/segment/realtime/ChatHandlers.java index c5ef70f17cce..b37bf7991a81 100644 --- a/server/src/main/java/org/apache/druid/segment/realtime/ChatHandlers.java +++ b/server/src/main/java/org/apache/druid/segment/realtime/ChatHandlers.java @@ -50,7 +50,7 @@ public static AuthorizationResult authorizationCheck( ); AuthorizationResult authResult = AuthorizationUtils.authorizeResourceAction(req, resourceAction, authorizerMapper); - if (!authResult.isUserWithNoRestriction()) { + if (!authResult.allowAccessWithNoRestriction()) { throw new ForbiddenException(authResult.getErrorMessage()); } 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 5dfdfc4357b0..e17d01663fae 100644 --- a/server/src/main/java/org/apache/druid/server/QueryLifecycle.java +++ b/server/src/main/java/org/apache/druid/server/QueryLifecycle.java @@ -321,7 +321,7 @@ private AuthorizationResult doAuthorize( transition(State.AUTHORIZING, State.UNAUTHORIZED); } else { transition(State.AUTHORIZING, State.AUTHORIZED); - if (this.baseQuery instanceof SegmentMetadataQuery && authorizationResult.isUserWithNoRestriction()) { + if (this.baseQuery instanceof SegmentMetadataQuery && authorizationResult.allowAccessWithNoRestriction()) { // skip restrictions mapping for SegmentMetadataQuery from user with no restriction } else { this.baseQuery = this.baseQuery.withDataSource(this.baseQuery.getDataSource() diff --git a/server/src/main/java/org/apache/druid/server/QueryResource.java b/server/src/main/java/org/apache/druid/server/QueryResource.java index cf40c6356b14..93a52abd0643 100644 --- a/server/src/main/java/org/apache/druid/server/QueryResource.java +++ b/server/src/main/java/org/apache/druid/server/QueryResource.java @@ -158,7 +158,7 @@ public Response cancelQuery(@PathParam("id") String queryId, @Context final Http authorizerMapper ); - if (!authResult.isUserWithNoRestriction()) { + if (!authResult.allowAccessWithNoRestriction()) { throw new ForbiddenException(authResult.getErrorMessage()); } 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 dc4aee86b94b..9332c0944779 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 @@ -62,7 +62,7 @@ public ContainerRequest filter(ContainerRequest request) getAuthorizerMapper() ); - if (!authResult.isUserWithNoRestriction()) { + if (!authResult.allowAccessWithNoRestriction()) { throw new ForbiddenException(authResult.getErrorMessage()); } 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 08e9af1cc259..cbf13cb9cc3b 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 @@ -63,7 +63,7 @@ public ContainerRequest filter(ContainerRequest request) getAuthorizerMapper() ); - if (!authResult.isUserWithNoRestriction()) { + if (!authResult.allowAccessWithNoRestriction()) { throw new ForbiddenException(authResult.getErrorMessage()); } 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 937db5a2daf8..74831d174d71 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 @@ -81,7 +81,7 @@ public boolean apply(PathSegment input) getAuthorizerMapper() ); - if (!authResult.isUserWithNoRestriction()) { + if (!authResult.allowAccessWithNoRestriction()) { throw new ForbiddenException(authResult.getErrorMessage()); } 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 f5e812d968d3..a2168850ffd5 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 @@ -66,7 +66,7 @@ public ContainerRequest filter(ContainerRequest request) getAuthorizerMapper() ); - if (!authResult.isUserWithNoRestriction()) { + if (!authResult.allowAccessWithNoRestriction()) { throw new ForbiddenException(authResult.getErrorMessage()); } diff --git a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java index 7402e0fbc435..e32bba354430 100644 --- a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java +++ b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java @@ -132,7 +132,7 @@ public boolean allowBasicAccess() *
    1. no policy found *
    2. the user has a no-restriction policy */ - public boolean isUserWithNoRestriction() + public boolean allowAccessWithNoRestriction() { return PERMISSION.ALLOW_NO_RESTRICTION.equals(permission) || (PERMISSION.ALLOW_WITH_RESTRICTION.equals(permission) && policyRestrictions.values() @@ -151,7 +151,7 @@ public String getErrorMessage() case DENY: return Objects.requireNonNull(failureMessage); case ALLOW_WITH_RESTRICTION: - if (!isUserWithNoRestriction()) { + if (!allowAccessWithNoRestriction()) { return Access.DEFAULT_ERROR_MESSAGE; } default: diff --git a/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java b/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java index 8aa0a3772f35..9a116e7fa8d1 100644 --- a/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java +++ b/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java @@ -77,9 +77,9 @@ public void testNoAccess() { AuthorizationResult result = AuthorizationResult.deny("this data source is not permitted"); assertFalse(result.allowBasicAccess()); - assertFalse(result.isUserWithNoRestriction()); + assertFalse(result.allowAccessWithNoRestriction()); assertEquals("this data source is not permitted", result.getErrorMessage()); - assertFalse(result.isUserWithNoRestriction()); + assertFalse(result.allowAccessWithNoRestriction()); } @Test @@ -87,7 +87,7 @@ public void testFullAccess() { AuthorizationResult result = AuthorizationResult.allowWithRestriction(ImmutableMap.of()); assertTrue(result.allowBasicAccess()); - assertTrue(result.isUserWithNoRestriction()); + assertTrue(result.allowAccessWithNoRestriction()); assertThrows(DruidException.class, result::getErrorMessage); AuthorizationResult resultWithEmptyPolicy = AuthorizationResult.allowWithRestriction(ImmutableMap.of( @@ -95,7 +95,7 @@ public void testFullAccess() Optional.empty() )); assertTrue(resultWithEmptyPolicy.allowBasicAccess()); - assertTrue(resultWithEmptyPolicy.isUserWithNoRestriction()); + assertTrue(resultWithEmptyPolicy.allowAccessWithNoRestriction()); assertThrows(DruidException.class, resultWithEmptyPolicy::getErrorMessage); AuthorizationResult resultWithNoRestrictionPolicy = AuthorizationResult.allowWithRestriction(ImmutableMap.of( @@ -103,7 +103,7 @@ public void testFullAccess() Optional.of(NoRestrictionPolicy.INSTANCE) )); assertTrue(resultWithNoRestrictionPolicy.allowBasicAccess()); - assertTrue(resultWithNoRestrictionPolicy.isUserWithNoRestriction()); + assertTrue(resultWithNoRestrictionPolicy.allowAccessWithNoRestriction()); assertThrows(DruidException.class, resultWithNoRestrictionPolicy::getErrorMessage); } @@ -120,7 +120,7 @@ public void testRestrictedAccess() ))) )); assertTrue(result.allowBasicAccess()); - assertFalse(result.isUserWithNoRestriction()); + assertFalse(result.allowAccessWithNoRestriction()); assertEquals("Unauthorized", result.getErrorMessage()); } } diff --git a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java index 675d7c8deab3..673b6f581346 100644 --- a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java +++ b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java @@ -518,11 +518,11 @@ public void testAuthorizeQueryContext_authorized() revisedContext ); - Assert.assertTrue(lifecycle.authorize(mockRequest()).isUserWithNoRestriction()); + Assert.assertTrue(lifecycle.authorize(mockRequest()).allowAccessWithNoRestriction()); lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertTrue(lifecycle.authorize(authenticationResult).isUserWithNoRestriction()); + Assert.assertTrue(lifecycle.authorize(authenticationResult).allowAccessWithNoRestriction()); } @Test @@ -610,11 +610,11 @@ public void testAuthorizeQueryContext_unsecuredKeys() revisedContext ); - Assert.assertTrue(lifecycle.authorize(mockRequest()).isUserWithNoRestriction()); + Assert.assertTrue(lifecycle.authorize(mockRequest()).allowAccessWithNoRestriction()); lifecycle = createLifecycle(authConfig); lifecycle.initialize(query); - Assert.assertTrue(lifecycle.authorize(authenticationResult).isUserWithNoRestriction()); + Assert.assertTrue(lifecycle.authorize(authenticationResult).allowAccessWithNoRestriction()); } @Test 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 516cd1b48205..6f87127b0968 100644 --- a/server/src/test/java/org/apache/druid/server/QueryResourceTest.java +++ b/server/src/test/java/org/apache/druid/server/QueryResourceTest.java @@ -126,7 +126,8 @@ public class QueryResourceTest { - private static final DefaultQueryRunnerFactoryConglomerate CONGLOMERATE = DefaultQueryRunnerFactoryConglomerate.buildFromQueryRunnerFactories(ImmutableMap.of()); + private static final DefaultQueryRunnerFactoryConglomerate CONGLOMERATE = DefaultQueryRunnerFactoryConglomerate.buildFromQueryRunnerFactories( + ImmutableMap.of()); private static final AuthenticationResult AUTHENTICATION_RESULT = new AuthenticationResult("druid", "druid", null, null); @@ -300,7 +301,9 @@ public void testGoodQueryWithQueryConfigOverrideDefault() throws IOException final List> responses = jsonMapper.readValue( response.baos.toByteArray(), - new TypeReference<>() {} + new TypeReference<>() + { + } ); Assert.assertEquals(0, responses.size()); @@ -395,78 +398,85 @@ public QueryRunner getQueryRunnerForSegments( public void testResponseWithIncludeTrailerHeader() throws IOException { queryResource = new QueryResource( - new QueryLifecycleFactory( - CONGLOMERATE, - new QuerySegmentWalker() - { - @Override - public QueryRunner getQueryRunnerForIntervals( - Query query, - Iterable intervals - ) - { - return (queryPlus, responseContext) -> new Sequence() { + new QueryLifecycleFactory( + CONGLOMERATE, + new QuerySegmentWalker() + { @Override - public OutType accumulate(OutType initValue, Accumulator accumulator) + public QueryRunner getQueryRunnerForIntervals( + Query query, + Iterable intervals + ) { - if (accumulator instanceof QueryResultPusher.StreamingHttpResponseAccumulator) { - try { - ((QueryResultPusher.StreamingHttpResponseAccumulator) accumulator).flush(); // initialized - } - catch (IOException ignore) { + return (queryPlus, responseContext) -> new Sequence() + { + @Override + public OutType accumulate(OutType initValue, Accumulator accumulator) + { + if (accumulator instanceof QueryResultPusher.StreamingHttpResponseAccumulator) { + try { + ((QueryResultPusher.StreamingHttpResponseAccumulator) accumulator).flush(); // initialized + } + catch (IOException ignore) { + } + } + + throw new QueryTimeoutException(); } - } - throw new QueryTimeoutException(); + @Override + public Yielder toYielder( + OutType initValue, + YieldingAccumulator accumulator + ) + { + return Yielders.done(initValue, null); + } + }; } @Override - public Yielder toYielder(OutType initValue, YieldingAccumulator accumulator) + public QueryRunner getQueryRunnerForSegments( + Query query, + Iterable specs + ) { - return Yielders.done(initValue, null); + throw new UnsupportedOperationException(); } - }; - } - - @Override - public QueryRunner getQueryRunnerForSegments( - Query query, - Iterable specs - ) - { - throw new UnsupportedOperationException(); - } - }, - new DefaultGenericQueryMetricsFactory(), - new NoopServiceEmitter(), - testRequestLogger, + }, + new DefaultGenericQueryMetricsFactory(), + new NoopServiceEmitter(), + testRequestLogger, + new AuthConfig(), + AuthTestUtils.TEST_AUTHORIZER_MAPPER, + Suppliers.ofInstance(new DefaultQueryConfig(ImmutableMap.of())) + ), + jsonMapper, + smileMapper, + queryScheduler, new AuthConfig(), - AuthTestUtils.TEST_AUTHORIZER_MAPPER, - Suppliers.ofInstance(new DefaultQueryConfig(ImmutableMap.of())) - ), - jsonMapper, - smileMapper, - queryScheduler, - new AuthConfig(), - null, - ResponseContextConfig.newConfig(true), - DRUID_NODE + null, + ResponseContextConfig.newConfig(true), + DRUID_NODE ); expectPermissiveHappyPathAuth(); org.eclipse.jetty.server.Response response = this.jettyResponseforRequest(testServletRequest); Assert.assertNull(queryResource.doPost(new ByteArrayInputStream( - SIMPLE_TIMESERIES_QUERY.getBytes(StandardCharsets.UTF_8)), - null /*pretty*/, - testServletRequest)); + SIMPLE_TIMESERIES_QUERY.getBytes(StandardCharsets.UTF_8)), + null /*pretty*/, + testServletRequest + )); Assert.assertTrue(response.containsHeader(HttpHeader.TRAILER.toString())); Assert.assertEquals(response.getHeader(HttpHeader.TRAILER.toString()), QueryResultPusher.RESULT_TRAILER_HEADERS); final HttpFields fields = response.getTrailers().get(); Assert.assertTrue(fields.containsKey(QueryResource.ERROR_MESSAGE_TRAILER_HEADER)); - Assert.assertEquals(fields.get(QueryResource.ERROR_MESSAGE_TRAILER_HEADER), - "Query did not complete within configured timeout period. You can increase query timeout or tune the performance of query."); + Assert.assertEquals( + fields.get(QueryResource.ERROR_MESSAGE_TRAILER_HEADER), + "Query did not complete within configured timeout period. You can increase query timeout or tune the performance of query." + ); Assert.assertTrue(fields.containsKey(QueryResource.RESPONSE_COMPLETE_TRAILER_HEADER)); Assert.assertEquals(fields.get(QueryResource.RESPONSE_COMPLETE_TRAILER_HEADER), "false"); @@ -501,7 +511,8 @@ public void testSuccessResponseWithTrailerHeader() throws IOException Assert.assertNull(queryResource.doPost(new ByteArrayInputStream( SIMPLE_TIMESERIES_QUERY.getBytes(StandardCharsets.UTF_8)), null /*pretty*/, - testServletRequest)); + testServletRequest + )); Assert.assertTrue(response.containsHeader(HttpHeader.TRAILER.toString())); final HttpFields fields = response.getTrailers().get(); @@ -625,7 +636,9 @@ public void testGoodQueryWithQueryConfigDoesNotOverrideQueryContext() throws IOE final List> responses = jsonMapper.readValue( response.baos.toByteArray(), - new TypeReference<>() {} + new TypeReference<>() + { + } ); Assert.assertNotNull(response); @@ -824,7 +837,7 @@ public Authorizer getAuthorizer(String name) public Access authorize(AuthenticationResult authenticationResult, Resource resource, Action action) { if (resource.getName().equals("allow")) { - return Access.allowWithRestriction(RowFilterPolicy.from(new NullFilter("col",null))); + return Access.allowWithRestriction(RowFilterPolicy.from(new NullFilter("col", null))); } else { return new Access(false); } @@ -874,7 +887,9 @@ public Access authorize(AuthenticationResult authenticationResult, Resource reso final List> responses = jsonMapper.readValue( response.baos.toByteArray(), - new TypeReference<>() {} + new TypeReference<>() + { + } ); Assert.assertEquals(0, responses.size()); 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 37d43073d6e4..26a9bbee575c 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 @@ -1153,7 +1153,7 @@ private static void checkStateReadAccessForServers( authorizerMapper ); - if (!authResult.isUserWithNoRestriction()) { + if (!authResult.allowAccessWithNoRestriction()) { throw new ForbiddenException("Insufficient permission to view servers: " + authResult.getErrorMessage()); } } diff --git a/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java b/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java index a5436ffc7b57..b53b49f0fcaa 100644 --- a/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java +++ b/sql/src/main/java/org/apache/druid/sql/http/SqlResource.java @@ -142,7 +142,7 @@ public Response cancelQuery( final AuthorizationResult authResult = authorizeCancellation(req, lifecycles); - if (authResult.isUserWithNoRestriction()) { + if (authResult.allowAccessWithNoRestriction()) { // should remove only the lifecycles in the snapshot. sqlLifecycleManager.removeAll(sqlQueryId, lifecycles); lifecycles.forEach(Cancelable::cancel); From 43c031260323f6aa17af528b06ee5a5594a7c20a Mon Sep 17 00:00:00 2001 From: cecemei Date: Thu, 9 Jan 2025 16:12:32 -0800 Subject: [PATCH 25/32] In DataSource interface, rename mapWithRestriction to withPolicies. Add more tests for policy package, also updated the classes a bit. Added tests for restricted datasource in SqlResourceTest --- .../org/apache/druid/query/DataSource.java | 4 +- .../druid/query/RestrictedDataSource.java | 3 +- .../apache/druid/query/TableDataSource.java | 2 +- .../query/policy/NoRestrictionPolicy.java | 7 +- .../org/apache/druid/query/policy/Policy.java | 2 + .../druid/query/policy/RowFilterPolicy.java | 16 +++-- .../segment/BypassRestrictedSegment.java | 24 +++++-- .../segment/RestrictedCursorFactory.java | 13 ++++ .../druid/segment/RestrictedSegment.java | 12 ++-- .../apache/druid/query/DataSourceTest.java | 22 +++--- .../druid/query/JoinDataSourceTest.java | 2 +- .../druid/query/RestrictedDataSourceTest.java | 4 +- .../query/policy/NoRestrictionPolicyTest.java | 28 +++++++- .../query/policy/RowFilterPolicyTest.java | 31 +++++++++ .../SegmentMetadataQuerySegmentWalker.java | 2 +- .../apache/druid/server/QueryLifecycle.java | 2 +- .../apache/druid/server/security/Access.java | 2 +- .../server/security/AuthorizationResult.java | 23 +++---- .../AuthorizationResultTest.java | 4 +- .../druid/server/QueryLifecycleTest.java | 4 +- .../security/ForbiddenExceptionTest.java | 2 +- .../druid/sql/calcite/util/CalciteTests.java | 2 +- .../druid/sql/http/SqlResourceTest.java | 67 ++++++++++++++----- 23 files changed, 203 insertions(+), 75 deletions(-) diff --git a/processing/src/main/java/org/apache/druid/query/DataSource.java b/processing/src/main/java/org/apache/druid/query/DataSource.java index 339632838593..0222ecd37077 100644 --- a/processing/src/main/java/org/apache/druid/query/DataSource.java +++ b/processing/src/main/java/org/apache/druid/query/DataSource.java @@ -134,11 +134,11 @@ public interface DataSource * the policyMap (used by druid-internal). Missing policy or adding a * non-NoRestrictionPolicy to RestrictedDataSource would throw. */ - default DataSource mapWithRestriction(Map> policyMap) + default DataSource withPolicies(Map> policyMap) { List children = this.getChildren() .stream() - .map(child -> child.mapWithRestriction(policyMap)) + .map(child -> child.withPolicies(policyMap)) .collect(Collectors.toList()); return this.withChildren(children); } diff --git a/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java b/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java index 91637c181f9b..7f51b7cf7bea 100644 --- a/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java +++ b/processing/src/main/java/org/apache/druid/query/RestrictedDataSource.java @@ -48,6 +48,7 @@ public class RestrictedDataSource implements DataSource { private final TableDataSource base; + // update List policy? private final Policy policy; @JsonProperty("base") @@ -145,7 +146,7 @@ public DataSource withUpdatedDataSource(DataSource newSource) } @Override - public DataSource mapWithRestriction(Map> policyMap) + public DataSource withPolicies(Map> policyMap) { if (!policyMap.containsKey(base.getName())) { throw new ISE("Missing policy check result for table [%s]", base.getName()); diff --git a/processing/src/main/java/org/apache/druid/query/TableDataSource.java b/processing/src/main/java/org/apache/druid/query/TableDataSource.java index b2e0504d4ca0..d735a75928d0 100644 --- a/processing/src/main/java/org/apache/druid/query/TableDataSource.java +++ b/processing/src/main/java/org/apache/druid/query/TableDataSource.java @@ -116,7 +116,7 @@ public DataSource withUpdatedDataSource(DataSource newSource) } @Override - public DataSource mapWithRestriction(Map> policyMap) + public DataSource withPolicies(Map> policyMap) { Optional policy = policyMap.getOrDefault(name, Optional.empty()); if (!policy.isPresent()) { diff --git a/processing/src/main/java/org/apache/druid/query/policy/NoRestrictionPolicy.java b/processing/src/main/java/org/apache/druid/query/policy/NoRestrictionPolicy.java index 28b5ae949b88..5753941a15bf 100644 --- a/processing/src/main/java/org/apache/druid/query/policy/NoRestrictionPolicy.java +++ b/processing/src/main/java/org/apache/druid/query/policy/NoRestrictionPolicy.java @@ -28,11 +28,14 @@ */ public class NoRestrictionPolicy implements Policy { - public static final NoRestrictionPolicy INSTANCE = new NoRestrictionPolicy(); + NoRestrictionPolicy() + { + } @JsonCreator - NoRestrictionPolicy() + public static NoRestrictionPolicy instance() { + return new NoRestrictionPolicy(); } @Override diff --git a/processing/src/main/java/org/apache/druid/query/policy/Policy.java b/processing/src/main/java/org/apache/druid/query/policy/Policy.java index 28d61ac0c1c5..11a1aff4fbb8 100644 --- a/processing/src/main/java/org/apache/druid/query/policy/Policy.java +++ b/processing/src/main/java/org/apache/druid/query/policy/Policy.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.apache.druid.guice.annotations.UnstableApi; import org.apache.druid.segment.CursorBuildSpec; /** @@ -32,6 +33,7 @@ @JsonSubTypes.Type(value = RowFilterPolicy.class, name = "row"), @JsonSubTypes.Type(value = NoRestrictionPolicy.class, name = "noRestriction") }) +@UnstableApi public interface Policy { /** diff --git a/processing/src/main/java/org/apache/druid/query/policy/RowFilterPolicy.java b/processing/src/main/java/org/apache/druid/query/policy/RowFilterPolicy.java index eb67e1457901..c4f930528813 100644 --- a/processing/src/main/java/org/apache/druid/query/policy/RowFilterPolicy.java +++ b/processing/src/main/java/org/apache/druid/query/policy/RowFilterPolicy.java @@ -21,13 +21,14 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.google.common.collect.ImmutableList; +import com.google.common.base.Preconditions; import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.Filter; import org.apache.druid.segment.CursorBuildSpec; -import org.apache.druid.segment.filter.AndFilter; +import org.apache.druid.segment.filter.Filters; import javax.annotation.Nonnull; +import java.util.Arrays; import java.util.Objects; /** @@ -35,12 +36,12 @@ */ public class RowFilterPolicy implements Policy { - @JsonProperty("rowFilter") private final DimFilter rowFilter; @JsonCreator RowFilterPolicy(@Nonnull @JsonProperty("rowFilter") DimFilter rowFilter) { + Preconditions.checkNotNull(rowFilter); this.rowFilter = rowFilter; } @@ -49,6 +50,12 @@ public static RowFilterPolicy from(@Nonnull DimFilter rowFilter) return new RowFilterPolicy(rowFilter); } + @JsonProperty + public DimFilter getRowFilter() + { + return rowFilter; + } + @Override public CursorBuildSpec visit(CursorBuildSpec spec) { @@ -56,8 +63,7 @@ public CursorBuildSpec visit(CursorBuildSpec spec) final Filter filter = spec.getFilter(); final Filter policyFilter = this.rowFilter.toFilter(); - final Filter newFilter = filter == null ? policyFilter : new AndFilter(ImmutableList.of(policyFilter, filter)); - builder.setFilter(newFilter); + builder.setFilter(Filters.and(Arrays.asList(policyFilter, filter))); return builder.build(); } diff --git a/processing/src/main/java/org/apache/druid/segment/BypassRestrictedSegment.java b/processing/src/main/java/org/apache/druid/segment/BypassRestrictedSegment.java index 6b74179b0539..f55a4bf7d6f0 100644 --- a/processing/src/main/java/org/apache/druid/segment/BypassRestrictedSegment.java +++ b/processing/src/main/java/org/apache/druid/segment/BypassRestrictedSegment.java @@ -19,20 +19,30 @@ package org.apache.druid.segment; -import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.policy.Policy; /** - * A wrapped {@link SegmentReference} with a {@link DimFilter} restriction, and the policy restriction can be bypassed. + * A {@link SegmentReference} wrapper with a {@link Policy} restriction that is not automatically enforced. + * Instead, it relies on the caller to apply or enforce the policy. + * + *

      + * Certain methods, such as {@link #as(Class)}, {@link #asQueryableIndex()}, and {@link #asCursorFactory()}, + * provide access to the underlying segment without automatically applying the policy, leaving it up to + * the caller to ensure compliance when needed. *

      - * In some methods, such as {@link #as(Class)}, {@link #asQueryableIndex()}, and {@link #asCursorFactory()}, the policy - * is ignored. + * This design provides flexibility for scenarios where policy enforcement is not required or desired. */ -class BypassRestrictedSegment extends RestrictedSegment +class BypassRestrictedSegment extends WrappedSegmentReference { - public BypassRestrictedSegment(SegmentReference delegate, Policy policy) + protected final Policy policy; + + public BypassRestrictedSegment( + SegmentReference delegate, + Policy policy + ) { - super(delegate, policy); + super(delegate); + this.policy = policy; } public Policy getPolicy() diff --git a/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java b/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java index 130fe59059d2..6d3aaf8cf603 100644 --- a/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java +++ b/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java @@ -25,6 +25,19 @@ import javax.annotation.Nullable; +/** + * A factory class for creating {@code Cursor} instances with strict adherence to {@link Policy} restrictions. + *

      + * The {@code CursorFactory} simplifies the process of initializing and retrieving {@code Cursor} objects while ensuring + * that any cursor created complies with the {@link Policy} restrictions. + *

      + * Policy Enforcement in {@link #makeCursorHolder}: + *

        + *
      • Row-level restrictions are enforced by adding filters to {@link CursorBuildSpec}, which is then passed to + * delegate for execution. This ensures that only relevant data are accessible by the client. + *
      + * + */ public class RestrictedCursorFactory implements CursorFactory { private final CursorFactory delegate; diff --git a/processing/src/main/java/org/apache/druid/segment/RestrictedSegment.java b/processing/src/main/java/org/apache/druid/segment/RestrictedSegment.java index e663d48c61e2..c4a7cb4828e8 100644 --- a/processing/src/main/java/org/apache/druid/segment/RestrictedSegment.java +++ b/processing/src/main/java/org/apache/druid/segment/RestrictedSegment.java @@ -31,12 +31,14 @@ import java.util.Optional; /** - * A wrapped {@link SegmentReference} with a {@link Policy} restriction. The policy must be applied when querying the - * wrapped segment, e.x {@link #asCursorFactory()} returns a policy-enforced {@link RestrictedCursorFactory}. The policy - * and wrapped SegmentReference (i.e. delegate) can't be accessed directly. + * A {@link SegmentReference} wrapper with a {@link Policy} restriction that is automatically enforced. + * The policy seamlessly governs queries on the wrapped segment, ensuring compliance. For example, + * {@link #asCursorFactory()} returns a policy-enforced {@link RestrictedCursorFactory}. + * *

      - * There's a backdoor to get the wrapped SegmentReference through {@code as(BypassRestrictedSegment.class)}, returning - * a policy-aware (but not enforced) {@link BypassRestrictedSegment} instance. + * Direct access to the policy or the underlying SegmentReference (the delegate) is not allowed. + * However, a backdoor is available via {@code as(BypassRestrictedSegment.class)}, allowing access to + * a {@link BypassRestrictedSegment} instance, which provides flexibility on policy enforcement. */ public class RestrictedSegment implements SegmentReference { diff --git a/processing/src/test/java/org/apache/druid/query/DataSourceTest.java b/processing/src/test/java/org/apache/druid/query/DataSourceTest.java index 5f38fdbbeddc..3d3de5a19f2a 100644 --- a/processing/src/test/java/org/apache/druid/query/DataSourceTest.java +++ b/processing/src/test/java/org/apache/druid/query/DataSourceTest.java @@ -85,7 +85,7 @@ public void testRestrictedDataSource() throws IOException ); Assert.assertEquals( - RestrictedDataSource.create(TableDataSource.create("somedatasource"), NoRestrictionPolicy.INSTANCE), + RestrictedDataSource.create(TableDataSource.create("somedatasource"), NoRestrictionPolicy.instance()), dataSource ); } @@ -138,9 +138,9 @@ public void testMapWithRestriction() UnionDataSource unionDataSource = new UnionDataSource(Lists.newArrayList(table1, table2, table3)); ImmutableMap> restrictions = ImmutableMap.of( "table1", - Optional.of(NoRestrictionPolicy.INSTANCE), + Optional.of(NoRestrictionPolicy.instance()), "table2", - Optional.of(NoRestrictionPolicy.INSTANCE), + Optional.of(NoRestrictionPolicy.instance()), "table3", Optional.of(RowFilterPolicy.from(new NullFilter( "some-column", @@ -149,15 +149,15 @@ public void testMapWithRestriction() ); Assert.assertEquals( - unionDataSource.mapWithRestriction(restrictions), + unionDataSource.withPolicies(restrictions), new UnionDataSource(Lists.newArrayList( RestrictedDataSource.create( table1, - NoRestrictionPolicy.INSTANCE + NoRestrictionPolicy.instance() ), RestrictedDataSource.create( table2, - NoRestrictionPolicy.INSTANCE + NoRestrictionPolicy.instance() ), RestrictedDataSource.create( table3, @@ -180,10 +180,10 @@ public void testMapWithRestriction_onRestrictedDataSource_fromDruidSystem() // The druid-system should get a NO_RESTRICTION policy attached on a table. ImmutableMap> noRestrictionPolicy = ImmutableMap.of( "table1", - Optional.of(NoRestrictionPolicy.INSTANCE) + Optional.of(NoRestrictionPolicy.instance()) ); - Assert.assertEquals(restrictedDataSource, restrictedDataSource.mapWithRestriction(noRestrictionPolicy)); + Assert.assertEquals(restrictedDataSource, restrictedDataSource.withPolicies(noRestrictionPolicy)); } @Test @@ -200,18 +200,18 @@ public void testMapWithRestriction_onRestrictedDataSource_alwaysThrows() ImmutableMap> noPolicyFound = ImmutableMap.of("table1", Optional.empty()); ImmutableMap> policyWasNotChecked = ImmutableMap.of(); - ISE e = Assert.assertThrows(ISE.class, () -> restrictedDataSource.mapWithRestriction(anotherRestrictions)); + ISE e = Assert.assertThrows(ISE.class, () -> restrictedDataSource.withPolicies(anotherRestrictions)); Assert.assertEquals( "Multiple restrictions on table [table1]: policy [RowFilterPolicy{rowFilter=random-column IS NULL}] and policy [RowFilterPolicy{rowFilter=some-column IS NULL}]", e.getMessage() ); - ISE e2 = Assert.assertThrows(ISE.class, () -> restrictedDataSource.mapWithRestriction(noPolicyFound)); + ISE e2 = Assert.assertThrows(ISE.class, () -> restrictedDataSource.withPolicies(noPolicyFound)); Assert.assertEquals( "No restriction found on table [table1], but had policy [RowFilterPolicy{rowFilter=random-column IS NULL}] before.", e2.getMessage() ); - ISE e3 = Assert.assertThrows(ISE.class, () -> restrictedDataSource.mapWithRestriction(policyWasNotChecked)); + ISE e3 = Assert.assertThrows(ISE.class, () -> restrictedDataSource.withPolicies(policyWasNotChecked)); Assert.assertEquals("Missing policy check result for table [table1]", e3.getMessage()); } } diff --git a/processing/src/test/java/org/apache/druid/query/JoinDataSourceTest.java b/processing/src/test/java/org/apache/druid/query/JoinDataSourceTest.java index c4fe921a5d0c..81bc18cf2e22 100644 --- a/processing/src/test/java/org/apache/druid/query/JoinDataSourceTest.java +++ b/processing/src/test/java/org/apache/druid/query/JoinDataSourceTest.java @@ -509,7 +509,7 @@ public void testGetAnalysisWithRestrictedDS() JoinDataSource dataSource = JoinDataSource.create( RestrictedDataSource.create( new TableDataSource("table1"), - NoRestrictionPolicy.INSTANCE + NoRestrictionPolicy.instance() ), new TableDataSource("table2"), "j.", diff --git a/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java b/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java index 0ed77c99be77..2a51bc60053b 100644 --- a/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java +++ b/processing/src/test/java/org/apache/druid/query/RestrictedDataSourceTest.java @@ -42,7 +42,7 @@ public class RestrictedDataSourceTest ); private final RestrictedDataSource restrictedBarDataSource = RestrictedDataSource.create( barDataSource, - NoRestrictionPolicy.INSTANCE + NoRestrictionPolicy.instance() ); @Test @@ -148,7 +148,7 @@ public void test_deserialize_fromObject() throws Exception Assert.assertEquals( deserializedRestrictedDataSource, - RestrictedDataSource.create(fooDataSource, NoRestrictionPolicy.INSTANCE) + RestrictedDataSource.create(fooDataSource, NoRestrictionPolicy.instance()) ); } diff --git a/processing/src/test/java/org/apache/druid/query/policy/NoRestrictionPolicyTest.java b/processing/src/test/java/org/apache/druid/query/policy/NoRestrictionPolicyTest.java index a9cc1ab549f5..0f20e11b416c 100644 --- a/processing/src/test/java/org/apache/druid/query/policy/NoRestrictionPolicyTest.java +++ b/processing/src/test/java/org/apache/druid/query/policy/NoRestrictionPolicyTest.java @@ -19,16 +19,42 @@ package org.apache.druid.query.policy; +import com.fasterxml.jackson.databind.ObjectMapper; +import nl.jqno.equalsverifier.EqualsVerifier; import org.apache.druid.segment.CursorBuildSpec; +import org.apache.druid.segment.TestHelper; import org.junit.Assert; import org.junit.Test; public class NoRestrictionPolicyTest { + @Test + public void test_equals() + { + EqualsVerifier.forClass(NoRestrictionPolicy.class).usingGetClass().verify(); + } + + @Test + public void test_deserialize_fromString() throws Exception + { + ObjectMapper jsonMapper = TestHelper.makeJsonMapper(); + Policy deserialized = jsonMapper.readValue("{\"type\":\"noRestriction\"}", Policy.class); + Assert.assertEquals(NoRestrictionPolicy.instance(), deserialized); + } + + @Test + public void test_serde_roundTrip() throws Exception + { + final NoRestrictionPolicy policy = NoRestrictionPolicy.instance(); + ObjectMapper jsonMapper = TestHelper.makeJsonMapper(); + Policy deserialized = jsonMapper.readValue(jsonMapper.writeValueAsString(policy), Policy.class); + Assert.assertEquals(policy, deserialized); + } + @Test public void testVisit() { - final NoRestrictionPolicy policy = NoRestrictionPolicy.INSTANCE; + final NoRestrictionPolicy policy = NoRestrictionPolicy.instance(); Assert.assertEquals(CursorBuildSpec.FULL_SCAN, policy.visit(CursorBuildSpec.FULL_SCAN)); } } diff --git a/processing/src/test/java/org/apache/druid/query/policy/RowFilterPolicyTest.java b/processing/src/test/java/org/apache/druid/query/policy/RowFilterPolicyTest.java index b698b4394cd1..e2f15499cdfe 100644 --- a/processing/src/test/java/org/apache/druid/query/policy/RowFilterPolicyTest.java +++ b/processing/src/test/java/org/apache/druid/query/policy/RowFilterPolicyTest.java @@ -19,12 +19,15 @@ package org.apache.druid.query.policy; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableList; +import nl.jqno.equalsverifier.EqualsVerifier; import org.apache.druid.common.config.NullHandling; import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.EqualityFilter; import org.apache.druid.query.filter.Filter; import org.apache.druid.segment.CursorBuildSpec; +import org.apache.druid.segment.TestHelper; import org.apache.druid.segment.column.ColumnType; import org.apache.druid.segment.filter.AndFilter; import org.junit.Assert; @@ -33,10 +36,38 @@ public class RowFilterPolicyTest { + private RowFilterPolicy simpleRowPolicy; + @Before public void setup() { NullHandling.initializeForTests(); + simpleRowPolicy = RowFilterPolicy.from(new EqualityFilter("col0", ColumnType.STRING, "val0", null)); + } + + @Test + public void test_equals() + { + EqualsVerifier.forClass(RowFilterPolicy.class).usingGetClass().withNonnullFields(new String[]{"rowFilter"}).verify(); + } + + @Test + public void test_deserialize_fromString() throws Exception + { + ObjectMapper jsonMapper = TestHelper.makeJsonMapper(); + Policy deserialized = jsonMapper.readValue( + "{\"type\":\"row\",\"rowFilter\":{\"type\":\"equals\",\"column\":\"col0\",\"matchValueType\":\"STRING\",\"matchValue\":\"val0\"}}\n", + Policy.class + ); + Assert.assertEquals(simpleRowPolicy, deserialized); + } + + @Test + public void test_serde_roundTrip() throws Exception + { + ObjectMapper jsonMapper = TestHelper.makeJsonMapper(); + Policy deserialized = jsonMapper.readValue(jsonMapper.writeValueAsString(simpleRowPolicy), Policy.class); + Assert.assertEquals(simpleRowPolicy, deserialized); } @Test diff --git a/server/src/main/java/org/apache/druid/segment/metadata/SegmentMetadataQuerySegmentWalker.java b/server/src/main/java/org/apache/druid/segment/metadata/SegmentMetadataQuerySegmentWalker.java index eba280aa7184..bc66fbf56d69 100644 --- a/server/src/main/java/org/apache/druid/segment/metadata/SegmentMetadataQuerySegmentWalker.java +++ b/server/src/main/java/org/apache/druid/segment/metadata/SegmentMetadataQuerySegmentWalker.java @@ -96,7 +96,7 @@ public QueryRunner getQueryRunnerForIntervals(Query query, Iterable QueryRunner getQueryRunnerForSegments(Query query, Iterable specs) { - return decorateRunner(query, new QueryRunner<>() + return decorateRunner(query, new QueryRunner() { @Override public Sequence run(final QueryPlus queryPlus, final ResponseContext responseContext) 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 e17d01663fae..6ffc8aff0bba 100644 --- a/server/src/main/java/org/apache/druid/server/QueryLifecycle.java +++ b/server/src/main/java/org/apache/druid/server/QueryLifecycle.java @@ -325,7 +325,7 @@ private AuthorizationResult doAuthorize( // skip restrictions mapping for SegmentMetadataQuery from user with no restriction } else { this.baseQuery = this.baseQuery.withDataSource(this.baseQuery.getDataSource() - .mapWithRestriction(authorizationResult.getPolicy())); + .withPolicies(authorizationResult.getPolicyMap())); } } diff --git a/server/src/main/java/org/apache/druid/server/security/Access.java b/server/src/main/java/org/apache/druid/server/security/Access.java index e7c582c91b77..456222c1dac5 100644 --- a/server/src/main/java/org/apache/druid/server/security/Access.java +++ b/server/src/main/java/org/apache/druid/server/security/Access.java @@ -42,7 +42,7 @@ public class Access private final String message; // A policy restriction on top of table-level read access. It should be empty if there are no policy restrictions // or if access is requested for an action other than reading the table. - private final Optional policy; + private final Optional policy; // should this be a list? /** * @deprecated use {@link #allow()} or {@link #deny(String)} instead diff --git a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java index e32bba354430..4feaabe733ab 100644 --- a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java +++ b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java @@ -49,7 +49,7 @@ public class AuthorizationResult * superusers, except in cases where granular ACL considerations are not a priority. */ public static final AuthorizationResult ALLOW_NO_RESTRICTION = new AuthorizationResult( - PERMISSION.ALLOW_NO_RESTRICTION, + Permission.ALLOW_NO_RESTRICTION, null, Collections.emptyMap() ); @@ -58,19 +58,19 @@ public class AuthorizationResult * Provides a default deny access result. */ public static final AuthorizationResult DENY = new AuthorizationResult( - PERMISSION.DENY, + Permission.DENY, Access.DENIED.getMessage(), Collections.emptyMap() ); - enum PERMISSION + enum Permission { ALLOW_NO_RESTRICTION, ALLOW_WITH_RESTRICTION, DENY } - private final PERMISSION permission; + private final Permission permission; @Nullable private final String failureMessage; @@ -78,7 +78,7 @@ enum PERMISSION private final Map> policyRestrictions; AuthorizationResult( - PERMISSION permission, + Permission permission, @Nullable String failureMessage, Map> policyRestrictions ) @@ -108,7 +108,7 @@ enum PERMISSION public static AuthorizationResult deny(@Nonnull String failureMessage) { - return new AuthorizationResult(PERMISSION.DENY, failureMessage, Collections.emptyMap()); + return new AuthorizationResult(Permission.DENY, failureMessage, Collections.emptyMap()); } public static AuthorizationResult allowWithRestriction(Map> policyRestrictions) @@ -116,7 +116,7 @@ public static AuthorizationResult allowWithRestriction(Map p.orElse(null)) - .filter(Objects::nonNull) // Can be replaced by Optional::stream after java 11 + .flatMap(Optional::stream) .allMatch(p -> (p instanceof NoRestrictionPolicy))); } @@ -159,7 +158,7 @@ public String getErrorMessage() } } - public Map> getPolicy() + public Map> getPolicyMap() { return policyRestrictions; } diff --git a/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java b/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java index 9a116e7fa8d1..620bdc91cfc8 100644 --- a/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java +++ b/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java @@ -60,7 +60,7 @@ public void testToString() AuthorizationResult result = AuthorizationResult.allowWithRestriction( ImmutableMap.of( "table1", - Optional.of(NoRestrictionPolicy.INSTANCE), + Optional.of(NoRestrictionPolicy.instance()), "table2", Optional.of( RowFilterPolicy.from(new EqualityFilter("column1", ColumnType.STRING, "val1", null))) @@ -100,7 +100,7 @@ public void testFullAccess() AuthorizationResult resultWithNoRestrictionPolicy = AuthorizationResult.allowWithRestriction(ImmutableMap.of( "table1", - Optional.of(NoRestrictionPolicy.INSTANCE) + Optional.of(NoRestrictionPolicy.instance()) )); assertTrue(resultWithNoRestrictionPolicy.allowBasicAccess()); assertTrue(resultWithNoRestrictionPolicy.allowAccessWithNoRestriction()); diff --git a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java index 673b6f581346..d63a162d56de 100644 --- a/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java +++ b/server/src/test/java/org/apache/druid/server/QueryLifecycleTest.java @@ -237,7 +237,7 @@ public void testRunSimple_withPolicyRestriction_segmentMetadataQueryRunAsInterna // The druid-internal gets a NoRestrictionPolicy. AuthorizationResult authorizationResult = AuthorizationResult.allowWithRestriction(ImmutableMap.of( DATASOURCE, - Optional.of(NoRestrictionPolicy.INSTANCE) + Optional.of(NoRestrictionPolicy.instance()) )); final SegmentMetadataQuery query = Druids.newSegmentMetadataQueryBuilder() .dataSource(DATASOURCE) @@ -431,7 +431,7 @@ public void testAuthorized_queryWithRestrictedDataSource_runWithSuperUserPermiss // Test the path historical receives a native json query from broker, query already has restriction on data source Policy rowFilterPolicy = RowFilterPolicy.from(new NullFilter("some-column", null)); // Internal druid system would get a NO_RESTRICTION on a restricted data source. - Access access = Access.allowWithRestriction(NoRestrictionPolicy.INSTANCE); + Access access = Access.allowWithRestriction(NoRestrictionPolicy.instance()); DataSource restrictedDataSource = RestrictedDataSource.create(TableDataSource.create(DATASOURCE), rowFilterPolicy); diff --git a/server/src/test/java/org/apache/druid/server/security/ForbiddenExceptionTest.java b/server/src/test/java/org/apache/druid/server/security/ForbiddenExceptionTest.java index 76b24e8a5243..4bc395634058 100644 --- a/server/src/test/java/org/apache/druid/server/security/ForbiddenExceptionTest.java +++ b/server/src/test/java/org/apache/druid/server/security/ForbiddenExceptionTest.java @@ -83,7 +83,7 @@ public void testAccess() Assert.assertEquals("Allowed:true, Message:, Policy: Optional.empty", access.toString()); Assert.assertEquals("Authorized", access.getMessage()); - access = Access.allowWithRestriction(NoRestrictionPolicy.INSTANCE); + access = Access.allowWithRestriction(NoRestrictionPolicy.instance()); Assert.assertTrue(access.isAllowed()); Assert.assertEquals("Allowed:true, Message:, Policy: Optional[NO_RESTRICTION]", access.toString()); Assert.assertEquals("Authorized, with restriction [NO_RESTRICTION]", access.getMessage()); 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 ab40dc0abb4e..719eb04ac496 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 @@ -138,7 +138,7 @@ public class CalciteTests public static final String BENCHMARK_DATASOURCE = "benchmark_ds"; public static final String TEST_SUPERUSER_NAME = "testSuperuser"; - public static final Policy POLICY_NO_RESTRICTION_SUPERUSER = NoRestrictionPolicy.INSTANCE; + public static final Policy POLICY_NO_RESTRICTION_SUPERUSER = NoRestrictionPolicy.instance(); public static final Policy POLICY_RESTRICTION = RowFilterPolicy.from(BaseCalciteQueryTest.numericSelector("m1", "6")); public static final AuthorizerMapper TEST_AUTHORIZER_MAPPER = new AuthorizerMapper(null) { diff --git a/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java b/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java index c6f1ac7d82ff..6e88543d3b79 100644 --- a/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java +++ b/sql/src/test/java/org/apache/druid/sql/http/SqlResourceTest.java @@ -119,7 +119,6 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.StreamingOutput; - import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; @@ -352,20 +351,39 @@ public void tearDown() throws Exception @Test public void testUnauthorized() { - try { - postForAsyncResponse( - createSimpleQueryWithId("id", "select count(*) from forbiddenDatasource"), - request() - ); - Assert.fail("doPost did not throw ForbiddenException for an unauthorized query"); - } - catch (ForbiddenException e) { - // expected - } + ForbiddenException e = Assert.assertThrows(ForbiddenException.class, () -> { + postForAsyncResponse(createSimpleQueryWithId("id", "select count(*) from forbiddenDatasource"), request()); + }); + Assert.assertEquals("Unauthorized", e.getMessage()); Assert.assertEquals(1, testRequestLogger.getSqlQueryLogs().size()); Assert.assertTrue(lifecycleManager.getAll("id").isEmpty()); } + @Test + public void testRestricted() throws Exception + { + req = makeSuperUserReq(); + final List> resultAsSuperUser = doPost(createSimpleQueryWithId( + "id", + "select count(*) as cnt from restrictedDatasource_m1_is_6" + )).rhs; + Assert.assertEquals(ImmutableList.of(ImmutableMap.of("cnt", 6)), resultAsSuperUser); + + checkSqlRequestLog(true, CalciteTests.TEST_SUPERUSER_NAME); + testRequestLogger.clear(); + Assert.assertTrue(lifecycleManager.getAll("id").isEmpty()); + + req = makeRegularUserReq(); + final List> resultAsRegularUser = doPost(createSimpleQueryWithId( + "id", + "select count(*) as cnt from restrictedDatasource_m1_is_6" + )).rhs; + Assert.assertEquals(ImmutableList.of(ImmutableMap.of("cnt", 1)), resultAsRegularUser); + checkSqlRequestLog(true); + testRequestLogger.clear(); + Assert.assertTrue(lifecycleManager.getAll("id").isEmpty()); + } + @Test public void testCountStar() throws Exception { @@ -745,7 +763,17 @@ public void testArrayResultFormatWithHeader() throws Exception List[] expectedQueryResults = new List[]{ Arrays.asList("2000-01-01T00:00:00.000Z", "", "a", "[\"a\",\"b\"]", 1, 1.0, 1.0, "\"AQAAAEAAAA==\"", nullStr), - Arrays.asList("2000-01-02T00:00:00.000Z", "10.1", nullStr, "[\"b\",\"c\"]", 1, 2.0, 2.0, "\"AQAAAQAAAAHNBA==\"", nullStr) + Arrays.asList( + "2000-01-02T00:00:00.000Z", + "10.1", + nullStr, + "[\"b\",\"c\"]", + 1, + 2.0, + 2.0, + "\"AQAAAQAAAAHNBA==\"", + nullStr + ) }; MockHttpServletResponse response = postForAsyncResponse( @@ -1576,7 +1604,7 @@ public ErrorResponseTransformStrategy getErrorResponseTransformStrategy() /** * See class-level javadoc for {@link org.apache.druid.sql.calcite.util.testoperator.AssertionErrorOperatorConversion} * for rationale as to why this test exists. - * + *

      * If this test starts failing, it could be indicative of us not handling the AssertionErrors well anymore, * OR it could be indicative of this specific code path not throwing an AssertionError anymore. If we run * into the latter case, we should seek out a new code path that generates the error from Calcite. In the best @@ -1899,15 +1927,22 @@ public void testQueryContextKeyNotAllowed() throws Exception checkSqlRequestLog(false); } - @SuppressWarnings("unchecked") private void checkSqlRequestLog(boolean success) + { + checkSqlRequestLog(success, CalciteTests.REGULAR_USER_AUTH_RESULT.getIdentity()); + } + + @SuppressWarnings("unchecked") + private void checkSqlRequestLog(boolean success, String user) { Assert.assertEquals(1, testRequestLogger.getSqlQueryLogs().size()); final Map stats = testRequestLogger.getSqlQueryLogs().get(0).getQueryStats().getStats(); - final Map queryContext = (Map) testRequestLogger.getSqlQueryLogs().get(0).getSqlQueryContext(); + final Map queryContext = (Map) testRequestLogger.getSqlQueryLogs() + .get(0) + .getSqlQueryContext(); Assert.assertEquals(success, stats.get("success")); - Assert.assertEquals(CalciteTests.REGULAR_USER_AUTH_RESULT.getIdentity(), stats.get("identity")); + Assert.assertEquals(user, stats.get("identity")); Assert.assertTrue(stats.containsKey("sqlQuery/time")); Assert.assertTrue(stats.containsKey("sqlQuery/planningTimeMs")); Assert.assertTrue(queryContext.containsKey(QueryContexts.CTX_SQL_QUERY_ID)); From cbce5734bf97e0f4c00a760e2f6442959a0db0e8 Mon Sep 17 00:00:00 2001 From: cecemei Date: Fri, 10 Jan 2025 10:06:28 -0800 Subject: [PATCH 26/32] remove NullHandling.initializeForTests --- .../apache/druid/query/DataSourceTest.java | 8 ------ .../query/policy/RowFilterPolicyTest.java | 27 +++++++++---------- .../AuthorizationResultTest.java | 8 ------ 3 files changed, 13 insertions(+), 30 deletions(-) diff --git a/processing/src/test/java/org/apache/druid/query/DataSourceTest.java b/processing/src/test/java/org/apache/druid/query/DataSourceTest.java index 3d3de5a19f2a..cd77f14246fd 100644 --- a/processing/src/test/java/org/apache/druid/query/DataSourceTest.java +++ b/processing/src/test/java/org/apache/druid/query/DataSourceTest.java @@ -23,7 +23,6 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; -import org.apache.druid.common.config.NullHandling; import org.apache.druid.java.util.common.ISE; import org.apache.druid.query.aggregation.LongSumAggregatorFactory; import org.apache.druid.query.dimension.DefaultDimensionSpec; @@ -34,7 +33,6 @@ import org.apache.druid.query.policy.RowFilterPolicy; import org.apache.druid.segment.TestHelper; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; import java.io.IOException; @@ -44,12 +42,6 @@ public class DataSourceTest { private static final ObjectMapper JSON_MAPPER = TestHelper.makeJsonMapper(); - @Before - public void setUp() - { - NullHandling.initializeForTests(); // Needed for loading QueryRunnerTestHelper static variables. - } - @Test public void testSerialization() throws IOException { diff --git a/processing/src/test/java/org/apache/druid/query/policy/RowFilterPolicyTest.java b/processing/src/test/java/org/apache/druid/query/policy/RowFilterPolicyTest.java index e2f15499cdfe..193843dbfcad 100644 --- a/processing/src/test/java/org/apache/druid/query/policy/RowFilterPolicyTest.java +++ b/processing/src/test/java/org/apache/druid/query/policy/RowFilterPolicyTest.java @@ -22,7 +22,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableList; import nl.jqno.equalsverifier.EqualsVerifier; -import org.apache.druid.common.config.NullHandling; import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.EqualityFilter; import org.apache.druid.query.filter.Filter; @@ -31,24 +30,24 @@ import org.apache.druid.segment.column.ColumnType; import org.apache.druid.segment.filter.AndFilter; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; public class RowFilterPolicyTest { - private RowFilterPolicy simpleRowPolicy; - - @Before - public void setup() - { - NullHandling.initializeForTests(); - simpleRowPolicy = RowFilterPolicy.from(new EqualityFilter("col0", ColumnType.STRING, "val0", null)); - } + private static final RowFilterPolicy SIMPLE_ROW_POLICY = RowFilterPolicy.from(new EqualityFilter( + "col0", + ColumnType.STRING, + "val0", + null + )); @Test public void test_equals() { - EqualsVerifier.forClass(RowFilterPolicy.class).usingGetClass().withNonnullFields(new String[]{"rowFilter"}).verify(); + EqualsVerifier.forClass(RowFilterPolicy.class) + .usingGetClass() + .withNonnullFields(new String[]{"rowFilter"}) + .verify(); } @Test @@ -59,15 +58,15 @@ public void test_deserialize_fromString() throws Exception "{\"type\":\"row\",\"rowFilter\":{\"type\":\"equals\",\"column\":\"col0\",\"matchValueType\":\"STRING\",\"matchValue\":\"val0\"}}\n", Policy.class ); - Assert.assertEquals(simpleRowPolicy, deserialized); + Assert.assertEquals(SIMPLE_ROW_POLICY, deserialized); } @Test public void test_serde_roundTrip() throws Exception { ObjectMapper jsonMapper = TestHelper.makeJsonMapper(); - Policy deserialized = jsonMapper.readValue(jsonMapper.writeValueAsString(simpleRowPolicy), Policy.class); - Assert.assertEquals(simpleRowPolicy, deserialized); + Policy deserialized = jsonMapper.readValue(jsonMapper.writeValueAsString(SIMPLE_ROW_POLICY), Policy.class); + Assert.assertEquals(SIMPLE_ROW_POLICY, deserialized); } @Test diff --git a/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java b/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java index 620bdc91cfc8..c6d62ce5713a 100644 --- a/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java +++ b/server/src/test/java/org/apache/druid/initialization/AuthorizationResultTest.java @@ -21,14 +21,12 @@ import com.google.common.collect.ImmutableMap; import nl.jqno.equalsverifier.EqualsVerifier; -import org.apache.druid.common.config.NullHandling; import org.apache.druid.error.DruidException; import org.apache.druid.query.filter.EqualityFilter; import org.apache.druid.query.policy.NoRestrictionPolicy; import org.apache.druid.query.policy.RowFilterPolicy; import org.apache.druid.segment.column.ColumnType; import org.apache.druid.server.security.AuthorizationResult; -import org.junit.Before; import org.junit.Test; import java.util.Optional; @@ -40,12 +38,6 @@ public class AuthorizationResultTest { - @Before - public void setUp() - { - NullHandling.initializeForTests(); - } - @Test public void testEquals() { From 04439678ddc694c32293333dcc235fe5d15af832 Mon Sep 17 00:00:00 2001 From: cecemei Date: Fri, 10 Jan 2025 10:26:25 -0800 Subject: [PATCH 27/32] fix calcite test --- .../java/org/apache/druid/sql/calcite/util/CalciteTests.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 719eb04ac496..25396de322a8 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 @@ -57,6 +57,7 @@ import org.apache.druid.query.policy.RowFilterPolicy; import org.apache.druid.rpc.indexing.NoopOverlordClient; import org.apache.druid.rpc.indexing.OverlordClient; +import org.apache.druid.segment.column.ColumnType; import org.apache.druid.segment.join.JoinableFactory; import org.apache.druid.segment.join.JoinableFactoryWrapper; import org.apache.druid.server.DruidNode; @@ -139,7 +140,7 @@ public class CalciteTests public static final String TEST_SUPERUSER_NAME = "testSuperuser"; public static final Policy POLICY_NO_RESTRICTION_SUPERUSER = NoRestrictionPolicy.instance(); - public static final Policy POLICY_RESTRICTION = RowFilterPolicy.from(BaseCalciteQueryTest.numericSelector("m1", "6")); + public static final Policy POLICY_RESTRICTION = RowFilterPolicy.from(BaseCalciteQueryTest.equality("m1", 6, ColumnType.LONG)); public static final AuthorizerMapper TEST_AUTHORIZER_MAPPER = new AuthorizerMapper(null) { @Override From 530fcba0549b6d6ad477200042a5513840760fdf Mon Sep 17 00:00:00 2001 From: cecemei Date: Fri, 10 Jan 2025 11:44:40 -0800 Subject: [PATCH 28/32] javadoc updates --- .../segment/RestrictedCursorFactory.java | 5 ++-- .../org/apache/druid/segment/Segment.java | 28 +++++++++---------- .../server/security/AuthorizationResult.java | 4 +++ 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java b/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java index 6d3aaf8cf603..de1d99e573c8 100644 --- a/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java +++ b/processing/src/main/java/org/apache/druid/segment/RestrictedCursorFactory.java @@ -26,12 +26,13 @@ import javax.annotation.Nullable; /** - * A factory class for creating {@code Cursor} instances with strict adherence to {@link Policy} restrictions. + * A factory class for creating {@code Cursor} instances with strict adherence to {@link Policy} restrictions. Created + * by {@link RestrictedSegment#asCursorFactory()}, and applies policies transparently. *

      * The {@code CursorFactory} simplifies the process of initializing and retrieving {@code Cursor} objects while ensuring * that any cursor created complies with the {@link Policy} restrictions. *

      - * Policy Enforcement in {@link #makeCursorHolder}: + * Policy enforcement in {@link #makeCursorHolder}: *

        *
      • Row-level restrictions are enforced by adding filters to {@link CursorBuildSpec}, which is then passed to * delegate for execution. This ensures that only relevant data are accessible by the client. diff --git a/processing/src/main/java/org/apache/druid/segment/Segment.java b/processing/src/main/java/org/apache/druid/segment/Segment.java index 135f4d556421..14cd1a4da443 100644 --- a/processing/src/main/java/org/apache/druid/segment/Segment.java +++ b/processing/src/main/java/org/apache/druid/segment/Segment.java @@ -57,31 +57,31 @@ default StorageAdapter asStorageAdapter() /** * Request an implementation of a particular interface. - * + *

        * If the passed-in interface is {@link QueryableIndex} or {@link CursorFactory}, then this method behaves * identically to {@link #asQueryableIndex()} or {@link #asCursorFactory()}. Other interfaces are only * expected to be requested by callers that have specific knowledge of extra features provided by specific * segment types. For example, an extension might provide a custom Segment type that can offer both * StorageAdapter and some new interface. That extension can also offer a Query that uses that new interface. - * + *

        * Implementations which accept classes other than {@link QueryableIndex} or {@link CursorFactory} are limited * to using those classes within the extension. This means that one extension cannot rely on the `Segment.as` * behavior of another extension. * * @param clazz desired interface * @param desired interface - * - * @return instance of clazz, or null if the interface is not supported by this segment - * - * @see CursorFactory to make cursors to run queries. Never null. - * @see QueryableIndex index object, if this is a memory-mapped regular segment. - * @see IndexedTable table object, if this is a joinable indexed table. - * @see TimeBoundaryInspector inspector for min/max timestamps, if supported by this segment. - * @see PhysicalSegmentInspector inspector for physical segment details, if supported by this segment. - * @see MaxIngestedEventTimeInspector inspector for {@link DataSourceMetadataResultValue#getMaxIngestedEventTime()} - * @see TopNOptimizationInspector inspector containing information for topN specific optimizations - * @see CloseableShapeshifter stepping stone to {@link org.apache.druid.query.rowsandcols.RowsAndColumns}. - * + * @return instance of clazz, or null if the interface is not supported by this segment, one of the following: + *

          + *
        • {@link CursorFactory}, to make cursors to run queries. Never null.
        • + *
        • {@link QueryableIndex}, index object, if this is a memory-mapped regular segment. + *
        • {@link IndexedTable}, table object, if this is a joinable indexed table. + *
        • {@link TimeBoundaryInspector}, inspector for min/max timestamps, if supported by this segment. + *
        • {@link PhysicalSegmentInspector}, inspector for physical segment details, if supported by this segment. + *
        • {@link MaxIngestedEventTimeInspector}, inspector for {@link DataSourceMetadataResultValue#getMaxIngestedEventTime()} + *
        • {@link TopNOptimizationInspector}, inspector containing information for topN specific optimizations + *
        • {@link CloseableShapeshifter}, stepping stone to {@link org.apache.druid.query.rowsandcols.RowsAndColumns}. + *
        • {@link BypassRestrictedSegment}, a policy-aware segment, converted from a policy-enforced segment. + *
        */ @SuppressWarnings({"unused", "unchecked"}) @Nullable diff --git a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java index 4feaabe733ab..d0e166b4e92a 100644 --- a/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java +++ b/server/src/main/java/org/apache/druid/server/security/AuthorizationResult.java @@ -158,6 +158,10 @@ public String getErrorMessage() } } + /** + * Returns a map of table and {@link Policy} restriction on the table. Empty value means the table doesn't have any + * restriction. + */ public Map> getPolicyMap() { return policyRestrictions; From 9b7652c4c5ad93f91d533fed7506f86317bd43cf Mon Sep 17 00:00:00 2001 From: cecemei Date: Fri, 10 Jan 2025 12:32:35 -0800 Subject: [PATCH 29/32] javadoc updates --- pom.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pom.xml b/pom.xml index c67df8127a03..b2cda14c3612 100644 --- a/pom.xml +++ b/pom.xml @@ -1647,12 +1647,6 @@ check - - org.codehaus.mojo.signature - - java18 - 1.0 - From 57574d6e05641ac7e1e216075e20bdf421eb8123 Mon Sep 17 00:00:00 2001 From: cecemei Date: Fri, 10 Jan 2025 12:39:04 -0800 Subject: [PATCH 30/32] update animal-sniffer-maven-plugin --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index b2cda14c3612..36618adacdd8 100644 --- a/pom.xml +++ b/pom.xml @@ -1647,6 +1647,7 @@ check + From b19f657bafe36288d2142ecf5e3d2b161b4106b7 Mon Sep 17 00:00:00 2001 From: cecemei Date: Fri, 10 Jan 2025 12:44:26 -0800 Subject: [PATCH 31/32] javadoc update --- .../druid/segment/BypassRestrictedSegment.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/processing/src/main/java/org/apache/druid/segment/BypassRestrictedSegment.java b/processing/src/main/java/org/apache/druid/segment/BypassRestrictedSegment.java index f55a4bf7d6f0..5ca7e6314513 100644 --- a/processing/src/main/java/org/apache/druid/segment/BypassRestrictedSegment.java +++ b/processing/src/main/java/org/apache/druid/segment/BypassRestrictedSegment.java @@ -22,15 +22,13 @@ import org.apache.druid.query.policy.Policy; /** - * A {@link SegmentReference} wrapper with a {@link Policy} restriction that is not automatically enforced. - * Instead, it relies on the caller to apply or enforce the policy. - * - *

        - * Certain methods, such as {@link #as(Class)}, {@link #asQueryableIndex()}, and {@link #asCursorFactory()}, - * provide access to the underlying segment without automatically applying the policy, leaving it up to - * the caller to ensure compliance when needed. + * A {@link SegmentReference} wrapper with a {@link Policy} restriction that is not applied. Instead, it relies on the + * caller to apply the policy. *

        - * This design provides flexibility for scenarios where policy enforcement is not required or desired. + * This class is useful when a query engine needs direct access to interfaces that cannot have policies applied + * transparently. For example, {@link RestrictedSegment} returns null for {@link #asQueryableIndex} because it cannot + * apply policies transparently to a {@link QueryableIndex}. To use one, a query engine needs to obtain a + * {@link BypassRestrictedSegment} and apply the policies itself. */ class BypassRestrictedSegment extends WrappedSegmentReference { From a1359a61fff75dbfabc1888f27a8e3d276d3f2ff Mon Sep 17 00:00:00 2001 From: cecemei Date: Fri, 10 Jan 2025 12:49:33 -0800 Subject: [PATCH 32/32] revert change in SegmentMetadataQuery style change --- .../query/metadata/metadata/SegmentMetadataQuery.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/processing/src/main/java/org/apache/druid/query/metadata/metadata/SegmentMetadataQuery.java b/processing/src/main/java/org/apache/druid/query/metadata/metadata/SegmentMetadataQuery.java index 09881b87e123..e0e5f81f45f3 100644 --- a/processing/src/main/java/org/apache/druid/query/metadata/metadata/SegmentMetadataQuery.java +++ b/processing/src/main/java/org/apache/druid/query/metadata/metadata/SegmentMetadataQuery.java @@ -74,7 +74,7 @@ public static AnalysisType fromString(String name) @Override public byte[] getCacheKey() { - return new byte[]{(byte) this.ordinal()}; + return new byte[] {(byte) this.ordinal()}; } } @@ -116,12 +116,9 @@ public SegmentMetadataQuery( // of truth for consumers of this class variable. The defaults are to preserve backwards compatibility. // In a future release, 28.0+, we can remove the deprecated property lenientAggregatorMerge. if (lenientAggregatorMerge != null && aggregatorMergeStrategy != null) { - throw InvalidInput.exception( - "Both lenientAggregatorMerge [%s] and aggregatorMergeStrategy [%s] parameters cannot be set." - + " Consider using aggregatorMergeStrategy since lenientAggregatorMerge is deprecated.", - lenientAggregatorMerge, - aggregatorMergeStrategy - ); + throw InvalidInput.exception("Both lenientAggregatorMerge [%s] and aggregatorMergeStrategy [%s] parameters cannot be set." + + " Consider using aggregatorMergeStrategy since lenientAggregatorMerge is deprecated.", + lenientAggregatorMerge, aggregatorMergeStrategy); } if (lenientAggregatorMerge != null) { this.aggregatorMergeStrategy = lenientAggregatorMerge