Skip to content

Commit f5fd7cb

Browse files
authored
Merge branch 'main' into ci-all-in-one
2 parents e8ce7e5 + 3a6f292 commit f5fd7cb

15 files changed

Lines changed: 506 additions & 30 deletions

File tree

.github/workflows/release-3-build-and-publish-artifacts.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,8 @@ jobs:
171171
172172
- name: Stage artifacts to Apache dist dev repository
173173
env:
174-
SVN_USERNAME: ${{ secrets.APACHE_USERNAME }}
175-
SVN_PASSWORD: ${{ secrets.APACHE_PASSWORD }}
174+
SVN_USERNAME: ${{ secrets.POLARIS_SVN_DEV_USERNAME }}
175+
SVN_PASSWORD: ${{ secrets.POLARIS_SVN_DEV_PASSWORD }}
176176
run: |
177177
echo "::add-mask::$SVN_PASSWORD"
178178
@@ -203,8 +203,8 @@ jobs:
203203
- name: Publish and close Apache Nexus staging repository
204204
id: publish-nexus
205205
env:
206-
ORG_GRADLE_PROJECT_apacheUsername: ${{ secrets.APACHE_USERNAME }}
207-
ORG_GRADLE_PROJECT_apachePassword: ${{ secrets.APACHE_PASSWORD }}
206+
ORG_GRADLE_PROJECT_apacheUsername: ${{ secrets.NEXUS_STAGE_DEPLOYER_USER }}
207+
ORG_GRADLE_PROJECT_apachePassword: ${{ secrets.NEXUS_STAGE_DEPLOYER_PW }}
208208
run: |
209209
source "${LIBS_DIR}/_exec.sh"
210210
@@ -379,8 +379,8 @@ jobs:
379379
380380
- name: Stage Helm chart to Apache dist dev repository
381381
env:
382-
SVN_USERNAME: ${{ secrets.APACHE_USERNAME }}
383-
SVN_PASSWORD: ${{ secrets.APACHE_PASSWORD }}
382+
SVN_USERNAME: ${{ secrets.POLARIS_SVN_DEV_USERNAME }}
383+
SVN_PASSWORD: ${{ secrets.POLARIS_SVN_DEV_PASSWORD }}
384384
run: |
385385
echo "::add-mask::$SVN_PASSWORD"
386386

.github/workflows/release-4-publish-release.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,8 @@ jobs:
179179
180180
- name: Copy distribution from SVN dev to release space
181181
env:
182-
SVN_USERNAME: ${{ secrets.APACHE_USERNAME }}
183-
SVN_PASSWORD: ${{ secrets.APACHE_PASSWORD }}
182+
SVN_USERNAME: ${{ secrets.POLARIS_SVN_DEV_USERNAME }}
183+
SVN_PASSWORD: ${{ secrets.POLARIS_SVN_DEV_PASSWORD }}
184184
run: |
185185
echo "::add-mask::$SVN_PASSWORD"
186186
@@ -214,8 +214,8 @@ jobs:
214214

215215
- name: Update Helm index in release space
216216
env:
217-
SVN_USERNAME: ${{ secrets.APACHE_USERNAME }}
218-
SVN_PASSWORD: ${{ secrets.APACHE_PASSWORD }}
217+
SVN_USERNAME: ${{ secrets.POLARIS_SVN_DEV_USERNAME }}
218+
SVN_PASSWORD: ${{ secrets.POLARIS_SVN_DEV_PASSWORD }}
219219
run: |
220220
echo "::add-mask::$SVN_PASSWORD"
221221
@@ -303,8 +303,8 @@ jobs:
303303
- name: Create GitHub release with artifacts
304304
env:
305305
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
306-
SVN_USERNAME: ${{ secrets.APACHE_USERNAME }}
307-
SVN_PASSWORD: ${{ secrets.APACHE_PASSWORD }}
306+
SVN_USERNAME: ${{ secrets.POLARIS_SVN_DEV_USERNAME }}
307+
SVN_PASSWORD: ${{ secrets.POLARIS_SVN_DEV_PASSWORD }}
308308
run: |
309309
echo "::add-mask::$SVN_PASSWORD"
310310
@@ -365,8 +365,8 @@ jobs:
365365
366366
- name: Release candidate repository on Nexus
367367
env:
368-
ORG_GRADLE_PROJECT_apacheUsername: ${{ secrets.APACHE_USERNAME }}
369-
ORG_GRADLE_PROJECT_apachePassword: ${{ secrets.APACHE_PASSWORD }}
368+
ORG_GRADLE_PROJECT_apacheUsername: ${{ secrets.NEXUS_STAGE_DEPLOYER_USER }}
369+
ORG_GRADLE_PROJECT_apachePassword: ${{ secrets.NEXUS_STAGE_DEPLOYER_PW }}
370370
run: |
371371
source "${LIBS_DIR}/_exec.sh"
372372

.github/workflows/release-X-cancel-release-candidate.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ jobs:
121121
122122
- name: Drop Apache Nexus staging repository
123123
env:
124-
NEXUS_USERNAME: ${{ secrets.APACHE_USERNAME }}
125-
NEXUS_PASSWORD: ${{ secrets.APACHE_PASSWORD }}
124+
NEXUS_USERNAME: ${{ secrets.NEXUS_STAGE_DEPLOYER_USER }}
125+
NEXUS_PASSWORD: ${{ secrets.NEXUS_STAGE_DEPLOYER_PW }}
126126
run: |
127127
echo "::add-mask::$NEXUS_PASSWORD"
128128
@@ -174,8 +174,8 @@ jobs:
174174
175175
- name: Delete artifacts from Apache dist dev repository
176176
env:
177-
SVN_USERNAME: ${{ secrets.APACHE_USERNAME }}
178-
SVN_PASSWORD: ${{ secrets.APACHE_PASSWORD }}
177+
SVN_USERNAME: ${{ secrets.POLARIS_SVN_DEV_USERNAME }}
178+
SVN_PASSWORD: ${{ secrets.POLARIS_SVN_DEV_PASSWORD }}
179179
run: |
180180
echo "::add-mask::$SVN_PASSWORD"
181181

.github/workflows/stale.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
---
2121
jobs:
2222
stale:
23+
if: github.repository == 'apache/polaris'
2324
runs-on: ubuntu-24.04
2425
steps:
2526
- uses: actions/stale@dcd2b9469d2220b7e8d08aedc00c105d277fd46b
@@ -33,5 +34,5 @@ on:
3334
schedule:
3435
- cron: "30 1 * * *"
3536
permissions:
36-
issues: read
37+
issues: write
3738
pull-requests: write

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ micrometer-bom = { module = "io.micrometer:micrometer-bom", version = "1.16.2" }
8282
microprofile-fault-tolerance-api = { module = "org.eclipse.microprofile.fault-tolerance:microprofile-fault-tolerance-api", version = "4.1.2" }
8383
mockito-core = { module = "org.mockito:mockito-core", version = "5.21.0" }
8484
mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version = "5.21.0" }
85-
mongodb-driver-sync = { module = "org.mongodb:mongodb-driver-sync", version = "5.6.2" }
85+
mongodb-driver-sync = { module = "org.mongodb:mongodb-driver-sync", version = "5.6.3" }
8686
opentelemetry-bom = { module = "io.opentelemetry:opentelemetry-bom", version = "1.58.0" }
8787
opentelemetry-instrumentation-bom-alpha = { module = "io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom-alpha", version= "2.20.1-alpha" }
8888
picocli = { module = "info.picocli:picocli-codegen", version.ref = "picocli" }

polaris-core/src/main/java/org/apache/polaris/core/storage/aws/AwsCredentialsStorageIntegration.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,17 +102,15 @@ public StorageAccessConfig getSubscopedCreds(
102102

103103
String roleSessionName =
104104
includePrincipalNameInSubscopedCredential
105-
? "polaris-" + polarisPrincipal.getName()
105+
? AwsRoleSessionNameSanitizer.sanitize("polaris-" + polarisPrincipal.getName())
106106
: "PolarisAwsCredentialsStorageIntegration";
107-
String cappedRoleSessionName =
108-
roleSessionName.substring(0, Math.min(roleSessionName.length(), 64));
109107

110108
if (shouldUseSts(storageConfig)) {
111109
AssumeRoleRequest.Builder request =
112110
AssumeRoleRequest.builder()
113111
.externalId(storageConfig.getExternalId())
114112
.roleArn(storageConfig.getRoleARN())
115-
.roleSessionName(cappedRoleSessionName)
113+
.roleSessionName(roleSessionName)
116114
.policy(
117115
policyString(
118116
storageConfig,
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.polaris.core.storage.aws;
20+
21+
import jakarta.annotation.Nonnull;
22+
import java.util.regex.Pattern;
23+
24+
/**
25+
* Utility class for sanitizing AWS STS role session names.
26+
*
27+
* <p>AWS STS role session names must conform to the pattern {@code [\w+=,.@-]*} and have a maximum
28+
* length of 64 characters. This class provides methods to sanitize arbitrary strings (such as
29+
* principal names) into valid role session names.
30+
*
31+
* @see <a href="https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html">AWS STS
32+
* AssumeRole API</a>
33+
*/
34+
public final class AwsRoleSessionNameSanitizer {
35+
36+
/**
37+
* AWS STS role session name maximum length. While the AssumedRoleId can be up to 193 characters,
38+
* the roleSessionName parameter itself is limited to 64 characters.
39+
*/
40+
static final int MAX_ROLE_SESSION_NAME_LENGTH = 64;
41+
42+
/**
43+
* Pattern matching characters that are NOT allowed in AWS STS role session names. AWS allows:
44+
* alphanumeric characters (a-z, A-Z, 0-9), underscore (_), plus (+), equals (=), comma (,),
45+
* period (.), at sign (@), and hyphen (-).
46+
*
47+
* <p>This pattern matches any character outside this allowed set.
48+
*/
49+
private static final Pattern INVALID_ROLE_SESSION_NAME_CHARS =
50+
Pattern.compile("[^a-zA-Z0-9_+=,.@-]");
51+
52+
/** Default replacement character for invalid characters. */
53+
private static final String DEFAULT_REPLACEMENT = "_";
54+
55+
private AwsRoleSessionNameSanitizer() {
56+
// Utility class to prevent instantiation
57+
}
58+
59+
/**
60+
* Sanitizes a string for use as an AWS STS role session name.
61+
*
62+
* <p>This method:
63+
*
64+
* <ol>
65+
* <li>Replaces any characters not matching {@code [\w+=,.@-]} with underscores
66+
* <li>Truncates the result to 64 characters (AWS maximum)
67+
* </ol>
68+
*
69+
* <p>The underscore replacement character was chosen because:
70+
*
71+
* <ul>
72+
* <li>It is always valid in role session names
73+
* <li>It is visually distinct and indicates a substitution occurred
74+
* <li>It does not introduce ambiguity (unlike hyphen which is common in names)
75+
* </ul>
76+
*
77+
* @param input the string to sanitize (typically a principal name)
78+
* @return a sanitized string safe for use as an AWS STS role session name
79+
*/
80+
public static @Nonnull String sanitize(@Nonnull String input) {
81+
String sanitized =
82+
INVALID_ROLE_SESSION_NAME_CHARS.matcher(input).replaceAll(DEFAULT_REPLACEMENT);
83+
return truncate(sanitized);
84+
}
85+
86+
/**
87+
* Truncates a string to the maximum allowed role session name length.
88+
*
89+
* @param input the string to truncate
90+
* @return the truncated string, or the original if already within limits
91+
*/
92+
static @Nonnull String truncate(@Nonnull String input) {
93+
if (input.length() <= MAX_ROLE_SESSION_NAME_LENGTH) {
94+
return input;
95+
}
96+
return input.substring(0, MAX_ROLE_SESSION_NAME_LENGTH);
97+
}
98+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.polaris.core.storage.aws;
20+
21+
import static org.assertj.core.api.Assertions.assertThat;
22+
23+
import java.util.regex.Pattern;
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.params.ParameterizedTest;
26+
import org.junit.jupiter.params.provider.CsvSource;
27+
28+
class AwsRoleSessionNameSanitizerTest {
29+
30+
/** AWS STS role session name validation pattern. */
31+
private static final Pattern AWS_ROLE_SESSION_NAME_PATTERN = Pattern.compile("[\\w+=,.@-]*");
32+
33+
@ParameterizedTest
34+
@CsvSource({
35+
"polaris-Invalid (local),polaris-Invalid__local_",
36+
"service/account:readonly,service_account_readonly",
37+
"user name,user_name",
38+
"polaris-test-principal,polaris-test-principal",
39+
"user@domain.com,user@domain.com",
40+
"key=value,key=value"
41+
})
42+
void testSanitize(String input, String expected) {
43+
assertThat(AwsRoleSessionNameSanitizer.sanitize(input)).isEqualTo(expected);
44+
}
45+
46+
@Test
47+
void testSanitizeTruncatesToMaxLength() {
48+
String longInput = "a".repeat(100);
49+
String result = AwsRoleSessionNameSanitizer.sanitize(longInput);
50+
assertThat(result).hasSize(AwsRoleSessionNameSanitizer.MAX_ROLE_SESSION_NAME_LENGTH);
51+
}
52+
53+
@Test
54+
void testSanitizeOutputMatchesAwsPattern() {
55+
String[] inputs = {
56+
"polaris-Invalid (local)",
57+
"special!@#$%chars",
58+
"path/to/resource",
59+
"very-long-name-" + "x".repeat(100)
60+
};
61+
62+
for (String input : inputs) {
63+
String sanitized = AwsRoleSessionNameSanitizer.sanitize(input);
64+
assertThat(AWS_ROLE_SESSION_NAME_PATTERN.matcher(sanitized).matches())
65+
.as("Sanitized '%s' should match AWS pattern", sanitized)
66+
.isTrue();
67+
assertThat(sanitized.length()).isLessThanOrEqualTo(64);
68+
}
69+
}
70+
}

polaris-core/src/test/java/org/apache/polaris/service/storage/aws/AwsCredentialsStorageIntegrationTest.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,6 +1045,84 @@ public void testGetSubscopedCredsLongPrincipalName() {
10451045
CredentialVendingContext.empty());
10461046
}
10471047

1048+
@Test
1049+
public void testGetSubscopedCredsPrincipalNameWithInvalidCharacters() {
1050+
StsClient stsClient = Mockito.mock(StsClient.class);
1051+
String roleARN = "arn:aws:iam::012345678901:role/jdoe";
1052+
String externalId = "externalId";
1053+
// Principal name with spaces and parentheses - invalid for AWS STS
1054+
PolarisPrincipal polarisPrincipalWithInvalidChars =
1055+
PolarisPrincipal.of("Invalid Principal (local)", Map.of(), Set.of());
1056+
1057+
Mockito.when(stsClient.assumeRole(Mockito.isA(AssumeRoleRequest.class)))
1058+
.thenAnswer(
1059+
invocation -> {
1060+
assertThat(invocation.getArguments()[0])
1061+
.isInstanceOf(AssumeRoleRequest.class)
1062+
.asInstanceOf(InstanceOfAssertFactories.type(AssumeRoleRequest.class))
1063+
.returns(externalId, AssumeRoleRequest::externalId)
1064+
.returns(roleARN, AssumeRoleRequest::roleArn)
1065+
// Spaces and parentheses should be replaced with underscores
1066+
.returns("polaris-Invalid_Principal__local_", AssumeRoleRequest::roleSessionName);
1067+
return ASSUME_ROLE_RESPONSE;
1068+
});
1069+
String warehouseDir = "s3://bucket/path/to/warehouse";
1070+
new AwsCredentialsStorageIntegration(
1071+
AwsStorageConfigurationInfo.builder()
1072+
.addAllowedLocation(warehouseDir)
1073+
.roleARN(roleARN)
1074+
.externalId(externalId)
1075+
.build(),
1076+
stsClient)
1077+
.getSubscopedCreds(
1078+
PRINCIPAL_INCLUDER_REALM_CONFIG,
1079+
true,
1080+
Set.of(warehouseDir + "/namespace/table"),
1081+
Set.of(warehouseDir + "/namespace/table"),
1082+
polarisPrincipalWithInvalidChars,
1083+
Optional.of("/namespace/table/credentials"),
1084+
CredentialVendingContext.empty());
1085+
}
1086+
1087+
@Test
1088+
public void testGetSubscopedCredsPrincipalNameWithSpecialCharacters() {
1089+
StsClient stsClient = Mockito.mock(StsClient.class);
1090+
String roleARN = "arn:aws:iam::012345678901:role/jdoe";
1091+
String externalId = "externalId";
1092+
// Principal name with slashes and colons
1093+
PolarisPrincipal polarisPrincipalWithSpecialChars =
1094+
PolarisPrincipal.of("service/account:readonly", Map.of(), Set.of());
1095+
1096+
Mockito.when(stsClient.assumeRole(Mockito.isA(AssumeRoleRequest.class)))
1097+
.thenAnswer(
1098+
invocation -> {
1099+
assertThat(invocation.getArguments()[0])
1100+
.isInstanceOf(AssumeRoleRequest.class)
1101+
.asInstanceOf(InstanceOfAssertFactories.type(AssumeRoleRequest.class))
1102+
.returns(externalId, AssumeRoleRequest::externalId)
1103+
.returns(roleARN, AssumeRoleRequest::roleArn)
1104+
// Slashes and colons should be replaced with underscores
1105+
.returns("polaris-service_account_readonly", AssumeRoleRequest::roleSessionName);
1106+
return ASSUME_ROLE_RESPONSE;
1107+
});
1108+
String warehouseDir = "s3://bucket/path/to/warehouse";
1109+
new AwsCredentialsStorageIntegration(
1110+
AwsStorageConfigurationInfo.builder()
1111+
.addAllowedLocation(warehouseDir)
1112+
.roleARN(roleARN)
1113+
.externalId(externalId)
1114+
.build(),
1115+
stsClient)
1116+
.getSubscopedCreds(
1117+
PRINCIPAL_INCLUDER_REALM_CONFIG,
1118+
true,
1119+
Set.of(warehouseDir + "/namespace/table"),
1120+
Set.of(warehouseDir + "/namespace/table"),
1121+
polarisPrincipalWithSpecialChars,
1122+
Optional.of("/namespace/table/credentials"),
1123+
CredentialVendingContext.empty());
1124+
}
1125+
10481126
private static @Nonnull String s3Arn(String partition, String bucket, String keyPrefix) {
10491127
String bucketArn = "arn:" + partition + ":s3:::" + bucket;
10501128
if (keyPrefix == null) {

0 commit comments

Comments
 (0)