Jackie-Jiang commented on code in PR #19664:
URL: https://github.com/apache/pinot/pull/19664#discussion_r4253443736


##########
pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotQueryResource.java:
##########
@@ -424,6 +459,163 @@ private StreamingOutput executeSqlQuery(@Context 
HttpHeaders httpHeaders, String
     }
   }
 
+  /// Executes a `DELETE` once the caller is authorized to delete rows from 
its table, see [#authorizeDelete].
+  ///
+  /// The caller is first checked without a table (the `READ` access type with 
the `/sql` endpoint, as for a
+  /// multi-stage query), before the statement is parsed and its table looked 
up: with basic auth, an unauthenticated
+  /// caller gets HTTP 401 here, before the table cache is consulted. The 
table is then resolved, with the database of
+  /// the request and in the case it is defined with, and the caller 
authorized to delete rows from the resolved
+  /// table. Only then is the table checked not to be a logical table and to 
exist, and every error message names the
+  /// table as the caller wrote it, so that a caller who is not authorized for 
a table learns neither whether it exists
+  /// nor the case it is defined with, nor whether the name is a logical 
table. The executor deletes rows from the
+  /// exact resolved table.
+  ///
+  /// The executor runs before the response streams, so that an exception it 
lets escape is mapped to an error
+  /// response by [#executeSqlQueryCatching] rather than failing the request 
with HTTP 500.
+  ///
+  /// @param request request the statement came with, whose remote address the 
log line names; null when the
+  ///                statement did not come with one
+  /// @throws QueryException with [QueryErrorCode#ACCESS_DENIED] if the caller 
is not authorized, with
+  ///                        [QueryErrorCode#QUERY_VALIDATION] if the table is 
a logical table, with
+  ///                        [QueryErrorCode#TABLE_DOES_NOT_EXIST] if the 
table does not exist, and with the errors of
+  ///                        [DataManipulationStatementParser#parse] and 
[DeleteStatement#resolveTableName]
+  private StreamingOutput executeDelete(SqlNodeAndOptions sqlNodeAndOptions, 
HttpHeaders httpHeaders,
+      @Nullable Request request) {
+    AccessControl accessControl = _accessControlFactory.create();
+    if (!accessControl.hasAccess(AccessType.READ, httpHeaders, SQL_ENDPOINT)) {
+      throw QueryErrorCode.ACCESS_DENIED.asException("Permission denied to 
delete rows");
+    }

Review Comment:
   This overload is a cluster-wide authorization check, not an 
authentication-only check. Both controller BasicAuth implementations inherit 
BaseBasicAuthAccessControl.hasAccess(AccessType, ...), which rejects any 
principal with a table allow-list or exclude-list. A principal with tables=t 
and permissions=read,delete therefore gets ACCESS_DENIED for DELETE FROM t 
WHERE id = 1 before the resolved-table checks or executor run, although it can 
execute the same DELETE through the broker. Please use an authentication-only 
precheck (while preserving authentication before the cache lookup), followed by 
the existing resolved-table authorization. A controller test using a real 
table-scoped BasicAuth principal would cover this; the unrestricted 
integration-test administrator and mocked access controls miss it.



##########
pinot-core/src/main/java/org/apache/pinot/core/auth/BasicAuthPrincipal.java:
##########
@@ -93,8 +94,11 @@ private boolean isTableIncluded(String tableName) {
     return _tables.isEmpty() || _tables.contains(tableName);
   }
 
+  /// The `excludeTables` list holds raw table names, so a name with a type 
suffix is also checked by its raw name:
+  /// with `excludeTables=[t]`, `t_OFFLINE` is excluded as `t` is, and a type 
suffix cannot bypass an excluded table.
   private boolean isTableNotExcluded(String tableName) {
-    return !_excludeTables.contains(tableName);
+    return !_excludeTables.contains(tableName)
+        && 
!_excludeTables.contains(TableNameBuilder.extractRawTableName(tableName));

Review Comment:
   Non-blocking performance suggestion for existing queries: return immediately 
when _excludeTables is empty, and only perform the second contains check when 
the extracted raw name differs from tableName. As written, every successful 
exact-name exclusion check proceeds to suffix extraction and another lookup, 
even with no exclusions configured; typed names also allocate a substring. 
Callers that already pass raw names repeat the same lookup. Keep the raw-name 
exclusion enforcement itself, since removing it would reopen the type-suffix 
bypass.



##########
pinot-common/src/main/java/org/apache/pinot/sql/parsers/CalciteSqlParser.java:
##########
@@ -122,19 +123,14 @@ public static SqlNodeAndOptions 
compileToSqlNodeAndOptions(String sql)
     sql = ParserUtils.sanitizeSql(sql);
 
     // extract and remove OPTIONS string
-    List<String> options = List.of();
     SqlOptionsMode legacyOptionSyntaxMode = 
QueryOptionsUtils.getLegacyOptionSyntaxMode();
-    if (legacyOptionSyntaxMode == SqlOptionsMode.IGNORE) {
-      sql = removeOptionsFromSql(sql);
-    } else {
-      options = extractOptionsFromSql(sql);
-      if (!options.isEmpty()) {
-        if (legacyOptionSyntaxMode == SqlOptionsMode.REJECT) {
-          throw new SqlCompilationException("Legacy OPTION(...) query options 
are not allowed on this cluster, use "
-              + "'SET <key> = <value>;' statements instead: " + options);
-        }
-        sql = removeOptionsFromSql(sql);
+    List<String> options = extractOptionsFromSql(sql);
+    if (!options.isEmpty()) {
+      if (legacyOptionSyntaxMode == SqlOptionsMode.REJECT) {
+        throw new SqlCompilationException("Legacy OPTION(...) query options 
are not allowed on this cluster, use "
+            + "'SET <key> = <value>;' statements instead: " + options);
       }
+      sql = removeOptionsFromSql(sql);

Review Comment:
   Non-blocking performance suggestion: preserve the single regex pass for 
legacy IGNORE mode. Previously that mode only called removeOptionsFromSql; it 
now allocates the extraction list on every parse and, when an OPTION suffix 
exists, scans the SQL again to remove it. One matcher can detect and strip the 
end-anchored suffix, retaining its presence so DELETE options are still 
rejected under IGNORE. Avoid materializing an options list when no suffix 
exists. The default ALLOW path already did both scans before this PR, so this 
is specifically added work for existing IGNORE-mode traffic, not a demonstrated 
regression for all queries.



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