Skip to content

Commit 7dc06dd

Browse files
committed
merge from main, address comments from @adutra
2 parents 5b19f6f + 32b2c27 commit 7dc06dd

File tree

177 files changed

+7515
-714
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

177 files changed

+7515
-714
lines changed

.github/workflows/stale.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
stale:
2323
runs-on: ubuntu-24.04
2424
steps:
25-
- uses: actions/stale@8f717f0dfca33b78d3c933452e42558e4456c8e7
25+
- uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f
2626
with:
2727
days-before-close: 5
2828
days-before-stale: 30

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ automatic storage credential refresh per table on the client side. Java client v
8888
The endpoint path is always returned when using vended credentials, but clients must enable the
8989
refresh-credentials flag for the desired storage provider.
9090

91+
- Added a Management API endpoint to reset principal credentials, controlled by the `ENABLE_CREDENTIAL_RESET` (default: true) feature flag.
92+
9193
### Changes
9294

9395
- Polaris Management API clients must be prepared to deal with new attributes in `AwsStorageConfigInfo` objects.

gradle/libs.versions.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ hive = "3.1.3"
2424
iceberg = "1.9.2" # Ensure to update the iceberg version in regtests to keep regtests up-to-date
2525
quarkus = "3.25.4"
2626
immutables = "2.11.3"
27+
jmh = "1.37"
2728
picocli = "4.7.7"
2829
scala212 = "2.12.19"
2930
spark35 = "3.5.6"
@@ -41,7 +42,7 @@ swagger = "1.6.16"
4142
antlr4-runtime = { module = "org.antlr:antlr4-runtime", version.strictly = "4.9.3" } # spark integration tests
4243
assertj-core = { module = "org.assertj:assertj-core", version = "3.27.4" }
4344
auth0-jwt = { module = "com.auth0:java-jwt", version = "4.5.0" }
44-
awssdk-bom = { module = "software.amazon.awssdk:bom", version = "2.32.29" }
45+
awssdk-bom = { module = "software.amazon.awssdk:bom", version = "2.33.0" }
4546
awaitility = { module = "org.awaitility:awaitility", version = "4.3.0" }
4647
azuresdk-bom = { module = "com.azure:azure-sdk-bom", version = "1.2.37" }
4748
caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version = "3.2.2" }
@@ -75,7 +76,11 @@ jakarta-ws-rs-api = { module = "jakarta.ws.rs:jakarta.ws.rs-api", version = "4.0
7576
jandex = { module = "io.smallrye.jandex:jandex", version ="3.4.0" }
7677
javax-servlet-api = { module = "javax.servlet:javax.servlet-api", version = "4.0.1" }
7778
junit-bom = { module = "org.junit:junit-bom", version = "5.13.4" }
79+
localstack = { module = "org.testcontainers:localstack", version = "1.19.7" }
7880
keycloak-admin-client = { module = "org.keycloak:keycloak-admin-client", version = "26.0.6" }
81+
jcstress-core = { module = "org.openjdk.jcstress:jcstress-core", version = "0.16" }
82+
jmh-core = { module = "org.openjdk.jmh:jmh-core", version.ref = "jmh" }
83+
jmh-generator-annprocess = { module = "org.openjdk.jmh:jmh-generator-annprocess", version.ref = "jmh" }
7984
logback-classic = { module = "ch.qos.logback:logback-classic", version = "1.5.18" }
8085
micrometer-bom = { module = "io.micrometer:micrometer-bom", version = "1.15.3" }
8186
microprofile-fault-tolerance-api = { module = "org.eclipse.microprofile.fault-tolerance:microprofile-fault-tolerance-api", version = "4.1.2" }
@@ -102,6 +107,8 @@ testcontainers-keycloak = { module = "com.github.dasniko:testcontainers-keycloak
102107
threeten-extra = { module = "org.threeten:threeten-extra", version = "1.8.0" }
103108

104109
[plugins]
110+
jcstress = { id = "io.github.reyerizo.gradle.jcstress", version = "0.8.15" }
111+
jmh = { id = "me.champeau.jmh", version = "0.7.3" }
105112
openapi-generator = { id = "org.openapi.generator", version = "7.12.0" }
106113
quarkus = { id = "io.quarkus", version.ref = "quarkus" }
107114
rat = { id = "org.nosphere.apache.rat", version = "0.8.1" }

gradle/projects.main.properties

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,9 @@ polaris-extensions-federation-hive=extensions/federation/hive
4848
polaris-config-docs-annotations=tools/config-docs/annotations
4949
polaris-config-docs-generator=tools/config-docs/generator
5050
polaris-config-docs-site=tools/config-docs/site
51+
52+
# id generation
53+
polaris-idgen-api=persistence/nosql/idgen/api
54+
polaris-idgen-impl=persistence/nosql/idgen/impl
55+
polaris-idgen-mocks=persistence/nosql/idgen/mocks
56+
polaris-idgen-spi=persistence/nosql/idgen/spi

integration-tests/src/main/java/org/apache/polaris/service/it/env/ManagementApi.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,20 @@ public PrincipalWithCredentials createPrincipal(CreatePrincipalRequest request)
8080
}
8181
}
8282

83+
/**
84+
* Retrieves a Principal by name via the management API.
85+
*
86+
* @param principalName the name of the principal to fetch
87+
* @return the Principal object
88+
*/
89+
public Principal getPrincipal(String principalName) {
90+
try (Response response =
91+
request("v1/principals/{principalName}", Map.of("principalName", principalName)).get()) {
92+
assertThat(response).returns(Response.Status.OK.getStatusCode(), Response::getStatus);
93+
return response.readEntity(Principal.class);
94+
}
95+
}
96+
8397
public void createPrincipalRole(String name) {
8498
createPrincipalRole(new PrincipalRole(name));
8599
}

integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisManagementServiceIntegrationTest.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,57 @@ public void testCreatePrincipalAndRotateCredentials() {
880880
// rotation that makes the old secret fall off retention.
881881
}
882882

883+
@Test
884+
public void testCreatePrincipalAndResetCredentialsWithCustomValues() {
885+
// Create a new principal using root user
886+
Principal principal =
887+
Principal.builder()
888+
.setName(client.newEntityName("myprincipal-reset"))
889+
.setProperties(Map.of("custom-tag", "bar"))
890+
.build();
891+
892+
PrincipalWithCredentials creds =
893+
managementApi.createPrincipal(new CreatePrincipalRequest(principal, true));
894+
895+
Map<String, String> customBody =
896+
Map.of(
897+
"clientId", "f174b76a7e1a99e2",
898+
"clientSecret", "27029d236abc08e204922b0a07031bc2");
899+
900+
PrincipalWithCredentials resetCreds;
901+
try (Response response =
902+
managementApi
903+
.request("v1/principals/{p}/reset", Map.of("p", principal.getName()))
904+
.post(Entity.json(customBody))) {
905+
906+
assertThat(response).returns(Response.Status.OK.getStatusCode(), Response::getStatus);
907+
resetCreds = response.readEntity(PrincipalWithCredentials.class);
908+
}
909+
910+
assertThat(resetCreds.getCredentials().getClientId()).isEqualTo("f174b76a7e1a99e2");
911+
assertThat(resetCreds.getCredentials().getClientSecret())
912+
.isEqualTo("27029d236abc08e204922b0a07031bc2");
913+
914+
// Validate that the principal entity itself is updated in sync with credentials
915+
Principal updatedPrincipal = managementApi.getPrincipal(principal.getName());
916+
assertThat(updatedPrincipal.getClientId()).isEqualTo("f174b76a7e1a99e2");
917+
918+
// Principal itself tries to reset with custom creds → should fail (403 Forbidden)
919+
String principalToken = client.obtainToken(resetCreds);
920+
customBody =
921+
Map.of(
922+
"clientId", "a174b76a7e1a99e3",
923+
"clientSecret", "37029d236abc08e204922b0a07031bc3");
924+
try (Response response =
925+
client
926+
.managementApi(principalToken)
927+
.request("v1/principals/{p}/reset", Map.of("p", principal.getName()))
928+
.post(Entity.json(customBody))) {
929+
930+
assertThat(response).returns(Response.Status.FORBIDDEN.getStatusCode(), Response::getStatus);
931+
}
932+
}
933+
883934
@Test
884935
public void testCreateFederatedPrincipalRoleSucceeds() {
885936
// Create a federated Principal Role

integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogViewIntegrationBase.java

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020

2121
import static org.apache.polaris.service.it.env.PolarisClient.polarisClient;
2222

23-
import com.google.common.collect.ImmutableMap;
2423
import java.lang.reflect.Method;
2524
import java.nio.file.Path;
2625
import java.nio.file.Paths;
@@ -88,6 +87,7 @@ public abstract class PolarisRestCatalogViewIntegrationBase extends ViewCatalogT
8887
private static ManagementApi managementApi;
8988

9089
private RESTCatalog restCatalog;
90+
private StorageConfigInfo storageConfig;
9191

9292
@BeforeAll
9393
static void setup(PolarisApiEndpoints apiEndpoints, ClientCredentials credentials) {
@@ -114,7 +114,7 @@ public void before(TestInfo testInfo) {
114114
Method method = testInfo.getTestMethod().orElseThrow();
115115
String catalogName = client.newEntityName(method.getName());
116116

117-
StorageConfigInfo storageConfig = getStorageConfigInfo();
117+
storageConfig = getStorageConfigInfo();
118118
String defaultBaseLocation =
119119
storageConfig.getAllowedLocations().getFirst()
120120
+ "/"
@@ -201,16 +201,10 @@ public void createViewWithCustomMetadataLocationUsingPolaris(@TempDir Path tempD
201201
TableIdentifier identifier = TableIdentifier.of("ns", "view");
202202

203203
String location = Paths.get(tempDir.toUri().toString()).toString();
204-
String customLocation = Paths.get(tempDir.toUri().toString(), "custom-location").toString();
205-
String customLocation2 = Paths.get(tempDir.toUri().toString(), "custom-location2").toString();
206-
String customLocationChild =
207-
Paths.get(tempDir.toUri().toString(), "custom-location/child").toString();
204+
String customLocation =
205+
Paths.get(storageConfig.getAllowedLocations().getFirst(), "/custom-location1").toString();
208206

209-
catalog()
210-
.createNamespace(
211-
identifier.namespace(),
212-
ImmutableMap.of(
213-
IcebergTableLikeEntity.USER_SPECIFIED_WRITE_METADATA_LOCATION_KEY, location));
207+
catalog().createNamespace(identifier.namespace());
214208

215209
Assertions.assertThat(catalog().viewExists(identifier)).as("View should not exist").isFalse();
216210

@@ -234,35 +228,5 @@ public void createViewWithCustomMetadataLocationUsingPolaris(@TempDir Path tempD
234228
Assertions.assertThat(((BaseView) view).operations().current().metadataFileLocation())
235229
.isNotNull()
236230
.startsWith(customLocation);
237-
238-
// CANNOT update the view with a new metadata location `baseLocation/customLocation2`,
239-
// even though the new location is still under the parent namespace's
240-
// `write.metadata.path=baseLocation`.
241-
Assertions.assertThatThrownBy(
242-
() ->
243-
catalog()
244-
.loadView(identifier)
245-
.updateProperties()
246-
.set(
247-
IcebergTableLikeEntity.USER_SPECIFIED_WRITE_METADATA_LOCATION_KEY,
248-
customLocation2)
249-
.commit())
250-
.isInstanceOf(ForbiddenException.class)
251-
.hasMessageContaining("Forbidden: Invalid locations");
252-
253-
// CANNOT update the view with a child metadata location `baseLocation/customLocation/child`,
254-
// even though it is a subpath of the original view's
255-
// `write.metadata.path=baseLocation/customLocation`.
256-
Assertions.assertThatThrownBy(
257-
() ->
258-
catalog()
259-
.loadView(identifier)
260-
.updateProperties()
261-
.set(
262-
IcebergTableLikeEntity.USER_SPECIFIED_WRITE_METADATA_LOCATION_KEY,
263-
customLocationChild)
264-
.commit())
265-
.isInstanceOf(ForbiddenException.class)
266-
.hasMessageContaining("Forbidden: Invalid locations");
267231
}
268232
}

persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,4 +747,15 @@ Optional<Optional<String>> hasOverlappingSiblings(
747747
@Nonnull PolarisCallContext callContext, T entity) {
748748
return Optional.empty();
749749
}
750+
751+
@Nullable
752+
@Override
753+
public PolarisPrincipalSecrets storePrincipalSecrets(
754+
@Nonnull PolarisCallContext callCtx,
755+
long principalId,
756+
@Nonnull String resolvedClientId,
757+
String customClientSecret) {
758+
throw new UnsupportedOperationException(
759+
"This method is not supported for EclipseLink as metastore");
760+
}
750761
}

persistence/eclipselink/src/test/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreManagerTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ protected PolarisTestMetaStoreManager createPolarisTestMetaStoreManager() {
8989
diagServices, store, Mockito.mock(), realmContext, null, "polaris", RANDOM_SECRETS);
9090
TransactionalMetaStoreManagerImpl metaStoreManager =
9191
new TransactionalMetaStoreManagerImpl(clock, diagServices);
92-
PolarisCallContext callCtx = new PolarisCallContext(realmContext, session, diagServices);
92+
PolarisCallContext callCtx = new PolarisCallContext(realmContext, session);
9393
return new PolarisTestMetaStoreManager(metaStoreManager, callCtx);
9494
}
9595

persistence/nosql/idgen/README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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+
20+
# Unique ID generation framework and monotonic clock
21+
22+
Provides a framework and implementations for unique ID generation, including a monotonically increasing timestamp/clock
23+
source.
24+
25+
Provides a
26+
[Snowflake-IDs](https://medium.com/@jitenderkmr/demystifying-snowflake-ids-a-unique-identifier-in-distributed-computing-72796a827c9d)
27+
implementation.
28+
29+
Consuming production should primarily leverage the `IdGenerator` and `MonotonicClock` interfaces.
30+
31+
## Snowflake ID source
32+
33+
The Snowflake ID source is configurable for each backend instance, but cannot be modified for an existing backend
34+
instance to prevent ID conflicts.
35+
36+
The epoch of these timestamps is 2025-03-01-00:00:00.0 GMT. Timestamps occupy 41 bits at
37+
millisecond precision, which lasts for about 69 years. Node-IDs are 10 bits, which allows 1024 concurrently active
38+
"JVMs running Polaris". 12 bits are used by the sequence number, which then allows each node to generate 4096 IDs per
39+
millisecond. One bit is reserved for future use.
40+
41+
Node IDs are leased by every "JVM running Polaris" for a period of time. The ID generator implementation guarantees
42+
that no IDs will be generated for a timestamp that exceeds the "lease time". Leases can be extended. The implementation
43+
leverages atomic database operations (CAS) for the lease implementation.
44+
45+
ID generators must not use timestamps before or after the lease period nor must they re-use an older timestamp. This
46+
requirement is satisfied using a monotonic clock implementation.
47+
48+
## Code structure
49+
50+
The code is structured into multiple modules. Consuming code should almost always pull in only the API module.
51+
52+
* `polaris-idgen-api` provides the necessary Java interfaces and immutable types.
53+
* `polaris-idgen-impl` provides the storage agnostic implementation.
54+
* `polaris-idgen-mocks` provides mocks for testing.
55+
* `polaris-idgen-spi` provides the necessary interfaces to construct ID generators.

0 commit comments

Comments
 (0)