This is an automated email from the ASF dual-hosted git repository.
jerryshao pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/branch-1.3 by this push:
new c0491090e0 [Cherry-pick to branch-1.3] [#13927] fix(credential):
handle AWS IRSA fallback and ADLS paths (#13928) (#13961)
c0491090e0 is described below
commit c0491090e01a054f833bb18e28e7f11fc36d031f
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Wed Oct 21 10:47:12 2026 +0800
[Cherry-pick to branch-1.3] [#13927] fix(credential): handle AWS IRSA
fallback and ADLS paths (#13928) (#13961)
**Cherry-pick Information:**
- Original commit: 026a660350666bb740c4f53c1b34b8e2707b2d88
- Target branch: `branch-1.3`
- Status: Conflicts resolved in `docs/security/credential-vending.md`
The credential vending documentation now places the IRSA fallback
details in the S3 IRSA section and the SAS path scope details in the
ADLS Token section.
Validation: `./gradlew :docs:build --console=plain` and `./gradlew
:bundles:aws:test :bundles:azure:test -PskipITs --console=plain`.
---------
Co-authored-by: roryqi <[email protected]>
Co-authored-by: roryqi <[email protected]>
---
.../s3/credential/AwsIrsaCredentialGenerator.java | 18 +-
.../credential/TestAwsIrsaCredentialGenerator.java | 56 ++++++
.../abs/credential/ADLSTokenGenerator.java | 111 +++++++++---
.../abs/credential/TestADLSTokenGenerator.java | 193 +++++++++++++++++++++
docs/security/credential-vending.md | 8 +-
5 files changed, 356 insertions(+), 30 deletions(-)
diff --git
a/bundles/aws/src/main/java/org/apache/gravitino/s3/credential/AwsIrsaCredentialGenerator.java
b/bundles/aws/src/main/java/org/apache/gravitino/s3/credential/AwsIrsaCredentialGenerator.java
index 2d5fe0f6ba..2026afbb56 100644
---
a/bundles/aws/src/main/java/org/apache/gravitino/s3/credential/AwsIrsaCredentialGenerator.java
+++
b/bundles/aws/src/main/java/org/apache/gravitino/s3/credential/AwsIrsaCredentialGenerator.java
@@ -28,6 +28,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Supplier;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.credential.AwsIrsaCredential;
@@ -55,6 +56,7 @@ import software.amazon.awssdk.services.sts.model.Credentials;
/** Generate AWS IRSA credentials according to the read and write paths. */
public class AwsIrsaCredentialGenerator implements
CredentialGenerator<AwsIrsaCredential> {
+ private final Supplier<String> environmentRoleArnSupplier;
private WebIdentityTokenSource tokenSource;
private String roleArn;
private int tokenExpireSecs;
@@ -64,12 +66,24 @@ public class AwsIrsaCredentialGenerator implements
CredentialGenerator<AwsIrsaCr
private StsClient basicModeStsClient;
private Map<String, StsAssumeRoleWithWebIdentityCredentialsProvider>
basicModeProviders;
+ /** Creates an AWS IRSA credential generator. */
+ public AwsIrsaCredentialGenerator() {
+ this(() -> System.getenv("AWS_ROLE_ARN"));
+ }
+
+ AwsIrsaCredentialGenerator(Supplier<String> environmentRoleArnSupplier) {
+ this.environmentRoleArnSupplier =
Objects.requireNonNull(environmentRoleArnSupplier);
+ }
+
@Override
public void initialize(Map<String, String> properties) {
this.tokenSource = WebIdentityTokenSources.create(properties);
S3CredentialConfig s3CredentialConfig = new S3CredentialConfig(properties);
- this.roleArn = s3CredentialConfig.s3RoleArn();
+ this.roleArn =
+
StringUtils.isNotBlank(properties.get(S3CredentialConfig.S3_ROLE_ARN.getKey()))
+ ? s3CredentialConfig.s3RoleArn()
+ : null;
this.tokenExpireSecs = s3CredentialConfig.tokenExpireInSecs();
this.region = s3CredentialConfig.region();
this.stsEndpoint = s3CredentialConfig.stsEndpoint();
@@ -330,7 +344,7 @@ public class AwsIrsaCredentialGenerator implements
CredentialGenerator<AwsIrsaCr
private String getValidatedRoleArn(String configRoleArn) {
String effectiveRoleArn =
- StringUtils.isNotBlank(configRoleArn) ? configRoleArn :
System.getenv("AWS_ROLE_ARN");
+ StringUtils.isNotBlank(configRoleArn) ? configRoleArn :
environmentRoleArnSupplier.get();
if (StringUtils.isBlank(effectiveRoleArn)) {
throw new IllegalStateException(
"No role ARN available. Either configure s3-role-arn or ensure
AWS_ROLE_ARN environment variable is set.");
diff --git
a/bundles/aws/src/test/java/org/apache/gravitino/s3/credential/TestAwsIrsaCredentialGenerator.java
b/bundles/aws/src/test/java/org/apache/gravitino/s3/credential/TestAwsIrsaCredentialGenerator.java
index f8c48b5c0d..ca261f02d8 100644
---
a/bundles/aws/src/test/java/org/apache/gravitino/s3/credential/TestAwsIrsaCredentialGenerator.java
+++
b/bundles/aws/src/test/java/org/apache/gravitino/s3/credential/TestAwsIrsaCredentialGenerator.java
@@ -84,6 +84,62 @@ class TestAwsIrsaCredentialGenerator {
assertFalse(body.contains("Policy="));
}
+ @Test
+ void
basicModeFallsBackToEnvironmentRoleArnWhenConfiguredRoleIsBlank(@TempDir Path
dir)
+ throws IOException {
+ Map<String, String> properties = createProperties(dir);
+ properties.put("s3-role-arn", " ");
+
+ try (AwsIrsaCredentialGenerator generator =
+ new AwsIrsaCredentialGenerator(() ->
"arn:aws:iam::123456789012:role/environment-role")) {
+ generator.initialize(properties);
+
+ AwsIrsaCredential credential = generator.generate(() -> "test-user");
+
+ assertEquals("access-key", credential.accessKeyId());
+ }
+
+ String body = URLDecoder.decode(requestBody.get(),
StandardCharsets.UTF_8.name());
+
assertTrue(body.contains("RoleArn=arn:aws:iam::123456789012:role/environment-role"));
+ }
+
+ @Test
+ void pathModeFallsBackToEnvironmentRoleArn(@TempDir Path dir) throws
IOException {
+ Map<String, String> properties = createProperties(dir);
+ properties.remove("s3-role-arn");
+
+ try (AwsIrsaCredentialGenerator generator =
+ new AwsIrsaCredentialGenerator(() ->
"arn:aws:iam::123456789012:role/environment-role")) {
+ generator.initialize(properties);
+ PathBasedCredentialContext context =
+ new PathBasedCredentialContext(
+ "test-user", Collections.singleton("s3://bucket/table"),
Collections.emptySet());
+
+ AwsIrsaCredential credential = generator.generate(context);
+
+ assertEquals("access-key", credential.accessKeyId());
+ }
+
+ String body = URLDecoder.decode(requestBody.get(),
StandardCharsets.UTF_8.name());
+
assertTrue(body.contains("RoleArn=arn:aws:iam::123456789012:role/environment-role"));
+ assertTrue(body.contains("Policy="));
+ }
+
+ @Test
+ void basicModeFailsWhenNoRoleArnIsAvailable(@TempDir Path dir) throws
IOException {
+ Map<String, String> properties = createProperties(dir);
+ properties.remove("s3-role-arn");
+
+ try (AwsIrsaCredentialGenerator generator = new
AwsIrsaCredentialGenerator(() -> null)) {
+ generator.initialize(properties);
+
+ IllegalStateException exception =
+ assertThrows(IllegalStateException.class, () ->
generator.generate(() -> "test-user"));
+
+ assertTrue(exception.getMessage().contains("No role ARN available"));
+ }
+ }
+
@Test
void basicModeReusesCachedStsCredentials(@TempDir Path dir) throws
IOException {
try (AwsIrsaCredentialGenerator generator = new
AwsIrsaCredentialGenerator()) {
diff --git
a/bundles/azure/src/main/java/org/apache/gravitino/abs/credential/ADLSTokenGenerator.java
b/bundles/azure/src/main/java/org/apache/gravitino/abs/credential/ADLSTokenGenerator.java
index f3cbb5b1fe..fbc69c343a 100644
---
a/bundles/azure/src/main/java/org/apache/gravitino/abs/credential/ADLSTokenGenerator.java
+++
b/bundles/azure/src/main/java/org/apache/gravitino/abs/credential/ADLSTokenGenerator.java
@@ -29,9 +29,13 @@ import
com.azure.storage.file.datalake.sas.DataLakeServiceSasSignatureValues;
import com.azure.storage.file.datalake.sas.PathSasPermission;
import java.io.IOException;
import java.time.OffsetDateTime;
-import java.util.HashSet;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
import org.apache.gravitino.credential.ADLSTokenCredential;
import org.apache.gravitino.credential.CredentialContext;
import org.apache.gravitino.credential.CredentialGenerator;
@@ -70,34 +74,14 @@ public class ADLSTokenGenerator implements
CredentialGenerator<ADLSTokenCredenti
Set<String> writePaths = pathContext.getWritePaths();
Set<String> readPaths = pathContext.getReadPaths();
- Set<String> combinedPaths = new HashSet<>(writePaths);
+ Set<String> combinedPaths = new TreeSet<>(writePaths);
combinedPaths.addAll(readPaths);
- if (combinedPaths.size() != 1) {
- throw new IllegalArgumentException(
- "ADLS should contain exactly one unique path, but found: "
- + combinedPaths.size()
- + " paths: "
- + combinedPaths);
- }
- String uniquePath = combinedPaths.iterator().next();
-
- ClientSecretCredential clientSecretCredential =
- new ClientSecretCredentialBuilder()
- .tenantId(tenantId)
- .clientId(clientId)
- .clientSecret(clientSecret)
- .build();
-
- DataLakeServiceClient dataLakeServiceClient =
- new DataLakeServiceClientBuilder()
- .endpoint(endpoint)
- .credential(clientSecretCredential)
- .buildClient();
+ ADLSLocationUtils.ADLSLocationParts locationParts =
findCredentialLocation(combinedPaths);
OffsetDateTime start = OffsetDateTime.now();
OffsetDateTime expiry = OffsetDateTime.now().plusSeconds(tokenExpireSecs);
- UserDelegationKey userDelegationKey =
dataLakeServiceClient.getUserDelegationKey(start, expiry);
+ UserDelegationKey userDelegationKey = getUserDelegationKey(start, expiry);
PathSasPermission pathSasPermission =
new
PathSasPermission().setReadPermission(true).setListPermission(true);
@@ -111,7 +95,6 @@ public class ADLSTokenGenerator implements
CredentialGenerator<ADLSTokenCredenti
DataLakeServiceSasSignatureValues signatureValues =
new DataLakeServiceSasSignatureValues(expiry, pathSasPermission);
- ADLSLocationUtils.ADLSLocationParts locationParts =
ADLSLocationUtils.parseLocation(uniquePath);
String sasToken =
new DataLakeSasImplUtil(
signatureValues,
@@ -125,6 +108,84 @@ public class ADLSTokenGenerator implements
CredentialGenerator<ADLSTokenCredenti
locationParts.getAccountName(), sasToken,
expiry.toInstant().toEpochMilli());
}
+ UserDelegationKey getUserDelegationKey(OffsetDateTime start, OffsetDateTime
expiry) {
+ ClientSecretCredential clientSecretCredential =
+ new ClientSecretCredentialBuilder()
+ .tenantId(tenantId)
+ .clientId(clientId)
+ .clientSecret(clientSecret)
+ .build();
+
+ DataLakeServiceClient dataLakeServiceClient =
+ new DataLakeServiceClientBuilder()
+ .endpoint(endpoint)
+ .credential(clientSecretCredential)
+ .buildClient();
+ return dataLakeServiceClient.getUserDelegationKey(start, expiry);
+ }
+
+ private ADLSLocationUtils.ADLSLocationParts
findCredentialLocation(Set<String> paths) {
+ if (paths.isEmpty()) {
+ throw new IllegalArgumentException(
+ "ADLS credential context should contain at least one path");
+ }
+
+ List<ADLSLocationUtils.ADLSLocationParts> parsedLocations =
+
paths.stream().map(ADLSLocationUtils::parseLocation).collect(Collectors.toList());
+ ADLSLocationUtils.ADLSLocationParts first = parsedLocations.get(0);
+ boolean hasDifferentScope =
+ parsedLocations.stream()
+ .anyMatch(
+ location ->
+
!storageAccountName.equalsIgnoreCase(location.getAccountName())
+ ||
!first.getAccountName().equalsIgnoreCase(location.getAccountName())
+ ||
!first.getContainer().equalsIgnoreCase(location.getContainer()));
+ if (hasDifferentScope) {
+ throw new IllegalArgumentException(
+ "ADLS paths must belong to the configured storage account and a
single container, but found: "
+ + paths);
+ }
+
+ Map<String, ADLSLocationUtils.ADLSLocationParts> uniqueLocations = new
LinkedHashMap<>();
+ parsedLocations.forEach(
+ location ->
+ uniqueLocations.putIfAbsent(
+ location.getContainer() + "\0" +
ADLSLocationUtils.trimSlashes(location.getPath()),
+ location));
+ List<ADLSLocationUtils.ADLSLocationParts> locations = new
ArrayList<>(uniqueLocations.values());
+
+ List<ADLSLocationUtils.ADLSLocationParts> rootLocations =
+ locations.stream()
+ .filter(
+ location ->
+ locations.stream()
+ .noneMatch(
+ possibleParent ->
+ possibleParent != location
+ && isDescendantOf(location,
possibleParent)))
+ .collect(Collectors.toList());
+ if (rootLocations.size() != 1) {
+ throw new IllegalArgumentException(
+ "ADLS credential paths must resolve to exactly one root location,
but found: "
+ + rootLocations.size()
+ + " roots in paths: "
+ + paths);
+ }
+
+ return rootLocations.get(0);
+ }
+
+ private static boolean isDescendantOf(
+ ADLSLocationUtils.ADLSLocationParts location,
+ ADLSLocationUtils.ADLSLocationParts possibleParent) {
+ String path = ADLSLocationUtils.trimSlashes(location.getPath());
+ String parentPath =
ADLSLocationUtils.trimSlashes(possibleParent.getPath());
+ if (parentPath.isEmpty()) {
+ return !path.isEmpty();
+ }
+ return path.startsWith(parentPath + "/");
+ }
+
@Override
public void close() throws IOException {}
}
diff --git
a/bundles/azure/src/test/java/org/apache/gravitino/abs/credential/TestADLSTokenGenerator.java
b/bundles/azure/src/test/java/org/apache/gravitino/abs/credential/TestADLSTokenGenerator.java
new file mode 100644
index 0000000000..98bd98c56d
--- /dev/null
+++
b/bundles/azure/src/test/java/org/apache/gravitino/abs/credential/TestADLSTokenGenerator.java
@@ -0,0 +1,193 @@
+/*
+ * 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.gravitino.abs.credential;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.azure.storage.file.datalake.models.UserDelegationKey;
+import java.time.OffsetDateTime;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import org.apache.gravitino.credential.ADLSTokenCredential;
+import org.apache.gravitino.credential.PathBasedCredentialContext;
+import org.apache.gravitino.storage.AzureProperties;
+import org.junit.jupiter.api.Test;
+
+class TestADLSTokenGenerator {
+
+ @Test
+ void generatesTokenForParentDirectoryOfMultiplePaths() throws Exception {
+ try (ADLSTokenGenerator generator = new TestableADLSTokenGenerator()) {
+ generator.initialize(createProperties());
+ PathBasedCredentialContext context =
+ new PathBasedCredentialContext(
+ "test-user",
+ Collections.emptySet(),
+ new HashSet<>(
+ Arrays.asList(
+
"abfss://[email protected]/warehouse/table",
+
"abfss://[email protected]/warehouse/table/data",
+
"abfss://[email protected]/warehouse/table/metadata")));
+
+ ADLSTokenCredential credential = generator.generate(context);
+
+ assertEquals("account", credential.accountName());
+ assertTrue(credential.sasToken().contains("sr=d"));
+ assertTrue(credential.sasToken().contains("sdd=2"));
+ }
+ }
+
+ @Test
+ void treatsEquivalentLocationsAsOneRoot() throws Exception {
+ try (ADLSTokenGenerator generator = new TestableADLSTokenGenerator()) {
+ generator.initialize(createProperties());
+ PathBasedCredentialContext context =
+ new PathBasedCredentialContext(
+ "test-user",
+ Collections.emptySet(),
+ new HashSet<>(
+ Arrays.asList(
+
"abfss://[email protected]/warehouse/table",
+
"abfs://[email protected]/warehouse/table/",
+
"abfss://[email protected]/warehouse/table/data")));
+
+ ADLSTokenCredential credential = generator.generate(context);
+
+ assertTrue(credential.sasToken().contains("sr=d"));
+ assertTrue(credential.sasToken().contains("sdd=2"));
+ }
+ }
+
+ @Test
+ void rejectsPathsWithoutProvidedCommonRoot() throws Exception {
+ String first =
"abfss://[email protected]/warehouse/table/data";
+ String second =
"abfss://[email protected]/warehouse/table/metadata";
+ try (ADLSTokenGenerator generator = new TestableADLSTokenGenerator()) {
+ generator.initialize(createProperties());
+ PathBasedCredentialContext context =
+ new PathBasedCredentialContext(
+ "test-user", Collections.emptySet(), new
HashSet<>(Arrays.asList(first, second)));
+
+ IllegalArgumentException exception =
+ assertThrows(IllegalArgumentException.class, () ->
generator.generate(context));
+
+ assertTrue(exception.getMessage().contains("exactly one root location"));
+ assertTrue(exception.getMessage().contains(first));
+ assertTrue(exception.getMessage().contains(second));
+ }
+ }
+
+ @Test
+ void rejectsSiblingPathsWithCommonStringPrefix() throws Exception {
+ String first =
"abfss://[email protected]/warehouse/table";
+ String second =
"abfss://[email protected]/warehouse/table2";
+ try (ADLSTokenGenerator generator = new TestableADLSTokenGenerator()) {
+ generator.initialize(createProperties());
+ PathBasedCredentialContext context =
+ new PathBasedCredentialContext(
+ "test-user", Collections.emptySet(), new
HashSet<>(Arrays.asList(first, second)));
+
+ IllegalArgumentException exception =
+ assertThrows(IllegalArgumentException.class, () ->
generator.generate(context));
+
+ assertTrue(exception.getMessage().contains("exactly one root location"));
+ }
+ }
+
+ @Test
+ void rejectsPathsFromDifferentContainers() throws Exception {
+ String first =
"abfss://[email protected]/warehouse/table";
+ String second =
"abfss://[email protected]/warehouse/table/data";
+ try (ADLSTokenGenerator generator = new TestableADLSTokenGenerator()) {
+ generator.initialize(createProperties());
+ PathBasedCredentialContext context =
+ new PathBasedCredentialContext(
+ "test-user", Collections.emptySet(), new
HashSet<>(Arrays.asList(first, second)));
+
+ IllegalArgumentException exception =
+ assertThrows(IllegalArgumentException.class, () ->
generator.generate(context));
+
+ assertTrue(exception.getMessage().contains(first));
+ assertTrue(exception.getMessage().contains(second));
+ }
+ }
+
+ @Test
+ void rejectsPathsFromDifferentStorageAccounts() throws Exception {
+ String first =
"abfss://[email protected]/warehouse/table";
+ String second =
"abfss://[email protected]/warehouse/table/data";
+ try (ADLSTokenGenerator generator = new TestableADLSTokenGenerator()) {
+ generator.initialize(createProperties());
+ PathBasedCredentialContext context =
+ new PathBasedCredentialContext(
+ "test-user", Collections.emptySet(), new
HashSet<>(Arrays.asList(first, second)));
+
+ IllegalArgumentException exception =
+ assertThrows(IllegalArgumentException.class, () ->
generator.generate(context));
+
+ assertTrue(exception.getMessage().contains(first));
+ assertTrue(exception.getMessage().contains(second));
+ }
+ }
+
+ @Test
+ void rejectsSinglePathFromUnconfiguredStorageAccount() throws Exception {
+ String path =
"abfss://[email protected]/warehouse/table";
+ try (ADLSTokenGenerator generator = new TestableADLSTokenGenerator()) {
+ generator.initialize(createProperties());
+ PathBasedCredentialContext context =
+ new PathBasedCredentialContext(
+ "test-user", Collections.emptySet(),
Collections.singleton(path));
+
+ IllegalArgumentException exception =
+ assertThrows(IllegalArgumentException.class, () ->
generator.generate(context));
+
+ assertTrue(exception.getMessage().contains(path));
+ }
+ }
+
+ private static Map<String, String> createProperties() {
+ Map<String, String> properties = new HashMap<>();
+ properties.put(AzureProperties.GRAVITINO_AZURE_STORAGE_ACCOUNT_NAME,
"account");
+ properties.put(AzureProperties.GRAVITINO_AZURE_TENANT_ID, "tenant");
+ properties.put(AzureProperties.GRAVITINO_AZURE_CLIENT_ID, "client");
+ properties.put(AzureProperties.GRAVITINO_AZURE_CLIENT_SECRET, "secret");
+ return properties;
+ }
+
+ private static class TestableADLSTokenGenerator extends ADLSTokenGenerator {
+ @Override
+ UserDelegationKey getUserDelegationKey(OffsetDateTime start,
OffsetDateTime expiry) {
+ return new UserDelegationKey()
+ .setSignedObjectId("object")
+ .setSignedTenantId("tenant")
+ .setSignedStart(start)
+ .setSignedExpiry(expiry)
+ .setSignedService("b")
+ .setSignedVersion("2020-02-10")
+ .setValue(Base64.getEncoder().encodeToString(new byte[32]));
+ }
+ }
+}
diff --git a/docs/security/credential-vending.md
b/docs/security/credential-vending.md
index 20b56966ad..09d2c0aab1 100644
--- a/docs/security/credential-vending.md
+++ b/docs/security/credential-vending.md
@@ -56,17 +56,17 @@ A credential using AWS IAM Roles for Service Accounts
(IRSA) to access S3 with t
| Gravitino server catalog properties | Gravitino Iceberg REST server
configurations | Description
| Default value | Required |
|-------------------------------------|----------------------------------------------------|-----------------------------------------------------------------------------------------------------------|---------------|----------|
| `credential-providers` |
`gravitino.iceberg-rest.credential-providers` | `aws-irsa` for AWS IRSA
credential provider.
| (none) | Yes |
-| `s3-role-arn` | `gravitino.iceberg-rest.s3-role-arn`
| The ARN of the IAM role to assume. Required for fine-grained
path-based access control. | (none) | Yes* |
+| `s3-role-arn` | `gravitino.iceberg-rest.s3-role-arn`
| The ARN of the IAM role to assume. Falls back to the pod's
`AWS_ROLE_ARN` when unset or blank. | (none) | No |
| `s3-region` | `gravitino.iceberg-rest.s3-region`
| The AWS region for STS operations. Used for fine-grained access
control. | (none) | No |
| `s3-token-expire-in-secs` |
`gravitino.iceberg-rest.s3-token-expire-in-secs` | Token expiration time in
seconds for fine-grained credentials. Cannot exceed role's max session
duration. | 3600 | No |
| `s3-token-service-endpoint` |
`gravitino.iceberg-rest.s3-token-service-endpoint` | Alternative STS endpoint
for fine-grained credential generation. Useful for S3-compatible services.
| (none) | No |
-**Note**: `s3-role-arn` is required only when using fine-grained path-based
access control with vended credentials. For basic IRSA usage without path
restrictions, only `credential-providers=aws-irsa` is needed.
+**Note**: Set `s3-role-arn` or provide `AWS_ROLE_ARN` in the pod environment.
Gravitino selects that role when credentials are requested. For table-path
requests, it adds a session policy covering the requested data, metadata, and
write locations. Catalog-level requests have no path to scope and return
credentials with the selected role's full permissions.
**Prerequisites for fine-grained mode:**
- EKS cluster with IRSA properly configured
- `AWS_WEB_IDENTITY_TOKEN_FILE` environment variable pointing to the service
account token
-- IAM role with permissions to assume the target role specified in
`s3-role-arn`
+- Target IAM role selected by `s3-role-arn` or `AWS_ROLE_ARN` with a trust
policy allowing `sts:AssumeRoleWithWebIdentity` from the pod's Kubernetes
service account
- Target IAM role with necessary S3 permissions for the data locations
#### S3 Secret Key Credential
@@ -136,6 +136,8 @@ A credential with static Azure storage account name and key.
An ADLS token is a token credential with scoped privileges, by leveraging
Azure [User Delegation
Sas](https://learn.microsoft.com/en-us/rest/api/storageservices/create-user-delegation-sas).
To use an ADLS token credential, you should create a Microsoft Entra ID
service principal and grant it proper privileges.
+The SAS is scoped to one requested root directory that covers the table
location and any requested write paths. A custom write path that is the parent
of the table path results in a broader SAS scope. Requests with independent
roots or paths in different storage accounts or containers are rejected.
+
| Gravitino server catalog properties | Gravitino Iceberg REST server
configurations | Description
| Default value | Required |
|-------------------------------------|-----------------------------------------------------|---------------------------------------------------------------------|---------------|----------|
| `credential-providers` |
`gravitino.iceberg-rest.credential-providers` | `adls-token` for ADLS
token credential. | (none) | Yes |