Skip to content

Commit b48e118

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

File tree

7 files changed

+293
-20
lines changed

7 files changed

+293
-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>
@@ -113,6 +114,19 @@
113114
<scope>compile</scope>
114115
</dependency>
115116

117+
<dependency>
118+
<groupId>org.assertj</groupId>
119+
<artifactId>assertj-core</artifactId>
120+
<scope>test</scope>
121+
</dependency>
122+
123+
<dependency>
124+
<groupId>com.tencentcloudapi</groupId>
125+
<artifactId>tencentcloud-sdk-java</artifactId>
126+
<version>${tencentcloud.verion}</version>
127+
<scope>test</scope>
128+
</dependency>
129+
116130
<dependency>
117131
<groupId>org.apache.hadoop</groupId>
118132
<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: 45 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.Test;
2428
import org.slf4j.Logger;
2529
import org.slf4j.LoggerFactory;
@@ -28,12 +32,14 @@
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;
3137
import static org.junit.Assert.assertNotNull;
38+
import static org.junit.Assert.assertTrue;
3239
import static org.junit.Assert.fail;
3340

3441
public class TestCosCredentials {
35-
private static final Logger LOG =
36-
LoggerFactory.getLogger(TestCosCredentials.class);
42+
private static final Logger LOG = LoggerFactory.getLogger(TestCosCredentials.class);
3743

3844
private final URI fsUri;
3945

@@ -50,10 +56,8 @@ public TestCosCredentials() throws URISyntaxException {
5056
@Test
5157
public void testSimpleCredentialsProvider() throws Throwable {
5258
Configuration configuration = new Configuration();
53-
configuration.set(CosNConfigKeys.COSN_SECRET_ID_KEY,
54-
testCosNSecretId);
55-
configuration.set(CosNConfigKeys.COSN_SECRET_KEY_KEY,
56-
testCosNSecretKey);
59+
configuration.set(CosNConfigKeys.COSN_SECRET_ID_KEY, testCosNSecretId);
60+
configuration.set(CosNConfigKeys.COSN_SECRET_KEY_KEY, testCosNSecretKey);
5761
validateCredentials(this.fsUri, configuration);
5862
}
5963

@@ -63,23 +67,22 @@ public void testEnvironmentCredentialsProvider() throws Throwable {
6367
// Set EnvironmentVariableCredentialsProvider as the CosCredentials
6468
// Provider.
6569
configuration.set(CosNConfigKeys.COSN_CREDENTIALS_PROVIDER,
66-
"org.apache.hadoop.fs.cosn.EnvironmentVariableCredentialsProvider");
70+
"org.apache.hadoop.fs.cosn.auth.EnvironmentVariableCredentialsProvider");
6771
// Set the environment variables storing the secret id and secret key.
6872
System.setProperty(Constants.COSN_SECRET_ID_ENV, testCosNEnvSecretId);
6973
System.setProperty(Constants.COSN_SECRET_KEY_ENV, testCosNEnvSecretKey);
7074
validateCredentials(this.fsUri, configuration);
7175
}
7276

73-
private void validateCredentials(URI uri, Configuration configuration)
74-
throws IOException {
77+
private void validateCredentials(URI uri, Configuration configuration) throws IOException {
7578
if (null != configuration) {
7679
COSCredentialsProvider credentialsProvider =
7780
CosNUtils.createCosCredentialsProviderSet(uri, configuration);
7881
COSCredentials cosCredentials = credentialsProvider.getCredentials();
7982
assertNotNull("The cos credentials obtained is null.", cosCredentials);
8083
if (configuration.get(
8184
CosNConfigKeys.COSN_CREDENTIALS_PROVIDER).compareToIgnoreCase(
82-
"org.apache.hadoop.fs.cosn.EnvironmentVariableCredentialsProvider")
85+
"org.apache.hadoop.fs.cosn.auth.EnvironmentVariableCredentialsProvider")
8386
== 0) {
8487
if (null == cosCredentials.getCOSAccessKeyId()
8588
|| cosCredentials.getCOSAccessKeyId().isEmpty()
@@ -131,4 +134,36 @@ private void validateCredentials(URI uri, Configuration configuration)
131134
}
132135
}
133136
}
137+
138+
@Test
139+
public void testTmpTokenCredentialsProvider() throws Throwable {
140+
Configuration configuration = new Configuration();
141+
// Set DynamicTemporaryCosnCredentialsProvider as the CosCredentials
142+
// Provider.
143+
configuration.set(CosNConfigKeys.COSN_CREDENTIALS_PROVIDER,
144+
"org.apache.hadoop.fs.cosn.auth.DynamicTemporaryCosnCredentialsProvider");
145+
146+
configuration.set(STS_SECRET_ID_KEY, System.getProperty(STS_SECRET_ID_KEY));
147+
configuration.set(STS_SECRET_KEY_KEY, System.getProperty(STS_SECRET_KEY_KEY));
148+
validateTmpTokenCredentials(this.fsUri, configuration);
149+
}
150+
151+
private void validateTmpTokenCredentials(URI uri, Configuration configuration)
152+
throws IOException {
153+
COSCredentialsProvider credentialsProvider =
154+
CosNUtils.createCosCredentialsProviderSet(uri, configuration);
155+
COSCredentials cosCredentials = credentialsProvider.getCredentials();
156+
assertNotNull("The cos credentials obtained is null.", cosCredentials);
157+
assertTrue("CredentialsProvider must be DynamicTemporaryCosnCredentialsProvider",
158+
StringUtils.equalsIgnoreCase(configuration.get(CosNConfigKeys.COSN_CREDENTIALS_PROVIDER),
159+
"org.apache.hadoop.fs.cosn.auth.DynamicTemporaryCosnCredentialsProvider"));
160+
161+
assertTrue("cosCredentials must be instanceof BasicSessionCredentials",
162+
cosCredentials instanceof BasicSessionCredentials);
163+
164+
assertNotNull("session access key id is null", cosCredentials.getCOSAccessKeyId());
165+
assertNotNull("session access key is null", cosCredentials.getCOSSecretKey());
166+
assertNotNull("access token is null",
167+
((BasicSessionCredentials) cosCredentials).getSessionToken());
168+
}
134169
}
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)