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 8af3e18210..5422cd09fd 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 @@ -303,7 +303,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/connection/ConnectionConfigInfoDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/ConnectionConfigInfoDpo.java index bb95dc4429..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 @@ -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.provider.ServiceIdentityProvider; import org.apache.polaris.core.secrets.SecretReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -225,9 +226,10 @@ 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. */ - public abstract ConnectionConfigInfo asConnectionConfigInfoModel(); + public abstract ConnectionConfigInfo asConnectionConfigInfoModel( + 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 05104dd208..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,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.provider.ServiceIdentityProvider; import org.apache.polaris.core.secrets.UserSecretsManager; /** @@ -88,7 +89,8 @@ public ConnectionConfigInfoDpo withServiceIdentity( } @Override - public ConnectionConfigInfo asConnectionConfigInfoModel() { + public ConnectionConfigInfo asConnectionConfigInfoModel( + ServiceIdentityProvider serviceIdentityProvider) { 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(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 1f9027f2be..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 @@ -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.provider.ServiceIdentityProvider; import org.apache.polaris.core.secrets.UserSecretsManager; /** @@ -88,13 +90,20 @@ public ConnectionConfigInfoDpo withServiceIdentity( } @Override - public ConnectionConfigInfo asConnectionConfigInfoModel() { + public ConnectionConfigInfo asConnectionConfigInfoModel( + ServiceIdentityProvider serviceIdentityProvider) { return HiveConnectionConfigInfo.builder() .setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.HIVE) .setUri(getUri()) .setWarehouse(getWarehouse()) .setAuthenticationParameters( getAuthenticationParameters().asAuthenticationParametersModel()) + .setServiceIdentity( + Optional.ofNullable(getServiceIdentity()) + .map( + serviceIdentityInfoDpo -> + 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 0a6e870b7f..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,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.provider.ServiceIdentityProvider; import org.apache.polaris.core.secrets.UserSecretsManager; /** @@ -80,7 +81,8 @@ public ConnectionConfigInfoDpo withServiceIdentity( } @Override - public ConnectionConfigInfo asConnectionConfigInfoModel() { + public ConnectionConfigInfo asConnectionConfigInfoModel( + ServiceIdentityProvider serviceIdentityProvider) { 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(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 a312995047..055ccd8959 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,8 @@ 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.identity.provider.ServiceIdentityProvider; import org.apache.polaris.core.secrets.SecretReference; import org.apache.polaris.core.storage.FileStorageConfigurationInfo; import org.apache.polaris.core.storage.PolarisStorageConfigurationInfo; @@ -108,6 +110,10 @@ public static CatalogEntity fromCatalog(RealmConfig realmConfig, Catalog catalog } public Catalog asCatalog() { + return this.asCatalog(null); + } + + public Catalog asCatalog(ServiceIdentityProvider serviceIdentityProvider) { Map internalProperties = getInternalPropertiesAsMap(); Catalog.TypeEnum catalogType = Optional.ofNullable(internalProperties.get(CATALOG_TYPE_PROPERTY)) @@ -118,6 +124,12 @@ public Catalog asCatalog() { CatalogProperties.builder(propertiesMap.get(DEFAULT_BASE_LOCATION_KEY)) .putAll(propertiesMap) .build(); + + // Right now, only external catalog may use ServiceIdentityProvider to resolve identity + Preconditions.checkState( + catalogType != Catalog.TypeEnum.EXTERNAL || serviceIdentityProvider != null, + "%s catalog needs ServiceIdentityProvider to resolve service identities", + Catalog.TypeEnum.EXTERNAL); return catalogType == Catalog.TypeEnum.EXTERNAL ? ExternalCatalog.builder() .setType(Catalog.TypeEnum.EXTERNAL) @@ -127,7 +139,7 @@ public Catalog asCatalog() { .setLastUpdateTimestamp(getLastUpdateTimestamp()) .setEntityVersion(getEntityVersion()) .setStorageConfigInfo(getStorageInfo(internalProperties)) - .setConnectionConfigInfo(getConnectionInfo(internalProperties)) + .setConnectionConfigInfo(getConnectionInfo(internalProperties, serviceIdentityProvider)) .build() : PolarisCatalog.builder() .setType(Catalog.TypeEnum.INTERNAL) @@ -187,11 +199,12 @@ private StorageConfigInfo getStorageInfo(Map internalProperties) return null; } - private ConnectionConfigInfo getConnectionInfo(Map internalProperties) { + private ConnectionConfigInfo getConnectionInfo( + Map internalProperties, ServiceIdentityProvider serviceIdentityProvider) { if (internalProperties.containsKey( PolarisEntityConstants.getConnectionConfigInfoPropertyName())) { ConnectionConfigInfoDpo configInfo = getConnectionConfigInfoDpo(); - return configInfo.asConnectionConfigInfoModel(); + return configInfo.asConnectionConfigInfoModel(serviceIdentityProvider); } return null; } @@ -352,11 +365,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/credential/AwsIamServiceIdentityCredential.java b/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/AwsIamServiceIdentityCredential.java new file mode 100644 index 0000000000..ae85cbbe13 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/AwsIamServiceIdentityCredential.java @@ -0,0 +1,100 @@ +/* + * 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 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.SecretReference; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; + +/** + * Represents an AWS IAM service identity credential used by Polaris to authenticate to AWS + * services. + * + *

This credential encapsulates: + * + *

    + *
  • 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) + *
+ * + *

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 AwsIamServiceIdentityCredential extends ServiceIdentityCredential { + + /** IAM role or user ARN representing the Polaris service identity. */ + private final String iamArn; + + /** AWS credentials provider for accessing AWS services. */ + private final AwsCredentialsProvider awsCredentialsProvider; + + public AwsIamServiceIdentityCredential(@Nullable String iamArn) { + this(null, iamArn, DefaultCredentialsProvider.builder().build()); + } + + public AwsIamServiceIdentityCredential( + @Nullable String iamArn, @Nonnull AwsCredentialsProvider awsCredentialsProvider) { + this(null, iamArn, awsCredentialsProvider); + } + + public AwsIamServiceIdentityCredential( + @Nullable SecretReference secretReference, + @Nullable String iamArn, + @Nonnull AwsCredentialsProvider awsCredentialsProvider) { + super(ServiceIdentityType.AWS_IAM, secretReference); + this.iamArn = iamArn; + this.awsCredentialsProvider = awsCredentialsProvider; + } + + public @Nullable String getIamArn() { + return iamArn; + } + + public @Nonnull AwsCredentialsProvider getAwsCredentialsProvider() { + return awsCredentialsProvider; + } + + @Override + public @Nonnull ServiceIdentityInfoDpo asServiceIdentityInfoDpo() { + return new AwsIamServiceIdentityInfoDpo(getIdentityInfoReference()); + } + + @Override + public @Nonnull ServiceIdentityInfo asServiceIdentityInfoModel() { + return AwsIamServiceIdentityInfo.builder() + .setIdentityType(ServiceIdentityInfo.IdentityTypeEnum.AWS_IAM) + .setIamArn(getIamArn()) + .build(); + } +} 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 new file mode 100644 index 0000000000..635c6fcd60 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/credential/ServiceIdentityCredential.java @@ -0,0 +1,91 @@ +/* + * 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 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; +import org.apache.polaris.core.secrets.SecretReference; +import software.amazon.awssdk.annotations.NotNull; + +/** + * Represents a service identity credential used by Polaris to authenticate to external systems. + * + *

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 {@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) + *
+ */ +public abstract class ServiceIdentityCredential { + private final ServiceIdentityType identityType; + private SecretReference identityInfoReference; + + public ServiceIdentityCredential(@Nonnull ServiceIdentityType identityType) { + this(identityType, null); + } + + public ServiceIdentityCredential( + @Nonnull ServiceIdentityType identityType, @Nullable SecretReference identityInfoReference) { + this.identityType = identityType; + this.identityInfoReference = identityInfoReference; + } + + public @NotNull ServiceIdentityType getIdentityType() { + return identityType; + } + + public @Nonnull SecretReference getIdentityInfoReference() { + return identityInfoReference; + } + + public void setIdentityInfoReference(@NotNull SecretReference identityInfoReference) { + this.identityInfoReference = identityInfoReference; + } + + /** + * 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 cddad3a422..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 @@ -21,26 +21,26 @@ 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.identity.credential.AwsIamServiceIdentityCredential; import org.apache.polaris.core.secrets.SecretReference; /** * 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 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. * - *

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, 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 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 ServiceIdentityProvider. */ public class AwsIamServiceIdentityInfoDpo extends ServiceIdentityInfoDpo { @@ -51,15 +51,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..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,18 +23,22 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.google.common.base.MoreObjects; -import jakarta.annotation.Nonnull; +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; +import org.apache.polaris.core.identity.credential.ServiceIdentityCredential; +import org.apache.polaris.core.identity.provider.ServiceIdentityProvider; import org.apache.polaris.core.secrets.SecretReference; /** * 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 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, @@ -72,10 +76,24 @@ 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. + * + *

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 abstract @Nonnull ServiceIdentityInfo asServiceIdentityInfoModel(); + public @Nullable ServiceIdentityInfo asServiceIdentityInfoModel( + ServiceIdentityProvider serviceIdentityProvider) { + Preconditions.checkNotNull( + serviceIdentityProvider, + "Need ServiceIdentityProvider to inject service identity info, should not be null"); + return serviceIdentityProvider.getServiceIdentityInfo(this).orElse(null); + } @Override public String toString() { 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..c652ddbe13 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/identity/provider/ServiceIdentityProvider.java @@ -0,0 +1,103 @@ +/* + * 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 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; + +/** + * 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); + + /** + * 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 ServiceIdentityCredential} with credentials, or + * empty if the identity cannot be resolved. + */ + 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 727fac0d0f..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 @@ -22,9 +22,16 @@ 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.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.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 +40,24 @@ public class ConnectionConfigInfoDpoTest { objectMapper.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); } + private ServiceIdentityProvider serviceIdentityProvider; + + @BeforeEach + void setUp() { + 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.getServiceIdentityCredential(Mockito.any())) + .thenReturn( + Optional.of( + new AwsIamServiceIdentityCredential("arn:aws:iam::123456789012:user/test-user"))); + } + @Test void testOAuthClientCredentialsParameters() throws JsonProcessingException { // Test deserialization and reserialization of the persistence JSON. @@ -64,7 +89,7 @@ void testOAuthClientCredentialsParameters() throws JsonProcessingException { // Test conversion into API model JSON. ConnectionConfigInfo connectionConfigInfoApiModel = - connectionConfigInfoDpo.asConnectionConfigInfoModel(); + connectionConfigInfoDpo.asConnectionConfigInfoModel(serviceIdentityProvider); String expectedApiModelJson = "" + "{" @@ -111,7 +136,7 @@ void testBearerAuthenticationParameters() throws JsonProcessingException { // Test conversion into API model JSON. ConnectionConfigInfo connectionConfigInfoApiModel = - connectionConfigInfoDpo.asConnectionConfigInfoModel(); + connectionConfigInfoDpo.asConnectionConfigInfoModel(serviceIdentityProvider); String expectedApiModelJson = "" + "{" @@ -148,7 +173,7 @@ void testImplicitAuthenticationParameters() throws JsonProcessingException { // Test conversion into API model JSON. ConnectionConfigInfo connectionConfigInfoApiModel = - connectionConfigInfoDpo.asConnectionConfigInfoModel(); + connectionConfigInfoDpo.asConnectionConfigInfoModel(serviceIdentityProvider); String expectedApiModelJson = "" + "{" @@ -184,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\"" + " }" @@ -200,7 +225,7 @@ void testSigV4AuthenticationParameters() throws JsonProcessingException { // Test conversion into API model JSON. ConnectionConfigInfo connectionConfigInfoApiModel = - connectionConfigInfoDpo.asConnectionConfigInfoModel(); + connectionConfigInfoDpo.asConnectionConfigInfoModel(serviceIdentityProvider); String expectedApiModelJson = "" + "{" @@ -217,7 +242,7 @@ void testSigV4AuthenticationParameters() throws JsonProcessingException { + " }," + " \"serviceIdentity\": {" + " \"identityType\": \"AWS_IAM\"," - + " \"iamArn\": \"\"" + + " \"iamArn\": \"arn:aws:iam::123456789012:user/test-user\"" + " }" + "}"; Assertions.assertEquals( 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..ce69a4f089 --- /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.SecretReference; +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() { + SecretReference ref = new SecretReference("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() { + SecretReference ref = new SecretReference("urn:polaris-secret:test:reference", Map.of()); + AwsIamServiceIdentityCredential credential = + new AwsIamServiceIdentityCredential( + ref, + "arn:aws:iam::123456789012:user/test-user", + DefaultCredentialsProvider.builder().build()); + + 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 80459812d6..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 @@ -100,6 +100,8 @@ 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.dpo.ServiceIdentityInfoDpo; +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; @@ -149,6 +151,7 @@ public class PolarisAdminService { private final PolarisAuthorizer authorizer; private final PolarisMetaStoreManager metaStoreManager; private final UserSecretsManager userSecretsManager; + private final ServiceIdentityProvider serviceIdentityProvider; private final ReservedProperties reservedProperties; // Initialized in the authorize methods. @@ -161,6 +164,7 @@ public PolarisAdminService( @Nonnull ResolutionManifestFactory resolutionManifestFactory, @Nonnull PolarisMetaStoreManager metaStoreManager, @Nonnull UserSecretsManager userSecretsManager, + @Nonnull ServiceIdentityProvider serviceIdentityProvider, @Nonnull SecurityContext securityContext, @Nonnull PolarisAuthorizer authorizer, @Nonnull ReservedProperties reservedProperties) { @@ -179,6 +183,7 @@ public PolarisAdminService( this.polarisPrincipal = (PolarisPrincipal) securityContext.getUserPrincipal(); this.authorizer = authorizer; this.userSecretsManager = userSecretsManager; + this.serviceIdentityProvider = serviceIdentityProvider; this.reservedProperties = reservedProperties; } @@ -190,6 +195,10 @@ private UserSecretsManager getUserSecretsManager() { return userSecretsManager; } + private ServiceIdentityProvider getServiceIdentityProvider() { + return serviceIdentityProvider; + } + private PolarisResolutionManifest newResolutionManifest(@Nullable String catalogName) { return resolutionManifestFactory.createResolutionManifest(securityContext, catalogName); } @@ -672,6 +681,12 @@ private Map extractSecretReferences( AuthenticationParametersDpo.INLINE_BEARER_TOKEN_REFERENCE_KEY, secretReference); break; } + case SIGV4: + { + // SigV4 authentication is not based on users provided secrets but based on the + // service identity managed by Polaris. Nothing to do here. + break; + } default: throw new IllegalStateException( "Unsupported authentication type: " @@ -751,10 +766,19 @@ public PolarisEntity createCatalog(CreateCatalogRequest catalogRequest) { AuthenticationParameters.AuthenticationTypeEnum.IMPLICIT.name()), "Implicit authentication based catalog federation is not supported."); } + + // Allocate service identity if needed for the authentication type. + // 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) .setConnectionConfigInfoDpoWithSecrets( - connectionConfigInfo, processedSecretReferences) + connectionConfigInfo, + processedSecretReferences, + serviceIdentityInfoDpoOptional.orElse(null)) .build(); } } @@ -930,7 +954,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(getServiceIdentityProvider())) + .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 03a8001a85..eebd3aa161 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,6 +71,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.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,15 +92,18 @@ public class PolarisServiceImpl private final RealmConfig realmConfig; private final ReservedProperties reservedProperties; private final PolarisAdminService adminService; + private final ServiceIdentityProvider serviceIdentityProvider; @Inject public PolarisServiceImpl( RealmConfig realmConfig, ReservedProperties reservedProperties, - PolarisAdminService adminService) { + PolarisAdminService adminService, + ServiceIdentityProvider serviceIdentityProvider) { this.realmConfig = realmConfig; this.reservedProperties = reservedProperties; this.adminService = adminService; + this.serviceIdentityProvider = serviceIdentityProvider; } private static Response toResponse(BaseResult result, Response.Status successStatus) { @@ -126,7 +130,8 @@ public Response createCatalog( validateStorageConfig(catalog.getStorageConfigInfo()); validateExternalCatalog(catalog); validateCatalogProperties(catalog.getProperties()); - Catalog newCatalog = CatalogEntity.of(adminService.createCatalog(request)).asCatalog(); + Catalog newCatalog = + CatalogEntity.of(adminService.createCatalog(request)).asCatalog(serviceIdentityProvider); LOGGER.info("Created new catalog {}", newCatalog); return Response.status(Response.Status.CREATED).entity(newCatalog).build(); } @@ -237,7 +242,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(serviceIdentityProvider)) + .build(); } /** From PolarisCatalogsApiService */ @@ -251,7 +257,11 @@ public Response updateCatalog( validateStorageConfig(updateRequest.getStorageConfigInfo()); } validateCatalogProperties(updateRequest.getProperties()); - return Response.ok(adminService.updateCatalog(catalogName, updateRequest).asCatalog()).build(); + return Response.ok( + adminService + .updateCatalog(catalogName, updateRequest) + .asCatalog(serviceIdentityProvider)) + .build(); } /** From PolarisCatalogsApiService */ 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 6a70d29600..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 @@ -60,6 +60,7 @@ import org.apache.polaris.service.auth.AuthenticationRealmConfiguration; import org.apache.polaris.service.auth.AuthenticationType; import org.apache.polaris.service.auth.Authenticator; +import org.apache.polaris.service.auth.external.OidcConfiguration; import org.apache.polaris.service.auth.external.tenant.OidcTenantResolver; import org.apache.polaris.service.auth.internal.broker.TokenBroker; import org.apache.polaris.service.auth.internal.broker.TokenBrokerFactory; @@ -387,8 +388,7 @@ public AuthenticationRealmConfiguration realmAuthConfig( @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(); } 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..e92b28d882 --- /dev/null +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/AwsIamServiceIdentityConfiguration.java @@ -0,0 +1,141 @@ +/* + * 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 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.SecretReference; +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. + * + *

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 + * AwsIamServiceIdentityCredential}; 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(); + + /** + * 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; + } + + /** + * 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 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 SecretReference secretReference) { + if (iamArn() == null) { + return Optional.empty(); + } + return Optional.of( + new AwsIamServiceIdentityCredential(secretReference, 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/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..dc03aa0c5f --- /dev/null +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/ResolvableServiceIdentityConfiguration.java @@ -0,0 +1,75 @@ +/* + * 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 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.SecretReference; + +/** + * 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 + * 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; + } + + /** + * 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. + * + *

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 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 SecretReference secretReference) { + 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 new file mode 100644 index 0000000000..fc6a2ce42e --- /dev/null +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/ServiceIdentityConfiguration.java @@ -0,0 +1,114 @@ +/* + * 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; + +/** + * Configuration interface for managing service identities across multiple realms in Polaris. + * + *

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

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 { + /** + * 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(); + + /** + * 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()); + } + + /** + * 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 = + realms().containsKey(realmIdentifier) ? realmIdentifier : DEFAULT_REALM_KEY; + return new RealmConfigEntry(resolvedRealmIdentifier, realms().get(resolvedRealmIdentifier)); + } + + /** + * 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 new file mode 100644 index 0000000000..bd11150b1b --- /dev/null +++ b/runtime/service/src/main/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProvider.java @@ -0,0 +1,155 @@ +/* + * 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.provider; + +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; +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.SecretReference; +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 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. + */ +@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"; + private static final String IDENTITY_INFO_REFERENCE_URN_FORMAT = + "urn:polaris-secret:default-identity-provider:%s:%s"; + + private final String realm; + private final RealmServiceIdentityConfiguration config; + + public DefaultServiceIdentityProvider() { + this.realm = DEFAULT_REALM_KEY; + this.config = null; + } + + @Inject + public DefaultServiceIdentityProvider( + RealmContext realmContext, ServiceIdentityConfiguration serviceIdentityConfiguration) { + ServiceIdentityConfiguration.RealmConfigEntry entry = + serviceIdentityConfiguration.forRealm(realmContext); + this.realm = entry.realm(); + this.config = entry.config(); + } + + @Override + public Optional allocateServiceIdentity( + @Nonnull ConnectionConfigInfo connectionConfig) { + if (config == null || connectionConfig.getAuthenticationParameters() == null) { + return Optional.empty(); + } + + AuthenticationParameters.AuthenticationTypeEnum authType = + connectionConfig.getAuthenticationParameters().getAuthenticationType(); + + // 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) { + if (config == null) { + return Optional.empty(); + } + + // Find the configuration matching the reference and return metadata only + SecretReference actualRef = 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) { + if (config == null) { + return Optional.empty(); + } + + // Find the configuration matching the reference and resolve credential lazily + SecretReference ref = serviceIdentityInfo.getIdentityInfoReference(); + + return config.serviceIdentityConfigurations().stream() + .filter( + identityConfig -> + buildIdentityInfoReference(realm, identityConfig.getType()).equals(ref)) + .findFirst() + .flatMap(identityConfig -> identityConfig.asServiceIdentityCredential(ref)); + } + + @VisibleForTesting + public RealmServiceIdentityConfiguration getRealmConfig() { + return config; + } + + /** + * Builds a {@link SecretReference} for the given realm and service identity 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 secret reference for this service identity + */ + 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/admin/ManagementServiceTest.java b/runtime/service/src/test/java/org/apache/polaris/service/admin/ManagementServiceTest.java index 87f98be0e1..e13d6fe080 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 @@ -59,6 +59,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.provider.DefaultServiceIdentityProvider; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -376,6 +377,7 @@ private PolarisAdminService setupPolarisAdminService( services.resolutionManifestFactory(), metaStoreManager, new UnsafeInMemorySecretsManager(), + new DefaultServiceIdentityProvider(), 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 b60bcb0a92..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,6 +56,7 @@ private PolarisAdminService newTestAdminService(Set activatedPrincipalRo resolutionManifestFactory, metaStoreManager, userSecretsManager, + serviceIdentityProvider, securityContext(authenticatedPrincipal), polarisAuthorizer, reservedProperties); @@ -134,7 +135,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(serviceIdentityProvider)); doTestSufficientPrivileges( List.of( @@ -153,7 +155,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(serviceIdentityProvider)); doTestInsufficientPrivileges( List.of( @@ -288,7 +291,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(serviceIdentityProvider)); adminService.createCatalog(createRequest); doTestSufficientPrivileges( @@ -308,7 +312,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(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 0165d75dc9..411c837351 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,6 +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.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; @@ -71,6 +72,7 @@ public class PolarisAdminServiceTest { @Mock private ResolutionManifestFactory resolutionManifestFactory; @Mock private PolarisMetaStoreManager metaStoreManager; @Mock private UserSecretsManager userSecretsManager; + @Mock private ServiceIdentityProvider identityProvider; @Mock private SecurityContext securityContext; @Mock private PolarisAuthorizer authorizer; @Mock private ReservedProperties reservedProperties; @@ -107,6 +109,7 @@ void setUp() throws Exception { resolutionManifestFactory, metaStoreManager, userSecretsManager, + 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 d0239a83d4..4eb9b0f46e 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,6 +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.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; @@ -195,6 +196,7 @@ public Map getConfigOverrides() { @Inject protected ResolutionManifestFactory resolutionManifestFactory; @Inject protected CallContextCatalogFactory callContextCatalogFactory; @Inject protected UserSecretsManagerFactory userSecretsManagerFactory; + @Inject protected ServiceIdentityProvider serviceIdentityProvider; @Inject protected PolarisDiagnostics diagServices; @Inject protected FileIOFactory fileIOFactory; @Inject protected PolarisEventListener polarisEventListener; @@ -263,6 +265,7 @@ public void before(TestInfo testInfo) { resolutionManifestFactory, metaStoreManager, userSecretsManager, + serviceIdentityProvider, securityContext(authenticatedRoot), polarisAuthorizer, reservedProperties); @@ -329,7 +332,7 @@ public void before(TestInfo testInfo) { .setDefaultBaseLocation(storageLocation) .setStorageConfigurationInfo(realmConfig, storageConfigModel, storageLocation) .build() - .asCatalog())); + .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 926c6c89cf..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,9 +40,11 @@ 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.provider.ServiceIdentityProvider; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory; import org.apache.polaris.core.secrets.UserSecretsManager; +import org.apache.polaris.core.secrets.UserSecretsManagerFactory; import org.apache.polaris.service.config.ReservedProperties; import org.apache.polaris.service.events.listeners.NoOpPolarisEventListener; import org.apache.polaris.service.events.listeners.PolarisEventListener; @@ -54,8 +56,10 @@ public class PolarisServiceImplTest { private final PolarisDiagnostics diagnostics = new PolarisDefaultDiagServiceImpl(); private ResolutionManifestFactory resolutionManifestFactory; + private UserSecretsManagerFactory userSecretsManagerFactory; private PolarisMetaStoreManager metaStoreManager; private UserSecretsManager userSecretsManager; + private ServiceIdentityProvider serviceIdentityProvider; private PolarisAuthorizer polarisAuthorizer; private CallContext callContext; private ReservedProperties reservedProperties; @@ -70,6 +74,7 @@ void setUp() { resolutionManifestFactory = Mockito.mock(ResolutionManifestFactory.class); metaStoreManager = Mockito.mock(PolarisMetaStoreManager.class); userSecretsManager = Mockito.mock(UserSecretsManager.class); + serviceIdentityProvider = Mockito.mock(ServiceIdentityProvider.class); polarisAuthorizer = Mockito.mock(PolarisAuthorizer.class); callContext = Mockito.mock(CallContext.class); reservedProperties = Mockito.mock(ReservedProperties.class); @@ -93,10 +98,13 @@ void setUp() { resolutionManifestFactory, metaStoreManager, userSecretsManager, + serviceIdentityProvider, securityContext, polarisAuthorizer, reservedProperties); - polarisService = new PolarisServiceImpl(realmConfig, reservedProperties, adminService); + polarisService = + new PolarisServiceImpl( + 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 8e930dd510..1d1071dfed 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,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.provider.ServiceIdentityProvider; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory; @@ -98,6 +99,7 @@ public abstract class AbstractPolarisGenericTableCatalogTest { @Inject MetaStoreManagerFactory metaStoreManagerFactory; @Inject UserSecretsManagerFactory userSecretsManagerFactory; + @Inject ServiceIdentityProvider serviceIdentityProvider; @Inject PolarisConfigurationStore configurationStore; @Inject StorageCredentialCache storageCredentialCache; @Inject PolarisStorageIntegrationProvider storageIntegrationProvider; @@ -173,6 +175,7 @@ public void before(TestInfo testInfo) { resolutionManifestFactory, metaStoreManager, userSecretsManager, + serviceIdentityProvider, securityContext, authorizer, reservedProperties); @@ -202,7 +205,7 @@ public void before(TestInfo testInfo) { FeatureConfiguration.DROP_WITH_PURGE_ENABLED.catalogConfig(), "true") .setStorageConfigurationInfo(realmConfig, storageConfigModel, storageLocation) .build() - .asCatalog())); + .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 64654aeb81..2418bcfcce 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 @@ -110,6 +110,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.provider.ServiceIdentityProvider; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.PolarisResolvedPathWrapper; @@ -229,6 +230,7 @@ public Map getConfigOverrides() { @Inject StorageCredentialCache storageCredentialCache; @Inject PolarisStorageIntegrationProvider storageIntegrationProvider; @Inject UserSecretsManagerFactory userSecretsManagerFactory; + @Inject ServiceIdentityProvider serviceIdentityProvider; @Inject PolarisDiagnostics diagServices; @Inject PolarisEventListener polarisEventListener; @@ -318,6 +320,7 @@ public void before(TestInfo testInfo) { resolutionManifestFactory, metaStoreManager, userSecretsManager, + serviceIdentityProvider, securityContext, authorizer, reservedProperties); @@ -347,7 +350,7 @@ public void before(TestInfo testInfo) { FeatureConfiguration.DROP_WITH_PURGE_ENABLED.catalogConfig(), "true") .setStorageConfigurationInfo(realmConfig, storageConfigModel, storageLocation) .build() - .asCatalog())); + .asCatalog(serviceIdentityProvider))); this.fileIOFactory = new DefaultFileIOFactory(storageCredentialCache, metaStoreManagerFactory); @@ -1378,7 +1381,7 @@ public void testUpdateNotificationCreateTableWithLocalFilePrefix() { .setDefaultBaseLocation("file://") .setName(catalogWithoutStorage) .build() - .asCatalog())); + .asCatalog(serviceIdentityProvider))); IcebergCatalog catalog = newIcebergCatalog(catalogWithoutStorage); catalog.initialize( @@ -1428,7 +1431,7 @@ public void testUpdateNotificationCreateTableWithHttpPrefix() { .setDefaultBaseLocation("http://maliciousdomain.com") .setName(catalogName) .build() - .asCatalog())); + .asCatalog(serviceIdentityProvider))); IcebergCatalog catalog = newIcebergCatalog(catalogName); catalog.initialize( @@ -1946,7 +1949,7 @@ public void testDropTableWithPurgeDisabled() { .setStorageConfigurationInfo( realmConfig, noPurgeStorageConfigModel, storageLocation) .build() - .asCatalog())); + .asCatalog(serviceIdentityProvider))); IcebergCatalog noPurgeCatalog = newIcebergCatalog(noPurgeCatalogName, metaStoreManager, fileIOFactory); noPurgeCatalog.initialize( @@ -2204,7 +2207,7 @@ public void createCatalogWithReservedProperty() { .setName("createCatalogWithReservedProperty") .setProperties(ImmutableMap.of("polaris.reserved", "true")) .build() - .asCatalog())); + .asCatalog(serviceIdentityProvider))); }) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("reserved prefix"); @@ -2219,7 +2222,7 @@ public void updateCatalogWithReservedProperty() { .setName("updateCatalogWithReservedProperty") .setProperties(ImmutableMap.of("a", "b")) .build() - .asCatalog())); + .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 8c34f74f40..c4b68658a5 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,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.provider.ServiceIdentityProvider; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.resolver.ResolutionManifestFactory; @@ -107,6 +108,7 @@ public Map getConfigOverrides() { @Inject MetaStoreManagerFactory metaStoreManagerFactory; @Inject UserSecretsManagerFactory userSecretsManagerFactory; + @Inject ServiceIdentityProvider serviceIdentityProvider; @Inject PolarisConfigurationStore configurationStore; @Inject StorageCredentialCache storageCredentialCache; @Inject PolarisDiagnostics diagServices; @@ -180,6 +182,7 @@ public void before(TestInfo testInfo) { resolutionManifestFactory, metaStoreManager, userSecretsManager, + serviceIdentityProvider, securityContext, authorizer, reservedProperties); @@ -199,7 +202,7 @@ public void before(TestInfo testInfo) { StorageConfigInfo.StorageTypeEnum.FILE, List.of("file://", "/", "*")), "file://tmp") .build() - .asCatalog())); + .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 2c6493c1b6..f9827fddfa 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 @@ -1707,7 +1707,7 @@ public void testSendNotificationSufficientPrivileges() { .setStorageConfigurationInfo(realmConfig, storageConfigModel, storageLocation) .setCatalogType("EXTERNAL") .build() - .asCatalog())); + .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 8473198f72..9da8260eb3 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,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.provider.ServiceIdentityProvider; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; import org.apache.polaris.core.persistence.PolicyMappingAlreadyExistsException; @@ -124,6 +125,7 @@ public abstract class AbstractPolicyCatalogTest { @Inject MetaStoreManagerFactory metaStoreManagerFactory; @Inject UserSecretsManagerFactory userSecretsManagerFactory; + @Inject ServiceIdentityProvider serviceIdentityProvider; @Inject PolarisConfigurationStore configurationStore; @Inject StorageCredentialCache storageCredentialCache; @Inject PolarisStorageIntegrationProvider storageIntegrationProvider; @@ -194,6 +196,7 @@ public void before(TestInfo testInfo) { resolutionManifestFactory, metaStoreManager, userSecretsManager, + serviceIdentityProvider, securityContext, authorizer, reservedProperties); @@ -221,7 +224,7 @@ public void before(TestInfo testInfo) { "true") .setStorageConfigurationInfo(realmConfig, storageConfigModel, storageLocation) .build() - .asCatalog())); + .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 fceeaa54e7..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 @@ -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.credential.AwsIamServiceIdentityCredential; +import org.apache.polaris.core.identity.dpo.AwsIamServiceIdentityInfoDpo; +import org.apache.polaris.core.identity.provider.ServiceIdentityProvider; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -43,16 +54,30 @@ 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 ServiceIdentityProvider serviceIdentityProvider; @BeforeEach public void setup() { RealmContext realmContext = () -> "realm"; this.realmConfig = new RealmConfigImpl(new PolarisConfigurationStore() {}, realmContext); + 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.getServiceIdentityCredential(Mockito.any())) + .thenReturn( + Optional.of( + new AwsIamServiceIdentityCredential("arn:aws:iam::123456789012:user/test-user"))); } @ParameterizedTest @@ -278,7 +303,7 @@ public void testCatalogTypeDefaultsToInternal() { .setStorageConfigurationInfo(realmConfig, storageConfigModel, baseLocation) .build(); - Catalog catalog = catalogEntity.asCatalog(); + Catalog catalog = catalogEntity.asCatalog(serviceIdentityProvider); assertThat(catalog.getType()).isEqualTo(Catalog.TypeEnum.INTERNAL); } @@ -301,7 +326,7 @@ public void testCatalogTypeExternalPreserved() { .setStorageConfigurationInfo(realmConfig, storageConfigModel, baseLocation) .build(); - Catalog catalog = catalogEntity.asCatalog(); + Catalog catalog = catalogEntity.asCatalog(serviceIdentityProvider); assertThat(catalog.getType()).isEqualTo(Catalog.TypeEnum.EXTERNAL); } @@ -324,7 +349,7 @@ public void testCatalogTypeInternalExplicitlySet() { .setStorageConfigurationInfo(realmConfig, storageConfigModel, baseLocation) .build(); - Catalog catalog = catalogEntity.asCatalog(); + Catalog catalog = catalogEntity.asCatalog(serviceIdentityProvider); assertThat(catalog.getType()).isEqualTo(Catalog.TypeEnum.INTERNAL); } @@ -362,11 +387,70 @@ public void testAwsConfigRoundTrip(AwsStorageConfigInfo config) throws JsonProce config.getAllowedLocations().getFirst()) .build(); - Catalog catalog = catalogEntity.asCatalog(); + Catalog catalog = catalogEntity.asCatalog(serviceIdentityProvider); 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(serviceIdentityProvider); + 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/test/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProviderTest.java b/runtime/service/src/test/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProviderTest.java new file mode 100644 index 0000000000..9342994e07 --- /dev/null +++ b/runtime/service/src/test/java/org/apache/polaris/service/identity/provider/DefaultServiceIdentityProviderTest.java @@ -0,0 +1,369 @@ +/* + * 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.provider; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; +import jakarta.inject.Inject; +import java.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.SecretReference; +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.StaticCredentialsProvider; + +@QuarkusTest +@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"; + + @InjectMock RealmContext realmContext; + @Inject ServiceIdentityConfiguration serviceIdentityConfiguration; + + public static class Profile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Map.of( + "quarkus.identity-provider.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 + 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"); + 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 + 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"); + 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 + 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"); + Assertions.assertThat(otherConfig.awsIamServiceIdentity().get().accessKeyId()).isEmpty(); + Assertions.assertThat(otherConfig.awsIamServiceIdentity().get().secretAccessKey()).isEmpty(); + Assertions.assertThat(otherConfig.awsIamServiceIdentity().get().sessionToken()).isEmpty(); + } + + @Test + void testAwsIamConfigurationLoading() { + // Check the default realm + Mockito.when(realmContext.getRealmIdentifier()).thenReturn(DEFAULT_REALM_KEY); + DefaultServiceIdentityProvider defaultProvider = + new DefaultServiceIdentityProvider(realmContext, serviceIdentityConfiguration); + + 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(awsConfig.get().accessKeyId()).isEmpty(); + + // Check the my-realm with static credentials + Mockito.when(realmContext.getRealmIdentifier()).thenReturn(MY_REALM_KEY); + DefaultServiceIdentityProvider myRealmProvider = + new DefaultServiceIdentityProvider(realmContext, serviceIdentityConfiguration); + + awsConfig = myRealmProvider.getRealmConfig().awsIamServiceIdentity(); + Assertions.assertThat(awsConfig).isPresent(); + Assertions.assertThat(awsConfig.get().iamArn()) + .isEqualTo("arn:aws:iam::123456789012:user/polaris-iam-user"); + 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); + + awsConfig = otherProvider.getRealmConfig().awsIamServiceIdentity(); + Assertions.assertThat(awsConfig).isPresent(); + Assertions.assertThat(awsConfig.get().iamArn()) + .isEqualTo("arn:aws:iam::123456789012:user/polaris-default-iam-user"); + } + + @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 SecretReference( + "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 SecretReference( + "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( + myRealmProvider.getRealmConfig().awsIamServiceIdentity().get().accessKeyId()) + .isEqualTo(Optional.of("access-key-id")); + } + + @Test + void testBuildIdentityInfoReferenceForDefaultRealm() { + // Test URN generation for default realm + SecretReference 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 + SecretReference ref = + DefaultServiceIdentityProvider.buildIdentityInfoReference( + "custom-realm", ServiceIdentityType.AWS_IAM); + + Assertions.assertThat(ref.getUrn()) + .isEqualTo("urn:polaris-secret:default-identity-provider:custom-realm:AWS_IAM"); + } +} 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 044398daf1..d118e52e9d 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,6 +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.provider.ServiceIdentityProvider; import org.apache.polaris.core.persistence.BasePersistence; import org.apache.polaris.core.persistence.MetaStoreManagerFactory; import org.apache.polaris.core.persistence.PolarisMetaStoreManager; @@ -74,6 +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.provider.DefaultServiceIdentityProvider; import org.apache.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory; import org.apache.polaris.service.secrets.UnsafeInMemorySecretsManagerFactory; import org.apache.polaris.service.storage.PolarisStorageIntegrationProviderImpl; @@ -217,6 +219,7 @@ public TestServices build() { UserSecretsManager userSecretsManager = userSecretsManagerFactory.getOrCreateUserSecretsManager(realmContext); + ServiceIdentityProvider serviceIdentityProvider = new DefaultServiceIdentityProvider(); FileIOFactory fileIOFactory = fileIOFactorySupplier.apply(storageCredentialCache, metaStoreManagerFactory); @@ -304,12 +307,14 @@ public String getAuthenticationScheme() { resolutionManifestFactory, metaStoreManager, userSecretsManager, + serviceIdentityProvider, securityContext, authorizer, reservedProperties); PolarisCatalogsApi catalogsApi = new PolarisCatalogsApi( - new PolarisServiceImpl(realmConfig, reservedProperties, adminService)); + new PolarisServiceImpl( + realmConfig, reservedProperties, adminService, serviceIdentityProvider)); return new TestServices( clock,