Skip to content

Commit 28131a7

Browse files
committed
merge from main, address comments from @flyrain
2 parents 6d82902 + f41d5bf commit 28131a7

File tree

97 files changed

+1515
-827
lines changed

Some content is hidden

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

97 files changed

+1515
-827
lines changed

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/baselibs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@ idea-ext = { module = "gradle.plugin.org.jetbrains.gradle.plugin.idea-ext:gradle
2323
jandex = { module = "org.kordamp.gradle:jandex-gradle-plugin", version = "2.2.0" }
2424
license-report = { module = "com.github.jk1:gradle-license-report", version = "2.9" }
2525
nexus-publish = { module = "io.github.gradle-nexus:publish-plugin", version = "2.0.0" }
26-
shadow = { module = "com.gradleup.shadow:shadow-gradle-plugin", version = "9.0.2" }
26+
shadow = { module = "com.gradleup.shadow:shadow-gradle-plugin", version = "9.1.0" }
2727
spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version = "7.2.1" }

gradle/libs.versions.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ swagger = "1.6.16"
4141
antlr4-runtime = { module = "org.antlr:antlr4-runtime", version.strictly = "4.9.3" } # spark integration tests
4242
assertj-core = { module = "org.assertj:assertj-core", version = "3.27.4" }
4343
auth0-jwt = { module = "com.auth0:java-jwt", version = "4.5.0" }
44-
awssdk-bom = { module = "software.amazon.awssdk:bom", version = "2.32.29" }
44+
awssdk-bom = { module = "software.amazon.awssdk:bom", version = "2.33.0" }
4545
awaitility = { module = "org.awaitility:awaitility", version = "4.3.0" }
4646
azuresdk-bom = { module = "com.azure:azure-sdk-bom", version = "1.2.37" }
4747
caffeine = { module = "com.github.ben-manes.caffeine:caffeine", version = "3.2.2" }
@@ -90,7 +90,7 @@ prometheus-metrics-exporter-servlet-jakarta = { module = "io.prometheus:promethe
9090
quarkus-bom = { module = "io.quarkus.platform:quarkus-bom", version.ref = "quarkus" }
9191
scala212-lang-library = { module = "org.scala-lang:scala-library", version.ref = "scala212" }
9292
scala212-lang-reflect = { module = "org.scala-lang:scala-reflect", version.ref = "scala212" }
93-
s3mock-testcontainers = { module = "com.adobe.testing:s3mock-testcontainers", version = "4.7.0" }
93+
s3mock-testcontainers = { module = "com.adobe.testing:s3mock-testcontainers", version = "4.8.0" }
9494
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
9595
smallrye-common-annotation = { module = "io.smallrye.common:smallrye-common-annotation", version = "2.13.8" }
9696
smallrye-config-core = { module = "io.smallrye.config:smallrye-config-core", version = "3.13.4" }

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/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcBasePersistenceImpl.java

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import java.util.function.Predicate;
3838
import java.util.stream.Collectors;
3939
import org.apache.polaris.core.PolarisCallContext;
40+
import org.apache.polaris.core.PolarisDiagnostics;
4041
import org.apache.polaris.core.entity.EntityNameLookupRecord;
4142
import org.apache.polaris.core.entity.LocationBasedEntity;
4243
import org.apache.polaris.core.entity.PolarisBaseEntity;
@@ -79,6 +80,7 @@ public class JdbcBasePersistenceImpl implements BasePersistence, IntegrationPers
7980

8081
private static final Logger LOGGER = LoggerFactory.getLogger(JdbcBasePersistenceImpl.class);
8182

83+
private final PolarisDiagnostics diagnostics;
8284
private final DatasourceOperations datasourceOperations;
8385
private final PrincipalSecretsGenerator secretsGenerator;
8486
private final PolarisStorageIntegrationProvider storageIntegrationProvider;
@@ -89,11 +91,13 @@ public class JdbcBasePersistenceImpl implements BasePersistence, IntegrationPers
8991
private static final int MAX_LOCATION_COMPONENTS = 40;
9092

9193
public JdbcBasePersistenceImpl(
94+
PolarisDiagnostics diagnostics,
9295
DatasourceOperations databaseOperations,
9396
PrincipalSecretsGenerator secretsGenerator,
9497
PolarisStorageIntegrationProvider storageIntegrationProvider,
9598
String realmId,
9699
int schemaVersion) {
100+
this.diagnostics = diagnostics;
97101
this.datasourceOperations = databaseOperations;
98102
this.secretsGenerator = secretsGenerator;
99103
this.storageIntegrationProvider = storageIntegrationProvider;
@@ -832,6 +836,42 @@ public PolarisPrincipalSecrets generateNewPrincipalSecrets(
832836
return principalSecrets;
833837
}
834838

839+
@Nullable
840+
@Override
841+
public PolarisPrincipalSecrets storePrincipalSecrets(
842+
@Nonnull PolarisCallContext callCtx,
843+
long principalId,
844+
@Nonnull String resolvedClientId,
845+
String customClientSecret) {
846+
PolarisPrincipalSecrets principalSecrets =
847+
new PolarisPrincipalSecrets(principalId, resolvedClientId, customClientSecret);
848+
try {
849+
ModelPrincipalAuthenticationData modelPrincipalAuthenticationData =
850+
ModelPrincipalAuthenticationData.fromPrincipalAuthenticationData(principalSecrets);
851+
datasourceOperations.executeUpdate(
852+
QueryGenerator.generateInsertQuery(
853+
ModelPrincipalAuthenticationData.ALL_COLUMNS,
854+
ModelPrincipalAuthenticationData.TABLE_NAME,
855+
modelPrincipalAuthenticationData
856+
.toMap(datasourceOperations.getDatabaseType())
857+
.values()
858+
.stream()
859+
.toList(),
860+
realmId));
861+
} catch (SQLException e) {
862+
LOGGER.error(
863+
"Failed to reset PrincipalSecrets for clientId: {}, due to {}",
864+
resolvedClientId,
865+
e.getMessage(),
866+
e);
867+
throw new RuntimeException(
868+
String.format("Failed to reset PrincipalSecrets for clientId: %s", resolvedClientId), e);
869+
}
870+
871+
// return those
872+
return principalSecrets;
873+
}
874+
835875
@Nullable
836876
@Override
837877
public PolarisPrincipalSecrets rotatePrincipalSecrets(
@@ -844,24 +884,20 @@ public PolarisPrincipalSecrets rotatePrincipalSecrets(
844884
PolarisPrincipalSecrets principalSecrets = loadPrincipalSecrets(callCtx, clientId);
845885

846886
// should be found
847-
callCtx
848-
.getDiagServices()
849-
.checkNotNull(
850-
principalSecrets,
851-
"cannot_find_secrets",
852-
"client_id={} principalId={}",
853-
clientId,
854-
principalId);
887+
diagnostics.checkNotNull(
888+
principalSecrets,
889+
"cannot_find_secrets",
890+
"client_id={} principalId={}",
891+
clientId,
892+
principalId);
855893

856894
// ensure principal id is matching
857-
callCtx
858-
.getDiagServices()
859-
.check(
860-
principalId == principalSecrets.getPrincipalId(),
861-
"principal_id_mismatch",
862-
"expectedId={} id={}",
863-
principalId,
864-
principalSecrets.getPrincipalId());
895+
diagnostics.check(
896+
principalId == principalSecrets.getPrincipalId(),
897+
"principal_id_mismatch",
898+
"expectedId={} id={}",
899+
principalId,
900+
principalSecrets.getPrincipalId());
865901

866902
// rotate the secrets
867903
principalSecrets.rotateSecrets(oldSecretHash);
@@ -1178,7 +1214,7 @@ public <T extends PolarisStorageConfigurationInfo> void persistStorageIntegratio
11781214
PolarisStorageIntegration<T> loadPolarisStorageIntegration(
11791215
@Nonnull PolarisCallContext callContext, @Nonnull PolarisBaseEntity entity) {
11801216
PolarisStorageConfigurationInfo storageConfig =
1181-
BaseMetaStoreManager.extractStorageConfiguration(callContext.getDiagServices(), entity);
1217+
BaseMetaStoreManager.extractStorageConfiguration(diagnostics, entity);
11821218
return storageIntegrationProvider.getStorageIntegrationForConfig(storageConfig);
11831219
}
11841220

persistence/relational-jdbc/src/main/java/org/apache/polaris/persistence/relational/jdbc/JdbcMetaStoreManagerFactory.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ private void initializeForRealm(
103103
realmId,
104104
() ->
105105
new JdbcBasePersistenceImpl(
106+
diagnostics,
106107
datasourceOperations,
107108
secretsGenerator(realmId, rootCredentialsSet),
108109
storageIntegrationProvider,
@@ -177,7 +178,7 @@ public Map<String, BaseResult> purgeRealms(Iterable<String> realms) {
177178
PolarisMetaStoreManager metaStoreManager = getOrCreateMetaStoreManager(realmContext);
178179
BasePersistence session = getOrCreateSession(realmContext);
179180

180-
PolarisCallContext callContext = new PolarisCallContext(realmContext, session, diagnostics);
181+
PolarisCallContext callContext = new PolarisCallContext(realmContext, session);
181182
BaseResult result = metaStoreManager.purge(callContext);
182183
results.put(realm, result);
183184

@@ -216,7 +217,7 @@ public synchronized EntityCache getOrCreateEntityCache(
216217
PolarisMetaStoreManager metaStoreManager = getOrCreateMetaStoreManager(realmContext);
217218
entityCacheMap.put(
218219
realmContext.getRealmIdentifier(),
219-
new InMemoryEntityCache(realmConfig, metaStoreManager));
220+
new InMemoryEntityCache(diagnostics, realmConfig, metaStoreManager));
220221
}
221222

222223
return entityCacheMap.get(realmContext.getRealmIdentifier());
@@ -233,8 +234,7 @@ private PrincipalSecretsResult bootstrapServiceAndCreatePolarisPrincipalForRealm
233234
PolarisMetaStoreManager metaStoreManager =
234235
metaStoreManagerMap.get(realmContext.getRealmIdentifier());
235236
BasePersistence metaStore = sessionSupplierMap.get(realmContext.getRealmIdentifier()).get();
236-
PolarisCallContext polarisContext =
237-
new PolarisCallContext(realmContext, metaStore, diagnostics);
237+
PolarisCallContext polarisContext = new PolarisCallContext(realmContext, metaStore);
238238

239239
Optional<PrincipalEntity> preliminaryRootPrincipal =
240240
metaStoreManager.findRootPrincipal(polarisContext);
@@ -268,8 +268,7 @@ private void checkPolarisServiceBootstrappedForRealm(RealmContext realmContext)
268268
PolarisMetaStoreManager metaStoreManager =
269269
metaStoreManagerMap.get(realmContext.getRealmIdentifier());
270270
BasePersistence metaStore = sessionSupplierMap.get(realmContext.getRealmIdentifier()).get();
271-
PolarisCallContext polarisContext =
272-
new PolarisCallContext(realmContext, metaStore, diagnostics);
271+
PolarisCallContext polarisContext = new PolarisCallContext(realmContext, metaStore);
273272

274273
Optional<PrincipalEntity> rootPrincipal = metaStoreManager.findRootPrincipal(polarisContext);
275274
if (rootPrincipal.isEmpty()) {

0 commit comments

Comments
 (0)