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


Reply via email to