org.junit.jupiter
junit-jupiter-api
diff --git a/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/AliyunCredentialsBridge.java b/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/AliyunCredentialsBridge.java
new file mode 100644
index 00000000000000..84794aacc26bf2
--- /dev/null
+++ b/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/AliyunCredentialsBridge.java
@@ -0,0 +1,62 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.filesystem.oss;
+
+import com.aliyun.oss.common.auth.Credentials;
+import com.aliyun.oss.common.auth.CredentialsProvider;
+import com.aliyun.oss.common.auth.DefaultCredentials;
+import com.aliyuncs.auth.AlibabaCloudCredentials;
+import com.aliyuncs.auth.AlibabaCloudCredentialsProvider;
+import com.aliyuncs.auth.BasicSessionCredentials;
+
+/**
+ * Bridges {@link AlibabaCloudCredentialsProvider} (aliyuncs SDK) to
+ * {@link CredentialsProvider} (OSS SDK) so SDK-managed providers such as
+ * {@code OIDCCredentialsProvider} and {@code STSAssumeRoleSessionCredentialsProvider}
+ * can be passed directly to {@code OSSClientBuilder.build()}.
+ *
+ * The aliyuncs providers handle credential caching and refresh internally;
+ * this class only bridges the credential types.
+ */
+class AliyunCredentialsBridge implements CredentialsProvider {
+
+ private final AlibabaCloudCredentialsProvider delegate;
+
+ AliyunCredentialsBridge(AlibabaCloudCredentialsProvider delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void setCredentials(Credentials credentials) {
+ }
+
+ @Override
+ public Credentials getCredentials() {
+ try {
+ AlibabaCloudCredentials c = delegate.getCredentials();
+ // Temporary credentials (STS/RRSA/ECS) return BasicSessionCredentials with a token
+ if (c instanceof BasicSessionCredentials) {
+ String token = ((BasicSessionCredentials) c).getSessionToken();
+ return new DefaultCredentials(c.getAccessKeyId(), c.getAccessKeySecret(), token);
+ }
+ return new DefaultCredentials(c.getAccessKeyId(), c.getAccessKeySecret());
+ } catch (Exception e) {
+ throw new IllegalStateException("Failed to obtain credentials: " + e.getMessage(), e);
+ }
+ }
+}
diff --git a/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/OssCredentialsProviderType.java b/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/OssCredentialsProviderType.java
new file mode 100644
index 00000000000000..39e173683adae1
--- /dev/null
+++ b/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/OssCredentialsProviderType.java
@@ -0,0 +1,92 @@
+// 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.doris.filesystem.oss;
+
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Credential provider mode for OSS filesystem access.
+ *
+ *
Mirrors {@code S3CredentialsProviderType} for consistency across providers.
+ */
+public enum OssCredentialsProviderType {
+
+ /**
+ * Try in order: INSTANCE_PROFILE → OIDC (if env configured) → ENV → static ak/sk → anonymous.
+ */
+ DEFAULT("DEFAULT"),
+
+ /**
+ * ECS instance metadata (100.100.100.200). Role name from {@code oss.ecs_ram_role_name}
+ * or auto-discovered from the metadata base URL when not set.
+ */
+ INSTANCE_PROFILE("INSTANCE_PROFILE"),
+
+ /**
+ * RRSA / Kubernetes pod identity via STS AssumeRoleWithOIDC.
+ * Role ARN and token file from {@code oss.oidc_provider_arn}/{@code oss.oidc_token_file}
+ * or from env vars {@code ALIBABA_CLOUD_ROLE_ARN} / {@code ALIBABA_CLOUD_OIDC_TOKEN_FILE}
+ * / {@code ALIBABA_CLOUD_OIDC_PROVIDER_ARN}.
+ */
+ OIDC("OIDC"),
+
+ /**
+ * Environment variables: {@code OSS_ACCESS_KEY_ID} and {@code OSS_ACCESS_KEY_SECRET}.
+ */
+ ENV("ENV"),
+
+ /** No credentials — public buckets only. */
+ ANONYMOUS("ANONYMOUS");
+
+ private final String mode;
+
+ OssCredentialsProviderType(String mode) {
+ this.mode = mode;
+ }
+
+ public String getMode() {
+ return mode;
+ }
+
+ public static OssCredentialsProviderType fromString(String value) {
+ if (StringUtils.isBlank(value)) {
+ return DEFAULT;
+ }
+ String normalized = value.trim().toUpperCase().replace('-', '_');
+ switch (normalized) {
+ case "INSTANCE_PROFILE":
+ case "ECS":
+ case "ECS_RAM_ROLE":
+ return INSTANCE_PROFILE;
+ case "OIDC":
+ case "RRSA":
+ case "WEB_IDENTITY":
+ return OIDC;
+ case "ENV":
+ case "ENVIRONMENT":
+ return ENV;
+ case "ANONYMOUS":
+ return ANONYMOUS;
+ case "DEFAULT":
+ return DEFAULT;
+ default:
+ throw new IllegalArgumentException(
+ "Unsupported oss.credentials_provider: " + value);
+ }
+ }
+}
diff --git a/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/OssFileSystemProperties.java b/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/OssFileSystemProperties.java
index 663b5596c242cd..1b664876c2e84b 100644
--- a/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/OssFileSystemProperties.java
+++ b/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/OssFileSystemProperties.java
@@ -62,6 +62,9 @@ public final class OssFileSystemProperties
public static final String FORCE_PARSING_BY_STANDARD_URI =
"oss.force_parsing_by_standard_uri";
+ public static final String CREDENTIALS_PROVIDER = "oss.credentials_provider";
+
+ public static final String DEFAULT_CREDENTIALS_PROVIDER = "DEFAULT";
public static final String DEFAULT_MAX_CONNECTIONS = "100";
public static final String DEFAULT_REQUEST_TIMEOUT_MS = "10000";
public static final String DEFAULT_CONNECTION_TIMEOUT_MS = "10000";
@@ -149,11 +152,36 @@ public final class OssFileSystemProperties
description = "The OSS role ARN for AssumeRole access.")
private String roleArn = "";
+ @ConnectorProperty(names = {"oss.role_session_name"},
+ required = false,
+ description = "Session name used for STS AssumeRole; default 'doris-session'.")
+ private String roleSessionName = "doris-session";
+
@ConnectorProperty(names = {"AWS_EXTERNAL_ID"},
required = false,
description = "The external ID for AssumeRole trust policy.")
private String externalId = "";
+ @ConnectorProperty(names = {"oss.ecs_ram_role_name"},
+ required = false,
+ description = "ECS-attached RAM role name; mutually exclusive with oss.role_arn.")
+ private String ecsRamRoleName = "";
+
+ @ConnectorProperty(names = {"oss.oidc_provider_arn"},
+ required = false,
+ description = "OIDC provider ARN for RRSA (Kubernetes pod identity).")
+ private String oidcProviderArn = "";
+
+ @ConnectorProperty(names = {"oss.oidc_token_file"},
+ required = false,
+ description = "Path to OIDC token file mounted by Kubernetes.")
+ private String oidcTokenFile = "";
+
+ @ConnectorProperty(names = {CREDENTIALS_PROVIDER, "AWS_CREDENTIALS_PROVIDER_TYPE"},
+ required = false,
+ description = "Credential provider mode: DEFAULT, INSTANCE_PROFILE, OIDC, ENV, ANONYMOUS.")
+ private String credentialsProvider = DEFAULT_CREDENTIALS_PROVIDER;
+
@ConnectorProperty(names = {"AWS_ROOT_PATH"},
required = false,
description = "The root path prefix inside the bucket.")
@@ -178,6 +206,14 @@ public static OssFileSystemProperties of(Map properties) {
@Override
public void validate() {
new ParamRules()
+ .mutuallyExclusive(roleArn, ecsRamRoleName,
+ "OSS_ROLE_ARN and oss.ecs_ram_role_name are mutually exclusive.")
+ .mutuallyExclusive(ecsRamRoleName, oidcProviderArn,
+ "oss.ecs_ram_role_name and oss.oidc_provider_arn are mutually exclusive.")
+ .requireTogether(new String[] {oidcProviderArn, oidcTokenFile},
+ "oss.oidc_provider_arn and oss.oidc_token_file must be set together.")
+ .check(this::hasUnsupportedCredentialsProviderType,
+ "Unsupported oss.credentials_provider: " + credentialsProvider)
.requireTogether(new String[] {accessKey, secretKey},
"Both the access key and the secret key must be set.")
.check(() -> StringUtils.isBlank(region),
@@ -313,6 +349,35 @@ public String getRoleArn() {
return roleArn;
}
+ public String getRoleSessionName() {
+ return roleSessionName;
+ }
+
+ public String getEcsRamRoleName() {
+ return ecsRamRoleName;
+ }
+
+ public String getOidcProviderArn() {
+ return oidcProviderArn;
+ }
+
+ public String getOidcTokenFile() {
+ return oidcTokenFile;
+ }
+
+ public OssCredentialsProviderType getCredentialsProviderType() {
+ return OssCredentialsProviderType.fromString(credentialsProvider);
+ }
+
+ private boolean hasUnsupportedCredentialsProviderType() {
+ try {
+ getCredentialsProviderType();
+ return false;
+ } catch (IllegalArgumentException e) {
+ return true;
+ }
+ }
+
@Override
public String getExternalId() {
return externalId;
diff --git a/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/OssObjStorage.java b/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/OssObjStorage.java
index cfb1da6853270c..8f370bb2859531 100644
--- a/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/OssObjStorage.java
+++ b/fe/fe-filesystem/fe-filesystem-oss/src/main/java/org/apache/doris/filesystem/oss/OssObjStorage.java
@@ -176,9 +176,53 @@ private static boolean isVirtualHostCompatible(String bucket) {
protected OSS buildOssClient(boolean pathStyle) throws IOException {
String endpoint = properties.getEndpoint();
+ OssCredentialsProviderType type = properties.getCredentialsProviderType();
+ switch (type) {
+ case INSTANCE_PROFILE:
+ return new OSSClientBuilder().build(endpoint,
+ new com.aliyun.oss.common.auth.EcsRamRoleCredentialsProvider(
+ resolveEcsRoleName()),
+ clientConfiguration(pathStyle));
+ case OIDC:
+ return new OSSClientBuilder().build(endpoint,
+ new AliyunCredentialsBridge(buildOidcProvider()),
+ clientConfiguration(pathStyle));
+ case ENV:
+ return new OSSClientBuilder().build(endpoint,
+ new com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider(),
+ clientConfiguration(pathStyle));
+ case ANONYMOUS:
+ return new OSSClientBuilder().build(endpoint, anonymousCredentialsProvider(),
+ anonymousClientConfiguration(pathStyle));
+ default:
+ break; // DEFAULT — fall through to property-driven chain below
+ }
+ // DEFAULT: property-driven chain
+ if (hasText(properties.getEcsRamRoleName())) {
+ return new OSSClientBuilder().build(endpoint,
+ new com.aliyun.oss.common.auth.EcsRamRoleCredentialsProvider(
+ properties.getEcsRamRoleName()),
+ clientConfiguration(pathStyle));
+ }
+ if (hasText(properties.getOidcProviderArn())) {
+ return new OSSClientBuilder().build(endpoint,
+ new AliyunCredentialsBridge(buildOidcProvider()),
+ clientConfiguration(pathStyle));
+ }
+ if (hasText(properties.getRoleArn())) {
+ return new OSSClientBuilder().build(endpoint,
+ new AliyunCredentialsBridge(buildAssumeRoleProvider()),
+ clientConfiguration(pathStyle));
+ }
+ // existing ak/sk / anonymous logic — unchanged
String accessKey = properties.getAccessKey();
String secretKey = properties.getSecretKey();
if (!hasText(accessKey)) {
+ if (hasText(System.getenv("OSS_ACCESS_KEY_ID"))) {
+ return new OSSClientBuilder().build(endpoint,
+ new com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider(),
+ clientConfiguration(pathStyle));
+ }
return new OSSClientBuilder().build(endpoint, anonymousCredentialsProvider(),
anonymousClientConfiguration(pathStyle));
}
@@ -190,6 +234,57 @@ protected OSS buildOssClient(boolean pathStyle) throws IOException {
return new OSSClientBuilder().build(endpoint, accessKey, secretKey, config);
}
+ // protected so tests can override
+ protected String discoverEcsRoleName() throws IOException {
+ java.net.URL url = new java.net.URL(
+ "http://100.100.100.200/latest/meta-data/ram/security-credentials/");
+ java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection();
+ conn.setConnectTimeout(3000);
+ conn.setReadTimeout(3000);
+ try (java.io.InputStream in = conn.getInputStream()) {
+ return new String(in.readAllBytes()).trim().split("\n")[0].trim();
+ } finally {
+ conn.disconnect();
+ }
+ }
+
+ private String resolveEcsRoleName() throws IOException {
+ String name = properties.getEcsRamRoleName();
+ return hasText(name) ? name : discoverEcsRoleName();
+ }
+
+ private com.aliyuncs.auth.AlibabaCloudCredentialsProvider buildOidcProvider() {
+ String roleArn = hasText(properties.getRoleArn())
+ ? properties.getRoleArn() : System.getenv("ALIBABA_CLOUD_ROLE_ARN");
+ String providerArn = hasText(properties.getOidcProviderArn())
+ ? properties.getOidcProviderArn()
+ : System.getenv("ALIBABA_CLOUD_OIDC_PROVIDER_ARN");
+ String tokenFile = hasText(properties.getOidcTokenFile())
+ ? properties.getOidcTokenFile()
+ : System.getenv("ALIBABA_CLOUD_OIDC_TOKEN_FILE");
+ return com.aliyuncs.auth.OIDCCredentialsProvider.builder()
+ .roleArn(roleArn)
+ .oidcProviderArn(providerArn)
+ .oidcTokenFilePath(tokenFile)
+ .roleSessionName(properties.getRoleSessionName())
+ .stsRegionId(properties.getRegion())
+ .build();
+ }
+
+ private com.aliyuncs.auth.AlibabaCloudCredentialsProvider buildAssumeRoleProvider() {
+ com.aliyuncs.auth.STSAssumeRoleSessionCredentialsProvider.Builder builder =
+ com.aliyuncs.auth.STSAssumeRoleSessionCredentialsProvider.builder()
+ .accessKeyId(properties.getAccessKey())
+ .accessKeySecret(properties.getSecretKey())
+ .roleArn(properties.getRoleArn())
+ .roleSessionName(properties.getRoleSessionName())
+ .stsRegionId(properties.getRegion());
+ if (hasText(properties.getExternalId())) {
+ builder.externalId(properties.getExternalId());
+ }
+ return builder.build();
+ }
+
@Override
public RemoteObjects listObjects(String remotePath, String continuationToken) throws IOException {
return listObjectsWithOptions(remotePath, ObjectListOptions.builder()
diff --git a/fe/fe-filesystem/fe-filesystem-oss/src/test/java/org/apache/doris/filesystem/oss/OssCredentialProviderTest.java b/fe/fe-filesystem/fe-filesystem-oss/src/test/java/org/apache/doris/filesystem/oss/OssCredentialProviderTest.java
new file mode 100644
index 00000000000000..edbe9245d72428
--- /dev/null
+++ b/fe/fe-filesystem/fe-filesystem-oss/src/test/java/org/apache/doris/filesystem/oss/OssCredentialProviderTest.java
@@ -0,0 +1,118 @@
+// 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.doris.filesystem.oss;
+
+import com.aliyun.oss.common.auth.Credentials;
+import com.aliyuncs.auth.AlibabaCloudCredentials;
+import com.aliyuncs.auth.BasicSessionCredentials;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for credential bridging and provider selection in OssObjStorage.
+ * No real OSS credentials required — all external calls are overridden.
+ */
+class OssCredentialProviderTest {
+
+ // ── AliyunCredentialsBridge ───────────────────────────────────────────────
+
+ @Test
+ void bridge_wrapsSessionCredentialsWithToken() {
+ BasicSessionCredentials alibabaCreds =
+ new BasicSessionCredentials("session-ak", "session-sk", "session-token");
+
+ AliyunCredentialsBridge bridge = new AliyunCredentialsBridge(() -> alibabaCreds);
+
+ Credentials ossCreds = bridge.getCredentials();
+ Assertions.assertEquals("session-ak", ossCreds.getAccessKeyId());
+ Assertions.assertEquals("session-sk", ossCreds.getSecretAccessKey());
+ Assertions.assertEquals("session-token", ossCreds.getSecurityToken());
+ }
+
+ @Test
+ void bridge_wrapsBasicCredentialsWithoutToken() {
+ AlibabaCloudCredentials alibabaCreds = new com.aliyuncs.auth.BasicCredentials("ak", "sk");
+
+ AliyunCredentialsBridge bridge = new AliyunCredentialsBridge(() -> alibabaCreds);
+
+ Credentials ossCreds = bridge.getCredentials();
+ Assertions.assertEquals("ak", ossCreds.getAccessKeyId());
+ Assertions.assertEquals("sk", ossCreds.getSecretAccessKey());
+ Assertions.assertTrue(ossCreds.getSecurityToken() == null
+ || ossCreds.getSecurityToken().isEmpty());
+ }
+
+ @Test
+ void bridge_wrapsProviderException() {
+ AliyunCredentialsBridge bridge = new AliyunCredentialsBridge(() -> {
+ throw new com.aliyuncs.exceptions.ClientException("network error");
+ });
+
+ Assertions.assertThrows(IllegalStateException.class, bridge::getCredentials);
+ }
+
+ // ── OssObjStorage — INSTANCE_PROFILE mode auto-discovery ─────────────────
+
+ @Test
+ void objStorage_instanceProfileMode_callsDiscoverEcsRoleName() {
+ OssFileSystemProperties props = OssFileSystemProperties.of(java.util.Map.of(
+ "oss.endpoint", "https://oss-cn-hangzhou.aliyuncs.com",
+ "oss.credentials_provider", "instance_profile"));
+
+ String[] captured = {null};
+ OssObjStorage storage = new OssObjStorage(props) {
+ @Override
+ protected String discoverEcsRoleName() {
+ captured[0] = "discovered";
+ return "AutoRole";
+ }
+ };
+
+ try {
+ storage.getClient();
+ } catch (Exception ignored) {
+ // expected — no real endpoint in tests
+ }
+ Assertions.assertEquals("discovered", captured[0],
+ "discoverEcsRoleName() should be called when no role name is set");
+ }
+
+ @Test
+ void objStorage_instanceProfileMode_usesConfiguredRoleNameWithoutDiscovery() {
+ OssFileSystemProperties props = OssFileSystemProperties.of(java.util.Map.of(
+ "oss.endpoint", "https://oss-cn-hangzhou.aliyuncs.com",
+ "oss.ecs_ram_role_name", "ExplicitRole",
+ "oss.credentials_provider", "instance_profile"));
+
+ String[] captured = {null};
+ OssObjStorage storage = new OssObjStorage(props) {
+ @Override
+ protected String discoverEcsRoleName() {
+ captured[0] = "should-not-be-called";
+ return "AutoRole";
+ }
+ };
+
+ try {
+ storage.getClient();
+ } catch (Exception ignored) {
+ // expected
+ }
+ Assertions.assertNull(captured[0], "discoverEcsRoleName() should NOT be called when role name is set");
+ }
+}
diff --git a/fe/fe-filesystem/fe-filesystem-oss/src/test/java/org/apache/doris/filesystem/oss/OssFileSystemPropertiesTest.java b/fe/fe-filesystem/fe-filesystem-oss/src/test/java/org/apache/doris/filesystem/oss/OssFileSystemPropertiesTest.java
index a4d7a11c6ce962..005bac4e5835f4 100644
--- a/fe/fe-filesystem/fe-filesystem-oss/src/test/java/org/apache/doris/filesystem/oss/OssFileSystemPropertiesTest.java
+++ b/fe/fe-filesystem/fe-filesystem-oss/src/test/java/org/apache/doris/filesystem/oss/OssFileSystemPropertiesTest.java
@@ -180,4 +180,129 @@ void objStorageDoesNotExposeLegacyToS3PropsTranslator() {
Assertions.assertNotEquals("toS3Props", method.getName());
}
}
+
+ @Test
+ void bind_roleArnAndRoleSessionName() {
+ OssFileSystemProperties p = OssFileSystemProperties.of(Map.of(
+ "oss.endpoint", "https://oss-cn-hangzhou.aliyuncs.com",
+ "oss.access_key", "ak",
+ "oss.secret_key", "sk",
+ "oss.role_arn", "acs:ram::123:role/r",
+ "oss.role_session_name", "my-session",
+ "oss.external_id", "ext-id"));
+
+ Assertions.assertEquals("acs:ram::123:role/r", p.getRoleArn());
+ Assertions.assertEquals("my-session", p.getRoleSessionName());
+ Assertions.assertEquals("ext-id", p.getExternalId());
+ }
+
+ @Test
+ void bind_roleSessionName_defaultsToDorisSession() {
+ OssFileSystemProperties p = OssFileSystemProperties.of(Map.of(
+ "oss.endpoint", "https://oss-cn-hangzhou.aliyuncs.com",
+ "oss.access_key", "ak",
+ "oss.secret_key", "sk",
+ "oss.role_arn", "acs:ram::123:role/r"));
+
+ Assertions.assertEquals("doris-session", p.getRoleSessionName());
+ }
+
+ @Test
+ void bind_ecsRamRoleName() {
+ OssFileSystemProperties p = OssFileSystemProperties.of(Map.of(
+ "oss.endpoint", "https://oss-cn-hangzhou.aliyuncs.com",
+ "oss.ecs_ram_role_name", "my-ecs-role"));
+
+ Assertions.assertEquals("my-ecs-role", p.getEcsRamRoleName());
+ Assertions.assertEquals("", p.getAccessKey());
+ }
+
+ @Test
+ void validate_roleArnAndEcsRamRoleAreMutuallyExclusive() {
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
+ OssFileSystemProperties.of(Map.of(
+ "oss.endpoint", "https://oss-cn-hangzhou.aliyuncs.com",
+ "oss.access_key", "ak",
+ "oss.secret_key", "sk",
+ "oss.role_arn", "acs:ram::123:role/r",
+ "oss.ecs_ram_role_name", "my-role")));
+ }
+
+ @Test
+ void validate_legacyRoleArnAliasesResolved() {
+ OssFileSystemProperties p = OssFileSystemProperties.of(Map.of(
+ "oss.endpoint", "https://oss-cn-hangzhou.aliyuncs.com",
+ "oss.access_key", "ak",
+ "oss.secret_key", "sk",
+ "OSS_ROLE_ARN", "acs:ram::123:role/legacy"));
+
+ Assertions.assertEquals("acs:ram::123:role/legacy", p.getRoleArn());
+ }
+
+ @Test
+ void bind_oidcRrsaProperties() {
+ OssFileSystemProperties p = OssFileSystemProperties.of(Map.of(
+ "oss.endpoint", "https://oss-cn-hangzhou.aliyuncs.com",
+ "OSS_ROLE_ARN", "acs:ram::123:role/r",
+ "oss.oidc_provider_arn", "acs:ram::123:oidc-provider/p",
+ "oss.oidc_token_file", "/var/run/secrets/token",
+ "oss.role_session_name", "doris-k8s"));
+
+ Assertions.assertEquals("acs:ram::123:role/r", p.getRoleArn());
+ Assertions.assertEquals("acs:ram::123:oidc-provider/p", p.getOidcProviderArn());
+ Assertions.assertEquals("/var/run/secrets/token", p.getOidcTokenFile());
+ Assertions.assertEquals("doris-k8s", p.getRoleSessionName());
+ }
+
+ @Test
+ void validate_oidcProviderArnRequiresTokenFileTogether() {
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
+ OssFileSystemProperties.of(Map.of(
+ "oss.endpoint", "https://oss-cn-hangzhou.aliyuncs.com",
+ "oss.oidc_provider_arn", "acs:ram::123:oidc-provider/p"
+ // oss.oidc_token_file missing — must be set together
+ )));
+ }
+
+ @Test
+ void validate_ecsRamRoleAndOidcAreMutuallyExclusive() {
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
+ OssFileSystemProperties.of(Map.of(
+ "oss.endpoint", "https://oss-cn-hangzhou.aliyuncs.com",
+ "oss.ecs_ram_role_name", "my-role",
+ "oss.oidc_provider_arn", "acs:ram::123:oidc-provider/p",
+ "oss.oidc_token_file", "/var/run/secrets/token")));
+ }
+
+ @Test
+ void bind_credentialsProviderType_instanceProfile() {
+ OssFileSystemProperties p = OssFileSystemProperties.of(Map.of(
+ "oss.endpoint", "https://oss-cn-hangzhou.aliyuncs.com",
+ "oss.credentials_provider", "instance_profile"));
+
+ Assertions.assertEquals(OssCredentialsProviderType.INSTANCE_PROFILE,
+ p.getCredentialsProviderType());
+ }
+
+ @Test
+ void bind_credentialsProviderType_aliases() {
+ Assertions.assertEquals(OssCredentialsProviderType.INSTANCE_PROFILE,
+ OssCredentialsProviderType.fromString("ecs"));
+ Assertions.assertEquals(OssCredentialsProviderType.OIDC,
+ OssCredentialsProviderType.fromString("rrsa"));
+ Assertions.assertEquals(OssCredentialsProviderType.OIDC,
+ OssCredentialsProviderType.fromString("WEB_IDENTITY"));
+ Assertions.assertEquals(OssCredentialsProviderType.DEFAULT,
+ OssCredentialsProviderType.fromString(""));
+ Assertions.assertEquals(OssCredentialsProviderType.DEFAULT,
+ OssCredentialsProviderType.fromString(null));
+ }
+
+ @Test
+ void validate_unsupportedCredentialsProvider() {
+ Assertions.assertThrows(IllegalArgumentException.class, () ->
+ OssFileSystemProperties.of(Map.of(
+ "oss.endpoint", "https://oss-cn-hangzhou.aliyuncs.com",
+ "oss.credentials_provider", "INVALID_TYPE")));
+ }
}