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 fe265c3072..d44585a0ef 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 @@ -20,6 +20,7 @@ import java.util.List; import java.util.Optional; +import org.apache.polaris.core.admin.model.AuthenticationParameters; import org.apache.polaris.core.admin.model.StorageConfigInfo; import org.apache.polaris.core.connection.ConnectionType; import org.apache.polaris.core.context.CallContext; @@ -261,6 +262,17 @@ public static void enforceFeatureEnabledOrThrow( .defaultValue(List.of(ConnectionType.ICEBERG_REST.name())) .buildFeatureConfiguration(); + public static final FeatureConfiguration> + SUPPORTED_EXTERNAL_CATALOG_AUTHENTICATION_TYPES = + PolarisConfiguration.>builder() + .key("SUPPORTED_EXTERNAL_CATALOG_AUTHENTICATION_TYPES") + .description("The list of supported authentication types for catalog federation") + .defaultValue( + List.of( + AuthenticationParameters.AuthenticationTypeEnum.OAUTH.name(), + AuthenticationParameters.AuthenticationTypeEnum.BEARER.name())) + .buildFeatureConfiguration(); + public static final FeatureConfiguration ICEBERG_COMMIT_MAX_RETRIES = PolarisConfiguration.builder() .key("ICEBERG_COMMIT_MAX_RETRIES") diff --git a/runtime/defaults/src/main/resources/application.properties b/runtime/defaults/src/main/resources/application.properties index 592501f76d..d88df31d4d 100644 --- a/runtime/defaults/src/main/resources/application.properties +++ b/runtime/defaults/src/main/resources/application.properties @@ -112,6 +112,7 @@ polaris.features."ENFORCE_PRINCIPAL_CREDENTIAL_ROTATION_REQUIRED_CHECKING"=false polaris.features."SUPPORTED_CATALOG_STORAGE_TYPES"=["S3","GCS","AZURE"] # polaris.features."ENABLE_CATALOG_FEDERATION"=true polaris.features."SUPPORTED_CATALOG_CONNECTION_TYPES"=["ICEBERG_REST"] +polaris.features."SUPPORTED_EXTERNAL_CATALOG_AUTHENTICATION_TYPES"=["OAUTH", "BEARER"] # realm overrides # polaris.features.realm-overrides."my-realm"."SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION"=true diff --git a/service/common/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java b/service/common/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java index 719e6d44b0..071e6b61aa 100644 --- a/service/common/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java +++ b/service/common/src/main/java/org/apache/polaris/service/admin/PolarisServiceImpl.java @@ -29,11 +29,13 @@ import org.apache.iceberg.exceptions.NotAuthorizedException; import org.apache.polaris.core.PolarisCallContext; import org.apache.polaris.core.admin.model.AddGrantRequest; +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.CatalogRole; import org.apache.polaris.core.admin.model.CatalogRoles; import org.apache.polaris.core.admin.model.Catalogs; +import org.apache.polaris.core.admin.model.ConnectionConfigInfo; import org.apache.polaris.core.admin.model.CreateCatalogRequest; import org.apache.polaris.core.admin.model.CreateCatalogRoleRequest; import org.apache.polaris.core.admin.model.CreatePrincipalRequest; @@ -145,7 +147,7 @@ public Response createCatalog( PolarisAdminService adminService = newAdminService(realmContext, securityContext); Catalog catalog = request.getCatalog(); validateStorageConfig(catalog.getStorageConfigInfo()); - validateConnectionConfigInfo(catalog); + validateExternalCatalog(catalog); Catalog newCatalog = new CatalogEntity(adminService.createCatalog(request)).asCatalog(); LOGGER.info("Created new catalog {}", newCatalog); return Response.status(Response.Status.CREATED).build(); @@ -169,30 +171,54 @@ private void validateStorageConfig(StorageConfigInfo storageConfigInfo) { } } - private void validateConnectionConfigInfo(Catalog catalog) { + private void validateExternalCatalog(Catalog catalog) { if (catalog.getType() == Catalog.TypeEnum.EXTERNAL) { if (catalog instanceof ExternalCatalog externalCatalog) { - if (externalCatalog.getConnectionConfigInfo() != null) { - String connectionType = - externalCatalog.getConnectionConfigInfo().getConnectionType().name(); - List supportedConnectionTypes = - callContext - .getPolarisCallContext() - .getConfigurationStore() - .getConfiguration( - callContext.getRealmContext(), - FeatureConfiguration.SUPPORTED_CATALOG_CONNECTION_TYPES) - .stream() - .map(s -> s.toUpperCase(Locale.ROOT)) - .toList(); - if (!supportedConnectionTypes.contains(connectionType)) { - throw new IllegalStateException("Unsupported connection type: " + connectionType); - } + ConnectionConfigInfo connectionConfigInfo = externalCatalog.getConnectionConfigInfo(); + if (connectionConfigInfo != null) { + validateConnectionConfigInfo(connectionConfigInfo); + validateAuthenticationParameters(connectionConfigInfo.getAuthenticationParameters()); } } } } + private void validateConnectionConfigInfo(ConnectionConfigInfo connectionConfigInfo) { + + String connectionType = connectionConfigInfo.getConnectionType().name(); + List supportedConnectionTypes = + callContext + .getPolarisCallContext() + .getConfigurationStore() + .getConfiguration( + callContext.getRealmContext(), + FeatureConfiguration.SUPPORTED_CATALOG_CONNECTION_TYPES) + .stream() + .map(s -> s.toUpperCase(Locale.ROOT)) + .toList(); + if (!supportedConnectionTypes.contains(connectionType)) { + throw new IllegalStateException("Unsupported connection type: " + connectionType); + } + } + + private void validateAuthenticationParameters(AuthenticationParameters authenticationParameters) { + + String authenticationType = authenticationParameters.getAuthenticationType().name(); + List supportedAuthenticationTypes = + callContext + .getPolarisCallContext() + .getConfigurationStore() + .getConfiguration( + callContext.getRealmContext(), + FeatureConfiguration.SUPPORTED_EXTERNAL_CATALOG_AUTHENTICATION_TYPES) + .stream() + .map(s -> s.toUpperCase(Locale.ROOT)) + .toList(); + if (!supportedAuthenticationTypes.contains(authenticationType)) { + throw new IllegalStateException("Unsupported authentication type: " + authenticationType); + } + } + /** From PolarisCatalogsApiService */ @Override public Response deleteCatalog( diff --git a/service/common/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplTest.java b/service/common/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplTest.java new file mode 100644 index 0000000000..fab1d7da41 --- /dev/null +++ b/service/common/src/test/java/org/apache/polaris/service/admin/PolarisServiceImplTest.java @@ -0,0 +1,238 @@ +/* + * 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.admin; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Method; +import java.util.List; +import org.apache.polaris.core.PolarisCallContext; +import org.apache.polaris.core.admin.model.AuthenticationParameters; +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.FileStorageConfigInfo; +import org.apache.polaris.core.admin.model.PolarisCatalog; +import org.apache.polaris.core.admin.model.StorageConfigInfo; +import org.apache.polaris.core.auth.PolarisAuthorizer; +import org.apache.polaris.core.config.FeatureConfiguration; +import org.apache.polaris.core.config.PolarisConfigurationStore; +import org.apache.polaris.core.context.CallContext; +import org.apache.polaris.core.context.RealmContext; +import org.apache.polaris.core.persistence.MetaStoreManagerFactory; +import org.apache.polaris.core.secrets.UserSecretsManagerFactory; +import org.apache.polaris.service.config.RealmEntityManagerFactory; +import org.apache.polaris.service.config.ReservedProperties; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class PolarisServiceImplTest { + + private RealmEntityManagerFactory entityManagerFactory; + private MetaStoreManagerFactory metaStoreManagerFactory; + private UserSecretsManagerFactory userSecretsManagerFactory; + private PolarisAuthorizer polarisAuthorizer; + private CallContext callContext; + private ReservedProperties reservedProperties; + private PolarisCallContext polarisCallContext; + private PolarisConfigurationStore configurationStore; + private RealmContext realmContext; + + private PolarisServiceImpl polarisService; + + @BeforeEach + void setUp() { + entityManagerFactory = Mockito.mock(RealmEntityManagerFactory.class); + metaStoreManagerFactory = Mockito.mock(MetaStoreManagerFactory.class); + userSecretsManagerFactory = Mockito.mock(UserSecretsManagerFactory.class); + polarisAuthorizer = Mockito.mock(PolarisAuthorizer.class); + callContext = Mockito.mock(CallContext.class); + reservedProperties = Mockito.mock(ReservedProperties.class); + polarisCallContext = Mockito.mock(PolarisCallContext.class); + configurationStore = Mockito.mock(PolarisConfigurationStore.class); + realmContext = Mockito.mock(RealmContext.class); + + when(callContext.getPolarisCallContext()).thenReturn(polarisCallContext); + when(callContext.getRealmContext()).thenReturn(realmContext); + when(polarisCallContext.getConfigurationStore()).thenReturn(configurationStore); + when(configurationStore.getConfiguration( + realmContext, FeatureConfiguration.SUPPORTED_CATALOG_CONNECTION_TYPES)) + .thenReturn(List.of("ICEBERG_REST")); + when(configurationStore.getConfiguration( + realmContext, FeatureConfiguration.SUPPORTED_EXTERNAL_CATALOG_AUTHENTICATION_TYPES)) + .thenReturn(List.of("OAUTH")); + + polarisService = + new PolarisServiceImpl( + entityManagerFactory, + metaStoreManagerFactory, + userSecretsManagerFactory, + polarisAuthorizer, + callContext, + reservedProperties); + } + + @Test + void testValidateExternalCatalog_InternalCatalog() { + StorageConfigInfo storageConfig = + FileStorageConfigInfo.builder(StorageConfigInfo.StorageTypeEnum.FILE) + .setAllowedLocations(List.of("file://tmp")) + .build(); + + PolarisCatalog internalCatalog = + PolarisCatalog.builder() + .setType(Catalog.TypeEnum.INTERNAL) + .setName("test-catalog") + .setProperties(new CatalogProperties("file://tmp")) + .setStorageConfigInfo(storageConfig) + .build(); + + assertThatCode(() -> invokeValidateExternalCatalog(polarisService, internalCatalog)) + .doesNotThrowAnyException(); + } + + @Test + void testValidateExternalCatalog_LegacyExternalCatalog() { + StorageConfigInfo storageConfig = + FileStorageConfigInfo.builder(StorageConfigInfo.StorageTypeEnum.FILE) + .setAllowedLocations(List.of("file://tmp")) + .build(); + ExternalCatalog externalCatalog = + ExternalCatalog.builder() + .setType(Catalog.TypeEnum.EXTERNAL) + .setName("test-catalog") + .setProperties(new CatalogProperties("file://tmp")) + .setStorageConfigInfo(storageConfig) + .setConnectionConfigInfo(null) + .build(); + + assertThatCode(() -> invokeValidateExternalCatalog(polarisService, externalCatalog)) + .doesNotThrowAnyException(); + } + + @Test + void testValidateExternalCatalog_SupportedExternalCatalog() { + StorageConfigInfo storageConfig = + FileStorageConfigInfo.builder(StorageConfigInfo.StorageTypeEnum.FILE) + .setAllowedLocations(List.of("file://tmp")) + .build(); + + ConnectionConfigInfo connectionConfigInfo = Mockito.mock(ConnectionConfigInfo.class); + AuthenticationParameters authenticationParameters = + Mockito.mock(AuthenticationParameters.class); + when(connectionConfigInfo.getConnectionType()) + .thenReturn(ConnectionConfigInfo.ConnectionTypeEnum.ICEBERG_REST); + when(connectionConfigInfo.getAuthenticationParameters()).thenReturn(authenticationParameters); + when(authenticationParameters.getAuthenticationType()) + .thenReturn(AuthenticationParameters.AuthenticationTypeEnum.OAUTH); + + ExternalCatalog externalCatalog = + ExternalCatalog.builder() + .setType(Catalog.TypeEnum.EXTERNAL) + .setName("test-catalog") + .setProperties(new CatalogProperties("file://tmp")) + .setStorageConfigInfo(storageConfig) + .setConnectionConfigInfo(connectionConfigInfo) + .build(); + + assertThatCode(() -> invokeValidateExternalCatalog(polarisService, externalCatalog)) + .doesNotThrowAnyException(); + } + + @Test + void testValidateExternalCatalog_UnsupportedConnectionType() { + StorageConfigInfo storageConfig = + FileStorageConfigInfo.builder(StorageConfigInfo.StorageTypeEnum.FILE) + .setAllowedLocations(List.of("file://tmp")) + .build(); + + ConnectionConfigInfo connectionConfigInfo = Mockito.mock(ConnectionConfigInfo.class); + AuthenticationParameters authenticationParameters = + Mockito.mock(AuthenticationParameters.class); + when(connectionConfigInfo.getConnectionType()) + .thenReturn(ConnectionConfigInfo.ConnectionTypeEnum.HADOOP); + when(connectionConfigInfo.getAuthenticationParameters()).thenReturn(authenticationParameters); + when(authenticationParameters.getAuthenticationType()) + .thenReturn(AuthenticationParameters.AuthenticationTypeEnum.OAUTH); + + ExternalCatalog externalCatalog = + ExternalCatalog.builder() + .setType(Catalog.TypeEnum.EXTERNAL) + .setName("test-catalog") + .setProperties(new CatalogProperties("file://tmp")) + .setStorageConfigInfo(storageConfig) + .setConnectionConfigInfo(connectionConfigInfo) + .build(); + + assertThatThrownBy(() -> invokeValidateExternalCatalog(polarisService, externalCatalog)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Unsupported connection type: HADOOP"); + } + + @Test + void testValidateExternalCatalog_UnsupportedAuthenticationType() { + StorageConfigInfo storageConfig = + FileStorageConfigInfo.builder(StorageConfigInfo.StorageTypeEnum.FILE) + .setAllowedLocations(List.of("file://tmp")) + .build(); + + ConnectionConfigInfo connectionConfigInfo = Mockito.mock(ConnectionConfigInfo.class); + AuthenticationParameters authenticationParameters = + Mockito.mock(AuthenticationParameters.class); + when(connectionConfigInfo.getConnectionType()) + .thenReturn(ConnectionConfigInfo.ConnectionTypeEnum.ICEBERG_REST); + when(connectionConfigInfo.getAuthenticationParameters()).thenReturn(authenticationParameters); + when(authenticationParameters.getAuthenticationType()) + .thenReturn(AuthenticationParameters.AuthenticationTypeEnum.BEARER); + + ExternalCatalog externalCatalog = + ExternalCatalog.builder() + .setType(Catalog.TypeEnum.EXTERNAL) + .setName("test-catalog") + .setProperties(new CatalogProperties("file://tmp")) + .setStorageConfigInfo(storageConfig) + .setConnectionConfigInfo(connectionConfigInfo) + .build(); + + assertThatThrownBy(() -> invokeValidateExternalCatalog(polarisService, externalCatalog)) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Unsupported authentication type: BEARER"); + } + + private void invokeValidateExternalCatalog(PolarisServiceImpl service, Catalog catalog) + throws Exception { + Method method = + PolarisServiceImpl.class.getDeclaredMethod("validateExternalCatalog", Catalog.class); + method.setAccessible(true); + try { + method.invoke(service, catalog); + } catch (java.lang.reflect.InvocationTargetException e) { + Throwable cause = e.getCause(); + if (cause instanceof Exception) { + throw (Exception) cause; + } else { + throw e; + } + } + } +}