This is an automated email from the ASF dual-hosted git repository.
etudenhoefner pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git
The following commit(s) were added to refs/heads/main by this push:
new 8ae75596c8 Azure: Add support to specify token credential provider
(#14136)
8ae75596c8 is described below
commit 8ae75596c8ba68b37d05ebd0647c8ba4e3a76cbf
Author: S N Munendra <[email protected]>
AuthorDate: Tue Sep 30 11:54:55 2025 +0530
Azure: Add support to specify token credential provider (#14136)
---
.../iceberg/azure/AdlsTokenCredentialProvider.java | 34 +++++
.../azure/AdlsTokenCredentialProviders.java | 96 +++++++++++++
.../org/apache/iceberg/azure/AzureProperties.java | 30 +++-
.../azure/TestAdlsTokenCredentialProviders.java | 152 +++++++++++++++++++++
.../apache/iceberg/azure/TestAzureProperties.java | 76 +++++++++++
5 files changed, 385 insertions(+), 3 deletions(-)
diff --git
a/azure/src/main/java/org/apache/iceberg/azure/AdlsTokenCredentialProvider.java
b/azure/src/main/java/org/apache/iceberg/azure/AdlsTokenCredentialProvider.java
new file mode 100644
index 0000000000..00d4e95b27
--- /dev/null
+++
b/azure/src/main/java/org/apache/iceberg/azure/AdlsTokenCredentialProvider.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.iceberg.azure;
+
+import com.azure.core.credential.TokenCredential;
+import java.util.Map;
+
+public interface AdlsTokenCredentialProvider {
+
+ TokenCredential credential();
+
+ /**
+ * Initialize Azure credential provider from provider properties.
+ *
+ * @param properties credential provider properties
+ */
+ void initialize(Map<String, String> properties);
+}
diff --git
a/azure/src/main/java/org/apache/iceberg/azure/AdlsTokenCredentialProviders.java
b/azure/src/main/java/org/apache/iceberg/azure/AdlsTokenCredentialProviders.java
new file mode 100644
index 0000000000..09faaa959c
--- /dev/null
+++
b/azure/src/main/java/org/apache/iceberg/azure/AdlsTokenCredentialProviders.java
@@ -0,0 +1,96 @@
+/*
+ * 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.iceberg.azure;
+
+import com.azure.core.credential.TokenCredential;
+import com.azure.identity.DefaultAzureCredentialBuilder;
+import java.util.Map;
+import org.apache.iceberg.common.DynConstructors;
+import org.apache.iceberg.relocated.com.google.common.base.Strings;
+import org.apache.iceberg.util.PropertyUtil;
+
+public class AdlsTokenCredentialProviders {
+
+ private static final DefaultTokenCredentialProvider
DEFAULT_TOKEN_CREDENTIAL_PROVIDER =
+ new DefaultTokenCredentialProvider();
+
+ private AdlsTokenCredentialProviders() {}
+
+ public static AdlsTokenCredentialProvider defaultFactory() {
+ return DEFAULT_TOKEN_CREDENTIAL_PROVIDER;
+ }
+
+ public static AdlsTokenCredentialProvider from(Map<String, String>
properties) {
+ String providerImpl =
+ PropertyUtil.propertyAsString(
+ properties, AzureProperties.ADLS_TOKEN_CREDENTIAL_PROVIDER, null);
+ Map<String, String> credentialProviderProperties =
+ PropertyUtil.propertiesWithPrefix(properties,
AzureProperties.ADLS_TOKEN_PROVIDER_PREFIX);
+ return loadCredentialProvider(providerImpl, credentialProviderProperties);
+ }
+
+ private static AdlsTokenCredentialProvider loadCredentialProvider(
+ String impl, Map<String, String> properties) {
+ if (Strings.isNullOrEmpty(impl)) {
+ AdlsTokenCredentialProvider provider = defaultFactory();
+ provider.initialize(properties);
+ return provider;
+ }
+
+ DynConstructors.Ctor<AdlsTokenCredentialProvider> ctor;
+ try {
+ ctor =
+ DynConstructors.builder(AdlsTokenCredentialProvider.class)
+ .loader(AdlsTokenCredentialProviders.class.getClassLoader())
+ .hiddenImpl(impl)
+ .buildChecked();
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Cannot initialize AdlsTokenCredentialProvider, missing no-arg
constructor: %s",
+ impl),
+ e);
+ }
+
+ AdlsTokenCredentialProvider provider;
+ try {
+ provider = ctor.newInstance();
+ } catch (ClassCastException e) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Cannot initialize AdlsTokenCredentialProvider, %s does not
implement AdlsTokenCredentialProvider.",
+ impl),
+ e);
+ }
+
+ provider.initialize(properties);
+ return provider;
+ }
+
+ static class DefaultTokenCredentialProvider implements
AdlsTokenCredentialProvider {
+
+ @Override
+ public TokenCredential credential() {
+ return new DefaultAzureCredentialBuilder().build();
+ }
+
+ @Override
+ public void initialize(Map<String, String> properties) {}
+ }
+}
diff --git a/azure/src/main/java/org/apache/iceberg/azure/AzureProperties.java
b/azure/src/main/java/org/apache/iceberg/azure/AzureProperties.java
index 9e2143f943..38ac573b59 100644
--- a/azure/src/main/java/org/apache/iceberg/azure/AzureProperties.java
+++ b/azure/src/main/java/org/apache/iceberg/azure/AzureProperties.java
@@ -21,7 +21,6 @@ package org.apache.iceberg.azure;
import com.azure.core.credential.AccessToken;
import com.azure.core.credential.TokenCredential;
import com.azure.core.credential.TokenRequestContext;
-import com.azure.identity.DefaultAzureCredentialBuilder;
import com.azure.storage.common.StorageSharedKeyCredential;
import com.azure.storage.file.datalake.DataLakeFileSystemClientBuilder;
import java.io.Serializable;
@@ -50,6 +49,29 @@ public class AzureProperties implements Serializable {
public static final String ADLS_SHARED_KEY_ACCOUNT_KEY =
"adls.auth.shared-key.account.key";
public static final String ADLS_TOKEN = "adls.token";
+ /**
+ * Configure the ADLS token credential provider used to get {@link
TokenCredential}. A fully
+ * qualified concrete class with package that implements the {@link
AdlsTokenCredentialProvider}
+ * interface is required.
+ *
+ * <p>The implementation class must have a no-arg constructor and will be
initialized by calling
+ * the {@link AdlsTokenCredentialProvider#initialize(Map)} method with the
catalog properties.
+ *
+ * <p>Example:
adls.token-credential-provider=com.example.MyCustomTokenCredentialProvider
+ *
+ * <p>When set, the {@link AdlsTokenCredentialProviders#from(Map)} method
will use this provider
+ * to get ADLS credentials instead of using the default.
+ */
+ public static final String ADLS_TOKEN_CREDENTIAL_PROVIDER =
"adls.token-credential-provider";
+
+ /**
+ * Used by the configured {@link #ADLS_TOKEN_CREDENTIAL_PROVIDER} value that
will be used by
+ * {@link AdlsTokenCredentialProviders#defaultFactory()} and other token
credential provider
+ * classes to pass provider-specific properties. Each property consists of a
key name and an
+ * associated value.
+ */
+ public static final String ADLS_TOKEN_PROVIDER_PREFIX =
"adls.token-credential-provider.";
+
/**
* When set, the {@link VendedAdlsCredentialProvider} will be used to fetch
and refresh vended
* credentials from this endpoint.
@@ -68,7 +90,7 @@ public class AzureProperties implements Serializable {
private String adlsRefreshCredentialsEndpoint;
private boolean adlsRefreshCredentialsEnabled;
private String token;
- private Map<String, String> allProperties;
+ private Map<String, String> allProperties = Collections.emptyMap();
public AzureProperties() {}
@@ -153,7 +175,9 @@ public class AzureProperties implements Serializable {
};
builder.credential(tokenCredential);
} else {
- builder.credential(new DefaultAzureCredentialBuilder().build());
+ AdlsTokenCredentialProvider credentialProvider =
+ AdlsTokenCredentialProviders.from(allProperties);
+ builder.credential(credentialProvider.credential());
}
}
diff --git
a/azure/src/test/java/org/apache/iceberg/azure/TestAdlsTokenCredentialProviders.java
b/azure/src/test/java/org/apache/iceberg/azure/TestAdlsTokenCredentialProviders.java
new file mode 100644
index 0000000000..5060a9bc44
--- /dev/null
+++
b/azure/src/test/java/org/apache/iceberg/azure/TestAdlsTokenCredentialProviders.java
@@ -0,0 +1,152 @@
+/*
+ * 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.iceberg.azure;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static
org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+import com.azure.core.credential.AccessToken;
+import com.azure.core.credential.TokenCredential;
+import com.azure.core.credential.TokenRequestContext;
+import java.time.OffsetDateTime;
+import java.util.Map;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Mono;
+
+public class TestAdlsTokenCredentialProviders {
+
+ @Test
+ public void useDefaultFactory() {
+ assertThat(AdlsTokenCredentialProviders.defaultFactory())
+ .isNotNull()
+
.isInstanceOf(AdlsTokenCredentialProviders.DefaultTokenCredentialProvider.class);
+ }
+
+ @Test
+ public void emptyPropertiesWithNoProvider() {
+ assertThat(AdlsTokenCredentialProviders.from(ImmutableMap.of()))
+ .isNotNull()
+
.isInstanceOf(AdlsTokenCredentialProviders.DefaultTokenCredentialProvider.class);
+ }
+
+ @Test
+ public void emptyCredentialProvider() {
+ Map<String, String> properties =
+ ImmutableMap.of(AzureProperties.ADLS_TOKEN_CREDENTIAL_PROVIDER, "");
+ assertThat(AdlsTokenCredentialProviders.from(properties))
+ .isNotNull()
+
.isInstanceOf(AdlsTokenCredentialProviders.DefaultTokenCredentialProvider.class);
+ }
+
+ @Test
+ public void defaultProviderAsCredentialProvider() {
+ Map<String, String> properties =
+ ImmutableMap.of(
+ AzureProperties.ADLS_TOKEN_CREDENTIAL_PROVIDER,
+
AdlsTokenCredentialProviders.DefaultTokenCredentialProvider.class.getName());
+ assertThat(AdlsTokenCredentialProviders.from(properties))
+ .isNotNull()
+
.isInstanceOf(AdlsTokenCredentialProviders.DefaultTokenCredentialProvider.class);
+ }
+
+ @Test
+ public void customProviderAsCredentialProvider() {
+ Map<String, String> properties =
+ ImmutableMap.of(
+ AzureProperties.ADLS_TOKEN_CREDENTIAL_PROVIDER,
+
TestAdlsTokenCredentialProviders.DummyTokenCredentialProvider.class.getName());
+ AdlsTokenCredentialProvider provider =
AdlsTokenCredentialProviders.from(properties);
+
+
assertThat(provider).isNotNull().isInstanceOf(DummyTokenCredentialProvider.class);
+ assertThat(provider.credential()).isInstanceOf(DummyTokenCredential.class);
+ }
+
+ @Test
+ public void nonExistentCredentialProvider() {
+ Map<String, String> properties =
+ ImmutableMap.of(
+ AzureProperties.ADLS_TOKEN_CREDENTIAL_PROVIDER,
+ "org.apache.iceberg.azure.NonExistentProvider");
+
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> AdlsTokenCredentialProviders.from(properties))
+ .withMessageContaining(
+ "Cannot initialize AdlsTokenCredentialProvider, missing no-arg
constructor");
+ }
+
+ @Test
+ public void nonImplementingClassAsCredentialProvider() {
+ Map<String, String> properties =
+ ImmutableMap.of(AzureProperties.ADLS_TOKEN_CREDENTIAL_PROVIDER,
"java.lang.String");
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> AdlsTokenCredentialProviders.from(properties))
+ .withMessageContaining("java.lang.String does not implement
AdlsTokenCredentialProvider");
+ }
+
+ @Test
+ public void loadCredentialProviderWithProperties() {
+ Map<String, String> properties =
+ ImmutableMap.of(
+ AzureProperties.ADLS_TOKEN_CREDENTIAL_PROVIDER,
+
TestAdlsTokenCredentialProviders.DummyTokenCredentialProvider.class.getName(),
+ AzureProperties.ADLS_TOKEN_PROVIDER_PREFIX + "client-id",
+ "clientId",
+ AzureProperties.ADLS_TOKEN_PROVIDER_PREFIX + "client-secret",
+ "clientSecret",
+ "custom.property",
+ "custom.value");
+
+ AdlsTokenCredentialProvider provider =
AdlsTokenCredentialProviders.from(properties);
+ assertThat(provider).isInstanceOf(DummyTokenCredentialProvider.class);
+ DummyTokenCredentialProvider credentialProvider =
(DummyTokenCredentialProvider) provider;
+ assertThat(credentialProvider.properties())
+ .containsEntry("client-id", "clientId")
+ .containsEntry("client-secret", "clientSecret")
+ .doesNotContainKey("custom.property")
+ .doesNotContainKey(AzureProperties.ADLS_TOKEN_CREDENTIAL_PROVIDER);
+ assertThat(provider.credential()).isInstanceOf(DummyTokenCredential.class);
+ }
+
+ static class DummyTokenCredentialProvider implements
AdlsTokenCredentialProvider {
+
+ private Map<String, String> properties;
+
+ @Override
+ public TokenCredential credential() {
+ return new DummyTokenCredential();
+ }
+
+ @Override
+ public void initialize(Map<String, String> credentialProperties) {
+ this.properties = credentialProperties;
+ }
+
+ public Map<String, String> properties() {
+ return properties;
+ }
+ }
+
+ static class DummyTokenCredential implements TokenCredential {
+ @Override
+ public Mono<AccessToken> getToken(TokenRequestContext request) {
+ return Mono.just(new AccessToken("dummy-token",
OffsetDateTime.now().plusHours(1)));
+ }
+ }
+}
diff --git
a/azure/src/test/java/org/apache/iceberg/azure/TestAzureProperties.java
b/azure/src/test/java/org/apache/iceberg/azure/TestAzureProperties.java
index 12cde198ea..514e7faad4 100644
--- a/azure/src/test/java/org/apache/iceberg/azure/TestAzureProperties.java
+++ b/azure/src/test/java/org/apache/iceberg/azure/TestAzureProperties.java
@@ -42,6 +42,9 @@ import com.azure.identity.DefaultAzureCredential;
import com.azure.storage.common.StorageSharedKeyCredential;
import com.azure.storage.file.datalake.DataLakeFileSystemClientBuilder;
import java.io.IOException;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.Map;
import java.util.Optional;
import org.apache.iceberg.CatalogProperties;
import org.apache.iceberg.TestHelpers;
@@ -68,6 +71,8 @@ public class TestAzureProperties {
.put(ADLS_WRITE_BLOCK_SIZE, "42")
.put(ADLS_SHARED_KEY_ACCOUNT_NAME, "me")
.put(ADLS_SHARED_KEY_ACCOUNT_KEY, "secret")
+ .put(AzureProperties.ADLS_TOKEN_CREDENTIAL_PROVIDER,
"provider")
+ .put(AzureProperties.ADLS_TOKEN_PROVIDER_PREFIX + "client-id",
"clientId")
.build());
AzureProperties serdedProps = roundTripSerializer.apply(props);
@@ -235,4 +240,75 @@ public class TestAzureProperties {
verify(clientBuilder,
never()).credential(any(StorageSharedKeyCredential.class));
verify(clientBuilder,
never()).credential(any(com.azure.identity.DefaultAzureCredential.class));
}
+
+ @Test
+ public void testDefaultTokenCredentialProvider() {
+ // No SAS, no shared key, no explicit token, no refresh endpoint ->
default token provider
+ AzureProperties props = new AzureProperties(ImmutableMap.of());
+
+ DataLakeFileSystemClientBuilder clientBuilder =
mock(DataLakeFileSystemClientBuilder.class);
+
+ props.applyClientConfiguration("account", clientBuilder);
+
+ // Default provider should be DefaultAzureCredential
+ verify(clientBuilder).credential(any(DefaultAzureCredential.class));
+ verify(clientBuilder, never()).sasToken(any());
+ verify(clientBuilder,
never()).credential(any(StorageSharedKeyCredential.class));
+ }
+
+ @Test
+ public void testCustomTokenCredentialProvider() {
+ ImmutableMap<String, String> properties =
+ ImmutableMap.<String, String>builder()
+ .put(
+ AzureProperties.ADLS_TOKEN_CREDENTIAL_PROVIDER,
+
TestAzureProperties.DummyTokenCredentialProvider.class.getName())
+ .put(AzureProperties.ADLS_TOKEN_PROVIDER_PREFIX + "client-id",
"clientId")
+ .put(AzureProperties.ADLS_TOKEN_PROVIDER_PREFIX + "client-secret",
"clientSecret")
+ .put("custom.property", "custom.value")
+ .build();
+
+ AzureProperties props = new AzureProperties(properties);
+
+ DataLakeFileSystemClientBuilder clientBuilder =
mock(DataLakeFileSystemClientBuilder.class);
+ ArgumentCaptor<TokenCredential> credentialCaptor =
+ ArgumentCaptor.forClass(TokenCredential.class);
+
+ props.applyClientConfiguration("account", clientBuilder);
+
+ verify(clientBuilder).credential(credentialCaptor.capture());
+ TokenCredential credential = credentialCaptor.getValue();
+ assertThat(credential).isInstanceOf(DummyTokenCredential.class);
+
+ // Provider should receive only prefixed properties, with prefix stripped
+ assertThat(DummyTokenCredentialProvider.properties)
+ .containsEntry("client-id", "clientId")
+ .containsEntry("client-secret", "clientSecret")
+ .doesNotContainKey("custom.property")
+ .doesNotContainKey(AzureProperties.ADLS_TOKEN_CREDENTIAL_PROVIDER);
+
+ verify(clientBuilder, never()).sasToken(any());
+ verify(clientBuilder,
never()).credential(any(StorageSharedKeyCredential.class));
+ }
+
+ static class DummyTokenCredential implements TokenCredential {
+ @Override
+ public Mono<AccessToken> getToken(TokenRequestContext request) {
+ return Mono.just(new AccessToken("dummy",
OffsetDateTime.now(ZoneOffset.UTC).plusHours(1)));
+ }
+ }
+
+ static class DummyTokenCredentialProvider implements
AdlsTokenCredentialProvider {
+ static Map<String, String> properties;
+
+ @Override
+ public TokenCredential credential() {
+ return new DummyTokenCredential();
+ }
+
+ @Override
+ public void initialize(Map<String, String> credentialProperties) {
+ properties = credentialProperties;
+ }
+ }
}