Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
67fdaca
adding support to use a kms key for s3 buckets
fabio-rizzo-01 Oct 10, 2025
5cd4c4d
Update CHANGELOG.md
fabio-rizzo-01 Oct 13, 2025
7db0a87
Merge branch 'main' into feature/s3-kms-key
fabio-rizzo-01 Oct 13, 2025
cf4ec3e
Update CHANGELOG.md
fabio-rizzo-01 Oct 13, 2025
ffbe979
Update CHANGELOG.md
fabio-rizzo-01 Oct 13, 2025
5a0f3eb
fixing indentation issue
fabio-rizzo-01 Oct 13, 2025
708630c
fixing indentation issue
fabio-rizzo-01 Oct 13, 2025
3a10f7d
temporary changes to handle iceberg s3 properties
fabio-rizzo-01 Oct 21, 2025
543dabc
Merge remote-tracking branch 'private/feature/s3-kms-key' into featur…
fabio-rizzo-01 Oct 21, 2025
351dc07
Merge remote-tracking branch 'private/main-30-06' into feature/s3-kms…
fabio-rizzo-01 Oct 21, 2025
b8dad37
Merge remote-tracking branch 'private/main-30-06' into feature/s3-kms…
fabio-rizzo-01 Oct 24, 2025
6feb766
Merge remote-tracking branch 'private/main-30-06' into feature/s3-kms…
fabio-rizzo-01 Nov 6, 2025
d0907a1
removed s3 key table changes. added new properties for AwsStorageConf…
fabio-rizzo-01 Nov 14, 2025
e026c78
Merge remote-tracking branch 'private/main-30-06' into feature/s3-kms…
fabio-rizzo-01 Nov 14, 2025
7c33b55
removed s3 key table changes. added new properties for AwsStorageConf…
fabio-rizzo-01 Nov 14, 2025
65a84fb
removed s3 key table changes. added new properties for AwsStorageConf…
fabio-rizzo-01 Nov 14, 2025
b8bacb9
adding support to use a kms key for s3 buckets (#17)
fabio-rizzo-01 Nov 14, 2025
8850df1
fixed merge issue
fabio-rizzo-01 Nov 14, 2025
83115fd
Merge branch 'main-30-06' into feature/s3-kms-key
fabio-rizzo-01 Nov 14, 2025
b604568
Feature/s3 kms key (#18)
fabio-rizzo-01 Nov 14, 2025
d85e4cf
fixed issue that broke integration tests and updated code based on PR…
fabio-rizzo-01 Nov 20, 2025
420c509
Merge remote-tracking branch 'private/feature/s3-kms-key' into featur…
fabio-rizzo-01 Nov 20, 2025
a7d6413
adding support to use a kms key for s3 buckets (#17)
fabio-rizzo-01 Nov 14, 2025
e4c8260
Feature/s3 kms key (#18)
fabio-rizzo-01 Nov 14, 2025
ab5cdf0
Merge remote-tracking branch 'private/main-30-06' into main-30-06
fabio-rizzo-01 Nov 20, 2025
7192c2f
Merge remote-tracking branch 'private/main-30-06' into feature/s3-kms…
fabio-rizzo-01 Nov 20, 2025
4b2dcd3
fixed issue that broke integration tests and updated code based on PR…
fabio-rizzo-01 Nov 20, 2025
5da097a
Feature/s3 kms key (#19)
fabio-rizzo-01 Nov 20, 2025
8bd5b74
adding support to use a kms key for s3 buckets (#17)
fabio-rizzo-01 Nov 14, 2025
8a1fe7e
Feature/s3 kms key (#18)
fabio-rizzo-01 Nov 14, 2025
5a60a69
Feature/s3 kms key (#19)
fabio-rizzo-01 Nov 20, 2025
2e882bf
Merge remote-tracking branch 'private/main-30-06' into main-30-06
fabio-rizzo-01 Nov 20, 2025
2addad4
Merge remote-tracking branch 'private/main-30-06' into feature/s3-kms…
fabio-rizzo-01 Nov 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti

### New Features

- Added KMS properties (optional) to catalog storage config to enable S3 data encryption.
- Added a finer grained authorization model for UpdateTable requests. Existing privileges continue to work for granting UpdateTable, such as `TABLE_WRITE_PROPERTIES`.
However, you can now instead grant privileges just for specific operations, such as `TABLE_ADD_SNAPSHOT`
- Added a Management API endpoint to reset principal credentials, controlled by the `ENABLE_CREDENTIAL_RESET` (default: true) feature flag.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public class CatalogSerializationTest {
private static final String TEST_LOCATION = "s3://test/";
private static final String TEST_CATALOG_NAME = "test-catalog";
private static final String TEST_ROLE_ARN = "arn:aws:iam::123456789012:role/test-role";
private static final String KMS_KEY = "arn:aws:kms:us-east-1:012345678901:key/allowed-key-1";

@BeforeEach
public void setUp() {
Expand Down Expand Up @@ -70,6 +71,36 @@ public void testJsonFormat() throws JsonProcessingException {
+ "\"properties\":{\"default-base-location\":\"s3://test/\"},"
+ "\"storageConfigInfo\":{"
+ "\"roleArn\":\"arn:aws:iam::123456789012:role/test-role\","
+ "\"allowedKmsKeys\":[],"
Copy link
Contributor

@dimas-b dimas-b Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit of a concern, unfortunately. (Sorry, I did not notice it before because of the other test code changes).

This means old clients will see the new allowedKmsKeys and other new properties (after a server upgrade) even if they were not set in their catalogs... IMHO, it's a potential backward compatibility issue.

However, our history shows that the community might be ok with that kind of change.
From my POV, it would be preferable to avoid changes on the wire, if possible... Meaning that defaults on newly added properties should not be visible to old clients... WDYT?

If that is too cumbersome, I'd like some more reviewer to express their opinions whether exposing new properties by default is acceptable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder, why does allowedKmsKeys show up in the default config JSON? I believe null values are not serialized... Is it not null by default? Could we make all new entries null by default to reduce impact to default data on the wire and handle those nulls in java code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is the immutable class that sets them to empty collection by default , this only happens with collections, and I didn't want to touch those setting because they might cause a lot more changes.
IMHO i don't think seeing the empty field returned is a massive issue but I'll leave the judgment to you guys.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a quick test with an old version of Polaris CLI, and it was able to create a non-KMS catalog without any errors reported.

I'm personally ok with this change.

+ "\"pathStyleAccess\":false,"
+ "\"storageType\":\"S3\","
+ "\"allowedLocations\":[]"
+ "}}");
}

@Test
public void testJsonFormatWithKmsProperties() throws JsonProcessingException {
Catalog catalog =
new Catalog(
Catalog.TypeEnum.INTERNAL,
TEST_CATALOG_NAME,
new CatalogProperties(TEST_LOCATION),
AwsStorageConfigInfo.builder(StorageConfigInfo.StorageTypeEnum.S3)
.setRoleArn(TEST_ROLE_ARN)
.setCurrentKmsKey(KMS_KEY)
.build());

String json = mapper.writeValueAsString(catalog);

assertThat(json)
.isEqualTo(
"{\"type\":\"INTERNAL\","
+ "\"name\":\"test-catalog\","
+ "\"properties\":{\"default-base-location\":\"s3://test/\"},"
+ "\"storageConfigInfo\":{"
+ "\"roleArn\":\"arn:aws:iam::123456789012:role/test-role\","
+ "\"currentKmsKey\":\"arn:aws:kms:us-east-1:012345678901:key/allowed-key-1\","
+ "\"allowedKmsKeys\":[],"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you make a separate test case for this? This "minimal" config is intended to test that older clients do not get new optional properties.

+ "\"pathStyleAccess\":false,"
+ "\"storageType\":\"S3\","
+ "\"allowedLocations\":[]"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ private StorageConfigInfo getStorageInfo(Map<String, String> internalProperties)
.setRoleArn(awsConfig.getRoleARN())
.setExternalId(awsConfig.getExternalId())
.setUserArn(awsConfig.getUserARN())
.setCurrentKmsKey(awsConfig.getCurrentKmsKey())
.setAllowedKmsKeys(awsConfig.getAllowedKmsKeys())
.setStorageType(StorageConfigInfo.StorageTypeEnum.S3)
.setAllowedLocations(awsConfig.getAllowedLocations())
.setRegion(awsConfig.getRegion())
Expand Down Expand Up @@ -308,6 +310,8 @@ public Builder setStorageConfigurationInfo(
AwsStorageConfigurationInfo.builder()
.allowedLocations(allowedLocations)
.roleARN(awsConfigModel.getRoleArn())
.currentKmsKey(awsConfigModel.getCurrentKmsKey())
.allowedKmsKeys(awsConfigModel.getAllowedKmsKeys())
.externalId(awsConfigModel.getExternalId())
.region(awsConfigModel.getRegion())
.endpoint(awsConfigModel.getEndpoint())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import jakarta.annotation.Nonnull;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
Expand All @@ -33,6 +34,8 @@
import org.apache.polaris.core.storage.StorageAccessProperty;
import org.apache.polaris.core.storage.StorageUtil;
import org.apache.polaris.core.storage.aws.StsClientProvider.StsDestination;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.policybuilder.iam.IamConditionOperator;
import software.amazon.awssdk.policybuilder.iam.IamEffect;
Expand All @@ -49,6 +52,9 @@ public class AwsCredentialsStorageIntegration
private final StsClientProvider stsClientProvider;
private final Optional<AwsCredentialsProvider> credentialsProvider;

private static final Logger LOGGER =
LoggerFactory.getLogger(AwsCredentialsStorageIntegration.class);

public AwsCredentialsStorageIntegration(
AwsStorageConfigurationInfo config, StsClient fixedClient) {
this(config, (destination) -> fixedClient);
Expand Down Expand Up @@ -80,6 +86,7 @@ public StorageAccessConfig getSubscopedCreds(
realmConfig.getConfig(STORAGE_CREDENTIAL_DURATION_SECONDS);
AwsStorageConfigurationInfo storageConfig = config();
String region = storageConfig.getRegion();
String accountId = storageConfig.getAwsAccountId();
StorageAccessConfig.Builder accessConfig = StorageAccessConfig.builder();

if (shouldUseSts(storageConfig)) {
Expand All @@ -90,10 +97,12 @@ public StorageAccessConfig getSubscopedCreds(
.roleSessionName("PolarisAwsCredentialsStorageIntegration")
.policy(
policyString(
storageConfig.getAwsPartition(),
storageConfig,
allowListOperation,
allowedReadLocations,
allowedWriteLocations)
allowedWriteLocations,
region,
accountId)
.toJson())
.durationSeconds(storageCredentialDurationSeconds);
credentialsProvider.ifPresent(
Expand Down Expand Up @@ -163,12 +172,13 @@ private boolean shouldUseSts(AwsStorageConfigurationInfo storageConfig) {
* ListBucket privileges with no resources. This prevents us from sending an empty policy to AWS
* and just assuming the role with full privileges.
*/
// TODO - add KMS key access
private IamPolicy policyString(
String awsPartition,
AwsStorageConfigurationInfo storageConfigurationInfo,
boolean allowList,
Set<String> readLocations,
Set<String> writeLocations) {
Set<String> writeLocations,
String region,
String accountId) {
IamPolicy.Builder policyBuilder = IamPolicy.builder();
IamStatement.Builder allowGetObjectStatementBuilder =
IamStatement.builder()
Expand All @@ -178,7 +188,9 @@ private IamPolicy policyString(
Map<String, IamStatement.Builder> bucketListStatementBuilder = new HashMap<>();
Map<String, IamStatement.Builder> bucketGetLocationStatementBuilder = new HashMap<>();

String arnPrefix = arnPrefixForPartition(awsPartition);
String arnPrefix = arnPrefixForPartition(storageConfigurationInfo.getAwsPartition());
String currentKmsKey = storageConfigurationInfo.getCurrentKmsKey();
List<String> allowedKmsKeys = storageConfigurationInfo.getAllowedKmsKeys();
Stream.concat(readLocations.stream(), writeLocations.stream())
.distinct()
.forEach(
Expand Down Expand Up @@ -225,6 +237,9 @@ private IamPolicy policyString(
arnPrefix + StorageUtil.concatFilePrefixes(parseS3Path(uri), "*", "/")));
});
policyBuilder.addStatement(allowPutObjectStatementBuilder.build());
addKmsKeyPolicy(currentKmsKey, allowedKmsKeys, policyBuilder, true, region, accountId);
} else {
addKmsKeyPolicy(currentKmsKey, allowedKmsKeys, policyBuilder, false, region, accountId);
}
if (!bucketListStatementBuilder.isEmpty()) {
bucketListStatementBuilder
Expand All @@ -242,6 +257,86 @@ private IamPolicy policyString(
return policyBuilder.addStatement(allowGetObjectStatementBuilder.build()).build();
}

private static void addKmsKeyPolicy(
String kmsKeyArn,
List<String> allowedKmsKeys,
IamPolicy.Builder policyBuilder,
boolean canWrite,
String region,
String accountId) {

IamStatement.Builder allowKms = buildBaseKmsStatement(canWrite);
boolean hasCurrentKey = kmsKeyArn != null;
boolean hasAllowedKeys = hasAllowedKmsKeys(allowedKmsKeys);

if (hasCurrentKey) {
addKmsKeyResource(kmsKeyArn, allowKms);
}

if (hasAllowedKeys) {
addAllowedKmsKeyResources(allowedKmsKeys, allowKms);
}

// Add KMS statement if we have any KMS key configuration
if (hasCurrentKey || hasAllowedKeys) {
policyBuilder.addStatement(allowKms.build());
} else if (!canWrite) {
// Only add wildcard KMS access for read-only operations when no specific keys are configured
// this check is for minio because it doesn't have region or account id
if (region != null && accountId != null) {
addAllKeysResource(region, accountId, allowKms);
policyBuilder.addStatement(allowKms.build());
}
}
}

private static IamStatement.Builder buildBaseKmsStatement(boolean canEncrypt) {
IamStatement.Builder allowKms =
IamStatement.builder()
.effect(IamEffect.ALLOW)
.addAction("kms:GenerateDataKeyWithoutPlaintext")
.addAction("kms:DescribeKey")
.addAction("kms:Decrypt")
.addAction("kms:GenerateDataKey");

if (canEncrypt) {
allowKms.addAction("kms:Encrypt");
}

return allowKms;
}

private static void addKmsKeyResource(String kmsKeyArn, IamStatement.Builder allowKms) {
if (kmsKeyArn != null) {
LOGGER.debug("Adding KMS key policy for key {}", kmsKeyArn);
allowKms.addResource(IamResource.create(kmsKeyArn));
}
}

private static boolean hasAllowedKmsKeys(List<String> allowedKmsKeys) {
return allowedKmsKeys != null && !allowedKmsKeys.isEmpty();
}

private static void addAllowedKmsKeyResources(
List<String> allowedKmsKeys, IamStatement.Builder allowKms) {
allowedKmsKeys.forEach(
keyArn -> {
LOGGER.debug("Adding allowed KMS key policy for key {}", keyArn);
allowKms.addResource(IamResource.create(keyArn));
});
}

private static void addAllKeysResource(
String region, String accountId, IamStatement.Builder allowKms) {
String allKeysArn = arnKeyAll(region, accountId);
allowKms.addResource(IamResource.create(allKeysArn));
LOGGER.debug("Adding KMS key policy for all keys in account {}", accountId);
}

private static String arnKeyAll(String region, String accountId) {
return String.format("arn:aws:kms:%s:%s:key/*", region, accountId);
}

private static String arnPrefixForPartition(String awsPartition) {
return String.format("arn:%s:s3:::", awsPartition != null ? awsPartition : "aws");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import jakarta.annotation.Nullable;
import java.net.URI;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo;
Expand Down Expand Up @@ -63,6 +64,14 @@ public String getFileIoImplClassName() {
@Nullable
public abstract String getRoleARN();

/** KMS Key ARN for server-side encryption,used for writes, optional */
@Nullable
public abstract String getCurrentKmsKey();

/** Comma-separated list of allowed KMS Key ARNs, optional */
@Nullable
public abstract List<String> getAllowedKmsKeys();

/** AWS external ID, optional */
@Nullable
public abstract String getExternalId();
Expand Down
Loading