From cd8294949c4114256c37c330d224fc9b345b6c2f Mon Sep 17 00:00:00 2001 From: Rulin Xing Date: Mon, 8 Sep 2025 00:36:59 -0700 Subject: [PATCH 1/7] Service Identity Injection --- .../core/config/FeatureConfiguration.java | 3 +- .../polaris/core/entity/CatalogEntity.java | 7 +- .../DefaultServiceIdentityRegistry.java | 88 +++++++++ .../registry/ServiceIdentityRegistry.java | 56 ++++++ .../ServiceIdentityRegistryFactory.java | 32 ++++ .../ResolvedAwsIamServiceIdentity.java | 119 ++++++++++++ .../resolved/ResolvedServiceIdentity.java | 62 +++++++ .../src/main/resources/application.properties | 7 + .../service/admin/PolarisAdminService.java | 25 ++- .../service/admin/PolarisServiceImpl.java | 8 + .../config/ProductionReadinessChecks.java | 19 ++ .../service/config/ServiceProducers.java | 26 +++ .../AwsIamServiceIdentityConfiguration.java | 74 ++++++++ .../RealmServiceIdentityConfiguration.java | 51 ++++++ ...esolvableServiceIdentityConfiguration.java | 41 +++++ .../ServiceIdentityConfiguration.java | 79 ++++++++ .../ServiceIdentityRegistryConfiguration.java | 35 ++++ ...DefaultServiceIdentityRegistryFactory.java | 140 +++++++++++++++ .../service/admin/ManagementServiceTest.java | 4 + .../admin/PolarisAdminServiceAuthzTest.java | 1 + .../admin/PolarisAdminServiceTest.java | 3 + .../service/admin/PolarisAuthzTestBase.java | 7 + .../service/admin/PolarisServiceImplTest.java | 4 + ...bstractPolarisGenericTableCatalogTest.java | 7 + .../iceberg/AbstractIcebergCatalogTest.java | 7 + .../AbstractIcebergCatalogViewTest.java | 7 + .../policy/AbstractPolicyCatalogTest.java | 7 + .../DefaultServiceIdentityRegistryTest.java | 169 ++++++++++++++++++ .../apache/polaris/service/TestServices.java | 5 + 29 files changed, 1089 insertions(+), 4 deletions(-) create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/identity/registry/DefaultServiceIdentityRegistry.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistry.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistryFactory.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedAwsIamServiceIdentity.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedServiceIdentity.java create mode 100644 runtime/service/src/main/java/org/apache/polaris/service/identity/AwsIamServiceIdentityConfiguration.java create mode 100644 runtime/service/src/main/java/org/apache/polaris/service/identity/RealmServiceIdentityConfiguration.java create mode 100644 runtime/service/src/main/java/org/apache/polaris/service/identity/ResolvableServiceIdentityConfiguration.java create mode 100644 runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityConfiguration.java create mode 100644 runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityRegistryConfiguration.java create mode 100644 runtime/service/src/main/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistryFactory.java create mode 100644 runtime/service/src/test/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistryTest.java diff --git a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java index ad91ad0240..7885c10421 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java @@ -293,7 +293,8 @@ public static void enforceFeatureEnabledOrThrow( .defaultValue( List.of( AuthenticationParameters.AuthenticationTypeEnum.OAUTH.name(), - AuthenticationParameters.AuthenticationTypeEnum.BEARER.name())) + AuthenticationParameters.AuthenticationTypeEnum.BEARER.name(), + AuthenticationParameters.AuthenticationTypeEnum.SIGV4.name())) .buildFeatureConfiguration(); public static final FeatureConfiguration ICEBERG_COMMIT_MAX_RETRIES = diff --git a/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java b/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java index 3558495ae0..2231449ba8 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java @@ -43,6 +43,7 @@ import org.apache.polaris.core.config.BehaviorChangeConfiguration; import org.apache.polaris.core.config.RealmConfig; import org.apache.polaris.core.connection.ConnectionConfigInfoDpo; +import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; import org.apache.polaris.core.secrets.SecretReference; import org.apache.polaris.core.storage.FileStorageConfigurationInfo; import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo; @@ -339,11 +340,13 @@ private void validateMaxAllowedLocations( public Builder setConnectionConfigInfoDpoWithSecrets( ConnectionConfigInfo connectionConfigurationModel, - Map secretReferences) { + Map secretReferences, + ServiceIdentityInfoDpo serviceIdentityInfoDpo) { if (connectionConfigurationModel != null) { ConnectionConfigInfoDpo config = ConnectionConfigInfoDpo.fromConnectionConfigInfoModelWithSecrets( - connectionConfigurationModel, secretReferences); + connectionConfigurationModel, secretReferences) + .withServiceIdentity(serviceIdentityInfoDpo); internalProperties.put( PolarisEntityConstants.getConnectionConfigInfoPropertyName(), config.serialize()); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/DefaultServiceIdentityRegistry.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/DefaultServiceIdentityRegistry.java new file mode 100644 index 0000000000..50f9d3aef0 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/DefaultServiceIdentityRegistry.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.core.identity.registry; + +import com.google.common.annotations.VisibleForTesting; +import java.util.EnumMap; +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.polaris.core.identity.ServiceIdentityType; +import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; +import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; + +/** + * Default implementation of {@link ServiceIdentityRegistry} that resolves service identities from + * statically configured values (typically defined via Quarkus server configuration). + * + *

This implementation supports both multi-tenant (e.g., SaaS) and self-managed (single-tenant) + * Polaris deployments: + * + *

    + *
  • In multi-tenant mode, each tenant (realm) can have its own set of service identities + * defined in the configuration. The same identity will consistently be assigned for each + * {@link ServiceIdentityType} within a given tenant. + *
  • In single-tenant or self-managed deployments, a single set of service identities can be + * defined and used system-wide. + *
+ */ +public class DefaultServiceIdentityRegistry implements ServiceIdentityRegistry { + + /** Map of service identity types to their resolved identities. */ + private final EnumMap resolvedServiceIdentities; + + /** Map of identity info references (URNs) to their resolved service identities. */ + private final Map referenceToResolvedServiceIdentity; + + public DefaultServiceIdentityRegistry( + EnumMap serviceIdentities) { + this.resolvedServiceIdentities = serviceIdentities; + this.referenceToResolvedServiceIdentity = + serviceIdentities.values().stream() + .collect( + Collectors.toMap( + identity -> identity.getIdentityInfoReference().getUrn(), + identity -> identity)); + } + + @Override + public ServiceIdentityInfoDpo assignServiceIdentity(ServiceIdentityType serviceIdentityType) { + ResolvedServiceIdentity resolvedServiceIdentity = + resolvedServiceIdentities.get(serviceIdentityType); + if (resolvedServiceIdentity == null) { + throw new IllegalArgumentException( + "Service identity type not supported: " + serviceIdentityType); + } + return resolvedServiceIdentity.asServiceIdentityInfoDpo(); + } + + @Override + public ResolvedServiceIdentity resolveServiceIdentity( + ServiceIdentityInfoDpo serviceIdentityInfo) { + ResolvedServiceIdentity resolvedServiceIdentity = + referenceToResolvedServiceIdentity.get( + serviceIdentityInfo.getIdentityInfoReference().getUrn()); + return resolvedServiceIdentity; + } + + @VisibleForTesting + public EnumMap getResolvedServiceIdentities() { + return resolvedServiceIdentities; + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistry.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistry.java new file mode 100644 index 0000000000..05f512715d --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistry.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.core.identity.registry; + +import org.apache.polaris.core.identity.ServiceIdentityType; +import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; +import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; + +/** + * A registry interface for managing and resolving service identities in Polaris. + * + *

In a multi-tenant Polaris deployment, each catalog or tenant may be associated with a distinct + * service identity that represents the Polaris service itself when accessing external systems + * (e.g., cloud services like AWS or GCP). This registry provides a central mechanism to manage + * those identities and resolve them at runtime. + * + *

The registry helps abstract the configuration and retrieval of service-managed credentials + * from the logic that uses them. It ensures a consistent and secure way to handle identity + * resolution across different deployment models, including SaaS and self-managed environments. + */ +public interface ServiceIdentityRegistry { + /** + * Assigns a new {@link ServiceIdentityInfoDpo} for the given service identity type. Typically + * used during entity creation to associate a default or generated identity. + * + * @param serviceIdentityType The type of service identity (e.g., AWS_IAM). + * @return A new {@link ServiceIdentityInfoDpo} representing the assigned service identity. + */ + ServiceIdentityInfoDpo assignServiceIdentity(ServiceIdentityType serviceIdentityType); + + /** + * Resolves the given service identity by retrieving the actual credential or secret referenced by + * it, typically from a secret manager or internal credential store. + * + * @param serviceIdentityInfo The service identity metadata to resolve. + * @return A {@link ResolvedServiceIdentity} including credentials and other resolved data. + */ + ResolvedServiceIdentity resolveServiceIdentity(ServiceIdentityInfoDpo serviceIdentityInfo); +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistryFactory.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistryFactory.java new file mode 100644 index 0000000000..b6036d5207 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistryFactory.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.core.identity.registry; + +import org.apache.polaris.core.context.RealmContext; + +/** + * Factory for creating {@link ServiceIdentityRegistry} instances. + * + *

Each {@link ServiceIdentityRegistry} instance is associated with a {@link RealmContext} and is + * responsible for managing the service identities for the user in that realm. + */ +public interface ServiceIdentityRegistryFactory { + ServiceIdentityRegistry getOrCreateServiceIdentityRegistry(RealmContext realmContext); +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedAwsIamServiceIdentity.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedAwsIamServiceIdentity.java new file mode 100644 index 0000000000..dcf928b525 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedAwsIamServiceIdentity.java @@ -0,0 +1,119 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.identity.resolved; + +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import jakarta.annotation.Nonnull; +import org.apache.polaris.core.identity.ServiceIdentityType; +import org.apache.polaris.core.identity.dpo.AwsIamServiceIdentityInfoDpo; +import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; +import org.apache.polaris.core.secrets.ServiceSecretReference; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.services.sts.StsClientBuilder; + +/** + * Represents a fully resolved AWS IAM service identity, including the associated IAM ARN and + * credentials. This class is used internally by Polaris to access AWS services on behalf of a + * configured service identity. + * + *

It contains AWS credentials (access key, secret, and optional session token) and provides a + * lazily initialized {@link StsClient} for performing role assumptions or identity verification. + * + *

The resolved identity can be converted back into its persisted DPO form using {@link + * #asServiceIdentityInfoDpo()}. + */ +public class ResolvedAwsIamServiceIdentity extends ResolvedServiceIdentity { + + /** IAM role or user ARN representing the Polaris service identity. */ + private final String iamArn; + + /** AWS access key ID of the AWS credential associated with the identity. */ + private final String accessKeyId; + + /** AWS secret access key of the AWS credential associated with the identity. */ + private final String secretAccessKey; + + /** The AWS session token of the AWS credential associated with the identity. */ + private final String sessionToken; + + public ResolvedAwsIamServiceIdentity( + String iamArn, String accessKeyId, String secretAccessKey, String sessionToken) { + this(null, iamArn, accessKeyId, secretAccessKey, sessionToken); + } + + public ResolvedAwsIamServiceIdentity( + ServiceSecretReference serviceSecretReference, + String iamArn, + String accessKeyId, + String secretAccessKey, + String sessionToken) { + super(ServiceIdentityType.AWS_IAM, serviceSecretReference); + this.iamArn = iamArn; + this.accessKeyId = accessKeyId; + this.secretAccessKey = secretAccessKey; + this.sessionToken = sessionToken; + } + + public String getIamArn() { + return iamArn; + } + + public String getAccessKeyId() { + return accessKeyId; + } + + public String getSecretAccessKey() { + return secretAccessKey; + } + + public String getSessionToken() { + return sessionToken; + } + + @Nonnull + @Override + public ServiceIdentityInfoDpo asServiceIdentityInfoDpo() { + return new AwsIamServiceIdentityInfoDpo(getIdentityInfoReference()); + } + + /** Returns a memoized supplier for creating an STS client using the resolved credentials. */ + public Supplier stsClientSupplier() { + return Suppliers.memoize( + () -> { + StsClientBuilder stsClientBuilder = StsClient.builder(); + if (getAccessKeyId() != null && getSecretAccessKey() != null) { + StaticCredentialsProvider awsCredentialsProvider = + StaticCredentialsProvider.create( + AwsBasicCredentials.create(getAccessKeyId(), getSecretAccessKey())); + if (getSessionToken() != null) { + awsCredentialsProvider = + StaticCredentialsProvider.create( + AwsSessionCredentials.create( + getAccessKeyId(), getSecretAccessKey(), getSessionToken())); + } + stsClientBuilder.credentialsProvider(awsCredentialsProvider); + } + return stsClientBuilder.build(); + }); + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedServiceIdentity.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedServiceIdentity.java new file mode 100644 index 0000000000..74b6a9516c --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedServiceIdentity.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.core.identity.resolved; + +import jakarta.annotation.Nonnull; +import org.apache.polaris.core.identity.ServiceIdentityType; +import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; +import org.apache.polaris.core.secrets.ServiceSecretReference; +import software.amazon.awssdk.annotations.NotNull; + +/** + * Represents a resolved service identity. + * + *

This class is used to represent the identity of a service after it has been resolved. It + * contains the type of the identity and any additional information for the service identity. E.g., + * The credential of the service identity. + */ +public abstract class ResolvedServiceIdentity { + private final ServiceIdentityType identityType; + private ServiceSecretReference identityInfoReference; + + public ResolvedServiceIdentity(ServiceIdentityType identityType) { + this(identityType, null); + } + + public ResolvedServiceIdentity( + ServiceIdentityType identityType, ServiceSecretReference identityInfoReference) { + this.identityType = identityType; + this.identityInfoReference = identityInfoReference; + } + + public @NotNull ServiceIdentityType getIdentityType() { + return identityType; + } + + public @Nonnull ServiceSecretReference getIdentityInfoReference() { + return identityInfoReference; + } + + public void setIdentityInfoReference(@NotNull ServiceSecretReference identityInfoReference) { + this.identityInfoReference = identityInfoReference; + } + + /** Converts this resolved identity into its corresponding persisted form (DPO). */ + public abstract @Nonnull ServiceIdentityInfoDpo asServiceIdentityInfoDpo(); +} diff --git a/runtime/defaults/src/main/resources/application.properties b/runtime/defaults/src/main/resources/application.properties index a521e8277f..7767e62b78 100644 --- a/runtime/defaults/src/main/resources/application.properties +++ b/runtime/defaults/src/main/resources/application.properties @@ -198,6 +198,12 @@ polaris.oidc.principal-roles-mapper.type=default # polaris.storage.gcp.token=token # polaris.storage.gcp.lifespan=PT1H +# Polaris Service Identity Config +polaris.service-identity.registry.type=default +# polaris.service-identity.aws-iam.iam-arn=arn:aws:iam::123456789012:user/polaris-iam-user +polaris.service-identity.aws-iam.iam-arn=arn:aws:iam::174739373489:user/managed/rxing +polaris.service-identity.my-realm.aws-iam.iam-arn=arn:aws:iam::123456789012:user/polaris-iam-user + quarkus.arc.ignored-split-packages=\ org.apache.polaris.service.catalog.api,\ org.apache.polaris.service.catalog.api.impl,\ @@ -209,6 +215,7 @@ quarkus.arc.ignored-split-packages=\ org.apache.polaris.service.auth.external.tenant,\ org.apache.polaris.service.auth.internal,\ org.apache.polaris.service.events,\ + org.apache.polaris.service.identity,\ org.apache.polaris.service.task,\ org.apache.polaris.service.secrets,\ org.apache.polaris.service.storage,\ diff --git a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java index 23341d7640..8dce9d4783 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java @@ -99,6 +99,9 @@ import org.apache.polaris.core.entity.table.IcebergTableLikeEntity; import org.apache.polaris.core.entity.table.federated.FederatedEntities; import org.apache.polaris.core.exceptions.CommitConflictException; +import org.apache.polaris.core.identity.ServiceIdentityType; +import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; import org.apache.polaris.core.persistence.dao.entity.CreateCatalogResult; @@ -146,6 +149,7 @@ public class PolarisAdminService { private final PolarisAuthorizer authorizer; private final PolarisMetaStoreManager metaStoreManager; private final UserSecretsManager userSecretsManager; + private final ServiceIdentityRegistry serviceIdentityRegistry; private final ReservedProperties reservedProperties; // Initialized in the authorize methods. @@ -157,6 +161,7 @@ public PolarisAdminService( @NotNull ResolutionManifestFactory resolutionManifestFactory, @NotNull PolarisMetaStoreManager metaStoreManager, @NotNull UserSecretsManager userSecretsManager, + @Nonnull ServiceIdentityRegistry serviceIdentityRegistry, @NotNull SecurityContext securityContext, @NotNull PolarisAuthorizer authorizer, @NotNull ReservedProperties reservedProperties) { @@ -175,6 +180,7 @@ public PolarisAdminService( this.polarisPrincipal = (PolarisPrincipal) securityContext.getUserPrincipal(); this.authorizer = authorizer; this.userSecretsManager = userSecretsManager; + this.serviceIdentityRegistry = serviceIdentityRegistry; this.reservedProperties = reservedProperties; } @@ -186,6 +192,10 @@ private UserSecretsManager getUserSecretsManager() { return userSecretsManager; } + private ServiceIdentityRegistry getServiceIdentityRegistry() { + return serviceIdentityRegistry; + } + private Optional findCatalogByName(String name) { return Optional.ofNullable(resolutionManifest.getResolvedReferenceCatalogEntity()) .map(path -> CatalogEntity.of(path.getRawLeafEntity())); @@ -686,6 +696,11 @@ private Map extractSecretReferences( AuthenticationParametersDpo.INLINE_BEARER_TOKEN_REFERENCE_KEY, secretReference); break; } + case SIGV4: + { + // SigV4 authentication is not secret-based + break; + } default: throw new IllegalStateException( "Unsupported authentication type: " @@ -765,10 +780,18 @@ public PolarisEntity createCatalog(CreateCatalogRequest catalogRequest) { AuthenticationParameters.AuthenticationTypeEnum.IMPLICIT.name()), "Implicit authentication based catalog federation is not supported."); } + + ServiceIdentityInfoDpo serviceIdentityInfo = null; + if (connectionConfigInfo.getAuthenticationParameters().getAuthenticationType() + == AuthenticationParameters.AuthenticationTypeEnum.SIGV4) { + serviceIdentityInfo = + serviceIdentityRegistry.assignServiceIdentity(ServiceIdentityType.AWS_IAM); + } + entity = new CatalogEntity.Builder(entity) .setConnectionConfigInfoDpoWithSecrets( - connectionConfigInfo, processedSecretReferences) + connectionConfigInfo, processedSecretReferences, serviceIdentityInfo) .build(); } } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java index 31bbd6cdd6..3918342848 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java @@ -75,6 +75,8 @@ import org.apache.polaris.core.entity.PolarisPrivilege; import org.apache.polaris.core.entity.PrincipalEntity; import org.apache.polaris.core.entity.PrincipalRoleEntity; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.dao.entity.BaseResult; @@ -104,6 +106,7 @@ public class PolarisServiceImpl private final PolarisAuthorizer polarisAuthorizer; private final MetaStoreManagerFactory metaStoreManagerFactory; private final UserSecretsManagerFactory userSecretsManagerFactory; + private final ServiceIdentityRegistryFactory serviceIdentityRegistryFactory; private final CallContext callContext; private final RealmConfig realmConfig; private final ReservedProperties reservedProperties; @@ -115,6 +118,7 @@ public PolarisServiceImpl( ResolutionManifestFactory resolutionManifestFactory, MetaStoreManagerFactory metaStoreManagerFactory, UserSecretsManagerFactory userSecretsManagerFactory, + ServiceIdentityRegistryFactory serviceIdentityRegistryFactory, PolarisAuthorizer polarisAuthorizer, CallContext callContext, ReservedProperties reservedProperties, @@ -123,6 +127,7 @@ public PolarisServiceImpl( this.resolutionManifestFactory = resolutionManifestFactory; this.metaStoreManagerFactory = metaStoreManagerFactory; this.userSecretsManagerFactory = userSecretsManagerFactory; + this.serviceIdentityRegistryFactory = serviceIdentityRegistryFactory; this.polarisAuthorizer = polarisAuthorizer; this.callContext = callContext; this.realmConfig = callContext.getRealmConfig(); @@ -141,12 +146,15 @@ private PolarisAdminService newAdminService( metaStoreManagerFactory.getOrCreateMetaStoreManager(realmContext); UserSecretsManager userSecretsManager = userSecretsManagerFactory.getOrCreateUserSecretsManager(realmContext); + ServiceIdentityRegistry serviceIdentityRegistry = + serviceIdentityRegistryFactory.getOrCreateServiceIdentityRegistry(realmContext); return new PolarisAdminService( diagnostics, callContext, resolutionManifestFactory, metaStoreManager, userSecretsManager, + serviceIdentityRegistry, securityContext, polarisAuthorizer, reservedProperties); diff --git a/runtime/service/src/main/java/org/apache/polaris/service/config/ProductionReadinessChecks.java b/runtime/service/src/main/java/org/apache/polaris/service/config/ProductionReadinessChecks.java index 50d0746910..05c36236b3 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/config/ProductionReadinessChecks.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/config/ProductionReadinessChecks.java @@ -44,6 +44,7 @@ import org.apache.polaris.service.context.TestRealmContextResolver; import org.apache.polaris.service.events.listeners.PolarisEventListener; import org.apache.polaris.service.events.listeners.TestPolarisEventListener; +import org.apache.polaris.service.identity.ServiceIdentityConfiguration; import org.apache.polaris.service.metrics.MetricsConfiguration; import org.apache.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory; import org.eclipse.microprofile.config.Config; @@ -232,6 +233,24 @@ public ProductionReadinessCheck checkPolarisEventListener( return ProductionReadinessCheck.OK; } + @Produces + public ProductionReadinessCheck checkServiceIdentities( + ServiceIdentityConfiguration configuration) { + List errors = new ArrayList<>(); + configuration + .realms() + .forEach( + (realm, config) -> { + if (config.awsIamServiceIdentity().isEmpty()) { + errors.add( + Error.of( + "AWS IAM Service identity is not configured.", + "polaris.service-identity.%saws-iam".formatted(authRealmSegment(realm)))); + } + }); + return ProductionReadinessCheck.of(errors); + } + private static String authRealmSegment(String realm) { return realm.equals(AuthenticationConfiguration.DEFAULT_REALM_KEY) ? "" : realm + "."; } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java b/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java index ff822e293c..2f52020af0 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java @@ -43,6 +43,8 @@ import org.apache.polaris.core.config.RealmConfig; import org.apache.polaris.core.context.CallContext; import org.apache.polaris.core.context.RealmContext; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; import org.apache.polaris.core.persistence.BasePersistence; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; @@ -72,6 +74,9 @@ import org.apache.polaris.service.context.RealmContextResolver; import org.apache.polaris.service.events.PolarisEventListenerConfiguration; import org.apache.polaris.service.events.listeners.PolarisEventListener; +import org.apache.polaris.service.identity.RealmServiceIdentityConfiguration; +import org.apache.polaris.service.identity.ServiceIdentityConfiguration; +import org.apache.polaris.service.identity.ServiceIdentityRegistryConfiguration; import org.apache.polaris.service.persistence.PersistenceConfiguration; import org.apache.polaris.service.ratelimiter.RateLimiter; import org.apache.polaris.service.ratelimiter.RateLimiterFilterConfiguration; @@ -241,6 +246,13 @@ public StsClientsPool stsClientsPool( return new StsClientsPool(config.effectiveClientsCacheMaxSize(), httpClient, meterRegistry); } + @Produces + public ServiceIdentityRegistryFactory serviceIdentityRegistryFactory( + ServiceIdentityRegistryConfiguration config, + @Any Instance serviceIdentityRegistryFactories) { + return serviceIdentityRegistryFactories.select(Identifier.Literal.of(config.type())).get(); + } + /** * Eagerly initialize the in-memory default realm on startup, so that users can check the * credentials printed to stdout immediately. @@ -402,6 +414,20 @@ public OidcTenantResolver oidcTenantResolver( return resolvers.select(Identifier.Literal.of(config.tenantResolver())).get(); } + @Produces + @RequestScoped + public RealmServiceIdentityConfiguration realmServiceIdentityConfig( + ServiceIdentityConfiguration config, RealmContext realmContext) { + return config.forRealm(realmContext); + } + + @Produces + @RequestScoped + public ServiceIdentityRegistry serviceIdentityRegistry( + ServiceIdentityRegistryFactory serviceIdentityRegistryFactory, RealmContext realmContext) { + return serviceIdentityRegistryFactory.getOrCreateServiceIdentityRegistry(realmContext); + } + public void closeTaskExecutor(@Disposes @Identifier("task-executor") ManagedExecutor executor) { executor.close(); } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/AwsIamServiceIdentityConfiguration.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/AwsIamServiceIdentityConfiguration.java new file mode 100644 index 0000000000..d69459f492 --- /dev/null +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/AwsIamServiceIdentityConfiguration.java @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.service.identity; + +import java.util.Optional; +import org.apache.polaris.core.identity.resolved.ResolvedAwsIamServiceIdentity; + +/** + * Configuration for an AWS IAM service identity used by Polaris to access AWS services. + * + *

This includes the IAM ARN and optionally, static credentials (access key, secret key, and + * session token). If credentials are provided, they will be used to construct a {@link + * ResolvedAwsIamServiceIdentity}; otherwise, the AWS default credential provider chain is used. + */ +public interface AwsIamServiceIdentityConfiguration extends ResolvableServiceIdentityConfiguration { + + /** The IAM role or user ARN representing the service identity. */ + String iamArn(); + + /** + * Optional AWS access key ID associated with the IAM identity. If not provided, the AWS default + * credential chain will be used. + */ + Optional accessKeyId(); + + /** + * Optional AWS secret access key associated with the IAM identity. If not provided, the AWS + * default credential chain will be used. + */ + Optional secretAccessKey(); + + /** + * Optional AWS session token associated with the IAM identity. If not provided, the AWS default + * credential chain will be used. + */ + Optional sessionToken(); + + /** + * Resolves this configuration into a {@link ResolvedAwsIamServiceIdentity} if the IAM ARN is + * present. + * + * @return the resolved identity, or an empty optional if the ARN is missing + */ + @Override + default Optional resolve() { + if (iamArn() == null) { + return Optional.empty(); + } else { + return Optional.of( + new ResolvedAwsIamServiceIdentity( + iamArn(), + accessKeyId().orElse(null), + secretAccessKey().orElse(null), + sessionToken().orElse(null))); + } + } +} diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/RealmServiceIdentityConfiguration.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/RealmServiceIdentityConfiguration.java new file mode 100644 index 0000000000..a58fa25a9e --- /dev/null +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/RealmServiceIdentityConfiguration.java @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.service.identity; + +import io.smallrye.config.WithName; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Represents service identity configuration for a specific realm. + * + *

Supports multiple identity types, such as AWS IAM. This interface allows each realm to define + * the credentials and metadata needed to resolve service-managed identities. + */ +public interface RealmServiceIdentityConfiguration { + /** + * Returns the AWS IAM service identity configuration for this realm, if present. + * + * @return an optional AWS IAM configuration + */ + @WithName("aws-iam") + Optional awsIamServiceIdentity(); + + /** + * Aggregates all configured service identity types into a list. This includes AWS IAM and + * potentially other types in the future. + * + * @return a list of configured service identity definitions + */ + default List serviceIdentityConfigurations() { + return Stream.of(awsIamServiceIdentity()).flatMap(Optional::stream).toList(); + } +} diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/ResolvableServiceIdentityConfiguration.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/ResolvableServiceIdentityConfiguration.java new file mode 100644 index 0000000000..093d5abb8f --- /dev/null +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/ResolvableServiceIdentityConfiguration.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.service.identity; + +import java.util.Optional; +import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; + +/** + * Represents a service identity configuration that can be resolved into a fully initialized {@link + * ResolvedServiceIdentity}. + * + *

This interface allows identity configurations (e.g., AWS IAM) to encapsulate the logic + * required to construct runtime credentials and metadata needed to authenticate as a + * Polaris-managed service identity. + */ +public interface ResolvableServiceIdentityConfiguration { + /** + * Attempts to resolve this configuration into a {@link ResolvedServiceIdentity}. + * + * @return an optional resolved service identity, or empty if resolution fails or is not + * configured + */ + Optional resolve(); +} diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityConfiguration.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityConfiguration.java new file mode 100644 index 0000000000..5cb41286a5 --- /dev/null +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityConfiguration.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.service.identity; + +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefaults; +import io.smallrye.config.WithParentName; +import io.smallrye.config.WithUnnamedKey; +import java.util.Map; +import org.apache.polaris.core.context.RealmContext; + +/** + * Represents the service identity configuration for one or more realms. + * + *

This interface supports multi-tenant configurations where each realm can define its own {@link + * RealmServiceIdentityConfiguration}. If a realm-specific configuration is not found, a fallback to + * the default configuration is applied. + */ +@ConfigMapping(prefix = "polaris.service-identity") +public interface ServiceIdentityConfiguration { + /** + * The key used to identify the default realm configuration. + * + *

This default is especially useful in testing scenarios and single-tenant deployments where + * only one realm is expected and explicitly configuring realms is unnecessary. + */ + String DEFAULT_REALM_KEY = ""; + + /** + * Returns a map of realm identifiers to their corresponding service identity configurations. + * + * @return the map of realm-specific configurations + */ + @WithParentName + @WithUnnamedKey(DEFAULT_REALM_KEY) + @WithDefaults + Map realms(); + + /** + * Returns the service identity configuration for the given {@link RealmContext}. Falls back to + * the default if the realm is not explicitly configured. + * + * @param realmContext the realm context + * @return the matching or default realm configuration + */ + default RealmServiceIdentityConfiguration forRealm(RealmContext realmContext) { + return forRealm(realmContext.getRealmIdentifier()); + } + + /** + * Returns the service identity configuration for the given realm identifier. Falls back to the + * default if the realm is not explicitly configured. + * + * @param realmIdentifier the identifier of the realm + * @return the matching or default realm configuration + */ + default RealmServiceIdentityConfiguration forRealm(String realmIdentifier) { + return realms().containsKey(realmIdentifier) + ? realms().get(realmIdentifier) + : realms().get(DEFAULT_REALM_KEY); + } +} diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityRegistryConfiguration.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityRegistryConfiguration.java new file mode 100644 index 0000000000..4d72810e1d --- /dev/null +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityRegistryConfiguration.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.service.identity; + +import io.quarkus.runtime.annotations.StaticInitSafe; +import io.smallrye.config.ConfigMapping; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; + +@StaticInitSafe +@ConfigMapping(prefix = "polaris.service-identity.registry") +public interface ServiceIdentityRegistryConfiguration { + + /** + * The type of the ServiceIdentityRegistryFactory to use. This is the {@link + * ServiceIdentityRegistryFactory} identifier. + */ + String type(); +} diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistryFactory.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistryFactory.java new file mode 100644 index 0000000000..17a7af02c3 --- /dev/null +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistryFactory.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.service.identity.registry; + +import com.google.common.annotations.VisibleForTesting; +import io.smallrye.common.annotation.Identifier; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import java.util.EnumMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import org.apache.polaris.core.context.RealmContext; +import org.apache.polaris.core.identity.ServiceIdentityType; +import org.apache.polaris.core.identity.registry.DefaultServiceIdentityRegistry; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; +import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; +import org.apache.polaris.core.secrets.ServiceSecretReference; +import org.apache.polaris.service.identity.RealmServiceIdentityConfiguration; +import org.apache.polaris.service.identity.ResolvableServiceIdentityConfiguration; +import org.apache.polaris.service.identity.ServiceIdentityConfiguration; + +@ApplicationScoped +@Identifier("default") +public class DefaultServiceIdentityRegistryFactory implements ServiceIdentityRegistryFactory { + private static final String DEFAULT_REALM_KEY = ServiceIdentityConfiguration.DEFAULT_REALM_KEY; + private static final String DEFAULT_REALM_NSS = "system:default"; + private static final String IDENTITY_INFO_REFERENCE_URN_FORMAT = + "urn:polaris-secret:default-identity-registry:%s:%s"; + + private final Map realmServiceIdentityRegistries; + + @Inject + public DefaultServiceIdentityRegistryFactory( + ServiceIdentityConfiguration serviceIdentityConfiguration) { + realmServiceIdentityRegistries = + serviceIdentityConfiguration.realms().entrySet().stream() + .collect( + Collectors.toMap( + Map.Entry::getKey, // realm identifier + entry -> { + RealmServiceIdentityConfiguration realmConfig = entry.getValue(); + + // Resolve all the service identities for the realm + EnumMap resolvedIdentities = + realmConfig.serviceIdentityConfigurations().stream() + .map(ResolvableServiceIdentityConfiguration::resolve) + .flatMap(Optional::stream) + .peek( + // Set the identity info reference for each resolved identity + identity -> + identity.setIdentityInfoReference( + buildIdentityInfoReference( + entry.getKey(), identity.getIdentityType()))) + .collect( + // Collect to an EnumMap, grouping by ServiceIdentityType + Collectors.toMap( + ResolvedServiceIdentity::getIdentityType, + identity -> identity, + (a, b) -> b, + () -> new EnumMap<>(ServiceIdentityType.class))); + return new DefaultServiceIdentityRegistry(resolvedIdentities); + })); + + if (!realmServiceIdentityRegistries.containsKey(DEFAULT_REALM_KEY)) { + // If no default realm is defined, create an empty registry + realmServiceIdentityRegistries.put( + DEFAULT_REALM_KEY, + new DefaultServiceIdentityRegistry(new EnumMap<>(ServiceIdentityType.class))); + } + } + + public DefaultServiceIdentityRegistryFactory() { + this(new DefaultServiceIdentityRegistry(new EnumMap<>(ServiceIdentityType.class))); + } + + public DefaultServiceIdentityRegistryFactory( + DefaultServiceIdentityRegistry defaultServiceIdentityRegistry) { + this(Map.of(DEFAULT_REALM_KEY, defaultServiceIdentityRegistry)); + } + + public DefaultServiceIdentityRegistryFactory( + Map realmServiceIdentityRegistries) { + this.realmServiceIdentityRegistries = realmServiceIdentityRegistries; + + if (!realmServiceIdentityRegistries.containsKey(DEFAULT_REALM_KEY)) { + // If no default realm is defined, create an empty registry + realmServiceIdentityRegistries.put( + DEFAULT_REALM_KEY, + new DefaultServiceIdentityRegistry(new EnumMap<>(ServiceIdentityType.class))); + } + } + + @Override + public ServiceIdentityRegistry getOrCreateServiceIdentityRegistry(RealmContext realmContext) { + return getServiceIdentityRegistryForRealm(realmContext); + } + + @VisibleForTesting + public Map getRealmServiceIdentityRegistries() { + return realmServiceIdentityRegistries; + } + + protected DefaultServiceIdentityRegistry getServiceIdentityRegistryForRealm( + RealmContext realmContext) { + return getServiceIdentityRegistryForRealm(realmContext.getRealmIdentifier()); + } + + protected DefaultServiceIdentityRegistry getServiceIdentityRegistryForRealm( + String realmIdentifier) { + return realmServiceIdentityRegistries.getOrDefault( + realmIdentifier, realmServiceIdentityRegistries.get(DEFAULT_REALM_KEY)); + } + + private ServiceSecretReference buildIdentityInfoReference( + String realm, ServiceIdentityType type) { + // urn:polaris-service-secret:default-identity-registry:: + return new ServiceSecretReference( + IDENTITY_INFO_REFERENCE_URN_FORMAT.formatted( + realm.equals(DEFAULT_REALM_KEY) ? DEFAULT_REALM_NSS : realm, type.name()), + Map.of()); + } +} diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java index f69e18b7d5..391140ecef 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java @@ -25,6 +25,7 @@ import jakarta.ws.rs.core.SecurityContext; import java.security.Principal; import java.time.Instant; +import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -48,6 +49,8 @@ import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.entity.PrincipalEntity; import org.apache.polaris.core.entity.PrincipalRoleEntity; +import org.apache.polaris.core.identity.ServiceIdentityType; +import org.apache.polaris.core.identity.registry.DefaultServiceIdentityRegistry; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.dao.entity.BaseResult; @@ -242,6 +245,7 @@ private PolarisAdminService setupPolarisAdminService( services.resolutionManifestFactory(), metaStoreManager, new UnsafeInMemorySecretsManager(), + new DefaultServiceIdentityRegistry(new EnumMap<>(ServiceIdentityType.class)), new SecurityContext() { @Override public Principal getUserPrincipal() { diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java index ad5fa0ce6a..611ebab559 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java @@ -54,6 +54,7 @@ private PolarisAdminService newTestAdminService(Set activatedPrincipalRo resolutionManifestFactory, metaStoreManager, userSecretsManager, + serviceIdentityRegistry, securityContext(authenticatedPrincipal), polarisAuthorizer, reservedProperties); diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java index 60e0559426..0239f9489f 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceTest.java @@ -37,6 +37,7 @@ import org.apache.polaris.core.entity.PolarisEntity; import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.entity.PolarisPrivilege; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; import org.apache.polaris.core.persistence.dao.entity.BaseResult; @@ -59,6 +60,7 @@ public class PolarisAdminServiceTest { @Mock private ResolutionManifestFactory resolutionManifestFactory; @Mock private PolarisMetaStoreManager metaStoreManager; @Mock private UserSecretsManager userSecretsManager; + @Mock private ServiceIdentityRegistry identityRegistry; @Mock private SecurityContext securityContext; @Mock private PolarisAuthorizer authorizer; @Mock private ReservedProperties reservedProperties; @@ -81,6 +83,7 @@ void setUp() throws Exception { resolutionManifestFactory, metaStoreManager, userSecretsManager, + identityRegistry, securityContext, authorizer, reservedProperties); diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java index fee88c25a5..85542f9eb7 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java @@ -66,6 +66,8 @@ import org.apache.polaris.core.entity.PolarisPrivilege; import org.apache.polaris.core.entity.PrincipalEntity; import org.apache.polaris.core.entity.PrincipalRoleEntity; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.dao.entity.BaseResult; @@ -187,6 +189,7 @@ public Map getConfigOverrides() { @Inject protected ResolutionManifestFactory resolutionManifestFactory; @Inject protected CallContextCatalogFactory callContextCatalogFactory; @Inject protected UserSecretsManagerFactory userSecretsManagerFactory; + @Inject protected ServiceIdentityRegistryFactory serviceIdentityRegistryFactory; @Inject protected PolarisDiagnostics diagServices; @Inject protected FileIOFactory fileIOFactory; @Inject protected PolarisEventListener polarisEventListener; @@ -201,6 +204,7 @@ public Map getConfigOverrides() { protected PolarisAdminService adminService; protected PolarisMetaStoreManager metaStoreManager; protected UserSecretsManager userSecretsManager; + protected ServiceIdentityRegistry serviceIdentityRegistry; protected PolarisBaseEntity catalogEntity; protected PrincipalEntity principalEntity; protected CallContext callContext; @@ -233,6 +237,8 @@ public void before(TestInfo testInfo) { QuarkusMock.installMockForType(containerRequestContext, ContainerRequestContext.class); metaStoreManager = managerFactory.getOrCreateMetaStoreManager(realmContext); userSecretsManager = userSecretsManagerFactory.getOrCreateUserSecretsManager(realmContext); + serviceIdentityRegistry = + serviceIdentityRegistryFactory.getOrCreateServiceIdentityRegistry(realmContext); polarisContext = new PolarisCallContext( @@ -254,6 +260,7 @@ public void before(TestInfo testInfo) { resolutionManifestFactory, metaStoreManager, userSecretsManager, + serviceIdentityRegistry, securityContext(authenticatedRoot), polarisAuthorizer, reservedProperties); diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplTest.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplTest.java index 1d22c48282..2bf58e29a8 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplTest.java @@ -38,6 +38,7 @@ import org.apache.polaris.core.config.FeatureConfiguration; import org.apache.polaris.core.config.RealmConfig; import org.apache.polaris.core.context.CallContext; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory; import org.apache.polaris.core.secrets.UserSecretsManagerFactory; @@ -54,6 +55,7 @@ public class PolarisServiceImplTest { private ResolutionManifestFactory resolutionManifestFactory; private MetaStoreManagerFactory metaStoreManagerFactory; private UserSecretsManagerFactory userSecretsManagerFactory; + private ServiceIdentityRegistryFactory serviceIdentityRegistryFactory; private PolarisAuthorizer polarisAuthorizer; private CallContext callContext; private ReservedProperties reservedProperties; @@ -67,6 +69,7 @@ void setUp() { resolutionManifestFactory = Mockito.mock(ResolutionManifestFactory.class); metaStoreManagerFactory = Mockito.mock(MetaStoreManagerFactory.class); userSecretsManagerFactory = Mockito.mock(UserSecretsManagerFactory.class); + serviceIdentityRegistryFactory = Mockito.mock(ServiceIdentityRegistryFactory.class); polarisAuthorizer = Mockito.mock(PolarisAuthorizer.class); callContext = Mockito.mock(CallContext.class); reservedProperties = Mockito.mock(ReservedProperties.class); @@ -86,6 +89,7 @@ void setUp() { resolutionManifestFactory, metaStoreManagerFactory, userSecretsManagerFactory, + serviceIdentityRegistryFactory, polarisAuthorizer, callContext, reservedProperties, diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/AbstractPolarisGenericTableCatalogTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/AbstractPolarisGenericTableCatalogTest.java index 98a30f0d98..ac1ba55936 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/AbstractPolarisGenericTableCatalogTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/AbstractPolarisGenericTableCatalogTest.java @@ -52,6 +52,8 @@ import org.apache.polaris.core.entity.PolarisEntity; import org.apache.polaris.core.entity.PrincipalEntity; import org.apache.polaris.core.entity.table.GenericTableEntity; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory; @@ -98,6 +100,7 @@ public abstract class AbstractPolarisGenericTableCatalogTest { @Inject MetaStoreManagerFactory metaStoreManagerFactory; @Inject UserSecretsManagerFactory userSecretsManagerFactory; + @Inject ServiceIdentityRegistryFactory identityRegistryFactory; @Inject PolarisConfigurationStore configurationStore; @Inject StorageCredentialCache storageCredentialCache; @Inject PolarisStorageIntegrationProvider storageIntegrationProvider; @@ -111,6 +114,7 @@ public abstract class AbstractPolarisGenericTableCatalogTest { private String realmName; private PolarisMetaStoreManager metaStoreManager; private UserSecretsManager userSecretsManager; + private ServiceIdentityRegistry serviceIdentityRegistry; private PolarisCallContext polarisContext; private RealmConfig realmConfig; private PolarisAdminService adminService; @@ -148,6 +152,8 @@ public void before(TestInfo testInfo) { QuarkusMock.installMockForType(realmContext, RealmContext.class); metaStoreManager = metaStoreManagerFactory.getOrCreateMetaStoreManager(realmContext); userSecretsManager = userSecretsManagerFactory.getOrCreateUserSecretsManager(realmContext); + serviceIdentityRegistry = + identityRegistryFactory.getOrCreateServiceIdentityRegistry(realmContext); polarisContext = new PolarisCallContext( realmContext, @@ -173,6 +179,7 @@ public void before(TestInfo testInfo) { resolutionManifestFactory, metaStoreManager, userSecretsManager, + serviceIdentityRegistry, securityContext, authorizer, reservedProperties); diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogTest.java index 1ec9d87ba8..8afbe1f8b3 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogTest.java @@ -108,6 +108,8 @@ import org.apache.polaris.core.entity.PrincipalEntity; import org.apache.polaris.core.entity.TaskEntity; import org.apache.polaris.core.exceptions.CommitConflictException; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; @@ -230,6 +232,7 @@ public Map getConfigOverrides() { @Inject StorageCredentialCache storageCredentialCache; @Inject PolarisStorageIntegrationProvider storageIntegrationProvider; @Inject UserSecretsManagerFactory userSecretsManagerFactory; + @Inject ServiceIdentityRegistryFactory serviceIdentityRegistryFactory; @Inject PolarisDiagnostics diagServices; @Inject PolarisEventListener polarisEventListener; @@ -237,6 +240,7 @@ public Map getConfigOverrides() { private String realmName; private PolarisMetaStoreManager metaStoreManager; private UserSecretsManager userSecretsManager; + private ServiceIdentityRegistry serviceIdentityRegistry; private PolarisCallContext polarisContext; private RealmConfig realmConfig; private PolarisAdminService adminService; @@ -279,6 +283,8 @@ public void before(TestInfo testInfo) { QuarkusMock.installMockForType(realmContext, RealmContext.class); metaStoreManager = metaStoreManagerFactory.getOrCreateMetaStoreManager(realmContext); userSecretsManager = userSecretsManagerFactory.getOrCreateUserSecretsManager(realmContext); + serviceIdentityRegistry = + serviceIdentityRegistryFactory.getOrCreateServiceIdentityRegistry(realmContext); polarisContext = new PolarisCallContext( realmContext, @@ -318,6 +324,7 @@ public void before(TestInfo testInfo) { resolutionManifestFactory, metaStoreManager, userSecretsManager, + serviceIdentityRegistry, securityContext, authorizer, reservedProperties); diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogViewTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogViewTest.java index f0e54ea3d0..27ad039bce 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogViewTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogViewTest.java @@ -49,6 +49,8 @@ import org.apache.polaris.core.context.RealmContext; import org.apache.polaris.core.entity.CatalogEntity; import org.apache.polaris.core.entity.PrincipalEntity; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory; @@ -110,6 +112,7 @@ public Map getConfigOverrides() { @Inject MetaStoreManagerFactory metaStoreManagerFactory; @Inject UserSecretsManagerFactory userSecretsManagerFactory; + @Inject ServiceIdentityRegistryFactory serviceIdentityRegistryFactory; @Inject PolarisConfigurationStore configurationStore; @Inject StorageCredentialCache storageCredentialCache; @Inject PolarisDiagnostics diagServices; @@ -122,6 +125,7 @@ public Map getConfigOverrides() { private String realmName; private PolarisMetaStoreManager metaStoreManager; private UserSecretsManager userSecretsManager; + private ServiceIdentityRegistry serviceIdentityRegistry; private PolarisCallContext polarisContext; private RealmConfig realmConfig; @@ -158,6 +162,8 @@ public void before(TestInfo testInfo) { metaStoreManager = metaStoreManagerFactory.getOrCreateMetaStoreManager(realmContext); userSecretsManager = userSecretsManagerFactory.getOrCreateUserSecretsManager(realmContext); + serviceIdentityRegistry = + serviceIdentityRegistryFactory.getOrCreateServiceIdentityRegistry(realmContext); polarisContext = new PolarisCallContext( realmContext, @@ -183,6 +189,7 @@ public void before(TestInfo testInfo) { resolutionManifestFactory, metaStoreManager, userSecretsManager, + serviceIdentityRegistry, securityContext, authorizer, reservedProperties); diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java index 6a603e3772..d49df676eb 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java @@ -58,6 +58,8 @@ import org.apache.polaris.core.entity.CatalogEntity; import org.apache.polaris.core.entity.PolarisEntity; import org.apache.polaris.core.entity.PrincipalEntity; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.PolicyMappingAlreadyExistsException; @@ -124,6 +126,7 @@ public abstract class AbstractPolicyCatalogTest { @Inject MetaStoreManagerFactory metaStoreManagerFactory; @Inject UserSecretsManagerFactory userSecretsManagerFactory; + @Inject ServiceIdentityRegistryFactory serviceIdentityRegistryFactory; @Inject PolarisConfigurationStore configurationStore; @Inject StorageCredentialCache storageCredentialCache; @Inject PolarisStorageIntegrationProvider storageIntegrationProvider; @@ -137,6 +140,7 @@ public abstract class AbstractPolicyCatalogTest { private String realmName; private PolarisMetaStoreManager metaStoreManager; private UserSecretsManager userSecretsManager; + private ServiceIdentityRegistry serviceIdentityRegistry; private PolarisCallContext polarisContext; private RealmConfig realmConfig; private PolarisAdminService adminService; @@ -169,6 +173,8 @@ public void before(TestInfo testInfo) { QuarkusMock.installMockForType(realmContext, RealmContext.class); metaStoreManager = metaStoreManagerFactory.getOrCreateMetaStoreManager(realmContext); userSecretsManager = userSecretsManagerFactory.getOrCreateUserSecretsManager(realmContext); + serviceIdentityRegistry = + serviceIdentityRegistryFactory.getOrCreateServiceIdentityRegistry(realmContext); polarisContext = new PolarisCallContext( realmContext, @@ -194,6 +200,7 @@ public void before(TestInfo testInfo) { resolutionManifestFactory, metaStoreManager, userSecretsManager, + serviceIdentityRegistry, securityContext, authorizer, reservedProperties); diff --git a/runtime/service/src/test/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistryTest.java b/runtime/service/src/test/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistryTest.java new file mode 100644 index 0000000000..4ca651dcc9 --- /dev/null +++ b/runtime/service/src/test/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistryTest.java @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.service.identity.registry; + +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; +import jakarta.inject.Inject; +import java.util.EnumMap; +import java.util.Map; +import java.util.Optional; +import org.apache.polaris.core.identity.ServiceIdentityType; +import org.apache.polaris.core.identity.registry.DefaultServiceIdentityRegistry; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; +import org.apache.polaris.core.identity.resolved.ResolvedAwsIamServiceIdentity; +import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; +import org.apache.polaris.core.secrets.ServiceSecretReference; +import org.apache.polaris.service.identity.RealmServiceIdentityConfiguration; +import org.apache.polaris.service.identity.ServiceIdentityConfiguration; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +@QuarkusTest +@TestProfile(DefaultServiceIdentityRegistryTest.Profile.class) +public class DefaultServiceIdentityRegistryTest { + private static final String DEFAULT_REALM_KEY = ServiceIdentityConfiguration.DEFAULT_REALM_KEY; + private static final String MY_REALM_KEY = "my-realm"; + + @Inject ServiceIdentityConfiguration serviceIdentityConfiguration; + @Inject ServiceIdentityRegistryFactory serviceIdentityRegistryFactory; + + public static class Profile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of( + "quarkus.identity-registry.type", + "default", + "polaris.service-identity.aws-iam.iam-arn", + "arn:aws:iam::123456789012:user/polaris-default-iam-user", + "polaris.service-identity.my-realm.aws-iam.iam-arn", + "arn:aws:iam::123456789012:user/polaris-iam-user", + "polaris.service-identity.my-realm.aws-iam.access-key-id", + "access-key-id", + "polaris.service-identity.my-realm.aws-iam.secret-access-key", + "secret-access-key", + "polaris.service-identity.my-realm.aws-iam.session-token", + "session-token"); + } + } + + @Test + void testServiceIdentityConfiguration() { + // Ensure that the service identity configuration is loaded correctly + Assertions.assertThat(serviceIdentityConfiguration.realms()).isNotNull(); + Assertions.assertThat(serviceIdentityConfiguration.realms()) + .containsKey(ServiceIdentityConfiguration.DEFAULT_REALM_KEY) + .containsKey(MY_REALM_KEY) + .size() + .isEqualTo(2); + + // Check the default realm configuration + RealmServiceIdentityConfiguration defaultConfig = + serviceIdentityConfiguration.forRealm(DEFAULT_REALM_KEY); + Assertions.assertThat(defaultConfig.awsIamServiceIdentity().isPresent()).isTrue(); + Assertions.assertThat(defaultConfig.awsIamServiceIdentity().get().iamArn()) + .isEqualTo("arn:aws:iam::123456789012:user/polaris-default-iam-user"); + Assertions.assertThat(defaultConfig.awsIamServiceIdentity().get().accessKeyId()).isEmpty(); + Assertions.assertThat(defaultConfig.awsIamServiceIdentity().get().secretAccessKey()).isEmpty(); + Assertions.assertThat(defaultConfig.awsIamServiceIdentity().get().sessionToken()).isEmpty(); + + // Check the my-realm configuration + RealmServiceIdentityConfiguration myRealmConfig = + serviceIdentityConfiguration.forRealm(MY_REALM_KEY); + Assertions.assertThat(myRealmConfig.awsIamServiceIdentity().isPresent()).isTrue(); + Assertions.assertThat(myRealmConfig.awsIamServiceIdentity().get().iamArn()) + .isEqualTo("arn:aws:iam::123456789012:user/polaris-iam-user"); + Assertions.assertThat(myRealmConfig.awsIamServiceIdentity().get().accessKeyId()) + .isEqualTo(Optional.of("access-key-id")); + Assertions.assertThat(myRealmConfig.awsIamServiceIdentity().get().secretAccessKey()) + .isEqualTo(Optional.of("secret-access-key")); + Assertions.assertThat(myRealmConfig.awsIamServiceIdentity().get().sessionToken()) + .isEqualTo(Optional.of("session-token")); + + // Check the unexisting realm configuration + RealmServiceIdentityConfiguration otherConfig = + serviceIdentityConfiguration.forRealm("other-realm"); + Assertions.assertThat(otherConfig.awsIamServiceIdentity().isPresent()).isTrue(); + Assertions.assertThat(otherConfig.awsIamServiceIdentity().get().iamArn()) + .isEqualTo("arn:aws:iam::123456789012:user/polaris-default-iam-user"); + Assertions.assertThat(otherConfig.awsIamServiceIdentity().get().accessKeyId()).isEmpty(); + Assertions.assertThat(otherConfig.awsIamServiceIdentity().get().secretAccessKey()).isEmpty(); + Assertions.assertThat(otherConfig.awsIamServiceIdentity().get().sessionToken()).isEmpty(); + } + + @Test + void testRealmServiceIdentityConfigToResolvedServiceIdentity() { + // Check the default realm + DefaultServiceIdentityRegistry defaultRegistry = + (DefaultServiceIdentityRegistry) + serviceIdentityRegistryFactory.getOrCreateServiceIdentityRegistry( + () -> DEFAULT_REALM_KEY); + EnumMap resolvedIdentities = + defaultRegistry.getResolvedServiceIdentities(); + + Assertions.assertThat(resolvedIdentities) + .containsKey(ServiceIdentityType.AWS_IAM) + .size() + .isEqualTo(1); + ResolvedAwsIamServiceIdentity resolvedAwsIamServiceIdentity = + (ResolvedAwsIamServiceIdentity) resolvedIdentities.get(ServiceIdentityType.AWS_IAM); + Assertions.assertThat(resolvedAwsIamServiceIdentity.getIamArn()) + .isEqualTo("arn:aws:iam::123456789012:user/polaris-default-iam-user"); + Assertions.assertThat(resolvedAwsIamServiceIdentity.getIdentityInfoReference()) + .isEqualTo( + new ServiceSecretReference( + "urn:polaris-secret:default-identity-registry:system:default:AWS_IAM", Map.of())); + Assertions.assertThat(resolvedAwsIamServiceIdentity.getAccessKeyId()).isNull(); + Assertions.assertThat(resolvedAwsIamServiceIdentity.getSecretAccessKey()).isNull(); + Assertions.assertThat(resolvedAwsIamServiceIdentity.getSessionToken()).isNull(); + + // Check the my-realm + DefaultServiceIdentityRegistry myRealmRegistry = + (DefaultServiceIdentityRegistry) + serviceIdentityRegistryFactory.getOrCreateServiceIdentityRegistry(() -> MY_REALM_KEY); + resolvedIdentities = myRealmRegistry.getResolvedServiceIdentities(); + + Assertions.assertThat(resolvedIdentities) + .containsKey(ServiceIdentityType.AWS_IAM) + .size() + .isEqualTo(1); + resolvedAwsIamServiceIdentity = + (ResolvedAwsIamServiceIdentity) resolvedIdentities.get(ServiceIdentityType.AWS_IAM); + Assertions.assertThat(resolvedAwsIamServiceIdentity.getIamArn()) + .isEqualTo("arn:aws:iam::123456789012:user/polaris-iam-user"); + Assertions.assertThat(resolvedAwsIamServiceIdentity.getIdentityInfoReference()) + .isEqualTo( + new ServiceSecretReference( + "urn:polaris-secret:default-identity-registry:my-realm:AWS_IAM", Map.of())); + Assertions.assertThat(resolvedAwsIamServiceIdentity.getAccessKeyId()) + .isEqualTo("access-key-id"); + Assertions.assertThat(resolvedAwsIamServiceIdentity.getSecretAccessKey()) + .isEqualTo("secret-access-key"); + Assertions.assertThat(resolvedAwsIamServiceIdentity.getSessionToken()) + .isEqualTo("session-token"); + + // Check the other realm + DefaultServiceIdentityRegistry otherRegistry = + (DefaultServiceIdentityRegistry) + serviceIdentityRegistryFactory.getOrCreateServiceIdentityRegistry(() -> "other-realm"); + Assertions.assertThat(otherRegistry).isEqualTo(defaultRegistry); + } +} diff --git a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java index 11ee88b0d0..03086969a9 100644 --- a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java +++ b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java @@ -43,6 +43,7 @@ import org.apache.polaris.core.context.CallContext; import org.apache.polaris.core.context.RealmContext; import org.apache.polaris.core.entity.PrincipalEntity; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; import org.apache.polaris.core.persistence.BasePersistence; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; @@ -70,6 +71,7 @@ import org.apache.polaris.service.context.catalog.PolarisCallContextCatalogFactory; import org.apache.polaris.service.events.listeners.PolarisEventListener; import org.apache.polaris.service.events.listeners.TestPolarisEventListener; +import org.apache.polaris.service.identity.registry.DefaultServiceIdentityRegistryFactory; import org.apache.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory; import org.apache.polaris.service.secrets.UnsafeInMemorySecretsManagerFactory; import org.apache.polaris.service.storage.PolarisStorageIntegrationProviderImpl; @@ -172,6 +174,8 @@ public TestServices build() { UserSecretsManagerFactory userSecretsManagerFactory = new UnsafeInMemorySecretsManagerFactory(); + ServiceIdentityRegistryFactory serviceIdentityRegistryFactory = + new DefaultServiceIdentityRegistryFactory(); BasePersistence metaStoreSession = metaStoreManagerFactory.getOrCreateSession(realmContext); CallContext callContext = @@ -285,6 +289,7 @@ public String getAuthenticationScheme() { resolutionManifestFactory, metaStoreManagerFactory, userSecretsManagerFactory, + serviceIdentityRegistryFactory, authorizer, callContext, reservedProperties, From 8fcf9980964143812c94240aa64bbaa32f424e6f Mon Sep 17 00:00:00 2001 From: Rulin Xing Date: Thu, 18 Sep 2025 02:42:19 -0700 Subject: [PATCH 2/7] Resolved some comments --- .../registry/ServiceIdentityRegistry.java | 6 +- .../ServiceIdentityRegistryFactory.java | 32 ---- .../src/main/resources/application.properties | 13 +- .../service/admin/PolarisAdminService.java | 2 +- .../service/admin/PolarisServiceImpl.java | 9 +- .../config/ProductionReadinessChecks.java | 19 --- .../service/config/ServiceProducers.java | 26 +--- .../ServiceIdentityConfiguration.java | 22 +++ .../ServiceIdentityRegistryConfiguration.java | 6 +- .../DefaultServiceIdentityRegistry.java | 74 ++++++++- ...DefaultServiceIdentityRegistryFactory.java | 140 ------------------ .../service/admin/ManagementServiceTest.java | 2 +- .../service/admin/PolarisAuthzTestBase.java | 6 +- .../service/admin/PolarisServiceImplTest.java | 8 +- ...bstractPolarisGenericTableCatalogTest.java | 6 +- .../iceberg/AbstractIcebergCatalogTest.java | 6 +- .../AbstractIcebergCatalogViewTest.java | 6 +- .../policy/AbstractPolicyCatalogTest.java | 6 +- .../DefaultServiceIdentityRegistryTest.java | 39 +++-- .../apache/polaris/service/TestServices.java | 9 +- 20 files changed, 160 insertions(+), 277 deletions(-) delete mode 100644 polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistryFactory.java rename {polaris-core/src/main/java/org/apache/polaris/core => runtime/service/src/main/java/org/apache/polaris/service}/identity/registry/DefaultServiceIdentityRegistry.java (51%) delete mode 100644 runtime/service/src/main/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistryFactory.java diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistry.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistry.java index 05f512715d..f9b81ba22f 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistry.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistry.java @@ -37,13 +37,13 @@ */ public interface ServiceIdentityRegistry { /** - * Assigns a new {@link ServiceIdentityInfoDpo} for the given service identity type. Typically + * Discover a new {@link ServiceIdentityInfoDpo} for the given service identity type. Typically * used during entity creation to associate a default or generated identity. * * @param serviceIdentityType The type of service identity (e.g., AWS_IAM). - * @return A new {@link ServiceIdentityInfoDpo} representing the assigned service identity. + * @return A new {@link ServiceIdentityInfoDpo} representing the discovered service identity. */ - ServiceIdentityInfoDpo assignServiceIdentity(ServiceIdentityType serviceIdentityType); + ServiceIdentityInfoDpo discoverServiceIdentity(ServiceIdentityType serviceIdentityType); /** * Resolves the given service identity by retrieving the actual credential or secret referenced by diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistryFactory.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistryFactory.java deleted file mode 100644 index b6036d5207..0000000000 --- a/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistryFactory.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.polaris.core.identity.registry; - -import org.apache.polaris.core.context.RealmContext; - -/** - * Factory for creating {@link ServiceIdentityRegistry} instances. - * - *

Each {@link ServiceIdentityRegistry} instance is associated with a {@link RealmContext} and is - * responsible for managing the service identities for the user in that realm. - */ -public interface ServiceIdentityRegistryFactory { - ServiceIdentityRegistry getOrCreateServiceIdentityRegistry(RealmContext realmContext); -} diff --git a/runtime/defaults/src/main/resources/application.properties b/runtime/defaults/src/main/resources/application.properties index 7767e62b78..cd63ae33e0 100644 --- a/runtime/defaults/src/main/resources/application.properties +++ b/runtime/defaults/src/main/resources/application.properties @@ -199,10 +199,17 @@ polaris.oidc.principal-roles-mapper.type=default # polaris.storage.gcp.lifespan=PT1H # Polaris Service Identity Config -polaris.service-identity.registry.type=default +# Default identity (can be overridden in per realm) # polaris.service-identity.aws-iam.iam-arn=arn:aws:iam::123456789012:user/polaris-iam-user -polaris.service-identity.aws-iam.iam-arn=arn:aws:iam::174739373489:user/managed/rxing -polaris.service-identity.my-realm.aws-iam.iam-arn=arn:aws:iam::123456789012:user/polaris-iam-user +# polaris.service-identity.aws-iam.access-key-id=accessKeyId +# polaris.service-identity.aws-iam.secret-access-key=secretAccessKey +# polaris.service-identity.aws-iam.session-token=sessionToken + +# Service identity Config for a specific realm +# polaris.service-identity.my-realm.aws-iam.iam-arn=arn:aws:iam::123456789012:user/polaris-iam-user +# polaris.service-identity.my-realm.aws-iam.access-key-id=accessKeyId +# polaris.service-identity.my-realm.aws-iam.secret-access-key=secretAccessKey +# polaris.service-identity.my-realm.aws-iam.session-token=sessionToken quarkus.arc.ignored-split-packages=\ org.apache.polaris.service.catalog.api,\ diff --git a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java index 8dce9d4783..3e6b34ad5c 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java @@ -785,7 +785,7 @@ public PolarisEntity createCatalog(CreateCatalogRequest catalogRequest) { if (connectionConfigInfo.getAuthenticationParameters().getAuthenticationType() == AuthenticationParameters.AuthenticationTypeEnum.SIGV4) { serviceIdentityInfo = - serviceIdentityRegistry.assignServiceIdentity(ServiceIdentityType.AWS_IAM); + serviceIdentityRegistry.discoverServiceIdentity(ServiceIdentityType.AWS_IAM); } entity = diff --git a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java index 3918342848..4f2ee916b6 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java @@ -76,7 +76,6 @@ import org.apache.polaris.core.entity.PrincipalEntity; import org.apache.polaris.core.entity.PrincipalRoleEntity; import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; -import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.dao.entity.BaseResult; @@ -106,7 +105,7 @@ public class PolarisServiceImpl private final PolarisAuthorizer polarisAuthorizer; private final MetaStoreManagerFactory metaStoreManagerFactory; private final UserSecretsManagerFactory userSecretsManagerFactory; - private final ServiceIdentityRegistryFactory serviceIdentityRegistryFactory; + private final ServiceIdentityRegistry serviceIdentityRegistry; private final CallContext callContext; private final RealmConfig realmConfig; private final ReservedProperties reservedProperties; @@ -118,7 +117,7 @@ public PolarisServiceImpl( ResolutionManifestFactory resolutionManifestFactory, MetaStoreManagerFactory metaStoreManagerFactory, UserSecretsManagerFactory userSecretsManagerFactory, - ServiceIdentityRegistryFactory serviceIdentityRegistryFactory, + ServiceIdentityRegistry serviceIdentityRegistry, PolarisAuthorizer polarisAuthorizer, CallContext callContext, ReservedProperties reservedProperties, @@ -127,7 +126,7 @@ public PolarisServiceImpl( this.resolutionManifestFactory = resolutionManifestFactory; this.metaStoreManagerFactory = metaStoreManagerFactory; this.userSecretsManagerFactory = userSecretsManagerFactory; - this.serviceIdentityRegistryFactory = serviceIdentityRegistryFactory; + this.serviceIdentityRegistry = serviceIdentityRegistry; this.polarisAuthorizer = polarisAuthorizer; this.callContext = callContext; this.realmConfig = callContext.getRealmConfig(); @@ -146,8 +145,6 @@ private PolarisAdminService newAdminService( metaStoreManagerFactory.getOrCreateMetaStoreManager(realmContext); UserSecretsManager userSecretsManager = userSecretsManagerFactory.getOrCreateUserSecretsManager(realmContext); - ServiceIdentityRegistry serviceIdentityRegistry = - serviceIdentityRegistryFactory.getOrCreateServiceIdentityRegistry(realmContext); return new PolarisAdminService( diagnostics, callContext, diff --git a/runtime/service/src/main/java/org/apache/polaris/service/config/ProductionReadinessChecks.java b/runtime/service/src/main/java/org/apache/polaris/service/config/ProductionReadinessChecks.java index 05c36236b3..50d0746910 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/config/ProductionReadinessChecks.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/config/ProductionReadinessChecks.java @@ -44,7 +44,6 @@ import org.apache.polaris.service.context.TestRealmContextResolver; import org.apache.polaris.service.events.listeners.PolarisEventListener; import org.apache.polaris.service.events.listeners.TestPolarisEventListener; -import org.apache.polaris.service.identity.ServiceIdentityConfiguration; import org.apache.polaris.service.metrics.MetricsConfiguration; import org.apache.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory; import org.eclipse.microprofile.config.Config; @@ -233,24 +232,6 @@ public ProductionReadinessCheck checkPolarisEventListener( return ProductionReadinessCheck.OK; } - @Produces - public ProductionReadinessCheck checkServiceIdentities( - ServiceIdentityConfiguration configuration) { - List errors = new ArrayList<>(); - configuration - .realms() - .forEach( - (realm, config) -> { - if (config.awsIamServiceIdentity().isEmpty()) { - errors.add( - Error.of( - "AWS IAM Service identity is not configured.", - "polaris.service-identity.%saws-iam".formatted(authRealmSegment(realm)))); - } - }); - return ProductionReadinessCheck.of(errors); - } - private static String authRealmSegment(String realm) { return realm.equals(AuthenticationConfiguration.DEFAULT_REALM_KEY) ? "" : realm + "."; } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java b/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java index 2f52020af0..41ecd5a610 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java @@ -44,7 +44,6 @@ import org.apache.polaris.core.context.CallContext; import org.apache.polaris.core.context.RealmContext; import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; -import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; import org.apache.polaris.core.persistence.BasePersistence; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; @@ -65,6 +64,7 @@ import org.apache.polaris.service.auth.Authenticator; import org.apache.polaris.service.auth.TokenBroker; import org.apache.polaris.service.auth.TokenBrokerFactory; +import org.apache.polaris.service.auth.external.OidcConfiguration; import org.apache.polaris.service.auth.external.tenant.OidcTenantResolver; import org.apache.polaris.service.catalog.api.IcebergRestOAuth2ApiService; import org.apache.polaris.service.catalog.io.FileIOConfiguration; @@ -74,9 +74,8 @@ import org.apache.polaris.service.context.RealmContextResolver; import org.apache.polaris.service.events.PolarisEventListenerConfiguration; import org.apache.polaris.service.events.listeners.PolarisEventListener; -import org.apache.polaris.service.identity.RealmServiceIdentityConfiguration; import org.apache.polaris.service.identity.ServiceIdentityConfiguration; -import org.apache.polaris.service.identity.ServiceIdentityRegistryConfiguration; +import org.apache.polaris.service.identity.registry.DefaultServiceIdentityRegistry; import org.apache.polaris.service.persistence.PersistenceConfiguration; import org.apache.polaris.service.ratelimiter.RateLimiter; import org.apache.polaris.service.ratelimiter.RateLimiterFilterConfiguration; @@ -246,13 +245,6 @@ public StsClientsPool stsClientsPool( return new StsClientsPool(config.effectiveClientsCacheMaxSize(), httpClient, meterRegistry); } - @Produces - public ServiceIdentityRegistryFactory serviceIdentityRegistryFactory( - ServiceIdentityRegistryConfiguration config, - @Any Instance serviceIdentityRegistryFactories) { - return serviceIdentityRegistryFactories.select(Identifier.Literal.of(config.type())).get(); - } - /** * Eagerly initialize the in-memory default realm on startup, so that users can check the * credentials printed to stdout immediately. @@ -409,23 +401,15 @@ public ActiveRolesProvider activeRolesProvider( @Produces public OidcTenantResolver oidcTenantResolver( - org.apache.polaris.service.auth.external.OidcConfiguration config, - @Any Instance resolvers) { + OidcConfiguration config, @Any Instance resolvers) { return resolvers.select(Identifier.Literal.of(config.tenantResolver())).get(); } - @Produces - @RequestScoped - public RealmServiceIdentityConfiguration realmServiceIdentityConfig( - ServiceIdentityConfiguration config, RealmContext realmContext) { - return config.forRealm(realmContext); - } - @Produces @RequestScoped public ServiceIdentityRegistry serviceIdentityRegistry( - ServiceIdentityRegistryFactory serviceIdentityRegistryFactory, RealmContext realmContext) { - return serviceIdentityRegistryFactory.getOrCreateServiceIdentityRegistry(realmContext); + RealmContext realmContext, ServiceIdentityConfiguration serviceIdentityConfiguration) { + return new DefaultServiceIdentityRegistry(realmContext, serviceIdentityConfiguration); } public void closeTaskExecutor(@Disposes @Identifier("task-executor") ManagedExecutor executor) { diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityConfiguration.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityConfiguration.java index 5cb41286a5..05deaa2eda 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityConfiguration.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityConfiguration.java @@ -76,4 +76,26 @@ default RealmServiceIdentityConfiguration forRealm(String realmIdentifier) { ? realms().get(realmIdentifier) : realms().get(DEFAULT_REALM_KEY); } + + /** + * Returns the actual key of the service identity configuration to use for the given {@link + * RealmContext}, falling back to the default if the specified realm is not configured. + * + * @param realmContext the realm context + * @return the actual realm identifier to use + */ + default String resolveRealm(RealmContext realmContext) { + return resolveRealm(realmContext.getRealmIdentifier()); + } + + /** + * Returns the actual key of the service identity configuration to use for the given realm + * identifier, falling back to the default if the specified realm is not configured. + * + * @param realmIdentifier the identifier of the realm + * @return the actual realm identifier to use + */ + default String resolveRealm(String realmIdentifier) { + return realms().containsKey(realmIdentifier) ? realmIdentifier : DEFAULT_REALM_KEY; + } } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityRegistryConfiguration.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityRegistryConfiguration.java index 4d72810e1d..9c063000da 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityRegistryConfiguration.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityRegistryConfiguration.java @@ -21,15 +21,15 @@ import io.quarkus.runtime.annotations.StaticInitSafe; import io.smallrye.config.ConfigMapping; -import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; @StaticInitSafe @ConfigMapping(prefix = "polaris.service-identity.registry") public interface ServiceIdentityRegistryConfiguration { /** - * The type of the ServiceIdentityRegistryFactory to use. This is the {@link - * ServiceIdentityRegistryFactory} identifier. + * The type of the ServiceIdentityRegistry to use. This is the {@link ServiceIdentityRegistry} + * identifier. */ String type(); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/DefaultServiceIdentityRegistry.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistry.java similarity index 51% rename from polaris-core/src/main/java/org/apache/polaris/core/identity/registry/DefaultServiceIdentityRegistry.java rename to runtime/service/src/main/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistry.java index 50f9d3aef0..7774232129 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/DefaultServiceIdentityRegistry.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistry.java @@ -17,15 +17,23 @@ * under the License. */ -package org.apache.polaris.core.identity.registry; +package org.apache.polaris.service.identity.registry; import com.google.common.annotations.VisibleForTesting; +import jakarta.inject.Inject; import java.util.EnumMap; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; +import org.apache.polaris.core.context.RealmContext; import org.apache.polaris.core.identity.ServiceIdentityType; import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; +import org.apache.polaris.core.secrets.ServiceSecretReference; +import org.apache.polaris.service.identity.RealmServiceIdentityConfiguration; +import org.apache.polaris.service.identity.ResolvableServiceIdentityConfiguration; +import org.apache.polaris.service.identity.ServiceIdentityConfiguration; /** * Default implementation of {@link ServiceIdentityRegistry} that resolves service identities from @@ -43,6 +51,10 @@ * */ public class DefaultServiceIdentityRegistry implements ServiceIdentityRegistry { + public static final String DEFAULT_REALM_KEY = ServiceIdentityConfiguration.DEFAULT_REALM_KEY; + public static final String DEFAULT_REALM_NSS = "system:default"; + private static final String IDENTITY_INFO_REFERENCE_URN_FORMAT = + "urn:polaris-secret:default-identity-registry:%s:%s"; /** Map of service identity types to their resolved identities. */ private final EnumMap resolvedServiceIdentities; @@ -50,6 +62,10 @@ public class DefaultServiceIdentityRegistry implements ServiceIdentityRegistry { /** Map of identity info references (URNs) to their resolved service identities. */ private final Map referenceToResolvedServiceIdentity; + public DefaultServiceIdentityRegistry() { + this(new EnumMap<>(ServiceIdentityType.class)); + } + public DefaultServiceIdentityRegistry( EnumMap serviceIdentities) { this.resolvedServiceIdentities = serviceIdentities; @@ -61,8 +77,41 @@ public DefaultServiceIdentityRegistry( identity -> identity)); } + @Inject + public DefaultServiceIdentityRegistry( + RealmContext realmContext, ServiceIdentityConfiguration serviceIdentityConfiguration) { + String serviceIdentityConfigKey = serviceIdentityConfiguration.resolveRealm(realmContext); + RealmServiceIdentityConfiguration realmServiceIdentityConfiguration = + serviceIdentityConfiguration.forRealm(realmContext); + + this.resolvedServiceIdentities = + realmServiceIdentityConfiguration.serviceIdentityConfigurations().stream() + .map(ResolvableServiceIdentityConfiguration::resolve) + .flatMap(Optional::stream) + .peek( + // Set the identity info reference for each resolved identity + identity -> + identity.setIdentityInfoReference( + buildIdentityInfoReference( + serviceIdentityConfigKey, identity.getIdentityType()))) + .collect( + // Collect to an EnumMap, grouping by ServiceIdentityType + Collectors.toMap( + ResolvedServiceIdentity::getIdentityType, + identity -> identity, + (a, b) -> b, + () -> new EnumMap<>(ServiceIdentityType.class))); + + this.referenceToResolvedServiceIdentity = + resolvedServiceIdentities.values().stream() + .collect( + Collectors.toMap( + identity -> identity.getIdentityInfoReference().getUrn(), + identity -> identity)); + } + @Override - public ServiceIdentityInfoDpo assignServiceIdentity(ServiceIdentityType serviceIdentityType) { + public ServiceIdentityInfoDpo discoverServiceIdentity(ServiceIdentityType serviceIdentityType) { ResolvedServiceIdentity resolvedServiceIdentity = resolvedServiceIdentities.get(serviceIdentityType); if (resolvedServiceIdentity == null) { @@ -85,4 +134,25 @@ public ResolvedServiceIdentity resolveServiceIdentity( public EnumMap getResolvedServiceIdentities() { return resolvedServiceIdentities; } + + /** + * Builds a {@link ServiceSecretReference} for the given realm and service identity type. + * + *

The URN format is: + * urn:polaris-service-secret:default-identity-registry:<realm>:<type> + * + *

If the realm is the default realm key, it is replaced with "system:default" in the URN. + * + * @param realm the realm identifier + * @param type the service identity type + * @return the constructed service secret reference + */ + private ServiceSecretReference buildIdentityInfoReference( + String realm, ServiceIdentityType type) { + // urn:polaris-service-secret:default-identity-registry:: + return new ServiceSecretReference( + IDENTITY_INFO_REFERENCE_URN_FORMAT.formatted( + realm.equals(DEFAULT_REALM_KEY) ? DEFAULT_REALM_NSS : realm, type.name()), + Map.of()); + } } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistryFactory.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistryFactory.java deleted file mode 100644 index 17a7af02c3..0000000000 --- a/runtime/service/src/main/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistryFactory.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.polaris.service.identity.registry; - -import com.google.common.annotations.VisibleForTesting; -import io.smallrye.common.annotation.Identifier; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import java.util.EnumMap; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; -import org.apache.polaris.core.context.RealmContext; -import org.apache.polaris.core.identity.ServiceIdentityType; -import org.apache.polaris.core.identity.registry.DefaultServiceIdentityRegistry; -import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; -import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; -import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; -import org.apache.polaris.core.secrets.ServiceSecretReference; -import org.apache.polaris.service.identity.RealmServiceIdentityConfiguration; -import org.apache.polaris.service.identity.ResolvableServiceIdentityConfiguration; -import org.apache.polaris.service.identity.ServiceIdentityConfiguration; - -@ApplicationScoped -@Identifier("default") -public class DefaultServiceIdentityRegistryFactory implements ServiceIdentityRegistryFactory { - private static final String DEFAULT_REALM_KEY = ServiceIdentityConfiguration.DEFAULT_REALM_KEY; - private static final String DEFAULT_REALM_NSS = "system:default"; - private static final String IDENTITY_INFO_REFERENCE_URN_FORMAT = - "urn:polaris-secret:default-identity-registry:%s:%s"; - - private final Map realmServiceIdentityRegistries; - - @Inject - public DefaultServiceIdentityRegistryFactory( - ServiceIdentityConfiguration serviceIdentityConfiguration) { - realmServiceIdentityRegistries = - serviceIdentityConfiguration.realms().entrySet().stream() - .collect( - Collectors.toMap( - Map.Entry::getKey, // realm identifier - entry -> { - RealmServiceIdentityConfiguration realmConfig = entry.getValue(); - - // Resolve all the service identities for the realm - EnumMap resolvedIdentities = - realmConfig.serviceIdentityConfigurations().stream() - .map(ResolvableServiceIdentityConfiguration::resolve) - .flatMap(Optional::stream) - .peek( - // Set the identity info reference for each resolved identity - identity -> - identity.setIdentityInfoReference( - buildIdentityInfoReference( - entry.getKey(), identity.getIdentityType()))) - .collect( - // Collect to an EnumMap, grouping by ServiceIdentityType - Collectors.toMap( - ResolvedServiceIdentity::getIdentityType, - identity -> identity, - (a, b) -> b, - () -> new EnumMap<>(ServiceIdentityType.class))); - return new DefaultServiceIdentityRegistry(resolvedIdentities); - })); - - if (!realmServiceIdentityRegistries.containsKey(DEFAULT_REALM_KEY)) { - // If no default realm is defined, create an empty registry - realmServiceIdentityRegistries.put( - DEFAULT_REALM_KEY, - new DefaultServiceIdentityRegistry(new EnumMap<>(ServiceIdentityType.class))); - } - } - - public DefaultServiceIdentityRegistryFactory() { - this(new DefaultServiceIdentityRegistry(new EnumMap<>(ServiceIdentityType.class))); - } - - public DefaultServiceIdentityRegistryFactory( - DefaultServiceIdentityRegistry defaultServiceIdentityRegistry) { - this(Map.of(DEFAULT_REALM_KEY, defaultServiceIdentityRegistry)); - } - - public DefaultServiceIdentityRegistryFactory( - Map realmServiceIdentityRegistries) { - this.realmServiceIdentityRegistries = realmServiceIdentityRegistries; - - if (!realmServiceIdentityRegistries.containsKey(DEFAULT_REALM_KEY)) { - // If no default realm is defined, create an empty registry - realmServiceIdentityRegistries.put( - DEFAULT_REALM_KEY, - new DefaultServiceIdentityRegistry(new EnumMap<>(ServiceIdentityType.class))); - } - } - - @Override - public ServiceIdentityRegistry getOrCreateServiceIdentityRegistry(RealmContext realmContext) { - return getServiceIdentityRegistryForRealm(realmContext); - } - - @VisibleForTesting - public Map getRealmServiceIdentityRegistries() { - return realmServiceIdentityRegistries; - } - - protected DefaultServiceIdentityRegistry getServiceIdentityRegistryForRealm( - RealmContext realmContext) { - return getServiceIdentityRegistryForRealm(realmContext.getRealmIdentifier()); - } - - protected DefaultServiceIdentityRegistry getServiceIdentityRegistryForRealm( - String realmIdentifier) { - return realmServiceIdentityRegistries.getOrDefault( - realmIdentifier, realmServiceIdentityRegistries.get(DEFAULT_REALM_KEY)); - } - - private ServiceSecretReference buildIdentityInfoReference( - String realm, ServiceIdentityType type) { - // urn:polaris-service-secret:default-identity-registry:: - return new ServiceSecretReference( - IDENTITY_INFO_REFERENCE_URN_FORMAT.formatted( - realm.equals(DEFAULT_REALM_KEY) ? DEFAULT_REALM_NSS : realm, type.name()), - Map.of()); - } -} diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java index 391140ecef..68a0bba931 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java @@ -50,7 +50,6 @@ import org.apache.polaris.core.entity.PrincipalEntity; import org.apache.polaris.core.entity.PrincipalRoleEntity; import org.apache.polaris.core.identity.ServiceIdentityType; -import org.apache.polaris.core.identity.registry.DefaultServiceIdentityRegistry; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.dao.entity.BaseResult; @@ -59,6 +58,7 @@ import org.apache.polaris.core.secrets.UnsafeInMemorySecretsManager; import org.apache.polaris.service.TestServices; import org.apache.polaris.service.config.ReservedProperties; +import org.apache.polaris.service.identity.registry.DefaultServiceIdentityRegistry; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java index 85542f9eb7..fe35fa991a 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java @@ -67,7 +67,6 @@ import org.apache.polaris.core.entity.PrincipalEntity; import org.apache.polaris.core.entity.PrincipalRoleEntity; import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; -import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.dao.entity.BaseResult; @@ -189,7 +188,7 @@ public Map getConfigOverrides() { @Inject protected ResolutionManifestFactory resolutionManifestFactory; @Inject protected CallContextCatalogFactory callContextCatalogFactory; @Inject protected UserSecretsManagerFactory userSecretsManagerFactory; - @Inject protected ServiceIdentityRegistryFactory serviceIdentityRegistryFactory; + @Inject protected ServiceIdentityRegistry serviceIdentityRegistry; @Inject protected PolarisDiagnostics diagServices; @Inject protected FileIOFactory fileIOFactory; @Inject protected PolarisEventListener polarisEventListener; @@ -204,7 +203,6 @@ public Map getConfigOverrides() { protected PolarisAdminService adminService; protected PolarisMetaStoreManager metaStoreManager; protected UserSecretsManager userSecretsManager; - protected ServiceIdentityRegistry serviceIdentityRegistry; protected PolarisBaseEntity catalogEntity; protected PrincipalEntity principalEntity; protected CallContext callContext; @@ -237,8 +235,6 @@ public void before(TestInfo testInfo) { QuarkusMock.installMockForType(containerRequestContext, ContainerRequestContext.class); metaStoreManager = managerFactory.getOrCreateMetaStoreManager(realmContext); userSecretsManager = userSecretsManagerFactory.getOrCreateUserSecretsManager(realmContext); - serviceIdentityRegistry = - serviceIdentityRegistryFactory.getOrCreateServiceIdentityRegistry(realmContext); polarisContext = new PolarisCallContext( diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplTest.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplTest.java index 2bf58e29a8..1a38de819e 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplTest.java @@ -38,7 +38,7 @@ import org.apache.polaris.core.config.FeatureConfiguration; import org.apache.polaris.core.config.RealmConfig; import org.apache.polaris.core.context.CallContext; -import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory; import org.apache.polaris.core.secrets.UserSecretsManagerFactory; @@ -55,7 +55,7 @@ public class PolarisServiceImplTest { private ResolutionManifestFactory resolutionManifestFactory; private MetaStoreManagerFactory metaStoreManagerFactory; private UserSecretsManagerFactory userSecretsManagerFactory; - private ServiceIdentityRegistryFactory serviceIdentityRegistryFactory; + private ServiceIdentityRegistry serviceIdentityRegistry; private PolarisAuthorizer polarisAuthorizer; private CallContext callContext; private ReservedProperties reservedProperties; @@ -69,7 +69,7 @@ void setUp() { resolutionManifestFactory = Mockito.mock(ResolutionManifestFactory.class); metaStoreManagerFactory = Mockito.mock(MetaStoreManagerFactory.class); userSecretsManagerFactory = Mockito.mock(UserSecretsManagerFactory.class); - serviceIdentityRegistryFactory = Mockito.mock(ServiceIdentityRegistryFactory.class); + serviceIdentityRegistry = Mockito.mock(ServiceIdentityRegistry.class); polarisAuthorizer = Mockito.mock(PolarisAuthorizer.class); callContext = Mockito.mock(CallContext.class); reservedProperties = Mockito.mock(ReservedProperties.class); @@ -89,7 +89,7 @@ void setUp() { resolutionManifestFactory, metaStoreManagerFactory, userSecretsManagerFactory, - serviceIdentityRegistryFactory, + serviceIdentityRegistry, polarisAuthorizer, callContext, reservedProperties, diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/AbstractPolarisGenericTableCatalogTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/AbstractPolarisGenericTableCatalogTest.java index ac1ba55936..0748f5887f 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/AbstractPolarisGenericTableCatalogTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/AbstractPolarisGenericTableCatalogTest.java @@ -53,7 +53,6 @@ import org.apache.polaris.core.entity.PrincipalEntity; import org.apache.polaris.core.entity.table.GenericTableEntity; import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; -import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory; @@ -100,7 +99,7 @@ public abstract class AbstractPolarisGenericTableCatalogTest { @Inject MetaStoreManagerFactory metaStoreManagerFactory; @Inject UserSecretsManagerFactory userSecretsManagerFactory; - @Inject ServiceIdentityRegistryFactory identityRegistryFactory; + @Inject ServiceIdentityRegistry serviceIdentityRegistry; @Inject PolarisConfigurationStore configurationStore; @Inject StorageCredentialCache storageCredentialCache; @Inject PolarisStorageIntegrationProvider storageIntegrationProvider; @@ -114,7 +113,6 @@ public abstract class AbstractPolarisGenericTableCatalogTest { private String realmName; private PolarisMetaStoreManager metaStoreManager; private UserSecretsManager userSecretsManager; - private ServiceIdentityRegistry serviceIdentityRegistry; private PolarisCallContext polarisContext; private RealmConfig realmConfig; private PolarisAdminService adminService; @@ -152,8 +150,6 @@ public void before(TestInfo testInfo) { QuarkusMock.installMockForType(realmContext, RealmContext.class); metaStoreManager = metaStoreManagerFactory.getOrCreateMetaStoreManager(realmContext); userSecretsManager = userSecretsManagerFactory.getOrCreateUserSecretsManager(realmContext); - serviceIdentityRegistry = - identityRegistryFactory.getOrCreateServiceIdentityRegistry(realmContext); polarisContext = new PolarisCallContext( realmContext, diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogTest.java index 8afbe1f8b3..044b13e153 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogTest.java @@ -109,7 +109,6 @@ import org.apache.polaris.core.entity.TaskEntity; import org.apache.polaris.core.exceptions.CommitConflictException; import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; -import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; @@ -232,7 +231,7 @@ public Map getConfigOverrides() { @Inject StorageCredentialCache storageCredentialCache; @Inject PolarisStorageIntegrationProvider storageIntegrationProvider; @Inject UserSecretsManagerFactory userSecretsManagerFactory; - @Inject ServiceIdentityRegistryFactory serviceIdentityRegistryFactory; + @Inject ServiceIdentityRegistry serviceIdentityRegistry; @Inject PolarisDiagnostics diagServices; @Inject PolarisEventListener polarisEventListener; @@ -240,7 +239,6 @@ public Map getConfigOverrides() { private String realmName; private PolarisMetaStoreManager metaStoreManager; private UserSecretsManager userSecretsManager; - private ServiceIdentityRegistry serviceIdentityRegistry; private PolarisCallContext polarisContext; private RealmConfig realmConfig; private PolarisAdminService adminService; @@ -283,8 +281,6 @@ public void before(TestInfo testInfo) { QuarkusMock.installMockForType(realmContext, RealmContext.class); metaStoreManager = metaStoreManagerFactory.getOrCreateMetaStoreManager(realmContext); userSecretsManager = userSecretsManagerFactory.getOrCreateUserSecretsManager(realmContext); - serviceIdentityRegistry = - serviceIdentityRegistryFactory.getOrCreateServiceIdentityRegistry(realmContext); polarisContext = new PolarisCallContext( realmContext, diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogViewTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogViewTest.java index 27ad039bce..a34a78e5f0 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogViewTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogViewTest.java @@ -50,7 +50,6 @@ import org.apache.polaris.core.entity.CatalogEntity; import org.apache.polaris.core.entity.PrincipalEntity; import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; -import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory; @@ -112,7 +111,7 @@ public Map getConfigOverrides() { @Inject MetaStoreManagerFactory metaStoreManagerFactory; @Inject UserSecretsManagerFactory userSecretsManagerFactory; - @Inject ServiceIdentityRegistryFactory serviceIdentityRegistryFactory; + @Inject ServiceIdentityRegistry serviceIdentityRegistry; @Inject PolarisConfigurationStore configurationStore; @Inject StorageCredentialCache storageCredentialCache; @Inject PolarisDiagnostics diagServices; @@ -125,7 +124,6 @@ public Map getConfigOverrides() { private String realmName; private PolarisMetaStoreManager metaStoreManager; private UserSecretsManager userSecretsManager; - private ServiceIdentityRegistry serviceIdentityRegistry; private PolarisCallContext polarisContext; private RealmConfig realmConfig; @@ -162,8 +160,6 @@ public void before(TestInfo testInfo) { metaStoreManager = metaStoreManagerFactory.getOrCreateMetaStoreManager(realmContext); userSecretsManager = userSecretsManagerFactory.getOrCreateUserSecretsManager(realmContext); - serviceIdentityRegistry = - serviceIdentityRegistryFactory.getOrCreateServiceIdentityRegistry(realmContext); polarisContext = new PolarisCallContext( realmContext, diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java index d49df676eb..acb5b16a55 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java @@ -59,7 +59,6 @@ import org.apache.polaris.core.entity.PolarisEntity; import org.apache.polaris.core.entity.PrincipalEntity; import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; -import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.PolicyMappingAlreadyExistsException; @@ -126,7 +125,7 @@ public abstract class AbstractPolicyCatalogTest { @Inject MetaStoreManagerFactory metaStoreManagerFactory; @Inject UserSecretsManagerFactory userSecretsManagerFactory; - @Inject ServiceIdentityRegistryFactory serviceIdentityRegistryFactory; + @Inject ServiceIdentityRegistry serviceIdentityRegistry; @Inject PolarisConfigurationStore configurationStore; @Inject StorageCredentialCache storageCredentialCache; @Inject PolarisStorageIntegrationProvider storageIntegrationProvider; @@ -140,7 +139,6 @@ public abstract class AbstractPolicyCatalogTest { private String realmName; private PolarisMetaStoreManager metaStoreManager; private UserSecretsManager userSecretsManager; - private ServiceIdentityRegistry serviceIdentityRegistry; private PolarisCallContext polarisContext; private RealmConfig realmConfig; private PolarisAdminService adminService; @@ -173,8 +171,6 @@ public void before(TestInfo testInfo) { QuarkusMock.installMockForType(realmContext, RealmContext.class); metaStoreManager = metaStoreManagerFactory.getOrCreateMetaStoreManager(realmContext); userSecretsManager = userSecretsManagerFactory.getOrCreateUserSecretsManager(realmContext); - serviceIdentityRegistry = - serviceIdentityRegistryFactory.getOrCreateServiceIdentityRegistry(realmContext); polarisContext = new PolarisCallContext( realmContext, diff --git a/runtime/service/src/test/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistryTest.java b/runtime/service/src/test/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistryTest.java index 4ca651dcc9..a89d33246d 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistryTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistryTest.java @@ -19,6 +19,7 @@ package org.apache.polaris.service.identity.registry; +import io.quarkus.test.InjectMock; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.QuarkusTestProfile; import io.quarkus.test.junit.TestProfile; @@ -26,9 +27,8 @@ import java.util.EnumMap; import java.util.Map; import java.util.Optional; +import org.apache.polaris.core.context.RealmContext; import org.apache.polaris.core.identity.ServiceIdentityType; -import org.apache.polaris.core.identity.registry.DefaultServiceIdentityRegistry; -import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; import org.apache.polaris.core.identity.resolved.ResolvedAwsIamServiceIdentity; import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; import org.apache.polaris.core.secrets.ServiceSecretReference; @@ -36,6 +36,7 @@ import org.apache.polaris.service.identity.ServiceIdentityConfiguration; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; @QuarkusTest @TestProfile(DefaultServiceIdentityRegistryTest.Profile.class) @@ -43,8 +44,8 @@ public class DefaultServiceIdentityRegistryTest { private static final String DEFAULT_REALM_KEY = ServiceIdentityConfiguration.DEFAULT_REALM_KEY; private static final String MY_REALM_KEY = "my-realm"; + @InjectMock RealmContext realmContext; @Inject ServiceIdentityConfiguration serviceIdentityConfiguration; - @Inject ServiceIdentityRegistryFactory serviceIdentityRegistryFactory; public static class Profile implements QuarkusTestProfile { @Override @@ -112,10 +113,9 @@ void testServiceIdentityConfiguration() { @Test void testRealmServiceIdentityConfigToResolvedServiceIdentity() { // Check the default realm + Mockito.when(realmContext.getRealmIdentifier()).thenReturn(DEFAULT_REALM_KEY); DefaultServiceIdentityRegistry defaultRegistry = - (DefaultServiceIdentityRegistry) - serviceIdentityRegistryFactory.getOrCreateServiceIdentityRegistry( - () -> DEFAULT_REALM_KEY); + new DefaultServiceIdentityRegistry(realmContext, serviceIdentityConfiguration); EnumMap resolvedIdentities = defaultRegistry.getResolvedServiceIdentities(); @@ -136,9 +136,9 @@ void testRealmServiceIdentityConfigToResolvedServiceIdentity() { Assertions.assertThat(resolvedAwsIamServiceIdentity.getSessionToken()).isNull(); // Check the my-realm + Mockito.when(realmContext.getRealmIdentifier()).thenReturn(MY_REALM_KEY); DefaultServiceIdentityRegistry myRealmRegistry = - (DefaultServiceIdentityRegistry) - serviceIdentityRegistryFactory.getOrCreateServiceIdentityRegistry(() -> MY_REALM_KEY); + new DefaultServiceIdentityRegistry(realmContext, serviceIdentityConfiguration); resolvedIdentities = myRealmRegistry.getResolvedServiceIdentities(); Assertions.assertThat(resolvedIdentities) @@ -160,10 +160,25 @@ void testRealmServiceIdentityConfigToResolvedServiceIdentity() { Assertions.assertThat(resolvedAwsIamServiceIdentity.getSessionToken()) .isEqualTo("session-token"); - // Check the other realm + // Check the other realm which does not exist in the configuration, should fallback to default + Mockito.when(realmContext.getRealmIdentifier()).thenReturn("other-realm"); DefaultServiceIdentityRegistry otherRegistry = - (DefaultServiceIdentityRegistry) - serviceIdentityRegistryFactory.getOrCreateServiceIdentityRegistry(() -> "other-realm"); - Assertions.assertThat(otherRegistry).isEqualTo(defaultRegistry); + new DefaultServiceIdentityRegistry(realmContext, serviceIdentityConfiguration); + resolvedIdentities = otherRegistry.getResolvedServiceIdentities(); + Assertions.assertThat(resolvedIdentities) + .containsKey(ServiceIdentityType.AWS_IAM) + .size() + .isEqualTo(1); + resolvedAwsIamServiceIdentity = + (ResolvedAwsIamServiceIdentity) resolvedIdentities.get(ServiceIdentityType.AWS_IAM); + Assertions.assertThat(resolvedAwsIamServiceIdentity.getIamArn()) + .isEqualTo("arn:aws:iam::123456789012:user/polaris-default-iam-user"); + Assertions.assertThat(resolvedAwsIamServiceIdentity.getIdentityInfoReference()) + .isEqualTo( + new ServiceSecretReference( + "urn:polaris-secret:default-identity-registry:system:default:AWS_IAM", Map.of())); + Assertions.assertThat(resolvedAwsIamServiceIdentity.getAccessKeyId()).isNull(); + Assertions.assertThat(resolvedAwsIamServiceIdentity.getSecretAccessKey()).isNull(); + Assertions.assertThat(resolvedAwsIamServiceIdentity.getSessionToken()).isNull(); } } diff --git a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java index 03086969a9..1e50d3be9e 100644 --- a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java +++ b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java @@ -43,7 +43,7 @@ import org.apache.polaris.core.context.CallContext; import org.apache.polaris.core.context.RealmContext; import org.apache.polaris.core.entity.PrincipalEntity; -import org.apache.polaris.core.identity.registry.ServiceIdentityRegistryFactory; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; import org.apache.polaris.core.persistence.BasePersistence; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; @@ -71,7 +71,7 @@ import org.apache.polaris.service.context.catalog.PolarisCallContextCatalogFactory; import org.apache.polaris.service.events.listeners.PolarisEventListener; import org.apache.polaris.service.events.listeners.TestPolarisEventListener; -import org.apache.polaris.service.identity.registry.DefaultServiceIdentityRegistryFactory; +import org.apache.polaris.service.identity.registry.DefaultServiceIdentityRegistry; import org.apache.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory; import org.apache.polaris.service.secrets.UnsafeInMemorySecretsManagerFactory; import org.apache.polaris.service.storage.PolarisStorageIntegrationProviderImpl; @@ -174,8 +174,6 @@ public TestServices build() { UserSecretsManagerFactory userSecretsManagerFactory = new UnsafeInMemorySecretsManagerFactory(); - ServiceIdentityRegistryFactory serviceIdentityRegistryFactory = - new DefaultServiceIdentityRegistryFactory(); BasePersistence metaStoreSession = metaStoreManagerFactory.getOrCreateSession(realmContext); CallContext callContext = @@ -201,6 +199,7 @@ public TestServices build() { new ResolutionManifestFactoryImpl(diagnostics, resolverFactory); UserSecretsManager userSecretsManager = userSecretsManagerFactory.getOrCreateUserSecretsManager(realmContext); + ServiceIdentityRegistry serviceIdentityRegistry = new DefaultServiceIdentityRegistry(); FileIOFactory fileIOFactory = fileIOFactorySupplier.apply(storageCredentialCache, metaStoreManagerFactory); @@ -289,7 +288,7 @@ public String getAuthenticationScheme() { resolutionManifestFactory, metaStoreManagerFactory, userSecretsManagerFactory, - serviceIdentityRegistryFactory, + serviceIdentityRegistry, authorizer, callContext, reservedProperties, From 1f2ac2bfa29bcfaa47754623d7daef5f48dd4d5f Mon Sep 17 00:00:00 2001 From: Rulin Xing Date: Thu, 18 Sep 2025 04:18:06 -0700 Subject: [PATCH 3/7] Return injected service identity info in response --- .../connection/ConnectionConfigInfoDpo.java | 4 +- .../hadoop/HadoopConnectionConfigInfoDpo.java | 8 +- .../hive/HiveConnectionConfigInfoDpo.java | 11 ++- .../IcebergRestConnectionConfigInfoDpo.java | 8 +- .../polaris/core/entity/CatalogEntity.java | 10 ++- .../dpo/AwsIamServiceIdentityInfoDpo.java | 11 --- .../identity/dpo/ServiceIdentityInfoDpo.java | 19 ++++- .../registry/ServiceIdentityRegistry.java | 4 +- .../ResolvedAwsIamServiceIdentity.java | 15 ++++ .../resolved/ResolvedServiceIdentity.java | 3 + .../ConnectionConfigInfoDpoTest.java | 26 ++++-- .../src/main/resources/application.properties | 4 +- .../service/admin/PolarisAdminService.java | 4 +- .../service/admin/PolarisServiceImpl.java | 19 +++-- .../DefaultServiceIdentityRegistry.java | 4 +- .../admin/PolarisAdminServiceAuthzTest.java | 12 ++- .../service/admin/PolarisAuthzTestBase.java | 2 +- .../service/admin/PolarisServiceImplTest.java | 8 +- ...bstractPolarisGenericTableCatalogTest.java | 2 +- .../iceberg/AbstractIcebergCatalogTest.java | 12 +-- .../AbstractIcebergCatalogViewTest.java | 2 +- .../IcebergCatalogHandlerAuthzTest.java | 2 +- .../policy/AbstractPolicyCatalogTest.java | 2 +- .../service/entity/CatalogEntityTest.java | 85 ++++++++++++++++++- .../apache/polaris/service/TestServices.java | 6 +- 25 files changed, 220 insertions(+), 63 deletions(-) diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpo.java index bb95dc4429..2db61531e3 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpo.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpo.java @@ -41,6 +41,7 @@ import org.apache.polaris.core.connection.iceberg.IcebergCatalogPropertiesProvider; import org.apache.polaris.core.connection.iceberg.IcebergRestConnectionConfigInfoDpo; import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; import org.apache.polaris.core.secrets.SecretReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -229,5 +230,6 @@ public abstract ConnectionConfigInfoDpo withServiceIdentity( * fields are one-to-one direct mappings, but some fields, such as secretReferences, might only be * applicable/present in the persistence object, but not the API model object. */ - public abstract ConnectionConfigInfo asConnectionConfigInfoModel(); + public abstract ConnectionConfigInfo asConnectionConfigInfoModel( + ServiceIdentityRegistry serviceIdentityRegistry); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/hadoop/HadoopConnectionConfigInfoDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/hadoop/HadoopConnectionConfigInfoDpo.java index 05104dd208..b9a7ef8267 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/connection/hadoop/HadoopConnectionConfigInfoDpo.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/hadoop/HadoopConnectionConfigInfoDpo.java @@ -32,6 +32,7 @@ import org.apache.polaris.core.connection.ConnectionConfigInfoDpo; import org.apache.polaris.core.connection.ConnectionType; import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; import org.apache.polaris.core.secrets.UserSecretsManager; /** @@ -88,7 +89,8 @@ public ConnectionConfigInfoDpo withServiceIdentity( } @Override - public ConnectionConfigInfo asConnectionConfigInfoModel() { + public ConnectionConfigInfo asConnectionConfigInfoModel( + ServiceIdentityRegistry serviceIdentityRegistry) { return HadoopConnectionConfigInfo.builder() .setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.HADOOP) .setUri(getUri()) @@ -97,7 +99,9 @@ public ConnectionConfigInfo asConnectionConfigInfoModel() { getAuthenticationParameters().asAuthenticationParametersModel()) .setServiceIdentity( Optional.ofNullable(getServiceIdentity()) - .map(ServiceIdentityInfoDpo::asServiceIdentityInfoModel) + .map( + serviceIdentityInfoDpo -> + serviceIdentityInfoDpo.asServiceIdentityInfoModel(serviceIdentityRegistry)) .orElse(null)) .build(); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/hive/HiveConnectionConfigInfoDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/hive/HiveConnectionConfigInfoDpo.java index 1f9027f2be..1d88c389c6 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/connection/hive/HiveConnectionConfigInfoDpo.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/hive/HiveConnectionConfigInfoDpo.java @@ -24,6 +24,7 @@ import jakarta.annotation.Nullable; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import org.apache.iceberg.CatalogProperties; import org.apache.polaris.core.admin.model.ConnectionConfigInfo; import org.apache.polaris.core.admin.model.HiveConnectionConfigInfo; @@ -31,6 +32,7 @@ import org.apache.polaris.core.connection.ConnectionConfigInfoDpo; import org.apache.polaris.core.connection.ConnectionType; import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; import org.apache.polaris.core.secrets.UserSecretsManager; /** @@ -88,13 +90,20 @@ public ConnectionConfigInfoDpo withServiceIdentity( } @Override - public ConnectionConfigInfo asConnectionConfigInfoModel() { + public ConnectionConfigInfo asConnectionConfigInfoModel( + ServiceIdentityRegistry serviceIdentityRegistry) { return HiveConnectionConfigInfo.builder() .setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.HIVE) .setUri(getUri()) .setWarehouse(getWarehouse()) .setAuthenticationParameters( getAuthenticationParameters().asAuthenticationParametersModel()) + .setServiceIdentity( + Optional.ofNullable(getServiceIdentity()) + .map( + serviceIdentityInfoDpo -> + serviceIdentityInfoDpo.asServiceIdentityInfoModel(serviceIdentityRegistry)) + .orElse(null)) .build(); } } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpo.java index 0a6e870b7f..6ac49a2ed0 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpo.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpo.java @@ -32,6 +32,7 @@ import org.apache.polaris.core.connection.ConnectionConfigInfoDpo; import org.apache.polaris.core.connection.ConnectionType; import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; import org.apache.polaris.core.secrets.UserSecretsManager; /** @@ -80,7 +81,8 @@ public ConnectionConfigInfoDpo withServiceIdentity( } @Override - public ConnectionConfigInfo asConnectionConfigInfoModel() { + public ConnectionConfigInfo asConnectionConfigInfoModel( + ServiceIdentityRegistry serviceIdentityRegistry) { return IcebergRestConnectionConfigInfo.builder() .setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.ICEBERG_REST) .setUri(getUri()) @@ -89,7 +91,9 @@ public ConnectionConfigInfo asConnectionConfigInfoModel() { getAuthenticationParameters().asAuthenticationParametersModel()) .setServiceIdentity( Optional.ofNullable(getServiceIdentity()) - .map(ServiceIdentityInfoDpo::asServiceIdentityInfoModel) + .map( + serviceIdentityInfoDpo -> + serviceIdentityInfoDpo.asServiceIdentityInfoModel(serviceIdentityRegistry)) .orElse(null)) .build(); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java b/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java index 166b077e02..368724b47f 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java @@ -44,6 +44,7 @@ import org.apache.polaris.core.config.RealmConfig; import org.apache.polaris.core.connection.ConnectionConfigInfoDpo; import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; import org.apache.polaris.core.secrets.SecretReference; import org.apache.polaris.core.storage.FileStorageConfigurationInfo; import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo; @@ -108,7 +109,7 @@ public static CatalogEntity fromCatalog(RealmConfig realmConfig, Catalog catalog return builder.build(); } - public Catalog asCatalog() { + public Catalog asCatalog(ServiceIdentityRegistry serviceIdentityRegistry) { Map internalProperties = getInternalPropertiesAsMap(); Catalog.TypeEnum catalogType = Optional.ofNullable(internalProperties.get(CATALOG_TYPE_PROPERTY)) @@ -128,7 +129,7 @@ public Catalog asCatalog() { .setLastUpdateTimestamp(getLastUpdateTimestamp()) .setEntityVersion(getEntityVersion()) .setStorageConfigInfo(getStorageInfo(internalProperties)) - .setConnectionConfigInfo(getConnectionInfo(internalProperties)) + .setConnectionConfigInfo(getConnectionInfo(internalProperties, serviceIdentityRegistry)) .build() : PolarisCatalog.builder() .setType(Catalog.TypeEnum.INTERNAL) @@ -187,11 +188,12 @@ private StorageConfigInfo getStorageInfo(Map internalProperties) return null; } - private ConnectionConfigInfo getConnectionInfo(Map internalProperties) { + private ConnectionConfigInfo getConnectionInfo( + Map internalProperties, ServiceIdentityRegistry serviceIdentityRegistry) { if (internalProperties.containsKey( PolarisEntityConstants.getConnectionConfigInfoPropertyName())) { ConnectionConfigInfoDpo configInfo = getConnectionConfigInfoDpo(); - return configInfo.asConnectionConfigInfoModel(); + return configInfo.asConnectionConfigInfoModel(serviceIdentityRegistry); } return null; } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/AwsIamServiceIdentityInfoDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/AwsIamServiceIdentityInfoDpo.java index cddad3a422..cc8066b496 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/AwsIamServiceIdentityInfoDpo.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/AwsIamServiceIdentityInfoDpo.java @@ -21,10 +21,8 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.base.MoreObjects; -import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import org.apache.polaris.core.admin.model.AwsIamServiceIdentityInfo; -import org.apache.polaris.core.admin.model.ServiceIdentityInfo; import org.apache.polaris.core.identity.ServiceIdentityType; import org.apache.polaris.core.secrets.SecretReference; @@ -51,15 +49,6 @@ public AwsIamServiceIdentityInfoDpo( super(ServiceIdentityType.AWS_IAM.getCode(), identityInfoReference); } - @Override - public @Nonnull ServiceIdentityInfo asServiceIdentityInfoModel() { - return AwsIamServiceIdentityInfo.builder() - .setIdentityType(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM) - // TODO: inject service identity info - .setIamArn("") - .build(); - } - @Override public String toString() { return MoreObjects.toStringHelper(this) diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/ServiceIdentityInfoDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/ServiceIdentityInfoDpo.java index 22f25dd70d..fb34413414 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/ServiceIdentityInfoDpo.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/dpo/ServiceIdentityInfoDpo.java @@ -23,10 +23,11 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.google.common.base.MoreObjects; -import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import org.apache.polaris.core.admin.model.ServiceIdentityInfo; import org.apache.polaris.core.identity.ServiceIdentityType; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; +import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; import org.apache.polaris.core.secrets.SecretReference; /** @@ -73,9 +74,21 @@ public SecretReference getIdentityInfoReference() { /** * Converts this persistence object to the corresponding API model. During the conversion, some - * fields will be dropped, e.g. the reference to the service identity's credential + * fields will be dropped, e.g., the reference to the service identity's credential + * + * @param serviceIdentityRegistry the service identity registry to resolve the identity */ - public abstract @Nonnull ServiceIdentityInfo asServiceIdentityInfoModel(); + public @Nullable ServiceIdentityInfo asServiceIdentityInfoModel( + ServiceIdentityRegistry serviceIdentityRegistry) { + if (serviceIdentityRegistry == null) { + return null; + } + + return serviceIdentityRegistry + .resolveServiceIdentity(this) + .map(ResolvedServiceIdentity::asServiceIdentityInfoModel) + .orElse(null); + } @Override public String toString() { diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistry.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistry.java index f9b81ba22f..f4b6b781e7 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistry.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistry.java @@ -19,6 +19,7 @@ package org.apache.polaris.core.identity.registry; +import java.util.Optional; import org.apache.polaris.core.identity.ServiceIdentityType; import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; @@ -52,5 +53,6 @@ public interface ServiceIdentityRegistry { * @param serviceIdentityInfo The service identity metadata to resolve. * @return A {@link ResolvedServiceIdentity} including credentials and other resolved data. */ - ResolvedServiceIdentity resolveServiceIdentity(ServiceIdentityInfoDpo serviceIdentityInfo); + Optional resolveServiceIdentity( + ServiceIdentityInfoDpo serviceIdentityInfo); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedAwsIamServiceIdentity.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedAwsIamServiceIdentity.java index dcf928b525..99e62ecf7d 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedAwsIamServiceIdentity.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedAwsIamServiceIdentity.java @@ -21,6 +21,8 @@ import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import jakarta.annotation.Nonnull; +import org.apache.polaris.core.admin.model.AwsIamServiceIdentityInfo; +import org.apache.polaris.core.admin.model.ServiceIdentityInfo; import org.apache.polaris.core.identity.ServiceIdentityType; import org.apache.polaris.core.identity.dpo.AwsIamServiceIdentityInfoDpo; import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; @@ -56,6 +58,10 @@ public class ResolvedAwsIamServiceIdentity extends ResolvedServiceIdentity { /** The AWS session token of the AWS credential associated with the identity. */ private final String sessionToken; + public ResolvedAwsIamServiceIdentity(String iamArn) { + this(null, iamArn, null, null, null); + } + public ResolvedAwsIamServiceIdentity( String iamArn, String accessKeyId, String secretAccessKey, String sessionToken) { this(null, iamArn, accessKeyId, secretAccessKey, sessionToken); @@ -96,6 +102,15 @@ public ServiceIdentityInfoDpo asServiceIdentityInfoDpo() { return new AwsIamServiceIdentityInfoDpo(getIdentityInfoReference()); } + @Nonnull + @Override + public ServiceIdentityInfo asServiceIdentityInfoModel() { + return AwsIamServiceIdentityInfo.builder() + .setIdentityType(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM) + .setIamArn(getIamArn()) + .build(); + } + /** Returns a memoized supplier for creating an STS client using the resolved credentials. */ public Supplier stsClientSupplier() { return Suppliers.memoize( diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedServiceIdentity.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedServiceIdentity.java index 74b6a9516c..d2272d69a7 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedServiceIdentity.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedServiceIdentity.java @@ -19,6 +19,7 @@ package org.apache.polaris.core.identity.resolved; import jakarta.annotation.Nonnull; +import org.apache.polaris.core.admin.model.ServiceIdentityInfo; import org.apache.polaris.core.identity.ServiceIdentityType; import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; import org.apache.polaris.core.secrets.ServiceSecretReference; @@ -59,4 +60,6 @@ public void setIdentityInfoReference(@NotNull ServiceSecretReference identityInf /** Converts this resolved identity into its corresponding persisted form (DPO). */ public abstract @Nonnull ServiceIdentityInfoDpo asServiceIdentityInfoDpo(); + + public abstract @Nonnull ServiceIdentityInfo asServiceIdentityInfoModel(); } diff --git a/polaris-core/src/test/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpoTest.java b/polaris-core/src/test/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpoTest.java index 727fac0d0f..b6d195bc68 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpoTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpoTest.java @@ -22,9 +22,14 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Optional; import org.apache.polaris.core.admin.model.ConnectionConfigInfo; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; +import org.apache.polaris.core.identity.resolved.ResolvedAwsIamServiceIdentity; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; public class ConnectionConfigInfoDpoTest { private static final ObjectMapper objectMapper = new ObjectMapper(); @@ -33,6 +38,17 @@ public class ConnectionConfigInfoDpoTest { objectMapper.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); } + private ServiceIdentityRegistry serviceIdentityRegistry; + + @BeforeEach + void setUp() { + serviceIdentityRegistry = Mockito.mock(ServiceIdentityRegistry.class); + Mockito.when(serviceIdentityRegistry.resolveServiceIdentity(Mockito.any())) + .thenReturn( + Optional.of( + new ResolvedAwsIamServiceIdentity("arn:aws:iam::123456789012:role/example-role"))); + } + @Test void testOAuthClientCredentialsParameters() throws JsonProcessingException { // Test deserialization and reserialization of the persistence JSON. @@ -64,7 +80,7 @@ void testOAuthClientCredentialsParameters() throws JsonProcessingException { // Test conversion into API model JSON. ConnectionConfigInfo connectionConfigInfoApiModel = - connectionConfigInfoDpo.asConnectionConfigInfoModel(); + connectionConfigInfoDpo.asConnectionConfigInfoModel(serviceIdentityRegistry); String expectedApiModelJson = "" + "{" @@ -111,7 +127,7 @@ void testBearerAuthenticationParameters() throws JsonProcessingException { // Test conversion into API model JSON. ConnectionConfigInfo connectionConfigInfoApiModel = - connectionConfigInfoDpo.asConnectionConfigInfoModel(); + connectionConfigInfoDpo.asConnectionConfigInfoModel(serviceIdentityRegistry); String expectedApiModelJson = "" + "{" @@ -148,7 +164,7 @@ void testImplicitAuthenticationParameters() throws JsonProcessingException { // Test conversion into API model JSON. ConnectionConfigInfo connectionConfigInfoApiModel = - connectionConfigInfoDpo.asConnectionConfigInfoModel(); + connectionConfigInfoDpo.asConnectionConfigInfoModel(serviceIdentityRegistry); String expectedApiModelJson = "" + "{" @@ -200,7 +216,7 @@ void testSigV4AuthenticationParameters() throws JsonProcessingException { // Test conversion into API model JSON. ConnectionConfigInfo connectionConfigInfoApiModel = - connectionConfigInfoDpo.asConnectionConfigInfoModel(); + connectionConfigInfoDpo.asConnectionConfigInfoModel(serviceIdentityRegistry); String expectedApiModelJson = "" + "{" @@ -217,7 +233,7 @@ void testSigV4AuthenticationParameters() throws JsonProcessingException { + " }," + " \"serviceIdentity\": {" + " \"identityType\": \"AWS_IAM\"," - + " \"iamArn\": \"\"" + + " \"iamArn\": \"arn:aws:iam::123456789012:role/example-role\"" + " }" + "}"; Assertions.assertEquals( diff --git a/runtime/defaults/src/main/resources/application.properties b/runtime/defaults/src/main/resources/application.properties index cd63ae33e0..359207a521 100644 --- a/runtime/defaults/src/main/resources/application.properties +++ b/runtime/defaults/src/main/resources/application.properties @@ -113,7 +113,7 @@ polaris.realm-context.require-header=false polaris.features."ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING"=false polaris.features."SUPPORTED_CATALOG_STORAGE_TYPES"=["S3","GCS","AZURE"] -# polaris.features."ENABLE_CATALOG_FEDERATION"=true +polaris.features."ENABLE_CATALOG_FEDERATION"=true polaris.features."SUPPORTED_CATALOG_CONNECTION_TYPES"=["ICEBERG_REST"] # realm overrides @@ -200,7 +200,7 @@ polaris.oidc.principal-roles-mapper.type=default # Polaris Service Identity Config # Default identity (can be overridden in per realm) -# polaris.service-identity.aws-iam.iam-arn=arn:aws:iam::123456789012:user/polaris-iam-user +polaris.service-identity.aws-iam.iam-arn=arn:aws:iam::123456789012:user/polaris-iam-user # polaris.service-identity.aws-iam.access-key-id=accessKeyId # polaris.service-identity.aws-iam.secret-access-key=secretAccessKey # polaris.service-identity.aws-iam.session-token=sessionToken diff --git a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java index eb46a4ef09..dce34625f2 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java @@ -973,7 +973,9 @@ private void validateUpdateCatalogDiffOrThrow( /** List all catalogs after checking for permission. */ public List listCatalogs() { authorizeBasicRootOperationOrThrow(PolarisAuthorizableOperation.LIST_CATALOGS); - return listCatalogsUnsafe().map(CatalogEntity::asCatalog).toList(); + return listCatalogsUnsafe() + .map(catalogEntity -> catalogEntity.asCatalog(getServiceIdentityRegistry())) + .toList(); } /** List all catalogs without checking for permission. */ diff --git a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java index aafc84ffdf..057f0990d4 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java @@ -71,8 +71,6 @@ import org.apache.polaris.core.entity.PrincipalEntity; import org.apache.polaris.core.entity.PrincipalRoleEntity; import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; -import org.apache.polaris.core.persistence.MetaStoreManagerFactory; -import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.dao.entity.BaseResult; import org.apache.polaris.core.persistence.dao.entity.PrivilegeResult; import org.apache.polaris.service.admin.api.PolarisCatalogsApiService; @@ -95,17 +93,20 @@ public class PolarisServiceImpl private final ReservedProperties reservedProperties; private final PolarisEventListener polarisEventListener; private final PolarisAdminService adminService; + private final ServiceIdentityRegistry serviceIdentityRegistry; @Inject public PolarisServiceImpl( RealmConfig realmConfig, ReservedProperties reservedProperties, PolarisEventListener polarisEventListener, - PolarisAdminService adminService) { + PolarisAdminService adminService, + ServiceIdentityRegistry serviceIdentityRegistry) { this.realmConfig = realmConfig; this.reservedProperties = reservedProperties; this.polarisEventListener = polarisEventListener; this.adminService = adminService; + this.serviceIdentityRegistry = serviceIdentityRegistry; } private static Response toResponse(BaseResult result, Response.Status successStatus) { @@ -131,7 +132,8 @@ public Response createCatalog( Catalog catalog = request.getCatalog(); validateStorageConfig(catalog.getStorageConfigInfo()); validateExternalCatalog(catalog); - Catalog newCatalog = CatalogEntity.of(adminService.createCatalog(request)).asCatalog(); + Catalog newCatalog = + CatalogEntity.of(adminService.createCatalog(request)).asCatalog(serviceIdentityRegistry); LOGGER.info("Created new catalog {}", newCatalog); return Response.status(Response.Status.CREATED).build(); } @@ -221,7 +223,8 @@ public Response deleteCatalog( @Override public Response getCatalog( String catalogName, RealmContext realmContext, SecurityContext securityContext) { - return Response.ok(adminService.getCatalog(catalogName).asCatalog()).build(); + return Response.ok(adminService.getCatalog(catalogName).asCatalog(serviceIdentityRegistry)) + .build(); } /** From PolarisCatalogsApiService */ @@ -234,7 +237,11 @@ public Response updateCatalog( if (updateRequest.getStorageConfigInfo() != null) { validateStorageConfig(updateRequest.getStorageConfigInfo()); } - return Response.ok(adminService.updateCatalog(catalogName, updateRequest).asCatalog()).build(); + return Response.ok( + adminService + .updateCatalog(catalogName, updateRequest) + .asCatalog(serviceIdentityRegistry)) + .build(); } /** From PolarisCatalogsApiService */ diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistry.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistry.java index 7774232129..0cda3c2109 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistry.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistry.java @@ -122,12 +122,12 @@ public ServiceIdentityInfoDpo discoverServiceIdentity(ServiceIdentityType servic } @Override - public ResolvedServiceIdentity resolveServiceIdentity( + public Optional resolveServiceIdentity( ServiceIdentityInfoDpo serviceIdentityInfo) { ResolvedServiceIdentity resolvedServiceIdentity = referenceToResolvedServiceIdentity.get( serviceIdentityInfo.getIdentityInfoReference().getUrn()); - return resolvedServiceIdentity; + return Optional.ofNullable(resolvedServiceIdentity); } @VisibleForTesting diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java index 611ebab559..fb8c1d92dd 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAdminServiceAuthzTest.java @@ -133,7 +133,8 @@ public void testCreateCatalogSufficientPrivileges() { adminService.grantPrivilegeOnRootContainerToPrincipalRole( PRINCIPAL_ROLE2, PolarisPrivilege.CATALOG_DROP)); final CatalogEntity newCatalog = new CatalogEntity.Builder().setName("new_catalog").build(); - final CreateCatalogRequest createRequest = new CreateCatalogRequest(newCatalog.asCatalog()); + final CreateCatalogRequest createRequest = + new CreateCatalogRequest(newCatalog.asCatalog(serviceIdentityRegistry)); doTestSufficientPrivileges( List.of( @@ -152,7 +153,8 @@ public void testCreateCatalogSufficientPrivileges() { @Test public void testCreateCatalogInsufficientPrivileges() { final CatalogEntity newCatalog = new CatalogEntity.Builder().setName("new_catalog").build(); - final CreateCatalogRequest createRequest = new CreateCatalogRequest(newCatalog.asCatalog()); + final CreateCatalogRequest createRequest = + new CreateCatalogRequest(newCatalog.asCatalog(serviceIdentityRegistry)); doTestInsufficientPrivileges( List.of( @@ -287,7 +289,8 @@ public void testDeleteCatalogSufficientPrivileges() { adminService.grantPrivilegeOnRootContainerToPrincipalRole( PRINCIPAL_ROLE2, PolarisPrivilege.CATALOG_CREATE)); final CatalogEntity newCatalog = new CatalogEntity.Builder().setName("new_catalog").build(); - final CreateCatalogRequest createRequest = new CreateCatalogRequest(newCatalog.asCatalog()); + final CreateCatalogRequest createRequest = + new CreateCatalogRequest(newCatalog.asCatalog(serviceIdentityRegistry)); adminService.createCatalog(createRequest); doTestSufficientPrivileges( @@ -307,7 +310,8 @@ public void testDeleteCatalogSufficientPrivileges() { @Test public void testDeleteCatalogInsufficientPrivileges() { final CatalogEntity newCatalog = new CatalogEntity.Builder().setName("new_catalog").build(); - final CreateCatalogRequest createRequest = new CreateCatalogRequest(newCatalog.asCatalog()); + final CreateCatalogRequest createRequest = + new CreateCatalogRequest(newCatalog.asCatalog(serviceIdentityRegistry)); adminService.createCatalog(createRequest); doTestInsufficientPrivileges( diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java index fe35fa991a..2ffc5ed27a 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java @@ -276,7 +276,7 @@ public void before(TestInfo testInfo) { .setDefaultBaseLocation(storageLocation) .setStorageConfigurationInfo(realmConfig, storageConfigModel, storageLocation) .build() - .asCatalog())); + .asCatalog(serviceIdentityRegistry))); initBaseCatalog(); diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplTest.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplTest.java index 5bec17c490..b280f20b04 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplTest.java @@ -41,7 +41,6 @@ import org.apache.polaris.core.config.RealmConfig; import org.apache.polaris.core.context.CallContext; import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; -import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory; import org.apache.polaris.core.secrets.UserSecretsManager; @@ -104,7 +103,12 @@ void setUp() { polarisAuthorizer, reservedProperties); polarisService = - new PolarisServiceImpl(realmConfig, reservedProperties, polarisEventListener, adminService); + new PolarisServiceImpl( + realmConfig, + reservedProperties, + polarisEventListener, + adminService, + serviceIdentityRegistry); } @Test diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/AbstractPolarisGenericTableCatalogTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/AbstractPolarisGenericTableCatalogTest.java index 0748f5887f..f6b5974267 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/AbstractPolarisGenericTableCatalogTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/AbstractPolarisGenericTableCatalogTest.java @@ -205,7 +205,7 @@ public void before(TestInfo testInfo) { FeatureConfiguration.DROP_WITH_PURGE_ENABLED.catalogConfig(), "true") .setStorageConfigurationInfo(realmConfig, storageConfigModel, storageLocation) .build() - .asCatalog())); + .asCatalog(serviceIdentityRegistry))); PolarisPassthroughResolutionView passthroughView = new PolarisPassthroughResolutionView( diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogTest.java index d034d53b76..ba90245d39 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogTest.java @@ -347,7 +347,7 @@ public void before(TestInfo testInfo) { FeatureConfiguration.DROP_WITH_PURGE_ENABLED.catalogConfig(), "true") .setStorageConfigurationInfo(realmConfig, storageConfigModel, storageLocation) .build() - .asCatalog())); + .asCatalog(serviceIdentityRegistry))); this.fileIOFactory = new DefaultFileIOFactory(storageCredentialCache, metaStoreManagerFactory); @@ -1311,7 +1311,7 @@ public void testUpdateNotificationCreateTableWithLocalFilePrefix() { .setDefaultBaseLocation("file://") .setName(catalogWithoutStorage) .build() - .asCatalog())); + .asCatalog(serviceIdentityRegistry))); IcebergCatalog catalog = newIcebergCatalog(catalogWithoutStorage); catalog.initialize( @@ -1361,7 +1361,7 @@ public void testUpdateNotificationCreateTableWithHttpPrefix() { .setDefaultBaseLocation("http://maliciousdomain.com") .setName(catalogName) .build() - .asCatalog())); + .asCatalog(serviceIdentityRegistry))); IcebergCatalog catalog = newIcebergCatalog(catalogName); catalog.initialize( @@ -1879,7 +1879,7 @@ public void testDropTableWithPurgeDisabled() { .setStorageConfigurationInfo( realmConfig, noPurgeStorageConfigModel, storageLocation) .build() - .asCatalog())); + .asCatalog(serviceIdentityRegistry))); IcebergCatalog noPurgeCatalog = newIcebergCatalog(noPurgeCatalogName, metaStoreManager, fileIOFactory); noPurgeCatalog.initialize( @@ -2137,7 +2137,7 @@ public void createCatalogWithReservedProperty() { .setName("createCatalogWithReservedProperty") .setProperties(ImmutableMap.of("polaris.reserved", "true")) .build() - .asCatalog())); + .asCatalog(serviceIdentityRegistry))); }) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("reserved prefix"); @@ -2152,7 +2152,7 @@ public void updateCatalogWithReservedProperty() { .setName("updateCatalogWithReservedProperty") .setProperties(ImmutableMap.of("a", "b")) .build() - .asCatalog())); + .asCatalog(serviceIdentityRegistry))); Assertions.assertThatCode( () -> { adminService.updateCatalog( diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogViewTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogViewTest.java index 2f430075dd..55e131c08c 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogViewTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/AbstractIcebergCatalogViewTest.java @@ -202,7 +202,7 @@ public void before(TestInfo testInfo) { StorageConfigInfo.StorageTypeEnum.FILE, List.of("file://", "/", "*")), "file://tmp") .build() - .asCatalog())); + .asCatalog(serviceIdentityRegistry))); PolarisPassthroughResolutionView passthroughView = new PolarisPassthroughResolutionView( diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerAuthzTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerAuthzTest.java index d359a95444..f0c92634a1 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerAuthzTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerAuthzTest.java @@ -1710,7 +1710,7 @@ public void testSendNotificationSufficientPrivileges() { .setStorageConfigurationInfo(realmConfig, storageConfigModel, storageLocation) .setCatalogType("EXTERNAL") .build() - .asCatalog())); + .asCatalog(serviceIdentityRegistry))); adminService.createCatalogRole( externalCatalog, new CatalogRoleEntity.Builder().setName(CATALOG_ROLE1).build()); adminService.createCatalogRole( diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java index acb5b16a55..6627a55f20 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/AbstractPolicyCatalogTest.java @@ -224,7 +224,7 @@ public void before(TestInfo testInfo) { "true") .setStorageConfigurationInfo(realmConfig, storageConfigModel, storageLocation) .build() - .asCatalog())); + .asCatalog(serviceIdentityRegistry))); PolarisPassthroughResolutionView passthroughView = new PolarisPassthroughResolutionView( diff --git a/runtime/service/src/test/java/org/apache/polaris/service/entity/CatalogEntityTest.java b/runtime/service/src/test/java/org/apache/polaris/service/entity/CatalogEntityTest.java index fceeaa54e7..52d14cd4c8 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/entity/CatalogEntityTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/entity/CatalogEntityTest.java @@ -23,19 +23,30 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.List; +import java.util.Optional; import java.util.stream.Stream; +import org.apache.polaris.core.admin.model.AuthenticationParameters; +import org.apache.polaris.core.admin.model.AwsIamServiceIdentityInfo; import org.apache.polaris.core.admin.model.AwsStorageConfigInfo; import org.apache.polaris.core.admin.model.AzureStorageConfigInfo; import org.apache.polaris.core.admin.model.Catalog; import org.apache.polaris.core.admin.model.CatalogProperties; +import org.apache.polaris.core.admin.model.ConnectionConfigInfo; +import org.apache.polaris.core.admin.model.ExternalCatalog; import org.apache.polaris.core.admin.model.GcpStorageConfigInfo; +import org.apache.polaris.core.admin.model.IcebergRestConnectionConfigInfo; import org.apache.polaris.core.admin.model.PolarisCatalog; +import org.apache.polaris.core.admin.model.ServiceIdentityInfo; +import org.apache.polaris.core.admin.model.SigV4AuthenticationParameters; import org.apache.polaris.core.admin.model.StorageConfigInfo; import org.apache.polaris.core.config.PolarisConfigurationStore; import org.apache.polaris.core.config.RealmConfig; import org.apache.polaris.core.config.RealmConfigImpl; import org.apache.polaris.core.context.RealmContext; import org.apache.polaris.core.entity.CatalogEntity; +import org.apache.polaris.core.identity.dpo.AwsIamServiceIdentityInfoDpo; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; +import org.apache.polaris.core.identity.resolved.ResolvedAwsIamServiceIdentity; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -43,16 +54,23 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mockito; public class CatalogEntityTest { private static final ObjectMapper MAPPER = new ObjectMapper(); private RealmConfig realmConfig; + private ServiceIdentityRegistry serviceIdentityRegistry; @BeforeEach public void setup() { RealmContext realmContext = () -> "realm"; this.realmConfig = new RealmConfigImpl(new PolarisConfigurationStore() {}, realmContext); + this.serviceIdentityRegistry = Mockito.mock(ServiceIdentityRegistry.class); + Mockito.when(serviceIdentityRegistry.resolveServiceIdentity(Mockito.any())) + .thenReturn( + Optional.of( + new ResolvedAwsIamServiceIdentity("arn:aws:iam::123456789012:user/test-user"))); } @ParameterizedTest @@ -278,7 +296,7 @@ public void testCatalogTypeDefaultsToInternal() { .setStorageConfigurationInfo(realmConfig, storageConfigModel, baseLocation) .build(); - Catalog catalog = catalogEntity.asCatalog(); + Catalog catalog = catalogEntity.asCatalog(serviceIdentityRegistry); assertThat(catalog.getType()).isEqualTo(Catalog.TypeEnum.INTERNAL); } @@ -301,7 +319,7 @@ public void testCatalogTypeExternalPreserved() { .setStorageConfigurationInfo(realmConfig, storageConfigModel, baseLocation) .build(); - Catalog catalog = catalogEntity.asCatalog(); + Catalog catalog = catalogEntity.asCatalog(serviceIdentityRegistry); assertThat(catalog.getType()).isEqualTo(Catalog.TypeEnum.EXTERNAL); } @@ -324,7 +342,7 @@ public void testCatalogTypeInternalExplicitlySet() { .setStorageConfigurationInfo(realmConfig, storageConfigModel, baseLocation) .build(); - Catalog catalog = catalogEntity.asCatalog(); + Catalog catalog = catalogEntity.asCatalog(serviceIdentityRegistry); assertThat(catalog.getType()).isEqualTo(Catalog.TypeEnum.INTERNAL); } @@ -362,11 +380,70 @@ public void testAwsConfigRoundTrip(AwsStorageConfigInfo config) throws JsonProce config.getAllowedLocations().getFirst()) .build(); - Catalog catalog = catalogEntity.asCatalog(); + Catalog catalog = catalogEntity.asCatalog(serviceIdentityRegistry); assertThat(catalog.getStorageConfigInfo()).isEqualTo(config); assertThat(MAPPER.writeValueAsString(catalog.getStorageConfigInfo())).isEqualTo(configStr); } + @Test + public void testServiceIdentityInjection() { + String baseLocation = "s3://test-bucket/path"; + AwsStorageConfigInfo storageConfigModel = + AwsStorageConfigInfo.builder() + .setRoleArn("arn:aws:iam::012345678901:role/test-role") + .setExternalId("externalId") + .setUserArn("aws::a:user:arn") + .setStorageType(StorageConfigInfo.StorageTypeEnum.S3) + .setAllowedLocations(List.of(baseLocation)) + .build(); + IcebergRestConnectionConfigInfo icebergRestConnectionConfigInfoModel = + IcebergRestConnectionConfigInfo.builder() + .setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.ICEBERG_REST) + .setUri("https://glue.us-west-2.amazonaws.com") + .setAuthenticationParameters( + SigV4AuthenticationParameters.builder() + .setAuthenticationType(AuthenticationParameters.AuthenticationTypeEnum.SIGV4) + .setRoleArn("arn:aws:iam::123456789012:role/test-role") + .setSigningName("glue") + .setSigningRegion("us-west-2") + .build()) + .build(); + CatalogEntity catalogEntity = + new CatalogEntity.Builder() + .setName("test-catalog") + .setCatalogType(Catalog.TypeEnum.EXTERNAL.name()) + .setDefaultBaseLocation(baseLocation) + .setStorageConfigurationInfo(realmConfig, storageConfigModel, baseLocation) + .setConnectionConfigInfoDpoWithSecrets( + icebergRestConnectionConfigInfoModel, null, new AwsIamServiceIdentityInfoDpo(null)) + .build(); + + Catalog catalog = catalogEntity.asCatalog(serviceIdentityRegistry); + assertThat(catalog.getType()).isEqualTo(Catalog.TypeEnum.EXTERNAL); + ExternalCatalog externalCatalog = (ExternalCatalog) catalog; + assertThat(externalCatalog.getConnectionConfigInfo().getConnectionType()) + .isEqualTo(ConnectionConfigInfo.ConnectionTypeEnum.ICEBERG_REST); + assertThat(externalCatalog.getConnectionConfigInfo().getUri()) + .isEqualTo("https://glue.us-west-2.amazonaws.com"); + + AuthenticationParameters authParams = + externalCatalog.getConnectionConfigInfo().getAuthenticationParameters(); + assertThat(authParams.getAuthenticationType()) + .isEqualTo(AuthenticationParameters.AuthenticationTypeEnum.SIGV4); + SigV4AuthenticationParameters sigV4AuthParams = (SigV4AuthenticationParameters) authParams; + assertThat(sigV4AuthParams.getSigningName()).isEqualTo("glue"); + assertThat(sigV4AuthParams.getSigningRegion()).isEqualTo("us-west-2"); + assertThat(sigV4AuthParams.getRoleArn()).isEqualTo("arn:aws:iam::123456789012:role/test-role"); + + ServiceIdentityInfo serviceIdentity = + externalCatalog.getConnectionConfigInfo().getServiceIdentity(); + assertThat(serviceIdentity.getIdentityType()) + .isEqualTo(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM); + AwsIamServiceIdentityInfo awsIamServiceIdentity = (AwsIamServiceIdentityInfo) serviceIdentity; + assertThat(awsIamServiceIdentity.getIamArn()) + .isEqualTo("arn:aws:iam::123456789012:user/test-user"); + } + public static Stream testAwsConfigRoundTrip() { AwsStorageConfigInfo.Builder b = AwsStorageConfigInfo.builder() diff --git a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java index d56ef59756..c1d8cae44c 100644 --- a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java +++ b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java @@ -297,7 +297,11 @@ public String getAuthenticationScheme() { PolarisCatalogsApi catalogsApi = new PolarisCatalogsApi( new PolarisServiceImpl( - realmConfig, reservedProperties, polarisEventListener, adminService)); + realmConfig, + reservedProperties, + polarisEventListener, + adminService, + serviceIdentityRegistry)); return new TestServices( clock, From 46f9690a2ef793856305c8e8b2efca745731a7a5 Mon Sep 17 00:00:00 2001 From: Rulin Xing Date: Mon, 22 Sep 2025 00:39:31 -0700 Subject: [PATCH 4/7] Use AwsCredentialsProvider to retrieve the credentials --- .../ResolvedAwsIamServiceIdentity.java | 80 ++++++------------- .../resolved/ResolvedServiceIdentity.java | 6 +- .../AwsIamServiceIdentityConfiguration.java | 32 ++++++-- .../ServiceIdentityRegistryConfiguration.java | 35 -------- 4 files changed, 56 insertions(+), 97 deletions(-) delete mode 100644 runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityRegistryConfiguration.java diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedAwsIamServiceIdentity.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedAwsIamServiceIdentity.java index 99e62ecf7d..503455bd0d 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedAwsIamServiceIdentity.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedAwsIamServiceIdentity.java @@ -21,90 +21,73 @@ import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import org.apache.polaris.core.admin.model.AwsIamServiceIdentityInfo; import org.apache.polaris.core.admin.model.ServiceIdentityInfo; import org.apache.polaris.core.identity.ServiceIdentityType; import org.apache.polaris.core.identity.dpo.AwsIamServiceIdentityInfoDpo; import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; import org.apache.polaris.core.secrets.ServiceSecretReference; -import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; -import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; -import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.services.sts.StsClient; import software.amazon.awssdk.services.sts.StsClientBuilder; /** * Represents a fully resolved AWS IAM service identity, including the associated IAM ARN and - * credentials. This class is used internally by Polaris to access AWS services on behalf of a - * configured service identity. + * credentials. Polaris uses this class internally to access AWS services on behalf of a configured + * service identity. * *

It contains AWS credentials (access key, secret, and optional session token) and provides a * lazily initialized {@link StsClient} for performing role assumptions or identity verification. * *

The resolved identity can be converted back into its persisted DPO form using {@link * #asServiceIdentityInfoDpo()}. + * + *

The resolved identity can also be converted into its API model representation using {@link + * #asServiceIdentityInfoModel()} */ public class ResolvedAwsIamServiceIdentity extends ResolvedServiceIdentity { /** IAM role or user ARN representing the Polaris service identity. */ private final String iamArn; - /** AWS access key ID of the AWS credential associated with the identity. */ - private final String accessKeyId; - - /** AWS secret access key of the AWS credential associated with the identity. */ - private final String secretAccessKey; + /** AWS credentials provider for accessing AWS services. */ + private final AwsCredentialsProvider awsCredentialsProvider; - /** The AWS session token of the AWS credential associated with the identity. */ - private final String sessionToken; - - public ResolvedAwsIamServiceIdentity(String iamArn) { - this(null, iamArn, null, null, null); + public ResolvedAwsIamServiceIdentity(@Nullable String iamArn) { + this(null, iamArn, DefaultCredentialsProvider.builder().build()); } public ResolvedAwsIamServiceIdentity( - String iamArn, String accessKeyId, String secretAccessKey, String sessionToken) { - this(null, iamArn, accessKeyId, secretAccessKey, sessionToken); + @Nullable String iamArn, @Nonnull AwsCredentialsProvider awsCredentialsProvider) { + this(null, iamArn, awsCredentialsProvider); } public ResolvedAwsIamServiceIdentity( - ServiceSecretReference serviceSecretReference, - String iamArn, - String accessKeyId, - String secretAccessKey, - String sessionToken) { + @Nullable ServiceSecretReference serviceSecretReference, + @Nullable String iamArn, + @Nonnull AwsCredentialsProvider awsCredentialsProvider) { super(ServiceIdentityType.AWS_IAM, serviceSecretReference); this.iamArn = iamArn; - this.accessKeyId = accessKeyId; - this.secretAccessKey = secretAccessKey; - this.sessionToken = sessionToken; + this.awsCredentialsProvider = awsCredentialsProvider; } - public String getIamArn() { + public @Nullable String getIamArn() { return iamArn; } - public String getAccessKeyId() { - return accessKeyId; - } - - public String getSecretAccessKey() { - return secretAccessKey; - } - - public String getSessionToken() { - return sessionToken; + public @Nonnull AwsCredentialsProvider getAwsCredentialsProvider() { + return awsCredentialsProvider; } - @Nonnull @Override - public ServiceIdentityInfoDpo asServiceIdentityInfoDpo() { + public @Nonnull ServiceIdentityInfoDpo asServiceIdentityInfoDpo() { return new AwsIamServiceIdentityInfoDpo(getIdentityInfoReference()); } - @Nonnull @Override - public ServiceIdentityInfo asServiceIdentityInfoModel() { + public @Nonnull ServiceIdentityInfo asServiceIdentityInfoModel() { return AwsIamServiceIdentityInfo.builder() .setIdentityType(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM) .setIamArn(getIamArn()) @@ -112,22 +95,11 @@ public ServiceIdentityInfo asServiceIdentityInfoModel() { } /** Returns a memoized supplier for creating an STS client using the resolved credentials. */ - public Supplier stsClientSupplier() { + public @Nonnull Supplier stsClientSupplier() { return Suppliers.memoize( () -> { - StsClientBuilder stsClientBuilder = StsClient.builder(); - if (getAccessKeyId() != null && getSecretAccessKey() != null) { - StaticCredentialsProvider awsCredentialsProvider = - StaticCredentialsProvider.create( - AwsBasicCredentials.create(getAccessKeyId(), getSecretAccessKey())); - if (getSessionToken() != null) { - awsCredentialsProvider = - StaticCredentialsProvider.create( - AwsSessionCredentials.create( - getAccessKeyId(), getSecretAccessKey(), getSessionToken())); - } - stsClientBuilder.credentialsProvider(awsCredentialsProvider); - } + StsClientBuilder stsClientBuilder = + StsClient.builder().credentialsProvider(getAwsCredentialsProvider()); return stsClientBuilder.build(); }); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedServiceIdentity.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedServiceIdentity.java index d2272d69a7..342d75b20f 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedServiceIdentity.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedServiceIdentity.java @@ -19,6 +19,7 @@ package org.apache.polaris.core.identity.resolved; import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import org.apache.polaris.core.admin.model.ServiceIdentityInfo; import org.apache.polaris.core.identity.ServiceIdentityType; import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; @@ -36,12 +37,13 @@ public abstract class ResolvedServiceIdentity { private final ServiceIdentityType identityType; private ServiceSecretReference identityInfoReference; - public ResolvedServiceIdentity(ServiceIdentityType identityType) { + public ResolvedServiceIdentity(@Nonnull ServiceIdentityType identityType) { this(identityType, null); } public ResolvedServiceIdentity( - ServiceIdentityType identityType, ServiceSecretReference identityInfoReference) { + @Nonnull ServiceIdentityType identityType, + @Nullable ServiceSecretReference identityInfoReference) { this.identityType = identityType; this.identityInfoReference = identityInfoReference; } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/AwsIamServiceIdentityConfiguration.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/AwsIamServiceIdentityConfiguration.java index d69459f492..f6f41bf1e7 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/identity/AwsIamServiceIdentityConfiguration.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/AwsIamServiceIdentityConfiguration.java @@ -19,8 +19,10 @@ package org.apache.polaris.service.identity; +import jakarta.annotation.Nonnull; import java.util.Optional; import org.apache.polaris.core.identity.resolved.ResolvedAwsIamServiceIdentity; +import software.amazon.awssdk.auth.credentials.*; /** * Configuration for an AWS IAM service identity used by Polaris to access AWS services. @@ -63,12 +65,30 @@ default Optional resolve() { if (iamArn() == null) { return Optional.empty(); } else { - return Optional.of( - new ResolvedAwsIamServiceIdentity( - iamArn(), - accessKeyId().orElse(null), - secretAccessKey().orElse(null), - sessionToken().orElse(null))); + return Optional.of(new ResolvedAwsIamServiceIdentity(iamArn(), awsCredentialsProvider())); + } + } + + /** + * Constructs an {@link AwsCredentialsProvider} based on the configured access key, secret key, + * and session token. If the access key and secret key are provided, a static credentials provider + * is created; otherwise, the default credentials provider chain is used. + * + * @return the constructed AWS credentials provider + */ + @Nonnull + default AwsCredentialsProvider awsCredentialsProvider() { + if (accessKeyId().isPresent() && secretAccessKey().isPresent()) { + if (sessionToken().isPresent()) { + return StaticCredentialsProvider.create( + AwsSessionCredentials.create( + accessKeyId().get(), secretAccessKey().get(), sessionToken().get())); + } else { + return StaticCredentialsProvider.create( + AwsBasicCredentials.create(accessKeyId().get(), secretAccessKey().get())); + } + } else { + return DefaultCredentialsProvider.builder().build(); } } } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityRegistryConfiguration.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityRegistryConfiguration.java deleted file mode 100644 index 9c063000da..0000000000 --- a/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityRegistryConfiguration.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.polaris.service.identity; - -import io.quarkus.runtime.annotations.StaticInitSafe; -import io.smallrye.config.ConfigMapping; -import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; - -@StaticInitSafe -@ConfigMapping(prefix = "polaris.service-identity.registry") -public interface ServiceIdentityRegistryConfiguration { - - /** - * The type of the ServiceIdentityRegistry to use. This is the {@link ServiceIdentityRegistry} - * identifier. - */ - String type(); -} From 0e7be85b319d19366e4e83676d5b9d1139bef57c Mon Sep 17 00:00:00 2001 From: Rulin Xing Date: Mon, 22 Sep 2025 09:16:36 -0700 Subject: [PATCH 5/7] Move some logic to ServiceIdentityConfiguration --- .../registry/ServiceIdentityRegistry.java | 2 +- .../src/main/resources/application.properties | 4 +- .../service/admin/PolarisAdminService.java | 20 ++++++- .../AwsIamServiceIdentityConfiguration.java | 17 +++++- ...esolvableServiceIdentityConfiguration.java | 5 +- .../ServiceIdentityConfiguration.java | 57 ++++++++----------- .../DefaultServiceIdentityRegistry.java | 26 ++------- .../DefaultServiceIdentityRegistryTest.java | 49 +++++++++++----- 8 files changed, 103 insertions(+), 77 deletions(-) diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistry.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistry.java index f4b6b781e7..75f23505c2 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistry.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistry.java @@ -44,7 +44,7 @@ public interface ServiceIdentityRegistry { * @param serviceIdentityType The type of service identity (e.g., AWS_IAM). * @return A new {@link ServiceIdentityInfoDpo} representing the discovered service identity. */ - ServiceIdentityInfoDpo discoverServiceIdentity(ServiceIdentityType serviceIdentityType); + Optional discoverServiceIdentity(ServiceIdentityType serviceIdentityType); /** * Resolves the given service identity by retrieving the actual credential or secret referenced by diff --git a/runtime/defaults/src/main/resources/application.properties b/runtime/defaults/src/main/resources/application.properties index 359207a521..cd63ae33e0 100644 --- a/runtime/defaults/src/main/resources/application.properties +++ b/runtime/defaults/src/main/resources/application.properties @@ -113,7 +113,7 @@ polaris.realm-context.require-header=false polaris.features."ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING"=false polaris.features."SUPPORTED_CATALOG_STORAGE_TYPES"=["S3","GCS","AZURE"] -polaris.features."ENABLE_CATALOG_FEDERATION"=true +# polaris.features."ENABLE_CATALOG_FEDERATION"=true polaris.features."SUPPORTED_CATALOG_CONNECTION_TYPES"=["ICEBERG_REST"] # realm overrides @@ -200,7 +200,7 @@ polaris.oidc.principal-roles-mapper.type=default # Polaris Service Identity Config # Default identity (can be overridden in per realm) -polaris.service-identity.aws-iam.iam-arn=arn:aws:iam::123456789012:user/polaris-iam-user +# polaris.service-identity.aws-iam.iam-arn=arn:aws:iam::123456789012:user/polaris-iam-user # polaris.service-identity.aws-iam.access-key-id=accessKeyId # polaris.service-identity.aws-iam.secret-access-key=secretAccessKey # polaris.service-identity.aws-iam.session-token=sessionToken diff --git a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java index dce34625f2..0488500d6c 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java @@ -784,17 +784,31 @@ public PolarisEntity createCatalog(CreateCatalogRequest catalogRequest) { "Implicit authentication based catalog federation is not supported."); } - ServiceIdentityInfoDpo serviceIdentityInfo = null; + // Discover service identity if needed for the authentication type. + Optional serviceIdentityInfoDpoOptional = Optional.empty(); if (connectionConfigInfo.getAuthenticationParameters().getAuthenticationType() == AuthenticationParameters.AuthenticationTypeEnum.SIGV4) { - serviceIdentityInfo = + serviceIdentityInfoDpoOptional = serviceIdentityRegistry.discoverServiceIdentity(ServiceIdentityType.AWS_IAM); + if (serviceIdentityInfoDpoOptional.isEmpty()) { + throw new IllegalStateException( + String.format( + "Cannot create Catalog %s. Failed to discover %s service identity for %s authentication", + entity.getName(), + ServiceIdentityType.AWS_IAM.name(), + connectionConfigInfo + .getAuthenticationParameters() + .getAuthenticationType() + .name())); + } } entity = new CatalogEntity.Builder(entity) .setConnectionConfigInfoDpoWithSecrets( - connectionConfigInfo, processedSecretReferences, serviceIdentityInfo) + connectionConfigInfo, + processedSecretReferences, + serviceIdentityInfoDpoOptional.orElse(null)) .build(); } } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/AwsIamServiceIdentityConfiguration.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/AwsIamServiceIdentityConfiguration.java index f6f41bf1e7..2182a93bcc 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/identity/AwsIamServiceIdentityConfiguration.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/AwsIamServiceIdentityConfiguration.java @@ -21,8 +21,14 @@ import jakarta.annotation.Nonnull; import java.util.Optional; +import org.apache.polaris.core.identity.ServiceIdentityType; import org.apache.polaris.core.identity.resolved.ResolvedAwsIamServiceIdentity; -import software.amazon.awssdk.auth.credentials.*; +import org.apache.polaris.service.identity.registry.DefaultServiceIdentityRegistry; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; /** * Configuration for an AWS IAM service identity used by Polaris to access AWS services. @@ -61,11 +67,16 @@ public interface AwsIamServiceIdentityConfiguration extends ResolvableServiceIde * @return the resolved identity, or an empty optional if the ARN is missing */ @Override - default Optional resolve() { + default Optional resolve(@Nonnull String realmIdentifier) { if (iamArn() == null) { return Optional.empty(); } else { - return Optional.of(new ResolvedAwsIamServiceIdentity(iamArn(), awsCredentialsProvider())); + return Optional.of( + new ResolvedAwsIamServiceIdentity( + DefaultServiceIdentityRegistry.buildIdentityInfoReference( + realmIdentifier, ServiceIdentityType.AWS_IAM), + iamArn(), + awsCredentialsProvider())); } } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/ResolvableServiceIdentityConfiguration.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/ResolvableServiceIdentityConfiguration.java index 093d5abb8f..f8b2a956ee 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/identity/ResolvableServiceIdentityConfiguration.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/ResolvableServiceIdentityConfiguration.java @@ -19,6 +19,7 @@ package org.apache.polaris.service.identity; +import jakarta.annotation.Nonnull; import java.util.Optional; import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; @@ -37,5 +38,7 @@ public interface ResolvableServiceIdentityConfiguration { * @return an optional resolved service identity, or empty if resolution fails or is not * configured */ - Optional resolve(); + default Optional resolve(@Nonnull String realmIdentifier) { + return Optional.empty(); + } } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityConfiguration.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityConfiguration.java index 05deaa2eda..5a99230abf 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityConfiguration.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityConfiguration.java @@ -23,8 +23,11 @@ import io.smallrye.config.WithDefaults; import io.smallrye.config.WithParentName; import io.smallrye.config.WithUnnamedKey; +import java.util.List; import java.util.Map; +import java.util.Optional; import org.apache.polaris.core.context.RealmContext; +import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; /** * Represents the service identity configuration for one or more realms. @@ -54,48 +57,38 @@ public interface ServiceIdentityConfiguration { Map realms(); /** - * Returns the service identity configuration for the given {@link RealmContext}. Falls back to - * the default if the realm is not explicitly configured. - * - * @param realmContext the realm context - * @return the matching or default realm configuration + * Resolves the actual realm configuration entry (identifier + config) to use for the given + * context. Falls back to the default if the specified realm is not configured. */ - default RealmServiceIdentityConfiguration forRealm(RealmContext realmContext) { + default RealmConfigEntry forRealm(RealmContext realmContext) { return forRealm(realmContext.getRealmIdentifier()); } /** - * Returns the service identity configuration for the given realm identifier. Falls back to the - * default if the realm is not explicitly configured. - * - * @param realmIdentifier the identifier of the realm - * @return the matching or default realm configuration + * Resolves the actual realm configuration entry (identifier + config) for the given realm + * identifier. Falls back to the default if the specified realm is not configured. */ - default RealmServiceIdentityConfiguration forRealm(String realmIdentifier) { - return realms().containsKey(realmIdentifier) - ? realms().get(realmIdentifier) - : realms().get(DEFAULT_REALM_KEY); + default RealmConfigEntry forRealm(String realmIdentifier) { + String resolvedRealmIdentifier = + realms().containsKey(realmIdentifier) ? realmIdentifier : DEFAULT_REALM_KEY; + return new RealmConfigEntry(resolvedRealmIdentifier, realms().get(resolvedRealmIdentifier)); } /** - * Returns the actual key of the service identity configuration to use for the given {@link - * RealmContext}, falling back to the default if the specified realm is not configured. - * - * @param realmContext the realm context - * @return the actual realm identifier to use + * Resolves and returns the list of {@link ResolvedServiceIdentity} objects for the given realm. */ - default String resolveRealm(RealmContext realmContext) { - return resolveRealm(realmContext.getRealmIdentifier()); - } + default List resolveServiceIdentities( + RealmContext realmContext) { + RealmConfigEntry entry = forRealm(realmContext); - /** - * Returns the actual key of the service identity configuration to use for the given realm - * identifier, falling back to the default if the specified realm is not configured. - * - * @param realmIdentifier the identifier of the realm - * @return the actual realm identifier to use - */ - default String resolveRealm(String realmIdentifier) { - return realms().containsKey(realmIdentifier) ? realmIdentifier : DEFAULT_REALM_KEY; + return entry.config().serviceIdentityConfigurations().stream() + .map( + resolvableServiceIdentityConfiguration -> + resolvableServiceIdentityConfiguration.resolve(entry.realm())) + .flatMap(Optional::stream) + .toList(); } + + /** A pairing of a resolved realm identifier and its associated configuration. */ + record RealmConfigEntry(String realm, RealmServiceIdentityConfiguration config) {} } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistry.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistry.java index 0cda3c2109..1fc16479a5 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistry.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistry.java @@ -31,8 +31,6 @@ import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; import org.apache.polaris.core.secrets.ServiceSecretReference; -import org.apache.polaris.service.identity.RealmServiceIdentityConfiguration; -import org.apache.polaris.service.identity.ResolvableServiceIdentityConfiguration; import org.apache.polaris.service.identity.ServiceIdentityConfiguration; /** @@ -80,20 +78,8 @@ public DefaultServiceIdentityRegistry( @Inject public DefaultServiceIdentityRegistry( RealmContext realmContext, ServiceIdentityConfiguration serviceIdentityConfiguration) { - String serviceIdentityConfigKey = serviceIdentityConfiguration.resolveRealm(realmContext); - RealmServiceIdentityConfiguration realmServiceIdentityConfiguration = - serviceIdentityConfiguration.forRealm(realmContext); - this.resolvedServiceIdentities = - realmServiceIdentityConfiguration.serviceIdentityConfigurations().stream() - .map(ResolvableServiceIdentityConfiguration::resolve) - .flatMap(Optional::stream) - .peek( - // Set the identity info reference for each resolved identity - identity -> - identity.setIdentityInfoReference( - buildIdentityInfoReference( - serviceIdentityConfigKey, identity.getIdentityType()))) + serviceIdentityConfiguration.resolveServiceIdentities(realmContext).stream() .collect( // Collect to an EnumMap, grouping by ServiceIdentityType Collectors.toMap( @@ -111,14 +97,14 @@ public DefaultServiceIdentityRegistry( } @Override - public ServiceIdentityInfoDpo discoverServiceIdentity(ServiceIdentityType serviceIdentityType) { + public Optional discoverServiceIdentity( + ServiceIdentityType serviceIdentityType) { ResolvedServiceIdentity resolvedServiceIdentity = resolvedServiceIdentities.get(serviceIdentityType); if (resolvedServiceIdentity == null) { - throw new IllegalArgumentException( - "Service identity type not supported: " + serviceIdentityType); + return Optional.empty(); } - return resolvedServiceIdentity.asServiceIdentityInfoDpo(); + return Optional.of(resolvedServiceIdentity.asServiceIdentityInfoDpo()); } @Override @@ -147,7 +133,7 @@ public EnumMap getResolvedServiceI * @param type the service identity type * @return the constructed service secret reference */ - private ServiceSecretReference buildIdentityInfoReference( + public static ServiceSecretReference buildIdentityInfoReference( String realm, ServiceIdentityType type) { // urn:polaris-service-secret:default-identity-registry:: return new ServiceSecretReference( diff --git a/runtime/service/src/test/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistryTest.java b/runtime/service/src/test/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistryTest.java index a89d33246d..c5553276e6 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistryTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistryTest.java @@ -37,6 +37,9 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; @QuarkusTest @TestProfile(DefaultServiceIdentityRegistryTest.Profile.class) @@ -77,8 +80,10 @@ void testServiceIdentityConfiguration() { .isEqualTo(2); // Check the default realm configuration - RealmServiceIdentityConfiguration defaultConfig = + ServiceIdentityConfiguration.RealmConfigEntry defaultConfigEntry = serviceIdentityConfiguration.forRealm(DEFAULT_REALM_KEY); + Assertions.assertThat(defaultConfigEntry.realm()).isEqualTo(DEFAULT_REALM_KEY); + RealmServiceIdentityConfiguration defaultConfig = defaultConfigEntry.config(); Assertions.assertThat(defaultConfig.awsIamServiceIdentity().isPresent()).isTrue(); Assertions.assertThat(defaultConfig.awsIamServiceIdentity().get().iamArn()) .isEqualTo("arn:aws:iam::123456789012:user/polaris-default-iam-user"); @@ -87,8 +92,10 @@ void testServiceIdentityConfiguration() { Assertions.assertThat(defaultConfig.awsIamServiceIdentity().get().sessionToken()).isEmpty(); // Check the my-realm configuration - RealmServiceIdentityConfiguration myRealmConfig = + ServiceIdentityConfiguration.RealmConfigEntry myRealmConfigEntry = serviceIdentityConfiguration.forRealm(MY_REALM_KEY); + Assertions.assertThat(myRealmConfigEntry.realm()).isEqualTo(MY_REALM_KEY); + RealmServiceIdentityConfiguration myRealmConfig = myRealmConfigEntry.config(); Assertions.assertThat(myRealmConfig.awsIamServiceIdentity().isPresent()).isTrue(); Assertions.assertThat(myRealmConfig.awsIamServiceIdentity().get().iamArn()) .isEqualTo("arn:aws:iam::123456789012:user/polaris-iam-user"); @@ -100,8 +107,10 @@ void testServiceIdentityConfiguration() { .isEqualTo(Optional.of("session-token")); // Check the unexisting realm configuration - RealmServiceIdentityConfiguration otherConfig = + ServiceIdentityConfiguration.RealmConfigEntry otherConfigEntry = serviceIdentityConfiguration.forRealm("other-realm"); + Assertions.assertThat(otherConfigEntry.realm()).isEqualTo(DEFAULT_REALM_KEY); + RealmServiceIdentityConfiguration otherConfig = otherConfigEntry.config(); Assertions.assertThat(otherConfig.awsIamServiceIdentity().isPresent()).isTrue(); Assertions.assertThat(otherConfig.awsIamServiceIdentity().get().iamArn()) .isEqualTo("arn:aws:iam::123456789012:user/polaris-default-iam-user"); @@ -131,9 +140,10 @@ void testRealmServiceIdentityConfigToResolvedServiceIdentity() { .isEqualTo( new ServiceSecretReference( "urn:polaris-secret:default-identity-registry:system:default:AWS_IAM", Map.of())); - Assertions.assertThat(resolvedAwsIamServiceIdentity.getAccessKeyId()).isNull(); - Assertions.assertThat(resolvedAwsIamServiceIdentity.getSecretAccessKey()).isNull(); - Assertions.assertThat(resolvedAwsIamServiceIdentity.getSessionToken()).isNull(); + Assertions.assertThat( + resolvedAwsIamServiceIdentity.getAwsCredentialsProvider() + instanceof DefaultCredentialsProvider) + .isTrue(); // Check the my-realm Mockito.when(realmContext.getRealmIdentifier()).thenReturn(MY_REALM_KEY); @@ -153,12 +163,20 @@ void testRealmServiceIdentityConfigToResolvedServiceIdentity() { .isEqualTo( new ServiceSecretReference( "urn:polaris-secret:default-identity-registry:my-realm:AWS_IAM", Map.of())); - Assertions.assertThat(resolvedAwsIamServiceIdentity.getAccessKeyId()) - .isEqualTo("access-key-id"); - Assertions.assertThat(resolvedAwsIamServiceIdentity.getSecretAccessKey()) - .isEqualTo("secret-access-key"); - Assertions.assertThat(resolvedAwsIamServiceIdentity.getSessionToken()) - .isEqualTo("session-token"); + Assertions.assertThat( + resolvedAwsIamServiceIdentity.getAwsCredentialsProvider() + instanceof StaticCredentialsProvider) + .isTrue(); + StaticCredentialsProvider staticCredentialsProvider = + (StaticCredentialsProvider) resolvedAwsIamServiceIdentity.getAwsCredentialsProvider(); + Assertions.assertThat( + staticCredentialsProvider.resolveCredentials() instanceof AwsSessionCredentials) + .isTrue(); + AwsSessionCredentials awsSessionCredentials = + (AwsSessionCredentials) staticCredentialsProvider.resolveCredentials(); + Assertions.assertThat(awsSessionCredentials.accessKeyId()).isEqualTo("access-key-id"); + Assertions.assertThat(awsSessionCredentials.secretAccessKey()).isEqualTo("secret-access-key"); + Assertions.assertThat(awsSessionCredentials.sessionToken()).isEqualTo("session-token"); // Check the other realm which does not exist in the configuration, should fallback to default Mockito.when(realmContext.getRealmIdentifier()).thenReturn("other-realm"); @@ -177,8 +195,9 @@ void testRealmServiceIdentityConfigToResolvedServiceIdentity() { .isEqualTo( new ServiceSecretReference( "urn:polaris-secret:default-identity-registry:system:default:AWS_IAM", Map.of())); - Assertions.assertThat(resolvedAwsIamServiceIdentity.getAccessKeyId()).isNull(); - Assertions.assertThat(resolvedAwsIamServiceIdentity.getSecretAccessKey()).isNull(); - Assertions.assertThat(resolvedAwsIamServiceIdentity.getSessionToken()).isNull(); + Assertions.assertThat( + resolvedAwsIamServiceIdentity.getAwsCredentialsProvider() + instanceof DefaultCredentialsProvider) + .isTrue(); } } From e38629858270f67cd124a019410209efed142066 Mon Sep 17 00:00:00 2001 From: Rulin Xing Date: Wed, 24 Sep 2025 10:37:06 -0700 Subject: [PATCH 6/7] Resolved more comments --- .../polaris/core/entity/CatalogEntity.java | 10 +++++++++ .../ResolvedAwsIamServiceIdentity.java | 13 ------------ .../AwsIamServiceIdentityConfiguration.java | 21 +++++++++++++------ ...esolvableServiceIdentityConfiguration.java | 14 ++++++++++++- .../ServiceIdentityConfiguration.java | 5 ++++- 5 files changed, 42 insertions(+), 21 deletions(-) diff --git a/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java b/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java index 368724b47f..ee1661e58f 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/entity/CatalogEntity.java @@ -109,6 +109,10 @@ public static CatalogEntity fromCatalog(RealmConfig realmConfig, Catalog catalog return builder.build(); } + public Catalog asCatalog() { + return this.asCatalog(null); + } + public Catalog asCatalog(ServiceIdentityRegistry serviceIdentityRegistry) { Map internalProperties = getInternalPropertiesAsMap(); Catalog.TypeEnum catalogType = @@ -120,6 +124,12 @@ public Catalog asCatalog(ServiceIdentityRegistry serviceIdentityRegistry) { CatalogProperties.builder(propertiesMap.get(DEFAULT_BASE_LOCATION_KEY)) .putAll(propertiesMap) .build(); + + // Right now, only external catalog may use ServiceIdentityRegistry to resolve identity + Preconditions.checkState( + catalogType != Catalog.TypeEnum.EXTERNAL || serviceIdentityRegistry != null, + "%s catalog needs ServiceIdentityRegistry to resolve service identities", + Catalog.TypeEnum.EXTERNAL); return catalogType == Catalog.TypeEnum.EXTERNAL ? ExternalCatalog.builder() .setType(Catalog.TypeEnum.EXTERNAL) diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedAwsIamServiceIdentity.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedAwsIamServiceIdentity.java index 503455bd0d..4abd6e12ea 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedAwsIamServiceIdentity.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedAwsIamServiceIdentity.java @@ -18,8 +18,6 @@ */ package org.apache.polaris.core.identity.resolved; -import com.google.common.base.Supplier; -import com.google.common.base.Suppliers; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import org.apache.polaris.core.admin.model.AwsIamServiceIdentityInfo; @@ -31,7 +29,6 @@ import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.services.sts.StsClient; -import software.amazon.awssdk.services.sts.StsClientBuilder; /** * Represents a fully resolved AWS IAM service identity, including the associated IAM ARN and @@ -93,14 +90,4 @@ public ResolvedAwsIamServiceIdentity( .setIamArn(getIamArn()) .build(); } - - /** Returns a memoized supplier for creating an STS client using the resolved credentials. */ - public @Nonnull Supplier stsClientSupplier() { - return Suppliers.memoize( - () -> { - StsClientBuilder stsClientBuilder = - StsClient.builder().credentialsProvider(getAwsCredentialsProvider()); - return stsClientBuilder.build(); - }); - } } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/AwsIamServiceIdentityConfiguration.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/AwsIamServiceIdentityConfiguration.java index 2182a93bcc..8116f58ef7 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/identity/AwsIamServiceIdentityConfiguration.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/AwsIamServiceIdentityConfiguration.java @@ -23,7 +23,7 @@ import java.util.Optional; import org.apache.polaris.core.identity.ServiceIdentityType; import org.apache.polaris.core.identity.resolved.ResolvedAwsIamServiceIdentity; -import org.apache.polaris.service.identity.registry.DefaultServiceIdentityRegistry; +import org.apache.polaris.core.secrets.ServiceSecretReference; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; @@ -60,6 +60,17 @@ public interface AwsIamServiceIdentityConfiguration extends ResolvableServiceIde */ Optional sessionToken(); + /** + * Returns the type of service identity represented by this configuration, which is always {@link + * ServiceIdentityType#AWS_IAM}. + * + * @return the AWS IAM service identity type + */ + @Override + default ServiceIdentityType getType() { + return ServiceIdentityType.AWS_IAM; + } + /** * Resolves this configuration into a {@link ResolvedAwsIamServiceIdentity} if the IAM ARN is * present. @@ -67,16 +78,14 @@ public interface AwsIamServiceIdentityConfiguration extends ResolvableServiceIde * @return the resolved identity, or an empty optional if the ARN is missing */ @Override - default Optional resolve(@Nonnull String realmIdentifier) { + default Optional resolve( + @Nonnull ServiceSecretReference serviceIdentityReference) { if (iamArn() == null) { return Optional.empty(); } else { return Optional.of( new ResolvedAwsIamServiceIdentity( - DefaultServiceIdentityRegistry.buildIdentityInfoReference( - realmIdentifier, ServiceIdentityType.AWS_IAM), - iamArn(), - awsCredentialsProvider())); + serviceIdentityReference, iamArn(), awsCredentialsProvider())); } } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/ResolvableServiceIdentityConfiguration.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/ResolvableServiceIdentityConfiguration.java index f8b2a956ee..dd3ef9ce3b 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/identity/ResolvableServiceIdentityConfiguration.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/ResolvableServiceIdentityConfiguration.java @@ -21,7 +21,9 @@ import jakarta.annotation.Nonnull; import java.util.Optional; +import org.apache.polaris.core.identity.ServiceIdentityType; import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; +import org.apache.polaris.core.secrets.ServiceSecretReference; /** * Represents a service identity configuration that can be resolved into a fully initialized {@link @@ -32,13 +34,23 @@ * Polaris-managed service identity. */ public interface ResolvableServiceIdentityConfiguration { + /** + * Returns the type of service identity represented by this configuration. + * + * @return the service identity type, or {@link ServiceIdentityType#NULL_TYPE} if not specified + */ + default ServiceIdentityType getType() { + return ServiceIdentityType.NULL_TYPE; + } + /** * Attempts to resolve this configuration into a {@link ResolvedServiceIdentity}. * * @return an optional resolved service identity, or empty if resolution fails or is not * configured */ - default Optional resolve(@Nonnull String realmIdentifier) { + default Optional resolve( + @Nonnull ServiceSecretReference serviceIdentityReference) { return Optional.empty(); } } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityConfiguration.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityConfiguration.java index 5a99230abf..ca002aba5f 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityConfiguration.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityConfiguration.java @@ -28,6 +28,7 @@ import java.util.Optional; import org.apache.polaris.core.context.RealmContext; import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; +import org.apache.polaris.service.identity.registry.DefaultServiceIdentityRegistry; /** * Represents the service identity configuration for one or more realms. @@ -84,7 +85,9 @@ default List resolveServiceIdentities( return entry.config().serviceIdentityConfigurations().stream() .map( resolvableServiceIdentityConfiguration -> - resolvableServiceIdentityConfiguration.resolve(entry.realm())) + resolvableServiceIdentityConfiguration.resolve( + DefaultServiceIdentityRegistry.buildIdentityInfoReference( + entry.realm(), resolvableServiceIdentityConfiguration.getType()))) .flatMap(Optional::stream) .toList(); } From 180c283d0ecd3dbfcac9399749e04c93207f29ce Mon Sep 17 00:00:00 2001 From: Rulin Xing Date: Mon, 8 Sep 2025 00:37:15 -0700 Subject: [PATCH 7/7] Connection Credential Retriaval --- .../hadoop/HadoopFederatedCatalogFactory.java | 9 +- .../hive/HiveFederatedCatalogFactory.java | 9 +- .../core/catalog/ExternalCatalogFactory.java | 6 +- .../BearerAuthenticationParametersDpo.java | 3 +- .../ImplicitAuthenticationParametersDpo.java | 7 +- .../OAuthClientCredentialsParametersDpo.java | 3 +- .../SigV4AuthenticationParametersDpo.java | 11 +- .../hadoop/HadoopConnectionConfigInfoDpo.java | 7 +- .../hive/HiveConnectionConfigInfoDpo.java | 7 +- .../IcebergCatalogPropertiesProvider.java | 4 +- .../IcebergRestConnectionConfigInfoDpo.java | 11 +- .../DefaultPolarisCredentialManager.java | 117 ++++++++++++++ .../credentials/PolarisCredentialManager.java | 54 +++++++ .../PolarisCredentialManagerFactory.java | 34 +++++ .../ConnectionCredentialProperty.java | 62 ++++++++ .../PolarisConnectionCredsVendor.java | 57 +++++++ .../identity/ResolvedServiceIdentityTest.java | 60 ++++++++ .../src/main/resources/application.properties | 4 + .../catalog/common/CatalogHandler.java | 8 + .../generic/GenericTableCatalogAdapter.java | 5 + .../generic/GenericTableCatalogHandler.java | 3 + .../iceberg/IcebergCatalogAdapter.java | 5 + .../iceberg/IcebergCatalogHandler.java | 8 +- .../IcebergRESTExternalCatalogFactory.java | 7 +- .../catalog/policy/PolicyCatalogAdapter.java | 5 + .../catalog/policy/PolicyCatalogHandler.java | 3 + .../service/config/ServiceProducers.java | 20 +++ ...efaultPolarisCredentialManagerFactory.java | 49 ++++++ ...PolarisCredentialManagerConfiguration.java | 41 +++++ .../service/admin/PolarisAuthzTestBase.java | 7 + ...isGenericTableCatalogHandlerAuthzTest.java | 1 + .../IcebergCatalogHandlerAuthzTest.java | 3 + .../policy/PolicyCatalogHandlerAuthzTest.java | 1 + .../DefaultPolarisCredentialManagerTest.java | 144 ++++++++++++++++++ .../apache/polaris/service/TestServices.java | 9 ++ 35 files changed, 763 insertions(+), 21 deletions(-) create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/credentials/DefaultPolarisCredentialManager.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/credentials/PolarisCredentialManager.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/credentials/PolarisCredentialManagerFactory.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/credentials/connection/ConnectionCredentialProperty.java create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/credentials/connection/PolarisConnectionCredsVendor.java create mode 100644 polaris-core/src/test/java/org/apache/polaris/core/identity/ResolvedServiceIdentityTest.java create mode 100644 runtime/service/src/main/java/org/apache/polaris/service/credentials/DefaultPolarisCredentialManagerFactory.java create mode 100644 runtime/service/src/main/java/org/apache/polaris/service/credentials/PolarisCredentialManagerConfiguration.java create mode 100644 runtime/service/src/test/java/org/apache/polaris/service/credentials/DefaultPolarisCredentialManagerTest.java diff --git a/extensions/federation/hadoop/src/main/java/org/apache/polaris/extensions/federation/hadoop/HadoopFederatedCatalogFactory.java b/extensions/federation/hadoop/src/main/java/org/apache/polaris/extensions/federation/hadoop/HadoopFederatedCatalogFactory.java index 8da714072d..95bd26f9d9 100644 --- a/extensions/federation/hadoop/src/main/java/org/apache/polaris/extensions/federation/hadoop/HadoopFederatedCatalogFactory.java +++ b/extensions/federation/hadoop/src/main/java/org/apache/polaris/extensions/federation/hadoop/HadoopFederatedCatalogFactory.java @@ -30,6 +30,7 @@ import org.apache.polaris.core.connection.ConnectionConfigInfoDpo; import org.apache.polaris.core.connection.ConnectionType; import org.apache.polaris.core.connection.hadoop.HadoopConnectionConfigInfoDpo; +import org.apache.polaris.core.credentials.PolarisCredentialManager; import org.apache.polaris.core.secrets.UserSecretsManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,7 +43,9 @@ public class HadoopFederatedCatalogFactory implements ExternalCatalogFactory { @Override public Catalog createCatalog( - ConnectionConfigInfoDpo connectionConfigInfoDpo, UserSecretsManager userSecretsManager) { + ConnectionConfigInfoDpo connectionConfigInfoDpo, + UserSecretsManager userSecretsManager, + PolarisCredentialManager polarisCredentialManager) { // Currently, Polaris supports Hadoop federation only via IMPLICIT authentication. // Hence, prior to initializing the configuration, ensure that the catalog uses // IMPLICIT authentication. @@ -56,7 +59,9 @@ public Catalog createCatalog( String warehouse = ((HadoopConnectionConfigInfoDpo) connectionConfigInfoDpo).getWarehouse(); HadoopCatalog hadoopCatalog = new HadoopCatalog(conf, warehouse); hadoopCatalog.initialize( - warehouse, connectionConfigInfoDpo.asIcebergCatalogProperties(userSecretsManager)); + warehouse, + connectionConfigInfoDpo.asIcebergCatalogProperties( + userSecretsManager, polarisCredentialManager)); return hadoopCatalog; } diff --git a/extensions/federation/hive/src/main/java/org/apache/polaris/extensions/federation/hive/HiveFederatedCatalogFactory.java b/extensions/federation/hive/src/main/java/org/apache/polaris/extensions/federation/hive/HiveFederatedCatalogFactory.java index 12c8d80f63..0f88acf091 100644 --- a/extensions/federation/hive/src/main/java/org/apache/polaris/extensions/federation/hive/HiveFederatedCatalogFactory.java +++ b/extensions/federation/hive/src/main/java/org/apache/polaris/extensions/federation/hive/HiveFederatedCatalogFactory.java @@ -29,6 +29,7 @@ import org.apache.polaris.core.connection.ConnectionConfigInfoDpo; import org.apache.polaris.core.connection.ConnectionType; import org.apache.polaris.core.connection.hive.HiveConnectionConfigInfoDpo; +import org.apache.polaris.core.credentials.PolarisCredentialManager; import org.apache.polaris.core.secrets.UserSecretsManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,7 +42,9 @@ public class HiveFederatedCatalogFactory implements ExternalCatalogFactory { @Override public Catalog createCatalog( - ConnectionConfigInfoDpo connectionConfigInfoDpo, UserSecretsManager userSecretsManager) { + ConnectionConfigInfoDpo connectionConfigInfoDpo, + UserSecretsManager userSecretsManager, + PolarisCredentialManager polarisCredentialManager) { // Currently, Polaris supports Hive federation only via IMPLICIT authentication. // Hence, prior to initializing the configuration, ensure that the catalog uses // IMPLICIT authentication. @@ -69,7 +72,9 @@ public Catalog createCatalog( // Kerberos instances are not suitable because Kerberos ties a single identity to the server. HiveCatalog hiveCatalog = new HiveCatalog(); hiveCatalog.initialize( - warehouse, connectionConfigInfoDpo.asIcebergCatalogProperties(userSecretsManager)); + warehouse, + connectionConfigInfoDpo.asIcebergCatalogProperties( + userSecretsManager, polarisCredentialManager)); return hiveCatalog; } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/catalog/ExternalCatalogFactory.java b/polaris-core/src/main/java/org/apache/polaris/core/catalog/ExternalCatalogFactory.java index 039a64ccd3..4624d893c4 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/catalog/ExternalCatalogFactory.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/catalog/ExternalCatalogFactory.java @@ -20,6 +20,7 @@ import org.apache.iceberg.catalog.Catalog; import org.apache.polaris.core.connection.ConnectionConfigInfoDpo; +import org.apache.polaris.core.credentials.PolarisCredentialManager; import org.apache.polaris.core.secrets.UserSecretsManager; /** @@ -35,11 +36,14 @@ public interface ExternalCatalogFactory { * * @param connectionConfig the connection configuration * @param userSecretsManager the user secrets manager for handling credentials + * @param polarisCredentialManager the Polaris credential manager for handling credentials * @return the initialized catalog * @throws IllegalStateException if the connection configuration is invalid */ Catalog createCatalog( - ConnectionConfigInfoDpo connectionConfig, UserSecretsManager userSecretsManager); + ConnectionConfigInfoDpo connectionConfig, + UserSecretsManager userSecretsManager, + PolarisCredentialManager polarisCredentialManager); /** * Creates a generic table catalog for the given connection configuration. diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/BearerAuthenticationParametersDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/BearerAuthenticationParametersDpo.java index 2da854a59a..c115020151 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/connection/BearerAuthenticationParametersDpo.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/BearerAuthenticationParametersDpo.java @@ -25,6 +25,7 @@ import org.apache.iceberg.rest.auth.OAuth2Properties; import org.apache.polaris.core.admin.model.AuthenticationParameters; import org.apache.polaris.core.admin.model.BearerAuthenticationParameters; +import org.apache.polaris.core.credentials.PolarisCredentialManager; import org.apache.polaris.core.secrets.SecretReference; import org.apache.polaris.core.secrets.UserSecretsManager; @@ -50,7 +51,7 @@ public BearerAuthenticationParametersDpo( @Override public @Nonnull Map asIcebergCatalogProperties( - UserSecretsManager secretsManager) { + UserSecretsManager secretsManager, PolarisCredentialManager credentialManager) { String bearerToken = secretsManager.readSecret(getBearerTokenReference()); return Map.of(OAuth2Properties.TOKEN, bearerToken); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/ImplicitAuthenticationParametersDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/ImplicitAuthenticationParametersDpo.java index dc19a789a9..1138724296 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/connection/ImplicitAuthenticationParametersDpo.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/ImplicitAuthenticationParametersDpo.java @@ -19,9 +19,11 @@ package org.apache.polaris.core.connection; import com.google.common.base.MoreObjects; +import jakarta.annotation.Nonnull; import java.util.Map; import org.apache.polaris.core.admin.model.AuthenticationParameters; import org.apache.polaris.core.admin.model.ImplicitAuthenticationParameters; +import org.apache.polaris.core.credentials.PolarisCredentialManager; import org.apache.polaris.core.secrets.UserSecretsManager; /** @@ -35,12 +37,13 @@ public ImplicitAuthenticationParametersDpo() { } @Override - public Map asIcebergCatalogProperties(UserSecretsManager secretsManager) { + public @Nonnull Map asIcebergCatalogProperties( + UserSecretsManager secretsManager, PolarisCredentialManager credentialManager) { return Map.of(); } @Override - public AuthenticationParameters asAuthenticationParametersModel() { + public @Nonnull AuthenticationParameters asAuthenticationParametersModel() { return ImplicitAuthenticationParameters.builder() .setAuthenticationType(AuthenticationParameters.AuthenticationTypeEnum.IMPLICIT) .build(); diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/OAuthClientCredentialsParametersDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/OAuthClientCredentialsParametersDpo.java index 270560b505..9c12936247 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/connection/OAuthClientCredentialsParametersDpo.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/OAuthClientCredentialsParametersDpo.java @@ -35,6 +35,7 @@ import org.apache.iceberg.rest.auth.OAuth2Util; import org.apache.polaris.core.admin.model.AuthenticationParameters; import org.apache.polaris.core.admin.model.OAuthClientCredentialsParameters; +import org.apache.polaris.core.credentials.PolarisCredentialManager; import org.apache.polaris.core.secrets.SecretReference; import org.apache.polaris.core.secrets.UserSecretsManager; @@ -104,7 +105,7 @@ public OAuthClientCredentialsParametersDpo( @Override public @Nonnull Map asIcebergCatalogProperties( - UserSecretsManager secretsManager) { + UserSecretsManager secretsManager, PolarisCredentialManager credentialManager) { HashMap properties = new HashMap<>(); if (getTokenUri() != null) { properties.put(OAuth2Properties.OAUTH2_SERVER_URI, getTokenUri()); diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/SigV4AuthenticationParametersDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/SigV4AuthenticationParametersDpo.java index 1d5ca8561f..c7e693f6c3 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/connection/SigV4AuthenticationParametersDpo.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/SigV4AuthenticationParametersDpo.java @@ -23,11 +23,14 @@ import com.google.common.collect.ImmutableMap; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import java.util.EnumMap; import java.util.Map; import org.apache.iceberg.aws.AwsProperties; import org.apache.iceberg.rest.auth.AuthProperties; import org.apache.polaris.core.admin.model.AuthenticationParameters; import org.apache.polaris.core.admin.model.SigV4AuthenticationParameters; +import org.apache.polaris.core.credentials.PolarisCredentialManager; +import org.apache.polaris.core.credentials.connection.ConnectionCredentialProperty; import org.apache.polaris.core.secrets.UserSecretsManager; /** @@ -93,7 +96,8 @@ public SigV4AuthenticationParametersDpo( @Nonnull @Override - public Map asIcebergCatalogProperties(UserSecretsManager secretsManager) { + public Map asIcebergCatalogProperties( + UserSecretsManager secretsManager, PolarisCredentialManager credentialManager) { ImmutableMap.Builder builder = ImmutableMap.builder(); builder.put(AuthProperties.AUTH_TYPE, AuthProperties.AUTH_TYPE_SIGV4); builder.put(AwsProperties.REST_SIGNER_REGION, getSigningRegion()); @@ -101,7 +105,10 @@ public Map asIcebergCatalogProperties(UserSecretsManager secrets builder.put(AwsProperties.REST_SIGNING_NAME, getSigningName()); } - // TODO: Add a credential manager to assume the role and get the aws session credentials + EnumMap connectionCredentialProperties = + credentialManager.getConnectionCredentials(null, this); + connectionCredentialProperties.forEach( + (key, value) -> builder.put(key.getPropertyName(), value)); return builder.build(); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/hadoop/HadoopConnectionConfigInfoDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/hadoop/HadoopConnectionConfigInfoDpo.java index b9a7ef8267..3025980fb1 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/connection/hadoop/HadoopConnectionConfigInfoDpo.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/hadoop/HadoopConnectionConfigInfoDpo.java @@ -31,6 +31,7 @@ import org.apache.polaris.core.connection.AuthenticationParametersDpo; import org.apache.polaris.core.connection.ConnectionConfigInfoDpo; import org.apache.polaris.core.connection.ConnectionType; +import org.apache.polaris.core.credentials.PolarisCredentialManager; import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; import org.apache.polaris.core.secrets.UserSecretsManager; @@ -71,13 +72,15 @@ public String toString() { @Override public @Nonnull Map asIcebergCatalogProperties( - UserSecretsManager secretsManager) { + UserSecretsManager secretsManager, PolarisCredentialManager credentialManager) { HashMap properties = new HashMap<>(); properties.put(CatalogProperties.URI, getUri()); if (getWarehouse() != null) { properties.put(CatalogProperties.WAREHOUSE_LOCATION, getWarehouse()); } - properties.putAll(getAuthenticationParameters().asIcebergCatalogProperties(secretsManager)); + properties.putAll( + getAuthenticationParameters() + .asIcebergCatalogProperties(secretsManager, credentialManager)); return properties; } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/hive/HiveConnectionConfigInfoDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/hive/HiveConnectionConfigInfoDpo.java index 1d88c389c6..1cda4ede21 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/connection/hive/HiveConnectionConfigInfoDpo.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/hive/HiveConnectionConfigInfoDpo.java @@ -31,6 +31,7 @@ import org.apache.polaris.core.connection.AuthenticationParametersDpo; import org.apache.polaris.core.connection.ConnectionConfigInfoDpo; import org.apache.polaris.core.connection.ConnectionType; +import org.apache.polaris.core.credentials.PolarisCredentialManager; import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; import org.apache.polaris.core.secrets.UserSecretsManager; @@ -70,14 +71,16 @@ public String toString() { @Override public @Nonnull Map asIcebergCatalogProperties( - UserSecretsManager secretsManager) { + UserSecretsManager secretsManager, PolarisCredentialManager polarisCredentialManager) { HashMap properties = new HashMap<>(); properties.put(CatalogProperties.URI, getUri()); if (getWarehouse() != null) { properties.put(CatalogProperties.WAREHOUSE_LOCATION, getWarehouse()); } if (getAuthenticationParameters() != null) { - properties.putAll(getAuthenticationParameters().asIcebergCatalogProperties(secretsManager)); + properties.putAll( + getAuthenticationParameters() + .asIcebergCatalogProperties(secretsManager, polarisCredentialManager)); } return properties; } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergCatalogPropertiesProvider.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergCatalogPropertiesProvider.java index 75af01100f..e17218f25e 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergCatalogPropertiesProvider.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergCatalogPropertiesProvider.java @@ -20,6 +20,7 @@ import jakarta.annotation.Nonnull; import java.util.Map; +import org.apache.polaris.core.credentials.PolarisCredentialManager; import org.apache.polaris.core.secrets.UserSecretsManager; /** @@ -30,5 +31,6 @@ */ public interface IcebergCatalogPropertiesProvider { @Nonnull - Map asIcebergCatalogProperties(UserSecretsManager secretsManager); + Map asIcebergCatalogProperties( + UserSecretsManager secretsManager, PolarisCredentialManager credentialManager); } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpo.java index 6ac49a2ed0..a9f841961e 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpo.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpo.java @@ -31,6 +31,7 @@ import org.apache.polaris.core.connection.AuthenticationParametersDpo; import org.apache.polaris.core.connection.ConnectionConfigInfoDpo; import org.apache.polaris.core.connection.ConnectionType; +import org.apache.polaris.core.credentials.PolarisCredentialManager; import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; import org.apache.polaris.core.secrets.UserSecretsManager; @@ -63,13 +64,19 @@ public String getRemoteCatalogName() { @Override public @Nonnull Map asIcebergCatalogProperties( - UserSecretsManager secretsManager) { + UserSecretsManager secretsManager, PolarisCredentialManager credentialManager) { HashMap properties = new HashMap<>(); properties.put(CatalogProperties.URI, getUri()); if (getRemoteCatalogName() != null) { properties.put(CatalogProperties.WAREHOUSE_LOCATION, getRemoteCatalogName()); } - properties.putAll(getAuthenticationParameters().asIcebergCatalogProperties(secretsManager)); + properties.putAll( + getAuthenticationParameters() + .asIcebergCatalogProperties( + secretsManager, + (serviceIdentity, authenticationParameters) -> + credentialManager.getConnectionCredentials( + getServiceIdentity(), authenticationParameters))); return properties; } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/credentials/DefaultPolarisCredentialManager.java b/polaris-core/src/main/java/org/apache/polaris/core/credentials/DefaultPolarisCredentialManager.java new file mode 100644 index 0000000000..4214af7237 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/credentials/DefaultPolarisCredentialManager.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.core.credentials; + +import com.google.common.annotations.VisibleForTesting; +import jakarta.annotation.Nonnull; +import java.util.EnumMap; +import java.util.Optional; +import org.apache.polaris.core.connection.AuthenticationParametersDpo; +import org.apache.polaris.core.connection.SigV4AuthenticationParametersDpo; +import org.apache.polaris.core.credentials.connection.ConnectionCredentialProperty; +import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; +import org.apache.polaris.core.identity.resolved.ResolvedAwsIamServiceIdentity; +import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.annotations.NotNull; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.services.sts.StsClientBuilder; +import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; +import software.amazon.awssdk.services.sts.model.AssumeRoleResponse; + +/** + * Default implementation of {@link PolarisCredentialManager} responsible for retrieving credentials + * used by Polaris to access external systems such as remote catalogs or cloud storage. + * + *

It resolves a {@link ServiceIdentityInfoDpo} into a {@link ResolvedServiceIdentity} using the + * {@link ServiceIdentityRegistry}, then uses the provided authentication parameters to generate + * temporary access credentials (e.g., via AWS STS AssumeRole). + * + *

This implementation currently supports AWS IAM service identities and can be extended to + * support other identity types or external services beyond catalogs, such as cloud storage. + */ +public class DefaultPolarisCredentialManager implements PolarisCredentialManager { + private final ServiceIdentityRegistry serviceIdentityRegistry; + + public DefaultPolarisCredentialManager(ServiceIdentityRegistry serviceIdentityRegistry) { + this.serviceIdentityRegistry = serviceIdentityRegistry; + } + + @Override + public @Nonnull EnumMap getConnectionCredentials( + ServiceIdentityInfoDpo serviceIdentity, + AuthenticationParametersDpo authenticationParameters) { + EnumMap credentialMap = + new EnumMap<>(ConnectionCredentialProperty.class); + Optional resolvedServiceIdentity = + serviceIdentityRegistry.resolveServiceIdentity(serviceIdentity); + if (resolvedServiceIdentity.isEmpty()) { + return credentialMap; + } + + switch (serviceIdentity.getIdentityType()) { + case AWS_IAM: + ResolvedAwsIamServiceIdentity resolvedAwsIamServiceIdentity = + (ResolvedAwsIamServiceIdentity) resolvedServiceIdentity.get(); + SigV4AuthenticationParametersDpo sigV4AuthenticationParameters = + (SigV4AuthenticationParametersDpo) authenticationParameters; + StsClient stsClient = getStsClient(resolvedAwsIamServiceIdentity); + AssumeRoleResponse response = + stsClient.assumeRole( + AssumeRoleRequest.builder() + .roleArn(sigV4AuthenticationParameters.getRoleArn()) + .roleSessionName( + Optional.ofNullable(sigV4AuthenticationParameters.getRoleSessionName()) + .orElse("polaris")) + .externalId(sigV4AuthenticationParameters.getExternalId()) + .build()); + credentialMap.put( + ConnectionCredentialProperty.AWS_ACCESS_KEY_ID, response.credentials().accessKeyId()); + credentialMap.put( + ConnectionCredentialProperty.AWS_SECRET_ACCESS_KEY, + response.credentials().secretAccessKey()); + credentialMap.put( + ConnectionCredentialProperty.AWS_SESSION_TOKEN, response.credentials().sessionToken()); + Optional.ofNullable(response.credentials().expiration()) + .ifPresent( + i -> { + credentialMap.put( + ConnectionCredentialProperty.EXPIRATION_TIME, + String.valueOf(i.toEpochMilli())); + }); + break; + default: + LoggerFactory.getLogger(DefaultPolarisCredentialManager.class) + .warn("Unsupported service identity type: {}", serviceIdentity.getIdentityType()); + return credentialMap; + } + return credentialMap; + } + + @VisibleForTesting + public StsClient getStsClient( + @NotNull ResolvedAwsIamServiceIdentity resolvedAwsIamServiceIdentity) { + // TODO: Use STS client pool to reduce client creation overhead + StsClientBuilder stsClientBuilder = StsClient.builder(); + stsClientBuilder.credentialsProvider(resolvedAwsIamServiceIdentity.getAwsCredentialsProvider()); + return stsClientBuilder.build(); + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/credentials/PolarisCredentialManager.java b/polaris-core/src/main/java/org/apache/polaris/core/credentials/PolarisCredentialManager.java new file mode 100644 index 0000000000..1039b3b3eb --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/credentials/PolarisCredentialManager.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.core.credentials; + +import jakarta.annotation.Nonnull; +import java.util.EnumMap; +import org.apache.polaris.core.connection.AuthenticationParametersDpo; +import org.apache.polaris.core.credentials.connection.ConnectionCredentialProperty; +import org.apache.polaris.core.credentials.connection.PolarisConnectionCredsVendor; +import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; + +/** + * PolarisCredentialManager is responsible for retrieving the credentials Polaris needs to access + * remote services such as federated catalogs or cloud storage. + * + *

It combines service-managed identity information (e.g., an IAM user Polaris uses) with + * user-defined authentication parameters (e.g., roleArn) to generate the credentials required for + * authentication with external systems. + * + *

Typical flow: + * + *

    + *
  1. Resolve the service identity and locate its associated credential (e.g., from a secret + * manager via the service identity registry). + *
  2. Use the resolved identity together with the authentication parameters to obtain the final + * access credentials. + *
+ * + *

This design supports both SaaS and self-managed deployments, ensuring a clear separation + * between user-provided configuration and Polaris-managed identity. + */ +public interface PolarisCredentialManager extends PolarisConnectionCredsVendor { + @Override + @Nonnull + EnumMap getConnectionCredentials( + ServiceIdentityInfoDpo serviceIdentity, AuthenticationParametersDpo authenticationParameters); +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/credentials/PolarisCredentialManagerFactory.java b/polaris-core/src/main/java/org/apache/polaris/core/credentials/PolarisCredentialManagerFactory.java new file mode 100644 index 0000000000..b74179d4c6 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/credentials/PolarisCredentialManagerFactory.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.core.credentials; + +import org.apache.polaris.core.context.RealmContext; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; + +/** + * Factory for creating {@link PolarisCredentialManager} instances. + * + *

Each {@link PolarisCredentialManager} instance is associated with a {@link RealmContext} and + * is responsible for managing the credentials for the user in that realm. + */ +public interface PolarisCredentialManagerFactory { + PolarisCredentialManager getOrCreatePolarisCredentialManager( + RealmContext realmContext, ServiceIdentityRegistry serviceIdentityRegistry); +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/credentials/connection/ConnectionCredentialProperty.java b/polaris-core/src/main/java/org/apache/polaris/core/credentials/connection/ConnectionCredentialProperty.java new file mode 100644 index 0000000000..b2d9133318 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/credentials/connection/ConnectionCredentialProperty.java @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.core.credentials.connection; + +import org.apache.iceberg.aws.AwsProperties; + +/** + * A subset of Iceberg catalog properties recognized by Polaris. + * + *

Most of these properties are meant to initialize Catalog objects for accessing the remote + * Catalog service. + */ +public enum ConnectionCredentialProperty { + AWS_ACCESS_KEY_ID(String.class, AwsProperties.REST_ACCESS_KEY_ID, "the aws access key id"), + AWS_SECRET_ACCESS_KEY( + String.class, AwsProperties.REST_SECRET_ACCESS_KEY, "the aws access key secret"), + AWS_SESSION_TOKEN(String.class, AwsProperties.REST_SESSION_TOKEN, "the aws scoped access token"), + EXPIRATION_TIME( + Long.class, "expiration-time", "the expiration time for the access token, in milliseconds"); + + private final Class valueType; + private final String propertyName; + private final String description; + private final boolean isCredential; + + ConnectionCredentialProperty(Class valueType, String propertyName, String description) { + this(valueType, propertyName, description, true); + } + + ConnectionCredentialProperty( + Class valueType, String propertyName, String description, boolean isCredential) { + this.valueType = valueType; + this.propertyName = propertyName; + this.description = description; + this.isCredential = isCredential; + } + + public String getPropertyName() { + return propertyName; + } + + public boolean isCredential() { + return isCredential; + } +} diff --git a/polaris-core/src/main/java/org/apache/polaris/core/credentials/connection/PolarisConnectionCredsVendor.java b/polaris-core/src/main/java/org/apache/polaris/core/credentials/connection/PolarisConnectionCredsVendor.java new file mode 100644 index 0000000000..8482f49f5f --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/credentials/connection/PolarisConnectionCredsVendor.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.core.credentials.connection; + +import jakarta.annotation.Nonnull; +import java.util.EnumMap; +import org.apache.polaris.core.connection.AuthenticationParametersDpo; +import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; + +/** + * Generates credentials Polaris uses to connect to external catalog services such as AWS Glue or + * other federated endpoints. Implementations combine service-managed identity metadata (such as an + * IAM user or role, defined in {@link ServiceIdentityInfoDpo}) with user-provided authentication + * parameters (such as a role ARN or external ID, defined in {@link AuthenticationParametersDpo}) to + * construct a credential map consumable by Polaris. + * + *

This interface allows pluggable behavior for different authentication mechanisms and service + * vendors. Implementations can support service-specific credential provisioning or caching + * strategies as needed. + */ +public interface PolarisConnectionCredsVendor { + + /** + * Retrieve credential values required to authenticate a remote connection. + * + *

The returned credentials are derived using the combination of Polaris-managed service + * identity and user-specified connection authentication parameters. Implementations may look up + * the service identity credential from a secret store or local config, and use it in conjunction + * with user-supplied data to produce scoped credentials for accessing remote services. + * + * @param serviceIdentity Polaris-managed identity metadata, including a reference to the backing + * credential (e.g., a secret ARN or ID) + * @param authenticationParameters Authentication configuration supplied by the Polaris user + * @return A map from {@link ConnectionCredentialProperty} to the resolved credential value, used + * by downstream systems to establish the connection + */ + @Nonnull + EnumMap getConnectionCredentials( + ServiceIdentityInfoDpo serviceIdentity, AuthenticationParametersDpo authenticationParameters); +} diff --git a/polaris-core/src/test/java/org/apache/polaris/core/identity/ResolvedServiceIdentityTest.java b/polaris-core/src/test/java/org/apache/polaris/core/identity/ResolvedServiceIdentityTest.java new file mode 100644 index 0000000000..20e0d154c3 --- /dev/null +++ b/polaris-core/src/test/java/org/apache/polaris/core/identity/ResolvedServiceIdentityTest.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.core.identity; + +import java.util.Map; +import org.apache.polaris.core.identity.dpo.AwsIamServiceIdentityInfoDpo; +import org.apache.polaris.core.identity.resolved.ResolvedAwsIamServiceIdentity; +import org.apache.polaris.core.secrets.ServiceSecretReference; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; + +public class ResolvedServiceIdentityTest { + + @Test + void testResolvedAwsIamServiceIdentity() { + AwsCredentialsProvider awsCredentialsProvider = + StaticCredentialsProvider.create( + AwsSessionCredentials.create("access-key", "secret-key", "session-token")); + + ResolvedAwsIamServiceIdentity identity = + new ResolvedAwsIamServiceIdentity( + "arn:aws:iam::123456789012:user/polaris-iam-user", awsCredentialsProvider); + AwsIamServiceIdentityInfoDpo dpo = + (AwsIamServiceIdentityInfoDpo) identity.asServiceIdentityInfoDpo(); + Assertions.assertThat(dpo.getIdentityType()).isEqualTo(ServiceIdentityType.AWS_IAM); + + ServiceSecretReference identityInfoReference = + new ServiceSecretReference( + "urn:polaris-secret:defualt-identity-registry:my-realm:aws-iam", Map.of()); + identity.setIdentityInfoReference(identityInfoReference); + dpo = (AwsIamServiceIdentityInfoDpo) identity.asServiceIdentityInfoDpo(); + Assertions.assertThat(dpo.getIdentityInfoReference()).isEqualTo(identityInfoReference); + + AwsSessionCredentials credentials = + (AwsSessionCredentials) identity.getAwsCredentialsProvider().resolveCredentials(); + Assertions.assertThat(credentials.accessKeyId()).isEqualTo("access-key"); + Assertions.assertThat(credentials.secretAccessKey()).isEqualTo("secret-key"); + Assertions.assertThat(credentials.sessionToken()).isEqualTo("session-token"); + } +} diff --git a/runtime/defaults/src/main/resources/application.properties b/runtime/defaults/src/main/resources/application.properties index b6aa08972b..0d873563ec 100644 --- a/runtime/defaults/src/main/resources/application.properties +++ b/runtime/defaults/src/main/resources/application.properties @@ -206,6 +206,9 @@ polaris.oidc.principal-roles-mapper.type=default # polaris.service-identity.my-realm.aws-iam.secret-access-key=secretAccessKey # polaris.service-identity.my-realm.aws-iam.session-token=sessionToken +# Polaris Credential Manager Config +polaris.credential-manager.type=default + quarkus.arc.ignored-split-packages=\ org.apache.polaris.service.catalog.api,\ org.apache.polaris.service.catalog.api.impl,\ @@ -216,6 +219,7 @@ quarkus.arc.ignored-split-packages=\ org.apache.polaris.service.auth.external.mapping,\ org.apache.polaris.service.auth.external.tenant,\ org.apache.polaris.service.auth.internal,\ + org.apache.polaris.service.credentials,\ org.apache.polaris.service.events,\ org.apache.polaris.service.identity,\ org.apache.polaris.service.task,\ diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java index 89a2e22302..b2fdd3c60a 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/common/CatalogHandler.java @@ -39,6 +39,7 @@ import org.apache.polaris.core.catalog.PolarisCatalogHelpers; import org.apache.polaris.core.config.RealmConfig; import org.apache.polaris.core.context.CallContext; +import org.apache.polaris.core.credentials.PolarisCredentialManager; import org.apache.polaris.core.entity.PolarisEntitySubType; import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; @@ -63,6 +64,7 @@ public abstract class CatalogHandler { protected final String catalogName; protected final PolarisAuthorizer authorizer; protected final UserSecretsManager userSecretsManager; + protected final PolarisCredentialManager credentialManager; protected final Instance externalCatalogFactories; protected final PolarisDiagnostics diagnostics; @@ -79,6 +81,7 @@ public CatalogHandler( String catalogName, PolarisAuthorizer authorizer, UserSecretsManager userSecretsManager, + PolarisCredentialManager credentialManager, Instance externalCatalogFactories) { this.diagnostics = diagnostics; this.callContext = callContext; @@ -95,6 +98,7 @@ public CatalogHandler( this.polarisPrincipal = (PolarisPrincipal) securityContext.getUserPrincipal(); this.authorizer = authorizer; this.userSecretsManager = userSecretsManager; + this.credentialManager = credentialManager; this.externalCatalogFactories = externalCatalogFactories; } @@ -102,6 +106,10 @@ protected UserSecretsManager getUserSecretsManager() { return userSecretsManager; } + protected PolarisCredentialManager getPolarisCredentialManager() { + return credentialManager; + } + /** Initialize the catalog once authorized. Called after all `authorize...` methods. */ protected abstract void initializeCatalog(); diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogAdapter.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogAdapter.java index 650c747dcf..ae16db5ba6 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogAdapter.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogAdapter.java @@ -32,6 +32,7 @@ import org.apache.polaris.core.config.RealmConfig; import org.apache.polaris.core.context.CallContext; import org.apache.polaris.core.context.RealmContext; +import org.apache.polaris.core.credentials.PolarisCredentialManager; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory; import org.apache.polaris.core.secrets.UserSecretsManager; @@ -61,6 +62,7 @@ public class GenericTableCatalogAdapter private final ReservedProperties reservedProperties; private final CatalogPrefixParser prefixParser; private final UserSecretsManager userSecretsManager; + private final PolarisCredentialManager polarisCredentialManager; private final Instance externalCatalogFactories; @Inject @@ -74,6 +76,7 @@ public GenericTableCatalogAdapter( CatalogPrefixParser prefixParser, ReservedProperties reservedProperties, UserSecretsManager userSecretsManager, + PolarisCredentialManager polarisCredentialManager, @Any Instance externalCatalogFactories) { this.diagnostics = diagnostics; this.realmContext = realmContext; @@ -85,6 +88,7 @@ public GenericTableCatalogAdapter( this.prefixParser = prefixParser; this.reservedProperties = reservedProperties; this.userSecretsManager = userSecretsManager; + this.polarisCredentialManager = polarisCredentialManager; this.externalCatalogFactories = externalCatalogFactories; } @@ -103,6 +107,7 @@ private GenericTableCatalogHandler newHandlerWrapper( prefixParser.prefixToCatalogName(realmContext, prefix), polarisAuthorizer, userSecretsManager, + polarisCredentialManager, externalCatalogFactories); } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandler.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandler.java index 1445572690..0123c7a144 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandler.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/generic/GenericTableCatalogHandler.java @@ -34,6 +34,7 @@ import org.apache.polaris.core.connection.ConnectionConfigInfoDpo; import org.apache.polaris.core.connection.ConnectionType; import org.apache.polaris.core.context.CallContext; +import org.apache.polaris.core.credentials.PolarisCredentialManager; import org.apache.polaris.core.entity.CatalogEntity; import org.apache.polaris.core.entity.PolarisEntitySubType; import org.apache.polaris.core.entity.table.GenericTableEntity; @@ -63,6 +64,7 @@ public GenericTableCatalogHandler( String catalogName, PolarisAuthorizer authorizer, UserSecretsManager userSecretsManager, + PolarisCredentialManager polarisCredentialManager, Instance externalCatalogFactories) { super( diagnostics, @@ -72,6 +74,7 @@ public GenericTableCatalogHandler( catalogName, authorizer, userSecretsManager, + polarisCredentialManager, externalCatalogFactories); this.metaStoreManager = metaStoreManager; } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java index a9552e78b6..a56f99d0ef 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java @@ -67,6 +67,7 @@ import org.apache.polaris.core.config.RealmConfig; import org.apache.polaris.core.context.CallContext; import org.apache.polaris.core.context.RealmContext; +import org.apache.polaris.core.credentials.PolarisCredentialManager; import org.apache.polaris.core.entity.PolarisEntity; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.ResolvedPolarisEntity; @@ -148,6 +149,7 @@ public class IcebergCatalogAdapter private final ResolverFactory resolverFactory; private final PolarisMetaStoreManager metaStoreManager; private final UserSecretsManager userSecretsManager; + private final PolarisCredentialManager credentialManager; private final PolarisAuthorizer polarisAuthorizer; private final CatalogPrefixParser prefixParser; private final ReservedProperties reservedProperties; @@ -165,6 +167,7 @@ public IcebergCatalogAdapter( ResolutionManifestFactory resolutionManifestFactory, PolarisMetaStoreManager metaStoreManager, UserSecretsManager userSecretsManager, + PolarisCredentialManager credentialManager, PolarisAuthorizer polarisAuthorizer, CatalogPrefixParser prefixParser, ReservedProperties reservedProperties, @@ -180,6 +183,7 @@ public IcebergCatalogAdapter( this.resolverFactory = resolverFactory; this.metaStoreManager = metaStoreManager; this.userSecretsManager = userSecretsManager; + this.credentialManager = credentialManager; this.polarisAuthorizer = polarisAuthorizer; this.prefixParser = prefixParser; this.reservedProperties = reservedProperties; @@ -218,6 +222,7 @@ IcebergCatalogHandler newHandlerWrapper(SecurityContext securityContext, String resolutionManifestFactory, metaStoreManager, userSecretsManager, + credentialManager, securityContext, catalogFactory, catalogName, diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java index d1daf8fa56..70a5bca456 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandler.java @@ -84,6 +84,7 @@ import org.apache.polaris.core.connection.ConnectionConfigInfoDpo; import org.apache.polaris.core.connection.ConnectionType; import org.apache.polaris.core.context.CallContext; +import org.apache.polaris.core.credentials.PolarisCredentialManager; import org.apache.polaris.core.entity.CatalogEntity; import org.apache.polaris.core.entity.PolarisEntity; import org.apache.polaris.core.entity.PolarisEntitySubType; @@ -151,6 +152,7 @@ public IcebergCatalogHandler( ResolutionManifestFactory resolutionManifestFactory, PolarisMetaStoreManager metaStoreManager, UserSecretsManager userSecretsManager, + PolarisCredentialManager credentialManager, SecurityContext securityContext, CallContextCatalogFactory catalogFactory, String catalogName, @@ -167,6 +169,7 @@ public IcebergCatalogHandler( catalogName, authorizer, userSecretsManager, + credentialManager, externalCatalogFactories); this.metaStoreManager = metaStoreManager; this.catalogFactory = catalogFactory; @@ -248,7 +251,10 @@ protected void initializeCatalog() { federatedCatalog = externalCatalogFactory .get() - .createCatalog(connectionConfigInfoDpo, getUserSecretsManager()); + .createCatalog( + connectionConfigInfoDpo, + getUserSecretsManager(), + getPolarisCredentialManager()); } else { throw new UnsupportedOperationException( "External catalog factory for type '" + connectionType + "' is unavailable."); diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergRESTExternalCatalogFactory.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergRESTExternalCatalogFactory.java index c1ce3b276f..7167e382e6 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergRESTExternalCatalogFactory.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergRESTExternalCatalogFactory.java @@ -29,6 +29,7 @@ import org.apache.polaris.core.connection.ConnectionConfigInfoDpo; import org.apache.polaris.core.connection.ConnectionType; import org.apache.polaris.core.connection.iceberg.IcebergRestConnectionConfigInfoDpo; +import org.apache.polaris.core.credentials.PolarisCredentialManager; import org.apache.polaris.core.secrets.UserSecretsManager; /** Factory class for creating an Iceberg REST catalog handle based on connection configuration. */ @@ -38,7 +39,9 @@ public class IcebergRESTExternalCatalogFactory implements ExternalCatalogFactory @Override public Catalog createCatalog( - ConnectionConfigInfoDpo connectionConfig, UserSecretsManager userSecretsManager) { + ConnectionConfigInfoDpo connectionConfig, + UserSecretsManager userSecretsManager, + PolarisCredentialManager polarisCredentialManager) { if (!(connectionConfig instanceof IcebergRestConnectionConfigInfoDpo icebergConfig)) { throw new IllegalArgumentException( "Expected IcebergRestConnectionConfigInfoDpo but got: " @@ -56,7 +59,7 @@ public Catalog createCatalog( federatedCatalog.initialize( icebergConfig.getRemoteCatalogName(), - connectionConfig.asIcebergCatalogProperties(userSecretsManager)); + connectionConfig.asIcebergCatalogProperties(userSecretsManager, polarisCredentialManager)); return federatedCatalog; } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogAdapter.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogAdapter.java index 98bb3d9f15..5458480650 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogAdapter.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogAdapter.java @@ -33,6 +33,7 @@ import org.apache.polaris.core.config.RealmConfig; import org.apache.polaris.core.context.CallContext; import org.apache.polaris.core.context.RealmContext; +import org.apache.polaris.core.credentials.PolarisCredentialManager; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory; import org.apache.polaris.core.policy.PolicyType; @@ -64,6 +65,7 @@ public class PolicyCatalogAdapter implements PolarisCatalogPolicyApiService, Cat private final PolarisAuthorizer polarisAuthorizer; private final CatalogPrefixParser prefixParser; private final UserSecretsManager userSecretsManager; + private final PolarisCredentialManager polarisCredentialManager; private final Instance externalCatalogFactories; @Inject @@ -76,6 +78,7 @@ public PolicyCatalogAdapter( PolarisAuthorizer polarisAuthorizer, CatalogPrefixParser prefixParser, UserSecretsManager userSecretsManager, + PolarisCredentialManager polarisCredentialManager, @Any Instance externalCatalogFactories) { this.diagnostics = diagnostics; this.realmContext = realmContext; @@ -86,6 +89,7 @@ public PolicyCatalogAdapter( this.polarisAuthorizer = polarisAuthorizer; this.prefixParser = prefixParser; this.userSecretsManager = userSecretsManager; + this.polarisCredentialManager = polarisCredentialManager; this.externalCatalogFactories = externalCatalogFactories; } @@ -103,6 +107,7 @@ private PolicyCatalogHandler newHandlerWrapper(SecurityContext securityContext, prefixParser.prefixToCatalogName(realmContext, prefix), polarisAuthorizer, userSecretsManager, + polarisCredentialManager, externalCatalogFactories); } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java index 96012a2c0b..6bd68e9507 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandler.java @@ -36,6 +36,7 @@ import org.apache.polaris.core.catalog.ExternalCatalogFactory; import org.apache.polaris.core.catalog.PolarisCatalogHelpers; import org.apache.polaris.core.context.CallContext; +import org.apache.polaris.core.credentials.PolarisCredentialManager; import org.apache.polaris.core.entity.PolarisEntitySubType; import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; @@ -72,6 +73,7 @@ public PolicyCatalogHandler( String catalogName, PolarisAuthorizer authorizer, UserSecretsManager userSecretsManager, + PolarisCredentialManager polarisCredentialManager, Instance externalCatalogFactories) { super( diagnostics, @@ -81,6 +83,7 @@ public PolicyCatalogHandler( catalogName, authorizer, userSecretsManager, + polarisCredentialManager, externalCatalogFactories); this.metaStoreManager = metaStoreManager; } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java b/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java index d0c123e7ff..199cc973f7 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java @@ -43,6 +43,8 @@ import org.apache.polaris.core.config.RealmConfig; import org.apache.polaris.core.context.CallContext; import org.apache.polaris.core.context.RealmContext; +import org.apache.polaris.core.credentials.PolarisCredentialManager; +import org.apache.polaris.core.credentials.PolarisCredentialManagerFactory; import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; import org.apache.polaris.core.persistence.BasePersistence; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; @@ -71,6 +73,7 @@ import org.apache.polaris.service.context.RealmContextConfiguration; import org.apache.polaris.service.context.RealmContextFilter; import org.apache.polaris.service.context.RealmContextResolver; +import org.apache.polaris.service.credentials.PolarisCredentialManagerConfiguration; import org.apache.polaris.service.events.PolarisEventListenerConfiguration; import org.apache.polaris.service.events.listeners.PolarisEventListener; import org.apache.polaris.service.identity.ServiceIdentityConfiguration; @@ -244,6 +247,13 @@ public StsClientsPool stsClientsPool( return new StsClientsPool(config.effectiveClientsCacheMaxSize(), httpClient, meterRegistry); } + @Produces + public PolarisCredentialManagerFactory credentialManagerFactory( + PolarisCredentialManagerConfiguration config, + @Any Instance credentialManagerFactories) { + return credentialManagerFactories.select(Identifier.Literal.of(config.type())).get(); + } + /** * Eagerly initialize the in-memory default realm on startup, so that users can check the * credentials printed to stdout immediately. @@ -401,6 +411,16 @@ public ServiceIdentityRegistry serviceIdentityRegistry( return new DefaultServiceIdentityRegistry(realmContext, serviceIdentityConfiguration); } + @Produces + @RequestScoped + public PolarisCredentialManager polarisCredentialManager( + PolarisCredentialManagerFactory polarisCredentialManagerFactory, + RealmContext realmContext, + ServiceIdentityRegistry serviceIdentityRegistry) { + return polarisCredentialManagerFactory.getOrCreatePolarisCredentialManager( + realmContext, serviceIdentityRegistry); + } + public void closeTaskExecutor(@Disposes @Identifier("task-executor") ManagedExecutor executor) { executor.close(); } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/credentials/DefaultPolarisCredentialManagerFactory.java b/runtime/service/src/main/java/org/apache/polaris/service/credentials/DefaultPolarisCredentialManagerFactory.java new file mode 100644 index 0000000000..2bdac6fbc5 --- /dev/null +++ b/runtime/service/src/main/java/org/apache/polaris/service/credentials/DefaultPolarisCredentialManagerFactory.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.service.credentials; + +import io.smallrye.common.annotation.Identifier; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.apache.polaris.core.context.RealmContext; +import org.apache.polaris.core.credentials.DefaultPolarisCredentialManager; +import org.apache.polaris.core.credentials.PolarisCredentialManager; +import org.apache.polaris.core.credentials.PolarisCredentialManagerFactory; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; + +@ApplicationScoped +@Identifier("default") +public class DefaultPolarisCredentialManagerFactory implements PolarisCredentialManagerFactory { + private final Map cachedCredentialManagers = + new ConcurrentHashMap<>(); + + @Inject + public DefaultPolarisCredentialManagerFactory() {} + + @Override + public PolarisCredentialManager getOrCreatePolarisCredentialManager( + RealmContext realmContext, ServiceIdentityRegistry serviceIdentityRegistry) { + return cachedCredentialManagers.computeIfAbsent( + realmContext.getRealmIdentifier(), + key -> new DefaultPolarisCredentialManager(serviceIdentityRegistry)); + } +} diff --git a/runtime/service/src/main/java/org/apache/polaris/service/credentials/PolarisCredentialManagerConfiguration.java b/runtime/service/src/main/java/org/apache/polaris/service/credentials/PolarisCredentialManagerConfiguration.java new file mode 100644 index 0000000000..fee5dcd3bb --- /dev/null +++ b/runtime/service/src/main/java/org/apache/polaris/service/credentials/PolarisCredentialManagerConfiguration.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.service.credentials; + +import io.quarkus.runtime.annotations.StaticInitSafe; +import io.smallrye.config.ConfigMapping; + +/** + * Quarkus configuration mapping for Polaris Credential Manager. + * + *

Defines which {@link org.apache.polaris.core.credentials.PolarisCredentialManagerFactory} + * implementation should be used at runtime. This allows switching between different credential + * management strategies via configuration. + */ +@StaticInitSafe +@ConfigMapping(prefix = "polaris.credential-manager") +public interface PolarisCredentialManagerConfiguration { + + /** + * The type of the PolarisCredentialManagerFactory to use. This is the {@link + * org.apache.polaris.core.credentials.PolarisCredentialManagerFactory} identifier. + */ + String type(); +} diff --git a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java index e7cf6c2bab..3403344c60 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/admin/PolarisAuthzTestBase.java @@ -65,6 +65,8 @@ import org.apache.polaris.core.config.RealmConfig; import org.apache.polaris.core.context.CallContext; import org.apache.polaris.core.context.RealmContext; +import org.apache.polaris.core.credentials.PolarisCredentialManager; +import org.apache.polaris.core.credentials.PolarisCredentialManagerFactory; import org.apache.polaris.core.entity.CatalogEntity; import org.apache.polaris.core.entity.CatalogRoleEntity; import org.apache.polaris.core.entity.PolarisBaseEntity; @@ -198,6 +200,7 @@ public Map getConfigOverrides() { @Inject protected CallContextCatalogFactory callContextCatalogFactory; @Inject protected UserSecretsManagerFactory userSecretsManagerFactory; @Inject protected ServiceIdentityRegistry serviceIdentityRegistry; + @Inject protected PolarisCredentialManagerFactory credentialManagerFactory; @Inject protected PolarisDiagnostics diagServices; @Inject protected FileIOFactory fileIOFactory; @Inject protected PolarisEventListener polarisEventListener; @@ -212,6 +215,7 @@ public Map getConfigOverrides() { protected PolarisAdminService adminService; protected PolarisMetaStoreManager metaStoreManager; protected UserSecretsManager userSecretsManager; + protected PolarisCredentialManager credentialManager; protected PolarisBaseEntity catalogEntity; protected PolarisBaseEntity federatedCatalogEntity; protected PrincipalEntity principalEntity; @@ -245,6 +249,9 @@ public void before(TestInfo testInfo) { QuarkusMock.installMockForType(containerRequestContext, ContainerRequestContext.class); metaStoreManager = managerFactory.getOrCreateMetaStoreManager(realmContext); userSecretsManager = userSecretsManagerFactory.getOrCreateUserSecretsManager(realmContext); + credentialManager = + credentialManagerFactory.getOrCreatePolarisCredentialManager( + realmContext, serviceIdentityRegistry); polarisContext = new PolarisCallContext( diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/PolarisGenericTableCatalogHandlerAuthzTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/PolarisGenericTableCatalogHandlerAuthzTest.java index b1296177b7..00c87f8997 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/PolarisGenericTableCatalogHandlerAuthzTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/generic/PolarisGenericTableCatalogHandlerAuthzTest.java @@ -54,6 +54,7 @@ private GenericTableCatalogHandler newWrapper( catalogName, polarisAuthorizer, null, + null, null); } diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerAuthzTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerAuthzTest.java index f0c92634a1..354635bb46 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerAuthzTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogHandlerAuthzTest.java @@ -106,6 +106,7 @@ private IcebergCatalogHandler newWrapper( resolutionManifestFactory, metaStoreManager, userSecretsManager, + credentialManager, securityContext(authenticatedPrincipal), factory, catalogName, @@ -247,6 +248,7 @@ public void testInsufficientPermissionsPriorToSecretRotation() { resolutionManifestFactory, metaStoreManager, userSecretsManager, + credentialManager, securityContext(authenticatedPrincipal), callContextCatalogFactory, CATALOG_NAME, @@ -285,6 +287,7 @@ public void testInsufficientPermissionsPriorToSecretRotation() { resolutionManifestFactory, metaStoreManager, userSecretsManager, + credentialManager, securityContext(authenticatedPrincipal1), callContextCatalogFactory, CATALOG_NAME, diff --git a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerAuthzTest.java b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerAuthzTest.java index 990a9cff28..9a85496db3 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerAuthzTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/catalog/policy/PolicyCatalogHandlerAuthzTest.java @@ -59,6 +59,7 @@ private PolicyCatalogHandler newWrapper(Set activatedPrincipalRoles, Str catalogName, polarisAuthorizer, null, + null, null); } diff --git a/runtime/service/src/test/java/org/apache/polaris/service/credentials/DefaultPolarisCredentialManagerTest.java b/runtime/service/src/test/java/org/apache/polaris/service/credentials/DefaultPolarisCredentialManagerTest.java new file mode 100644 index 0000000000..6e317d832e --- /dev/null +++ b/runtime/service/src/test/java/org/apache/polaris/service/credentials/DefaultPolarisCredentialManagerTest.java @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.service.credentials; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; +import jakarta.inject.Inject; +import java.time.Instant; +import java.util.EnumMap; +import java.util.Map; +import org.apache.polaris.core.connection.SigV4AuthenticationParametersDpo; +import org.apache.polaris.core.context.RealmContext; +import org.apache.polaris.core.credentials.DefaultPolarisCredentialManager; +import org.apache.polaris.core.credentials.connection.ConnectionCredentialProperty; +import org.apache.polaris.core.identity.ServiceIdentityType; +import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; +import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; +import org.apache.polaris.core.identity.resolved.ResolvedAwsIamServiceIdentity; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import software.amazon.awssdk.auth.credentials.AwsCredentials; +import software.amazon.awssdk.services.sts.StsClient; +import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; +import software.amazon.awssdk.services.sts.model.AssumeRoleResponse; +import software.amazon.awssdk.services.sts.model.Credentials; + +@QuarkusTest +@TestProfile(DefaultPolarisCredentialManagerTest.Profile.class) +public class DefaultPolarisCredentialManagerTest { + + @InjectMock RealmContext realmContext; + + @Inject PolarisCredentialManagerConfiguration configuration; + @Inject ServiceIdentityRegistry serviceIdentityRegistry; + + DefaultPolarisCredentialManager credentialManager; + + public static class Profile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of( + "quarkus.identity-registry.type", + "default", + "polaris.service-identity.my-realm.aws-iam.iam-arn", + "arn:aws:iam::123456789012:user/polaris-iam-user", + "polaris.service-identity.my-realm.aws-iam.access-key-id", + "access-key-id", + "polaris.service-identity.my-realm.aws-iam.secret-access-key", + "secret-access-key", + "polaris.credential-manager.type", + "default"); + } + } + + @BeforeEach + void setup() { + // Mock the realm context to return a specific realm + when(realmContext.getRealmIdentifier()).thenReturn("my-realm"); + + credentialManager = Mockito.spy(new DefaultPolarisCredentialManager(serviceIdentityRegistry)); + doAnswer( + invocation -> { + // Capture the identity here + ResolvedAwsIamServiceIdentity identity = invocation.getArgument(0); + + StsClient mockStsClient = mock(StsClient.class); + when(mockStsClient.assumeRole(Mockito.any(AssumeRoleRequest.class))) + .thenAnswer( + stsInvocation -> { + // Validate identity at the time assumeRole is called + AwsCredentials credentials = + identity.getAwsCredentialsProvider().resolveCredentials(); + if (!"access-key-id".equals(credentials.accessKeyId()) + || !"secret-access-key".equals(credentials.secretAccessKey())) { + throw new IllegalArgumentException("Invalid credentials on assumeRole"); + } + + // Return mocked credentials + Credentials tmpSessionCredentials = + Credentials.builder() + .accessKeyId("tmp-access-key-id") + .secretAccessKey("tmp-secret-access-key") + .sessionToken("tmp-session-token") + .expiration(Instant.now().plusSeconds(3600)) + .build(); + + return AssumeRoleResponse.builder() + .credentials(tmpSessionCredentials) + .build(); + }); + return mockStsClient; + }) + .when(credentialManager) + .getStsClient(any()); + } + + @Test + public void testGetConnectionCredentialsForSigV4() { + ServiceIdentityInfoDpo serviceIdentityInfo = + serviceIdentityRegistry.discoverServiceIdentity(ServiceIdentityType.AWS_IAM).get(); + EnumMap credentials = + credentialManager.getConnectionCredentials( + serviceIdentityInfo, + new SigV4AuthenticationParametersDpo( + "arn:aws:iam::123456789012:role/polaris-users-iam-role", + null, + null, + "us-west-2", + "glue")); + Assertions.assertThat(credentials) + .containsEntry(ConnectionCredentialProperty.AWS_ACCESS_KEY_ID, "tmp-access-key-id") + .containsEntry(ConnectionCredentialProperty.AWS_SECRET_ACCESS_KEY, "tmp-secret-access-key") + .containsEntry(ConnectionCredentialProperty.AWS_SESSION_TOKEN, "tmp-session-token") + .containsKey(ConnectionCredentialProperty.EXPIRATION_TIME) + .size() + .isEqualTo(4); + } +} diff --git a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java index 922228149d..813eae1a65 100644 --- a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java +++ b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java @@ -45,6 +45,8 @@ import org.apache.polaris.core.config.RealmConfig; import org.apache.polaris.core.context.CallContext; import org.apache.polaris.core.context.RealmContext; +import org.apache.polaris.core.credentials.PolarisCredentialManager; +import org.apache.polaris.core.credentials.PolarisCredentialManagerFactory; import org.apache.polaris.core.entity.PrincipalEntity; import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; import org.apache.polaris.core.persistence.BasePersistence; @@ -73,6 +75,7 @@ import org.apache.polaris.service.config.ReservedProperties; import org.apache.polaris.service.context.catalog.CallContextCatalogFactory; import org.apache.polaris.service.context.catalog.PolarisCallContextCatalogFactory; +import org.apache.polaris.service.credentials.DefaultPolarisCredentialManagerFactory; import org.apache.polaris.service.events.listeners.PolarisEventListener; import org.apache.polaris.service.events.listeners.TestPolarisEventListener; import org.apache.polaris.service.identity.registry.DefaultServiceIdentityRegistry; @@ -193,6 +196,8 @@ public TestServices build() { UserSecretsManagerFactory userSecretsManagerFactory = new UnsafeInMemorySecretsManagerFactory(); + PolarisCredentialManagerFactory credentialManagerFactory = + new DefaultPolarisCredentialManagerFactory(); BasePersistence metaStoreSession = metaStoreManagerFactory.getOrCreateSession(realmContext); CallContext callContext = @@ -219,6 +224,9 @@ public TestServices build() { UserSecretsManager userSecretsManager = userSecretsManagerFactory.getOrCreateUserSecretsManager(realmContext); ServiceIdentityRegistry serviceIdentityRegistry = new DefaultServiceIdentityRegistry(); + PolarisCredentialManager credentialManager = + credentialManagerFactory.getOrCreatePolarisCredentialManager( + realmContext, serviceIdentityRegistry); FileIOFactory fileIOFactory = fileIOFactorySupplier.apply(storageCredentialCache, metaStoreManagerFactory); @@ -255,6 +263,7 @@ public TestServices build() { resolutionManifestFactory, metaStoreManager, userSecretsManager, + credentialManager, authorizer, new DefaultCatalogPrefixParser(), reservedProperties,