This is an automated email from the ASF dual-hosted git repository. apucher pushed a commit to branch manual-authorization-annotation in repository https://gitbox.apache.org/repos/asf/pinot.git
commit ee391e918b188e6e7e6dade5f11157b4c04cf1d4 Author: Alexander Pucher <[email protected]> AuthorDate: Fri Aug 19 13:21:45 2022 -0700 add @ManualAuthorization annotation for non-standard endpoints --- .../api/access/AuthenticationFilter.java | 5 +++ .../controller/api/access/ManualAuthorization.java | 37 ++++++++++++++++++++++ .../api/resources/PinotQueryResource.java | 2 ++ .../api/resources/PinotSchemaRestletResource.java | 15 +++++++-- .../api/resources/PinotTableRestletResource.java | 23 ++++++++++++-- .../api/resources/TableConfigsRestletResource.java | 28 +++++++++++++++- 6 files changed, 105 insertions(+), 5 deletions(-) diff --git a/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AuthenticationFilter.java b/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AuthenticationFilter.java index 8ebd1a2883..2ff482acc9 100644 --- a/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AuthenticationFilter.java +++ b/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AuthenticationFilter.java @@ -82,6 +82,11 @@ public class AuthenticationFilter implements ContainerRequestFilter { return; } + // check if the method's authorization is disabled (i.e. performed manually within method) + if (endpointMethod.isAnnotationPresent(ManualAuthorization.class)) { + return; + } + // Note that table name is extracted from "path parameters" or "query parameters" if it's defined as one of the // followings: // - "tableName", diff --git a/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/ManualAuthorization.java b/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/ManualAuthorization.java new file mode 100644 index 0000000000..b0624418c8 --- /dev/null +++ b/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/ManualAuthorization.java @@ -0,0 +1,37 @@ +/** + * 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.pinot.controller.api.access; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/** + * Annotation to be used on top of REST endpoints. Methods annotated with this annotation don't perform default + * authorization via AuthenticationFilter. This is useful when performing authorization manually via calls to + * {@code AuthenticationFiler.validatePermissions()} + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface ManualAuthorization { + +} diff --git a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotQueryResource.java b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotQueryResource.java index a382122c33..ebee631174 100644 --- a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotQueryResource.java +++ b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotQueryResource.java @@ -54,6 +54,7 @@ import org.apache.pinot.common.utils.request.RequestUtils; import org.apache.pinot.controller.ControllerConf; import org.apache.pinot.controller.api.access.AccessControl; import org.apache.pinot.controller.api.access.AccessControlFactory; +import org.apache.pinot.controller.api.access.ManualAuthorization; import org.apache.pinot.controller.helix.core.PinotHelixResourceManager; import org.apache.pinot.core.query.executor.sql.SqlQueryExecutor; import org.apache.pinot.spi.utils.CommonConstants; @@ -87,6 +88,7 @@ public class PinotQueryResource { @POST @Path("sql") + @ManualAuthorization // performed by broker public String handlePostSql(String requestJsonStr, @Context HttpHeaders httpHeaders) { try { JsonNode requestJson = JsonUtils.stringToJsonNode(requestJsonStr); diff --git a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotSchemaRestletResource.java b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotSchemaRestletResource.java index 2bb05d0553..c5a39a38c2 100644 --- a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotSchemaRestletResource.java +++ b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotSchemaRestletResource.java @@ -61,6 +61,7 @@ import org.apache.pinot.controller.api.access.AccessControlFactory; import org.apache.pinot.controller.api.access.AccessControlUtils; import org.apache.pinot.controller.api.access.AccessType; import org.apache.pinot.controller.api.access.Authenticate; +import org.apache.pinot.controller.api.access.ManualAuthorization; import org.apache.pinot.controller.api.events.MetadataEventNotifierFactory; import org.apache.pinot.controller.api.events.SchemaEventType; import org.apache.pinot.controller.api.exception.ControllerApplicationException; @@ -209,6 +210,7 @@ public class PinotSchemaRestletResource { @ApiResponse(code = 400, message = "Missing or invalid request body"), @ApiResponse(code = 500, message = "Internal error") }) + @ManualAuthorization // performed after parsing schema public ConfigSuccessResponse addSchema( @ApiParam(value = "Whether to override the schema if the schema exists") @DefaultValue("true") @QueryParam("override") boolean override, FormDataMultiPart multiPart, @Context HttpHeaders httpHeaders, @@ -235,6 +237,7 @@ public class PinotSchemaRestletResource { @ApiResponse(code = 400, message = "Missing or invalid request body"), @ApiResponse(code = 500, message = "Internal error") }) + @ManualAuthorization // performed after parsing schema public ConfigSuccessResponse addSchema( @ApiParam(value = "Whether to override the schema if the schema exists") @DefaultValue("true") @QueryParam("override") boolean override, String schemaJsonString, @Context HttpHeaders httpHeaders, @@ -266,11 +269,15 @@ public class PinotSchemaRestletResource { @ApiResponse(code = 400, message = "Missing or invalid request body"), @ApiResponse(code = 500, message = "Internal error") }) - public String validateSchema(FormDataMultiPart multiPart) { + @ManualAuthorization // performed after parsing schema + public String validateSchema(FormDataMultiPart multiPart, @Context HttpHeaders httpHeaders, @Context Request request) { Pair<Schema, Map<String, Object>> schemaAndUnrecognizedProps = getSchemaAndUnrecognizedPropertiesFromMultiPart(multiPart); Schema schema = schemaAndUnrecognizedProps.getLeft(); + String endpointUrl = request.getRequestURL().toString(); validateSchemaInternal(schema); + AccessControlUtils.validatePermission(schema.getSchemaName(), AccessType.CREATE, httpHeaders, endpointUrl, + _accessControlFactory.create()); ObjectNode response = schema.toJsonObject(); response.set("unrecognizedProperties", JsonUtils.objectToJsonNode(schemaAndUnrecognizedProps.getRight())); try { @@ -291,7 +298,8 @@ public class PinotSchemaRestletResource { @ApiResponse(code = 400, message = "Missing or invalid request body"), @ApiResponse(code = 500, message = "Internal error") }) - public String validateSchema(String schemaJsonString) { + @ManualAuthorization // performed after parsing schema + public String validateSchema(String schemaJsonString, @Context HttpHeaders httpHeaders, @Context Request request) { Pair<Schema, Map<String, Object>> schemaAndUnrecognizedProps = null; try { schemaAndUnrecognizedProps = JsonUtils.stringToObjectAndUnrecognizedProperties(schemaJsonString, Schema.class); @@ -300,7 +308,10 @@ public class PinotSchemaRestletResource { throw new ControllerApplicationException(LOGGER, msg, Response.Status.BAD_REQUEST, e); } Schema schema = schemaAndUnrecognizedProps.getLeft(); + String endpointUrl = request.getRequestURL().toString(); validateSchemaInternal(schema); + AccessControlUtils.validatePermission(schema.getSchemaName(), AccessType.CREATE, httpHeaders, endpointUrl, + _accessControlFactory.create()); ObjectNode response = schema.toJsonObject(); response.set("unrecognizedProperties", JsonUtils.objectToJsonNode(schemaAndUnrecognizedProps.getRight())); try { diff --git a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotTableRestletResource.java b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotTableRestletResource.java index 5d5030a4e6..2965e80ca2 100644 --- a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotTableRestletResource.java +++ b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotTableRestletResource.java @@ -80,6 +80,7 @@ import org.apache.pinot.controller.api.access.AccessControlFactory; import org.apache.pinot.controller.api.access.AccessControlUtils; import org.apache.pinot.controller.api.access.AccessType; import org.apache.pinot.controller.api.access.Authenticate; +import org.apache.pinot.controller.api.access.ManualAuthorization; import org.apache.pinot.controller.api.exception.ControllerApplicationException; import org.apache.pinot.controller.api.exception.InvalidTableConfigException; import org.apache.pinot.controller.api.exception.TableAlreadyExistsException; @@ -168,6 +169,7 @@ public class PinotTableRestletResource { @Produces(MediaType.APPLICATION_JSON) @Path("/tables") @ApiOperation(value = "Adds a table", notes = "Adds a table") + @ManualAuthorization // performed after parsing table configs public ConfigSuccessResponse addTable( String tableConfigStr, @ApiParam(value = "comma separated list of validation type(s) to skip. supported types: (ALL|TASK|UPSERT)") @@ -534,7 +536,8 @@ public class PinotTableRestletResource { public ObjectNode checkTableConfig( String tableConfigStr, @ApiParam(value = "comma separated list of validation type(s) to skip. supported types: (ALL|TASK|UPSERT)") - @QueryParam("validationTypesToSkip") @Nullable String typesToSkip) { + @QueryParam("validationTypesToSkip") @Nullable String typesToSkip, @Context HttpHeaders httpHeaders, + @Context Request request) { Pair<TableConfig, Map<String, Object>> tableConfig; try { tableConfig = JsonUtils.stringToObjectAndUnrecognizedProperties(tableConfigStr, TableConfig.class); @@ -542,6 +545,13 @@ public class PinotTableRestletResource { String msg = String.format("Invalid table config json string: %s", tableConfigStr); throw new ControllerApplicationException(LOGGER, msg, Response.Status.BAD_REQUEST, e); } + + // validate permission + String tableName = tableConfig.getLeft().getTableName(); + String endpointUrl = request.getRequestURL().toString(); + AccessControlUtils.validatePermission(tableName, AccessType.CREATE, httpHeaders, endpointUrl, + _accessControlFactory.create()); + ObjectNode validationResponse = validateConfig(tableConfig.getLeft(), _pinotHelixResourceManager.getSchemaForTableConfig(tableConfig.getLeft()), typesToSkip); @@ -562,12 +572,21 @@ public class PinotTableRestletResource { public String validateTableAndSchema( TableAndSchemaConfig tableSchemaConfig, @ApiParam(value = "comma separated list of validation type(s) to skip. supported types: (ALL|TASK|UPSERT)") - @QueryParam("validationTypesToSkip") @Nullable String typesToSkip) { + @QueryParam("validationTypesToSkip") @Nullable String typesToSkip, @Context HttpHeaders httpHeaders, + @Context Request request) { TableConfig tableConfig = tableSchemaConfig.getTableConfig(); Schema schema = tableSchemaConfig.getSchema(); + if (schema == null) { schema = _pinotHelixResourceManager.getSchemaForTableConfig(tableConfig); } + + // validate permission + String schemaName = schema != null ? schema.getSchemaName() : null; + String endpointUrl = request.getRequestURL().toString(); + AccessControlUtils.validatePermission(schemaName, AccessType.CREATE, httpHeaders, endpointUrl, + _accessControlFactory.create()); + return validateConfig(tableSchemaConfig.getTableConfig(), schema, typesToSkip).toString(); } diff --git a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/TableConfigsRestletResource.java b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/TableConfigsRestletResource.java index d238756783..d309fd2dba 100644 --- a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/TableConfigsRestletResource.java +++ b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/TableConfigsRestletResource.java @@ -56,6 +56,7 @@ import org.apache.pinot.controller.api.access.AccessControlFactory; import org.apache.pinot.controller.api.access.AccessControlUtils; import org.apache.pinot.controller.api.access.AccessType; import org.apache.pinot.controller.api.access.Authenticate; +import org.apache.pinot.controller.api.access.ManualAuthorization; import org.apache.pinot.controller.api.exception.ControllerApplicationException; import org.apache.pinot.controller.api.exception.InvalidTableConfigException; import org.apache.pinot.controller.api.exception.TableAlreadyExistsException; @@ -160,6 +161,7 @@ public class TableConfigsRestletResource { @Path("/tableConfigs") @ApiOperation(value = "Add the TableConfigs using the tableConfigsStr json", notes = "Add the TableConfigs using the tableConfigsStr json") + @ManualAuthorization // performed after parsing table configs public ConfigSuccessResponse addConfig( String tableConfigsStr, @ApiParam(value = "comma separated list of validation type(s) to skip. supported types: (ALL|TASK|UPSERT)") @@ -378,7 +380,8 @@ public class TableConfigsRestletResource { @ApiOperation(value = "Validate the TableConfigs", notes = "Validate the TableConfigs") public String validateConfig(String tableConfigsStr, @ApiParam(value = "comma separated list of validation type(s) to skip. supported types: (ALL|TASK|UPSERT)") - @QueryParam("validationTypesToSkip") @Nullable String typesToSkip) { + @QueryParam("validationTypesToSkip") @Nullable String typesToSkip, @Context HttpHeaders httpHeaders, + @Context Request request) { Pair<TableConfigs, Map<String, Object>> tableConfigsAndUnrecognizedProps; TableConfigs tableConfigs; try { @@ -389,6 +392,29 @@ public class TableConfigsRestletResource { throw new ControllerApplicationException(LOGGER, String.format("Invalid TableConfigs json string: %s", tableConfigsStr), Response.Status.BAD_REQUEST, e); } + + String endpointUrl = request.getRequestURL().toString(); + AccessControl accessControl = _accessControlFactory.create(); + Schema schema = tableConfigs.getSchema(); + TableConfig offlineTableConfig = tableConfigs.getOffline(); + TableConfig realtimeTableConfig = tableConfigs.getRealtime(); + + AccessControlUtils + .validatePermission(schema.getSchemaName(), AccessType.CREATE, httpHeaders, endpointUrl, accessControl); + + if (offlineTableConfig != null) { + tuneConfig(offlineTableConfig, schema); + AccessControlUtils + .validatePermission(offlineTableConfig.getTableName(), AccessType.CREATE, httpHeaders, endpointUrl, + accessControl); + } + if (realtimeTableConfig != null) { + tuneConfig(realtimeTableConfig, schema); + AccessControlUtils + .validatePermission(realtimeTableConfig.getTableName(), AccessType.CREATE, httpHeaders, endpointUrl, + accessControl); + } + TableConfigs validatedTableConfigs = validateConfig(tableConfigs, typesToSkip); ObjectNode response = JsonUtils.objectToJsonNode(validatedTableConfigs).deepCopy(); response.set("unrecognizedProperties", JsonUtils.objectToJsonNode(tableConfigsAndUnrecognizedProps.getRight())); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
