Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions fe/fe-filesystem/fe-filesystem-oss/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ under the License.
<artifactId>aliyun-sdk-oss</artifactId>
<version>${aliyun-sdk-oss.version}</version>
</dependency>
<!-- Explicit: aliyun-java-sdk-core used directly in AssumeRole and ECS credential providers -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.7.6</version>
</dependency>
<!-- Explicit: aliyun-java-sdk-sts not transitively pulled by aliyun-sdk-oss -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-sts</artifactId>
<version>3.1.3</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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()}.
*
* <p>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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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.")
Expand All @@ -178,6 +206,14 @@ public static OssFileSystemProperties of(Map<String, String> 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),
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand All @@ -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()
Expand Down
Loading
Loading