Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0c3651a
initial commit
fivetran-arunsuri Jul 28, 2025
03455e0
formatting
fivetran-arunsuri Jul 28, 2025
4831a84
spotlessApply
fivetran-arunsuri Jul 31, 2025
619a78a
handle few review comments
fivetran-arunsuri Jul 31, 2025
d618bd9
add the api implementation for in memory metastore as well
fivetran-arunsuri Aug 3, 2025
da876d3
add the integration tests
fivetran-arunsuri Aug 4, 2025
5d92646
formatting
fivetran-arunsuri Aug 4, 2025
bc16ffd
fixed more refactoring comments
fivetran-arunsuri Aug 4, 2025
3f87ef7
formatting
fivetran-arunsuri Aug 4, 2025
b07ec0f
import a package
fivetran-arunsuri Aug 11, 2025
3ee7401
fix some comments
fivetran-arunsuri Aug 12, 2025
5e23beb
handle comments
fivetran-arunsuri Aug 14, 2025
a621231
:polaris-core:spotlessApply
fivetran-arunsuri Aug 15, 2025
ac4d8b3
some more refactor
fivetran-arunsuri Aug 15, 2025
d4d98d5
fix test
fivetran-arunsuri Aug 15, 2025
add9284
refactor
fivetran-arunsuri Aug 15, 2025
3ae1f79
comments
fivetran-arunsuri Aug 19, 2025
a4623b6
comments
fivetran-arunsuri Aug 19, 2025
bb5ad29
rebase
fivetran-arunsuri Aug 19, 2025
95d60b5
make changes to PolarisPrincipalSecrets
fivetran-arunsuri Aug 20, 2025
f0ab60d
make changes to APi doc
fivetran-arunsuri Aug 20, 2025
5f0b4f9
handle idempotency
fivetran-arunsuri Aug 21, 2025
e347b9b
handle idempotency
fivetran-arunsuri Aug 22, 2025
fedaa7b
handle if principal exists
fivetran-arunsuri Aug 22, 2025
9560fc9
fix test
fivetran-arunsuri Aug 22, 2025
3c1cb9f
handle comments
fivetran-arunsuri Aug 25, 2025
1b3ca19
handle comments
fivetran-arunsuri Aug 25, 2025
e6a0407
add a FF
fivetran-arunsuri Aug 27, 2025
c7501f1
rebase
fivetran-arunsuri Aug 28, 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ automatic storage credential refresh per table on the client side. Java client v
The endpoint path is always returned when using vended credentials, but clients must enable the
refresh-credentials flag for the desired storage provider.

- Added a Management API endpoint to reset principal credentials, controlled by the `ENABLE_CREDENTIAL_RESET` (default: true) feature flag.

### Changes

- Polaris Management API clients must be prepared to deal with new attributes in `AwsStorageConfigInfo` objects.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ public PrincipalWithCredentials createPrincipal(CreatePrincipalRequest request)
}
}

/**
* Retrieves a Principal by name via the management API.
*
* @param principalName the name of the principal to fetch
* @return the Principal object
*/
public Principal getPrincipal(String principalName) {
try (Response response =
request("v1/principals/{principalName}", Map.of("principalName", principalName)).get()) {
assertThat(response).returns(Response.Status.OK.getStatusCode(), Response::getStatus);
return response.readEntity(Principal.class);
}
}

public void createPrincipalRole(String name) {
createPrincipalRole(new PrincipalRole(name));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,57 @@ public void testCreatePrincipalAndRotateCredentials() {
// rotation that makes the old secret fall off retention.
}

@Test
public void testCreatePrincipalAndResetCredentialsWithCustomValues() {
// Create a new principal using root user
Principal principal =
Principal.builder()
.setName(client.newEntityName("myprincipal-reset"))
.setProperties(Map.of("custom-tag", "bar"))
.build();

PrincipalWithCredentials creds =
managementApi.createPrincipal(new CreatePrincipalRequest(principal, true));

Map<String, String> customBody =
Map.of(
"clientId", "f174b76a7e1a99e2",
"clientSecret", "27029d236abc08e204922b0a07031bc2");

PrincipalWithCredentials resetCreds;
try (Response response =
managementApi
.request("v1/principals/{p}/reset", Map.of("p", principal.getName()))
.post(Entity.json(customBody))) {

assertThat(response).returns(Response.Status.OK.getStatusCode(), Response::getStatus);
resetCreds = response.readEntity(PrincipalWithCredentials.class);
}

assertThat(resetCreds.getCredentials().getClientId()).isEqualTo("f174b76a7e1a99e2");
assertThat(resetCreds.getCredentials().getClientSecret())
.isEqualTo("27029d236abc08e204922b0a07031bc2");

// Validate that the principal entity itself is updated in sync with credentials
Principal updatedPrincipal = managementApi.getPrincipal(principal.getName());
assertThat(updatedPrincipal.getClientId()).isEqualTo("f174b76a7e1a99e2");

// Principal itself tries to reset with custom creds → should fail (403 Forbidden)
String principalToken = client.obtainToken(resetCreds);
customBody =
Map.of(
"clientId", "a174b76a7e1a99e3",
"clientSecret", "37029d236abc08e204922b0a07031bc3");
try (Response response =
client
.managementApi(principalToken)
.request("v1/principals/{p}/reset", Map.of("p", principal.getName()))
.post(Entity.json(customBody))) {

assertThat(response).returns(Response.Status.FORBIDDEN.getStatusCode(), Response::getStatus);
}
}

@Test
public void testCreateFederatedPrincipalRoleSucceeds() {
// Create a federated Principal Role
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -747,4 +747,15 @@ Optional<Optional<String>> hasOverlappingSiblings(
@Nonnull PolarisCallContext callContext, T entity) {
return Optional.empty();
}

@Nullable
@Override
public PolarisPrincipalSecrets storePrincipalSecrets(
@Nonnull PolarisCallContext callCtx,
long principalId,
@Nonnull String resolvedClientId,
String customClientSecret) {
throw new UnsupportedOperationException(
"This method is not supported for EclipseLink as metastore");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,42 @@ public PolarisPrincipalSecrets generateNewPrincipalSecrets(
return principalSecrets;
}

@Nullable
@Override
public PolarisPrincipalSecrets storePrincipalSecrets(
@Nonnull PolarisCallContext callCtx,
long principalId,
@Nonnull String resolvedClientId,
String customClientSecret) {
PolarisPrincipalSecrets principalSecrets =
new PolarisPrincipalSecrets(principalId, resolvedClientId, customClientSecret);
try {
ModelPrincipalAuthenticationData modelPrincipalAuthenticationData =
ModelPrincipalAuthenticationData.fromPrincipalAuthenticationData(principalSecrets);
datasourceOperations.executeUpdate(
QueryGenerator.generateInsertQuery(
ModelPrincipalAuthenticationData.ALL_COLUMNS,
ModelPrincipalAuthenticationData.TABLE_NAME,
modelPrincipalAuthenticationData
.toMap(datasourceOperations.getDatabaseType())
.values()
.stream()
.toList(),
realmId));
} catch (SQLException e) {
LOGGER.error(
"Failed to reset PrincipalSecrets for clientId: {}, due to {}",
resolvedClientId,
e.getMessage(),
e);
throw new RuntimeException(
String.format("Failed to reset PrincipalSecrets for clientId: %s", resolvedClientId), e);
}

// return those
return principalSecrets;
}

@Nullable
@Override
public PolarisPrincipalSecrets rotatePrincipalSecrets(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
package org.apache.polaris.core.auth;

import static org.apache.polaris.core.entity.PolarisEntityConstants.getRootPrincipalName;
import static org.apache.polaris.core.entity.PolarisPrivilege.CATALOG_ATTACH_POLICY;
import static org.apache.polaris.core.entity.PolarisPrivilege.CATALOG_CREATE;
import static org.apache.polaris.core.entity.PolarisPrivilege.CATALOG_DETACH_POLICY;
Expand Down Expand Up @@ -581,6 +582,7 @@ public void authorizeOrThrow(
boolean enforceCredentialRotationRequiredState =
realmConfig.getConfig(
FeatureConfiguration.ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING);
boolean isRoot = getRootPrincipalName().equals(polarisPrincipal.getName());
if (enforceCredentialRotationRequiredState
&& polarisPrincipal
.getProperties()
Expand All @@ -589,6 +591,14 @@ public void authorizeOrThrow(
throw new ForbiddenException(
"Principal '%s' is not authorized for op %s due to PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_STATE",
polarisPrincipal.getName(), authzOp);
} else if (authzOp == PolarisAuthorizableOperation.RESET_CREDENTIALS) {
if (!isRoot) {
throw new ForbiddenException("Only Root principal(service-admin) can perform %s", authzOp);
}
LOGGER
.atDebug()
.addKeyValue("principalName", polarisPrincipal.getName())
.log("Root principal allowed to reset credentials");
} else if (!isAuthorized(polarisPrincipal, activatedEntities, authzOp, targets, secondaries)) {
throw new ForbiddenException(
"Principal '%s' with activated PrincipalRoles '%s' and activated grants via '%s' is not authorized for op %s",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import jakarta.annotation.Nonnull;
import org.apache.polaris.core.PolarisCallContext;
import org.apache.polaris.core.persistence.dao.entity.PrincipalSecretsResult;
import software.amazon.awssdk.annotations.NotNull;

/** Manages secrets for Polaris principals. */
public interface PolarisSecretsManager {
Expand Down Expand Up @@ -54,4 +55,38 @@ PrincipalSecretsResult rotatePrincipalSecrets(
long principalId,
boolean reset,
@Nonnull String oldSecretHash);

/**
* Reset the secrets of a principal entity.
*
* <p>This operation makes the specified secrets (either provided by the caller or newly
* generated) the active credentials for the principal. It effectively overwrites any previous
* secrets and sets the provided values as the new client id/secret for the principal.
*
* @param callCtx call context
* @param principalId id of the principal
* @param resolvedClientId current principal client id
* @param customClientSecret optional new client secret to assign (may be {@code null} if
* system-generated)
* @return the secrets associated with the principal, including the updated client id and secret
*/
@Nonnull
PrincipalSecretsResult resetPrincipalSecrets(
@Nonnull PolarisCallContext callCtx,
long principalId,
@NotNull String resolvedClientId,
String customClientSecret);

/**
* Permanently delete the secrets of a principal.
*
* <p>This operation removes all stored secrets associated with the given principal
*
* @param callCtx call context
* @param clientId principal client id
* @param principalId id of the principal whose secrets should be deleted
*/
@Nonnull
void deletePrincipalSecrets(
@Nonnull PolarisCallContext callCtx, @Nonnull String clientId, long principalId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -367,4 +367,13 @@ public static void enforceFeatureEnabledOrThrow(
+ "it is still possible to enforce the uniqueness of table locations within a catalog.")
.defaultValue(false)
.buildFeatureConfiguration();

public static final FeatureConfiguration<Boolean> ENABLE_CREDENTIAL_RESET =
PolarisConfiguration.<Boolean>builder()
.key("ENABLE_CREDENTIAL_RESET")
.description(
"Flag to enable or disable the API to reset principal credentials. "
+ "Defaults to enabled, but service providers may want to disable it.")
.defaultValue(true)
.buildFeatureConfiguration();
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.annotation.Nullable;
import java.security.SecureRandom;
import org.apache.commons.codec.digest.DigestUtils;

Expand Down Expand Up @@ -138,6 +139,17 @@ public PolarisPrincipalSecrets(long principalId) {
this.secondarySecretHash = hashSecret(secondarySecret);
}

public PolarisPrincipalSecrets(long principalId, String newClientId, @Nullable String newSecret) {
this.principalId = principalId;
this.principalClientId = newClientId;
this.mainSecret = (newSecret != null) ? newSecret : this.generateRandomHexString(32);
this.secondarySecret = this.generateRandomHexString(32);

this.secretSalt = this.generateRandomHexString(16);
this.mainSecretHash = hashSecret(mainSecret);
this.secondarySecretHash = hashSecret(secondarySecret);
}

/** Rotate the main secrets */
public void rotateSecrets(String newSecondaryHash) {
this.secondarySecret = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,15 @@ private void revokeGrantRecord(
: new PrincipalSecretsResult(secrets);
}

/** {@inheritDoc} */
@Override
public @Nonnull void deletePrincipalSecrets(
@Nonnull PolarisCallContext callCtx, @Nonnull String clientId, long principalId) {
// get metastore we should be using
BasePersistence ms = callCtx.getMetaStore();
((IntegrationPersistence) ms).deletePrincipalSecrets(callCtx, clientId, principalId);
}

/** {@inheritDoc} */
@Override
public @Nonnull PrincipalSecretsResult rotatePrincipalSecrets(
Expand Down Expand Up @@ -912,6 +921,30 @@ private void revokeGrantRecord(
: new PrincipalSecretsResult(secrets);
}

@Override
public @Nonnull PrincipalSecretsResult resetPrincipalSecrets(
@Nonnull PolarisCallContext callCtx,
long principalId,
@Nonnull String resolvedClientId,
String customClientSecret) {
// get metastore we should be using
BasePersistence ms = callCtx.getMetaStore();
// if not found, the principal must have been dropped
EntityResult loadEntityResult =
loadEntity(
callCtx, PolarisEntityConstants.getNullId(), principalId, PolarisEntityType.PRINCIPAL);
if (loadEntityResult.getReturnStatus() != BaseResult.ReturnStatus.SUCCESS) {
return new PrincipalSecretsResult(BaseResult.ReturnStatus.ENTITY_NOT_FOUND, null);
}

PolarisPrincipalSecrets secrets =
((IntegrationPersistence) ms)
.storePrincipalSecrets(callCtx, principalId, resolvedClientId, customClientSecret);
return (secrets == null)
? new PrincipalSecretsResult(BaseResult.ReturnStatus.ENTITY_NOT_FOUND, null)
: new PrincipalSecretsResult(secrets);
}

/** {@inheritDoc} */
@Override
public @Nonnull EntityResult createEntityIfNotExists(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,25 @@ PolarisPrincipalSecrets rotatePrincipalSecrets(
boolean reset,
@Nonnull String oldSecretHash);

/**
* Store the secrets of a principal entity.
*
* <p>This method creates and persists new credentials for the given client ID. The credentials
* are expected not to already exist for the given client ID.
*
* @param callCtx call context
* @param principalId the principal id
* @param resolvedClientId
* @param customClientSecret the secret for the principal
* @return the stored principal secrets
*/
@Nullable
PolarisPrincipalSecrets storePrincipalSecrets(
@Nonnull PolarisCallContext callCtx,
long principalId,
@Nonnull String resolvedClientId,
String customClientSecret);

/**
* When dropping a principal, we also need to drop the secrets of that principal
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ public PrincipalSecretsResult loadPrincipalSecrets(
return null;
}

@Override
public void deletePrincipalSecrets(
@Nonnull PolarisCallContext callCtx, @Nonnull String clientId, long principalId) {
diagnostics.fail("illegal_method_in_transaction_workspace", "loadPrincipalSecrets");
}

@Override
public PrincipalSecretsResult rotatePrincipalSecrets(
@Nonnull PolarisCallContext callCtx,
Expand All @@ -172,6 +178,18 @@ public PrincipalSecretsResult rotatePrincipalSecrets(
return null;
}

@Override
public PrincipalSecretsResult resetPrincipalSecrets(
@Nonnull PolarisCallContext callCtx,
long principalId,
@Nonnull String resolvedClientId,
String customClientSecret) {
callCtx
.getDiagServices()
.fail("illegal_method_in_transaction_workspace", "resetPrincipalSecrets");
return null;
}

@Override
public CreateCatalogResult createCatalog(
@Nonnull PolarisCallContext callCtx,
Expand Down
Loading