This is an automated email from the ASF dual-hosted git repository.
dimas 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 b064aa379 feat(session-tags): Add realm label and per-field selection
for AWS STS session tags (#3823)
b064aa379 is described below
commit b064aa3795e5d956dbe2b707f3b1161ecdd01c1d
Author: Anand K Sankaran <[email protected]>
AuthorDate: Tue Feb 24 07:12:22 2026 -0800
feat(session-tags): Add realm label and per-field selection for AWS STS
session tags (#3823)
* feat(session-tags): Add realm label and per-field selection for AWS STS
session tags
Replaces the all-or-nothing INCLUDE_SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL and
INCLUDE_TRACE_ID_IN_SESSION_TAGS boolean flags with a single list-based
config
SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL that lets operators choose exactly
which
fields to include as session tags in AWS STS AssumeRole requests.
This gives operators control over how many of the 2048-character STS packed
policy budget their session tags consume, helping avoid the policy size
limit
errors described in #3243.
Supported fields: realm, catalog, namespace, table, principal, roles,
trace_id
- Add SessionTagField enum encapsulating tag key and context extraction per
field
- Add realm field to CredentialVendingContext (populated from RealmContext)
- Replace boolean feature flags with SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL
list config
- Update AwsSessionTagsBuilder.buildSessionTags() to accept
Set<SessionTagField>
- Update AwsCredentialsStorageIntegration and StorageCredentialCache to use
the new config
- Update StorageAccessConfigProvider to inject RealmContext and populate
realm in context
- Add tests for realm tag inclusion/exclusion
- Update CHANGELOG.md with new feature note
* Address review comments: typed List<String> for config, constant for
trace_id field name
* Address PR review comments
- Improve wording: 'effectively eliminates credential cache reuse'
(r2834779162)
- Remove inline import java.util.Optional::stream (r2834808079)
- Remove confusing statement about per-request fields (r2834811461)
- Refactor realm tag tests to share common code (r2834824634)
---------
Co-authored-by: Anand Kumar Sankaran <[email protected]>
---
CHANGELOG.md | 2 +-
.../polaris/core/config/FeatureConfiguration.java | 67 ++++++++----
.../core/storage/CredentialVendingContext.java | 7 ++
.../aws/AwsCredentialsStorageIntegration.java | 22 ++--
.../core/storage/aws/AwsSessionTagsBuilder.java | 70 +++----------
.../polaris/core/storage/aws/SessionTagField.java | 116 +++++++++++++++++++++
.../core/storage/cache/StorageCredentialCache.java | 12 ++-
.../aws/AwsCredentialsStorageIntegrationTest.java | 110 +++++++++++++++++--
.../catalog/io/StorageAccessConfigProvider.java | 28 +++--
.../org/apache/polaris/service/TestServices.java | 2 +-
10 files changed, 327 insertions(+), 109 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7fad73b9f..646b11a07 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -50,7 +50,7 @@ request adding CHANGELOG notes for breaking (!) changes and
possibly other secti
- Added `topologySpreadConstraints` support in Helm chart.
- Added `priorityClassName` support in Helm chart.
- Added support for including principal name in subscoped credentials.
`INCLUDE_PRINCIPAL_NAME_IN_SUBSCOPED_CREDENTIAL` (default: false) can be used
to toggle this feature. If enabled, cached credentials issued to one principal
will no longer be available for others.
-- Added support for including OpenTelemetry trace IDs in AWS STS session tags.
This requires session tags to be enabled via
`INCLUDE_SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL` and can be toggled with
`INCLUDE_TRACE_ID_IN_SESSION_TAGS` (default: false). Note: enabling trace IDs
disables credential caching (each request has a unique trace ID), which may
increase STS calls and latency.
+- Added per-field selection for AWS STS session tags in credential vending.
The new `SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL` configuration accepts a
comma-separated list of fields to include as session tags (supported: `realm`,
`catalog`, `namespace`, `table`, `principal`, `roles`, `trace_id`). This
replaces the previous `INCLUDE_SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL` and
`INCLUDE_TRACE_ID_IN_SESSION_TAGS` boolean flags. Selecting only the fields
needed helps avoid AWS STS packed policy siz [...]
- Added support for [Kubernetes Gateway API](https://gateway-api.sigs.k8s.io/)
to the Helm Chart.
- Added `hierarchical` flag to `AzureStorageConfigInfo` to allow more precise
SAS token down-scoping in ADLS when
the [hierarchical
namespace](https://learn.microsoft.com/en-us/azure/storage/blobs/data-lake-storage-namespace)
diff --git
a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java
b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java
index c3083e310..973d5b822 100644
---
a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java
+++
b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java
@@ -91,31 +91,52 @@ public class FeatureConfiguration<T> extends
PolarisConfiguration<T> {
.defaultValue(false)
.buildFeatureConfiguration();
- public static final FeatureConfiguration<Boolean>
INCLUDE_SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL =
- PolarisConfiguration.<Boolean>builder()
- .key("INCLUDE_SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL")
+ /**
+ * The set of fields that are supported as AWS STS session tag labels in
credential vending.
+ *
+ * <p>Supported values:
+ *
+ * <ul>
+ * <li>{@code realm} - The realm identifier for the request
+ * <li>{@code catalog} - The name of the catalog vending credentials
+ * <li>{@code namespace} - The namespace being accessed (dot-separated)
+ * <li>{@code table} - The table name being accessed
+ * <li>{@code principal} - The principal name requesting credentials
+ * <li>{@code roles} - Comma-separated list of activated principal roles
+ * <li>{@code trace_id} - OpenTelemetry trace ID (WARNING: disables
credential caching)
+ * </ul>
+ */
+ public static final List<String> SUPPORTED_SESSION_TAG_FIELDS =
+ List.<String>of("realm", "catalog", "namespace", "table", "principal",
"roles", "trace_id");
+
+ /**
+ * The config name for the trace ID session tag field. Included as a
constant because callers need
+ * to check for its presence specifically — enabling it populates the trace
ID into the credential
+ * vending context, which effectively eliminates credential cache reuse
since each request has a
+ * unique trace ID.
+ */
+ public static final String SESSION_TAG_FIELD_TRACE_ID = "trace_id";
+
+ public static final FeatureConfiguration<List<String>>
SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL =
+ PolarisConfiguration.<List<String>>builder()
+ .key("SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL")
.description(
- "If set to true, session tags (catalog, namespace, table,
principal, roles) will be included\n"
- + "in AWS STS AssumeRole requests for credential vending.
These tags appear in CloudTrail events,\n"
- + "enabling correlation between catalog operations and S3
data access.\n"
+ "A comma-separated list of fields to include as session tags in
AWS STS AssumeRole requests\n"
+ + "for credential vending. These tags appear in CloudTrail
events, enabling correlation between\n"
+ + "catalog operations and S3 data access. An empty list
(default) disables session tags entirely.\n"
+ "Requires the IAM role trust policy to allow
sts:TagSession action.\n"
- + "Note that enabling this feature may lead to degradation
in temporary credential caching as \n"
- + "catalog will no longer be able to reuse credentials for
different tables/namespaces/roles.")
- .defaultValue(false)
- .buildFeatureConfiguration();
-
- public static final FeatureConfiguration<Boolean>
INCLUDE_TRACE_ID_IN_SESSION_TAGS =
- PolarisConfiguration.<Boolean>builder()
- .key("INCLUDE_TRACE_ID_IN_SESSION_TAGS")
- .description(
- "If set to true (and
INCLUDE_SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL is also true), the OpenTelemetry\n"
- + "trace ID will be included as a session tag in AWS STS
AssumeRole requests. This enables\n"
- + "end-to-end correlation between catalog operations
(Polaris events), credential vending (CloudTrail),\n"
- + "and metrics reports from compute engines.\n"
- + "WARNING: Enabling this feature completely disables
credential caching because every request\n"
- + "has a unique trace ID. This may significantly increase
latency and STS API costs.\n"
- + "Consider leaving this disabled unless end-to-end tracing
correlation is critical.")
- .defaultValue(false)
+ + "\n"
+ + "Supported fields: "
+ + String.join(", ", SUPPORTED_SESSION_TAG_FIELDS)
+ + "\n"
+ + "\n"
+ + "Note: each additional field may contribute to AWS STS
packed policy size (max 2048 characters).\n"
+ + "Fields with long values (e.g. deeply nested namespaces)
can cause STS policy size limit errors.\n"
+ + "Choose only the fields needed for your CloudTrail
correlation requirements.\n"
+ + "\n"
+ + "WARNING: Including 'trace_id' effectively eliminates
credential cache reuse because every\n"
+ + "request has a unique trace ID. This may significantly
increase latency and STS API costs.")
+ .defaultValue(List.<String>of())
.buildFeatureConfiguration();
public static final FeatureConfiguration<Boolean> ALLOW_SETTING_S3_ENDPOINTS
=
diff --git
a/polaris-core/src/main/java/org/apache/polaris/core/storage/CredentialVendingContext.java
b/polaris-core/src/main/java/org/apache/polaris/core/storage/CredentialVendingContext.java
index 40c262726..464738c8c 100644
---
a/polaris-core/src/main/java/org/apache/polaris/core/storage/CredentialVendingContext.java
+++
b/polaris-core/src/main/java/org/apache/polaris/core/storage/CredentialVendingContext.java
@@ -29,6 +29,7 @@ import org.apache.polaris.immutables.PolarisImmutable;
* <p>When session tags are enabled, this context provides:
*
* <ul>
+ * <li>{@code realm} - The realm identifier for the request
* <li>{@code catalogName} - The name of the catalog vending credentials
* <li>{@code namespace} - The namespace/database being accessed (e.g.,
"db.schema")
* <li>{@code tableName} - The name of the table being accessed
@@ -44,6 +45,7 @@ public interface CredentialVendingContext {
// Default session tag keys for cloud provider credentials (e.g., AWS STS
session tags).
// These appear in cloud audit logs (e.g., CloudTrail) for correlation
purposes.
+ String TAG_KEY_REALM = "polaris:realm";
String TAG_KEY_CATALOG = "polaris:catalog";
String TAG_KEY_NAMESPACE = "polaris:namespace";
String TAG_KEY_TABLE = "polaris:table";
@@ -51,6 +53,9 @@ public interface CredentialVendingContext {
String TAG_KEY_ROLES = "polaris:roles";
String TAG_KEY_TRACE_ID = "polaris:trace_id";
+ /** The realm identifier for the request. */
+ Optional<String> realm();
+
/** The name of the catalog that is vending credentials. */
Optional<String> catalogName();
@@ -103,6 +108,8 @@ public interface CredentialVendingContext {
}
interface Builder {
+ Builder realm(Optional<String> realm);
+
Builder catalogName(Optional<String> catalogName);
Builder namespace(Optional<String> namespace);
diff --git
a/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java
b/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java
index 136d95cfa..e1bddbd26 100644
---
a/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java
+++
b/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java
@@ -23,11 +23,13 @@ import static
org.apache.polaris.core.storage.aws.AwsSessionTagsBuilder.buildSes
import jakarta.annotation.Nonnull;
import java.net.URI;
+import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.polaris.core.auth.PolarisPrincipal;
import org.apache.polaris.core.config.FeatureConfiguration;
@@ -97,8 +99,13 @@ public class AwsCredentialsStorageIntegration
boolean includePrincipalNameInSubscopedCredential =
realmConfig.getConfig(FeatureConfiguration.INCLUDE_PRINCIPAL_NAME_IN_SUBSCOPED_CREDENTIAL);
- boolean includeSessionTags =
-
realmConfig.getConfig(FeatureConfiguration.INCLUDE_SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL);
+ List<String> sessionTagFieldNames =
+
realmConfig.getConfig(FeatureConfiguration.SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL);
+ Set<SessionTagField> enabledSessionTagFields =
+ sessionTagFieldNames.stream()
+ .map(SessionTagField::fromConfigName)
+ .flatMap(Optional::stream)
+ .collect(Collectors.toCollection(() ->
EnumSet.noneOf(SessionTagField.class)));
String roleSessionName =
includePrincipalNameInSubscopedCredential
@@ -121,12 +128,13 @@ public class AwsCredentialsStorageIntegration
.toJson())
.durationSeconds(storageCredentialDurationSeconds);
- // Add session tags when the feature is enabled.
- // Note: The trace ID is controlled at the source
(StorageAccessConfigProvider).
- // If INCLUDE_TRACE_ID_IN_SESSION_TAGS is enabled, the context will
contain the trace ID.
- if (includeSessionTags) {
+ // Add session tags for the configured fields.
+ // Note: trace_id is only present in context when the caller has
included it
+ // (StorageAccessConfigProvider populates it when "trace_id" is in
enabledSessionTagFields).
+ if (!enabledSessionTagFields.isEmpty()) {
List<Tag> sessionTags =
- buildSessionTags(polarisPrincipal.getName(),
credentialVendingContext);
+ buildSessionTags(
+ polarisPrincipal.getName(), credentialVendingContext,
enabledSessionTagFields);
if (!sessionTags.isEmpty()) {
request.tags(sessionTags);
// Mark all tags as transitive for role chaining support
diff --git
a/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsSessionTagsBuilder.java
b/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsSessionTagsBuilder.java
index 7931c7204..70da1b454 100644
---
a/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsSessionTagsBuilder.java
+++
b/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsSessionTagsBuilder.java
@@ -18,8 +18,10 @@
*/
package org.apache.polaris.core.storage.aws;
-import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
import org.apache.polaris.core.storage.CredentialVendingContext;
import software.amazon.awssdk.services.sts.model.Tag;
@@ -40,63 +42,25 @@ public final class AwsSessionTagsBuilder {
}
/**
- * Builds a list of AWS STS session tags from the principal name and
credential vending context.
- * These tags will appear in CloudTrail events for correlation purposes.
+ * Builds a list of AWS STS session tags from the principal name and
credential vending context,
+ * including only the fields specified in {@code enabledFields}.
*
- * <p>The trace ID tag is only included if {@link
CredentialVendingContext#traceId()} is present.
- * This is controlled at the source (StorageAccessConfigProvider) based on
the
- * INCLUDE_TRACE_ID_IN_SESSION_TAGS feature flag.
+ * <p>Only fields present in {@code enabledFields} will be included in the
returned list. The
+ * trace ID tag is only included if {@link SessionTagField#TRACE_ID} is in
{@code enabledFields}
+ * and {@link CredentialVendingContext#traceId()} is present (the context's
traceId is populated
+ * by the caller based on configuration).
*
* @param principalName the name of the principal requesting credentials
- * @param context the credential vending context containing catalog,
namespace, table, roles, and
- * optionally trace ID
+ * @param context the credential vending context
+ * @param enabledFields the set of {@link SessionTagField}s to include
* @return a list of STS Tags to attach to the AssumeRole request
*/
- public static List<Tag> buildSessionTags(String principalName,
CredentialVendingContext context) {
- List<Tag> tags = new ArrayList<>();
-
- // Always include all tags with "unknown" placeholder for missing values
- // This ensures consistent tag presence in CloudTrail for correlation
- tags.add(
- Tag.builder()
- .key(CredentialVendingContext.TAG_KEY_PRINCIPAL)
- .value(truncateTagValue(principalName))
- .build());
- tags.add(
- Tag.builder()
- .key(CredentialVendingContext.TAG_KEY_ROLES)
-
.value(truncateTagValue(context.activatedRoles().orElse(TAG_VALUE_UNKNOWN)))
- .build());
- tags.add(
- Tag.builder()
- .key(CredentialVendingContext.TAG_KEY_CATALOG)
-
.value(truncateTagValue(context.catalogName().orElse(TAG_VALUE_UNKNOWN)))
- .build());
- tags.add(
- Tag.builder()
- .key(CredentialVendingContext.TAG_KEY_NAMESPACE)
-
.value(truncateTagValue(context.namespace().orElse(TAG_VALUE_UNKNOWN)))
- .build());
- tags.add(
- Tag.builder()
- .key(CredentialVendingContext.TAG_KEY_TABLE)
-
.value(truncateTagValue(context.tableName().orElse(TAG_VALUE_UNKNOWN)))
- .build());
-
- // Only include trace ID if it's present in the context.
- // The context's traceId is only populated when
INCLUDE_TRACE_ID_IN_SESSION_TAGS is enabled.
- // This allows efficient credential caching when trace IDs are not needed
in session tags.
- context
- .traceId()
- .ifPresent(
- traceId ->
- tags.add(
- Tag.builder()
- .key(CredentialVendingContext.TAG_KEY_TRACE_ID)
- .value(truncateTagValue(traceId))
- .build()));
-
- return tags;
+ public static List<Tag> buildSessionTags(
+ String principalName, CredentialVendingContext context,
Set<SessionTagField> enabledFields) {
+ return enabledFields.stream()
+ .map(field -> field.buildTag(principalName, context))
+ .flatMap(Optional::stream)
+ .collect(Collectors.toList());
}
/**
diff --git
a/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/SessionTagField.java
b/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/SessionTagField.java
new file mode 100644
index 000000000..6036539c2
--- /dev/null
+++
b/polaris-core/src/main/java/org/apache/polaris/core/storage/aws/SessionTagField.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.polaris.core.storage.aws;
+
+import java.util.Optional;
+import org.apache.polaris.core.storage.CredentialVendingContext;
+import software.amazon.awssdk.services.sts.model.Tag;
+
+/**
+ * Enum representing the supported fields that can be included as AWS STS
session tags in credential
+ * vending. Each value knows its tag key and how to extract its value from a
{@link
+ * CredentialVendingContext}.
+ *
+ * <p>These tags appear in CloudTrail events for correlation between catalog
operations and S3 data
+ * access. Configure via {@code SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL} using
the {@link #configName}
+ * of each desired field.
+ */
+public enum SessionTagField {
+ REALM("realm", CredentialVendingContext.TAG_KEY_REALM) {
+ @Override
+ public Optional<Tag> buildTag(String principalName,
CredentialVendingContext context) {
+ return
Optional.of(tag(context.realm().orElse(AwsSessionTagsBuilder.TAG_VALUE_UNKNOWN)));
+ }
+ },
+ CATALOG("catalog", CredentialVendingContext.TAG_KEY_CATALOG) {
+ @Override
+ public Optional<Tag> buildTag(String principalName,
CredentialVendingContext context) {
+ return Optional.of(
+
tag(context.catalogName().orElse(AwsSessionTagsBuilder.TAG_VALUE_UNKNOWN)));
+ }
+ },
+ NAMESPACE("namespace", CredentialVendingContext.TAG_KEY_NAMESPACE) {
+ @Override
+ public Optional<Tag> buildTag(String principalName,
CredentialVendingContext context) {
+ return
Optional.of(tag(context.namespace().orElse(AwsSessionTagsBuilder.TAG_VALUE_UNKNOWN)));
+ }
+ },
+ TABLE("table", CredentialVendingContext.TAG_KEY_TABLE) {
+ @Override
+ public Optional<Tag> buildTag(String principalName,
CredentialVendingContext context) {
+ return
Optional.of(tag(context.tableName().orElse(AwsSessionTagsBuilder.TAG_VALUE_UNKNOWN)));
+ }
+ },
+ PRINCIPAL("principal", CredentialVendingContext.TAG_KEY_PRINCIPAL) {
+ @Override
+ public Optional<Tag> buildTag(String principalName,
CredentialVendingContext context) {
+ return Optional.of(tag(principalName));
+ }
+ },
+ ROLES("roles", CredentialVendingContext.TAG_KEY_ROLES) {
+ @Override
+ public Optional<Tag> buildTag(String principalName,
CredentialVendingContext context) {
+ return Optional.of(
+
tag(context.activatedRoles().orElse(AwsSessionTagsBuilder.TAG_VALUE_UNKNOWN)));
+ }
+ },
+ /**
+ * The OpenTelemetry trace ID. Only emits a tag when {@link
CredentialVendingContext#traceId()} is
+ * present (the caller populates it only when this field is configured).
+ *
+ * <p>WARNING: including this field disables credential caching because
every request has a unique
+ * trace ID.
+ */
+ TRACE_ID("trace_id", CredentialVendingContext.TAG_KEY_TRACE_ID) {
+ @Override
+ public Optional<Tag> buildTag(String principalName,
CredentialVendingContext context) {
+ return context.traceId().map(this::tag);
+ }
+ };
+
+ /** The string name used in the {@code SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL}
config list. */
+ public final String configName;
+
+ private final String tagKey;
+
+ SessionTagField(String configName, String tagKey) {
+ this.configName = configName;
+ this.tagKey = tagKey;
+ }
+
+ /**
+ * Builds an STS {@link Tag} for this field from the given context, or
{@link Optional#empty()} if
+ * the field has no value to emit (e.g. {@code trace_id} when no trace is
active).
+ */
+ public abstract Optional<Tag> buildTag(String principalName,
CredentialVendingContext context);
+
+ Tag tag(String value) {
+ return
Tag.builder().key(tagKey).value(AwsSessionTagsBuilder.truncateTagValue(value)).build();
+ }
+
+ /** Returns the {@link SessionTagField} for the given config name, or empty
if not found. */
+ public static Optional<SessionTagField> fromConfigName(String configName) {
+ for (SessionTagField field : values()) {
+ if (field.configName.equals(configName)) {
+ return Optional.of(field);
+ }
+ }
+ return Optional.empty();
+ }
+}
diff --git
a/polaris-core/src/main/java/org/apache/polaris/core/storage/cache/StorageCredentialCache.java
b/polaris-core/src/main/java/org/apache/polaris/core/storage/cache/StorageCredentialCache.java
index ac6c8f80f..4f252ddce 100644
---
a/polaris-core/src/main/java/org/apache/polaris/core/storage/cache/StorageCredentialCache.java
+++
b/polaris-core/src/main/java/org/apache/polaris/core/storage/cache/StorageCredentialCache.java
@@ -25,6 +25,7 @@ import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import java.time.Duration;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -127,19 +128,20 @@ public class StorageCredentialCache {
boolean includePrincipalNameInSubscopedCredential =
realmConfig.getConfig(FeatureConfiguration.INCLUDE_PRINCIPAL_NAME_IN_SUBSCOPED_CREDENTIAL);
- boolean includeSessionTags =
-
realmConfig.getConfig(FeatureConfiguration.INCLUDE_SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL);
+ List<String> sessionTagFields =
+
realmConfig.getConfig(FeatureConfiguration.SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL);
+ boolean includeSessionTags = !sessionTagFields.isEmpty();
// When session tags are enabled, the cache key needs to include:
// 1. The credential vending context to avoid returning cached credentials
with different
- // session tags (catalog/namespace/table/roles/traceId)
+ // session tags (realm/catalog/namespace/table/roles/traceId)
// 2. The principal, because the polaris:principal session tag is included
in AWS credentials
// and we must not serve credentials tagged for principal A to
principal B
// When session tags are disabled, we only include principal if explicitly
configured.
//
// Note: The trace ID is controlled at the source
(StorageAccessConfigProvider). When
- // INCLUDE_TRACE_ID_IN_SESSION_TAGS is disabled, the context's traceId is
left empty,
- // which allows efficient caching across requests with different trace IDs.
+ // "trace_id" is not in SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL, the
context's traceId is left
+ // empty, which allows efficient caching across requests with different
trace IDs.
boolean includePrincipalInCacheKey =
includePrincipalNameInSubscopedCredential || includeSessionTags;
// When session tags are disabled, use empty context to ensure consistent
cache key behavior
diff --git
a/polaris-core/src/test/java/org/apache/polaris/service/storage/aws/AwsCredentialsStorageIntegrationTest.java
b/polaris-core/src/test/java/org/apache/polaris/service/storage/aws/AwsCredentialsStorageIntegrationTest.java
index 81513896d..0746888ad 100644
---
a/polaris-core/src/test/java/org/apache/polaris/service/storage/aws/AwsCredentialsStorageIntegrationTest.java
+++
b/polaris-core/src/test/java/org/apache/polaris/service/storage/aws/AwsCredentialsStorageIntegrationTest.java
@@ -19,8 +19,7 @@
package org.apache.polaris.service.storage.aws;
import static
org.apache.polaris.core.config.FeatureConfiguration.INCLUDE_PRINCIPAL_NAME_IN_SUBSCOPED_CREDENTIAL;
-import static
org.apache.polaris.core.config.FeatureConfiguration.INCLUDE_SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL;
-import static
org.apache.polaris.core.config.FeatureConfiguration.INCLUDE_TRACE_ID_IN_SESSION_TAGS;
+import static
org.apache.polaris.core.config.FeatureConfiguration.SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL;
import static org.assertj.core.api.Assertions.assertThat;
import jakarta.annotation.Nonnull;
@@ -68,8 +67,17 @@ class AwsCredentialsStorageIntegrationTest extends
BaseStorageIntegrationTest {
public static final RealmConfig PRINCIPAL_INCLUDER_REALM_CONFIG =
enabledFeatures(INCLUDE_PRINCIPAL_NAME_IN_SUBSCOPED_CREDENTIAL);
+ // Session tags enabled with the default fields: catalog, namespace, table,
principal, roles
private static final RealmConfig SESSION_TAGS_ENABLED_CONFIG =
- enabledFeatures(INCLUDE_SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL);
+ sessionTagFields("catalog", "namespace", "table", "principal", "roles");
+
+ // Session tags enabled with realm + default fields
+ private static final RealmConfig SESSION_TAGS_WITH_REALM_CONFIG =
+ sessionTagFields("realm", "catalog", "namespace", "table", "principal",
"roles");
+
+ // Session tags enabled with all fields including trace_id
+ private static final RealmConfig SESSION_TAGS_WITH_TRACE_ID_CONFIG =
+ sessionTagFields("catalog", "namespace", "table", "principal", "roles",
"trace_id");
public static final AssumeRoleResponse ASSUME_ROLE_RESPONSE =
AssumeRoleResponse.builder()
@@ -93,6 +101,17 @@ class AwsCredentialsStorageIntegrationTest extends
BaseStorageIntegrationTest {
() -> "realm");
}
+ private static RealmConfig sessionTagFields(String... fields) {
+ return new RealmConfigImpl(
+ (rc, name) -> {
+ if (name.equals(SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL.key())) {
+ return List.of(fields);
+ }
+ return null;
+ },
+ () -> "realm");
+ }
+
@ParameterizedTest
@ValueSource(strings = {"s3a", "s3"})
public void testGetSubscopedCreds(String scheme) {
@@ -1217,10 +1236,8 @@ class AwsCredentialsStorageIntegrationTest extends
BaseStorageIntegrationTest {
String bucket = "bucket";
String warehouseKeyPrefix = "path/to/warehouse";
- // Create a realm config with both session tags AND trace_id enabled
- RealmConfig sessionTagsAndTraceIdEnabledConfig =
- enabledFeatures(
- INCLUDE_SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL,
INCLUDE_TRACE_ID_IN_SESSION_TAGS);
+ // Create a realm config with session tags AND trace_id enabled
+ RealmConfig sessionTagsAndTraceIdEnabledConfig =
SESSION_TAGS_WITH_TRACE_ID_CONFIG;
ArgumentCaptor<AssumeRoleRequest> requestCaptor =
ArgumentCaptor.forClass(AssumeRoleRequest.class);
@@ -1475,6 +1492,83 @@ class AwsCredentialsStorageIntegrationTest extends
BaseStorageIntegrationTest {
.noneMatch(tag -> tag.key().equals("polaris:trace_id"));
}
+ /**
+ * Helper method to invoke getSubscopedCreds and capture the
AssumeRoleRequest for assertions.
+ *
+ * @param realmConfig the realm configuration to use
+ * @param context the credential vending context
+ * @return the captured AssumeRoleRequest
+ */
+ private AssumeRoleRequest invokeGetSubscopedCredsAndCaptureRequest(
+ RealmConfig realmConfig, CredentialVendingContext context) {
+ StsClient stsClient = Mockito.mock(StsClient.class);
+ String roleARN = "arn:aws:iam::012345678901:role/jdoe";
+ String externalId = "externalId";
+ String bucket = "bucket";
+ String warehouseKeyPrefix = "path/to/warehouse";
+
+ ArgumentCaptor<AssumeRoleRequest> requestCaptor =
+ ArgumentCaptor.forClass(AssumeRoleRequest.class);
+
Mockito.when(stsClient.assumeRole(requestCaptor.capture())).thenReturn(ASSUME_ROLE_RESPONSE);
+
+ new AwsCredentialsStorageIntegration(
+ AwsStorageConfigurationInfo.builder()
+ .addAllowedLocation(s3Path(bucket, warehouseKeyPrefix))
+ .roleARN(roleARN)
+ .externalId(externalId)
+ .build(),
+ stsClient)
+ .getSubscopedCreds(
+ realmConfig,
+ true,
+ Set.of(s3Path(bucket, warehouseKeyPrefix)),
+ Set.of(s3Path(bucket, warehouseKeyPrefix)),
+ POLARIS_PRINCIPAL,
+ Optional.empty(),
+ context);
+
+ return requestCaptor.getValue();
+ }
+
+ @Test
+ public void testRealmSessionTagIncludedWhenConfigured() {
+ CredentialVendingContext context =
+ CredentialVendingContext.builder()
+ .realm(Optional.of("my-realm"))
+ .catalogName(Optional.of("test-catalog"))
+ .namespace(Optional.of("db.schema"))
+ .tableName(Optional.of("my_table"))
+ .activatedRoles(Optional.of("admin"))
+ .build();
+
+ AssumeRoleRequest capturedRequest =
+
invokeGetSubscopedCredsAndCaptureRequest(SESSION_TAGS_WITH_REALM_CONFIG,
context);
+
+ // 6 tags: realm + catalog + namespace + table + principal + roles
+ Assertions.assertThat(capturedRequest.tags()).hasSize(6);
+ Assertions.assertThat(capturedRequest.tags())
+ .anyMatch(tag -> tag.key().equals("polaris:realm") &&
tag.value().equals("my-realm"));
+ Assertions.assertThat(capturedRequest.tags())
+ .anyMatch(tag -> tag.key().equals("polaris:catalog") &&
tag.value().equals("test-catalog"));
+
Assertions.assertThat(capturedRequest.transitiveTagKeys()).contains("polaris:realm");
+ }
+
+ @Test
+ public void testRealmSessionTagNotIncludedWhenNotConfigured() {
+ CredentialVendingContext context =
+ CredentialVendingContext.builder()
+ .realm(Optional.of("my-realm"))
+ .catalogName(Optional.of("test-catalog"))
+ .build();
+
+ // SESSION_TAGS_ENABLED_CONFIG does NOT include "realm" in the field list
+ AssumeRoleRequest capturedRequest =
+ invokeGetSubscopedCredsAndCaptureRequest(SESSION_TAGS_ENABLED_CONFIG,
context);
+
+ Assertions.assertThat(capturedRequest.tags())
+ .noneMatch(tag -> tag.key().equals("polaris:realm"));
+ }
+
/**
* Tests graceful error handling when STS throws an exception due to missing
sts:TagSession
* permission. When the IAM role's trust policy doesn't allow
sts:TagSession, the assumeRole call
@@ -1492,7 +1586,7 @@ class AwsCredentialsStorageIntegrationTest extends
BaseStorageIntegrationTest {
String warehouseKeyPrefix = "path/to/warehouse";
RealmConfig sessionTagsEnabledConfig =
- enabledFeatures(INCLUDE_SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL);
+ sessionTagFields("catalog", "namespace", "table", "principal",
"roles");
// Simulate STS throwing AccessDeniedException when sts:TagSession is not
allowed
// In AWS SDK v2, this is represented as StsException with error code
"AccessDenied"
diff --git
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/io/StorageAccessConfigProvider.java
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/io/StorageAccessConfigProvider.java
index 76f5a53f9..8b8acb9e6 100644
---
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/io/StorageAccessConfigProvider.java
+++
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/io/StorageAccessConfigProvider.java
@@ -32,6 +32,7 @@ import org.apache.iceberg.catalog.Namespace;
import org.apache.iceberg.catalog.TableIdentifier;
import org.apache.polaris.core.auth.PolarisPrincipal;
import org.apache.polaris.core.config.FeatureConfiguration;
+import org.apache.polaris.core.context.RealmContext;
import org.apache.polaris.core.entity.PolarisEntity;
import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper;
import org.apache.polaris.core.storage.CredentialVendingContext;
@@ -57,15 +58,18 @@ public class StorageAccessConfigProvider {
private final StorageCredentialCache storageCredentialCache;
private final StorageCredentialsVendor storageCredentialsVendor;
private final PolarisPrincipal polarisPrincipal;
+ private final RealmContext realmContext;
@Inject
public StorageAccessConfigProvider(
StorageCredentialCache storageCredentialCache,
StorageCredentialsVendor storageCredentialsVendor,
- PolarisPrincipal polarisPrincipal) {
+ PolarisPrincipal polarisPrincipal,
+ RealmContext realmContext) {
this.storageCredentialCache = storageCredentialCache;
this.storageCredentialsVendor = storageCredentialsVendor;
this.polarisPrincipal = polarisPrincipal;
+ this.realmContext = realmContext;
}
/**
@@ -167,6 +171,14 @@ public class StorageAccessConfigProvider {
TableIdentifier tableIdentifier, PolarisResolvedPathWrapper
resolvedPath) {
CredentialVendingContext.Builder builder =
CredentialVendingContext.builder();
+ List<String> sessionTagFields =
+ storageCredentialsVendor
+ .getRealmConfig()
+
.getConfig(FeatureConfiguration.SESSION_TAGS_IN_SUBSCOPED_CREDENTIAL);
+
+ // Realm identifier
+ builder.realm(Optional.of(realmContext.getRealmIdentifier()));
+
// Extract catalog name from the first entity in the resolved path
List<PolarisEntity> fullPath = resolvedPath.getRawFullPath();
if (fullPath != null && !fullPath.isEmpty()) {
@@ -189,16 +201,10 @@ public class StorageAccessConfigProvider {
builder.activatedRoles(Optional.of(rolesString));
}
- // Only include trace ID when the feature flag is enabled.
- // When enabled, trace IDs are included in AWS STS session tags and become
part of the
- // credential cache key (since they affect the vended credentials).
- // When disabled (default), trace IDs are not included, allowing efficient
credential
- // caching across requests with different trace IDs.
- boolean includeTraceIdInSessionTags =
- storageCredentialsVendor
- .getRealmConfig()
- .getConfig(FeatureConfiguration.INCLUDE_TRACE_ID_IN_SESSION_TAGS);
- if (includeTraceIdInSessionTags) {
+ // Only include trace ID when "trace_id" is in the configured session tag
fields.
+ // When included, trace IDs become part of the credential cache key (since
they affect
+ // the vended credentials), which disables effective credential caching.
+ if
(sessionTagFields.contains(FeatureConfiguration.SESSION_TAG_FIELD_TRACE_ID)) {
builder.traceId(getCurrentTraceId());
}
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 8c1ab641a..e6310a09e 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
@@ -293,7 +293,7 @@ public record TestServices(
new StorageCredentialsVendor(metaStoreManager, callContext);
StorageAccessConfigProvider storageAccessConfigProvider =
new StorageAccessConfigProvider(
- storageCredentialCache, storageCredentialsVendor, principal);
+ storageCredentialCache, storageCredentialsVendor, principal,
realmContext);
FileIOFactory fileIOFactory = fileIOFactorySupplier.get();
TaskExecutor taskExecutor = Mockito.mock(TaskExecutor.class);