Skip to content

Commit 6c493f5

Browse files
leosanqingcnauroth
authored andcommitted
HADOOP-19648. Cos use token credential will lose token field
Closes #7867 Signed-off-by: Chris Nauroth <[email protected]>
1 parent 0d7efae commit 6c493f5

File tree

7 files changed

+294
-20
lines changed

7 files changed

+294
-20
lines changed

hadoop-cloud-storage-project/hadoop-cos/pom.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
<properties>
3535
<file.encoding>UTF-8</file.encoding>
3636
<downloadSources>true</downloadSources>
37+
<tencentcloud.verion>3.1.1322</tencentcloud.verion>
3738
</properties>
3839

3940
<profiles>
@@ -108,6 +109,19 @@
108109
<scope>compile</scope>
109110
</dependency>
110111

112+
<dependency>
113+
<groupId>org.assertj</groupId>
114+
<artifactId>assertj-core</artifactId>
115+
<scope>test</scope>
116+
</dependency>
117+
118+
<dependency>
119+
<groupId>com.tencentcloudapi</groupId>
120+
<artifactId>tencentcloud-sdk-java</artifactId>
121+
<version>${tencentcloud.verion}</version>
122+
<scope>test</scope>
123+
</dependency>
124+
111125
<dependency>
112126
<groupId>org.apache.hadoop</groupId>
113127
<artifactId>hadoop-common</artifactId>

hadoop-cloud-storage-project/hadoop-cos/src/main/java/org/apache/hadoop/fs/cosn/CosNativeFileSystemStore.java

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@
3232

3333
import com.qcloud.cos.COSClient;
3434
import com.qcloud.cos.ClientConfig;
35-
import com.qcloud.cos.auth.BasicCOSCredentials;
36-
import com.qcloud.cos.auth.COSCredentials;
3735
import com.qcloud.cos.endpoint.SuffixEndpointBuilder;
3836
import com.qcloud.cos.exception.CosClientException;
3937
import com.qcloud.cos.exception.CosServiceException;
@@ -103,11 +101,6 @@ private void initCOSClient(URI uri, Configuration conf) throws IOException {
103101
throw new IOException(exceptionMsg);
104102
}
105103

106-
COSCredentials cosCred;
107-
cosCred = new BasicCOSCredentials(
108-
credentialProviderList.getCredentials().getCOSAccessKeyId(),
109-
credentialProviderList.getCredentials().getCOSSecretKey());
110-
111104
boolean useHttps = conf.getBoolean(CosNConfigKeys.COSN_USE_HTTPS_KEY,
112105
CosNConfigKeys.DEFAULT_USE_HTTPS);
113106

@@ -133,7 +126,7 @@ private void initCOSClient(URI uri, Configuration conf) throws IOException {
133126
conf.getInt(CosNConfigKeys.MAX_CONNECTION_NUM,
134127
CosNConfigKeys.DEFAULT_MAX_CONNECTION_NUM));
135128

136-
this.cosClient = new COSClient(cosCred, config);
129+
this.cosClient = new COSClient(credentialProviderList.getCredentials(), config);
137130
}
138131

139132
/**

hadoop-cloud-storage-project/hadoop-cos/src/test/java/org/apache/hadoop/fs/cosn/TestCosCredentials.java

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@
1717
*/
1818
package org.apache.hadoop.fs.cosn;
1919

20+
import com.qcloud.cos.auth.BasicSessionCredentials;
2021
import com.qcloud.cos.auth.COSCredentials;
2122
import com.qcloud.cos.auth.COSCredentialsProvider;
23+
24+
import org.apache.commons.lang3.StringUtils;
2225
import org.apache.hadoop.conf.Configuration;
26+
2327
import org.junit.jupiter.api.Test;
2428
import org.slf4j.Logger;
2529
import org.slf4j.LoggerFactory;
@@ -28,12 +32,15 @@
2832
import java.net.URI;
2933
import java.net.URISyntaxException;
3034

35+
import static org.apache.hadoop.fs.cosn.auth.DynamicTemporaryCosnCredentialsProvider.STS_SECRET_ID_KEY;
36+
import static org.apache.hadoop.fs.cosn.auth.DynamicTemporaryCosnCredentialsProvider.STS_SECRET_KEY_KEY;
37+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
3138
import static org.junit.jupiter.api.Assertions.assertNotNull;
39+
import static org.junit.jupiter.api.Assertions.assertTrue;
3240
import static org.junit.jupiter.api.Assertions.fail;
3341

3442
public class TestCosCredentials {
35-
private static final Logger LOG =
36-
LoggerFactory.getLogger(TestCosCredentials.class);
43+
private static final Logger LOG = LoggerFactory.getLogger(TestCosCredentials.class);
3744

3845
private final URI fsUri;
3946

@@ -50,10 +57,8 @@ public TestCosCredentials() throws URISyntaxException {
5057
@Test
5158
public void testSimpleCredentialsProvider() throws Throwable {
5259
Configuration configuration = new Configuration();
53-
configuration.set(CosNConfigKeys.COSN_SECRET_ID_KEY,
54-
testCosNSecretId);
55-
configuration.set(CosNConfigKeys.COSN_SECRET_KEY_KEY,
56-
testCosNSecretKey);
60+
configuration.set(CosNConfigKeys.COSN_SECRET_ID_KEY, testCosNSecretId);
61+
configuration.set(CosNConfigKeys.COSN_SECRET_KEY_KEY, testCosNSecretKey);
5762
validateCredentials(this.fsUri, configuration);
5863
}
5964

@@ -63,23 +68,22 @@ public void testEnvironmentCredentialsProvider() throws Throwable {
6368
// Set EnvironmentVariableCredentialsProvider as the CosCredentials
6469
// Provider.
6570
configuration.set(CosNConfigKeys.COSN_CREDENTIALS_PROVIDER,
66-
"org.apache.hadoop.fs.cosn.EnvironmentVariableCredentialsProvider");
71+
"org.apache.hadoop.fs.cosn.auth.EnvironmentVariableCredentialsProvider");
6772
// Set the environment variables storing the secret id and secret key.
6873
System.setProperty(Constants.COSN_SECRET_ID_ENV, testCosNEnvSecretId);
6974
System.setProperty(Constants.COSN_SECRET_KEY_ENV, testCosNEnvSecretKey);
7075
validateCredentials(this.fsUri, configuration);
7176
}
7277

73-
private void validateCredentials(URI uri, Configuration configuration)
74-
throws IOException {
78+
private void validateCredentials(URI uri, Configuration configuration) throws IOException {
7579
if (null != configuration) {
7680
COSCredentialsProvider credentialsProvider =
7781
CosNUtils.createCosCredentialsProviderSet(uri, configuration);
7882
COSCredentials cosCredentials = credentialsProvider.getCredentials();
7983
assertNotNull(cosCredentials, "The cos credentials obtained is null.");
8084
if (configuration.get(
8185
CosNConfigKeys.COSN_CREDENTIALS_PROVIDER).compareToIgnoreCase(
82-
"org.apache.hadoop.fs.cosn.EnvironmentVariableCredentialsProvider")
86+
"org.apache.hadoop.fs.cosn.auth.EnvironmentVariableCredentialsProvider")
8387
== 0) {
8488
if (null == cosCredentials.getCOSAccessKeyId()
8589
|| cosCredentials.getCOSAccessKeyId().isEmpty()
@@ -131,4 +135,36 @@ private void validateCredentials(URI uri, Configuration configuration)
131135
}
132136
}
133137
}
138+
139+
@Test
140+
public void testTmpTokenCredentialsProvider() throws Throwable {
141+
Configuration configuration = new Configuration();
142+
// Set DynamicTemporaryCosnCredentialsProvider as the CosCredentials
143+
// Provider.
144+
configuration.set(CosNConfigKeys.COSN_CREDENTIALS_PROVIDER,
145+
"org.apache.hadoop.fs.cosn.auth.DynamicTemporaryCosnCredentialsProvider");
146+
147+
configuration.set(STS_SECRET_ID_KEY, System.getProperty(STS_SECRET_ID_KEY));
148+
configuration.set(STS_SECRET_KEY_KEY, System.getProperty(STS_SECRET_KEY_KEY));
149+
validateTmpTokenCredentials(this.fsUri, configuration);
150+
}
151+
152+
private void validateTmpTokenCredentials(URI uri, Configuration configuration)
153+
throws IOException {
154+
COSCredentialsProvider credentialsProvider =
155+
CosNUtils.createCosCredentialsProviderSet(uri, configuration);
156+
COSCredentials cosCredentials = credentialsProvider.getCredentials();
157+
assertNotNull(cosCredentials, "The cos credentials obtained is null.");
158+
assertTrue(
159+
StringUtils.equalsIgnoreCase(configuration.get(CosNConfigKeys.COSN_CREDENTIALS_PROVIDER),
160+
"org.apache.hadoop.fs.cosn.auth.DynamicTemporaryCosnCredentialsProvider"),
161+
"CredentialsProvider must be DynamicTemporaryCosnCredentialsProvider");
162+
163+
assertInstanceOf(BasicSessionCredentials.class, cosCredentials,
164+
"cosCredentials must be instanceof BasicSessionCredentials");
165+
assertNotNull(cosCredentials.getCOSAccessKeyId(), "session access key id is null");
166+
assertNotNull(cosCredentials.getCOSSecretKey(), "session access key is null");
167+
assertNotNull(((BasicSessionCredentials) cosCredentials).getSessionToken(),
168+
"access token is null");
169+
}
134170
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.hadoop.fs.cosn.auth;
19+
20+
import com.qcloud.cos.auth.BasicSessionCredentials;
21+
import com.qcloud.cos.auth.COSCredentials;
22+
import com.qcloud.cos.auth.COSCredentialsProvider;
23+
24+
import org.apache.hadoop.conf.Configuration;
25+
import org.apache.hadoop.fs.cosn.CosNConfigKeys;
26+
27+
import com.tencentcloudapi.common.Credential;
28+
import com.tencentcloudapi.common.profile.ClientProfile;
29+
import com.tencentcloudapi.common.profile.HttpProfile;
30+
import com.tencentcloudapi.sts.v20180813.StsClient;
31+
import com.tencentcloudapi.sts.v20180813.models.GetFederationTokenRequest;
32+
import com.tencentcloudapi.sts.v20180813.models.GetFederationTokenResponse;
33+
import org.slf4j.Logger;
34+
import org.slf4j.LoggerFactory;
35+
36+
import java.io.IOException;
37+
import java.util.concurrent.atomic.AtomicReference;
38+
39+
/**
40+
* A COSCredentialsProvider that generates temporary credentials from Tencent Cloud STS.
41+
* This provider requires a long-term secret ID and key with permission to call
42+
* the STS GetFederationToken action.
43+
*/
44+
public class DynamicTemporaryCosnCredentialsProvider implements COSCredentialsProvider {
45+
private static final Logger LOG =
46+
LoggerFactory.getLogger(DynamicTemporaryCosnCredentialsProvider.class);
47+
48+
public static final String STS_SECRET_ID_KEY = "fs.cosn.auth.sts.secret.id";
49+
public static final String STS_SECRET_KEY_KEY = "fs.cosn.auth.sts.secret.key";
50+
public static final String STS_ENDPOINT_KEY = "fs.cosn.auth.sts.endpoint";
51+
public static final String DEFAULT_STS_ENDPOINT = "sts.tencentcloudapi.com";
52+
public static final String TOKEN_DURATION_SECONDS_KEY = "fs.cosn.auth.sts.token.duration.seconds";
53+
public static final int DEFAULT_TOKEN_DURATION_SECONDS = 900; // 15 minutes
54+
55+
private final String longTermSecretId;
56+
private final String longTermSecretKey;
57+
private final String stsEndpoint;
58+
private final String region;
59+
private final String bucketName;
60+
private final long durationSeconds;
61+
62+
private final AtomicReference<ExpiringCredentials> expiringCredentialsRef =
63+
new AtomicReference<>();
64+
65+
public DynamicTemporaryCosnCredentialsProvider(Configuration conf) throws IOException {
66+
this.longTermSecretId = conf.get(STS_SECRET_ID_KEY);
67+
this.longTermSecretKey = conf.get(STS_SECRET_KEY_KEY);
68+
this.stsEndpoint = conf.get(STS_ENDPOINT_KEY, DEFAULT_STS_ENDPOINT);
69+
this.region = conf.get(CosNConfigKeys.COSN_REGION_KEY);
70+
this.bucketName = conf.get("fs.defaultFS").replace("cosn://", "");
71+
this.durationSeconds = conf.getLong(TOKEN_DURATION_SECONDS_KEY, DEFAULT_TOKEN_DURATION_SECONDS);
72+
73+
if (this.longTermSecretId == null || this.longTermSecretKey == null) {
74+
throw new IOException(
75+
"Long-term STS credentials not provided in configuration. Please set " + STS_SECRET_ID_KEY
76+
+ " and " + STS_SECRET_KEY_KEY);
77+
}
78+
if (this.region == null || this.bucketName == null) {
79+
throw new IOException("Bucket region or name not configured.");
80+
}
81+
}
82+
83+
@Override
84+
public COSCredentials getCredentials() {
85+
ExpiringCredentials current = expiringCredentialsRef.get();
86+
// Refresh if credentials are not present, or are within 60 seconds of expiry.
87+
if (current == null
88+
|| System.currentTimeMillis() >= current.getExpirationTimeMillis() - 60000) {
89+
LOG.info("STS credentials expired or not found, requesting new token.");
90+
refresh();
91+
}
92+
return expiringCredentialsRef.get().getCredentials();
93+
}
94+
95+
@Override
96+
public void refresh() {
97+
try {
98+
Credential cred = new Credential(this.longTermSecretId, this.longTermSecretKey);
99+
HttpProfile httpProfile = new HttpProfile();
100+
httpProfile.setEndpoint(this.stsEndpoint);
101+
ClientProfile clientProfile = new ClientProfile();
102+
clientProfile.setHttpProfile(httpProfile);
103+
104+
StsClient client = new StsClient(cred, this.region, clientProfile);
105+
GetFederationTokenRequest req = new GetFederationTokenRequest();
106+
107+
String policyTemplate = "{\"version\":\"2.0\",\"statement\":[{\"action\":[\"cos:*\"],"
108+
+ "\"effect\":\"allow\",\"resource\":[\"qcs::cos:%s:uid/%s:%s/*\"]}]}";
109+
String policy =
110+
String.format(policyTemplate, this.region, getAppIdFromBucket(this.bucketName),
111+
this.bucketName);
112+
req.setPolicy(policy);
113+
114+
req.setDurationSeconds(this.durationSeconds);
115+
req.setName("HadoopCosNContractTest");
116+
117+
GetFederationTokenResponse resp = client.GetFederationToken(req);
118+
119+
long expirationTimeMillis = (resp.getExpiredTime() * 1000);
120+
BasicSessionCredentials credentials =
121+
new BasicSessionCredentials(resp.getCredentials().getTmpSecretId(),
122+
resp.getCredentials().getTmpSecretKey(), resp.getCredentials().getToken());
123+
124+
expiringCredentialsRef.set(new ExpiringCredentials(credentials, expirationTimeMillis));
125+
LOG.info("Successfully refreshed STS credentials. Expiration: {}",
126+
new java.util.Date(expirationTimeMillis));
127+
128+
} catch (Exception e) {
129+
LOG.error("Failed to get token from STS: {}", e.toString());
130+
throw new RuntimeException("Failed to get token from STS", e);
131+
}
132+
}
133+
134+
private String getAppIdFromBucket(String bucket) {
135+
int lastDash = bucket.lastIndexOf('-');
136+
if (lastDash != -1 && lastDash < bucket.length() - 1) {
137+
return bucket.substring(lastDash + 1);
138+
}
139+
throw new IllegalArgumentException("Could not determine AppID from bucket name: " + bucket);
140+
}
141+
142+
/**
143+
* Helper class to hold credentials and their expiration time.
144+
*/
145+
private static class ExpiringCredentials {
146+
private final BasicSessionCredentials credentials;
147+
private final long expirationTimeMillis;
148+
149+
ExpiringCredentials(BasicSessionCredentials credentials, long expirationTimeMillis) {
150+
this.credentials = credentials;
151+
this.expirationTimeMillis = expirationTimeMillis;
152+
}
153+
154+
BasicSessionCredentials getCredentials() {
155+
return credentials;
156+
}
157+
158+
long getExpirationTimeMillis() {
159+
return expirationTimeMillis;
160+
}
161+
}
162+
}

0 commit comments

Comments
 (0)