Vamsi-klu commented on code in PR #18975:
URL: https://github.com/apache/pinot/pull/18975#discussion_r3860045180
##########
pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AccessControlUtils.java:
##########
@@ -74,6 +75,36 @@ public static void validatePermission(@Nullable String
tableName, AccessType acc
Response.Status.FORBIDDEN);
}
+ /// Maps a fine grained action name (see [Actions]) to the coarse
[AccessType] bucket that guards the corresponding
+ /// endpoint, so that access control implementations backed by coarse
permissions can authorize fine grained actions.
+ /// Action names follow a `<verb><noun>` convention, hence the verb prefix
selects the bucket.
+ ///
+ /// Actions whose verb does not match the access type of their endpoint are
listed as exact matches, which are
+ /// evaluated before the prefixes. Keep new actions in sync with the
`@Authenticate` annotation of the endpoint they
+ /// protect, otherwise a principal configured with only that endpoint's
coarse permission is denied.
Review Comment:
This thread is on an older revision. AccessControlUtils no longer derives
AccessType from action prefixes, so the REBALANCE_TENANT_TABLES / CREATE_TASK /
FORCE_RELEASE mismatches are not in this PR. Current work is the cluster-scope
check plus declared-query-param resolution. Resolving as obsolete.
##########
pinot-core/src/main/java/org/apache/pinot/core/auth/FineGrainedAuthUtils.java:
##########
@@ -37,34 +43,58 @@
public class FineGrainedAuthUtils {
private static final Logger LOGGER =
LoggerFactory.getLogger(FineGrainedAuthUtils.class);
+ /// Memoizes [#declaredQueryParams(Method)]; bounded by the number of
endpoints.
+ private static final Map<Method, Set<String>> DECLARED_QUERY_PARAMS = new
ConcurrentHashMap<>();
private FineGrainedAuthUtils() {
}
- /// Returns the parameter from the path or query params.
- /// @param paramName to look for
- /// @param pathParams path params
- /// @param queryParams query params
- /// @return the value of the parameter
- private static String findParam(String paramName, MultivaluedMap<String,
String> pathParams,
- MultivaluedMap<String, String> queryParams) {
- String name = pathParams.getFirst(paramName);
- if (name == null) {
- name = queryParams.getFirst(paramName);
+ /// Returns the names `endpointMethod` binds as [QueryParam]s.
+ ///
+ /// Only method-level `@QueryParam` binding is recognized; an endpoint
binding parameters through `@BeanParam` or
+ /// resource-class field injection is treated as declaring none. That
direction denies table scope rather than
+ /// granting it, so it fails closed. No in-tree controller or broker
resource uses either form today.
+ ///
+ /// Results are memoized because [Method#getParameterAnnotations()]
re-parses the class-file annotation bytes on
+ /// every call, and this runs on every request before authentication. The
key set is bounded by the number of
+ /// endpoints.
+ public static Set<String> declaredQueryParams(Method endpointMethod) {
+ return DECLARED_QUERY_PARAMS.computeIfAbsent(endpointMethod,
FineGrainedAuthUtils::findDeclaredQueryParams);
+ }
+
+ private static Set<String> findDeclaredQueryParams(Method endpointMethod) {
+ Set<String> declared = new HashSet<>();
+ for (Annotation[] parameterAnnotations :
endpointMethod.getParameterAnnotations()) {
+ for (Annotation parameterAnnotation : parameterAnnotations) {
+ if (parameterAnnotation instanceof QueryParam queryParam) {
+ declared.add(queryParam.value());
+ }
+ }
}
- return name;
+ return Set.copyOf(declared);
}
/// Finds the raw target parameter identified by an [Authorize] annotation.
///
+ /// Path parameters are template variables of the endpoint's own `@Path` and
are always trusted. Query parameters
+ /// are caller-supplied, so only a name the method binds as `@QueryParam`
may identify the table.
+ ///
/// @param auth annotation identifying the authorization target
+ /// @param endpointMethod the resource method, used to decide which query
parameters are declared
/// @param pathParams request path parameters
/// @param queryParams request query parameters
/// @return the unnormalized table parameter value, or `null` for a cluster
target or missing table parameter
@Nullable
- public static String findRawTargetId(Authorize auth, MultivaluedMap<String,
String> pathParams,
- MultivaluedMap<String, String> queryParams) {
- return auth.targetType() == TargetType.TABLE ? findParam(auth.paramName(),
pathParams, queryParams) : null;
+ public static String findRawTargetId(Authorize auth, Method endpointMethod,
+ MultivaluedMap<String, String> pathParams, MultivaluedMap<String,
String> queryParams) {
+ if (auth.targetType() != TargetType.TABLE) {
+ return null;
+ }
+ String targetId = pathParams.getFirst(auth.paramName());
+ if (targetId == null &&
declaredQueryParams(endpointMethod).contains(auth.paramName())) {
+ targetId = queryParams.getFirst(auth.paramName());
+ }
+ return targetId;
}
Review Comment:
PR description still has the Scope notes: FineGrainedAuthUtils is shared
with the broker, the 8 in-tree broker TABLE endpoints use PathParam,
findRawTargetId went from 3 to 4 args with no overload. The master merge did
not change that. Resolving.
##########
pinot-core/src/main/java/org/apache/pinot/core/auth/FineGrainedAuthUtils.java:
##########
@@ -97,8 +127,10 @@ public static void validateFineGrainedAuth(Method
endpointMethod, UriInfo uriInf
Response.Status.INTERNAL_SERVER_ERROR);
}
- // find the paramName in the path or query params
- targetId = findRawTargetId(auth, uriInfo.getPathParameters(),
uriInfo.getQueryParameters());
+ // Path params are part of the endpoint declaration. Query params are
caller-supplied, so only a name
+ // the method binds as @QueryParam may identify the table. Otherwise a
caller could append
+ // ?tableName=<a table it is scoped to> and have a cluster-wide
request authorized as table-scoped.
+ targetId = findRawTargetId(auth, endpointMethod,
uriInfo.getPathParameters(), uriInfo.getQueryParameters());
Review Comment:
Agreed this is not a hold. We reverted 33fd79729a so this PR does not ship
403. After merging master, a missing or unbound table param is 400 from #19231,
not 500 and not 403. Empty paramName on the annotation is still 500. 403 stays
follow-up if we still want it.
##########
pinot-core/src/main/java/org/apache/pinot/core/auth/FineGrainedAuthUtils.java:
##########
@@ -33,38 +40,83 @@
import org.slf4j.LoggerFactory;
-/// Utility methods to share in Broker and Controller request filters related
to fine grain authorization.
+/// Shared broker and controller helpers for fine-grained authorization.
+///
+/// The broker request filter calls [#validateFineGrainedAuth], so tightening
target resolution
+/// here applies to both roles. Every in-tree broker `@Authorize(targetType =
TargetType.TABLE)`
+/// endpoint (`PinotBrokerDebug`, `PinotBrokerRouting`) binds `tableName` as a
`@PathParam` inside
+/// its own `@Path` template, so the declared-query-param filter does not
change broker resolution.
+///
+/// [#findRawTargetId] is public. It previously took 3 arguments (`Authorize`,
path map, query
+/// map) and now takes 4 (`Authorize`, `Method`, path map, query map). There
is no overload: the
+/// `Method` is required to apply the declared-parameter filter. A plugin
calling the old
+/// signature will fail to link.
public class FineGrainedAuthUtils {
private static final Logger LOGGER =
LoggerFactory.getLogger(FineGrainedAuthUtils.class);
+ /// Memoizes [#declaredQueryParams(Method)]; bounded by the number of
endpoints.
+ private static final Map<Method, Set<String>> DECLARED_QUERY_PARAMS = new
ConcurrentHashMap<>();
+
+ /// Status when `@Authorize(TABLE)` names a parameter the endpoint never
binds.
+ ///
+ /// Historically [Response.Status#INTERNAL_SERVER_ERROR], which pages on
error-rate alerts and
+ /// reads as a controller bug. [Response.Status#FORBIDDEN] is the
authorization outcome.
+ /// Restore `INTERNAL_SERVER_ERROR` here — or pass it to
[#unboundTableParamException] — to
+ /// revert to the previous status. Tests pin both directions.
+ static final Response.Status UNBOUND_TABLE_PARAM_STATUS =
Response.Status.FORBIDDEN;
Review Comment:
The constant, both unboundTableParamException helpers, and the revert test
stayed deleted through the master merge. The throw site is not 403. Resolving.
##########
pinot-core/src/main/java/org/apache/pinot/core/auth/FineGrainedAuthUtils.java:
##########
@@ -92,18 +144,21 @@ public static void validateFineGrainedAuth(Method
endpointMethod, UriInfo uriInf
if (auth.targetType() == TargetType.TABLE) {
// paramName is mandatory for table level authorization
if (StringUtils.isEmpty(auth.paramName())) {
+ // TODO: PinotTableRestletResource#copyTable declares
@Authorize(TABLE) with no paramName
+ // and already 500s here. Prefer startup-time validation of the
resource model (or 403)
+ // so a misannotation is not an error-rate page.
throw new WebApplicationException(
"paramName not found for table level authorization in API: " +
uriInfo.getRequestUri(),
Response.Status.INTERNAL_SERVER_ERROR);
}
- // find the paramName in the path or query params
- targetId = findRawTargetId(auth, uriInfo.getPathParameters(),
uriInfo.getQueryParameters());
+ // Path params are part of the endpoint declaration. Query params are
caller-supplied, so only a name
+ // the method binds as @QueryParam may identify the table. Otherwise a
caller could append
+ // ?tableName=<a table it is scoped to> and have a cluster-wide
request authorized as table-scoped.
+ targetId = findRawTargetId(auth, endpointMethod,
uriInfo.getPathParameters(), uriInfo.getQueryParameters());
if (StringUtils.isEmpty(targetId)) {
- throw new WebApplicationException(
- "Could not find paramName " + auth.paramName() + " in path or
query params of the API: "
- + uriInfo.getRequestUri(),
Response.Status.INTERNAL_SERVER_ERROR);
+ throw unboundTableParamException(auth.paramName(),
uriInfo.getRequestUri());
Review Comment:
33fd79729a stays reverted. The tree still matches the approved security fix,
plus master #19231 400 for a missing table param. 403 is follow-up. Resolving.
##########
pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AccessControl.java:
##########
@@ -25,6 +25,15 @@
import org.apache.pinot.spi.annotations.InterfaceStability;
+/// Controller access-control SPI.
+///
+/// Custom implementations should audit two resolution changes from
apache/pinot#18975:
+/// 1. A table name appended as an undeclared query parameter no longer reaches
+/// [#hasAccess(String, AccessType, HttpHeaders, String)]; the request
arrives with a `null`
+/// table name and must be treated as cluster-wide.
+/// 2. [org.apache.pinot.core.auth.FineGrainedAuthUtils#findRawTargetId]
changed from 3 arguments
+/// (`Authorize`, path map, query map) to 4 by adding the resource
`Method`. There is no
+/// overload; a plugin calling the old signature will fail to link.
Review Comment:
Class-level migration note stayed out after the revert and the master merge.
Per-method javadoc still has the durable contract: null table name means
cluster-wide. Resolving.
##########
pinot-core/src/test/java/org/apache/pinot/core/auth/FineGrainedAuthUtilsTest.java:
##########
@@ -36,22 +38,113 @@
public class FineGrainedAuthUtilsTest {
@Test
- public void testFindRawTargetId() throws Exception {
+ public void testFindRawTargetId()
+ throws Exception {
MultivaluedMap<String, String> pathParams = new MultivaluedHashMap<>();
MultivaluedMap<String, String> queryParams = new MultivaluedHashMap<>();
- Authorize tableAuth =
TestResource.class.getDeclaredMethod("getTable").getAnnotation(Authorize.class);
+ Method tableMethod = TestResource.class.getDeclaredMethod("getTable");
+ Method tableQueryMethod =
TestResource.class.getDeclaredMethod("getTableByQuery", String.class);
+ Authorize tableAuth = tableMethod.getAnnotation(Authorize.class);
+ Authorize tableQueryAuth = tableQueryMethod.getAnnotation(Authorize.class);
Authorize clusterAuth =
getAnnotatedMethod().getAnnotation(Authorize.class);
pathParams.putSingle("tableName", "pathTable");
- assertEquals(FineGrainedAuthUtils.findRawTargetId(tableAuth, pathParams,
queryParams), "pathTable");
+ assertEquals(FineGrainedAuthUtils.findRawTargetId(tableAuth, tableMethod,
pathParams, queryParams), "pathTable");
pathParams.clear();
queryParams.putSingle("tableName", "queryTable");
- assertEquals(FineGrainedAuthUtils.findRawTargetId(tableAuth, pathParams,
queryParams), "queryTable");
- assertNull(FineGrainedAuthUtils.findRawTargetId(clusterAuth, pathParams,
queryParams));
+ // The annotation names tableName, but getTable never binds it, so the
query value is not trusted.
+ assertNull(FineGrainedAuthUtils.findRawTargetId(tableAuth, tableMethod,
pathParams, queryParams));
+ assertEquals(FineGrainedAuthUtils.findRawTargetId(tableQueryAuth,
tableQueryMethod, pathParams, queryParams),
+ "queryTable");
+ assertNull(FineGrainedAuthUtils.findRawTargetId(clusterAuth,
getAnnotatedMethod(), pathParams, queryParams));
queryParams.clear();
- assertNull(FineGrainedAuthUtils.findRawTargetId(tableAuth, pathParams,
queryParams));
+ assertNull(FineGrainedAuthUtils.findRawTargetId(tableAuth, tableMethod,
pathParams, queryParams));
+ }
+
+ @Test
+ public void testValidateFineGrainedAuthIgnoresUndeclaredTableQueryParam()
+ throws Exception {
+ FineGrainedAccessControl ac = Mockito.mock(FineGrainedAccessControl.class);
+ Mockito.when(ac.hasAccess(Mockito.any(HttpHeaders.class), Mockito.any(),
Mockito.any(), Mockito.any()))
+ .thenReturn(true);
+
+ UriInfo mockUriInfo = Mockito.mock(UriInfo.class);
+ MultivaluedMap<String, String> pathParams = new MultivaluedHashMap<>();
+ MultivaluedMap<String, String> queryParams = new MultivaluedHashMap<>();
+ queryParams.putSingle("tableName", "callerPicked");
+ Mockito.when(mockUriInfo.getPathParameters()).thenReturn(pathParams);
+ Mockito.when(mockUriInfo.getQueryParameters()).thenReturn(queryParams);
+
Mockito.when(mockUriInfo.getRequestUri()).thenReturn(URI.create("http://localhost/tables"));
+ HttpHeaders mockHttpHeaders = Mockito.mock(HttpHeaders.class);
+
+ Method unboundTableMethod =
TestResource.class.getDeclaredMethod("getTable");
+ try {
+ FineGrainedAuthUtils.validateFineGrainedAuth(unboundTableMethod,
mockUriInfo, mockHttpHeaders, ac);
+ Assert.fail("Expected WebApplicationException");
+ } catch (WebApplicationException e) {
+ Assert.assertTrue(e.getMessage().contains("Could not find paramName"));
+ Assert.assertEquals(e.getResponse().getStatus(),
+ FineGrainedAuthUtils.UNBOUND_TABLE_PARAM_STATUS.getStatusCode());
+ Assert.assertEquals(e.getResponse().getStatus(),
Response.Status.FORBIDDEN.getStatusCode());
+ }
+ Mockito.verify(ac, Mockito.never())
+ .hasAccess(Mockito.any(HttpHeaders.class), Mockito.any(),
Mockito.any(), Mockito.any());
+ }
+
+ @Test
+ public void testUnboundTableParamExceptionRevertsToInternalServerError() {
+ URI requestUri = URI.create("http://localhost/tables");
+ WebApplicationException current =
FineGrainedAuthUtils.unboundTableParamException("tableName", requestUri);
+ Assert.assertEquals(current.getResponse().getStatus(),
Response.Status.FORBIDDEN.getStatusCode());
+
+ // Revert path: passing the previous status restores the 500 that this
case used to pin.
+ WebApplicationException reverted =
FineGrainedAuthUtils.unboundTableParamException("tableName", requestUri,
+ Response.Status.INTERNAL_SERVER_ERROR);
+ Assert.assertTrue(reverted.getMessage().contains("Could not find
paramName"));
+ Assert.assertEquals(reverted.getResponse().getStatus(),
Response.Status.INTERNAL_SERVER_ERROR.getStatusCode());
+ Assert.assertEquals(reverted.getMessage(), current.getMessage());
+ }
Review Comment:
The revert-seam test stayed deleted. The pin is still
testValidateFineGrainedAuthIgnoresUndeclaredTableQueryParam, and hasAccess is
never called. Status there is 400 after #19231. Resolving.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]