xiangfu0 commented on code in PR #18975:
URL: https://github.com/apache/pinot/pull/18975#discussion_r4244764417


##########
pinot-core/src/main/java/org/apache/pinot/core/auth/Authorize.java:
##########
@@ -37,6 +37,11 @@
   // If the target type is the Pinot cluster, then this field is not required
   String paramName() default "";
 
+  /// Optional path or query parameter that narrows a cluster operation to one 
table for coarse access control.
+  /// Use only when the entire operation is limited to that table, rather than 
filtering part of the response.
+  /// Omitting the parameter retains cluster-wide scope. Fine-grained 
authorization still uses targetType.
+  String tableScopeParam() default "";

Review Comment:
   Documented the fail-closed behavior on tableScopeParam: missing input, or a 
name absent from the endpoint path parameters and declared query parameters, 
resolves no table and retains cluster-wide authorization. A table-scoped 
principal is denied.
   
   Addressed in `6b0ece6f8c`. Validation: 31 focused tests passed, plus 
affected-module formatting, Checkstyle, and license checks.



##########
pinot-controller/src/test/java/org/apache/pinot/controller/api/access/AuthenticationFilterTest.java:
##########
@@ -179,15 +293,93 @@ public void testReadOnlyEndpointsDeclareReadAccess() {
 
   @Test
   public void testSegmentCompletionGetEndpointsDeclareCreateAccess() {
+    MultivaluedMap<String, String> pathParams = new MultivaluedHashMap<>();
+    MultivaluedMap<String, String> queryParams = new MultivaluedHashMap<>();
+    queryParams.putSingle("tableName", "someTable");
     for (String methodName : new String[]{"extendBuildTime", 
"segmentConsumed", "segmentStoppedConsuming",
         "segmentCommitStart", "reduceSegmentSize"}) {
       Method method = 
Arrays.stream(LLCSegmentCompletionHandlers.class.getDeclaredMethods())
           .filter(candidate -> 
candidate.getName().equals(methodName)).findFirst().orElseThrow();
       assertEquals(_authFilter.extractAccessType(method), AccessType.CREATE);
       assertEquals(method.getAnnotation(Authorize.class).action(), 
Actions.Cluster.COMMIT_SEGMENT);
+      // These callbacks name no table, so they take the cluster-wide check. 
That is why a table-scoped server token
+      // is denied and realtime commit stalls — the consequence documented on 
BaseBasicAuthAccessControl.
+      assertNull(AuthenticationFilter.extractTableName(method, pathParams, 
queryParams),
+          methodName + " must not resolve a table name");
     }
   }
 
+  /// Guards the assumption stated on 
[org.apache.pinot.core.auth.FineGrainedAuthUtils#getDeclaredQueryParams]: a
+  /// table target's parameter must be bound as a `@PathParam` or a 
method-level `@QueryParam`. An endpoint binding it
+  /// via `@BeanParam` or field injection would resolve no table and hard-fail 
with 400 for every caller, admin
+  /// included.
+  @Test
+  public void testTableAuthorizeParamsAreBoundOnTheMethod()
+      throws Exception {
+    List<Class<?>> resourceClasses = controllerResourceClasses();
+    assertTrue(resourceClasses.size() > 20, "expected to discover the 
controller resource classes, found "
+        + resourceClasses.size());
+    List<String> violations = new ArrayList<>();
+    for (Class<?> resourceClass : resourceClasses) {
+      for (Method method : resourceClass.getDeclaredMethods()) {
+        Authorize authorize = method.getAnnotation(Authorize.class);
+        if (authorize == null || authorize.targetType() != TargetType.TABLE) {

Review Comment:
   Extended the existing resource binding guard to validate non-empty 
tableScopeParam declarations on CLUSTER endpoints as well as paramName on TABLE 
endpoints. The renamed testAuthorizeTableParamsAreBoundOnTheMethod scans the 
controller resources and reports unbound table parameters.
   
   Addressed in `6b0ece6f8c`. Validation: 31 focused tests passed, plus 
affected-module formatting, Checkstyle, and license checks.



##########
pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AuthenticationFilter.java:
##########
@@ -140,35 +145,55 @@ AccessType extractAccessType(Method endpointMethod) {
     return AccessType.READ;
   }
 
+  /// Resolves the table `endpointMethod` acts on, or `null` when the request 
addresses the cluster rather than a
+  /// table. `AccessControlUtils.validatePermission` picks the table-scoped or 
the cluster-wide check on that answer,
+  /// so it must not be steerable by the caller.
+  ///
+  /// Path parameters are template variables of the endpoint's own `@Path`, 
hence part of its declaration. Query
+  /// parameters are caller-supplied and JAX-RS surfaces every one present on 
the URI, so only those the endpoint
+  /// declares may name the table: otherwise a caller could append 
`?tableName=<a table it is scoped to>` to a cluster
+  /// endpoint and have it authorized as a table-scoped request.
+  ///
+  @Nullable
   @VisibleForTesting
   static String extractTableName(Method endpointMethod, MultivaluedMap<String, 
String> pathParameters,
       MultivaluedMap<String, String> queryParameters) {
     Authorize authorize = endpointMethod.getAnnotation(Authorize.class);
     if (authorize != null && authorize.targetType() == TargetType.TABLE) {
-      return FineGrainedAuthUtils.findRawTargetId(authorize, pathParameters, 
queryParameters);
+      // Same primitive as FineGrainedAuthUtils.validateFineGrainedAuth: path 
first, then a declared query param.
+      return FineGrainedAuthUtils.findRawTargetId(authorize, endpointMethod, 
pathParameters, queryParameters);
+    }
+    if (authorize != null && authorize.targetType() == TargetType.CLUSTER) {
+      String parameter = authorize.tableScopeParam();

Review Comment:
   Confirmed the CLUSTER branch remains authoritative and returns before the 
generic table-name heuristic. The extended resource binding guard now verifies 
that every explicit cluster tableScopeParam is bound by the method, covering 
the missing enforcement noted here.
   
   Addressed in `6b0ece6f8c`. Validation: 31 focused tests passed, plus 
affected-module formatting, Checkstyle, and license checks.



##########
pinot-core/src/main/java/org/apache/pinot/core/auth/BasicAuthPrincipal.java:
##########
@@ -62,6 +74,21 @@ public boolean hasTable(String tableName) {
     return isTableIncluded(tableName) && isTableNotExcluded(tableName);
   }
 
+  /// Returns whether this principal is scoped to every table, i.e. it carries 
neither an allow-list nor an
+  /// exclude-list. Access control implementations use this to decide requests 
that name no table, so the return value
+  /// must satisfy: `true` implies [#hasTable(String)] holds for every table 
name. A subclass that narrows table scope
+  /// by any other means — in particular by overriding [#hasTable(String)] — 
must override this method to match, or it
+  /// will report unrestricted scope while denying individual tables.
+  ///
+  /// Table lists are the signal rather than [ZkBasicAuthPrincipal]'s 
`RoleType`. The static factory has no role, both
+  /// controller factories share this check, and an ADMIN that also carries an 
allow-list is still scoped. Granting
+  /// cluster access by role would let that principal reach cluster state 
outside the list. The bootstrapped admin
+  /// is bootstrapped with a null `UserConfig#getTables()`, which normalizes 
to an empty set here, so
+  /// `RoleType.ADMIN` without a table list still passes.
+  public boolean hasUnrestrictedTableAccess() {

Review Comment:
   Keeping both methods overridable to preserve compatibility with existing 
BasicAuthPrincipal subclasses. Making the previously overridable hasTable 
method final would break those subclasses; making only 
hasUnrestrictedTableAccess final would not enforce the invariant and would 
prevent custom restrictions from being reflected in the cluster check. The 
Javadoc already requires subclasses narrowing hasTable to override 
hasUnrestrictedTableAccess, and the PR migration notes now make that 
requirement explicit. The in-tree ZkBasicAuthPrincipal does not override 
hasTable and follows the same table-list policy.
   
   Addressed in `6b0ece6f8c`. Validation: 31 focused tests passed, plus 
affected-module formatting, Checkstyle, and license checks.



##########
pinot-core/src/main/java/org/apache/pinot/core/auth/BasicAuthPrincipal.java:
##########
@@ -44,12 +47,21 @@ public BasicAuthPrincipal(String name, String token, 
Set<String> tables, Set<Str
       Set<String> permissions, Map<String, List<String>> rlsFilters) {
     _name = name;
     _token = token;
-    _tables = tables;
-    _excludeTables = excludeTables;
+    // Copied because hasUnrestrictedTableAccess() gates cluster-level 
authorization off these sets: a caller that
+    // retained and later cleared its own set would silently widen this 
principal to unrestricted scope. Copied
+    // permissively rather than with Set.copyOf, which rejects null elements — 
a user record stored in ZooKeeper can
+    // carry a null table name, and the broker and server rebuild every 
principal on each request, so one malformed
+    // record would otherwise fail authentication for every user.
+    _tables = copyOfNullable(tables);
+    _excludeTables = copyOfNullable(excludeTables);
     _permissions = permissions.stream().map(s -> 
s.toLowerCase()).collect(Collectors.toSet());
     _rlsFilters = rlsFilters;
   }
 
+  private static Set<String> copyOfNullable(@Nullable Set<String> values) {

Review Comment:
   Added testBasicAuthPrincipalWithNullTableEntry for allow-lists and 
exclude-lists containing both a valid table and null. It verifies construction 
succeeds, valid table authorization behaves correctly, and the malformed entry 
never makes the principal unrestricted.
   
   Addressed in `6b0ece6f8c`. Validation: 31 focused tests passed, plus 
affected-module formatting, Checkstyle, and license checks.



-- 
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]

Reply via email to