From cd8294949c4114256c37c330d224fc9b345b6c2f Mon Sep 17 00:00:00 2001 From: Rulin Xing Date: Mon, 8 Sep 2025 00:36:59 -0700 Subject: [PATCH 01/12] 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 02/12] 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 03/12] 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 04/12] 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 05/12] 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 06/12] 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 870d682901155c147a621666610636c69a08342a Mon Sep 17 00:00:00 2001 From: Rulin Xing Date: Thu, 2 Oct 2025 14:06:11 -0700 Subject: [PATCH 07/12] Rename ServiceIdentityRegistry to ServiceIdentityProvider --- .../connection/ConnectionConfigInfoDpo.java | 4 +- .../hadoop/HadoopConnectionConfigInfoDpo.java | 6 +- .../hive/HiveConnectionConfigInfoDpo.java | 6 +- .../IcebergRestConnectionConfigInfoDpo.java | 6 +- .../polaris/core/entity/CatalogEntity.java | 16 +-- .../identity/dpo/ServiceIdentityInfoDpo.java | 14 +-- .../provider/ServiceIdentityProvider.java | 101 ++++++++++++++++++ .../registry/ServiceIdentityRegistry.java | 58 ---------- .../ConnectionConfigInfoDpoTest.java | 31 ++++-- .../service/admin/PolarisAdminService.java | 20 ++-- .../service/admin/PolarisServiceImpl.java | 14 +-- .../service/config/ServiceProducers.java | 8 +- .../ServiceIdentityConfiguration.java | 4 +- .../DefaultServiceIdentityProvider.java} | 60 ++++++++--- .../service/admin/ManagementServiceTest.java | 4 +- .../admin/PolarisAdminServiceAuthzTest.java | 10 +- .../admin/PolarisAdminServiceTest.java | 6 +- .../service/admin/PolarisAuthzTestBase.java | 8 +- .../service/admin/PolarisServiceImplTest.java | 10 +- ...bstractPolarisGenericTableCatalogTest.java | 8 +- .../iceberg/AbstractIcebergCatalogTest.java | 18 ++-- .../AbstractIcebergCatalogViewTest.java | 8 +- .../IcebergCatalogHandlerAuthzTest.java | 2 +- .../policy/AbstractPolicyCatalogTest.java | 8 +- .../service/entity/CatalogEntityTest.java | 25 +++-- .../DefaultServiceIdentityProviderTest.java} | 32 +++--- .../apache/polaris/service/TestServices.java | 10 +- 27 files changed, 293 insertions(+), 204 deletions(-) create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/identity/provider/ServiceIdentityProvider.java delete mode 100644 polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistry.java rename runtime/service/src/main/java/org/apache/polaris/service/identity/{registry/DefaultServiceIdentityRegistry.java => provider/DefaultServiceIdentityProvider.java} (71%) rename runtime/service/src/test/java/org/apache/polaris/service/identity/{registry/DefaultServiceIdentityRegistryTest.java => provider/DefaultServiceIdentityProviderTest.java} (91%) 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 2db61531e3..b7d00e4a87 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,7 +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.identity.provider.ServiceIdentityProvider; import org.apache.polaris.core.secrets.SecretReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -231,5 +231,5 @@ public abstract ConnectionConfigInfoDpo withServiceIdentity( * applicable/present in the persistence object, but not the API model object. */ public abstract ConnectionConfigInfo asConnectionConfigInfoModel( - ServiceIdentityRegistry serviceIdentityRegistry); + ServiceIdentityProvider serviceIdentityProvider); } 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..66fe8bb707 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,7 +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.identity.provider.ServiceIdentityProvider; import org.apache.polaris.core.secrets.UserSecretsManager; /** @@ -90,7 +90,7 @@ public ConnectionConfigInfoDpo withServiceIdentity( @Override public ConnectionConfigInfo asConnectionConfigInfoModel( - ServiceIdentityRegistry serviceIdentityRegistry) { + ServiceIdentityProvider serviceIdentityProvider) { return HadoopConnectionConfigInfo.builder() .setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.HADOOP) .setUri(getUri()) @@ -101,7 +101,7 @@ public ConnectionConfigInfo asConnectionConfigInfoModel( Optional.ofNullable(getServiceIdentity()) .map( serviceIdentityInfoDpo -> - serviceIdentityInfoDpo.asServiceIdentityInfoModel(serviceIdentityRegistry)) + serviceIdentityInfoDpo.asServiceIdentityInfoModel(serviceIdentityProvider)) .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 1d88c389c6..22b067a6c1 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 @@ -32,7 +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.identity.provider.ServiceIdentityProvider; import org.apache.polaris.core.secrets.UserSecretsManager; /** @@ -91,7 +91,7 @@ public ConnectionConfigInfoDpo withServiceIdentity( @Override public ConnectionConfigInfo asConnectionConfigInfoModel( - ServiceIdentityRegistry serviceIdentityRegistry) { + ServiceIdentityProvider serviceIdentityProvider) { return HiveConnectionConfigInfo.builder() .setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.HIVE) .setUri(getUri()) @@ -102,7 +102,7 @@ public ConnectionConfigInfo asConnectionConfigInfoModel( Optional.ofNullable(getServiceIdentity()) .map( serviceIdentityInfoDpo -> - serviceIdentityInfoDpo.asServiceIdentityInfoModel(serviceIdentityRegistry)) + serviceIdentityInfoDpo.asServiceIdentityInfoModel(serviceIdentityProvider)) .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 6ac49a2ed0..ee01691b97 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,7 +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.identity.provider.ServiceIdentityProvider; import org.apache.polaris.core.secrets.UserSecretsManager; /** @@ -82,7 +82,7 @@ public ConnectionConfigInfoDpo withServiceIdentity( @Override public ConnectionConfigInfo asConnectionConfigInfoModel( - ServiceIdentityRegistry serviceIdentityRegistry) { + ServiceIdentityProvider serviceIdentityProvider) { return IcebergRestConnectionConfigInfo.builder() .setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.ICEBERG_REST) .setUri(getUri()) @@ -93,7 +93,7 @@ public ConnectionConfigInfo asConnectionConfigInfoModel( Optional.ofNullable(getServiceIdentity()) .map( serviceIdentityInfoDpo -> - serviceIdentityInfoDpo.asServiceIdentityInfoModel(serviceIdentityRegistry)) + serviceIdentityInfoDpo.asServiceIdentityInfoModel(serviceIdentityProvider)) .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 ee1661e58f..11d63ba603 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,7 +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.identity.provider.ServiceIdentityProvider; import org.apache.polaris.core.secrets.SecretReference; import org.apache.polaris.core.storage.FileStorageConfigurationInfo; import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo; @@ -113,7 +113,7 @@ public Catalog asCatalog() { return this.asCatalog(null); } - public Catalog asCatalog(ServiceIdentityRegistry serviceIdentityRegistry) { + public Catalog asCatalog(ServiceIdentityProvider serviceIdentityProvider) { Map internalProperties = getInternalPropertiesAsMap(); Catalog.TypeEnum catalogType = Optional.ofNullable(internalProperties.get(CATALOG_TYPE_PROPERTY)) @@ -125,10 +125,10 @@ public Catalog asCatalog(ServiceIdentityRegistry serviceIdentityRegistry) { .putAll(propertiesMap) .build(); - // Right now, only external catalog may use ServiceIdentityRegistry to resolve identity + // Right now, only external catalog may use ServiceIdentityProvider to resolve identity Preconditions.checkState( - catalogType != Catalog.TypeEnum.EXTERNAL || serviceIdentityRegistry != null, - "%s catalog needs ServiceIdentityRegistry to resolve service identities", + catalogType != Catalog.TypeEnum.EXTERNAL || serviceIdentityProvider != null, + "%s catalog needs ServiceIdentityProvider to resolve service identities", Catalog.TypeEnum.EXTERNAL); return catalogType == Catalog.TypeEnum.EXTERNAL ? ExternalCatalog.builder() @@ -139,7 +139,7 @@ public Catalog asCatalog(ServiceIdentityRegistry serviceIdentityRegistry) { .setLastUpdateTimestamp(getLastUpdateTimestamp()) .setEntityVersion(getEntityVersion()) .setStorageConfigInfo(getStorageInfo(internalProperties)) - .setConnectionConfigInfo(getConnectionInfo(internalProperties, serviceIdentityRegistry)) + .setConnectionConfigInfo(getConnectionInfo(internalProperties, serviceIdentityProvider)) .build() : PolarisCatalog.builder() .setType(Catalog.TypeEnum.INTERNAL) @@ -199,11 +199,11 @@ private StorageConfigInfo getStorageInfo(Map internalProperties) } private ConnectionConfigInfo getConnectionInfo( - Map internalProperties, ServiceIdentityRegistry serviceIdentityRegistry) { + Map internalProperties, ServiceIdentityProvider serviceIdentityProvider) { if (internalProperties.containsKey( PolarisEntityConstants.getConnectionConfigInfoPropertyName())) { ConnectionConfigInfoDpo configInfo = getConnectionConfigInfoDpo(); - return configInfo.asConnectionConfigInfoModel(serviceIdentityRegistry); + return configInfo.asConnectionConfigInfoModel(serviceIdentityProvider); } return null; } 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 fb34413414..b089cb8d73 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 @@ -26,8 +26,7 @@ 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.identity.provider.ServiceIdentityProvider; import org.apache.polaris.core.secrets.SecretReference; /** @@ -76,18 +75,15 @@ 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 * - * @param serviceIdentityRegistry the service identity registry to resolve the identity + * @param serviceIdentityProvider the service identity provider to resolve the identity */ public @Nullable ServiceIdentityInfo asServiceIdentityInfoModel( - ServiceIdentityRegistry serviceIdentityRegistry) { - if (serviceIdentityRegistry == null) { + ServiceIdentityProvider serviceIdentityProvider) { + if (serviceIdentityProvider == null) { return null; } - return serviceIdentityRegistry - .resolveServiceIdentity(this) - .map(ResolvedServiceIdentity::asServiceIdentityInfoModel) - .orElse(null); + return serviceIdentityProvider.getServiceIdentityInfo(this).orElse(null); } @Override diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/provider/ServiceIdentityProvider.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/provider/ServiceIdentityProvider.java new file mode 100644 index 0000000000..50dd0d2ec7 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/provider/ServiceIdentityProvider.java @@ -0,0 +1,101 @@ +/* + * 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.provider; + +import java.util.Optional; + +import jakarta.annotation.Nonnull; +import org.apache.polaris.core.admin.model.ConnectionConfigInfo; +import org.apache.polaris.core.admin.model.ServiceIdentityInfo; +import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; +import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; + +/** + * A provider 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 provider offers a central mechanism to allocate + * service identities to catalog entities and resolve them at runtime. + * + *

The provider 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. + * + *

Key responsibilities: + * + *

    + *
  • Allocation: Assign a service identity to a catalog entity during creation. The + * actual credentials are not stored in the entity; only a reference is persisted. + *
  • Resolution (with credentials): Retrieve the full identity including credentials for + * authentication purposes (e.g., signing requests with SigV4). + *
  • Resolution (without credentials): Retrieve the identity information for display in + * API responses without exposing sensitive credentials. + *
+ */ +public interface ServiceIdentityProvider { + /** + * Allocates a {@link ServiceIdentityInfoDpo} for the given connection configuration. This method + * is typically invoked during catalog entity creation to associate a service identity with the + * catalog for accessing external services. + * + *

The allocation strategy is implementation-specific: + * + *

    + *
  • A vendor may choose to use the same service identity across all entities in an account. + *
  • Alternatively, different service identities can be assigned per catalog entity. + *
  • The associated DPO stores only a reference to the service identity, not the credentials + * themselves. + *
+ * + * @param connectionConfig The connection configuration for which a service identity should be + * allocated. + * @return An {@link Optional} containing the allocated {@link ServiceIdentityInfoDpo}, or empty + * if no service identity is available or applicable for this connection. + */ + Optional allocateServiceIdentity(@Nonnull ConnectionConfigInfo connectionConfig); + + /** + * Retrieves the user-facing {@link ServiceIdentityInfo} model for the given service identity + * reference, without exposing sensitive credentials. + * + *

This method is used when generating API responses (e.g., {@code getCatalog}) to return + * identity details such as the AWS IAM user ARN, but not the actual credentials. + * + * @param serviceIdentityInfo The service identity metadata to resolve. + * @return An {@link Optional} containing the {@link ServiceIdentityInfo} model for API responses, + * or empty if the identity cannot be resolved. + */ + Optional getServiceIdentityInfo(@Nonnull ServiceIdentityInfoDpo serviceIdentityInfo); + + /** + * Resolves the given service identity by retrieving the actual credential or secret referenced by + * it, typically from a secret manager or internal credential store. + * + *

This method is used when Polaris needs to authenticate to external systems using the service + * identity, such as when signing requests with SigV4 authentication. + * + * @param serviceIdentityInfo The service identity metadata to resolve. + * @return An {@link Optional} containing a {@link ResolvedServiceIdentity} with credentials and + * other resolved data, or empty if the identity cannot be resolved. + */ + Optional resolveServiceIdentity( + @Nonnull ServiceIdentityInfoDpo serviceIdentityInfo); +} 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 deleted file mode 100644 index 75f23505c2..0000000000 --- a/polaris-core/src/main/java/org/apache/polaris/core/identity/registry/ServiceIdentityRegistry.java +++ /dev/null @@ -1,58 +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 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; - -/** - * 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 { - /** - * 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 discovered service identity. - */ - Optional discoverServiceIdentity(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. - */ - Optional resolveServiceIdentity( - ServiceIdentityInfoDpo serviceIdentityInfo); -} 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 b6d195bc68..82e2c39000 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 @@ -23,8 +23,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Optional; +import org.apache.polaris.core.admin.model.AwsIamServiceIdentityInfo; import org.apache.polaris.core.admin.model.ConnectionConfigInfo; -import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; +import org.apache.polaris.core.admin.model.ServiceIdentityInfo; +import org.apache.polaris.core.identity.provider.ServiceIdentityProvider; import org.apache.polaris.core.identity.resolved.ResolvedAwsIamServiceIdentity; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -38,15 +40,22 @@ public class ConnectionConfigInfoDpoTest { objectMapper.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); } - private ServiceIdentityRegistry serviceIdentityRegistry; + private ServiceIdentityProvider serviceIdentityProvider; @BeforeEach void setUp() { - serviceIdentityRegistry = Mockito.mock(ServiceIdentityRegistry.class); - Mockito.when(serviceIdentityRegistry.resolveServiceIdentity(Mockito.any())) + serviceIdentityProvider = Mockito.mock(ServiceIdentityProvider.class); + Mockito.when(serviceIdentityProvider.getServiceIdentityInfo(Mockito.any())) .thenReturn( Optional.of( - new ResolvedAwsIamServiceIdentity("arn:aws:iam::123456789012:role/example-role"))); + AwsIamServiceIdentityInfo.builder() + .setIdentityType(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM) + .setIamArn("arn:aws:iam::123456789012:user/test-user") + .build())); + Mockito.when(serviceIdentityProvider.resolveServiceIdentity(Mockito.any())) + .thenReturn( + Optional.of( + new ResolvedAwsIamServiceIdentity("arn:aws:iam::123456789012:user/test-user"))); } @Test @@ -80,7 +89,7 @@ void testOAuthClientCredentialsParameters() throws JsonProcessingException { // Test conversion into API model JSON. ConnectionConfigInfo connectionConfigInfoApiModel = - connectionConfigInfoDpo.asConnectionConfigInfoModel(serviceIdentityRegistry); + connectionConfigInfoDpo.asConnectionConfigInfoModel(serviceIdentityProvider); String expectedApiModelJson = "" + "{" @@ -127,7 +136,7 @@ void testBearerAuthenticationParameters() throws JsonProcessingException { // Test conversion into API model JSON. ConnectionConfigInfo connectionConfigInfoApiModel = - connectionConfigInfoDpo.asConnectionConfigInfoModel(serviceIdentityRegistry); + connectionConfigInfoDpo.asConnectionConfigInfoModel(serviceIdentityProvider); String expectedApiModelJson = "" + "{" @@ -164,7 +173,7 @@ void testImplicitAuthenticationParameters() throws JsonProcessingException { // Test conversion into API model JSON. ConnectionConfigInfo connectionConfigInfoApiModel = - connectionConfigInfoDpo.asConnectionConfigInfoModel(serviceIdentityRegistry); + connectionConfigInfoDpo.asConnectionConfigInfoModel(serviceIdentityProvider); String expectedApiModelJson = "" + "{" @@ -200,7 +209,7 @@ void testSigV4AuthenticationParameters() throws JsonProcessingException { + " \"serviceIdentity\": {" + " \"identityTypeCode\": 1," + " \"identityInfoReference\": {" - + " \"urn\": \"urn:polaris-secret:default-identity-registry:my-realm:AWS_IAM\"," + + " \"urn\": \"urn:polaris-secret:default-identity-provider:my-realm:AWS_IAM\"," + " \"referencePayload\": {" + " \"key\": \"value\"" + " }" @@ -216,7 +225,7 @@ void testSigV4AuthenticationParameters() throws JsonProcessingException { // Test conversion into API model JSON. ConnectionConfigInfo connectionConfigInfoApiModel = - connectionConfigInfoDpo.asConnectionConfigInfoModel(serviceIdentityRegistry); + connectionConfigInfoDpo.asConnectionConfigInfoModel(serviceIdentityProvider); String expectedApiModelJson = "" + "{" @@ -233,7 +242,7 @@ void testSigV4AuthenticationParameters() throws JsonProcessingException { + " }," + " \"serviceIdentity\": {" + " \"identityType\": \"AWS_IAM\"," - + " \"iamArn\": \"arn:aws:iam::123456789012:role/example-role\"" + + " \"iamArn\": \"arn:aws:iam::123456789012:user/test-user\"" + " }" + "}"; Assertions.assertEquals( 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 9ba5af4af6..0d2bb23444 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 @@ -102,7 +102,7 @@ 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.identity.provider.ServiceIdentityProvider; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; import org.apache.polaris.core.persistence.dao.entity.BaseResult; @@ -152,7 +152,7 @@ public class PolarisAdminService { private final PolarisAuthorizer authorizer; private final PolarisMetaStoreManager metaStoreManager; private final UserSecretsManager userSecretsManager; - private final ServiceIdentityRegistry serviceIdentityRegistry; + private final ServiceIdentityProvider serviceIdentityProvider; private final ReservedProperties reservedProperties; // Initialized in the authorize methods. @@ -165,7 +165,7 @@ public PolarisAdminService( @Nonnull ResolutionManifestFactory resolutionManifestFactory, @Nonnull PolarisMetaStoreManager metaStoreManager, @Nonnull UserSecretsManager userSecretsManager, - @Nonnull ServiceIdentityRegistry serviceIdentityRegistry, + @Nonnull ServiceIdentityProvider serviceIdentityProvider, @Nonnull SecurityContext securityContext, @Nonnull PolarisAuthorizer authorizer, @Nonnull ReservedProperties reservedProperties) { @@ -184,7 +184,7 @@ public PolarisAdminService( this.polarisPrincipal = (PolarisPrincipal) securityContext.getUserPrincipal(); this.authorizer = authorizer; this.userSecretsManager = userSecretsManager; - this.serviceIdentityRegistry = serviceIdentityRegistry; + this.serviceIdentityProvider = serviceIdentityProvider; this.reservedProperties = reservedProperties; } @@ -196,8 +196,8 @@ private UserSecretsManager getUserSecretsManager() { return userSecretsManager; } - private ServiceIdentityRegistry getServiceIdentityRegistry() { - return serviceIdentityRegistry; + private ServiceIdentityProvider getServiceIdentityProvider() { + return serviceIdentityProvider; } private Optional findCatalogByName(String name) { @@ -781,16 +781,16 @@ public PolarisEntity createCatalog(CreateCatalogRequest catalogRequest) { "Implicit authentication based catalog federation is not supported."); } - // Discover service identity if needed for the authentication type. + // Allocate service identity if needed for the authentication type. Optional serviceIdentityInfoDpoOptional = Optional.empty(); if (connectionConfigInfo.getAuthenticationParameters().getAuthenticationType() == AuthenticationParameters.AuthenticationTypeEnum.SIGV4) { serviceIdentityInfoDpoOptional = - serviceIdentityRegistry.discoverServiceIdentity(ServiceIdentityType.AWS_IAM); + serviceIdentityProvider.allocateServiceIdentity(connectionConfigInfo); if (serviceIdentityInfoDpoOptional.isEmpty()) { throw new IllegalStateException( String.format( - "Cannot create Catalog %s. Failed to discover %s service identity for %s authentication", + "Cannot create Catalog %s. Failed to allocate %s service identity for %s authentication", entity.getName(), ServiceIdentityType.AWS_IAM.name(), connectionConfigInfo @@ -985,7 +985,7 @@ private void validateUpdateCatalogDiffOrThrow( public List listCatalogs() { authorizeBasicRootOperationOrThrow(PolarisAuthorizableOperation.LIST_CATALOGS); return listCatalogsUnsafe() - .map(catalogEntity -> catalogEntity.asCatalog(getServiceIdentityRegistry())) + .map(catalogEntity -> catalogEntity.asCatalog(getServiceIdentityProvider())) .toList(); } 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 7aff0826d5..aff42fb374 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 @@ -70,7 +70,7 @@ 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.provider.ServiceIdentityProvider; 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; @@ -91,18 +91,18 @@ public class PolarisServiceImpl private final RealmConfig realmConfig; private final ReservedProperties reservedProperties; private final PolarisAdminService adminService; - private final ServiceIdentityRegistry serviceIdentityRegistry; + private final ServiceIdentityProvider serviceIdentityProvider; @Inject public PolarisServiceImpl( RealmConfig realmConfig, ReservedProperties reservedProperties, PolarisAdminService adminService, - ServiceIdentityRegistry serviceIdentityRegistry) { + ServiceIdentityProvider serviceIdentityProvider) { this.realmConfig = realmConfig; this.reservedProperties = reservedProperties; this.adminService = adminService; - this.serviceIdentityRegistry = serviceIdentityRegistry; + this.serviceIdentityProvider = serviceIdentityProvider; } private static Response toResponse(BaseResult result, Response.Status successStatus) { @@ -129,7 +129,7 @@ public Response createCatalog( validateStorageConfig(catalog.getStorageConfigInfo()); validateExternalCatalog(catalog); Catalog newCatalog = - CatalogEntity.of(adminService.createCatalog(request)).asCatalog(serviceIdentityRegistry); + CatalogEntity.of(adminService.createCatalog(request)).asCatalog(serviceIdentityProvider); LOGGER.info("Created new catalog {}", newCatalog); return Response.status(Response.Status.CREATED).entity(newCatalog).build(); } @@ -219,7 +219,7 @@ public Response deleteCatalog( @Override public Response getCatalog( String catalogName, RealmContext realmContext, SecurityContext securityContext) { - return Response.ok(adminService.getCatalog(catalogName).asCatalog(serviceIdentityRegistry)) + return Response.ok(adminService.getCatalog(catalogName).asCatalog(serviceIdentityProvider)) .build(); } @@ -236,7 +236,7 @@ public Response updateCatalog( return Response.ok( adminService .updateCatalog(catalogName, updateRequest) - .asCatalog(serviceIdentityRegistry)) + .asCatalog(serviceIdentityProvider)) .build(); } 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 c54585fbd4..455196d404 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,7 +43,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.identity.registry.ServiceIdentityRegistry; +import org.apache.polaris.core.identity.provider.ServiceIdentityProvider; import org.apache.polaris.core.persistence.BasePersistence; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; @@ -74,7 +74,7 @@ import org.apache.polaris.service.events.PolarisEventListenerConfiguration; import org.apache.polaris.service.events.listeners.PolarisEventListener; import org.apache.polaris.service.identity.ServiceIdentityConfiguration; -import org.apache.polaris.service.identity.registry.DefaultServiceIdentityRegistry; +import org.apache.polaris.service.identity.provider.DefaultServiceIdentityProvider; import org.apache.polaris.service.persistence.PersistenceConfiguration; import org.apache.polaris.service.ratelimiter.RateLimiter; import org.apache.polaris.service.ratelimiter.RateLimiterFilterConfiguration; @@ -396,9 +396,9 @@ public OidcTenantResolver oidcTenantResolver( @Produces @RequestScoped - public ServiceIdentityRegistry serviceIdentityRegistry( + public ServiceIdentityProvider serviceIdentityProvider( RealmContext realmContext, ServiceIdentityConfiguration serviceIdentityConfiguration) { - return new DefaultServiceIdentityRegistry(realmContext, serviceIdentityConfiguration); + return new DefaultServiceIdentityProvider(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 ca002aba5f..98888a426b 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,7 +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; +import org.apache.polaris.service.identity.provider.DefaultServiceIdentityProvider; /** * Represents the service identity configuration for one or more realms. @@ -86,7 +86,7 @@ default List resolveServiceIdentities( .map( resolvableServiceIdentityConfiguration -> resolvableServiceIdentityConfiguration.resolve( - DefaultServiceIdentityRegistry.buildIdentityInfoReference( + DefaultServiceIdentityProvider.buildIdentityInfoReference( entry.realm(), resolvableServiceIdentityConfiguration.getType()))) .flatMap(Optional::stream) .toList(); 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/provider/DefaultServiceIdentityProvider.java similarity index 71% rename from runtime/service/src/main/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistry.java rename to runtime/service/src/main/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProvider.java index 1fc16479a5..c59c3e3b89 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/provider/DefaultServiceIdentityProvider.java @@ -17,24 +17,28 @@ * under the License. */ -package org.apache.polaris.service.identity.registry; +package org.apache.polaris.service.identity.provider; import com.google.common.annotations.VisibleForTesting; +import jakarta.annotation.Nonnull; 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.admin.model.AuthenticationParameters; +import org.apache.polaris.core.admin.model.ConnectionConfigInfo; +import org.apache.polaris.core.admin.model.ServiceIdentityInfo; 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.provider.ServiceIdentityProvider; import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; import org.apache.polaris.core.secrets.ServiceSecretReference; import org.apache.polaris.service.identity.ServiceIdentityConfiguration; /** - * Default implementation of {@link ServiceIdentityRegistry} that resolves service identities from + * Default implementation of {@link ServiceIdentityProvider} 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) @@ -48,11 +52,11 @@ * defined and used system-wide. * */ -public class DefaultServiceIdentityRegistry implements ServiceIdentityRegistry { +public class DefaultServiceIdentityProvider implements ServiceIdentityProvider { 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"; + "urn:polaris-secret:default-identity-provider:%s:%s"; /** Map of service identity types to their resolved identities. */ private final EnumMap resolvedServiceIdentities; @@ -60,11 +64,11 @@ public class DefaultServiceIdentityRegistry implements ServiceIdentityRegistry { /** Map of identity info references (URNs) to their resolved service identities. */ private final Map referenceToResolvedServiceIdentity; - public DefaultServiceIdentityRegistry() { + public DefaultServiceIdentityProvider() { this(new EnumMap<>(ServiceIdentityType.class)); } - public DefaultServiceIdentityRegistry( + public DefaultServiceIdentityProvider( EnumMap serviceIdentities) { this.resolvedServiceIdentities = serviceIdentities; this.referenceToResolvedServiceIdentity = @@ -76,7 +80,7 @@ public DefaultServiceIdentityRegistry( } @Inject - public DefaultServiceIdentityRegistry( + public DefaultServiceIdentityProvider( RealmContext realmContext, ServiceIdentityConfiguration serviceIdentityConfiguration) { this.resolvedServiceIdentities = serviceIdentityConfiguration.resolveServiceIdentities(realmContext).stream() @@ -97,8 +101,26 @@ public DefaultServiceIdentityRegistry( } @Override - public Optional discoverServiceIdentity( - ServiceIdentityType serviceIdentityType) { + public Optional allocateServiceIdentity( + @Nonnull ConnectionConfigInfo connectionConfig) { + // Determine the service identity type based on the authentication parameters + if (connectionConfig.getAuthenticationParameters() == null) { + return Optional.empty(); + } + + AuthenticationParameters.AuthenticationTypeEnum authenticationType = + connectionConfig.getAuthenticationParameters().getAuthenticationType(); + + ServiceIdentityType serviceIdentityType = null; + if (authenticationType == AuthenticationParameters.AuthenticationTypeEnum.SIGV4) { + serviceIdentityType = ServiceIdentityType.AWS_IAM; + } + // Add more authentication types and their corresponding service identity types as needed + + if (serviceIdentityType == null) { + return Optional.empty(); + } + ResolvedServiceIdentity resolvedServiceIdentity = resolvedServiceIdentities.get(serviceIdentityType); if (resolvedServiceIdentity == null) { @@ -107,9 +129,21 @@ public Optional discoverServiceIdentity( return Optional.of(resolvedServiceIdentity.asServiceIdentityInfoDpo()); } + @Override + public Optional getServiceIdentityInfo( + @Nonnull ServiceIdentityInfoDpo serviceIdentityInfo) { + ResolvedServiceIdentity resolvedServiceIdentity = + referenceToResolvedServiceIdentity.get( + serviceIdentityInfo.getIdentityInfoReference().getUrn()); + if (resolvedServiceIdentity == null) { + return Optional.empty(); + } + return Optional.of(resolvedServiceIdentity.asServiceIdentityInfoModel()); + } + @Override public Optional resolveServiceIdentity( - ServiceIdentityInfoDpo serviceIdentityInfo) { + @Nonnull ServiceIdentityInfoDpo serviceIdentityInfo) { ResolvedServiceIdentity resolvedServiceIdentity = referenceToResolvedServiceIdentity.get( serviceIdentityInfo.getIdentityInfoReference().getUrn()); @@ -125,7 +159,7 @@ public EnumMap getResolvedServiceI * 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> + * urn:polaris-service-secret:default-identity-provider:<realm>:<type> * *

If the realm is the default realm key, it is replaced with "system:default" in the URN. * @@ -135,7 +169,7 @@ public EnumMap getResolvedServiceI */ public static ServiceSecretReference buildIdentityInfoReference( String realm, ServiceIdentityType type) { - // urn:polaris-service-secret:default-identity-registry:: + // urn:polaris-service-secret:default-identity-provider:: return new ServiceSecretReference( IDENTITY_INFO_REFERENCE_URN_FORMAT.formatted( realm.equals(DEFAULT_REALM_KEY) ? DEFAULT_REALM_NSS : realm, type.name()), 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 f729a34a40..573568126c 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 @@ -56,7 +56,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.apache.polaris.service.identity.provider.DefaultServiceIdentityProvider; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -237,7 +237,7 @@ private PolarisAdminService setupPolarisAdminService( services.resolutionManifestFactory(), metaStoreManager, new UnsafeInMemorySecretsManager(), - new DefaultServiceIdentityRegistry(new EnumMap<>(ServiceIdentityType.class)), + new DefaultServiceIdentityProvider(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 12a28dac8b..066aebf209 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 @@ -56,7 +56,7 @@ private PolarisAdminService newTestAdminService(Set activatedPrincipalRo resolutionManifestFactory, metaStoreManager, userSecretsManager, - serviceIdentityRegistry, + serviceIdentityProvider, securityContext(authenticatedPrincipal), polarisAuthorizer, reservedProperties); @@ -136,7 +136,7 @@ public void testCreateCatalogSufficientPrivileges() { PRINCIPAL_ROLE2, PolarisPrivilege.CATALOG_DROP)); final CatalogEntity newCatalog = new CatalogEntity.Builder().setName("new_catalog").build(); final CreateCatalogRequest createRequest = - new CreateCatalogRequest(newCatalog.asCatalog(serviceIdentityRegistry)); + new CreateCatalogRequest(newCatalog.asCatalog(serviceIdentityProvider)); doTestSufficientPrivileges( List.of( @@ -156,7 +156,7 @@ public void testCreateCatalogSufficientPrivileges() { public void testCreateCatalogInsufficientPrivileges() { final CatalogEntity newCatalog = new CatalogEntity.Builder().setName("new_catalog").build(); final CreateCatalogRequest createRequest = - new CreateCatalogRequest(newCatalog.asCatalog(serviceIdentityRegistry)); + new CreateCatalogRequest(newCatalog.asCatalog(serviceIdentityProvider)); doTestInsufficientPrivileges( List.of( @@ -292,7 +292,7 @@ public void testDeleteCatalogSufficientPrivileges() { PRINCIPAL_ROLE2, PolarisPrivilege.CATALOG_CREATE)); final CatalogEntity newCatalog = new CatalogEntity.Builder().setName("new_catalog").build(); final CreateCatalogRequest createRequest = - new CreateCatalogRequest(newCatalog.asCatalog(serviceIdentityRegistry)); + new CreateCatalogRequest(newCatalog.asCatalog(serviceIdentityProvider)); adminService.createCatalog(createRequest); doTestSufficientPrivileges( @@ -313,7 +313,7 @@ public void testDeleteCatalogSufficientPrivileges() { public void testDeleteCatalogInsufficientPrivileges() { final CatalogEntity newCatalog = new CatalogEntity.Builder().setName("new_catalog").build(); final CreateCatalogRequest createRequest = - new CreateCatalogRequest(newCatalog.asCatalog(serviceIdentityRegistry)); + new CreateCatalogRequest(newCatalog.asCatalog(serviceIdentityProvider)); adminService.createCatalog(createRequest); doTestInsufficientPrivileges( 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 e2b4920879..3e8a9953d3 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 @@ -46,7 +46,7 @@ import org.apache.polaris.core.entity.PolarisEntityType; import org.apache.polaris.core.entity.PolarisPrivilege; import org.apache.polaris.core.entity.table.IcebergTableLikeEntity; -import org.apache.polaris.core.identity.registry.ServiceIdentityRegistry; +import org.apache.polaris.core.identity.provider.ServiceIdentityProvider; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; import org.apache.polaris.core.persistence.dao.entity.BaseResult; @@ -72,7 +72,7 @@ public class PolarisAdminServiceTest { @Mock private ResolutionManifestFactory resolutionManifestFactory; @Mock private PolarisMetaStoreManager metaStoreManager; @Mock private UserSecretsManager userSecretsManager; - @Mock private ServiceIdentityRegistry identityRegistry; + @Mock private ServiceIdentityProvider identityProvider; @Mock private SecurityContext securityContext; @Mock private PolarisAuthorizer authorizer; @Mock private ReservedProperties reservedProperties; @@ -109,7 +109,7 @@ void setUp() throws Exception { resolutionManifestFactory, metaStoreManager, userSecretsManager, - identityRegistry, + identityProvider, 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 3c4c925d4b..d58ee1f812 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 @@ -72,7 +72,7 @@ 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.provider.ServiceIdentityProvider; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.dao.entity.BaseResult; @@ -196,7 +196,7 @@ public Map getConfigOverrides() { @Inject protected ResolutionManifestFactory resolutionManifestFactory; @Inject protected CallContextCatalogFactory callContextCatalogFactory; @Inject protected UserSecretsManagerFactory userSecretsManagerFactory; - @Inject protected ServiceIdentityRegistry serviceIdentityRegistry; + @Inject protected ServiceIdentityProvider serviceIdentityProvider; @Inject protected PolarisDiagnostics diagServices; @Inject protected FileIOFactory fileIOFactory; @Inject protected PolarisEventListener polarisEventListener; @@ -265,7 +265,7 @@ public void before(TestInfo testInfo) { resolutionManifestFactory, metaStoreManager, userSecretsManager, - serviceIdentityRegistry, + serviceIdentityProvider, securityContext(authenticatedRoot), polarisAuthorizer, reservedProperties); @@ -332,7 +332,7 @@ public void before(TestInfo testInfo) { .setDefaultBaseLocation(storageLocation) .setStorageConfigurationInfo(realmConfig, storageConfigModel, storageLocation) .build() - .asCatalog(serviceIdentityRegistry))); + .asCatalog(serviceIdentityProvider))); federatedCatalogEntity = adminService.createCatalog(new CreateCatalogRequest(externalCatalog)); 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 339a1b888a..0a8996e5f9 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 @@ -40,7 +40,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.ServiceIdentityRegistry; +import org.apache.polaris.core.identity.provider.ServiceIdentityProvider; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory; import org.apache.polaris.core.secrets.UserSecretsManager; @@ -59,7 +59,7 @@ public class PolarisServiceImplTest { private UserSecretsManagerFactory userSecretsManagerFactory; private PolarisMetaStoreManager metaStoreManager; private UserSecretsManager userSecretsManager; - private ServiceIdentityRegistry serviceIdentityRegistry; + private ServiceIdentityProvider serviceIdentityProvider; private PolarisAuthorizer polarisAuthorizer; private CallContext callContext; private ReservedProperties reservedProperties; @@ -74,7 +74,7 @@ void setUp() { resolutionManifestFactory = Mockito.mock(ResolutionManifestFactory.class); metaStoreManager = Mockito.mock(PolarisMetaStoreManager.class); userSecretsManager = Mockito.mock(UserSecretsManager.class); - serviceIdentityRegistry = Mockito.mock(ServiceIdentityRegistry.class); + serviceIdentityProvider = Mockito.mock(ServiceIdentityProvider.class); polarisAuthorizer = Mockito.mock(PolarisAuthorizer.class); callContext = Mockito.mock(CallContext.class); reservedProperties = Mockito.mock(ReservedProperties.class); @@ -98,13 +98,13 @@ void setUp() { resolutionManifestFactory, metaStoreManager, userSecretsManager, - serviceIdentityRegistry, + serviceIdentityProvider, securityContext, polarisAuthorizer, reservedProperties); polarisService = new PolarisServiceImpl( - realmConfig, reservedProperties, adminService, serviceIdentityRegistry); + realmConfig, reservedProperties, adminService, serviceIdentityProvider); } @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 f6b5974267..0edd699dce 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,7 +52,7 @@ 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.provider.ServiceIdentityProvider; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory; @@ -99,7 +99,7 @@ public abstract class AbstractPolarisGenericTableCatalogTest { @Inject MetaStoreManagerFactory metaStoreManagerFactory; @Inject UserSecretsManagerFactory userSecretsManagerFactory; - @Inject ServiceIdentityRegistry serviceIdentityRegistry; + @Inject ServiceIdentityProvider serviceIdentityProvider; @Inject PolarisConfigurationStore configurationStore; @Inject StorageCredentialCache storageCredentialCache; @Inject PolarisStorageIntegrationProvider storageIntegrationProvider; @@ -175,7 +175,7 @@ public void before(TestInfo testInfo) { resolutionManifestFactory, metaStoreManager, userSecretsManager, - serviceIdentityRegistry, + serviceIdentityProvider, securityContext, authorizer, reservedProperties); @@ -205,7 +205,7 @@ public void before(TestInfo testInfo) { FeatureConfiguration.DROP_WITH_PURGE_ENABLED.catalogConfig(), "true") .setStorageConfigurationInfo(realmConfig, storageConfigModel, storageLocation) .build() - .asCatalog(serviceIdentityRegistry))); + .asCatalog(serviceIdentityProvider))); 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 ba90245d39..39dd0f7e4f 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,7 +108,7 @@ 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.provider.ServiceIdentityProvider; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; @@ -228,7 +228,7 @@ public Map getConfigOverrides() { @Inject StorageCredentialCache storageCredentialCache; @Inject PolarisStorageIntegrationProvider storageIntegrationProvider; @Inject UserSecretsManagerFactory userSecretsManagerFactory; - @Inject ServiceIdentityRegistry serviceIdentityRegistry; + @Inject ServiceIdentityProvider serviceIdentityProvider; @Inject PolarisDiagnostics diagServices; @Inject PolarisEventListener polarisEventListener; @@ -317,7 +317,7 @@ public void before(TestInfo testInfo) { resolutionManifestFactory, metaStoreManager, userSecretsManager, - serviceIdentityRegistry, + serviceIdentityProvider, securityContext, authorizer, reservedProperties); @@ -347,7 +347,7 @@ public void before(TestInfo testInfo) { FeatureConfiguration.DROP_WITH_PURGE_ENABLED.catalogConfig(), "true") .setStorageConfigurationInfo(realmConfig, storageConfigModel, storageLocation) .build() - .asCatalog(serviceIdentityRegistry))); + .asCatalog(serviceIdentityProvider))); this.fileIOFactory = new DefaultFileIOFactory(storageCredentialCache, metaStoreManagerFactory); @@ -1311,7 +1311,7 @@ public void testUpdateNotificationCreateTableWithLocalFilePrefix() { .setDefaultBaseLocation("file://") .setName(catalogWithoutStorage) .build() - .asCatalog(serviceIdentityRegistry))); + .asCatalog(serviceIdentityProvider))); IcebergCatalog catalog = newIcebergCatalog(catalogWithoutStorage); catalog.initialize( @@ -1361,7 +1361,7 @@ public void testUpdateNotificationCreateTableWithHttpPrefix() { .setDefaultBaseLocation("http://maliciousdomain.com") .setName(catalogName) .build() - .asCatalog(serviceIdentityRegistry))); + .asCatalog(serviceIdentityProvider))); IcebergCatalog catalog = newIcebergCatalog(catalogName); catalog.initialize( @@ -1879,7 +1879,7 @@ public void testDropTableWithPurgeDisabled() { .setStorageConfigurationInfo( realmConfig, noPurgeStorageConfigModel, storageLocation) .build() - .asCatalog(serviceIdentityRegistry))); + .asCatalog(serviceIdentityProvider))); 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(serviceIdentityRegistry))); + .asCatalog(serviceIdentityProvider))); }) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("reserved prefix"); @@ -2152,7 +2152,7 @@ public void updateCatalogWithReservedProperty() { .setName("updateCatalogWithReservedProperty") .setProperties(ImmutableMap.of("a", "b")) .build() - .asCatalog(serviceIdentityRegistry))); + .asCatalog(serviceIdentityProvider))); 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 55e131c08c..6cb593002c 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,7 +49,7 @@ 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.provider.ServiceIdentityProvider; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory; @@ -108,7 +108,7 @@ public Map getConfigOverrides() { @Inject MetaStoreManagerFactory metaStoreManagerFactory; @Inject UserSecretsManagerFactory userSecretsManagerFactory; - @Inject ServiceIdentityRegistry serviceIdentityRegistry; + @Inject ServiceIdentityProvider serviceIdentityProvider; @Inject PolarisConfigurationStore configurationStore; @Inject StorageCredentialCache storageCredentialCache; @Inject PolarisDiagnostics diagServices; @@ -182,7 +182,7 @@ public void before(TestInfo testInfo) { resolutionManifestFactory, metaStoreManager, userSecretsManager, - serviceIdentityRegistry, + serviceIdentityProvider, securityContext, authorizer, reservedProperties); @@ -202,7 +202,7 @@ public void before(TestInfo testInfo) { StorageConfigInfo.StorageTypeEnum.FILE, List.of("file://", "/", "*")), "file://tmp") .build() - .asCatalog(serviceIdentityRegistry))); + .asCatalog(serviceIdentityProvider))); 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 f0c92634a1..78b8e9a417 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(serviceIdentityRegistry))); + .asCatalog(serviceIdentityProvider))); 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 6627a55f20..518dac25ed 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,7 +58,7 @@ 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.provider.ServiceIdentityProvider; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.PolicyMappingAlreadyExistsException; @@ -125,7 +125,7 @@ public abstract class AbstractPolicyCatalogTest { @Inject MetaStoreManagerFactory metaStoreManagerFactory; @Inject UserSecretsManagerFactory userSecretsManagerFactory; - @Inject ServiceIdentityRegistry serviceIdentityRegistry; + @Inject ServiceIdentityProvider serviceIdentityProvider; @Inject PolarisConfigurationStore configurationStore; @Inject StorageCredentialCache storageCredentialCache; @Inject PolarisStorageIntegrationProvider storageIntegrationProvider; @@ -196,7 +196,7 @@ public void before(TestInfo testInfo) { resolutionManifestFactory, metaStoreManager, userSecretsManager, - serviceIdentityRegistry, + serviceIdentityProvider, securityContext, authorizer, reservedProperties); @@ -224,7 +224,7 @@ public void before(TestInfo testInfo) { "true") .setStorageConfigurationInfo(realmConfig, storageConfigModel, storageLocation) .build() - .asCatalog(serviceIdentityRegistry))); + .asCatalog(serviceIdentityProvider))); 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 52d14cd4c8..9bf47f1543 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 @@ -45,7 +45,7 @@ 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.provider.ServiceIdentityProvider; import org.apache.polaris.core.identity.resolved.ResolvedAwsIamServiceIdentity; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -60,14 +60,21 @@ public class CatalogEntityTest { private static final ObjectMapper MAPPER = new ObjectMapper(); private RealmConfig realmConfig; - private ServiceIdentityRegistry serviceIdentityRegistry; + private ServiceIdentityProvider serviceIdentityProvider; @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())) + this.serviceIdentityProvider = Mockito.mock(ServiceIdentityProvider.class); + Mockito.when(serviceIdentityProvider.getServiceIdentityInfo(Mockito.any())) + .thenReturn( + Optional.of( + AwsIamServiceIdentityInfo.builder() + .setIdentityType(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM) + .setIamArn("arn:aws:iam::123456789012:user/test-user") + .build())); + Mockito.when(serviceIdentityProvider.resolveServiceIdentity(Mockito.any())) .thenReturn( Optional.of( new ResolvedAwsIamServiceIdentity("arn:aws:iam::123456789012:user/test-user"))); @@ -296,7 +303,7 @@ public void testCatalogTypeDefaultsToInternal() { .setStorageConfigurationInfo(realmConfig, storageConfigModel, baseLocation) .build(); - Catalog catalog = catalogEntity.asCatalog(serviceIdentityRegistry); + Catalog catalog = catalogEntity.asCatalog(serviceIdentityProvider); assertThat(catalog.getType()).isEqualTo(Catalog.TypeEnum.INTERNAL); } @@ -319,7 +326,7 @@ public void testCatalogTypeExternalPreserved() { .setStorageConfigurationInfo(realmConfig, storageConfigModel, baseLocation) .build(); - Catalog catalog = catalogEntity.asCatalog(serviceIdentityRegistry); + Catalog catalog = catalogEntity.asCatalog(serviceIdentityProvider); assertThat(catalog.getType()).isEqualTo(Catalog.TypeEnum.EXTERNAL); } @@ -342,7 +349,7 @@ public void testCatalogTypeInternalExplicitlySet() { .setStorageConfigurationInfo(realmConfig, storageConfigModel, baseLocation) .build(); - Catalog catalog = catalogEntity.asCatalog(serviceIdentityRegistry); + Catalog catalog = catalogEntity.asCatalog(serviceIdentityProvider); assertThat(catalog.getType()).isEqualTo(Catalog.TypeEnum.INTERNAL); } @@ -380,7 +387,7 @@ public void testAwsConfigRoundTrip(AwsStorageConfigInfo config) throws JsonProce config.getAllowedLocations().getFirst()) .build(); - Catalog catalog = catalogEntity.asCatalog(serviceIdentityRegistry); + Catalog catalog = catalogEntity.asCatalog(serviceIdentityProvider); assertThat(catalog.getStorageConfigInfo()).isEqualTo(config); assertThat(MAPPER.writeValueAsString(catalog.getStorageConfigInfo())).isEqualTo(configStr); } @@ -418,7 +425,7 @@ public void testServiceIdentityInjection() { icebergRestConnectionConfigInfoModel, null, new AwsIamServiceIdentityInfoDpo(null)) .build(); - Catalog catalog = catalogEntity.asCatalog(serviceIdentityRegistry); + Catalog catalog = catalogEntity.asCatalog(serviceIdentityProvider); assertThat(catalog.getType()).isEqualTo(Catalog.TypeEnum.EXTERNAL); ExternalCatalog externalCatalog = (ExternalCatalog) catalog; assertThat(externalCatalog.getConnectionConfigInfo().getConnectionType()) 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/provider/DefaultServiceIdentityProviderTest.java similarity index 91% rename from runtime/service/src/test/java/org/apache/polaris/service/identity/registry/DefaultServiceIdentityRegistryTest.java rename to runtime/service/src/test/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProviderTest.java index c5553276e6..3f62023f2a 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/provider/DefaultServiceIdentityProviderTest.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.polaris.service.identity.registry; +package org.apache.polaris.service.identity.provider; import io.quarkus.test.InjectMock; import io.quarkus.test.junit.QuarkusTest; @@ -42,8 +42,8 @@ import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; @QuarkusTest -@TestProfile(DefaultServiceIdentityRegistryTest.Profile.class) -public class DefaultServiceIdentityRegistryTest { +@TestProfile(DefaultServiceIdentityProviderTest.Profile.class) +public class DefaultServiceIdentityProviderTest { private static final String DEFAULT_REALM_KEY = ServiceIdentityConfiguration.DEFAULT_REALM_KEY; private static final String MY_REALM_KEY = "my-realm"; @@ -54,7 +54,7 @@ public static class Profile implements QuarkusTestProfile { @Override public Map getConfigOverrides() { return Map.of( - "quarkus.identity-registry.type", + "quarkus.identity-provider.type", "default", "polaris.service-identity.aws-iam.iam-arn", "arn:aws:iam::123456789012:user/polaris-default-iam-user", @@ -123,10 +123,10 @@ void testServiceIdentityConfiguration() { void testRealmServiceIdentityConfigToResolvedServiceIdentity() { // Check the default realm Mockito.when(realmContext.getRealmIdentifier()).thenReturn(DEFAULT_REALM_KEY); - DefaultServiceIdentityRegistry defaultRegistry = - new DefaultServiceIdentityRegistry(realmContext, serviceIdentityConfiguration); + DefaultServiceIdentityProvider defaultProvider = + new DefaultServiceIdentityProvider(realmContext, serviceIdentityConfiguration); EnumMap resolvedIdentities = - defaultRegistry.getResolvedServiceIdentities(); + defaultProvider.getResolvedServiceIdentities(); Assertions.assertThat(resolvedIdentities) .containsKey(ServiceIdentityType.AWS_IAM) @@ -139,7 +139,7 @@ void testRealmServiceIdentityConfigToResolvedServiceIdentity() { Assertions.assertThat(resolvedAwsIamServiceIdentity.getIdentityInfoReference()) .isEqualTo( new ServiceSecretReference( - "urn:polaris-secret:default-identity-registry:system:default:AWS_IAM", Map.of())); + "urn:polaris-secret:default-identity-provider:system:default:AWS_IAM", Map.of())); Assertions.assertThat( resolvedAwsIamServiceIdentity.getAwsCredentialsProvider() instanceof DefaultCredentialsProvider) @@ -147,9 +147,9 @@ void testRealmServiceIdentityConfigToResolvedServiceIdentity() { // Check the my-realm Mockito.when(realmContext.getRealmIdentifier()).thenReturn(MY_REALM_KEY); - DefaultServiceIdentityRegistry myRealmRegistry = - new DefaultServiceIdentityRegistry(realmContext, serviceIdentityConfiguration); - resolvedIdentities = myRealmRegistry.getResolvedServiceIdentities(); + DefaultServiceIdentityProvider myRealmProvider = + new DefaultServiceIdentityProvider(realmContext, serviceIdentityConfiguration); + resolvedIdentities = myRealmProvider.getResolvedServiceIdentities(); Assertions.assertThat(resolvedIdentities) .containsKey(ServiceIdentityType.AWS_IAM) @@ -162,7 +162,7 @@ void testRealmServiceIdentityConfigToResolvedServiceIdentity() { Assertions.assertThat(resolvedAwsIamServiceIdentity.getIdentityInfoReference()) .isEqualTo( new ServiceSecretReference( - "urn:polaris-secret:default-identity-registry:my-realm:AWS_IAM", Map.of())); + "urn:polaris-secret:default-identity-provider:my-realm:AWS_IAM", Map.of())); Assertions.assertThat( resolvedAwsIamServiceIdentity.getAwsCredentialsProvider() instanceof StaticCredentialsProvider) @@ -180,9 +180,9 @@ void testRealmServiceIdentityConfigToResolvedServiceIdentity() { // Check the other realm which does not exist in the configuration, should fallback to default Mockito.when(realmContext.getRealmIdentifier()).thenReturn("other-realm"); - DefaultServiceIdentityRegistry otherRegistry = - new DefaultServiceIdentityRegistry(realmContext, serviceIdentityConfiguration); - resolvedIdentities = otherRegistry.getResolvedServiceIdentities(); + DefaultServiceIdentityProvider otherProvider = + new DefaultServiceIdentityProvider(realmContext, serviceIdentityConfiguration); + resolvedIdentities = otherProvider.getResolvedServiceIdentities(); Assertions.assertThat(resolvedIdentities) .containsKey(ServiceIdentityType.AWS_IAM) .size() @@ -194,7 +194,7 @@ void testRealmServiceIdentityConfigToResolvedServiceIdentity() { Assertions.assertThat(resolvedAwsIamServiceIdentity.getIdentityInfoReference()) .isEqualTo( new ServiceSecretReference( - "urn:polaris-secret:default-identity-registry:system:default:AWS_IAM", Map.of())); + "urn:polaris-secret:default-identity-provider:system:default:AWS_IAM", Map.of())); Assertions.assertThat( resolvedAwsIamServiceIdentity.getAwsCredentialsProvider() instanceof DefaultCredentialsProvider) 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..8e4ca1111f 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 @@ -46,7 +46,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.ServiceIdentityRegistry; +import org.apache.polaris.core.identity.provider.ServiceIdentityProvider; import org.apache.polaris.core.persistence.BasePersistence; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; @@ -75,7 +75,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.DefaultServiceIdentityRegistry; +import org.apache.polaris.service.identity.provider.DefaultServiceIdentityProvider; import org.apache.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory; import org.apache.polaris.service.secrets.UnsafeInMemorySecretsManagerFactory; import org.apache.polaris.service.storage.PolarisStorageIntegrationProviderImpl; @@ -218,7 +218,7 @@ public TestServices build() { new ResolutionManifestFactoryImpl(diagnostics, resolverFactory); UserSecretsManager userSecretsManager = userSecretsManagerFactory.getOrCreateUserSecretsManager(realmContext); - ServiceIdentityRegistry serviceIdentityRegistry = new DefaultServiceIdentityRegistry(); + ServiceIdentityProvider serviceIdentityProvider = new DefaultServiceIdentityProvider(); FileIOFactory fileIOFactory = fileIOFactorySupplier.apply(storageCredentialCache, metaStoreManagerFactory); @@ -307,14 +307,14 @@ public String getAuthenticationScheme() { resolutionManifestFactory, metaStoreManager, userSecretsManager, - serviceIdentityRegistry, + serviceIdentityProvider, securityContext, authorizer, reservedProperties); PolarisCatalogsApi catalogsApi = new PolarisCatalogsApi( new PolarisServiceImpl( - realmConfig, reservedProperties, adminService, serviceIdentityRegistry)); + realmConfig, reservedProperties, adminService, serviceIdentityProvider)); return new TestServices( clock, From 8d46702fed0b2dc028b0954a987ae6ca322ebdd9 Mon Sep 17 00:00:00 2001 From: Rulin Xing Date: Fri, 3 Oct 2025 02:16:22 -0700 Subject: [PATCH 08/12] Rename ResolvedServiceIdentity to ServiceIdentityCredential --- .../AwsIamServiceIdentityCredential.java} | 37 +++++---- .../ServiceIdentityCredential.java} | 41 +++++++--- .../dpo/AwsIamServiceIdentityInfoDpo.java | 17 ++-- .../identity/dpo/ServiceIdentityInfoDpo.java | 18 +++-- .../provider/ServiceIdentityProvider.java | 22 +++--- .../ConnectionConfigInfoDpoTest.java | 6 +- .../AwsIamServiceIdentityConfiguration.java | 18 +++-- ...esolvableServiceIdentityConfiguration.java | 18 +++-- .../ServiceIdentityConfiguration.java | 54 ++++++++++--- .../DefaultServiceIdentityProvider.java | 77 ++++++++++--------- .../service/entity/CatalogEntityTest.java | 6 +- .../DefaultServiceIdentityProviderTest.java | 55 +++++++------ 12 files changed, 225 insertions(+), 144 deletions(-) rename polaris-core/src/main/java/org/apache/polaris/core/identity/{resolved/ResolvedAwsIamServiceIdentity.java => credential/AwsIamServiceIdentityCredential.java} (71%) rename polaris-core/src/main/java/org/apache/polaris/core/identity/{resolved/ResolvedServiceIdentity.java => credential/ServiceIdentityCredential.java} (59%) 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/credential/AwsIamServiceIdentityCredential.java similarity index 71% rename from polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedAwsIamServiceIdentity.java rename to polaris-core/src/main/java/org/apache/polaris/core/identity/credential/AwsIamServiceIdentityCredential.java index 4abd6e12ea..0a97601124 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/credential/AwsIamServiceIdentityCredential.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.core.identity.resolved; +package org.apache.polaris.core.identity.credential; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; @@ -28,23 +28,30 @@ import org.apache.polaris.core.secrets.ServiceSecretReference; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; -import software.amazon.awssdk.services.sts.StsClient; /** - * Represents a fully resolved AWS IAM service identity, including the associated IAM ARN and - * credentials. Polaris uses this class internally to access AWS services on behalf of a configured - * service identity. + * Represents an AWS IAM service identity credential used by Polaris to authenticate to AWS + * services. * - *

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. + *

This credential encapsulates: * - *

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

    + *
  • The IAM ARN (role or user) representing the Polaris service identity + *
  • An {@link AwsCredentialsProvider} that supplies AWS credentials (access key, secret key, + * and optional session token) + *
* - *

The resolved identity can also be converted into its API model representation using {@link - * #asServiceIdentityInfoModel()} + *

Polaris uses this identity to assume customer-provided IAM roles when accessing remote + * catalogs with SigV4 authentication. The {@link AwsCredentialsProvider} can be configured to use + * either: + * + *

    + *
  • Static credentials (for testing or single-tenant deployments) + *
  • DefaultCredentialsProvider (which chains through various AWS credential sources) + *
  • Custom credential providers (for vendor-specific secret management) + *
*/ -public class ResolvedAwsIamServiceIdentity extends ResolvedServiceIdentity { +public class AwsIamServiceIdentityCredential extends ServiceIdentityCredential { /** IAM role or user ARN representing the Polaris service identity. */ private final String iamArn; @@ -52,16 +59,16 @@ public class ResolvedAwsIamServiceIdentity extends ResolvedServiceIdentity { /** AWS credentials provider for accessing AWS services. */ private final AwsCredentialsProvider awsCredentialsProvider; - public ResolvedAwsIamServiceIdentity(@Nullable String iamArn) { + public AwsIamServiceIdentityCredential(@Nullable String iamArn) { this(null, iamArn, DefaultCredentialsProvider.builder().build()); } - public ResolvedAwsIamServiceIdentity( + public AwsIamServiceIdentityCredential( @Nullable String iamArn, @Nonnull AwsCredentialsProvider awsCredentialsProvider) { this(null, iamArn, awsCredentialsProvider); } - public ResolvedAwsIamServiceIdentity( + public AwsIamServiceIdentityCredential( @Nullable ServiceSecretReference serviceSecretReference, @Nullable String iamArn, @Nonnull AwsCredentialsProvider awsCredentialsProvider) { 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/credential/ServiceIdentityCredential.java similarity index 59% rename from polaris-core/src/main/java/org/apache/polaris/core/identity/resolved/ResolvedServiceIdentity.java rename to polaris-core/src/main/java/org/apache/polaris/core/identity/credential/ServiceIdentityCredential.java index 342d75b20f..b7428cb439 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/credential/ServiceIdentityCredential.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.polaris.core.identity.resolved; +package org.apache.polaris.core.identity.credential; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; @@ -27,21 +27,29 @@ import software.amazon.awssdk.annotations.NotNull; /** - * Represents a resolved service identity. + * Represents a service identity credential used by Polaris to authenticate to external systems. * - *

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. + *

This class encapsulates both the service identity metadata (e.g., AWS IAM ARN) and the + * associated credentials (e.g., AWS access keys) needed to authenticate as the Polaris service when + * accessing external catalog services. + * + *

The credential contains: + * + *

    + *
  • Identity type (e.g., AWS_IAM) + *
  • A reference to where the credential is stored (for persistence) + *
  • The actual authentication credentials (implementation-specific) + *
*/ -public abstract class ResolvedServiceIdentity { +public abstract class ServiceIdentityCredential { private final ServiceIdentityType identityType; private ServiceSecretReference identityInfoReference; - public ResolvedServiceIdentity(@Nonnull ServiceIdentityType identityType) { + public ServiceIdentityCredential(@Nonnull ServiceIdentityType identityType) { this(identityType, null); } - public ResolvedServiceIdentity( + public ServiceIdentityCredential( @Nonnull ServiceIdentityType identityType, @Nullable ServiceSecretReference identityInfoReference) { this.identityType = identityType; @@ -60,8 +68,23 @@ public void setIdentityInfoReference(@NotNull ServiceSecretReference identityInf this.identityInfoReference = identityInfoReference; } - /** Converts this resolved identity into its corresponding persisted form (DPO). */ + /** + * Converts this service identity credential into its corresponding persisted form (DPO). + * + *

The DPO contains only a reference to the credential, not the credential itself, as the + * actual secrets are managed externally. + * + * @return The persistence object representation + */ public abstract @Nonnull ServiceIdentityInfoDpo asServiceIdentityInfoDpo(); + /** + * Converts this service identity credential into its API model representation. + * + *

The model contains identity information (e.g., IAM ARN) but excludes sensitive credentials + * such as access keys or session tokens. + * + * @return The API model representation for client responses + */ public abstract @Nonnull ServiceIdentityInfo asServiceIdentityInfoModel(); } 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 cc8066b496..cd15addb87 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 @@ -29,16 +29,17 @@ /** * Persistence-layer representation of an AWS IAM service identity used by Polaris. * - *

This class models an AWS IAM identity (either a user or role) and extends {@link - * ServiceIdentityInfoDpo}. It is typically used internally to store a reference to the actual - * credential (e.g., via {@link SecretReference}). + *

This class stores only a {@link SecretReference} pointing to where the actual AWS credentials + * are managed (e.g., in a secret manager or configuration store). The credentials themselves are + * not persisted in this object. * - *

During the runtime, it will be resolved to an actual ResolvedAwsIamServiceIdentityInfo object - * which contains the actual service identity info (e.g., the IAM user arn) and the corresponding - * credential. + *

At runtime, the reference can be used to retrieve the full {@link + * org.apache.polaris.core.identity.credential.AwsIamServiceIdentityCredential} which contains both + * the identity metadata (e.g., IAM ARN) and the actual AWS credentials needed for authentication. * - *

Instances of this class are convertible to the public API model {@link - * AwsIamServiceIdentityInfo}. + *

Instances of this class can be converted to the public API model {@link + * AwsIamServiceIdentityInfo} via a {@link + * org.apache.polaris.core.identity.provider.ServiceIdentityProvider}. */ public class AwsIamServiceIdentityInfoDpo extends ServiceIdentityInfoDpo { 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 b089cb8d73..d176cac7e4 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 @@ -33,8 +33,10 @@ * The internal persistence-object counterpart to ServiceIdentityInfo defined in the API model. * Important: JsonSubTypes must be kept in sync with {@link ServiceIdentityType}. * - *

During the runtime, it will be resolved to an actual ResolvedServiceIdentityInfo object which - * contains the actual service identity info and the corresponding credential. + *

This DPO stores only a reference to the service identity credential, not the actual secrets. + * The reference can be used at runtime to retrieve the full {@link + * org.apache.polaris.core.identity.credential.ServiceIdentityCredential} with credentials through a + * {@link org.apache.polaris.core.identity.provider.ServiceIdentityProvider}. */ @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -72,10 +74,16 @@ 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 + * Converts this persistence object to the corresponding API model. * - * @param serviceIdentityProvider the service identity provider to resolve the identity + *

The conversion uses the provided {@link ServiceIdentityProvider} to retrieve the user-facing + * identity information (e.g., AWS IAM ARN) without exposing sensitive credentials. The credential + * reference stored in this DPO is not included in the API model. + * + * @param serviceIdentityProvider the service identity provider used to retrieve display + * information + * @return the API model representation, or null if the provider is null or cannot resolve the + * identity */ public @Nullable ServiceIdentityInfo asServiceIdentityInfoModel( ServiceIdentityProvider serviceIdentityProvider) { diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/provider/ServiceIdentityProvider.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/provider/ServiceIdentityProvider.java index 50dd0d2ec7..c652ddbe13 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/identity/provider/ServiceIdentityProvider.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/provider/ServiceIdentityProvider.java @@ -19,13 +19,12 @@ package org.apache.polaris.core.identity.provider; -import java.util.Optional; - import jakarta.annotation.Nonnull; +import java.util.Optional; import org.apache.polaris.core.admin.model.ConnectionConfigInfo; import org.apache.polaris.core.admin.model.ServiceIdentityInfo; +import org.apache.polaris.core.identity.credential.ServiceIdentityCredential; import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; -import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; /** * A provider interface for managing and resolving service identities in Polaris. @@ -70,7 +69,8 @@ public interface ServiceIdentityProvider { * @return An {@link Optional} containing the allocated {@link ServiceIdentityInfoDpo}, or empty * if no service identity is available or applicable for this connection. */ - Optional allocateServiceIdentity(@Nonnull ConnectionConfigInfo connectionConfig); + Optional allocateServiceIdentity( + @Nonnull ConnectionConfigInfo connectionConfig); /** * Retrieves the user-facing {@link ServiceIdentityInfo} model for the given service identity @@ -83,19 +83,21 @@ public interface ServiceIdentityProvider { * @return An {@link Optional} containing the {@link ServiceIdentityInfo} model for API responses, * or empty if the identity cannot be resolved. */ - Optional getServiceIdentityInfo(@Nonnull ServiceIdentityInfoDpo serviceIdentityInfo); + Optional getServiceIdentityInfo( + @Nonnull ServiceIdentityInfoDpo serviceIdentityInfo); /** - * Resolves the given service identity by retrieving the actual credential or secret referenced by - * it, typically from a secret manager or internal credential store. + * Retrieves the service identity credential by resolving the actual credential or secret + * referenced by the given service identity info, typically from a secret manager or internal + * credential store. * *

This method is used when Polaris needs to authenticate to external systems using the service * identity, such as when signing requests with SigV4 authentication. * * @param serviceIdentityInfo The service identity metadata to resolve. - * @return An {@link Optional} containing a {@link ResolvedServiceIdentity} with credentials and - * other resolved data, or empty if the identity cannot be resolved. + * @return An {@link Optional} containing a {@link ServiceIdentityCredential} with credentials, or + * empty if the identity cannot be resolved. */ - Optional resolveServiceIdentity( + Optional getServiceIdentityCredential( @Nonnull ServiceIdentityInfoDpo serviceIdentityInfo); } 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 82e2c39000..5d8dbc7d48 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 @@ -26,8 +26,8 @@ import org.apache.polaris.core.admin.model.AwsIamServiceIdentityInfo; import org.apache.polaris.core.admin.model.ConnectionConfigInfo; import org.apache.polaris.core.admin.model.ServiceIdentityInfo; +import org.apache.polaris.core.identity.credential.AwsIamServiceIdentityCredential; import org.apache.polaris.core.identity.provider.ServiceIdentityProvider; -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; @@ -52,10 +52,10 @@ void setUp() { .setIdentityType(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM) .setIamArn("arn:aws:iam::123456789012:user/test-user") .build())); - Mockito.when(serviceIdentityProvider.resolveServiceIdentity(Mockito.any())) + Mockito.when(serviceIdentityProvider.getServiceIdentityCredential(Mockito.any())) .thenReturn( Optional.of( - new ResolvedAwsIamServiceIdentity("arn:aws:iam::123456789012:user/test-user"))); + new AwsIamServiceIdentityCredential("arn:aws:iam::123456789012:user/test-user"))); } @Test 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 8116f58ef7..48e0a8d652 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 @@ -22,7 +22,7 @@ import jakarta.annotation.Nonnull; import java.util.Optional; import org.apache.polaris.core.identity.ServiceIdentityType; -import org.apache.polaris.core.identity.resolved.ResolvedAwsIamServiceIdentity; +import org.apache.polaris.core.identity.credential.AwsIamServiceIdentityCredential; import org.apache.polaris.core.secrets.ServiceSecretReference; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; @@ -35,7 +35,7 @@ * *

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. + * AwsIamServiceIdentityCredential}; otherwise, the AWS default credential provider chain is used. */ public interface AwsIamServiceIdentityConfiguration extends ResolvableServiceIdentityConfiguration { @@ -72,19 +72,23 @@ default ServiceIdentityType getType() { } /** - * Resolves this configuration into a {@link ResolvedAwsIamServiceIdentity} if the IAM ARN is - * present. + * Converts this configuration into a {@link AwsIamServiceIdentityCredential}. * - * @return the resolved identity, or an empty optional if the ARN is missing + *

Creates a credential object containing the configured IAM ARN and AWS credentials provider. + * The credentials provider is constructed based on whether static credentials (access key, secret + * key, session token) are configured or whether to use the default AWS credential chain. + * + * @param serviceIdentityReference the reference to associate with this credential + * @return the service identity credential, or empty if the IAM ARN is not configured */ @Override - default Optional resolve( + default Optional resolve( @Nonnull ServiceSecretReference serviceIdentityReference) { if (iamArn() == null) { return Optional.empty(); } else { return Optional.of( - new ResolvedAwsIamServiceIdentity( + new AwsIamServiceIdentityCredential( 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 dd3ef9ce3b..697e052ac8 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 @@ -22,12 +22,12 @@ 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.identity.credential.ServiceIdentityCredential; import org.apache.polaris.core.secrets.ServiceSecretReference; /** - * Represents a service identity configuration that can be resolved into a fully initialized {@link - * ResolvedServiceIdentity}. + * Represents a service identity configuration that can be converted into a fully initialized {@link + * ServiceIdentityCredential}. * *

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 @@ -44,12 +44,16 @@ default ServiceIdentityType getType() { } /** - * Attempts to resolve this configuration into a {@link ResolvedServiceIdentity}. + * Converts this configuration into a {@link ServiceIdentityCredential} with actual credentials. * - * @return an optional resolved service identity, or empty if resolution fails or is not - * configured + *

Implementations should construct the appropriate credential object (e.g., {@link + * org.apache.polaris.core.identity.credential.AwsIamServiceIdentityCredential}) using the + * configured values and the provided secret reference. + * + * @param serviceIdentityReference the reference to associate with this credential for persistence + * @return an optional service identity credential, or empty if required configuration is missing */ - default Optional resolve( + 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 98888a426b..a9363a6a4f 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 @@ -27,15 +27,23 @@ import java.util.Map; import java.util.Optional; import org.apache.polaris.core.context.RealmContext; -import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; +import org.apache.polaris.core.identity.credential.ServiceIdentityCredential; import org.apache.polaris.service.identity.provider.DefaultServiceIdentityProvider; /** - * Represents the service identity configuration for one or more realms. + * Configuration interface for managing service identities across multiple realms in Polaris. * - *

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. + *

A service identity represents the Polaris service itself when it needs to authenticate to + * external systems (e.g., AWS services for SigV4 authentication). Each realm can configure its own + * set of service identities for different cloud providers. + * + *

This interface supports multi-tenant deployments where each realm (tenant) can have distinct + * service identities, as well as single-tenant deployments with a default configuration shared + * across all catalogs. + * + *

Configuration is loaded from {@code polaris.service-identity.*} properties at startup and + * includes credentials that Polaris uses to assume customer-provided roles when accessing federated + * catalogs. */ @ConfigMapping(prefix = "polaris.service-identity") public interface ServiceIdentityConfiguration { @@ -58,16 +66,24 @@ public interface ServiceIdentityConfiguration { Map realms(); /** - * 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. + * Retrieves the configuration entry for the given realm context. + * + *

If the realm has no specific configuration, falls back to the default realm configuration. + * + * @param realmContext the realm context + * @return the configuration entry containing the realm identifier and its configuration */ default RealmConfigEntry forRealm(RealmContext realmContext) { return forRealm(realmContext.getRealmIdentifier()); } /** - * 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. + * Retrieves the configuration entry for the given realm identifier. + * + *

If the realm has no specific configuration, falls back to the default realm configuration. + * + * @param realmIdentifier the realm identifier + * @return the configuration entry containing the realm identifier and its configuration */ default RealmConfigEntry forRealm(String realmIdentifier) { String resolvedRealmIdentifier = @@ -76,9 +92,17 @@ default RealmConfigEntry forRealm(String realmIdentifier) { } /** - * Resolves and returns the list of {@link ResolvedServiceIdentity} objects for the given realm. + * Loads and returns the list of {@link ServiceIdentityCredential} objects configured for the + * given realm. + * + *

This method retrieves the realm's configuration, builds credential references for each + * configured service identity, and constructs the corresponding {@link ServiceIdentityCredential} + * objects with their credentials. + * + * @param realmContext the realm context for which to load service identities + * @return a list of service identity credentials configured for the realm */ - default List resolveServiceIdentities( + default List resolveServiceIdentityCredentials( RealmContext realmContext) { RealmConfigEntry entry = forRealm(realmContext); @@ -92,6 +116,12 @@ default List resolveServiceIdentities( .toList(); } - /** A pairing of a resolved realm identifier and its associated configuration. */ + /** + * A pairing of a realm identifier and its associated service identity configuration. + * + * @param realm the realm identifier (may be the default if the requested realm was not + * configured) + * @param config the service identity configuration for this realm + */ record RealmConfigEntry(String realm, RealmServiceIdentityConfiguration config) {} } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProvider.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProvider.java index c59c3e3b89..f50327f963 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProvider.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProvider.java @@ -31,26 +31,31 @@ import org.apache.polaris.core.admin.model.ServiceIdentityInfo; import org.apache.polaris.core.context.RealmContext; import org.apache.polaris.core.identity.ServiceIdentityType; +import org.apache.polaris.core.identity.credential.ServiceIdentityCredential; import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; import org.apache.polaris.core.identity.provider.ServiceIdentityProvider; -import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; import org.apache.polaris.core.secrets.ServiceSecretReference; import org.apache.polaris.service.identity.ServiceIdentityConfiguration; /** - * Default implementation of {@link ServiceIdentityProvider} that resolves service identities from - * statically configured values (typically defined via Quarkus server configuration). + * Default implementation of {@link ServiceIdentityProvider} that provides service identity + * credentials from statically configured values. * - *

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

This implementation loads service identity configurations at startup from Quarkus application + * properties and maintains them in memory. It supports both multi-tenant and single-tenant + * 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. + *
  • Multi-tenant mode: Each realm can define its own service identities. When allocating + * an identity to a catalog, the provider selects the appropriate identity based on the + * catalog's realm and authentication type. + *
  • Single-tenant mode: A single default set of service identities is used for all + * catalogs. *
+ * + *

All service identities must be configured before server startup. This implementation does not + * support dynamic credential rotation or runtime identity registration. Vendors requiring such + * functionality should implement a custom {@link ServiceIdentityProvider}. */ public class DefaultServiceIdentityProvider implements ServiceIdentityProvider { public static final String DEFAULT_REALM_KEY = ServiceIdentityConfiguration.DEFAULT_REALM_KEY; @@ -58,20 +63,20 @@ public class DefaultServiceIdentityProvider implements ServiceIdentityProvider { private static final String IDENTITY_INFO_REFERENCE_URN_FORMAT = "urn:polaris-secret:default-identity-provider:%s:%s"; - /** Map of service identity types to their resolved identities. */ - private final EnumMap resolvedServiceIdentities; + /** Map of service identity types to their credentials. */ + private final EnumMap serviceIdentityCredentials; - /** Map of identity info references (URNs) to their resolved service identities. */ - private final Map referenceToResolvedServiceIdentity; + /** Map of identity info references (URNs) to their service identity credentials. */ + private final Map referenceToServiceIdentityCredential; public DefaultServiceIdentityProvider() { this(new EnumMap<>(ServiceIdentityType.class)); } public DefaultServiceIdentityProvider( - EnumMap serviceIdentities) { - this.resolvedServiceIdentities = serviceIdentities; - this.referenceToResolvedServiceIdentity = + EnumMap serviceIdentities) { + this.serviceIdentityCredentials = serviceIdentities; + this.referenceToServiceIdentityCredential = serviceIdentities.values().stream() .collect( Collectors.toMap( @@ -82,18 +87,18 @@ public DefaultServiceIdentityProvider( @Inject public DefaultServiceIdentityProvider( RealmContext realmContext, ServiceIdentityConfiguration serviceIdentityConfiguration) { - this.resolvedServiceIdentities = - serviceIdentityConfiguration.resolveServiceIdentities(realmContext).stream() + this.serviceIdentityCredentials = + serviceIdentityConfiguration.resolveServiceIdentityCredentials(realmContext).stream() .collect( // Collect to an EnumMap, grouping by ServiceIdentityType Collectors.toMap( - ResolvedServiceIdentity::getIdentityType, + ServiceIdentityCredential::getIdentityType, identity -> identity, (a, b) -> b, () -> new EnumMap<>(ServiceIdentityType.class))); - this.referenceToResolvedServiceIdentity = - resolvedServiceIdentities.values().stream() + this.referenceToServiceIdentityCredential = + serviceIdentityCredentials.values().stream() .collect( Collectors.toMap( identity -> identity.getIdentityInfoReference().getUrn(), @@ -121,38 +126,38 @@ public Optional allocateServiceIdentity( return Optional.empty(); } - ResolvedServiceIdentity resolvedServiceIdentity = - resolvedServiceIdentities.get(serviceIdentityType); - if (resolvedServiceIdentity == null) { + ServiceIdentityCredential serviceIdentityCredential = + serviceIdentityCredentials.get(serviceIdentityType); + if (serviceIdentityCredential == null) { return Optional.empty(); } - return Optional.of(resolvedServiceIdentity.asServiceIdentityInfoDpo()); + return Optional.of(serviceIdentityCredential.asServiceIdentityInfoDpo()); } @Override public Optional getServiceIdentityInfo( @Nonnull ServiceIdentityInfoDpo serviceIdentityInfo) { - ResolvedServiceIdentity resolvedServiceIdentity = - referenceToResolvedServiceIdentity.get( + ServiceIdentityCredential serviceIdentityCredential = + referenceToServiceIdentityCredential.get( serviceIdentityInfo.getIdentityInfoReference().getUrn()); - if (resolvedServiceIdentity == null) { + if (serviceIdentityCredential == null) { return Optional.empty(); } - return Optional.of(resolvedServiceIdentity.asServiceIdentityInfoModel()); + return Optional.of(serviceIdentityCredential.asServiceIdentityInfoModel()); } @Override - public Optional resolveServiceIdentity( + public Optional getServiceIdentityCredential( @Nonnull ServiceIdentityInfoDpo serviceIdentityInfo) { - ResolvedServiceIdentity resolvedServiceIdentity = - referenceToResolvedServiceIdentity.get( + ServiceIdentityCredential serviceIdentityCredential = + referenceToServiceIdentityCredential.get( serviceIdentityInfo.getIdentityInfoReference().getUrn()); - return Optional.ofNullable(resolvedServiceIdentity); + return Optional.ofNullable(serviceIdentityCredential); } @VisibleForTesting - public EnumMap getResolvedServiceIdentities() { - return resolvedServiceIdentities; + public EnumMap getServiceIdentityCredentials() { + return serviceIdentityCredentials; } /** 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 9bf47f1543..47e61f5734 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 @@ -44,9 +44,9 @@ 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.credential.AwsIamServiceIdentityCredential; import org.apache.polaris.core.identity.dpo.AwsIamServiceIdentityInfoDpo; import org.apache.polaris.core.identity.provider.ServiceIdentityProvider; -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; @@ -74,10 +74,10 @@ public void setup() { .setIdentityType(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM) .setIamArn("arn:aws:iam::123456789012:user/test-user") .build())); - Mockito.when(serviceIdentityProvider.resolveServiceIdentity(Mockito.any())) + Mockito.when(serviceIdentityProvider.getServiceIdentityCredential(Mockito.any())) .thenReturn( Optional.of( - new ResolvedAwsIamServiceIdentity("arn:aws:iam::123456789012:user/test-user"))); + new AwsIamServiceIdentityCredential("arn:aws:iam::123456789012:user/test-user"))); } @ParameterizedTest diff --git a/runtime/service/src/test/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProviderTest.java b/runtime/service/src/test/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProviderTest.java index 3f62023f2a..97f0f2f98b 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProviderTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProviderTest.java @@ -29,8 +29,8 @@ import java.util.Optional; import org.apache.polaris.core.context.RealmContext; import org.apache.polaris.core.identity.ServiceIdentityType; -import org.apache.polaris.core.identity.resolved.ResolvedAwsIamServiceIdentity; -import org.apache.polaris.core.identity.resolved.ResolvedServiceIdentity; +import org.apache.polaris.core.identity.credential.AwsIamServiceIdentityCredential; +import org.apache.polaris.core.identity.credential.ServiceIdentityCredential; import org.apache.polaris.core.secrets.ServiceSecretReference; import org.apache.polaris.service.identity.RealmServiceIdentityConfiguration; import org.apache.polaris.service.identity.ServiceIdentityConfiguration; @@ -120,55 +120,53 @@ void testServiceIdentityConfiguration() { } @Test - void testRealmServiceIdentityConfigToResolvedServiceIdentity() { + void testRealmServiceIdentityConfigToServiceIdentityCredential() { // Check the default realm Mockito.when(realmContext.getRealmIdentifier()).thenReturn(DEFAULT_REALM_KEY); DefaultServiceIdentityProvider defaultProvider = new DefaultServiceIdentityProvider(realmContext, serviceIdentityConfiguration); - EnumMap resolvedIdentities = - defaultProvider.getResolvedServiceIdentities(); + EnumMap identityCredentials = + defaultProvider.getServiceIdentityCredentials(); - Assertions.assertThat(resolvedIdentities) + Assertions.assertThat(identityCredentials) .containsKey(ServiceIdentityType.AWS_IAM) .size() .isEqualTo(1); - ResolvedAwsIamServiceIdentity resolvedAwsIamServiceIdentity = - (ResolvedAwsIamServiceIdentity) resolvedIdentities.get(ServiceIdentityType.AWS_IAM); - Assertions.assertThat(resolvedAwsIamServiceIdentity.getIamArn()) + AwsIamServiceIdentityCredential awsIamCredential = + (AwsIamServiceIdentityCredential) identityCredentials.get(ServiceIdentityType.AWS_IAM); + Assertions.assertThat(awsIamCredential.getIamArn()) .isEqualTo("arn:aws:iam::123456789012:user/polaris-default-iam-user"); - Assertions.assertThat(resolvedAwsIamServiceIdentity.getIdentityInfoReference()) + Assertions.assertThat(awsIamCredential.getIdentityInfoReference()) .isEqualTo( new ServiceSecretReference( "urn:polaris-secret:default-identity-provider:system:default:AWS_IAM", Map.of())); Assertions.assertThat( - resolvedAwsIamServiceIdentity.getAwsCredentialsProvider() - instanceof DefaultCredentialsProvider) + awsIamCredential.getAwsCredentialsProvider() instanceof DefaultCredentialsProvider) .isTrue(); // Check the my-realm Mockito.when(realmContext.getRealmIdentifier()).thenReturn(MY_REALM_KEY); DefaultServiceIdentityProvider myRealmProvider = new DefaultServiceIdentityProvider(realmContext, serviceIdentityConfiguration); - resolvedIdentities = myRealmProvider.getResolvedServiceIdentities(); + identityCredentials = myRealmProvider.getServiceIdentityCredentials(); - Assertions.assertThat(resolvedIdentities) + Assertions.assertThat(identityCredentials) .containsKey(ServiceIdentityType.AWS_IAM) .size() .isEqualTo(1); - resolvedAwsIamServiceIdentity = - (ResolvedAwsIamServiceIdentity) resolvedIdentities.get(ServiceIdentityType.AWS_IAM); - Assertions.assertThat(resolvedAwsIamServiceIdentity.getIamArn()) + awsIamCredential = + (AwsIamServiceIdentityCredential) identityCredentials.get(ServiceIdentityType.AWS_IAM); + Assertions.assertThat(awsIamCredential.getIamArn()) .isEqualTo("arn:aws:iam::123456789012:user/polaris-iam-user"); - Assertions.assertThat(resolvedAwsIamServiceIdentity.getIdentityInfoReference()) + Assertions.assertThat(awsIamCredential.getIdentityInfoReference()) .isEqualTo( new ServiceSecretReference( "urn:polaris-secret:default-identity-provider:my-realm:AWS_IAM", Map.of())); Assertions.assertThat( - resolvedAwsIamServiceIdentity.getAwsCredentialsProvider() - instanceof StaticCredentialsProvider) + awsIamCredential.getAwsCredentialsProvider() instanceof StaticCredentialsProvider) .isTrue(); StaticCredentialsProvider staticCredentialsProvider = - (StaticCredentialsProvider) resolvedAwsIamServiceIdentity.getAwsCredentialsProvider(); + (StaticCredentialsProvider) awsIamCredential.getAwsCredentialsProvider(); Assertions.assertThat( staticCredentialsProvider.resolveCredentials() instanceof AwsSessionCredentials) .isTrue(); @@ -182,22 +180,21 @@ void testRealmServiceIdentityConfigToResolvedServiceIdentity() { Mockito.when(realmContext.getRealmIdentifier()).thenReturn("other-realm"); DefaultServiceIdentityProvider otherProvider = new DefaultServiceIdentityProvider(realmContext, serviceIdentityConfiguration); - resolvedIdentities = otherProvider.getResolvedServiceIdentities(); - Assertions.assertThat(resolvedIdentities) + identityCredentials = otherProvider.getServiceIdentityCredentials(); + Assertions.assertThat(identityCredentials) .containsKey(ServiceIdentityType.AWS_IAM) .size() .isEqualTo(1); - resolvedAwsIamServiceIdentity = - (ResolvedAwsIamServiceIdentity) resolvedIdentities.get(ServiceIdentityType.AWS_IAM); - Assertions.assertThat(resolvedAwsIamServiceIdentity.getIamArn()) + awsIamCredential = + (AwsIamServiceIdentityCredential) identityCredentials.get(ServiceIdentityType.AWS_IAM); + Assertions.assertThat(awsIamCredential.getIamArn()) .isEqualTo("arn:aws:iam::123456789012:user/polaris-default-iam-user"); - Assertions.assertThat(resolvedAwsIamServiceIdentity.getIdentityInfoReference()) + Assertions.assertThat(awsIamCredential.getIdentityInfoReference()) .isEqualTo( new ServiceSecretReference( "urn:polaris-secret:default-identity-provider:system:default:AWS_IAM", Map.of())); Assertions.assertThat( - resolvedAwsIamServiceIdentity.getAwsCredentialsProvider() - instanceof DefaultCredentialsProvider) + awsIamCredential.getAwsCredentialsProvider() instanceof DefaultCredentialsProvider) .isTrue(); } } From cf4b7513e9c467f444ca14820cbc09b00d3e60b4 Mon Sep 17 00:00:00 2001 From: Rulin Xing Date: Fri, 3 Oct 2025 03:51:48 -0700 Subject: [PATCH 09/12] Simplify the logic and add more tests --- .../credential/ServiceIdentityCredential.java | 3 +- .../dpo/AwsIamServiceIdentityInfoDpo.java | 8 +- .../identity/dpo/ServiceIdentityInfoDpo.java | 6 +- .../AwsIamServiceIdentityCredentialTest.java | 164 ++++++++++ .../service/admin/PolarisAdminService.java | 22 +- .../AwsIamServiceIdentityConfiguration.java | 36 ++- ...esolvableServiceIdentityConfiguration.java | 19 +- .../ServiceIdentityConfiguration.java | 29 -- .../DefaultServiceIdentityProvider.java | 134 ++++---- .../service/admin/ManagementServiceTest.java | 4 +- .../DefaultServiceIdentityProviderTest.java | 287 ++++++++++++++---- 11 files changed, 507 insertions(+), 205 deletions(-) create mode 100644 polaris-core/src/test/java/org/apache/polaris/core/identity/credential/AwsIamServiceIdentityCredentialTest.java diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/ServiceIdentityCredential.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/ServiceIdentityCredential.java index b7428cb439..12c7efd8e5 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/ServiceIdentityCredential.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/ServiceIdentityCredential.java @@ -38,7 +38,8 @@ *

    *
  • Identity type (e.g., AWS_IAM) *
  • A reference to where the credential is stored (for persistence) - *
  • The actual authentication credentials (implementation-specific) + *
  • The actual authentication credentials (implementation-specific, e.g., + * AwsCredentialsProvider) *
*/ public abstract class ServiceIdentityCredential { 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 cd15addb87..5852bdf583 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 @@ -24,6 +24,7 @@ import jakarta.annotation.Nullable; import org.apache.polaris.core.admin.model.AwsIamServiceIdentityInfo; import org.apache.polaris.core.identity.ServiceIdentityType; +import org.apache.polaris.core.identity.credential.AwsIamServiceIdentityCredential; import org.apache.polaris.core.secrets.SecretReference; /** @@ -34,12 +35,11 @@ * not persisted in this object. * *

At runtime, the reference can be used to retrieve the full {@link - * org.apache.polaris.core.identity.credential.AwsIamServiceIdentityCredential} which contains both - * the identity metadata (e.g., IAM ARN) and the actual AWS credentials needed for authentication. + * AwsIamServiceIdentityCredential} which contains both the identity metadata (e.g., IAM ARN) and + * the actual AWS credentials needed for authentication. * *

Instances of this class can be converted to the public API model {@link - * AwsIamServiceIdentityInfo} via a {@link - * org.apache.polaris.core.identity.provider.ServiceIdentityProvider}. + * AwsIamServiceIdentityInfo} via a ServiceIdentityProvider. */ public class AwsIamServiceIdentityInfoDpo extends ServiceIdentityInfoDpo { 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 d176cac7e4..0f6bfef665 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 @@ -26,6 +26,7 @@ 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.credential.ServiceIdentityCredential; import org.apache.polaris.core.identity.provider.ServiceIdentityProvider; import org.apache.polaris.core.secrets.SecretReference; @@ -34,9 +35,8 @@ * Important: JsonSubTypes must be kept in sync with {@link ServiceIdentityType}. * *

This DPO stores only a reference to the service identity credential, not the actual secrets. - * The reference can be used at runtime to retrieve the full {@link - * org.apache.polaris.core.identity.credential.ServiceIdentityCredential} with credentials through a - * {@link org.apache.polaris.core.identity.provider.ServiceIdentityProvider}. + * The reference can be used at runtime to retrieve the full {@link ServiceIdentityCredential} with + * credentials through a {@link ServiceIdentityProvider}. */ @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, diff --git a/polaris-core/src/test/java/org/apache/polaris/core/identity/credential/AwsIamServiceIdentityCredentialTest.java b/polaris-core/src/test/java/org/apache/polaris/core/identity/credential/AwsIamServiceIdentityCredentialTest.java new file mode 100644 index 0000000000..135d6bd707 --- /dev/null +++ b/polaris-core/src/test/java/org/apache/polaris/core/identity/credential/AwsIamServiceIdentityCredentialTest.java @@ -0,0 +1,164 @@ +/* + * 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.credential; + +import java.util.Map; +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 org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; + +public class AwsIamServiceIdentityCredentialTest { + + @Test + void testConstructorWithIamArnOnly() { + AwsIamServiceIdentityCredential credential = + new AwsIamServiceIdentityCredential("arn:aws:iam::123456789012:user/test-user"); + + Assertions.assertThat(credential.getIamArn()) + .isEqualTo("arn:aws:iam::123456789012:user/test-user"); + Assertions.assertThat(credential.getIdentityType()).isEqualTo(ServiceIdentityType.AWS_IAM); + Assertions.assertThat(credential.getAwsCredentialsProvider()) + .isInstanceOf(DefaultCredentialsProvider.class); + } + + @Test + void testConstructorWithIamArnAndCredentialsProvider() { + StaticCredentialsProvider credProvider = + StaticCredentialsProvider.create(AwsBasicCredentials.create("access-key", "secret-key")); + + AwsIamServiceIdentityCredential credential = + new AwsIamServiceIdentityCredential( + "arn:aws:iam::123456789012:role/test-role", credProvider); + + Assertions.assertThat(credential.getIamArn()) + .isEqualTo("arn:aws:iam::123456789012:role/test-role"); + Assertions.assertThat(credential.getAwsCredentialsProvider()).isEqualTo(credProvider); + } + + @Test + void testConstructorWithAllParameters() { + ServiceSecretReference ref = + new ServiceSecretReference("urn:polaris-secret:test:ref", Map.of()); + StaticCredentialsProvider credProvider = + StaticCredentialsProvider.create( + AwsSessionCredentials.create("access-key", "secret-key", "session-token")); + + AwsIamServiceIdentityCredential credential = + new AwsIamServiceIdentityCredential( + ref, "arn:aws:iam::123456789012:user/test-user", credProvider); + + Assertions.assertThat(credential.getIamArn()) + .isEqualTo("arn:aws:iam::123456789012:user/test-user"); + Assertions.assertThat(credential.getIdentityInfoReference()).isEqualTo(ref); + Assertions.assertThat(credential.getAwsCredentialsProvider()).isEqualTo(credProvider); + } + + @Test + void testConversionToDpo() { + ServiceSecretReference ref = + new ServiceSecretReference("urn:polaris-secret:test:reference", Map.of()); + AwsIamServiceIdentityCredential credential = + new AwsIamServiceIdentityCredential( + ref, "arn:aws:iam::123456789012:user/test-user", DefaultCredentialsProvider.create()); + + ServiceIdentityInfoDpo dpo = credential.asServiceIdentityInfoDpo(); + + Assertions.assertThat(dpo).isNotNull(); + Assertions.assertThat(dpo).isInstanceOf(AwsIamServiceIdentityInfoDpo.class); + Assertions.assertThat(dpo.getIdentityType()).isEqualTo(ServiceIdentityType.AWS_IAM); + Assertions.assertThat(dpo.getIdentityInfoReference()).isEqualTo(ref); + } + + @Test + void testConversionToModel() { + AwsIamServiceIdentityCredential credential = + new AwsIamServiceIdentityCredential("arn:aws:iam::123456789012:user/polaris-service"); + + ServiceIdentityInfo model = credential.asServiceIdentityInfoModel(); + + Assertions.assertThat(model).isNotNull(); + Assertions.assertThat(model).isInstanceOf(AwsIamServiceIdentityInfo.class); + Assertions.assertThat(model.getIdentityType()) + .isEqualTo(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM); + + AwsIamServiceIdentityInfo awsModel = (AwsIamServiceIdentityInfo) model; + Assertions.assertThat(awsModel.getIamArn()) + .isEqualTo("arn:aws:iam::123456789012:user/polaris-service"); + } + + @Test + void testCredentialsProviderWithStaticBasicCredentials() { + StaticCredentialsProvider credProvider = + StaticCredentialsProvider.create(AwsBasicCredentials.create("my-key-id", "my-secret")); + + AwsIamServiceIdentityCredential credential = + new AwsIamServiceIdentityCredential("arn:aws:iam::123456789012:user/test", credProvider); + + Assertions.assertThat(credential.getAwsCredentialsProvider()).isEqualTo(credProvider); + AwsBasicCredentials creds = (AwsBasicCredentials) credProvider.resolveCredentials(); + Assertions.assertThat(creds.accessKeyId()).isEqualTo("my-key-id"); + Assertions.assertThat(creds.secretAccessKey()).isEqualTo("my-secret"); + } + + @Test + void testCredentialsProviderWithSessionCredentials() { + StaticCredentialsProvider credProvider = + StaticCredentialsProvider.create( + AwsSessionCredentials.create("access-key", "secret-key", "session-token")); + + AwsIamServiceIdentityCredential credential = + new AwsIamServiceIdentityCredential("arn:aws:iam::123456789012:role/test", credProvider); + + Assertions.assertThat(credential.getAwsCredentialsProvider()).isEqualTo(credProvider); + AwsSessionCredentials creds = (AwsSessionCredentials) credProvider.resolveCredentials(); + Assertions.assertThat(creds.accessKeyId()).isEqualTo("access-key"); + Assertions.assertThat(creds.secretAccessKey()).isEqualTo("secret-key"); + Assertions.assertThat(creds.sessionToken()).isEqualTo("session-token"); + } + + @Test + void testModelDoesNotExposeCredentials() { + // Verify that the API model contains identity info but not credentials + StaticCredentialsProvider credProvider = + StaticCredentialsProvider.create( + AwsBasicCredentials.create("secret-access-key-id", "secret-access-key")); + + AwsIamServiceIdentityCredential credential = + new AwsIamServiceIdentityCredential("arn:aws:iam::123456789012:user/service", credProvider); + + ServiceIdentityInfo model = credential.asServiceIdentityInfoModel(); + AwsIamServiceIdentityInfo awsModel = (AwsIamServiceIdentityInfo) model; + + // Model should have the ARN + Assertions.assertThat(awsModel.getIamArn()).isEqualTo("arn:aws:iam::123456789012:user/service"); + + // Model should NOT have access keys or secrets (they're not in the AwsIamServiceIdentityInfo + // class) + // This is by design - credentials are never exposed in API responses + } +} 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 0d2bb23444..f59b75300b 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 @@ -100,7 +100,6 @@ 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.provider.ServiceIdentityProvider; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; @@ -782,23 +781,10 @@ public PolarisEntity createCatalog(CreateCatalogRequest catalogRequest) { } // Allocate service identity if needed for the authentication type. - Optional serviceIdentityInfoDpoOptional = Optional.empty(); - if (connectionConfigInfo.getAuthenticationParameters().getAuthenticationType() - == AuthenticationParameters.AuthenticationTypeEnum.SIGV4) { - serviceIdentityInfoDpoOptional = - serviceIdentityProvider.allocateServiceIdentity(connectionConfigInfo); - if (serviceIdentityInfoDpoOptional.isEmpty()) { - throw new IllegalStateException( - String.format( - "Cannot create Catalog %s. Failed to allocate %s service identity for %s authentication", - entity.getName(), - ServiceIdentityType.AWS_IAM.name(), - connectionConfigInfo - .getAuthenticationParameters() - .getAuthenticationType() - .name())); - } - } + // The provider will determine if a service identity is required based on the connection + // config. + Optional serviceIdentityInfoDpoOptional = + serviceIdentityProvider.allocateServiceIdentity(connectionConfigInfo); entity = new CatalogEntity.Builder(entity) 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 48e0a8d652..9772aac49a 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,6 +21,8 @@ import jakarta.annotation.Nonnull; import java.util.Optional; +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.credential.AwsIamServiceIdentityCredential; import org.apache.polaris.core.secrets.ServiceSecretReference; @@ -72,25 +74,47 @@ default ServiceIdentityType getType() { } /** - * Converts this configuration into a {@link AwsIamServiceIdentityCredential}. + * Returns the {@link AwsIamServiceIdentityInfo} model containing only the IAM ARN. + * + *

This method is lightweight and does not construct AWS credential providers. It should be + * used for API responses where only identity metadata is needed. + * + * @return the service identity info model, or empty if the IAM ARN is not configured + */ + @Override + default Optional asServiceIdentityInfoModel() { + if (iamArn() == null) { + return Optional.empty(); + } + return Optional.of( + AwsIamServiceIdentityInfo.builder() + .setIdentityType(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM) + .setIamArn(iamArn()) + .build()); + } + + /** + * Converts this configuration into a {@link AwsIamServiceIdentityCredential} with actual AWS + * credentials. * *

Creates a credential object containing the configured IAM ARN and AWS credentials provider. * The credentials provider is constructed based on whether static credentials (access key, secret * key, session token) are configured or whether to use the default AWS credential chain. * + *

This method should only be called when credentials are actually needed for authentication. + * * @param serviceIdentityReference the reference to associate with this credential * @return the service identity credential, or empty if the IAM ARN is not configured */ @Override - default Optional resolve( + default Optional asServiceIdentityCredential( @Nonnull ServiceSecretReference serviceIdentityReference) { if (iamArn() == null) { return Optional.empty(); - } else { - return Optional.of( - new AwsIamServiceIdentityCredential( - serviceIdentityReference, iamArn(), awsCredentialsProvider())); } + return Optional.of( + new AwsIamServiceIdentityCredential( + 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 697e052ac8..8f1ddfe0c7 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,6 +21,7 @@ import jakarta.annotation.Nonnull; import java.util.Optional; +import org.apache.polaris.core.admin.model.ServiceIdentityInfo; import org.apache.polaris.core.identity.ServiceIdentityType; import org.apache.polaris.core.identity.credential.ServiceIdentityCredential; import org.apache.polaris.core.secrets.ServiceSecretReference; @@ -43,17 +44,31 @@ default ServiceIdentityType getType() { return ServiceIdentityType.NULL_TYPE; } + /** + * Converts this configuration into a {@link ServiceIdentityInfo} model containing identity + * metadata without credentials. + * + *

This method is used when only identity information (e.g., IAM ARN) is needed for API + * responses, without exposing sensitive credentials. + * + * @return an optional service identity info model, or empty if required configuration is missing + */ + default Optional asServiceIdentityInfoModel() { + return Optional.empty(); + } + /** * Converts this configuration into a {@link ServiceIdentityCredential} with actual credentials. * - *

Implementations should construct the appropriate credential object (e.g., {@link + *

This method should only be called when credentials are actually needed for authentication. + * Implementations should construct the appropriate credential object (e.g., {@link * org.apache.polaris.core.identity.credential.AwsIamServiceIdentityCredential}) using the * configured values and the provided secret reference. * * @param serviceIdentityReference the reference to associate with this credential for persistence * @return an optional service identity credential, or empty if required configuration is missing */ - default Optional resolve( + default Optional asServiceIdentityCredential( @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 a9363a6a4f..8778beae7d 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,12 +23,8 @@ 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.credential.ServiceIdentityCredential; -import org.apache.polaris.service.identity.provider.DefaultServiceIdentityProvider; /** * Configuration interface for managing service identities across multiple realms in Polaris. @@ -91,31 +87,6 @@ default RealmConfigEntry forRealm(String realmIdentifier) { return new RealmConfigEntry(resolvedRealmIdentifier, realms().get(resolvedRealmIdentifier)); } - /** - * Loads and returns the list of {@link ServiceIdentityCredential} objects configured for the - * given realm. - * - *

This method retrieves the realm's configuration, builds credential references for each - * configured service identity, and constructs the corresponding {@link ServiceIdentityCredential} - * objects with their credentials. - * - * @param realmContext the realm context for which to load service identities - * @return a list of service identity credentials configured for the realm - */ - default List resolveServiceIdentityCredentials( - RealmContext realmContext) { - RealmConfigEntry entry = forRealm(realmContext); - - return entry.config().serviceIdentityConfigurations().stream() - .map( - resolvableServiceIdentityConfiguration -> - resolvableServiceIdentityConfiguration.resolve( - DefaultServiceIdentityProvider.buildIdentityInfoReference( - entry.realm(), resolvableServiceIdentityConfiguration.getType()))) - .flatMap(Optional::stream) - .toList(); - } - /** * A pairing of a realm identifier and its associated service identity configuration. * diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProvider.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProvider.java index f50327f963..58624b0646 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProvider.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProvider.java @@ -22,40 +22,29 @@ import com.google.common.annotations.VisibleForTesting; import jakarta.annotation.Nonnull; 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.admin.model.AuthenticationParameters; import org.apache.polaris.core.admin.model.ConnectionConfigInfo; import org.apache.polaris.core.admin.model.ServiceIdentityInfo; import org.apache.polaris.core.context.RealmContext; import org.apache.polaris.core.identity.ServiceIdentityType; import org.apache.polaris.core.identity.credential.ServiceIdentityCredential; +import org.apache.polaris.core.identity.dpo.AwsIamServiceIdentityInfoDpo; import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; import org.apache.polaris.core.identity.provider.ServiceIdentityProvider; 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 ServiceIdentityProvider} that provides service identity * credentials from statically configured values. * - *

This implementation loads service identity configurations at startup from Quarkus application - * properties and maintains them in memory. It supports both multi-tenant and single-tenant - * deployments: - * - *

    - *
  • Multi-tenant mode: Each realm can define its own service identities. When allocating - * an identity to a catalog, the provider selects the appropriate identity based on the - * catalog's realm and authentication type. - *
  • Single-tenant mode: A single default set of service identities is used for all - * catalogs. - *
- * - *

All service identities must be configured before server startup. This implementation does not - * support dynamic credential rotation or runtime identity registration. Vendors requiring such - * functionality should implement a custom {@link ServiceIdentityProvider}. + *

This implementation loads service identity configurations at startup and uses them to provide + * identity information and credentials on demand. All resolution is done lazily - credentials are + * only created when actually needed for authentication. */ public class DefaultServiceIdentityProvider implements ServiceIdentityProvider { public static final String DEFAULT_REALM_KEY = ServiceIdentityConfiguration.DEFAULT_REALM_KEY; @@ -63,101 +52,86 @@ public class DefaultServiceIdentityProvider implements ServiceIdentityProvider { private static final String IDENTITY_INFO_REFERENCE_URN_FORMAT = "urn:polaris-secret:default-identity-provider:%s:%s"; - /** Map of service identity types to their credentials. */ - private final EnumMap serviceIdentityCredentials; - - /** Map of identity info references (URNs) to their service identity credentials. */ - private final Map referenceToServiceIdentityCredential; + private final String realm; + private final RealmServiceIdentityConfiguration config; public DefaultServiceIdentityProvider() { - this(new EnumMap<>(ServiceIdentityType.class)); - } - - public DefaultServiceIdentityProvider( - EnumMap serviceIdentities) { - this.serviceIdentityCredentials = serviceIdentities; - this.referenceToServiceIdentityCredential = - serviceIdentities.values().stream() - .collect( - Collectors.toMap( - identity -> identity.getIdentityInfoReference().getUrn(), - identity -> identity)); + this.realm = DEFAULT_REALM_KEY; + this.config = null; } @Inject public DefaultServiceIdentityProvider( RealmContext realmContext, ServiceIdentityConfiguration serviceIdentityConfiguration) { - this.serviceIdentityCredentials = - serviceIdentityConfiguration.resolveServiceIdentityCredentials(realmContext).stream() - .collect( - // Collect to an EnumMap, grouping by ServiceIdentityType - Collectors.toMap( - ServiceIdentityCredential::getIdentityType, - identity -> identity, - (a, b) -> b, - () -> new EnumMap<>(ServiceIdentityType.class))); - - this.referenceToServiceIdentityCredential = - serviceIdentityCredentials.values().stream() - .collect( - Collectors.toMap( - identity -> identity.getIdentityInfoReference().getUrn(), - identity -> identity)); + ServiceIdentityConfiguration.RealmConfigEntry entry = + serviceIdentityConfiguration.forRealm(realmContext); + this.realm = entry.realm(); + this.config = entry.config(); } @Override public Optional allocateServiceIdentity( @Nonnull ConnectionConfigInfo connectionConfig) { - // Determine the service identity type based on the authentication parameters - if (connectionConfig.getAuthenticationParameters() == null) { + if (config == null || connectionConfig.getAuthenticationParameters() == null) { return Optional.empty(); } - AuthenticationParameters.AuthenticationTypeEnum authenticationType = + AuthenticationParameters.AuthenticationTypeEnum authType = connectionConfig.getAuthenticationParameters().getAuthenticationType(); - ServiceIdentityType serviceIdentityType = null; - if (authenticationType == AuthenticationParameters.AuthenticationTypeEnum.SIGV4) { - serviceIdentityType = ServiceIdentityType.AWS_IAM; - } - // Add more authentication types and their corresponding service identity types as needed - - if (serviceIdentityType == null) { - return Optional.empty(); - } - - ServiceIdentityCredential serviceIdentityCredential = - serviceIdentityCredentials.get(serviceIdentityType); - if (serviceIdentityCredential == null) { - return Optional.empty(); - } - return Optional.of(serviceIdentityCredential.asServiceIdentityInfoDpo()); + // Map authentication type to service identity type and check if configured + return switch (authType) { + case SIGV4 -> + config.awsIamServiceIdentity().isPresent() + ? Optional.of( + new AwsIamServiceIdentityInfoDpo( + buildIdentityInfoReference(realm, ServiceIdentityType.AWS_IAM))) + : Optional.empty(); + default -> Optional.empty(); + }; } @Override public Optional getServiceIdentityInfo( @Nonnull ServiceIdentityInfoDpo serviceIdentityInfo) { - ServiceIdentityCredential serviceIdentityCredential = - referenceToServiceIdentityCredential.get( - serviceIdentityInfo.getIdentityInfoReference().getUrn()); - if (serviceIdentityCredential == null) { + if (config == null) { return Optional.empty(); } - return Optional.of(serviceIdentityCredential.asServiceIdentityInfoModel()); + + // Find the configuration matching the reference and return metadata only + ServiceSecretReference actualRef = + (ServiceSecretReference) serviceIdentityInfo.getIdentityInfoReference(); + + return config.serviceIdentityConfigurations().stream() + .filter( + identityConfig -> + buildIdentityInfoReference(realm, identityConfig.getType()).equals(actualRef)) + .findFirst() + .flatMap(ResolvableServiceIdentityConfiguration::asServiceIdentityInfoModel); } @Override public Optional getServiceIdentityCredential( @Nonnull ServiceIdentityInfoDpo serviceIdentityInfo) { - ServiceIdentityCredential serviceIdentityCredential = - referenceToServiceIdentityCredential.get( - serviceIdentityInfo.getIdentityInfoReference().getUrn()); - return Optional.ofNullable(serviceIdentityCredential); + if (config == null) { + return Optional.empty(); + } + + // Find the configuration matching the reference and resolve credential lazily + ServiceSecretReference actualRef = + (ServiceSecretReference) serviceIdentityInfo.getIdentityInfoReference(); + + return config.serviceIdentityConfigurations().stream() + .filter( + identityConfig -> + buildIdentityInfoReference(realm, identityConfig.getType()).equals(actualRef)) + .findFirst() + .flatMap(identityConfig -> identityConfig.asServiceIdentityCredential(actualRef)); } @VisibleForTesting - public EnumMap getServiceIdentityCredentials() { - return serviceIdentityCredentials; + public RealmServiceIdentityConfiguration getRealmConfig() { + return config; } /** 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 573568126c..cc38ea87aa 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,7 +25,6 @@ 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,7 +47,6 @@ 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.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.dao.entity.BaseResult; import org.apache.polaris.core.persistence.dao.entity.CreateCatalogResult; @@ -237,7 +235,7 @@ private PolarisAdminService setupPolarisAdminService( services.resolutionManifestFactory(), metaStoreManager, new UnsafeInMemorySecretsManager(), - new DefaultServiceIdentityProvider(new EnumMap<>(ServiceIdentityType.class)), + new DefaultServiceIdentityProvider(), new SecurityContext() { @Override public Principal getUserPrincipal() { diff --git a/runtime/service/src/test/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProviderTest.java b/runtime/service/src/test/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProviderTest.java index 97f0f2f98b..4d48a1d12b 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProviderTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProviderTest.java @@ -24,21 +24,29 @@ 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.admin.model.AuthenticationParameters; +import org.apache.polaris.core.admin.model.AwsIamServiceIdentityInfo; +import org.apache.polaris.core.admin.model.BearerAuthenticationParameters; +import org.apache.polaris.core.admin.model.ConnectionConfigInfo; +import org.apache.polaris.core.admin.model.IcebergRestConnectionConfigInfo; +import org.apache.polaris.core.admin.model.ServiceIdentityInfo; +import org.apache.polaris.core.admin.model.SigV4AuthenticationParameters; import org.apache.polaris.core.context.RealmContext; import org.apache.polaris.core.identity.ServiceIdentityType; import org.apache.polaris.core.identity.credential.AwsIamServiceIdentityCredential; import org.apache.polaris.core.identity.credential.ServiceIdentityCredential; +import org.apache.polaris.core.identity.dpo.AwsIamServiceIdentityInfoDpo; +import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; import org.apache.polaris.core.secrets.ServiceSecretReference; +import org.apache.polaris.service.identity.AwsIamServiceIdentityConfiguration; 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; 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 @@ -120,81 +128,242 @@ void testServiceIdentityConfiguration() { } @Test - void testRealmServiceIdentityConfigToServiceIdentityCredential() { + void testAwsIamConfigurationLoading() { // Check the default realm Mockito.when(realmContext.getRealmIdentifier()).thenReturn(DEFAULT_REALM_KEY); DefaultServiceIdentityProvider defaultProvider = new DefaultServiceIdentityProvider(realmContext, serviceIdentityConfiguration); - EnumMap identityCredentials = - defaultProvider.getServiceIdentityCredentials(); - Assertions.assertThat(identityCredentials) - .containsKey(ServiceIdentityType.AWS_IAM) - .size() - .isEqualTo(1); - AwsIamServiceIdentityCredential awsIamCredential = - (AwsIamServiceIdentityCredential) identityCredentials.get(ServiceIdentityType.AWS_IAM); - Assertions.assertThat(awsIamCredential.getIamArn()) + Optional awsConfig = + defaultProvider.getRealmConfig().awsIamServiceIdentity(); + Assertions.assertThat(awsConfig).isPresent(); + Assertions.assertThat(awsConfig.get().iamArn()) .isEqualTo("arn:aws:iam::123456789012:user/polaris-default-iam-user"); - Assertions.assertThat(awsIamCredential.getIdentityInfoReference()) - .isEqualTo( - new ServiceSecretReference( - "urn:polaris-secret:default-identity-provider:system:default:AWS_IAM", Map.of())); - Assertions.assertThat( - awsIamCredential.getAwsCredentialsProvider() instanceof DefaultCredentialsProvider) - .isTrue(); + Assertions.assertThat(awsConfig.get().accessKeyId()).isEmpty(); - // Check the my-realm + // Check the my-realm with static credentials Mockito.when(realmContext.getRealmIdentifier()).thenReturn(MY_REALM_KEY); DefaultServiceIdentityProvider myRealmProvider = new DefaultServiceIdentityProvider(realmContext, serviceIdentityConfiguration); - identityCredentials = myRealmProvider.getServiceIdentityCredentials(); - Assertions.assertThat(identityCredentials) - .containsKey(ServiceIdentityType.AWS_IAM) - .size() - .isEqualTo(1); - awsIamCredential = - (AwsIamServiceIdentityCredential) identityCredentials.get(ServiceIdentityType.AWS_IAM); - Assertions.assertThat(awsIamCredential.getIamArn()) + awsConfig = myRealmProvider.getRealmConfig().awsIamServiceIdentity(); + Assertions.assertThat(awsConfig).isPresent(); + Assertions.assertThat(awsConfig.get().iamArn()) .isEqualTo("arn:aws:iam::123456789012:user/polaris-iam-user"); - Assertions.assertThat(awsIamCredential.getIdentityInfoReference()) - .isEqualTo( - new ServiceSecretReference( - "urn:polaris-secret:default-identity-provider:my-realm:AWS_IAM", Map.of())); - Assertions.assertThat( - awsIamCredential.getAwsCredentialsProvider() instanceof StaticCredentialsProvider) - .isTrue(); - StaticCredentialsProvider staticCredentialsProvider = - (StaticCredentialsProvider) awsIamCredential.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 + Assertions.assertThat(awsConfig.get().accessKeyId()).isEqualTo(Optional.of("access-key-id")); + Assertions.assertThat(awsConfig.get().secretAccessKey()) + .isEqualTo(Optional.of("secret-access-key")); + Assertions.assertThat(awsConfig.get().sessionToken()).isEqualTo(Optional.of("session-token")); + + // Check the other realm (should fallback to default) Mockito.when(realmContext.getRealmIdentifier()).thenReturn("other-realm"); DefaultServiceIdentityProvider otherProvider = new DefaultServiceIdentityProvider(realmContext, serviceIdentityConfiguration); - identityCredentials = otherProvider.getServiceIdentityCredentials(); - Assertions.assertThat(identityCredentials) - .containsKey(ServiceIdentityType.AWS_IAM) - .size() - .isEqualTo(1); - awsIamCredential = - (AwsIamServiceIdentityCredential) identityCredentials.get(ServiceIdentityType.AWS_IAM); - Assertions.assertThat(awsIamCredential.getIamArn()) + + awsConfig = otherProvider.getRealmConfig().awsIamServiceIdentity(); + Assertions.assertThat(awsConfig).isPresent(); + Assertions.assertThat(awsConfig.get().iamArn()) .isEqualTo("arn:aws:iam::123456789012:user/polaris-default-iam-user"); - Assertions.assertThat(awsIamCredential.getIdentityInfoReference()) - .isEqualTo( + } + + @Test + void testAllocateServiceIdentityWithSigV4Authentication() { + // Test allocateServiceIdentity with SigV4 authentication + Mockito.when(realmContext.getRealmIdentifier()).thenReturn(DEFAULT_REALM_KEY); + DefaultServiceIdentityProvider provider = + new DefaultServiceIdentityProvider(realmContext, serviceIdentityConfiguration); + + ConnectionConfigInfo connectionConfig = + IcebergRestConnectionConfigInfo.builder() + .setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.ICEBERG_REST) + .setUri("https://example.com/catalog") + .setAuthenticationParameters( + SigV4AuthenticationParameters.builder() + .setAuthenticationType(AuthenticationParameters.AuthenticationTypeEnum.SIGV4) + .setRoleArn("arn:aws:iam::123456789012:role/customer-role") + .build()) + .build(); + + Optional result = provider.allocateServiceIdentity(connectionConfig); + + Assertions.assertThat(result).isPresent(); + ServiceIdentityInfoDpo serviceIdentityDpo = result.get(); + Assertions.assertThat(serviceIdentityDpo).isInstanceOf(AwsIamServiceIdentityInfoDpo.class); + Assertions.assertThat(serviceIdentityDpo.getIdentityType()) + .isEqualTo(ServiceIdentityType.AWS_IAM); + Assertions.assertThat(serviceIdentityDpo.getIdentityInfoReference().getUrn()) + .contains("default-identity-provider"); + } + + @Test + void testAllocateServiceIdentityWithBearerAuthenticationReturnsEmpty() { + // Test allocateServiceIdentity with non-SigV4 authentication returns empty + Mockito.when(realmContext.getRealmIdentifier()).thenReturn(DEFAULT_REALM_KEY); + DefaultServiceIdentityProvider provider = + new DefaultServiceIdentityProvider(realmContext, serviceIdentityConfiguration); + + ConnectionConfigInfo connectionConfig = + IcebergRestConnectionConfigInfo.builder() + .setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.ICEBERG_REST) + .setUri("https://example.com/catalog") + .setAuthenticationParameters( + BearerAuthenticationParameters.builder() + .setAuthenticationType(AuthenticationParameters.AuthenticationTypeEnum.BEARER) + .setBearerToken("some-token") + .build()) + .build(); + + Optional result = provider.allocateServiceIdentity(connectionConfig); + + Assertions.assertThat(result).isEmpty(); + } + + @Test + void testAllocateServiceIdentityWithNullAuthParametersReturnsEmpty() { + // Test allocateServiceIdentity with null authentication parameters + Mockito.when(realmContext.getRealmIdentifier()).thenReturn(DEFAULT_REALM_KEY); + DefaultServiceIdentityProvider provider = + new DefaultServiceIdentityProvider(realmContext, serviceIdentityConfiguration); + + ConnectionConfigInfo connectionConfig = + IcebergRestConnectionConfigInfo.builder() + .setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.ICEBERG_REST) + .setUri("https://example.com/catalog") + .build(); + + Optional result = provider.allocateServiceIdentity(connectionConfig); + + Assertions.assertThat(result).isEmpty(); + } + + @Test + void testGetServiceIdentityInfoReturnsInfoWithoutCredentials() { + // Test getServiceIdentityInfo returns user-facing info without credentials + Mockito.when(realmContext.getRealmIdentifier()).thenReturn(DEFAULT_REALM_KEY); + DefaultServiceIdentityProvider provider = + new DefaultServiceIdentityProvider(realmContext, serviceIdentityConfiguration); + + ServiceIdentityInfoDpo serviceIdentityDpo = + new AwsIamServiceIdentityInfoDpo( new ServiceSecretReference( "urn:polaris-secret:default-identity-provider:system:default:AWS_IAM", Map.of())); + + Optional result = provider.getServiceIdentityInfo(serviceIdentityDpo); + + Assertions.assertThat(result).isPresent(); + ServiceIdentityInfo info = result.get(); + Assertions.assertThat(info.getIdentityType()) + .isEqualTo(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM); + Assertions.assertThat(info).isInstanceOf(AwsIamServiceIdentityInfo.class); + AwsIamServiceIdentityInfo awsInfo = (AwsIamServiceIdentityInfo) info; + Assertions.assertThat(awsInfo.getIamArn()) + .isEqualTo("arn:aws:iam::123456789012:user/polaris-default-iam-user"); + } + + @Test + void testGetServiceIdentityCredentialReturnsCredentialWithSecrets() { + // Test getServiceIdentityCredential returns full credential with secrets + Mockito.when(realmContext.getRealmIdentifier()).thenReturn(MY_REALM_KEY); + DefaultServiceIdentityProvider provider = + new DefaultServiceIdentityProvider(realmContext, serviceIdentityConfiguration); + + ServiceIdentityInfoDpo serviceIdentityDpo = + new AwsIamServiceIdentityInfoDpo( + new ServiceSecretReference( + "urn:polaris-secret:default-identity-provider:my-realm:AWS_IAM", Map.of())); + + Optional result = + provider.getServiceIdentityCredential(serviceIdentityDpo); + + Assertions.assertThat(result).isPresent(); + ServiceIdentityCredential credential = result.get(); + Assertions.assertThat(credential).isInstanceOf(AwsIamServiceIdentityCredential.class); + + AwsIamServiceIdentityCredential awsCredential = (AwsIamServiceIdentityCredential) credential; + Assertions.assertThat(awsCredential.getIamArn()) + .isEqualTo("arn:aws:iam::123456789012:user/polaris-iam-user"); + Assertions.assertThat(awsCredential.getAwsCredentialsProvider()) + .isInstanceOf(StaticCredentialsProvider.class); + + // Verify credentials are accessible + StaticCredentialsProvider credProvider = + (StaticCredentialsProvider) awsCredential.getAwsCredentialsProvider(); + AwsSessionCredentials creds = (AwsSessionCredentials) credProvider.resolveCredentials(); + Assertions.assertThat(creds.accessKeyId()).isEqualTo("access-key-id"); + Assertions.assertThat(creds.secretAccessKey()).isEqualTo("secret-access-key"); + Assertions.assertThat(creds.sessionToken()).isEqualTo("session-token"); + } + + @Test + void testEmptyProviderAllocateServiceIdentityReturnsEmpty() { + // Test that an empty provider returns empty when allocating + DefaultServiceIdentityProvider emptyProvider = new DefaultServiceIdentityProvider(); + + ConnectionConfigInfo connectionConfig = + IcebergRestConnectionConfigInfo.builder() + .setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.ICEBERG_REST) + .setUri("https://example.com/catalog") + .setAuthenticationParameters( + SigV4AuthenticationParameters.builder() + .setAuthenticationType(AuthenticationParameters.AuthenticationTypeEnum.SIGV4) + .setRoleArn("arn:aws:iam::123456789012:role/customer-role") + .build()) + .build(); + + Optional result = + emptyProvider.allocateServiceIdentity(connectionConfig); + + Assertions.assertThat(result).isEmpty(); + } + + @Test + void testMultiTenantScenarioDifferentRealmsGetDifferentIdentities() { + // Test that different realms have different configurations + Mockito.when(realmContext.getRealmIdentifier()).thenReturn(DEFAULT_REALM_KEY); + DefaultServiceIdentityProvider defaultProvider = + new DefaultServiceIdentityProvider(realmContext, serviceIdentityConfiguration); + + Mockito.when(realmContext.getRealmIdentifier()).thenReturn(MY_REALM_KEY); + DefaultServiceIdentityProvider myRealmProvider = + new DefaultServiceIdentityProvider(realmContext, serviceIdentityConfiguration); + + // Verify different IAM ARNs from configuration + Assertions.assertThat(defaultProvider.getRealmConfig().awsIamServiceIdentity()).isPresent(); + Assertions.assertThat(defaultProvider.getRealmConfig().awsIamServiceIdentity().get().iamArn()) + .isEqualTo("arn:aws:iam::123456789012:user/polaris-default-iam-user"); + + Assertions.assertThat(myRealmProvider.getRealmConfig().awsIamServiceIdentity()).isPresent(); + Assertions.assertThat(myRealmProvider.getRealmConfig().awsIamServiceIdentity().get().iamArn()) + .isEqualTo("arn:aws:iam::123456789012:user/polaris-iam-user"); + + // Verify different credential configurations + Assertions.assertThat( + defaultProvider.getRealmConfig().awsIamServiceIdentity().get().accessKeyId()) + .isEmpty(); Assertions.assertThat( - awsIamCredential.getAwsCredentialsProvider() instanceof DefaultCredentialsProvider) - .isTrue(); + myRealmProvider.getRealmConfig().awsIamServiceIdentity().get().accessKeyId()) + .isEqualTo(Optional.of("access-key-id")); + } + + @Test + void testBuildIdentityInfoReferenceForDefaultRealm() { + // Test URN generation for default realm + ServiceSecretReference ref = + DefaultServiceIdentityProvider.buildIdentityInfoReference( + DEFAULT_REALM_KEY, ServiceIdentityType.AWS_IAM); + + Assertions.assertThat(ref.getUrn()) + .isEqualTo("urn:polaris-secret:default-identity-provider:system:default:AWS_IAM"); + } + + @Test + void testBuildIdentityInfoReferenceForCustomRealm() { + // Test URN generation for custom realm + ServiceSecretReference ref = + DefaultServiceIdentityProvider.buildIdentityInfoReference( + "custom-realm", ServiceIdentityType.AWS_IAM); + + Assertions.assertThat(ref.getUrn()) + .isEqualTo("urn:polaris-secret:default-identity-provider:custom-realm:AWS_IAM"); } } From dfa36aa282fd0dbe646dc460af919ee0bf1613d2 Mon Sep 17 00:00:00 2001 From: Rulin Xing Date: Fri, 3 Oct 2025 04:46:15 -0700 Subject: [PATCH 10/12] Use SecretReference and fix some small issues --- .../AwsIamServiceIdentityCredential.java | 6 ++--- .../credential/ServiceIdentityCredential.java | 13 +++++----- .../AwsIamServiceIdentityCredentialTest.java | 12 ++++----- .../src/main/resources/application.properties | 1 - .../AwsIamServiceIdentityConfiguration.java | 9 +++---- ...esolvableServiceIdentityConfiguration.java | 6 ++--- .../DefaultServiceIdentityProvider.java | 26 ++++++++----------- .../DefaultServiceIdentityProviderTest.java | 10 +++---- 8 files changed, 38 insertions(+), 45 deletions(-) diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/AwsIamServiceIdentityCredential.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/AwsIamServiceIdentityCredential.java index 0a97601124..ae85cbbe13 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/AwsIamServiceIdentityCredential.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/AwsIamServiceIdentityCredential.java @@ -25,7 +25,7 @@ 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 org.apache.polaris.core.secrets.SecretReference; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; @@ -69,10 +69,10 @@ public AwsIamServiceIdentityCredential( } public AwsIamServiceIdentityCredential( - @Nullable ServiceSecretReference serviceSecretReference, + @Nullable SecretReference secretReference, @Nullable String iamArn, @Nonnull AwsCredentialsProvider awsCredentialsProvider) { - super(ServiceIdentityType.AWS_IAM, serviceSecretReference); + super(ServiceIdentityType.AWS_IAM, secretReference); this.iamArn = iamArn; this.awsCredentialsProvider = awsCredentialsProvider; } diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/ServiceIdentityCredential.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/ServiceIdentityCredential.java index 12c7efd8e5..e7cdb11a65 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/ServiceIdentityCredential.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/ServiceIdentityCredential.java @@ -23,7 +23,7 @@ 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; +import org.apache.polaris.core.secrets.SecretReference; import software.amazon.awssdk.annotations.NotNull; /** @@ -37,22 +37,21 @@ * *

    *
  • Identity type (e.g., AWS_IAM) - *
  • A reference to where the credential is stored (for persistence) + *
  • A {@link SecretReference} pointing to where the credential configuration is stored *
  • The actual authentication credentials (implementation-specific, e.g., * AwsCredentialsProvider) *
*/ public abstract class ServiceIdentityCredential { private final ServiceIdentityType identityType; - private ServiceSecretReference identityInfoReference; + private SecretReference identityInfoReference; public ServiceIdentityCredential(@Nonnull ServiceIdentityType identityType) { this(identityType, null); } public ServiceIdentityCredential( - @Nonnull ServiceIdentityType identityType, - @Nullable ServiceSecretReference identityInfoReference) { + @Nonnull ServiceIdentityType identityType, @Nullable SecretReference identityInfoReference) { this.identityType = identityType; this.identityInfoReference = identityInfoReference; } @@ -61,11 +60,11 @@ public ServiceIdentityCredential( return identityType; } - public @Nonnull ServiceSecretReference getIdentityInfoReference() { + public @Nonnull SecretReference getIdentityInfoReference() { return identityInfoReference; } - public void setIdentityInfoReference(@NotNull ServiceSecretReference identityInfoReference) { + public void setIdentityInfoReference(@NotNull SecretReference identityInfoReference) { this.identityInfoReference = identityInfoReference; } diff --git a/polaris-core/src/test/java/org/apache/polaris/core/identity/credential/AwsIamServiceIdentityCredentialTest.java b/polaris-core/src/test/java/org/apache/polaris/core/identity/credential/AwsIamServiceIdentityCredentialTest.java index 135d6bd707..ce69a4f089 100644 --- a/polaris-core/src/test/java/org/apache/polaris/core/identity/credential/AwsIamServiceIdentityCredentialTest.java +++ b/polaris-core/src/test/java/org/apache/polaris/core/identity/credential/AwsIamServiceIdentityCredentialTest.java @@ -24,7 +24,7 @@ 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 org.apache.polaris.core.secrets.SecretReference; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; @@ -62,8 +62,7 @@ void testConstructorWithIamArnAndCredentialsProvider() { @Test void testConstructorWithAllParameters() { - ServiceSecretReference ref = - new ServiceSecretReference("urn:polaris-secret:test:ref", Map.of()); + SecretReference ref = new SecretReference("urn:polaris-secret:test:ref", Map.of()); StaticCredentialsProvider credProvider = StaticCredentialsProvider.create( AwsSessionCredentials.create("access-key", "secret-key", "session-token")); @@ -80,11 +79,12 @@ void testConstructorWithAllParameters() { @Test void testConversionToDpo() { - ServiceSecretReference ref = - new ServiceSecretReference("urn:polaris-secret:test:reference", Map.of()); + SecretReference ref = new SecretReference("urn:polaris-secret:test:reference", Map.of()); AwsIamServiceIdentityCredential credential = new AwsIamServiceIdentityCredential( - ref, "arn:aws:iam::123456789012:user/test-user", DefaultCredentialsProvider.create()); + ref, + "arn:aws:iam::123456789012:user/test-user", + DefaultCredentialsProvider.builder().build()); ServiceIdentityInfoDpo dpo = credential.asServiceIdentityInfoDpo(); diff --git a/runtime/defaults/src/main/resources/application.properties b/runtime/defaults/src/main/resources/application.properties index 1a94121929..c1aa912369 100644 --- a/runtime/defaults/src/main/resources/application.properties +++ b/runtime/defaults/src/main/resources/application.properties @@ -221,7 +221,6 @@ 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/identity/AwsIamServiceIdentityConfiguration.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/AwsIamServiceIdentityConfiguration.java index 9772aac49a..e92b28d882 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 @@ -25,7 +25,7 @@ import org.apache.polaris.core.admin.model.ServiceIdentityInfo; import org.apache.polaris.core.identity.ServiceIdentityType; import org.apache.polaris.core.identity.credential.AwsIamServiceIdentityCredential; -import org.apache.polaris.core.secrets.ServiceSecretReference; +import org.apache.polaris.core.secrets.SecretReference; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; @@ -103,18 +103,17 @@ default Optional asServiceIdentityInfoModel() { * *

This method should only be called when credentials are actually needed for authentication. * - * @param serviceIdentityReference the reference to associate with this credential + * @param secretReference the secret reference to associate with this credential * @return the service identity credential, or empty if the IAM ARN is not configured */ @Override default Optional asServiceIdentityCredential( - @Nonnull ServiceSecretReference serviceIdentityReference) { + @Nonnull SecretReference secretReference) { if (iamArn() == null) { return Optional.empty(); } return Optional.of( - new AwsIamServiceIdentityCredential( - serviceIdentityReference, iamArn(), awsCredentialsProvider())); + new AwsIamServiceIdentityCredential(secretReference, 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 8f1ddfe0c7..dc03aa0c5f 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 @@ -24,7 +24,7 @@ import org.apache.polaris.core.admin.model.ServiceIdentityInfo; import org.apache.polaris.core.identity.ServiceIdentityType; import org.apache.polaris.core.identity.credential.ServiceIdentityCredential; -import org.apache.polaris.core.secrets.ServiceSecretReference; +import org.apache.polaris.core.secrets.SecretReference; /** * Represents a service identity configuration that can be converted into a fully initialized {@link @@ -65,11 +65,11 @@ default Optional asServiceIdentityInfoModel() { * org.apache.polaris.core.identity.credential.AwsIamServiceIdentityCredential}) using the * configured values and the provided secret reference. * - * @param serviceIdentityReference the reference to associate with this credential for persistence + * @param secretReference the secret reference to associate with this credential for persistence * @return an optional service identity credential, or empty if required configuration is missing */ default Optional asServiceIdentityCredential( - @Nonnull ServiceSecretReference serviceIdentityReference) { + @Nonnull SecretReference secretReference) { return Optional.empty(); } } diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProvider.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProvider.java index 58624b0646..cfc273d46d 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProvider.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProvider.java @@ -33,7 +33,7 @@ import org.apache.polaris.core.identity.dpo.AwsIamServiceIdentityInfoDpo; import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; import org.apache.polaris.core.identity.provider.ServiceIdentityProvider; -import org.apache.polaris.core.secrets.ServiceSecretReference; +import org.apache.polaris.core.secrets.SecretReference; import org.apache.polaris.service.identity.RealmServiceIdentityConfiguration; import org.apache.polaris.service.identity.ResolvableServiceIdentityConfiguration; import org.apache.polaris.service.identity.ServiceIdentityConfiguration; @@ -99,8 +99,7 @@ public Optional getServiceIdentityInfo( } // Find the configuration matching the reference and return metadata only - ServiceSecretReference actualRef = - (ServiceSecretReference) serviceIdentityInfo.getIdentityInfoReference(); + SecretReference actualRef = serviceIdentityInfo.getIdentityInfoReference(); return config.serviceIdentityConfigurations().stream() .filter( @@ -118,15 +117,14 @@ public Optional getServiceIdentityCredential( } // Find the configuration matching the reference and resolve credential lazily - ServiceSecretReference actualRef = - (ServiceSecretReference) serviceIdentityInfo.getIdentityInfoReference(); + SecretReference ref = serviceIdentityInfo.getIdentityInfoReference(); return config.serviceIdentityConfigurations().stream() .filter( identityConfig -> - buildIdentityInfoReference(realm, identityConfig.getType()).equals(actualRef)) + buildIdentityInfoReference(realm, identityConfig.getType()).equals(ref)) .findFirst() - .flatMap(identityConfig -> identityConfig.asServiceIdentityCredential(actualRef)); + .flatMap(identityConfig -> identityConfig.asServiceIdentityCredential(ref)); } @VisibleForTesting @@ -135,21 +133,19 @@ public RealmServiceIdentityConfiguration getRealmConfig() { } /** - * Builds a {@link ServiceSecretReference} for the given realm and service identity type. + * Builds a {@link SecretReference} for the given realm and service identity type. * - *

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

The URN format is: urn:polaris-secret:default-identity-provider:<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 + * @return the constructed secret reference for this service identity */ - public static ServiceSecretReference buildIdentityInfoReference( - String realm, ServiceIdentityType type) { - // urn:polaris-service-secret:default-identity-provider:: - return new ServiceSecretReference( + public static SecretReference buildIdentityInfoReference(String realm, ServiceIdentityType type) { + // urn:polaris-secret:default-identity-provider:: + return new SecretReference( 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/identity/provider/DefaultServiceIdentityProviderTest.java b/runtime/service/src/test/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProviderTest.java index 4d48a1d12b..9342994e07 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProviderTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProviderTest.java @@ -39,7 +39,7 @@ import org.apache.polaris.core.identity.credential.ServiceIdentityCredential; import org.apache.polaris.core.identity.dpo.AwsIamServiceIdentityInfoDpo; import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; -import org.apache.polaris.core.secrets.ServiceSecretReference; +import org.apache.polaris.core.secrets.SecretReference; import org.apache.polaris.service.identity.AwsIamServiceIdentityConfiguration; import org.apache.polaris.service.identity.RealmServiceIdentityConfiguration; import org.apache.polaris.service.identity.ServiceIdentityConfiguration; @@ -245,7 +245,7 @@ void testGetServiceIdentityInfoReturnsInfoWithoutCredentials() { ServiceIdentityInfoDpo serviceIdentityDpo = new AwsIamServiceIdentityInfoDpo( - new ServiceSecretReference( + new SecretReference( "urn:polaris-secret:default-identity-provider:system:default:AWS_IAM", Map.of())); Optional result = provider.getServiceIdentityInfo(serviceIdentityDpo); @@ -269,7 +269,7 @@ void testGetServiceIdentityCredentialReturnsCredentialWithSecrets() { ServiceIdentityInfoDpo serviceIdentityDpo = new AwsIamServiceIdentityInfoDpo( - new ServiceSecretReference( + new SecretReference( "urn:polaris-secret:default-identity-provider:my-realm:AWS_IAM", Map.of())); Optional result = @@ -348,7 +348,7 @@ void testMultiTenantScenarioDifferentRealmsGetDifferentIdentities() { @Test void testBuildIdentityInfoReferenceForDefaultRealm() { // Test URN generation for default realm - ServiceSecretReference ref = + SecretReference ref = DefaultServiceIdentityProvider.buildIdentityInfoReference( DEFAULT_REALM_KEY, ServiceIdentityType.AWS_IAM); @@ -359,7 +359,7 @@ void testBuildIdentityInfoReferenceForDefaultRealm() { @Test void testBuildIdentityInfoReferenceForCustomRealm() { // Test URN generation for custom realm - ServiceSecretReference ref = + SecretReference ref = DefaultServiceIdentityProvider.buildIdentityInfoReference( "custom-realm", ServiceIdentityType.AWS_IAM); From dede7a52878e4bea20998e80f1fa1f994b498e27 Mon Sep 17 00:00:00 2001 From: Rulin Xing Date: Fri, 3 Oct 2025 09:37:18 -0700 Subject: [PATCH 11/12] Resolved more comments --- .../core/connection/ConnectionConfigInfoDpo.java | 2 +- .../credential/ServiceIdentityCredential.java | 3 ++- .../dpo/AwsIamServiceIdentityInfoDpo.java | 13 +++++++------ .../identity/dpo/ServiceIdentityInfoDpo.java | 15 ++++++++------- .../src/main/resources/application.properties | 15 +-------------- .../service/admin/PolarisAdminService.java | 3 ++- .../polaris/service/config/ServiceProducers.java | 10 ---------- .../identity/ServiceIdentityConfiguration.java | 16 ++++++++++++++++ .../provider/DefaultServiceIdentityProvider.java | 2 ++ 9 files changed, 39 insertions(+), 40 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 b7d00e4a87..555a4008d0 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 @@ -226,7 +226,7 @@ public abstract ConnectionConfigInfoDpo withServiceIdentity( @Nonnull ServiceIdentityInfoDpo serviceIdentityInfo); /** - * Produces the correponding API-model ConnectionConfigInfo for this persistence object; many + * Produces the corresponding API-model ConnectionConfigInfo for this persistence object; many * 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. */ diff --git a/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/ServiceIdentityCredential.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/ServiceIdentityCredential.java index e7cdb11a65..635c6fcd60 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/ServiceIdentityCredential.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/ServiceIdentityCredential.java @@ -37,7 +37,8 @@ * *

    *
  • Identity type (e.g., AWS_IAM) - *
  • A {@link SecretReference} pointing to where the credential configuration is stored + *
  • A {@link SecretReference} that serves as a unique identifier for this service identity + * instance (used for lookups and persistence) *
  • The actual authentication credentials (implementation-specific, e.g., * AwsCredentialsProvider) *
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 5852bdf583..1e8d77c606 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 @@ -30,13 +30,14 @@ /** * Persistence-layer representation of an AWS IAM service identity used by Polaris. * - *

This class stores only a {@link SecretReference} pointing to where the actual AWS credentials - * are managed (e.g., in a secret manager or configuration store). The credentials themselves are - * not persisted in this object. + *

This class stores only the identity type and a {@link SecretReference} that serves as a unique + * identifier for this service identity instance. The reference is used to look up the identity's + * configuration at runtime. The actual credentials (AWS access keys) and metadata (IAM ARN) are not + * persisted in this object. * - *

At runtime, the reference can be used to retrieve the full {@link - * AwsIamServiceIdentityCredential} which contains both the identity metadata (e.g., IAM ARN) and - * the actual AWS credentials needed for authentication. + *

At runtime, a ServiceIdentityProvider uses the reference to look up the configuration and + * retrieve the full {@link AwsIamServiceIdentityCredential} which contains both the identity + * metadata (e.g., IAM ARN) and the actual AWS credentials needed for authentication. * *

Instances of this class can be converted to the public API model {@link * AwsIamServiceIdentityInfo} via a ServiceIdentityProvider. 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 0f6bfef665..de30bed300 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,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; import jakarta.annotation.Nullable; import org.apache.polaris.core.admin.model.ServiceIdentityInfo; import org.apache.polaris.core.identity.ServiceIdentityType; @@ -34,9 +35,10 @@ * The internal persistence-object counterpart to ServiceIdentityInfo defined in the API model. * Important: JsonSubTypes must be kept in sync with {@link ServiceIdentityType}. * - *

This DPO stores only a reference to the service identity credential, not the actual secrets. - * The reference can be used at runtime to retrieve the full {@link ServiceIdentityCredential} with - * credentials through a {@link ServiceIdentityProvider}. + *

This DPO stores only the identity type and a {@link SecretReference} that serves as a unique + * identifier for the service identity instance. The reference is used at runtime by a {@link + * ServiceIdentityProvider} to look up the configuration and retrieve the full {@link + * ServiceIdentityCredential} with credentials and metadata. */ @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, @@ -87,10 +89,9 @@ public SecretReference getIdentityInfoReference() { */ public @Nullable ServiceIdentityInfo asServiceIdentityInfoModel( ServiceIdentityProvider serviceIdentityProvider) { - if (serviceIdentityProvider == null) { - return null; - } - + Preconditions.checkNotNull( + serviceIdentityProvider, + "Need ServiceIdentityProvider to inject service identity info, should not be null"); return serviceIdentityProvider.getServiceIdentityInfo(this).orElse(null); } diff --git a/runtime/defaults/src/main/resources/application.properties b/runtime/defaults/src/main/resources/application.properties index c1aa912369..2084422094 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 @@ -197,19 +197,6 @@ polaris.oidc.principal-roles-mapper.type=default # polaris.storage.gcp.token=token # polaris.storage.gcp.lifespan=PT1H -# 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.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,\ org.apache.polaris.service.catalog.api.impl,\ 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 228b710676..8e0b80a3b3 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 @@ -683,7 +683,8 @@ private Map extractSecretReferences( } case SIGV4: { - // SigV4 authentication is not secret-based + // SigV4 authentication is not based on users provided secrets but based on the + // service identity managed by Polaris. Nothing to do here. break; } default: 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 f6e0705836..214ba9ad8a 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,7 +43,6 @@ 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.provider.ServiceIdentityProvider; import org.apache.polaris.core.persistence.BasePersistence; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; @@ -73,8 +72,6 @@ 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.ServiceIdentityConfiguration; -import org.apache.polaris.service.identity.provider.DefaultServiceIdentityProvider; import org.apache.polaris.service.persistence.PersistenceConfiguration; import org.apache.polaris.service.ratelimiter.RateLimiter; import org.apache.polaris.service.ratelimiter.RateLimiterFilterConfiguration; @@ -395,13 +392,6 @@ public OidcTenantResolver oidcTenantResolver( return resolvers.select(Identifier.Literal.of(config.tenantResolver())).get(); } - @Produces - @RequestScoped - public ServiceIdentityProvider serviceIdentityProvider( - RealmContext realmContext, ServiceIdentityConfiguration serviceIdentityConfiguration) { - return new DefaultServiceIdentityProvider(realmContext, serviceIdentityConfiguration); - } - public void closeTaskExecutor(@Disposes @Identifier("task-executor") ManagedExecutor executor) { executor.close(); } 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 8778beae7d..fc6a2ce42e 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 @@ -40,6 +40,22 @@ *

Configuration is loaded from {@code polaris.service-identity.*} properties at startup and * includes credentials that Polaris uses to assume customer-provided roles when accessing federated * catalogs. + * + *

Example Configuration: + * + *

{@code
+ * # Default service identity (used when no realm-specific configuration exists)
+ * polaris.service-identity.aws-iam.iam-arn=arn:aws:iam::123456789012:user/polaris-default-user
+ * # Optional: provide static credentials, or omit to use AWS default credential chain
+ * polaris.service-identity.aws-iam.access-key-id=
+ * polaris.service-identity.aws-iam.secret-access-key=
+ * polaris.service-identity.aws-iam.session-token=
+ *
+ * # Realm-specific service identity for multi-tenant deployments
+ * polaris.service-identity.my-realm.aws-iam.iam-arn=arn:aws:iam::123456789012:user/my-realm-user
+ * polaris.service-identity.my-realm.aws-iam.access-key-id=
+ * polaris.service-identity.my-realm.aws-iam.secret-access-key=
+ * }
*/ @ConfigMapping(prefix = "polaris.service-identity") public interface ServiceIdentityConfiguration { diff --git a/runtime/service/src/main/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProvider.java b/runtime/service/src/main/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProvider.java index cfc273d46d..bd11150b1b 100644 --- a/runtime/service/src/main/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProvider.java +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProvider.java @@ -21,6 +21,7 @@ import com.google.common.annotations.VisibleForTesting; import jakarta.annotation.Nonnull; +import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; import java.util.Map; import java.util.Optional; @@ -46,6 +47,7 @@ * identity information and credentials on demand. All resolution is done lazily - credentials are * only created when actually needed for authentication. */ +@RequestScoped public class DefaultServiceIdentityProvider implements ServiceIdentityProvider { public static final String DEFAULT_REALM_KEY = ServiceIdentityConfiguration.DEFAULT_REALM_KEY; public static final String DEFAULT_REALM_NSS = "system:default"; From f14b5d3527216b3b6c18aaa04de37d8d8303fc39 Mon Sep 17 00:00:00 2001 From: Rulin Xing Date: Fri, 3 Oct 2025 10:31:56 -0700 Subject: [PATCH 12/12] Disable Catalog Federation --- runtime/defaults/src/main/resources/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/runtime/defaults/src/main/resources/application.properties b/runtime/defaults/src/main/resources/application.properties index 2084422094..def1694c7c 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