-
Notifications
You must be signed in to change notification settings - Fork 332
Add KMS support for S3 #1424
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add KMS support for S3 #1424
Changes from 10 commits
931eb2c
ac045b5
c08749f
963e940
5c95751
62e4171
271e908
2695f12
a4789d2
d0e94c9
8fc808a
2ad69dd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ | |
| */ | ||
| package org.apache.polaris.core.storage.aws; | ||
|
|
||
| import static org.apache.polaris.core.config.FeatureConfiguration.KMS_SUPPORT_LEVEL_S3; | ||
| import static org.apache.polaris.core.config.FeatureConfiguration.STORAGE_CREDENTIAL_DURATION_SECONDS; | ||
|
|
||
| import jakarta.annotation.Nonnull; | ||
|
|
@@ -27,6 +28,7 @@ | |
| import java.util.Optional; | ||
| import java.util.Set; | ||
| import java.util.stream.Stream; | ||
| import org.apache.polaris.core.config.FeatureConfiguration; | ||
| import org.apache.polaris.core.context.CallContext; | ||
| import org.apache.polaris.core.storage.AccessConfig; | ||
| import org.apache.polaris.core.storage.InMemoryStorageIntegration; | ||
|
|
@@ -81,10 +83,11 @@ public AccessConfig getSubscopedCreds( | |
| .roleSessionName("PolarisAwsCredentialsStorageIntegration") | ||
| .policy( | ||
| policyString( | ||
| storageConfig.getRoleARN(), | ||
| storageConfig, | ||
| allowListOperation, | ||
| allowedReadLocations, | ||
| allowedWriteLocations) | ||
| allowedWriteLocations, | ||
| callContext) | ||
| .toJson()) | ||
| .durationSeconds(storageCredentialDurationSeconds); | ||
| credentialsProvider.ifPresent( | ||
|
|
@@ -141,9 +144,12 @@ public AccessConfig getSubscopedCreds( | |
| * 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 roleArn, boolean allowList, Set<String> readLocations, Set<String> writeLocations) { | ||
| AwsStorageConfigurationInfo awsStorageConfigurationInfo, | ||
| boolean allowList, | ||
| Set<String> readLocations, | ||
| Set<String> writeLocations, | ||
| CallContext callContext) { | ||
| IamPolicy.Builder policyBuilder = IamPolicy.builder(); | ||
| IamStatement.Builder allowGetObjectStatementBuilder = | ||
| IamStatement.builder() | ||
|
|
@@ -153,7 +159,10 @@ private IamPolicy policyString( | |
| Map<String, IamStatement.Builder> bucketListStatementBuilder = new HashMap<>(); | ||
| Map<String, IamStatement.Builder> bucketGetLocationStatementBuilder = new HashMap<>(); | ||
|
|
||
| String arnPrefix = getArnPrefixFor(roleArn); | ||
| String roleARN = awsStorageConfigurationInfo.getRoleARN(); | ||
| String arnPrefix = getArnPrefixFor(roleARN); | ||
| String region = awsStorageConfigurationInfo.getRegion(); | ||
| String awsAccountId = awsStorageConfigurationInfo.getAwsAccountId(); | ||
| Stream.concat(readLocations.stream(), writeLocations.stream()) | ||
| .distinct() | ||
| .forEach( | ||
|
|
@@ -214,7 +223,32 @@ private IamPolicy policyString( | |
| bucketGetLocationStatementBuilder | ||
| .values() | ||
| .forEach(statementBuilder -> policyBuilder.addStatement(statementBuilder.build())); | ||
| return policyBuilder.addStatement(allowGetObjectStatementBuilder.build()).build(); | ||
|
|
||
| policyBuilder.addStatement(allowGetObjectStatementBuilder.build()); | ||
|
|
||
| if (isKMSSupported(callContext)) { | ||
|
||
| policyBuilder.addStatement( | ||
| IamStatement.builder() | ||
| .effect(IamEffect.ALLOW) | ||
| .addAction("kms:GenerateDataKey") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we separate out the read and write paths into two different statements? |
||
| .addAction("kms:Decrypt") | ||
| .addAction("kms:DescribeKey") | ||
| .addResource(getKMSArnPrefix(roleARN) + region + ":" + awsAccountId + ":key/*") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this come from the catalog configuration instead?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean the key? I was thinking explicity key support can be added in a follow-up PR where we can support KMS at a table level |
||
| .addCondition(IamConditionOperator.STRING_EQUALS, "aws:PrincipalArn", roleARN) | ||
| .addCondition( | ||
| IamConditionOperator.STRING_LIKE, | ||
| "kms:EncryptionContext:aws:s3:arn", | ||
| getArnPrefixFor(roleARN) | ||
| + StorageUtil.getBucket( | ||
| URI.create(awsStorageConfigurationInfo.getAllowedLocations().get(0))) | ||
|
||
| + "/*") | ||
| .addCondition( | ||
| IamConditionOperator.STRING_EQUALS, | ||
| "kms:ViaService", | ||
| getS3Endpoint(roleARN, region)) | ||
| .build()); | ||
| } | ||
| return policyBuilder.build(); | ||
| } | ||
|
|
||
| private String getArnPrefixFor(String roleArn) { | ||
|
|
@@ -227,6 +261,24 @@ private String getArnPrefixFor(String roleArn) { | |
| } | ||
| } | ||
|
|
||
| private static String getKMSArnPrefix(String roleArn) { | ||
| if (roleArn.contains("aws-cn")) { | ||
| return "arn:aws-cn:kms:"; | ||
| } else if (roleArn.contains("aws-us-gov")) { | ||
| return "arn:aws-us-gov:kms:"; | ||
| } else { | ||
| return "arn:aws:kms:"; | ||
| } | ||
| } | ||
|
|
||
| private static String getS3Endpoint(String roleArn, String region) { | ||
| if (roleArn.contains("aws-cn")) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could do the following instead: Region regionObj = Region.of(region);
String.format("s3.%s.%s", region.id(), region.metadata().partition().dnsSuffix())) |
||
| return "s3." + region + ".amazonaws.com.cn"; | ||
| } else { | ||
| return "s3." + region + ".amazonaws.com"; | ||
| } | ||
| } | ||
|
|
||
| private static @Nonnull String parseS3Path(URI uri) { | ||
| String bucket = StorageUtil.getBucket(uri); | ||
| String path = trimLeadingSlash(uri.getPath()); | ||
|
|
@@ -239,4 +291,11 @@ private String getArnPrefixFor(String roleArn) { | |
| } | ||
| return path; | ||
| } | ||
|
|
||
| private boolean isKMSSupported(CallContext callContext) { | ||
| return !callContext | ||
| .getRealmConfig() | ||
| .getConfig(KMS_SUPPORT_LEVEL_S3) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With this approach to getting the config value, is
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm deferring the table level support in a follow-up PR
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How is this method going to work in the follow-up PR? :) |
||
| .equals(FeatureConfiguration.KmsSupportLevel.NONE); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to add an end-to-end test for non-default values... Otherwise, how do we know they work if configured? 😉
Ideally, we should test with MinIO too... It looks like MinIO has KMS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have Java based e2e tests? I can work on adding one for this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cf. https://github.com/apache/polaris/blob/main/runtime/service/src/intTest/java/org/apache/polaris/service/it/RestCatalogMinIOSpecialIT.java
Cf. https://github.com/apache/polaris/blob/main/runtime/service/src/intTest/java/org/apache/polaris/service/it/PolarisRestCatalogMinIOIT.java