Skip to content

Commit

Permalink
add row-level security filter
Browse files Browse the repository at this point in the history
  • Loading branch information
cecemei committed Dec 13, 2024
1 parent 24e5d8a commit 85bb1f7
Show file tree
Hide file tree
Showing 54 changed files with 1,808 additions and 500 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -54,7 +54,7 @@ public ContainerRequest filter(ContainerRequest request)
getAction(request)
);

final Access authResult = AuthorizationUtils.authorizeResourceAction(
final AuthorizationResult authResult = AuthorizationUtils.authorizeResourceAction(
getReq(),
resourceAction,
getAuthorizerMapper()
Expand All @@ -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()
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -108,17 +108,17 @@ public CatalogResource(
* </ul>
*
* @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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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.
*
* <p>
* 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.
*/
Expand Down Expand Up @@ -467,9 +467,9 @@ private Response listAllTableMetadata(final HttpServletRequest req)
List<Pair<SchemaSpec, TableMetadata>> 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<Pair<SchemaSpec, TableMetadata>> filtered = AuthorizationUtils.filterAuthorizedResources(
Expand All @@ -483,9 +483,9 @@ private Response listAllTableMetadata(final HttpServletRequest req)
);

List<TableMetadata> 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();
}

Expand All @@ -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();
}
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@

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;
import org.apache.druid.server.security.ResourceAction;

import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Objects;

/**
* Utility methods for MSQ resources such as {@link ControllerResource}.
Expand All @@ -41,10 +42,14 @@ public static void authorizeAdminRequest(
{
final List<ResourceAction> 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()));
}
}

Expand All @@ -57,10 +62,14 @@ public static void authorizeQueryRequest(
{
final List<ResourceAction> 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()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -484,7 +484,13 @@ private Response buildTaskResponse(Sequence<Object[]> sequence, AuthenticationRe
}
String taskId = String.valueOf(firstRow[0]);

Optional<SqlStatementResult> statementResult = getStatementStatus(taskId, authenticationResult, true, Action.READ, false);
Optional<SqlStatementResult> statementResult = getStatementStatus(
taskId,
authenticationResult,
true,
Action.READ,
false
);

if (statementResult.isPresent()) {
return Response.status(Response.Status.OK).entity(statementResult.get()).build();
Expand Down Expand Up @@ -585,7 +591,11 @@ private Optional<SqlStatementResult> 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;
Expand Down Expand Up @@ -640,9 +650,9 @@ private Optional<SqlStatementResult> 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,
Expand All @@ -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
Expand Down Expand Up @@ -990,7 +1000,11 @@ private <T> T contactOverlord(final ListenableFuture<T> 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
);
}

}
Loading

0 comments on commit 85bb1f7

Please sign in to comment.