(feat) Implement metrics rest api - #4115
Conversation
1d37c50 to
ac66c27
Compare
dimas-b
left a comment
There was a problem hiding this comment.
LGTM with one minor remaining comment 😅
@sungwy , @sneethiraj : FYI about the new AuthZ operation.
flyingImer
left a comment
There was a problem hiding this comment.
The direction looks right to me
Two structural observations:
-
With this PR, MetricsPersistence grows from 2 write methods to 4 (read + write). It's marked @beta and the javadoc calls it a "Service Provider Interface." But it lives on BasePersistence, which only local DB backends implement. NoSqlMetaStoreManager and RemotePolarisMetaStoreManager go through empty BasePersistence implementations, so these methods are permanently no-op for them. Meanwhile, the actual SPI interfaces (PolarisMetricsReporter, PolarisMetricsManager) have no annotation at all. The @beta signal is on the wrong layer IIUC
-
The write path enters through PolarisMetricsManager on MetaStoreManager, but this read path bypasses that layer and goes straight to BasePersistence via callContext.getMetaStore(). If we want the metrics read API to work for non-JDBC backends, it would need a MetaStoreManager-level entry point, same as writes.
Not blocking on this. I think the question of where metrics persistence should sit architecturally is worth a discussion on dev@.
Thank you. I have added About the second point, thank you. Would this mean a read method to |
Thanks for adding @beta. Reads should go through MetaStoreManager too, same as writes. If reads stay on BasePersistence, non-JDBC backends can't implement the read API at all. I'd prefer fixing that in this PR so the read path ships with the same layering as writes. Separately, the persistence schema discussion on dev@ is still open. A follow-up issue linking to that thread would help track it. |
Pushed a commit (and rebased against updated main). For the persistence schema discussion — I'll open a follow-up issue linking to the dev@ thread once there's a message to reference. Happy to do that now if you can share the thread link (I don't have it handy). Please let me know. |
flyingImer
left a comment
There was a problem hiding this comment.
Are you planning to merge as-is now that Dmitri approved, or is there another round? Asking because the May 7 metrics sync landed on a few directional items that touch the schema and SPI shape here. Left some questions inline.
|
@obelix74 : it looks like this PR got a lot of conflicts 🤷 |
Resolved all conflicts and push. Rebased against main. |
flyingImer
left a comment
There was a problem hiding this comment.
Thanks for continuing to push this forward. The direction still looks good to me: exposing persisted metrics through a beta read API, using a stable response envelope, keeping the API in an extension module, and routing reads through table-scoped authz all make sense.
One thing I would still like to clarify before this merges is sequencing with the metrics SPI/schema work we discussed after the May 7 sync.
From the previous review thread, I think we already converged on a few points:
- the current
MetricsPersistence/PolarisMetricsManagerlayering is transitional, and #4397 is expected to move metrics persistence out of the old aggregatedBasePersistenceshape; - the current
scan_metrics_report/commit_metrics_reportsplit is also transitional, with the follow-up direction being a single metrics report model/table with a metric type discriminator; listScanMetrics/listCommitMetricsare therefore likely interim API/SPI shapes, and may collapse or be rerouted when the schema/SPI consolidation happens.
I don't think this PR has to solve all of that before it can make progress. But I do think we should avoid accidentally standardizing the transitional shape just because this PR is ready first.
Could we make the sequencing explicit before merge? For example, either rebase on #4397 if that lands first, or link a concrete follow-up that tracks:
- moving metrics persistence to the standalone SPI shape,
- consolidating the metrics schema/model,
- deciding whether the per-type list methods remain public SPI surface or collapse behind a typed query API.
Added extension for publishing Polaris events to a
Kafka topic.
Application properties are:
polaris.event-listener.types=kafka
polaris.event-listener.kafka.topic
polaris.event-listener.kafka.synchronous-mode
polaris.event-listener.kafka.properties
polaris.event-listener.kafka.properties is used to pass
a set of properties through to the Kafka producer, for
example bootstrap.server.
The Kafka ProducerRecord key is the UUID from the PolarisEvent
Metadata EventID and the value is a JSON key-value map encoded
as a String.
JSON message is generated in helper class to support future
reuse.
Tests use Kafka test container.
Fixes apache#4115
Added extension for publishing Polaris events to a
Kafka topic.
Application properties are:
polaris.event-listener.types=kafka
polaris.event-listener.kafka.topic
polaris.event-listener.kafka.synchronous-mode
polaris.event-listener.kafka.properties
polaris.event-listener.kafka.properties is used to pass
a set of properties through to the Kafka producer, for
example bootstrap.server.
The Kafka ProducerRecord key is the UUID from the PolarisEvent
Metadata EventID and the value is a JSON key/value map encoded
as a String. TableIdentifiers are encoded as arrays of Strings.
JSON message is generated in helper class to support future
reuse.
Tests use Kafka test container.
Fixes apache#4115
Added extension for publishing Polaris events to a
Kafka topic.
Application properties are:
polaris.event-listener.types=kafka
polaris.event-listener.kafka.topic
polaris.event-listener.kafka.synchronous-mode
polaris.event-listener.kafka.properties
polaris.event-listener.kafka.properties is used to pass
a set of properties through to the Kafka producer, for
example bootstrap.server.
The Kafka ProducerRecord key is the UUID from the PolarisEvent
Metadata EventID and the value is a JSON key/value map encoded
as a String. TableIdentifiers are encoded as arrays of Strings.
JSON message is generated in helper class to support future
reuse.
Tests use Kafka test container.
Fixes apache#4115
Added extension for publishing Polaris events to a
Kafka topic.
Application properties are:
polaris.event-listener.types=kafka
polaris.event-listener.kafka.topic
polaris.event-listener.kafka.synchronous-mode
polaris.event-listener.kafka.properties
polaris.event-listener.kafka.properties is used to pass
a set of properties through to the Kafka producer, for
example bootstrap.server.
The Kafka ProducerRecord key is the UUID from the PolarisEvent
Metadata EventID and the value is a JSON key/value map encoded
as a String. TableIdentifiers are encoded as arrays of Strings.
JSON message is generated in helper class to support future
reuse.
Tests use Kafka test container.
Fixes apache#4115
Added extension for publishing Polaris events to a
Kafka topic.
Application properties are:
polaris.event-listener.types=kafka
polaris.event-listener.kafka.topic
polaris.event-listener.kafka.synchronous-mode
polaris.event-listener.kafka.properties
polaris.event-listener.kafka.properties is used to pass
a set of properties through to the Kafka producer, for
example bootstrap.server.
The Kafka ProducerRecord key is the UUID from the PolarisEvent
Metadata EventID and the value is a JSON key/value map encoded
as a String. TableIdentifiers are encoded as arrays of Strings.
JSON message is generated in helper class to support future
reuse.
Tests use Kafka test container.
Fixes apache#4115
|
Closed by mistake |
| Page<ScanMetricsRecord> listScanReports( | ||
| long catalogId, | ||
| long tableId, | ||
| @Nullable Long snapshotId, | ||
| @Nullable String principalName, | ||
| @Nullable Long timestampFrom, | ||
| @Nullable Long timestampTo, | ||
| @NonNull PageToken pageToken); | ||
|
|
||
| /** | ||
| * Lists persisted commit metrics reports for the given table, applying the supplied filters and | ||
| * returning at most one page of results. | ||
| */ | ||
| Page<CommitMetricsRecord> listCommitReports( | ||
| long catalogId, | ||
| long tableId, | ||
| @Nullable Long snapshotId, | ||
| @Nullable String principalName, | ||
| @Nullable Long timestampFrom, | ||
| @Nullable Long timestampTo, | ||
| @NonNull PageToken pageToken); | ||
| } |
There was a problem hiding this comment.
should scan and commit just being enveloped rather than individual SPI method?
could you please remind me the rationale of you choosing one over the other?
There was a problem hiding this comment.
Good question — the two-method shape here mirrors MetricsPersistence (the write-side SPI this interface is the read counterpart to), which also has two separate methods, writeScanReport/writeCommitReport, each taking its own concrete record type rather than an envelope.
The enveloping in IcebergMetricsReporter/MetricsReportEnvelope solves a different problem: it's the single ingestion entry point that receives whichever Iceberg report type shows up at runtime, so it needs one polymorphic signature with an explicit MetricType discriminator for callers to branch on. PersistingMetricsReporter (which implements IcebergMetricsReporter) unwraps that envelope and dispatches to the strongly-typed MetricsPersistence write methods.
MetricsQuerySpi doesn't have that "single arrival point" problem — callers already know whether they want scan or commit reports (it's the metricType query param), so there's no runtime type to discriminate. Keeping listScanReports/listCommitReports as separate, strongly-typed methods avoids introducing a union/envelope type on the read side purely to unwrap it again one line later, and keeps it symmetric with the write SPI it's paired with.
Happy to reconsider if you see a concrete benefit to enveloping here that I'm missing.
There was a problem hiding this comment.
Good catch — collapsed listScanReports/listCommitReports into a single enveloped listReports(MetricType, ...) returning Page<? extends MetricsRecordIdentity>, matching the discriminated envelope the REST layer already uses (ListMetricsResponse oneOf). See 4a0b224.
There was a problem hiding this comment.
I agree with collapsing this to one envelope-based method. The remaining gap is that MetricType and the returned Page are still unrelated values, so a provider can pair COMMIT with scan records and the service has to recover the relationship through casts. Could listMetrics return a sealed QueryResult with typed ScanResult and CommitResult variants, with metricType derived from the variant? That keeps one entry point, makes an invalid envelope fail to compile, and lets the service use an exhaustive switch without casts. One check at the SPI seam can verify that the returned variant matches the requested runtime MetricType.
For example:
sealed interface QueryResult permits ScanResult, CommitResult {
MetricType metricType();
}
record ScanResult(Page<ScanMetricsRecord> reports)
implements QueryResult {
@Override
public MetricType metricType() {
return MetricType.S.SCAN;
}
}
record CommitResult(Page<CommitMetricsRecord> reports)
implements QueryResult {
@Override
public MetricType metricType() {
return MetricType.COMMIT;
}
}
QueryResult listReports(
MetricType metricType,
long catalogId,
List<Long> tableIds,
Long snapshotId,
Long timestampFrom,
Long timestampTo,
PageToken pageToken);
This way will safeguard such:
new CommitResult(scanPage); // compile error
service no longer needs to cast:
QueryResult result = provider.listReports(type, ...);
checkState(
result.metricType() == type,
"Provider returned %s for %s",
result.metricType(),
type);
return switch (result) {
case ScanResult scan ->
toScanResponse(scan.reports());
case CommitResult commit ->
toCommitResponse(commit.reports());
};
no op can simply be:
return switch (metricType) {
case SCAN ->
new ScanResult(Page.fromItems(List.of()));
case COMMIT ->
new CommitResult(Page.fromItems(List.of()));
};
There was a problem hiding this comment.
Implemented as suggested in 975c01c (refactor(metrics): make MetricsQuerySpi.listReports return a typed sealed QueryResult):
listReportsnow returns a sealedQueryResultwithScanResult/CommitResultvariants, each carrying a strongly-typedPage<ScanMetricsRecord>/Page<CommitMetricsRecord>and derivingmetricType()from the variant.MetricsReportsServicechecksresult.metricType() == typeand then switches exhaustively overScanResult/CommitResult— no casts.NoOpMetricsQueryreturnsnew ScanResult(...)/new CommitResult(...)directly via aswitchon the requestedmetricType, matching your proposed no-op shape.
Thanks for the detailed sketch — implemented essentially as written.
There was a problem hiding this comment.
It's intentional, not merge noise — this is the direct answer to your earlier ask on this thread: #4115 (comment) ("Could we provide an operator-consumable service-definition update and test LIST_TABLE_METRICS against that exact artifact?").
The problem it closes: authz_tests/dev_polaris.json's serviceDef is what RangerPolarisAuthorizerTest actually authorizes LIST_TABLE_METRICS against, but nothing shipped for operators to register with a real Ranger Admin — so the tested access types and the ones an operator could actually grant could silently diverge. This test asserts the new extensions/auth/ranger/src/main/resources/polaris-ranger-servicedef.json (the artifact I added for operators, referenced from the README) is byte-identical to that fixture's serviceDef, so any future edit to one without the other fails CI instead of drifting unnoticed.
Happy to fold it into RangerPolarisAuthorizerTest as one more @Test method instead of a separate class if you'd rather keep the file count down — let me know which you prefer.
There was a problem hiding this comment.
(Apologies — my previous reply above landed on this thread by mistake; it was meant for the RangerServiceDefConsistencyTest thread. Posting the correct reply there now: #4115 (comment))
| PageToken pt = PageToken.build(pageToken, pageSize, () -> true); | ||
| MetricsQuerySpi provider = queryProvider.get(); | ||
|
|
||
| if ("commit".equalsIgnoreCase(metricType)) { |
There was a problem hiding this comment.
Spec says metricType is required, enum: [scan, commit], 400 otherwise. Code checks only equalsIgnoreCase("commit"), so any other value including a typo falls to the scan branch and succeeds. I see it as a bug
There was a problem hiding this comment.
Confirmed and fixed in da04347 — metricType is now strictly validated (throws IllegalArgumentException / 400 for anything other than scan/commit), covered by a new test.
| PolarisResolutionManifest manifest = resolveAndAuthorizeTableMetrics(catalogName, identifier); | ||
|
|
||
| CatalogEntity catalogEntity = manifest.getResolvedCatalogEntity(); | ||
| long catalogId = catalogEntity != null ? catalogEntity.getId() : -1L; |
There was a problem hiding this comment.
info: is -1L a safe fallback?
There was a problem hiding this comment.
Agreed, not safe — replaced the -1L sentinel with a checkNotNull precondition in da04347, matching the fail-fast pattern used in IcebergCatalogHandler.
|
|
||
| openapi: 3.0.3 | ||
| info: | ||
| title: Apache Polaris Metrics Reports API |
There was a problem hiding this comment.
If we're aiming to have a REST API for metrics queries, I'd suggest moving it to /spec. REST specs should be global for the entire Polaris project, so we shouldn't have separate REST specs for metric queries spread across different extensions.
| minimum: 1 | ||
| default: 100 | ||
| description: Maximum number of results to return per page | ||
| - name: snapshotId |
| type: integer | ||
| format: int64 | ||
| description: Filter results to a specific snapshot ID | ||
| - name: principalName |
There was a problem hiding this comment.
What is the use case of having a principalName filter?
| default: localhost | ||
|
|
||
| paths: | ||
| /catalogs/{catalogName}/namespaces/{namespace}/tables/{table}: |
There was a problem hiding this comment.
Do we need catalog/ns/table in the URL path? Hardcoding this hierarchy prevents querying metrics across multiple catalog objects in a single request.
A POST-based endpoint (e.g., accepting a QueryMetricsRequest payload) might be more flexible here, similar to the event query pattern in the IRC event spec (Apache Iceberg PR #12584).
Introduce the read/query surface for persisted Iceberg metrics, layered so the query SPI is optional and the HTTP handler is a thin adaptor: - Add the OpenAPI spec spec/metrics-reports-service.yml and the generated api/metrics-reports-service module (api under org.apache.polaris.service .metrics.api, models under org.apache.polaris.core.metrics.api.model). - Add MetricsQuerySpi in a new extensions/metrics-reports/spi module (listScanReports/listCommitReports over core record types + PageToken). - Add the REST handler MetricsReportsService in runtime/service as a thin HTTP-to-SPI adaptor: resolves catalog/namespace/table names to ids, authorizes, and delegates to MetricsQuerySpi; returns HTTP 501 when no durable query backend is installed. - Add read-path authorization in polaris-core: TABLE_READ_METRICS privilege and LIST_TABLE_METRICS operation, wired through PolarisAuthorizerImpl and RbacOperationSemantics; mirror the mapping in the Ranger extension and its authz test fixtures. - Docs + changelog for the beta API and the new privilege. The JDBC implementation of MetricsQuerySpi follows in PR2.
…query default (PR1) Address the community review of PR1 to match the agreed intent: - Relocate the metrics query REST API to extensions: the OpenAPI spec and the generated module now live under extensions/metrics-reports/api (coordinate polaris-extensions-metrics-reports-api), signalling an optional, extension-scoped feature. runtime/service still compile-depends on it for the thin handler. - Add a no-op default MetricsQuerySpi (NoOpMetricsQuery) in extensions/metrics-reports/base, annotated @io.quarkus.arc.DefaultBean so it is active only when no durable backend contributes a MetricsQuerySpi. The read path now always resolves a provider and returns an empty page instead of HTTP 501 when no backend is installed. - Simplify MetricsReportsService accordingly (drop the 501 branch) and update its unit tests to assert the empty-page behavior.
…R1 review) - listTableMetrics silently treated any non-"commit" metricType value as "scan" (e.g. a typo), even though the spec requires metricType to be one of [scan, commit] and reject anything else with 400. Reject values that don't match "scan" or "commit". - Replace the -1L catalogId sentinel (used when the resolved catalog entity was unexpectedly null) with a checkNotNull precondition, matching the fail-fast pattern already used in IcebergCatalogHandler.
…ports method (PR1 review) MetricsQuerySpi previously exposed separate listScanReports/listCommitReports methods even though the REST layer already models the response as a single enveloped, discriminated type. Replace both with a single listReports(MetricType, ...) method returning Page<? extends MetricsRecordIdentity>, matching the existing oneOf/discriminator envelope pattern used by the API spec.
… query (PR1 review) Address review feedback on apache#4115: - Move metrics-reports-service.yml into the shared /spec directory instead of keeping a REST spec scoped to one extension. - Replace the per-table GET endpoint with a POST /catalogs/{catalogName}/metrics/query endpoint accepting a QueryMetricsRequest body, so a single request can span multiple tables in a catalog (mirrors the Iceberg events query pattern). Each report's object now carries a table reference to disambiguate results. - Drop the principalName filter parameter; no concrete use case was given for it.
flyingImer
left a comment
There was a problem hiding this comment.
The API direction looks right, and the latest POST multi-table shape addresses the API-shape feedback. I don't think this is merge-ready yet. Two contract blockers and two narrower AuthZ/scope questions are inline. Please also rebase onto main and refresh the stacked-PR description before the next round.
| public class NoOpMetricsQuery implements MetricsQuerySpi { | ||
|
|
||
| @Override | ||
| public Page<? extends MetricsRecordIdentity> listReports( |
There was a problem hiding this comment.
listMetrics? not sure if reports term makes the most sense here
| SUPER_PRIVILEGES.putAll( | ||
| TABLE_READ_METRICS, | ||
| List.of(CATALOG_MANAGE_CONTENT, TABLE_FULL_METADATA, TABLE_READ_DATA, TABLE_READ_METRICS)); | ||
| SUPER_PRIVILEGES.putAll( |
There was a problem hiding this comment.
TABLE_READ_DATA currently subsumes TABLE_READ_METRICS here. That lets every data reader query principalName, requestId, and trace/span IDs for all stored reports on the table without an explicit metrics grant. Is that intended? Since this PR adds a dedicated privilege for operational metadata, my bias is to keep it independently grantable unless we explicitly want that disclosure in the TABLE_READ_DATA contract.
| TABLE_READ_METRICS( | ||
| 103, | ||
| PolarisEntityType.TABLE_LIKE, | ||
| List.of(PolarisEntitySubType.ICEBERG_TABLE, PolarisEntitySubType.GENERIC_TABLE), |
There was a problem hiding this comment.
Maybe I missed this earlier: TABLE_READ_METRICS is grantable on generic tables here, but the API and current ingestion path are Iceberg-only. A generic-table request can authorize successfully and then return no reports, which makes the contract look broader than the implementation. Could we restrict this privilege and the service lookup to ICEBERG_TABLE until generic-table metrics exist?
…READ_DATA implication (PR1 review) TABLE_READ_METRICS gates operational metadata (principal name, request id, otel trace/span ids) on metrics reports, separate from table data. It was listed as implied by TABLE_READ_DATA, which let any data reader see that metadata without an explicit metrics grant; drop that implication so it's only implied by TABLE_FULL_METADATA (and CATALOG_MANAGE_CONTENT). The privilege was also grantable on GENERIC_TABLE, but metrics ingestion and the query API only cover Iceberg tables today, so a generic-table request could authorize successfully and then return no reports, making the contract look broader than the implementation. Restrict the privilege and the service's table resolution to ICEBERG_TABLE until generic-table metrics exist; a generic-table request now fails with 404 instead of a misleadingly-successful empty result.
| private static final String TABLE_WRITE_PROPERTIES = "table-properties-write"; | ||
| private static final String TABLE_READ_DATA = "table-data-read"; | ||
| private static final String TABLE_WRITE_DATA = "table-data-write"; | ||
| private static final String TABLE_READ_METRICS = "table-metrics-read"; |
There was a problem hiding this comment.
LIST_TABLE_METRICS now requests table-metrics-read, but Polaris pins Ranger 2.9.0 and neither its Polaris service definition nor Ranger master defines that access type. Ranger validates policy access types against the service definition, while this PR adds it only to embedded test fixtures and does not add it to the table resource’s accessTypeRestrictions. Operators therefore cannot grant the new permission through the shipped definition. Could we provide an operator-consumable service-definition update and test LIST_TABLE_METRICS against that exact artifact?
There was a problem hiding this comment.
Addressed in 32d7bf7351d (fix(metrics): ship operator-consumable Ranger service def, close data-read/metrics-read gap):
- Extracted the
serviceDefintoextensions/auth/ranger/src/main/resources/polaris-ranger-servicedef.json, the artifact operators register with Ranger Admin (README updated with the registration steps). - Added
table-metrics-readto thetableresource'saccessTypeRestrictions, which was indeed missing. - Added
RangerServiceDefConsistencyTest, which fails the build if that shipped artifact ever drifts from theserviceDefinauthz_tests/dev_polaris.json— the same fixtureRangerPolarisAuthorizerTestexercises forLIST_TABLE_METRICSand the rest of the authz suite. So the operator-facing service definition and the tested one are now provably the same artifact.
| Page<ScanMetricsRecord> listScanReports( | ||
| long catalogId, | ||
| long tableId, | ||
| @Nullable Long snapshotId, | ||
| @Nullable String principalName, | ||
| @Nullable Long timestampFrom, | ||
| @Nullable Long timestampTo, | ||
| @NonNull PageToken pageToken); | ||
|
|
||
| /** | ||
| * Lists persisted commit metrics reports for the given table, applying the supplied filters and | ||
| * returning at most one page of results. | ||
| */ | ||
| Page<CommitMetricsRecord> listCommitReports( | ||
| long catalogId, | ||
| long tableId, | ||
| @Nullable Long snapshotId, | ||
| @Nullable String principalName, | ||
| @Nullable Long timestampFrom, | ||
| @Nullable Long timestampTo, | ||
| @NonNull PageToken pageToken); | ||
| } |
There was a problem hiding this comment.
I agree with collapsing this to one envelope-based method. The remaining gap is that MetricType and the returned Page are still unrelated values, so a provider can pair COMMIT with scan records and the service has to recover the relationship through casts. Could listMetrics return a sealed QueryResult with typed ScanResult and CommitResult variants, with metricType derived from the variant? That keeps one entry point, makes an invalid envelope fail to compile, and lets the service use an exhaustive switch without casts. One check at the SPI seam can verify that the returned variant matches the requested runtime MetricType.
For example:
sealed interface QueryResult permits ScanResult, CommitResult {
MetricType metricType();
}
record ScanResult(Page<ScanMetricsRecord> reports)
implements QueryResult {
@Override
public MetricType metricType() {
return MetricType.S.SCAN;
}
}
record CommitResult(Page<CommitMetricsRecord> reports)
implements QueryResult {
@Override
public MetricType metricType() {
return MetricType.COMMIT;
}
}
QueryResult listReports(
MetricType metricType,
long catalogId,
List<Long> tableIds,
Long snapshotId,
Long timestampFrom,
Long timestampTo,
PageToken pageToken);
This way will safeguard such:
new CommitResult(scanPage); // compile error
service no longer needs to cast:
QueryResult result = provider.listReports(type, ...);
checkState(
result.metricType() == type,
"Provider returned %s for %s",
result.metricType(),
type);
return switch (result) {
case ScanResult scan ->
toScanResponse(scan.reports());
case CommitResult commit ->
toCommitResponse(commit.reports());
};
no op can simply be:
return switch (metricType) {
case SCAN ->
new ScanResult(Page.fromItems(List.of()));
case COMMIT ->
new CommitResult(Page.fromItems(List.of()));
};
| { "itemId": 29, "name": "table-drop", "label": "Table Drop", "category": "DELETE" }, | ||
| { "itemId": 30, "name": "table-list", "label": "Table List", "category": "READ" }, | ||
| { "itemId": 31, "name": "table-data-read", "label": "Table Data Read", "category": "READ", "impliedGrants": [ "table-list", "table-properties-read" ] }, | ||
| { "itemId": 31, "name": "table-data-read", "label": "Table Data Read", "category": "READ", "impliedGrants": [ "table-list", "table-properties-read", "table-metrics-read" ] }, |
There was a problem hiding this comment.
IIUC, Ranger and native RBAC give different results for the same conceptual grant: a user with only table-data-read can access table metrics through Ranger, but not through native RBAC. In Ranger, this happens because table-data-read implies table-metrics-read. Could we remove that implication from both table-data-read and table-data-write and add a negative test showing that data access alone does not authorize LIST_TABLE_METRICS?
There was a problem hiding this comment.
Addressed in 32d7bf7351d, same commit as the sibling thread above:
- Dropped
table-metrics-readfromtable-data-read's andtable-data-write'simpliedGrantsin bothauthz_tests/dev_polaris.jsonand the intTest fixture, matching the native RBAC side (table-metrics-readis now only implied bytable-metadata-full/catalog-content-managethere too). - Added a policy item granting only
table-data-read/table-data-writeto a new test principal (dataonly1) in both fixtures. - Added a negative
LIST_TABLE_METRICStest case fordataonly1(expectedisAllowed: false) alongside a positive case foradmin1, intests_authz_table.json.
|
@obelix74 : Do you have time to refresh this PR? |
Working on it. |
…-read/metrics-read gap (PR1 review) Ranger validates policy access types against the registered service definition. dev_polaris.json under test resources was the only place table-metrics-read existed, so operators had no artifact to register with Ranger Admin, and the table resource's own accessTypeRestrictions never listed table-metrics-read in the first place. Extract the serviceDef into extensions/auth/ranger/src/main/resources/ polaris-ranger-servicedef.json as the artifact operators register, add table-metrics-read to the table resource's accessTypeRestrictions, and add RangerServiceDefConsistencyTest so the shipped artifact and the fixture RangerPolarisAuthorizerTest runs against can never drift apart again. Also drop table-metrics-read from table-data-read/table-data-write's impliedGrants in both the unit-test and intTest fixtures, matching the native RBAC side (table-metrics-read is only implied by table-metadata-full and catalog-content-manage there); a data reader/writer should not automatically see metrics report metadata (principal name, request id, otel ids) via Ranger either. Add a negative LIST_TABLE_METRICS test case for a principal with only table-data-read/table-data-write access. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…aled QueryResult (PR1 review) MetricType and the returned Page were unrelated values, so a provider could pair COMMIT with scan records and the service had to recover the relationship through unchecked casts. Introduce a sealed QueryResult with ScanResult/CommitResult variants that each carry a strongly-typed Page and derive their MetricType from the variant, so an invalid pairing fails to compile instead of failing at runtime. MetricsReportsService checks that the returned variant matches the requested MetricType and then switches exhaustively over ScanResult/ CommitResult with no casts. NoOpMetricsQuery now returns the matching variant directly instead of an untyped empty Page. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# Conflicts: # bom/build.gradle.kts # gradle/projects.main.properties
| "serviceDef": { | ||
| "name": "polaris", | ||
| "displayName": "Polaris (draft)", | ||
| "displayName": "Apache Polaris", |
There was a problem hiding this comment.
Is this change related to metrics?
There was a problem hiding this comment.
Good catch, not related to metrics at all — that was collateral from making my new RangerServiceDefConsistencyTest's byte-equality check look tidier. Reverted in 5b55c34 (both here and in the intTest fixture / the new polaris-ranger-servicedef.json), back to Polaris (draft).
| * keeping the two identical is what makes those tests representative of the artifact operators | ||
| * register with Ranger Admin. | ||
| */ | ||
| public class RangerServiceDefConsistencyTest { |
There was a problem hiding this comment.
This looks like a spurious change in this PR 🤔
There was a problem hiding this comment.
It's intentional, not merge noise — this is the direct answer to your earlier ask on the sibling thread: #4115 (comment) ("Could we provide an operator-consumable service-definition update and test LIST_TABLE_METRICS against that exact artifact?").
The problem it closes: authz_tests/dev_polaris.json's serviceDef is what RangerPolarisAuthorizerTest actually authorizes LIST_TABLE_METRICS against, but nothing shipped for operators to register with a real Ranger Admin — so the tested access types and the ones an operator could actually grant could silently diverge. This test asserts the new extensions/auth/ranger/src/main/resources/polaris-ranger-servicedef.json (the artifact I added for operators, referenced from the README) is byte-identical to that fixture's serviceDef, so any future edit to one without the other fails CI instead of drifting unnoticed.
Happy to fold it into RangerPolarisAuthorizerTest as one more @Test method instead of a separate class if you'd rather keep the file count down — let me know which you prefer.
…splayName tweak (post-merge) Merging upstream/main pulled in apache#5194 (Update CatalogHandler and PolarisAdminServier to authorize using AuthorizationIntent), which removed PolarisAuthorizer.authorizeOrThrow entirely. Migrate MetricsReportsService.resolveAndAuthorizeTableMetrics to build one AuthorizationRequest of SingleTargetAuthorizationIntent(LIST_TABLE_METRICS, ...) per table, call resolveAuthorizationInputs once, do the existing not-found checks against the manifest, then authorize(...).throwIfDenied() once for the whole batch (AND-combined, short-circuits on first deny) -- mirroring CatalogHandler.authorizeBatchTableLikeOperationOrThrow. Update MetricsReportsServiceTest's mocks to the authorize()/AuthorizationDecision style used elsewhere post-migration. Also revert the dev_polaris.json/polaris-ranger-servicedef.json "Polaris (draft)" -> "Apache Polaris" displayName tweak from the prior commit: unrelated to metrics, flagged in review (apache#4115 (comment)). spotlessApply also reflowed the TABLE_READ_METRICS javadoc comment in PolarisPrivilege.java by a couple of characters (newer google-java-format line-wrap width); included since it touches PR1's own new javadoc. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
What / Why
Part of the 3-PR split of the Iceberg metrics work (agreed with EJ, Dmitri, Anand):
Changes
extensions/metrics-reports/api(polaris-extensions-metrics-reports-api) — the metrics API ships as an optional, extension-scoped feature.MetricsQuerySpiinextensions/metrics-reports/spi; a no-op default (NoOpMetricsQuery,@DefaultBean) inextensions/metrics-reports/baseso the read path returns an empty page (HTTP 200) until a durable backend is installed.MetricsReportsServiceinruntime/service(resolves names→ids, authorizes, delegates toMetricsQuerySpi).polaris-core:TABLE_READ_METRICSprivilege +LIST_TABLE_METRICSoperation, wired through the authorizer/RBAC and mirrored in the Ranger extension.Testing
./gradlew buildand:polaris-runtime-service:intTest(Quarkus) pass.