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;