From a56d546d9a6d0bf7dcc83aa9d792ce38073908d8 Mon Sep 17 00:00:00 2001 From: PhillHenry Date: Fri, 6 Feb 2026 12:44:28 +0000 Subject: [PATCH] feat: Be Able to Configure an External Catalog to Use org.apache.iceberg.gcp.auth.GoogleAuthManager as it's AuthManager (#3451). It allows an "End-User Credentials (Pass-Through) Approach" in that Polaris provides credentials for accessing storage rather than taking them from the BigLake metastore. As part of the work, stopped a transitive dependency pulling in incompatible versions of proto-google-cloud-iamcredentials-v1 (protobuf 4.33.2) vs. protobuf-java (4.32.1). ProtobufSmokeTest should stop future regressions. New DTO and DPO created but no changes were made to others. --- .../core/config/FeatureConfiguration.java | 1 + .../AuthenticationParametersDpo.java | 4 + .../core/connection/AuthenticationType.java | 1 + .../connection/ConnectionConfigInfoDpo.java | 3 +- .../GcpAuthenticationParametersDpo.java | 72 +++++ .../IcebergRestConnectionConfigInfoDpo.java | 34 ++- .../GcpAuthenticationParametersDpoTest.java | 82 ++++++ ...cebergRestConnectionConfigInfoDpoTest.java | 70 +++++ runtime/server/build.gradle.kts | 5 +- .../storage/gcp/ProtobufSmokeTest.java | 40 +++ runtime/service/README.md | 8 + .../it/GcpCatalogFederationIntegrationIT.java | 277 ++++++++++++++++++ .../service/admin/PolarisAdminService.java | 2 + .../DefaultPolarisCredentialManagerTest.java | 10 +- .../credentials/TestObjectFactory.java | 34 +++ .../BearerConnectionCredentialVendorTest.java | 9 +- ...mplicitConnectionCredentialVendorTest.java | 5 +- .../OAuthClientCredentialVendorTest.java | 9 +- .../SigV4ConnectionCredentialVendorTest.java | 13 +- .../config-sections/flags-polaris_features.md | 2 +- spec/polaris-management-service.yml | 8 + 21 files changed, 657 insertions(+), 32 deletions(-) create mode 100644 polaris-core/src/main/java/org/apache/polaris/core/connection/GcpAuthenticationParametersDpo.java create mode 100644 polaris-core/src/test/java/org/apache/polaris/core/connection/GcpAuthenticationParametersDpoTest.java create mode 100644 polaris-core/src/test/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpoTest.java create mode 100644 runtime/server/src/test/java/org/apache/polaris/service/storage/gcp/ProtobufSmokeTest.java create mode 100644 runtime/service/src/cloudTest/java/org/apache/polaris/service/it/GcpCatalogFederationIntegrationIT.java create mode 100644 runtime/service/src/test/java/org/apache/polaris/service/credentials/TestObjectFactory.java diff --git a/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java b/polaris-core/src/main/java/org/apache/polaris/core/config/FeatureConfiguration.java index 48eed5b2309..46a757973be 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 @@ -373,6 +373,7 @@ public static void enforceFeatureEnabledOrThrow( List.of( AuthenticationParameters.AuthenticationTypeEnum.OAUTH.name(), AuthenticationParameters.AuthenticationTypeEnum.BEARER.name(), + AuthenticationParameters.AuthenticationTypeEnum.GCP.name(), AuthenticationParameters.AuthenticationTypeEnum.SIGV4.name())) .buildFeatureConfiguration(); diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/AuthenticationParametersDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/AuthenticationParametersDpo.java index 923d3a5f84e..b4d6c52a96f 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/connection/AuthenticationParametersDpo.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/AuthenticationParametersDpo.java @@ -43,6 +43,7 @@ @JsonSubTypes.Type(value = BearerAuthenticationParametersDpo.class, name = "2"), @JsonSubTypes.Type(value = ImplicitAuthenticationParametersDpo.class, name = "3"), @JsonSubTypes.Type(value = SigV4AuthenticationParametersDpo.class, name = "4"), + @JsonSubTypes.Type(value = GcpAuthenticationParametersDpo.class, name = "5"), }) public abstract class AuthenticationParametersDpo implements IcebergCatalogPropertiesProvider { @@ -73,6 +74,9 @@ public static AuthenticationParametersDpo fromAuthenticationParametersModelWithS Map secretReferences) { final AuthenticationParametersDpo config; switch (authenticationParameters.getAuthenticationType()) { + case GCP: + config = new GcpAuthenticationParametersDpo(); + break; case OAUTH: OAuthClientCredentialsParameters oauthClientCredentialsModel = (OAuthClientCredentialsParameters) authenticationParameters; diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/AuthenticationType.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/AuthenticationType.java index 334c4c14766..daaddc82573 100644 --- a/polaris-core/src/main/java/org/apache/polaris/core/connection/AuthenticationType.java +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/AuthenticationType.java @@ -35,6 +35,7 @@ public enum AuthenticationType { BEARER(2), IMPLICIT(3), SIGV4(4), + GCP(5), ; private static final AuthenticationType[] REVERSE_MAPPING_ARRAY; 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 bc9f18ab9c3..7a3bc6d7d35 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 @@ -205,7 +205,8 @@ public static ConnectionConfigInfoDpo fromConnectionConfigInfoModelWithSecrets( icebergRestConfigModel.getUri(), authenticationParameters, null /*Service Identity Info*/, - icebergRestConfigModel.getRemoteCatalogName()); + icebergRestConfigModel.getRemoteCatalogName(), + icebergRestConfigModel.getProperties()); break; case HADOOP: HadoopConnectionConfigInfo hadoopConfigModel = diff --git a/polaris-core/src/main/java/org/apache/polaris/core/connection/GcpAuthenticationParametersDpo.java b/polaris-core/src/main/java/org/apache/polaris/core/connection/GcpAuthenticationParametersDpo.java new file mode 100644 index 00000000000..f3c19f2c457 --- /dev/null +++ b/polaris-core/src/main/java/org/apache/polaris/core/connection/GcpAuthenticationParametersDpo.java @@ -0,0 +1,72 @@ +/* + * 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.connection; + +import jakarta.annotation.Nonnull; +import java.util.HashMap; +import java.util.Map; +import org.apache.iceberg.rest.auth.AuthProperties; +import org.apache.polaris.core.admin.model.AuthenticationParameters; +import org.apache.polaris.core.admin.model.GcpAuthenticationParameters; +import org.apache.polaris.core.credentials.PolarisCredentialManager; + +/** + * See {@link org.apache.iceberg.rest.RESTUtil#configHeaders(Map)} and {@link + * org.apache.iceberg.rest.auth.AuthManagers#loadAuthManager(String, Map)} for why we do this. + */ +public class GcpAuthenticationParametersDpo extends AuthenticationParametersDpo { + + public GcpAuthenticationParametersDpo() { + super(AuthenticationType.GCP.getCode()); + } + + @Nonnull + @Override + public Map asIcebergCatalogProperties( + PolarisCredentialManager credentialManager) { + HashMap properties = new HashMap<>(); + properties.put(AuthProperties.AUTH_TYPE, AuthProperties.AUTH_TYPE_GOOGLE); + return properties; + } + + @Nonnull + @Override + public GcpAuthenticationParameters asAuthenticationParametersModel() { + return GcpAuthenticationParameters.builder() + .setAuthenticationType(AuthenticationParameters.AuthenticationTypeEnum.GCP) + .build(); + } + + @Override + public String toString() { + return "GcpAuthenticationParametersDpo{}"; + } + + @Override + public boolean equals(Object o) { + if (o == null || !(o instanceof GcpAuthenticationParametersDpo that)) return false; + return true; + } + + @Override + public int hashCode() { + return -1; + } +} 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 56e3144fd04..0cfdff68cc4 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 @@ -23,6 +23,7 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import org.apache.iceberg.CatalogProperties; @@ -43,8 +44,16 @@ public class IcebergRestConnectionConfigInfoDpo extends ConnectionConfigInfoDpo implements IcebergCatalogPropertiesProvider { + public static final String GOOGLE_USER_PROJECT_HEADER_KEY = "header.x-goog-user-project"; + + private static final List ALLOWED_PROPERTIES = List.of(GOOGLE_USER_PROJECT_HEADER_KEY); + private final String remoteCatalogName; + /** + * @param properties Properties that might be specifically needed for a particular implementation + * of a REST API. + */ public IcebergRestConnectionConfigInfoDpo( @JsonProperty(value = "uri", required = true) @Nonnull String uri, @JsonProperty(value = "authenticationParameters", required = true) @Nonnull @@ -52,9 +61,15 @@ public IcebergRestConnectionConfigInfoDpo( @JsonProperty(value = "serviceIdentity", required = false) @Nullable ServiceIdentityInfoDpo serviceIdentityInfo, @JsonProperty(value = "remoteCatalogName", required = false) @Nullable - String remoteCatalogName) { + String remoteCatalogName, + @JsonProperty(value = "properties", required = false) @Nullable + Map properties) { super( - ConnectionType.ICEBERG_REST.getCode(), uri, authenticationParameters, serviceIdentityInfo); + ConnectionType.ICEBERG_REST.getCode(), + uri, + authenticationParameters, + serviceIdentityInfo, + properties); this.remoteCatalogName = remoteCatalogName; } @@ -72,6 +87,13 @@ public String getRemoteCatalogName() { } // Add authentication-specific metadata (non-credential properties) properties.putAll(getAuthenticationParameters().asIcebergCatalogProperties(credentialManager)); + + for (String headerKey : ALLOWED_PROPERTIES) { + if (getProperties().containsKey(headerKey)) { + properties.put(headerKey, getProperties().get(headerKey)); + } + } + // Add connection credentials from Polaris credential manager ConnectionCredentials connectionCredentials = credentialManager.getConnectionCredentials(this); properties.putAll(connectionCredentials.credentials()); @@ -82,7 +104,11 @@ public String getRemoteCatalogName() { public ConnectionConfigInfoDpo withServiceIdentity( @Nonnull ServiceIdentityInfoDpo serviceIdentityInfo) { return new IcebergRestConnectionConfigInfoDpo( - getUri(), getAuthenticationParameters(), serviceIdentityInfo, getRemoteCatalogName()); + getUri(), + getAuthenticationParameters(), + serviceIdentityInfo, + getRemoteCatalogName(), + getProperties()); } @Override @@ -100,6 +126,7 @@ public ConnectionConfigInfo asConnectionConfigInfoModel( serviceIdentityInfoDpo -> serviceIdentityInfoDpo.asServiceIdentityInfoModel(serviceIdentityProvider)) .orElse(null)) + .setProperties(getProperties()) .build(); } @@ -111,6 +138,7 @@ public String toString() { .add("remoteCatalogName", getRemoteCatalogName()) .add("authenticationParameters", getAuthenticationParameters().toString()) .add("serviceIdentity", getServiceIdentity()) + .add("properties", getProperties()) .toString(); } } diff --git a/polaris-core/src/test/java/org/apache/polaris/core/connection/GcpAuthenticationParametersDpoTest.java b/polaris-core/src/test/java/org/apache/polaris/core/connection/GcpAuthenticationParametersDpoTest.java new file mode 100644 index 00000000000..ca50464f795 --- /dev/null +++ b/polaris-core/src/test/java/org/apache/polaris/core/connection/GcpAuthenticationParametersDpoTest.java @@ -0,0 +1,82 @@ +/* + * 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.connection; + +import static java.util.stream.Collectors.toUnmodifiableSet; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCharSequence; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Map; +import java.util.Set; +import org.apache.polaris.core.admin.model.GcpAuthenticationParameters; +import org.apache.polaris.core.connection.iceberg.IcebergRestConnectionConfigInfoDpo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class GcpAuthenticationParametersDpoTest { + + private GcpAuthenticationParametersDpo dpo; + + @BeforeEach + void setUp() { + dpo = new GcpAuthenticationParametersDpo(); + } + + @Test + void testSerializeAndDeserialize() throws Exception { + var connectionConfig = + new IcebergRestConnectionConfigInfoDpo( + "https://biglake.googleapis.com/iceberg/v1/restcatalog", + dpo, + null, + null, + Map.of("x", "y")); + + assertThatCharSequence(connectionConfig.toString()) + .isEqualTo(ConnectionConfigInfoDpo.deserialize(connectionConfig.serialize()).toString()); + } + + @Test + void testConversionToDTOCapturesAllFields() { + GcpAuthenticationParameters authenticationParameters = dpo.asAuthenticationParametersModel(); + Set dtoGetMethods = + Arrays.stream(GcpAuthenticationParameters.class.getDeclaredMethods()) + .map(Method::getName) + .filter(x -> x.startsWith("get")) + .collect(toUnmodifiableSet()); + dtoGetMethods.stream() + .forEach( + x -> { + try { + var expected = + GcpAuthenticationParametersDpo.class.getMethod(x, (Class) null).invoke(dpo); + var actual = + GcpAuthenticationParameters.class + .getMethod(x, (Class) null) + .invoke(authenticationParameters); + assertThat(expected).isEqualTo(actual); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } +} diff --git a/polaris-core/src/test/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpoTest.java b/polaris-core/src/test/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpoTest.java new file mode 100644 index 00000000000..6c8cd030ed9 --- /dev/null +++ b/polaris-core/src/test/java/org/apache/polaris/core/connection/iceberg/IcebergRestConnectionConfigInfoDpoTest.java @@ -0,0 +1,70 @@ +/* + * 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.connection.iceberg; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCharSequence; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Map; +import org.apache.polaris.core.admin.model.ConnectionConfigInfo; +import org.apache.polaris.core.connection.ConnectionConfigInfoDpo; +import org.apache.polaris.core.connection.GcpAuthenticationParametersDpo; +import org.apache.polaris.core.credentials.PolarisCredentialManager; +import org.apache.polaris.core.credentials.connection.ConnectionCredentials; +import org.jspecify.annotations.NonNull; +import org.junit.jupiter.api.Test; + +class IcebergRestConnectionConfigInfoDpoTest { + + @Test + void testRoundTrip() { + IcebergRestConnectionConfigInfoDpo dpo = createDpo(Map.of("x", "y")); + ConnectionConfigInfo dto = dpo.asConnectionConfigInfoModel(null); + assertThatCharSequence(dpo.toString()) + .isEqualTo( + ConnectionConfigInfoDpo.fromConnectionConfigInfoModelWithSecrets(dto, Map.of()) + .toString()); + } + + @Test + void testNullAdditionalHeadersHandledGracefully() { + IcebergRestConnectionConfigInfoDpo dpo = createDpo(null); + PolarisCredentialManager mockCredentialManager = mock(PolarisCredentialManager.class); + ConnectionCredentials mockCredentials = mock(ConnectionCredentials.class); + String expectedKey = "credential_key"; + String expectedValue = "credential_value"; + when(mockCredentials.credentials()).thenReturn(Map.of(expectedKey, expectedValue)); + when(mockCredentialManager.getConnectionCredentials(dpo)).thenReturn(mockCredentials); + Map properties = dpo.asIcebergCatalogProperties(mockCredentialManager); + assertThat(properties).containsEntry(expectedKey, expectedValue); + } + + private static @NonNull IcebergRestConnectionConfigInfoDpo createDpo( + Map additionalHeaders) { + return new IcebergRestConnectionConfigInfoDpo( + "https://biglake.googleapis.com/iceberg/v1/restcatalog", + new GcpAuthenticationParametersDpo(), + null, + null, + additionalHeaders); + } +} diff --git a/runtime/server/build.gradle.kts b/runtime/server/build.gradle.kts index b6dade0943c..c75653a3ba3 100644 --- a/runtime/server/build.gradle.kts +++ b/runtime/server/build.gradle.kts @@ -48,7 +48,10 @@ dependencies { } // enforce the Quarkus _platform_ here, to get a consistent and validated set of dependencies - implementation(enforcedPlatform(libs.quarkus.bom)) + implementation(enforcedPlatform(libs.quarkus.bom)) { + exclude(group = "com.google.protobuf", module = "protobuf-java") + exclude(group = "com.google.protobuf", module = "protobuf-java-util") + } implementation("io.quarkus:quarkus-container-image-docker") } diff --git a/runtime/server/src/test/java/org/apache/polaris/service/storage/gcp/ProtobufSmokeTest.java b/runtime/server/src/test/java/org/apache/polaris/service/storage/gcp/ProtobufSmokeTest.java new file mode 100644 index 00000000000..8e5f071f265 --- /dev/null +++ b/runtime/server/src/test/java/org/apache/polaris/service/storage/gcp/ProtobufSmokeTest.java @@ -0,0 +1,40 @@ +/* + * 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.storage.gcp; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ProtobufSmokeTest { + + /** + * The static initializers of com.google.cloud.iam.credentials.v1.GenerateAccessTokenRequest + * will throw an error if the versions of libraries 'proto-google-cloud-iamcredentials-v1' + * and 'protobuf-java' are not compatible + */ + @Test + public void testProtobufVersions() { + Assertions.assertThatCode( + () -> + Class.forName("com.google.cloud.iam.credentials.v1.GenerateAccessTokenRequest") + .getName()) + .doesNotThrowAnyException(); + } +} diff --git a/runtime/service/README.md b/runtime/service/README.md index fd4893a505f..54a12c1e5df 100644 --- a/runtime/service/README.md +++ b/runtime/service/README.md @@ -46,3 +46,11 @@ export INTEGRATION_TEST_GCS_PATH="gs://bucket/subpath" export INTEGRATION_TEST_GCS_SERVICE_ACCOUNT="your-service-account" ./gradlew :polaris-runtime-service:cloudTest ``` +To run the GCP federated catalog test using end-user credentials: +```shell +export INTEGRATION_TEST_GCS_FEDERATED_CATALOG=YOUR_GCS_WAREHOUSE +export INTEGRATION_TEST_GCS_FEDERATED_PATH=YOUR_GCS_BUCKET_PATH +export INTEGRATION_TEST_GCS_FEDERATED_QUOTA_PROJECT=YOUR_PROJECT_ID +export INTEGRATION_TEST_GCS_SERVICE_ACCOUNT=YOUR_SERVICE_ACCOUNT_USER@YOUR_PROJECT_ID.iam.gserviceaccount.com +./gradlew :polaris-runtime-service:cloudTest --tests "org.apache.polaris.service.it.GcpCatalogFederationIntegrationIT" +``` \ No newline at end of file diff --git a/runtime/service/src/cloudTest/java/org/apache/polaris/service/it/GcpCatalogFederationIntegrationIT.java b/runtime/service/src/cloudTest/java/org/apache/polaris/service/it/GcpCatalogFederationIntegrationIT.java new file mode 100644 index 00000000000..af52a3c4759 --- /dev/null +++ b/runtime/service/src/cloudTest/java/org/apache/polaris/service/it/GcpCatalogFederationIntegrationIT.java @@ -0,0 +1,277 @@ +/* + * 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.it; + +import static org.apache.iceberg.types.Types.NestedField.optional; +import static org.apache.iceberg.types.Types.NestedField.required; +import static org.apache.polaris.core.connection.iceberg.IcebergRestConnectionConfigInfoDpo.GOOGLE_USER_PROJECT_HEADER_KEY; +import static org.apache.polaris.service.it.env.PolarisClient.polarisClient; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import org.apache.iceberg.DataFile; +import org.apache.iceberg.DataFiles; +import org.apache.iceberg.FileFormat; +import org.apache.iceberg.ManifestFile; +import org.apache.iceberg.PartitionSpec; +import org.apache.iceberg.Schema; +import org.apache.iceberg.Snapshot; +import org.apache.iceberg.Table; +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.io.FileIO; +import org.apache.iceberg.io.OutputFile; +import org.apache.iceberg.rest.RESTCatalog; +import org.apache.iceberg.rest.auth.OAuth2Properties; +import org.apache.iceberg.types.Types; +import org.apache.polaris.core.admin.model.AuthenticationParameters; +import org.apache.polaris.core.admin.model.Catalog; +import org.apache.polaris.core.admin.model.CatalogGrant; +import org.apache.polaris.core.admin.model.CatalogPrivilege; +import org.apache.polaris.core.admin.model.CatalogProperties; +import org.apache.polaris.core.admin.model.CatalogRole; +import org.apache.polaris.core.admin.model.ConnectionConfigInfo; +import org.apache.polaris.core.admin.model.ExternalCatalog; +import org.apache.polaris.core.admin.model.GcpAuthenticationParameters; +import org.apache.polaris.core.admin.model.GcpStorageConfigInfo; +import org.apache.polaris.core.admin.model.GrantResource; +import org.apache.polaris.core.admin.model.IcebergRestConnectionConfigInfo; +import org.apache.polaris.core.admin.model.PrincipalWithCredentials; +import org.apache.polaris.core.admin.model.StorageConfigInfo; +import org.apache.polaris.service.it.env.CatalogApi; +import org.apache.polaris.service.it.env.ClientCredentials; +import org.apache.polaris.service.it.env.ManagementApi; +import org.apache.polaris.service.it.env.PolarisApiEndpoints; +import org.apache.polaris.service.it.env.PolarisClient; +import org.apache.polaris.service.it.ext.PolarisIntegrationTestExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * You need to run your Polaris with: polaris.features."ENABLE_CATALOG_FEDERATION"=true for this + * test to run against it successfully. + */ +@ExtendWith(PolarisIntegrationTestExtension.class) +@EnabledIfEnvironmentVariable(named = "INTEGRATION_TEST_GCS_FEDERATED_PATH", matches = ".+") +public class GcpCatalogFederationIntegrationIT { + + private static final Logger LOGGER = + LoggerFactory.getLogger(GcpCatalogFederationIntegrationIT.class); + private static final String BIG_LAKE_REST_URI = + "https://biglake.googleapis.com/iceberg/v1/restcatalog"; + private static CatalogApi catalogApi; + + static { + String googleCredentials = System.getenv("GOOGLE_APPLICATION_CREDENTIALS"); + if (googleCredentials == null) { + LOGGER.warn("GOOGLE_APPLICATION_CREDENTIALS environment variable is not set"); + } else { + LOGGER.info("GOOGLE_APPLICATION_CREDENTIALS defined"); + } + } + + private static final String SERVICE_ACCOUNT = + System.getenv("INTEGRATION_TEST_GCS_SERVICE_ACCOUNT"); + private static final String FEDERATED_CATALOG = + System.getenv("INTEGRATION_TEST_GCS_FEDERATED_CATALOG"); + private static final String QUOTA_PROJECT = + System.getenv("INTEGRATION_TEST_GCS_FEDERATED_QUOTA_PROJECT"); + private static final String BASE_LOCATION = System.getenv("INTEGRATION_TEST_GCS_FEDERATED_PATH"); + + private static final String PRINCIPAL_NAME = "test-catalog-federation-user"; + private static final String PRINCIPAL_ROLE_NAME = "test-catalog-federation-user-role"; + private String localCatalogName; + + private static final CatalogGrant DEFAULT_CATALOG_GRANT = + CatalogGrant.builder() + .setType(GrantResource.TypeEnum.CATALOG) + .setPrivilege(CatalogPrivilege.CATALOG_MANAGE_CONTENT) + .build(); + + private static PolarisApiEndpoints endpoints; + private static PolarisClient client; + private static ManagementApi managementApi; + private PrincipalWithCredentials newUserCredentials; + + @BeforeAll + static void setup(PolarisApiEndpoints apiEndpoints, ClientCredentials credentials) { + endpoints = apiEndpoints; + client = polarisClient(endpoints); + String adminToken = client.obtainToken(credentials); + managementApi = client.managementApi(adminToken); + catalogApi = client.catalogApi(adminToken); + } + + @BeforeEach + void before() { + setupCatalogs(); + } + + @AfterEach + void tearDown() { + managementApi.dropCatalog(localCatalogName); + managementApi.deletePrincipalRole(PRINCIPAL_ROLE_NAME); + managementApi.deletePrincipal(PRINCIPAL_NAME); + } + + private void setupCatalogs() { + purgePolaris(); + LOGGER.info( + "federated catalog name = {}, service account = {}, base location = {}, quota project = {}", + FEDERATED_CATALOG, + SERVICE_ACCOUNT, + BASE_LOCATION, + QUOTA_PROJECT); + newUserCredentials = managementApi.createPrincipalWithRole(PRINCIPAL_NAME, PRINCIPAL_ROLE_NAME); + localCatalogName = "test_catalog" + UUID.randomUUID().toString().replace("-", ""); + createCatalog(); + permissionCatalog(); + } + + private void permissionCatalog() { + String localCatalogRoleName = + "test-catalog-role_" + UUID.randomUUID().toString().replace("-", ""); + managementApi.createCatalogRole(localCatalogName, localCatalogRoleName); + managementApi.addGrant(localCatalogName, localCatalogRoleName, DEFAULT_CATALOG_GRANT); + CatalogRole localCatalogRole = + managementApi.getCatalogRole(localCatalogName, localCatalogRoleName); + managementApi.grantCatalogRoleToPrincipalRole( + PRINCIPAL_ROLE_NAME, localCatalogName, localCatalogRole); + } + + private void createCatalog() { + CatalogProperties bucketProperties = new CatalogProperties(BASE_LOCATION); + bucketProperties.put("enable.credential.vending", "true"); + + GcpAuthenticationParameters authenticationParameters = + new GcpAuthenticationParameters(AuthenticationParameters.AuthenticationTypeEnum.GCP); + + IcebergRestConnectionConfigInfo connectionInfo = + IcebergRestConnectionConfigInfo.builder() + .setRemoteCatalogName(FEDERATED_CATALOG) + .setConnectionType(ConnectionConfigInfo.ConnectionTypeEnum.ICEBERG_REST) + .setProperties(Map.of(GOOGLE_USER_PROJECT_HEADER_KEY, QUOTA_PROJECT)) + .setAuthenticationParameters(authenticationParameters) + .setUri(BIG_LAKE_REST_URI) + .build(); + + ExternalCatalog catalog = + new ExternalCatalog( + connectionInfo, + Catalog.TypeEnum.EXTERNAL, + localCatalogName, + bucketProperties, + System.currentTimeMillis(), + System.currentTimeMillis(), + null, + GcpStorageConfigInfo.builder() + .setGcsServiceAccount(SERVICE_ACCOUNT) + .setStorageType(StorageConfigInfo.StorageTypeEnum.GCS) + .setAllowedLocations(List.of(BASE_LOCATION)) + .build()); + + managementApi.createCatalog(catalog); + } + + private void purgePolaris() { + managementApi.listPrincipals().stream() + .filter(p -> p.getName().equals(PRINCIPAL_NAME)) + .forEach(p -> managementApi.deletePrincipal(p.getName())); + managementApi.listPrincipalRoles().stream() + .filter(r -> r.getName().equals(PRINCIPAL_ROLE_NAME)) + .forEach(r -> managementApi.deletePrincipalRole(r.getName())); + } + + private static final Schema SCHEMA = + new Schema( + required(1, "id", Types.IntegerType.get(), "doc"), + optional(2, "data", Types.StringType.get())); + + @Test + void testFederatedCatalogBasicReadWriteOperations() { + String namespace = "test_namespace" + UUID.randomUUID().toString().replace("-", ""); + String tableName = "test_table"; + catalogApi.createNamespace(localCatalogName, namespace); + TableIdentifier id = TableIdentifier.of(namespace, tableName); + RESTCatalog restCatalog = new RESTCatalog(); + String userToken = client.obtainToken(newUserCredentials); + ImmutableMap.Builder propertiesBuilder = + ImmutableMap.builder() + .put( + org.apache.iceberg.CatalogProperties.URI, endpoints.catalogApiEndpoint().toString()) + .put(OAuth2Properties.TOKEN, userToken) + .put("warehouse", localCatalogName) + .put("header.X-Iceberg-Access-Delegation", "vended-credentials") + .putAll(endpoints.extraHeaders("header.")); + + restCatalog.initialize("polaris", propertiesBuilder.buildKeepingLast()); + Table table = restCatalog.createTable(id, SCHEMA); + assertThat(table).isNotNull(); + assertThat(table.currentSnapshot()).isNull(); + writeRowTo(table); + assertThat(rowCountFor(table)).isEqualTo(1); + } + + private void writeRowTo(Table table) { + @SuppressWarnings("resource") + FileIO io = table.io(); + + URI loc = + URI.create( + table + .locationProvider() + .newDataLocation( + String.format( + "test-file-%s.txt", UUID.randomUUID().toString().replace("-", "")))); + + OutputFile f1 = io.newOutputFile(loc.toString()); + String location = f1.location(); + DataFile df = + DataFiles.builder(PartitionSpec.unpartitioned()) + .withPath(location) + .withFormat(FileFormat.PARQUET) // bogus value + .withFileSizeInBytes(4) + .withRecordCount(1) + .build(); + + table.newAppend().appendFile(df).commit(); + } + + private long rowCountFor(Table table) { + Snapshot currentSnapshot = table.currentSnapshot(); + assertThat(currentSnapshot).isNotNull(); + + long totalRows = 0; + for (ManifestFile manifest : currentSnapshot.allManifests(table.io())) { + totalRows += manifest.addedRowsCount() + manifest.existingRowsCount(); + totalRows -= manifest.deletedRowsCount(); + } + return totalRows; + } +} 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 7c74f0fa0d3..6e7972779ac 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 @@ -678,6 +678,8 @@ private Map extractSecretReferences( // service identity managed by Polaris. Nothing to do here. break; } + case GCP: + break; // only contains user IDs, not credentials default: throw new IllegalStateException( "Unsupported authentication type: " diff --git a/runtime/service/src/test/java/org/apache/polaris/service/credentials/DefaultPolarisCredentialManagerTest.java b/runtime/service/src/test/java/org/apache/polaris/service/credentials/DefaultPolarisCredentialManagerTest.java index 76b65c9014d..1d17ee83525 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/credentials/DefaultPolarisCredentialManagerTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/credentials/DefaultPolarisCredentialManagerTest.java @@ -18,6 +18,7 @@ */ package org.apache.polaris.service.credentials; +import static org.apache.polaris.service.credentials.TestObjectFactory.createConnectionConfig; import static org.mockito.Mockito.when; import io.quarkus.test.InjectMock; @@ -125,7 +126,11 @@ public void testDelegatesToSigV4Vendor() { // Create connection config IcebergRestConnectionConfigInfoDpo connectionConfig = new IcebergRestConnectionConfigInfoDpo( - "https://test-catalog.example.com", authParams, testServiceIdentity, "test-catalog"); + "https://test-catalog.example.com", + authParams, + testServiceIdentity, + "test-catalog", + Map.of()); // Should delegate to TestSigV4Vendor ConnectionCredentials credentials = @@ -152,8 +157,7 @@ public void testDelegatesToOAuthVendor() { // Create connection config IcebergRestConnectionConfigInfoDpo connectionConfig = - new IcebergRestConnectionConfigInfoDpo( - "https://test-catalog.example.com", authParams, testServiceIdentity, "test-catalog"); + createConnectionConfig(authParams, testServiceIdentity); // Should delegate to TestOAuthVendor ConnectionCredentials credentials = diff --git a/runtime/service/src/test/java/org/apache/polaris/service/credentials/TestObjectFactory.java b/runtime/service/src/test/java/org/apache/polaris/service/credentials/TestObjectFactory.java new file mode 100644 index 00000000000..535d103d9f8 --- /dev/null +++ b/runtime/service/src/test/java/org/apache/polaris/service/credentials/TestObjectFactory.java @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.polaris.service.credentials; + +import java.util.Map; +import org.apache.polaris.core.connection.AuthenticationParametersDpo; +import org.apache.polaris.core.connection.iceberg.IcebergRestConnectionConfigInfoDpo; +import org.apache.polaris.core.identity.dpo.ServiceIdentityInfoDpo; +import org.jetbrains.annotations.NotNull; + +public class TestObjectFactory { + public static @NotNull IcebergRestConnectionConfigInfoDpo createConnectionConfig( + AuthenticationParametersDpo authParams, ServiceIdentityInfoDpo serviceIdentity) { + return new IcebergRestConnectionConfigInfoDpo( + "https://test-catalog.example.com", authParams, serviceIdentity, "test-catalog", Map.of()); + } +} diff --git a/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/BearerConnectionCredentialVendorTest.java b/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/BearerConnectionCredentialVendorTest.java index d8c0141423e..e4109f40649 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/BearerConnectionCredentialVendorTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/BearerConnectionCredentialVendorTest.java @@ -18,6 +18,7 @@ */ package org.apache.polaris.service.credentials.connection; +import static org.apache.polaris.service.credentials.TestObjectFactory.createConnectionConfig; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -57,9 +58,7 @@ public void testGetConnectionCredentials() { BearerAuthenticationParametersDpo authParams = new BearerAuthenticationParametersDpo(bearerTokenRef); - IcebergRestConnectionConfigInfoDpo connectionConfig = - new IcebergRestConnectionConfigInfoDpo( - "https://catalog.example.com", authParams, null, "test-catalog"); + IcebergRestConnectionConfigInfoDpo connectionConfig = createConnectionConfig(authParams, null); // Execute ConnectionCredentials credentials = bearerVendor.getConnectionCredentials(connectionConfig); @@ -97,9 +96,7 @@ public void testGetConnectionCredentialsWithInvalidSecretReference() { BearerAuthenticationParametersDpo authParams = new BearerAuthenticationParametersDpo(invalidSecretRef); - IcebergRestConnectionConfigInfoDpo connectionConfig = - new IcebergRestConnectionConfigInfoDpo( - "https://catalog.example.com", authParams, null, "test-catalog"); + IcebergRestConnectionConfigInfoDpo connectionConfig = createConnectionConfig(authParams, null); // Execute & Verify - should propagate the exception from secrets manager Assertions.assertThatThrownBy(() -> bearerVendor.getConnectionCredentials(connectionConfig)) diff --git a/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/ImplicitConnectionCredentialVendorTest.java b/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/ImplicitConnectionCredentialVendorTest.java index a2c817671fc..6b84e9bd5cc 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/ImplicitConnectionCredentialVendorTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/ImplicitConnectionCredentialVendorTest.java @@ -18,6 +18,7 @@ */ package org.apache.polaris.service.credentials.connection; +import static org.apache.polaris.service.credentials.TestObjectFactory.createConnectionConfig; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -46,9 +47,7 @@ public void testGetConnectionCredentials() { // Setup ImplicitAuthenticationParametersDpo authParams = new ImplicitAuthenticationParametersDpo(); - IcebergRestConnectionConfigInfoDpo connectionConfig = - new IcebergRestConnectionConfigInfoDpo( - "https://catalog.example.com", authParams, null, "test-catalog"); + IcebergRestConnectionConfigInfoDpo connectionConfig = createConnectionConfig(authParams, null); // Execute ConnectionCredentials credentials = implicitVendor.getConnectionCredentials(connectionConfig); diff --git a/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/OAuthClientCredentialVendorTest.java b/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/OAuthClientCredentialVendorTest.java index f60fd81512d..09c7f0f2b6e 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/OAuthClientCredentialVendorTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/OAuthClientCredentialVendorTest.java @@ -18,6 +18,7 @@ */ package org.apache.polaris.service.credentials.connection; +import static org.apache.polaris.service.credentials.TestObjectFactory.createConnectionConfig; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -62,9 +63,7 @@ public void testGetConnectionCredentials() { clientSecretRef, List.of("catalog", "read:data")); - IcebergRestConnectionConfigInfoDpo connectionConfig = - new IcebergRestConnectionConfigInfoDpo( - "https://catalog.example.com", authParams, null, "test-catalog"); + IcebergRestConnectionConfigInfoDpo connectionConfig = createConnectionConfig(authParams, null); // Execute ConnectionCredentials credentials = oauthVendor.getConnectionCredentials(connectionConfig); @@ -104,9 +103,7 @@ public void testGetConnectionCredentialsWithInvalidSecretReference() { new OAuthClientCredentialsParametersDpo( "https://auth.example.com/token", "my-client-id", invalidSecretRef, List.of("catalog")); - IcebergRestConnectionConfigInfoDpo connectionConfig = - new IcebergRestConnectionConfigInfoDpo( - "https://catalog.example.com", authParams, null, "test-catalog"); + IcebergRestConnectionConfigInfoDpo connectionConfig = createConnectionConfig(authParams, null); // Execute & Verify - should propagate the exception from secrets manager Assertions.assertThatThrownBy(() -> oauthVendor.getConnectionCredentials(connectionConfig)) diff --git a/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/SigV4ConnectionCredentialVendorTest.java b/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/SigV4ConnectionCredentialVendorTest.java index c4bd78598e3..8fb18706b4a 100644 --- a/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/SigV4ConnectionCredentialVendorTest.java +++ b/runtime/service/src/test/java/org/apache/polaris/service/credentials/connection/SigV4ConnectionCredentialVendorTest.java @@ -18,6 +18,7 @@ */ package org.apache.polaris.service.credentials.connection; +import static org.apache.polaris.service.credentials.TestObjectFactory.createConnectionConfig; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -113,8 +114,7 @@ public void testGetCredentialsWithSigV4Auth() { // Create connection config with service identity and auth params IcebergRestConnectionConfigInfoDpo connectionConfig = - new IcebergRestConnectionConfigInfoDpo( - "https://test-catalog.example.com", authParams, serviceIdentity, "test-catalog"); + createConnectionConfig(authParams, serviceIdentity); // Get credentials ConnectionCredentials credentials = vendor.getConnectionCredentials(connectionConfig); @@ -157,8 +157,7 @@ public void testGetCredentialsWithDefaultSessionName() { // Create connection config with service identity and auth params IcebergRestConnectionConfigInfoDpo connectionConfig = - new IcebergRestConnectionConfigInfoDpo( - "https://test-catalog.example.com", authParams, serviceIdentity, "test-catalog"); + createConnectionConfig(authParams, serviceIdentity); ConnectionCredentials credentials = vendor.getConnectionCredentials(connectionConfig); @@ -195,8 +194,7 @@ public void testStsDestinationUsesSigningRegion() { "glue"); IcebergRestConnectionConfigInfoDpo connectionConfig = - new IcebergRestConnectionConfigInfoDpo( - "https://test-catalog.example.com", authParams, serviceIdentity, "test-catalog"); + createConnectionConfig(authParams, serviceIdentity); vendor.getConnectionCredentials(connectionConfig); @@ -216,8 +214,7 @@ public void testStsDestinationUsesDifferentRegions() { "arn:aws:iam::123456789012:role/customer-role", null, null, "ap-southeast-1", null); IcebergRestConnectionConfigInfoDpo connectionConfig = - new IcebergRestConnectionConfigInfoDpo( - "https://test-catalog.example.com", authParams, serviceIdentity, "test-catalog"); + createConnectionConfig(authParams, serviceIdentity); vendor.getConnectionCredentials(connectionConfig); diff --git a/site/content/in-dev/unreleased/configuration/config-sections/flags-polaris_features.md b/site/content/in-dev/unreleased/configuration/config-sections/flags-polaris_features.md index 3a24803023c..df78aeb4402 100644 --- a/site/content/in-dev/unreleased/configuration/config-sections/flags-polaris_features.md +++ b/site/content/in-dev/unreleased/configuration/config-sections/flags-polaris_features.md @@ -480,7 +480,7 @@ The list of supported storage types for a catalog The list of supported authentication types for catalog federation - **Type:** `List` -- **Default:** `[OAUTH, BEARER, SIGV4]` +- **Default:** `[OAUTH, BEARER, GCP, SIGV4]` --- diff --git a/spec/polaris-management-service.yml b/spec/polaris-management-service.yml index 3b1082792c9..2c93d001f5b 100644 --- a/spec/polaris-management-service.yml +++ b/spec/polaris-management-service.yml @@ -994,6 +994,7 @@ components: - BEARER - SIGV4 - IMPLICIT + - GCP description: The type of authentication to use when connecting to the remote rest service required: - authenticationType @@ -1004,6 +1005,13 @@ components: BEARER: "#/components/schemas/BearerAuthenticationParameters" SIGV4: "#/components/schemas/SigV4AuthenticationParameters" IMPLICIT: "#/components/schemas/ImplicitAuthenticationParameters" + GCP: "#/components/schemas/GcpAuthenticationParameters" + + GcpAuthenticationParameters: + type: object + description: Uses GCP authentication from the Iceberg codebase + allOf: + - $ref: '#/components/schemas/AuthenticationParameters' OAuthClientCredentialsParameters: type: object