Skip to content

Commit

Permalink
add @ManualAuthorization annotation for non-standard endpoints (#9252)
Browse files Browse the repository at this point in the history
This PR adds a new annotation @ManualAuthorization for REST endpoints, which allows developers to skip the default authorization and deserialize payloads before manually invoking authorization, e.g. via AccessControlUtils.validatePermissions(). This annotation comes with obvious risks and should be used sparingly, as it enables requests to bypass most of the AuthFilter.
  • Loading branch information
apucher authored Aug 25, 2022
1 parent ae87819 commit f6e26c2
Show file tree
Hide file tree
Showing 15 changed files with 156 additions and 167 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
package org.apache.pinot.controller.api.access;

import javax.annotation.Nullable;
import javax.ws.rs.core.HttpHeaders;
import org.apache.pinot.spi.annotations.InterfaceAudience;
import org.apache.pinot.spi.annotations.InterfaceStability;
Expand All @@ -40,7 +41,9 @@ public interface AccessControl {
* @return Whether the client has data access to the table
*/
@Deprecated
boolean hasDataAccess(HttpHeaders httpHeaders, String tableName);
default boolean hasDataAccess(HttpHeaders httpHeaders, String tableName) {
return hasAccess(tableName, AccessType.READ, httpHeaders, null);
}

/**
* Return whether the client has permission to the given table
Expand All @@ -51,7 +54,8 @@ public interface AccessControl {
* @param endpointUrl the request url for which this access control is called
* @return whether the client has permission
*/
default boolean hasAccess(String tableName, AccessType accessType, HttpHeaders httpHeaders, String endpointUrl) {
default boolean hasAccess(@Nullable String tableName, AccessType accessType, HttpHeaders httpHeaders,
@Nullable String endpointUrl) {
return true;
}

Expand All @@ -63,12 +67,8 @@ default boolean hasAccess(String tableName, AccessType accessType, HttpHeaders h
* @param endpointUrl the request url for which this access control is called
* @return whether the client has permission
*/
default boolean hasAccess(AccessType accessType, HttpHeaders httpHeaders, String endpointUrl) {
return true;
}

default boolean hasAccess(HttpHeaders httpHeaders) {
return true;
default boolean hasAccess(AccessType accessType, HttpHeaders httpHeaders, @Nullable String endpointUrl) {
return hasAccess(null, accessType, httpHeaders, endpointUrl);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,56 +50,32 @@ private AccessControlUtils() {
*/
public static void validatePermission(@Nullable String tableName, AccessType accessType,
@Nullable HttpHeaders httpHeaders, @Nullable String endpointUrl, AccessControl accessControl) {
String message = null;
String userMessage = getUserMessage(tableName, accessType, endpointUrl);
String rawTableName = TableNameBuilder.extractRawTableName(tableName);

try {
if (StringUtils.isBlank(tableName)) {
message = String.format("%s '%s'", accessType, endpointUrl);
if (!accessControl.hasAccess(accessType, httpHeaders, endpointUrl)) {
accessDenied(message);
if (rawTableName == null) {
if (accessControl.hasAccess(accessType, httpHeaders, endpointUrl)) {
return;
}
} else {
message = String.format("%s '%s' for table '%s'", accessType, endpointUrl, tableName);
String rawTableName = TableNameBuilder.extractRawTableName(tableName);
if (!accessControl.hasAccess(rawTableName, accessType, httpHeaders, endpointUrl)) {
accessDenied(message);
if (accessControl.hasAccess(rawTableName, accessType, httpHeaders, endpointUrl)) {
return;
}
}
} catch (ControllerApplicationException e) {
throw e;
} catch (Exception e) {
throw new ControllerApplicationException(LOGGER, "Caught exception while validating permission for " + message,
Response.Status.INTERNAL_SERVER_ERROR, e);
throw new ControllerApplicationException(LOGGER, "Caught exception while validating permission for "
+ userMessage, Response.Status.INTERNAL_SERVER_ERROR, e);
}
}

/**
* Validate permission for the given access type for a non-table level endpoint
*
* @param accessType type of the access
* @param httpHeaders HTTP headers containing requester identity required by access control object
* @param endpointUrl the request url for which this access control is called
* @param accessControl AccessControl object which does the actual validation
*/
public static void validatePermission(AccessType accessType, @Nullable HttpHeaders httpHeaders,
@Nullable String endpointUrl, AccessControl accessControl) {
validatePermission(null, accessType, httpHeaders, endpointUrl, accessControl);
throw new ControllerApplicationException(LOGGER, "Permission is denied for " + userMessage,
Response.Status.FORBIDDEN);
}

/**
* Validate permission for the given access type and endpointUrl
*
* @param httpHeaders HTTP headers containing requester identity required by access control object
* @param endpointUrl the request url for which this access control is called
*/
public static void validatePermission(@Nullable HttpHeaders httpHeaders, @Nullable String endpointUrl,
AccessControl accessControl) {
if (!accessControl.hasAccess(httpHeaders)) {
accessDenied(endpointUrl);
private static String getUserMessage(String tableName, AccessType accessType, String endpointUrl) {
if (StringUtils.isBlank(tableName)) {
return String.format("%s '%s'", accessType, endpointUrl);
}
}

private static void accessDenied(String resource) {
throw new ControllerApplicationException(LOGGER, "Permission is denied for " + resource,
Response.Status.FORBIDDEN);
return String.format("%s '%s' for table '%s'", accessType, endpointUrl, tableName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,10 @@ public class AuthenticationFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext)
throws IOException {
Request request = _requestProvider.get();
Method endpointMethod = _resourceInfo.getResourceMethod();
AccessControl accessControl = _accessControlFactory.create();
String endpointUrl = _requestProvider.get().getRequestURL().toString();
String endpointUrl = request.getRequestURI().substring(request.getContextPath().length()); // extract path only
UriInfo uriInfo = requestContext.getUriInfo();

// exclude public/unprotected paths
Expand All @@ -82,6 +83,11 @@ public void filter(ContainerRequestContext requestContext)
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",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* 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 {
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@
import org.apache.pinot.controller.helix.core.PinotHelixResourceManager;
import org.apache.pinot.core.auth.BasicAuthUtils;
import org.apache.pinot.core.auth.ZkBasicAuthPrincipal;
import org.apache.pinot.spi.config.user.ComponentType;
import org.apache.pinot.spi.config.user.RoleType;
import org.apache.pinot.spi.env.PinotConfiguration;


Expand Down Expand Up @@ -95,12 +93,6 @@ public boolean hasAccess(AccessType accessType, HttpHeaders httpHeaders, String
return getPrincipal(httpHeaders).isPresent();
}

@Override
public boolean hasAccess(HttpHeaders httpHeaders) {
return getPrincipal(httpHeaders)
.filter(p -> p.hasPermission(RoleType.ADMIN, ComponentType.CONTROLLER)).isPresent();
}

private Optional<ZkBasicAuthPrincipal> getPrincipal(HttpHeaders headers) {
if (headers == null) {
return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,13 @@
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.helix.store.zk.ZkHelixPropertyStore;
import org.apache.helix.zookeeper.datamodel.ZNRecord;
import org.apache.pinot.common.metadata.ZKMetadataProvider;
import org.apache.pinot.common.utils.BcryptUtils;
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.exception.ControllerApplicationException;
Expand All @@ -54,7 +51,6 @@
import org.apache.pinot.spi.config.user.ComponentType;
import org.apache.pinot.spi.config.user.UserConfig;
import org.apache.pinot.spi.utils.JsonUtils;
import org.glassfish.grizzly.http.server.Request;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -93,17 +89,12 @@ public class PinotAccessControlUserRestletResource {
@Inject
PinotHelixResourceManager _pinotHelixResourceManager;

@Inject
AccessControlFactory _accessControlFactory;

@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/users")
@ApiOperation(value = "List all uses in cluster", notes = "List all users in cluster")
public String listUers(@Context HttpHeaders httpHeaders, @Context Request request) {
public String listUers() {
try {
String endpointUrl = request.getRequestURL().toString();
AccessControlUtils.validatePermission(httpHeaders, endpointUrl, _accessControlFactory.create());
ZkHelixPropertyStore<ZNRecord> propertyStore = _pinotHelixResourceManager.getPropertyStore();
Map<String, UserConfig> allUserInfo = ZKMetadataProvider.getAllUserInfo(propertyStore);
return JsonUtils.newObjectNode().set("users", JsonUtils.objectToJsonNode(allUserInfo)).toString();
Expand All @@ -116,11 +107,8 @@ public String listUers(@Context HttpHeaders httpHeaders, @Context Request reques
@Produces(MediaType.APPLICATION_JSON)
@Path("/users/{username}")
@ApiOperation(value = "Get an user in cluster", notes = "Get an user in cluster")
public String getUser(@PathParam("username") String username, @QueryParam("component") String componentTypeStr,
@Context HttpHeaders httpHeaders, @Context Request request) {
public String getUser(@PathParam("username") String username, @QueryParam("component") String componentTypeStr) {
try {
String endpointUrl = request.getRequestURL().toString();
AccessControlUtils.validatePermission(httpHeaders, endpointUrl, _accessControlFactory.create());
ZkHelixPropertyStore<ZNRecord> propertyStore = _pinotHelixResourceManager.getPropertyStore();
ComponentType componentType = Constants.validateComponentType(componentTypeStr);
String usernameWithType = username + "_" + componentType.name();
Expand All @@ -136,16 +124,14 @@ public String getUser(@PathParam("username") String username, @QueryParam("compo
@Produces(MediaType.APPLICATION_JSON)
@Path("/users")
@ApiOperation(value = "Add a user", notes = "Add a user")
public SuccessResponse addUser(String userConfigStr, @Context HttpHeaders httpHeaders, @Context Request request) {
public SuccessResponse addUser(String userConfigStr) {
// TODO introduce a table config ctor with json string.

UserConfig userConfig;
String username;
try {
userConfig = JsonUtils.stringToObject(userConfigStr, UserConfig.class);
username = userConfig.getUserName();
String endpointUrl = request.getRequestURL().toString();
AccessControlUtils.validatePermission(httpHeaders, endpointUrl, _accessControlFactory.create());
if (username.contains(".") || username.contains(" ")) {
throw new IllegalStateException("Username: " + username + " containing '.' or space is not allowed");
}
Expand All @@ -171,8 +157,7 @@ public SuccessResponse addUser(String userConfigStr, @Context HttpHeaders httpHe
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(value = "Delete a user", notes = "Delete a user")
public SuccessResponse deleteUser(@PathParam("username") String username,
@QueryParam("component") String componentTypeStr,
@Context HttpHeaders httpHeaders, @Context Request request) {
@QueryParam("component") String componentTypeStr) {

List<String> usersDeleted = new LinkedList<>();
String usernameWithComponentType = username + "_" + componentTypeStr;
Expand All @@ -182,9 +167,6 @@ public SuccessResponse deleteUser(@PathParam("username") String username,
boolean userExist = false;
userExist = _pinotHelixResourceManager.hasUser(username, componentTypeStr);

String endpointUrl = request.getRequestURL().toString();
AccessControlUtils.validatePermission(httpHeaders, endpointUrl, _accessControlFactory.create());

_pinotHelixResourceManager.deleteUser(usernameWithComponentType);
if (userExist) {
usersDeleted.add(username);
Expand All @@ -210,16 +192,11 @@ public SuccessResponse updateUserConfig(
@PathParam("username") String username,
@QueryParam("component") String componentTypeStr,
@QueryParam("passwordChanged") boolean passwordChanged,
String userConfigString,
@Context HttpHeaders httpHeaders,
@Context Request request) {
String userConfigString) {

UserConfig userConfig;
String usernameWithComponentType = username + "_" + componentTypeStr;
try {
String endpointUrl = request.getRequestURL().toString();
AccessControlUtils.validatePermission(httpHeaders, endpointUrl, _accessControlFactory.create());

userConfig = JsonUtils.stringToObject(userConfigString, UserConfig.class);
if (passwordChanged) {
userConfig.setPassword(BcryptUtils.encrypt(userConfig.getPassword()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import org.apache.commons.lang3.StringUtils;
import org.apache.pinot.controller.api.access.AccessControl;
import org.apache.pinot.controller.api.access.AccessControlFactory;
import org.apache.pinot.controller.api.access.AccessType;
Expand Down Expand Up @@ -76,11 +75,6 @@ public boolean verify(@ApiParam(value = "Table name without type") @QueryParam("
@ApiParam(value = "API access type") @QueryParam("accessType") AccessType accessType,
@ApiParam(value = "Endpoint URL") @QueryParam("endpointUrl") String endpointUrl) {
AccessControl accessControl = _accessControlFactory.create();

if (StringUtils.isBlank(tableName)) {
return accessControl.hasAccess(accessType, _httpHeaders, endpointUrl);
}

return accessControl.hasAccess(tableName, accessType, _httpHeaders, endpointUrl);
}

Expand Down
Loading

0 comments on commit f6e26c2

Please sign in to comment.