This is an automated email from the ASF dual-hosted git repository.

adutra pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git


The following commit(s) were added to refs/heads/main by this push:
     new 8b108d6be Require table read/write privilege for metrics reporting 
(#3724)
8b108d6be is described below

commit 8b108d6be7222a8ed78b1b2b70816ecbeea1b327
Author: Alexandre Dutra <[email protected]>
AuthorDate: Fri Feb 13 12:22:29 2026 +0100

    Require table read/write privilege for metrics reporting (#3724)
    
    This commit changes the metrics report endpoint to require either 
`TABLE_READ_DATA` or `TABLE_WRITE_DATA` privileges, depending on the report 
type (scan vs commit respectively).
---
 CHANGELOG.md                                       |  1 +
 .../it/test/PolarisRestCatalogIntegrationBase.java |  3 +
 .../core/auth/PolarisAuthorizableOperation.java    |  3 +-
 .../catalog/iceberg/IcebergCatalogAdapter.java     | 22 ++----
 .../catalog/iceberg/IcebergCatalogHandler.java     | 20 +++++
 .../iceberg/IcebergCatalogHandlerFactory.java      |  6 ++
 .../AbstractIcebergCatalogHandlerAuthzTest.java    | 89 ++++++++++++++++++++++
 .../org/apache/polaris/service/TestServices.java   |  9 +--
 8 files changed, 131 insertions(+), 22 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index f5cc31fcc..67e224b46 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -39,6 +39,7 @@ request adding CHANGELOG notes for breaking (!) changes and 
possibly other secti
 - The (Before/After)CommitTableEvent has been removed.
 - The `PolarisMetricsReporter.reportMetric()` method signature has been 
extended to include a `receivedTimestamp` parameter of type `java.time.Instant`.
 - The `ExternalCatalogFactory.createCatalog()` and `createGenericCatalog()` 
method signatures have been extended to include a `catalogProperties` parameter 
of type `Map<String, String>` for passing through proxy and timeout settings to 
federated catalog HTTP clients.
+- Metrics reporting now requires the `TABLE_READ_DATA` privilege on the target 
table for read (scan) metrics and `TABLE_WRITE_DATA` for write (commit) metrics.
 
 ### New Features
 
diff --git 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java
 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java
index 69a39c7b0..765c22b5b 100644
--- 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java
+++ 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java
@@ -947,6 +947,9 @@ public abstract class PolarisRestCatalogIntegrationBase 
extends CatalogTests<RES
 
   @Test
   public void testSendMetricsReport() {
+    restCatalog.createNamespace(Namespace.of("ns1"));
+    restCatalog.buildTable(TableIdentifier.of(Namespace.of("ns1"), "tbl1"), 
SCHEMA).create();
+
     ScanReport scanReport =
         ImmutableScanReport.builder()
             .tableName("tbl1")
diff --git 
a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizableOperation.java
 
b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizableOperation.java
index 9d12cc148..ee373a232 100644
--- 
a/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizableOperation.java
+++ 
b/polaris-core/src/main/java/org/apache/polaris/core/auth/PolarisAuthorizableOperation.java
@@ -144,7 +144,8 @@ public enum PolarisAuthorizableOperation {
   DROP_VIEW(VIEW_DROP),
   VIEW_EXISTS(VIEW_LIST),
   RENAME_VIEW(VIEW_DROP, EnumSet.of(VIEW_LIST, VIEW_CREATE)),
-  REPORT_METRICS(EnumSet.noneOf(PolarisPrivilege.class)),
+  REPORT_READ_METRICS(TABLE_READ_DATA),
+  REPORT_WRITE_METRICS(TABLE_WRITE_DATA),
   SEND_NOTIFICATIONS(
       EnumSet.of(
           TABLE_CREATE, TABLE_WRITE_PROPERTIES, TABLE_DROP, NAMESPACE_CREATE, 
NAMESPACE_DROP)),
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
index a240186d0..24c959c3e 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
@@ -28,7 +28,6 @@ import jakarta.inject.Inject;
 import jakarta.ws.rs.core.HttpHeaders;
 import jakarta.ws.rs.core.Response;
 import jakarta.ws.rs.core.SecurityContext;
-import java.time.Clock;
 import java.util.EnumSet;
 import java.util.Optional;
 import java.util.function.Function;
@@ -62,7 +61,6 @@ import 
org.apache.polaris.service.catalog.common.CatalogAdapter;
 import org.apache.polaris.service.config.ReservedProperties;
 import org.apache.polaris.service.http.IcebergHttpUtil;
 import org.apache.polaris.service.http.IfNoneMatch;
-import org.apache.polaris.service.reporting.PolarisMetricsReporter;
 import org.apache.polaris.service.types.CommitTableRequest;
 import org.apache.polaris.service.types.CommitViewRequest;
 import org.apache.polaris.service.types.NotificationRequest;
@@ -79,12 +77,9 @@ public class IcebergCatalogAdapter
 
   private static final Logger LOGGER = 
LoggerFactory.getLogger(IcebergCatalogAdapter.class);
 
-  private final RealmContext realmContext;
   private final RealmConfig realmConfig;
   private final CatalogPrefixParser prefixParser;
   private final ReservedProperties reservedProperties;
-  private final PolarisMetricsReporter metricsReporter;
-  private final Clock clock;
   private final IcebergCatalogHandlerFactory handlerFactory;
 
   @Inject
@@ -92,15 +87,10 @@ public class IcebergCatalogAdapter
       CallContext callContext,
       CatalogPrefixParser prefixParser,
       ReservedProperties reservedProperties,
-      PolarisMetricsReporter metricsReporter,
-      Clock clock,
       IcebergCatalogHandlerFactory handlerFactory) {
-    this.realmContext = callContext.getRealmContext();
     this.realmConfig = callContext.getRealmConfig();
     this.prefixParser = prefixParser;
     this.reservedProperties = reservedProperties;
-    this.metricsReporter = metricsReporter;
-    this.clock = clock;
     this.handlerFactory = handlerFactory;
   }
 
@@ -665,13 +655,15 @@ public class IcebergCatalogAdapter
       ReportMetricsRequest reportMetricsRequest,
       RealmContext realmContext,
       SecurityContext securityContext) {
-    String catalogName = prefixParser.prefixToCatalogName(prefix);
     Namespace ns = decodeNamespace(namespace);
     TableIdentifier tableIdentifier = TableIdentifier.of(ns, 
RESTUtil.decodeString(table));
-
-    metricsReporter.reportMetric(
-        catalogName, tableIdentifier, reportMetricsRequest.report(), 
clock.instant());
-    return Response.status(Response.Status.NO_CONTENT).build();
+    return withCatalog(
+        securityContext,
+        prefix,
+        catalog -> {
+          catalog.reportMetrics(tableIdentifier, reportMetricsRequest);
+          return Response.status(Response.Status.NO_CONTENT).build();
+        });
   }
 
   @Override
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
index 3f70f4a72..599ff152e 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java
@@ -32,6 +32,7 @@ import jakarta.annotation.Nonnull;
 import jakarta.annotation.Nullable;
 import jakarta.enterprise.inject.Instance;
 import java.io.Closeable;
+import java.time.Clock;
 import java.time.OffsetDateTime;
 import java.time.ZoneOffset;
 import java.util.ArrayList;
@@ -64,6 +65,7 @@ import org.apache.iceberg.exceptions.CommitFailedException;
 import org.apache.iceberg.exceptions.ForbiddenException;
 import org.apache.iceberg.exceptions.NoSuchTableException;
 import org.apache.iceberg.exceptions.NotFoundException;
+import org.apache.iceberg.metrics.ScanReport;
 import org.apache.iceberg.rest.Endpoint;
 import org.apache.iceberg.rest.credentials.ImmutableCredential;
 import org.apache.iceberg.rest.requests.CommitTransactionRequest;
@@ -72,6 +74,7 @@ import org.apache.iceberg.rest.requests.CreateTableRequest;
 import org.apache.iceberg.rest.requests.CreateViewRequest;
 import org.apache.iceberg.rest.requests.RegisterTableRequest;
 import org.apache.iceberg.rest.requests.RenameTableRequest;
+import org.apache.iceberg.rest.requests.ReportMetricsRequest;
 import org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest;
 import org.apache.iceberg.rest.requests.UpdateTableRequest;
 import org.apache.iceberg.rest.responses.ConfigResponse;
@@ -121,6 +124,7 @@ import org.apache.polaris.service.events.EventAttributeMap;
 import org.apache.polaris.service.events.EventAttributes;
 import org.apache.polaris.service.http.IcebergHttpUtil;
 import org.apache.polaris.service.http.IfNoneMatch;
+import org.apache.polaris.service.reporting.PolarisMetricsReporter;
 import org.apache.polaris.service.types.NotificationRequest;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -196,6 +200,10 @@ public abstract class IcebergCatalogHandler extends 
CatalogHandler implements Au
 
   protected abstract EventAttributeMap eventAttributeMap();
 
+  protected abstract PolarisMetricsReporter metricsReporter();
+
+  protected abstract Clock clock();
+
   // Catalog instance will be initialized after authorizing resolver 
successfully resolves
   // the catalog entity.
   @SuppressWarnings("immutables:incompat")
@@ -652,6 +660,18 @@ public abstract class IcebergCatalogHandler extends 
CatalogHandler implements Au
         && notificationCatalog.sendNotification(identifier, request);
   }
 
+  public void reportMetrics(TableIdentifier identifier, ReportMetricsRequest 
request) {
+
+    PolarisAuthorizableOperation op =
+        request.report() instanceof ScanReport
+            ? PolarisAuthorizableOperation.REPORT_READ_METRICS
+            : PolarisAuthorizableOperation.REPORT_WRITE_METRICS;
+
+    authorizeBasicTableLikeOperationOrThrow(op, 
PolarisEntitySubType.ICEBERG_TABLE, identifier);
+
+    metricsReporter().reportMetric(catalogName(), identifier, 
request.report(), clock().instant());
+  }
+
   /**
    * Fetch the metastore table entity for the given table identifier
    *
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFactory.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFactory.java
index 40f69bc4c..08feb4897 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFactory.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerFactory.java
@@ -22,6 +22,7 @@ import jakarta.enterprise.context.RequestScoped;
 import jakarta.enterprise.inject.Any;
 import jakarta.enterprise.inject.Instance;
 import jakarta.inject.Inject;
+import java.time.Clock;
 import org.apache.polaris.core.PolarisDiagnostics;
 import org.apache.polaris.core.auth.PolarisAuthorizer;
 import org.apache.polaris.core.auth.PolarisPrincipal;
@@ -36,6 +37,7 @@ import 
org.apache.polaris.service.catalog.io.StorageAccessConfigProvider;
 import org.apache.polaris.service.config.ReservedProperties;
 import org.apache.polaris.service.context.catalog.CallContextCatalogFactory;
 import org.apache.polaris.service.events.EventAttributeMap;
+import org.apache.polaris.service.reporting.PolarisMetricsReporter;
 
 @RequestScoped
 public class IcebergCatalogHandlerFactory {
@@ -54,6 +56,8 @@ public class IcebergCatalogHandlerFactory {
   @Inject @Any Instance<ExternalCatalogFactory> externalCatalogFactories;
   @Inject StorageAccessConfigProvider storageAccessConfigProvider;
   @Inject EventAttributeMap eventAttributeMap;
+  @Inject PolarisMetricsReporter metricsReporter;
+  @Inject Clock clock;
 
   public IcebergCatalogHandler createHandler(String catalogName, 
PolarisPrincipal principal) {
     return ImmutableIcebergCatalogHandler.builder()
@@ -73,6 +77,8 @@ public class IcebergCatalogHandlerFactory {
         .externalCatalogFactories(externalCatalogFactories)
         .storageAccessConfigProvider(storageAccessConfigProvider)
         .eventAttributeMap(eventAttributeMap)
+        .metricsReporter(metricsReporter)
+        .clock(clock)
         .build();
   }
 }
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogHandlerAuthzTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogHandlerAuthzTest.java
index 999870412..aeea4abb2 100644
--- 
a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogHandlerAuthzTest.java
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogHandlerAuthzTest.java
@@ -37,7 +37,15 @@ import org.apache.iceberg.catalog.Catalog;
 import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.iceberg.expressions.Expressions;
 import org.apache.iceberg.io.FileIO;
+import org.apache.iceberg.metrics.CommitMetrics;
+import org.apache.iceberg.metrics.CommitMetricsResult;
+import org.apache.iceberg.metrics.CommitReport;
+import org.apache.iceberg.metrics.ImmutableCommitReport;
+import org.apache.iceberg.metrics.ImmutableScanReport;
+import org.apache.iceberg.metrics.ScanMetrics;
+import org.apache.iceberg.metrics.ScanMetricsResult;
 import org.apache.iceberg.rest.requests.CommitTransactionRequest;
 import org.apache.iceberg.rest.requests.CreateNamespaceRequest;
 import org.apache.iceberg.rest.requests.CreateTableRequest;
@@ -45,6 +53,7 @@ import org.apache.iceberg.rest.requests.CreateViewRequest;
 import org.apache.iceberg.rest.requests.ImmutableCreateViewRequest;
 import org.apache.iceberg.rest.requests.RegisterTableRequest;
 import org.apache.iceberg.rest.requests.RenameTableRequest;
+import org.apache.iceberg.rest.requests.ReportMetricsRequest;
 import org.apache.iceberg.rest.requests.UpdateNamespacePropertiesRequest;
 import org.apache.iceberg.rest.requests.UpdateTableRequest;
 import org.apache.iceberg.view.ImmutableSQLViewRepresentation;
@@ -2189,4 +2198,84 @@ public abstract class 
AbstractIcebergCatalogHandlerAuthzTest extends PolarisAuth
             newWrapper()
                 .loadTable(TABLE_NS1A_2, "all")); // Load table requires 
different privileges
   }
+
+  @Test
+  public void testReportReadMetricsSufficientPrivileges() {
+    ImmutableScanReport report =
+        ImmutableScanReport.builder()
+            .tableName(TABLE_NS1A_1.name())
+            .snapshotId(123L)
+            .schemaId(456)
+            .projectedFieldIds(List.of(1, 2, 3))
+            .projectedFieldNames(List.of("f1", "f2", "f3"))
+            .filter(Expressions.alwaysTrue())
+            .scanMetrics(ScanMetricsResult.fromScanMetrics(ScanMetrics.noop()))
+            .build();
+    ReportMetricsRequest request = ReportMetricsRequest.of(report);
+    doTestSufficientPrivileges(
+        List.of(PolarisPrivilege.TABLE_READ_DATA, 
PolarisPrivilege.CATALOG_MANAGE_CONTENT),
+        () -> newWrapper().reportMetrics(TABLE_NS1A_1, request),
+        null /* cleanupAction */);
+  }
+
+  @Test
+  public void testReportReadMetricsInsufficientPrivileges() {
+    ImmutableScanReport report =
+        ImmutableScanReport.builder()
+            .tableName(TABLE_NS1A_1.name())
+            .snapshotId(123L)
+            .schemaId(456)
+            .projectedFieldIds(List.of(1, 2, 3))
+            .projectedFieldNames(List.of("f1", "f2", "f3"))
+            .filter(Expressions.alwaysTrue())
+            .scanMetrics(ScanMetricsResult.fromScanMetrics(ScanMetrics.noop()))
+            .build();
+    ReportMetricsRequest request = ReportMetricsRequest.of(report);
+    doTestInsufficientPrivileges(
+        List.of(
+            PolarisPrivilege.TABLE_READ_PROPERTIES,
+            PolarisPrivilege.TABLE_FULL_METADATA,
+            PolarisPrivilege.TABLE_CREATE,
+            PolarisPrivilege.TABLE_LIST,
+            PolarisPrivilege.TABLE_DROP),
+        () -> newWrapper().reportMetrics(TABLE_NS1A_1, request));
+  }
+
+  @Test
+  public void testReportWriteMetricsSufficientPrivileges() {
+    CommitReport commitReport =
+        ImmutableCommitReport.builder()
+            .tableName(TABLE_NS1A_1.name())
+            .snapshotId(23L)
+            .operation("DELETE")
+            .sequenceNumber(4L)
+            .commitMetrics(CommitMetricsResult.from(CommitMetrics.noop(), 
Map.of()))
+            .build();
+    ReportMetricsRequest request = ReportMetricsRequest.of(commitReport);
+    doTestSufficientPrivileges(
+        List.of(PolarisPrivilege.TABLE_WRITE_DATA, 
PolarisPrivilege.CATALOG_MANAGE_CONTENT),
+        () -> newWrapper().reportMetrics(TABLE_NS1A_1, request),
+        null /* cleanupAction */);
+  }
+
+  @Test
+  public void testReportWriteMetricsInsufficientPrivileges() {
+    CommitReport commitReport =
+        ImmutableCommitReport.builder()
+            .tableName(TABLE_NS1A_1.name())
+            .snapshotId(23L)
+            .operation("DELETE")
+            .sequenceNumber(4L)
+            .commitMetrics(CommitMetricsResult.from(CommitMetrics.noop(), 
Map.of()))
+            .build();
+    ReportMetricsRequest request = ReportMetricsRequest.of(commitReport);
+    doTestInsufficientPrivileges(
+        List.of(
+            PolarisPrivilege.TABLE_READ_PROPERTIES,
+            PolarisPrivilege.TABLE_FULL_METADATA,
+            PolarisPrivilege.TABLE_CREATE,
+            PolarisPrivilege.TABLE_LIST,
+            PolarisPrivilege.TABLE_DROP),
+        () -> newWrapper().reportMetrics(TABLE_NS1A_1, request));
+  }
 }
diff --git 
a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
 
b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
index e18e8207f..8c1ab641a 100644
--- 
a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
+++ 
b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
@@ -345,18 +345,15 @@ public record TestServices(
                   .externalCatalogFactories(externalCatalogFactory)
                   .storageAccessConfigProvider(storageAccessConfigProvider)
                   .eventAttributeMap(eventAttributeMap)
+                  .metricsReporter(new DefaultMetricsReporter())
+                  .clock(clock)
                   .build();
             }
           };
 
       IcebergCatalogAdapter catalogService =
           new IcebergCatalogAdapter(
-              callContext,
-              new DefaultCatalogPrefixParser(),
-              reservedProperties,
-              new DefaultMetricsReporter(),
-              Clock.systemUTC(),
-              handlerFactory);
+              callContext, new DefaultCatalogPrefixParser(), 
reservedProperties, handlerFactory);
 
       // Optionally wrap with event delegator
       IcebergRestCatalogApiService finalRestCatalogService = catalogService;

Reply via email to