Skip to content

Commit 0f36fc9

Browse files
authored
S3 PrivateLink Support.
Controlling access to your VPC endpoints for Amazon S3 by specifying endpointOverride in ClientConfiguration.
1 parent 93b3adc commit 0f36fc9

File tree

17 files changed

+391
-76
lines changed

17 files changed

+391
-76
lines changed

Docs/ClientConfiguration_Parameters.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,14 @@ This value determines the length of time, in milliseconds, to wait before timing
5252
The retry strategy defaults to exponential backoff. You can override this default by implementing a subclass of RetryStrategy and passing an instance.
5353

5454
### Endpoint Override
55-
Do not alter the endpoint.
55+
This value will override the endpoints the client hits.
56+
57+
Specially, for Amazon S3 and S3 Control clients, you could configure this value to access Amazon S3 interface VPC endpoints (AWS PrivateLink).
58+
59+
For example,
60+
- use `bucket.vpce-1a2b3c4d-5e6f.s3.us-east-1.vpce.amazonaws.com` to access S3 buckets.
61+
- use `accesspoint.vpce-1a2b3c4d-5e6f.s3.us-east-1.vpce.amazonaws.com` to access S3 Access Points.
62+
- use `control.vpce-1a2b3c4d-5e6f.s3.us-east-1.vpce.amazonaws.com` for S3 Control APIs.
5663

5764
### Proxy Scheme, Host, Port, User Name, and Password
5865
These settings allow you to configure a proxy for all communication with AWS. Examples of when this functionality might be useful include debugging in conjunction with the Burp suite, or using a proxy to connect to the internet.

aws-cpp-sdk-s3-integration-tests/BucketAndObjectOperationTest.cpp

+117-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <aws/core/client/ClientConfiguration.h>
99
#include <aws/core/client/CoreErrors.h>
1010
#include <aws/core/client/RetryStrategy.h>
11+
#include <aws/core/client/DefaultRetryStrategy.h>
1112
#include <aws/core/http/HttpClientFactory.h>
1213
#include <aws/core/http/HttpClient.h>
1314
#include <aws/core/utils/crypto/Cipher.h>
@@ -42,6 +43,7 @@
4243
#include <aws/testing/ProxyConfig.h>
4344
#include <aws/testing/platform/PlatformTesting.h>
4445
#include <aws/testing/TestingEnvironment.h>
46+
#include <aws/testing/mocks/monitoring/TestingMonitoring.h>
4547
#include <fstream>
4648

4749
#ifdef _WIN32
@@ -76,6 +78,7 @@ namespace
7678
static std::string BASE_EVENT_STREAM_LARGE_FILE_TEST_BUCKET_NAME = "largeeventstream";
7779
static std::string BASE_EVENT_STREAM_ERRORS_IN_EVENT_TEST_BUCKET_NAME = "errorsinevent";
7880
static std::string BASE_CROSS_REGION_BUCKET_NAME = "crossregion";
81+
static std::string BASE_ENDPOINT_OVERRIDE_BUCKET_NAME = "endpointoverride";
7982
static const char* ALLOCATION_TAG = "BucketAndObjectOperationTest";
8083
static const char* TEST_OBJ_KEY = "TestObjectKey";
8184
static const char* TEST_NOT_MODIFIED_OBJ_KEY = "TestNotModifiedObjectKey";
@@ -86,6 +89,7 @@ namespace
8689
//to get around this, this string is url encoded version of "TestUnicode中国Key". At test time, we'll convert it to the unicode string
8790
static const char* URLENCODED_UNICODE_KEY = "TestUnicode%E4%B8%AD%E5%9B%BDKey";
8891
static const char* URIESCAPE_KEY = "Esc ape+Me$";
92+
static const char* CUSTOM_ENDPOINT_OVERRIDE = "beta.example.com";
8993

9094
static const int TIMEOUT_MAX = 20;
9195

@@ -113,6 +117,7 @@ namespace
113117
AppendUUID(BASE_EVENT_STREAM_LARGE_FILE_TEST_BUCKET_NAME);
114118
AppendUUID(BASE_EVENT_STREAM_ERRORS_IN_EVENT_TEST_BUCKET_NAME);
115119
AppendUUID(BASE_CROSS_REGION_BUCKET_NAME);
120+
AppendUUID(BASE_ENDPOINT_OVERRIDE_BUCKET_NAME);
116121
}
117122

118123
class RetryFiveTimesRetryStrategy: public Aws::Client::RetryStrategy
@@ -176,10 +181,13 @@ namespace
176181
retryClient = Aws::MakeShared<S3Client>(ALLOCATION_TAG,
177182
Aws::MakeShared<DefaultAWSCredentialsProviderChain>(ALLOCATION_TAG), config,
178183
AWSAuthV4Signer::PayloadSigningPolicy::Never /*signPayloads*/, true /*useVirtualAddressing*/);
184+
// Using client side monitoring for endpoint override testing.
185+
TestingMonitoringManager::InitTestingMonitoring();
179186
}
180187

181188
static void TearDownTestCase()
182189
{
190+
TestingMonitoringManager::CleanupTestingMonitoring();
183191
DeleteBucket(CalculateBucketName(BASE_CREATE_BUCKET_TEST_NAME.c_str()));
184192
DeleteBucket(CalculateBucketName(BASE_DNS_UNFRIENDLY_TEST_NAME.c_str()));
185193
DeleteBucket(CalculateBucketName(BASE_LOCATION_BUCKET_TEST_NAME.c_str()));
@@ -1701,6 +1709,115 @@ namespace
17011709
ASSERT_TRUE(deleteBucketOutcome.IsSuccess());
17021710
}
17031711

1712+
TEST_F(BucketAndObjectOperationTest, TestCustomEndpointOverride)
1713+
{
1714+
// Access Point ARN without dualstack
1715+
ASSERT_STREQ("myendpoint-123456789012.beta.example.com",
1716+
S3Endpoint::ForAccessPointArn(S3ARN("arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint"), "", false /* useDualStack */, "beta.example.com").c_str());
1717+
// Outpost Access Point ARN without dualstack
1718+
ASSERT_STREQ("myaccesspoint-123456789012.op-01234567890123456.beta.example.com",
1719+
S3Endpoint::ForOutpostsArn(S3ARN("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint"), "",
1720+
false /* useDualStack */, "beta.example.com").c_str());
1721+
1722+
Aws::String fullBucketName = CalculateBucketName(BASE_ENDPOINT_OVERRIDE_BUCKET_NAME.c_str());
1723+
Aws::StringStream ss;
1724+
1725+
// Traditional bucket name with virtual addressing
1726+
ClientConfiguration config;
1727+
config.region = Aws::Region::US_WEST_2;
1728+
config.endpointOverride = CUSTOM_ENDPOINT_OVERRIDE;
1729+
config.retryStrategy = Aws::MakeShared<Aws::Client::DefaultRetryStrategy>(ALLOCATION_TAG, 0 /* don't retry */, 25);
1730+
S3Client s3ClientWithVirtualAddressing(config, AWSAuthV4Signer::PayloadSigningPolicy::Never, true /*useVirtualAddressing*/);
1731+
1732+
ListObjectsRequest listObjectsRequest;
1733+
listObjectsRequest.SetBucket(fullBucketName);
1734+
auto listObjectsOutcome = s3ClientWithVirtualAddressing.ListObjects(listObjectsRequest);
1735+
ASSERT_FALSE(listObjectsOutcome.IsSuccess());
1736+
ASSERT_EQ(HttpResponseCode::REQUEST_NOT_MADE, listObjectsOutcome.GetError().GetResponseCode());
1737+
ss << "https://" << fullBucketName << "." << CUSTOM_ENDPOINT_OVERRIDE;
1738+
ASSERT_STREQ(ss.str().c_str(), TestingMonitoringMetrics::s_lastUriString.c_str());
1739+
ASSERT_STREQ("s3", TestingMonitoringMetrics::s_lastSigningServiceName.c_str());
1740+
1741+
// Access Point Arn with virtual addressing
1742+
listObjectsRequest.SetBucket("arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint");
1743+
listObjectsOutcome = s3ClientWithVirtualAddressing.ListObjects(listObjectsRequest);
1744+
ASSERT_FALSE(listObjectsOutcome.IsSuccess());
1745+
ASSERT_EQ(HttpResponseCode::REQUEST_NOT_MADE, listObjectsOutcome.GetError().GetResponseCode());
1746+
ss.str("");
1747+
ss << "https://myendpoint-123456789012." << CUSTOM_ENDPOINT_OVERRIDE;
1748+
ASSERT_STREQ(ss.str().c_str(), TestingMonitoringMetrics::s_lastUriString.c_str());
1749+
ASSERT_STREQ("s3", TestingMonitoringMetrics::s_lastSigningServiceName.c_str());
1750+
1751+
// Outposts Access Point Arn with virtual addressing
1752+
listObjectsRequest.SetBucket("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint");
1753+
listObjectsOutcome = s3ClientWithVirtualAddressing.ListObjects(listObjectsRequest);
1754+
ASSERT_FALSE(listObjectsOutcome.IsSuccess());
1755+
ASSERT_EQ(HttpResponseCode::REQUEST_NOT_MADE, listObjectsOutcome.GetError().GetResponseCode());
1756+
ss.str("");
1757+
ss << "https://myaccesspoint-123456789012.op-01234567890123456." << CUSTOM_ENDPOINT_OVERRIDE;
1758+
ASSERT_STREQ(ss.str().c_str(), TestingMonitoringMetrics::s_lastUriString.c_str());
1759+
ASSERT_STREQ("s3-outposts", TestingMonitoringMetrics::s_lastSigningServiceName.c_str());
1760+
1761+
// ListBuckets
1762+
auto listBucketsOutcome = s3ClientWithVirtualAddressing.ListBuckets();
1763+
ASSERT_FALSE(listBucketsOutcome.IsSuccess());
1764+
ASSERT_EQ(HttpResponseCode::REQUEST_NOT_MADE, listBucketsOutcome.GetError().GetResponseCode());
1765+
ss.str("");
1766+
ss << "https://" << CUSTOM_ENDPOINT_OVERRIDE;
1767+
ASSERT_STREQ(ss.str().c_str(), TestingMonitoringMetrics::s_lastUriString.c_str());
1768+
ASSERT_STREQ("s3", TestingMonitoringMetrics::s_lastSigningServiceName.c_str());
1769+
1770+
// Tradition bucket name with path addressing
1771+
S3Client s3ClientWithPathAddressing(config, AWSAuthV4Signer::PayloadSigningPolicy::Never, false /*useVirtualAddressing*/);
1772+
listObjectsRequest.SetBucket(fullBucketName);
1773+
listObjectsOutcome = s3ClientWithPathAddressing.ListObjects(listObjectsRequest);
1774+
ASSERT_FALSE(listObjectsOutcome.IsSuccess());
1775+
ASSERT_EQ(HttpResponseCode::REQUEST_NOT_MADE, listObjectsOutcome.GetError().GetResponseCode());
1776+
ss.str("");
1777+
ss << "https://" << CUSTOM_ENDPOINT_OVERRIDE << "/" << fullBucketName;
1778+
ASSERT_STREQ(ss.str().c_str(), TestingMonitoringMetrics::s_lastUriString.c_str());
1779+
ASSERT_STREQ("s3", TestingMonitoringMetrics::s_lastSigningServiceName.c_str());
1780+
1781+
// Use arn region, Access Point Arn with virtual addressing
1782+
Aws::String awsS3UseArnRegion = Aws::Environment::GetEnv("AWS_S3_USE_ARN_REGION");
1783+
Aws::Environment::SetEnv("AWS_S3_USE_ARN_REGION", "true", 1);
1784+
config.region = Aws::Region::EU_WEST_1;
1785+
S3Client s3ClientInEuWest1(config, AWSAuthV4Signer::PayloadSigningPolicy::Never, true /*useVirtualAddressing*/);
1786+
if (awsS3UseArnRegion.empty())
1787+
{
1788+
Aws::Environment::UnSetEnv("AWS_S3_USE_ARN_REGION");
1789+
}
1790+
else
1791+
{
1792+
Aws::Environment::SetEnv("AWS_S3_USE_ARN_REGION", awsS3UseArnRegion.c_str(), 1);
1793+
}
1794+
listObjectsRequest.SetBucket("arn:aws:s3:us-west-2:123456789012:accesspoint:myendpoint");
1795+
listObjectsOutcome = s3ClientInEuWest1.ListObjects(listObjectsRequest);
1796+
ASSERT_FALSE(listObjectsOutcome.IsSuccess());
1797+
ASSERT_EQ(HttpResponseCode::REQUEST_NOT_MADE, listObjectsOutcome.GetError().GetResponseCode());
1798+
ss.str("");
1799+
ss << "https://myendpoint-123456789012." << CUSTOM_ENDPOINT_OVERRIDE;
1800+
ASSERT_STREQ(ss.str().c_str(), TestingMonitoringMetrics::s_lastUriString.c_str());
1801+
ASSERT_STREQ("s3", TestingMonitoringMetrics::s_lastSigningServiceName.c_str());
1802+
ASSERT_STREQ("us-west-2", TestingMonitoringMetrics::s_lastSigningRegion.c_str());
1803+
1804+
// Failure case, dualstack endpoint is not compatible with custom endpoint override.
1805+
config.region = Aws::Region::US_WEST_2;
1806+
config.useDualStack = true;
1807+
S3Client s3ClientWithDualStack(config, AWSAuthV4Signer::PayloadSigningPolicy::Never, true /*useVirtualAddressing*/);
1808+
listObjectsRequest.SetBucket(fullBucketName);
1809+
listObjectsOutcome = s3ClientWithDualStack.ListObjects(listObjectsRequest);
1810+
ASSERT_FALSE(listObjectsOutcome.IsSuccess());
1811+
ASSERT_EQ(HttpResponseCode::REQUEST_NOT_MADE, listObjectsOutcome.GetError().GetResponseCode());
1812+
ASSERT_EQ(S3Errors::VALIDATION, listObjectsOutcome.GetError().GetErrorType());
1813+
1814+
listObjectsRequest.SetBucket("arn:aws:s3-outposts:us-west-2:123456789012:outpost:op-01234567890123456:accesspoint:myaccesspoint");
1815+
listObjectsOutcome = s3ClientWithDualStack.ListObjects(listObjectsRequest);
1816+
ASSERT_FALSE(listObjectsOutcome.IsSuccess());
1817+
ASSERT_EQ(HttpResponseCode::REQUEST_NOT_MADE, listObjectsOutcome.GetError().GetResponseCode());
1818+
ASSERT_EQ(S3Errors::VALIDATION, listObjectsOutcome.GetError().GetErrorType());
1819+
}
1820+
17041821
TEST_F(BucketAndObjectOperationTest, TestS3AccessPointARNValidation)
17051822
{
17061823
// The followings are examples for valid S3 ARN:
@@ -1837,5 +1954,4 @@ namespace
18371954
ASSERT_STREQ("access-point-name-123456789120.outpost-id.s3-outposts.cn-north-1.amazonaws.com.cn",
18381955
S3Endpoint::ForOutpostsArn(S3ARN("arn:aws-cn:s3-outposts:cn-north-1:123456789120:outpost:outpost-id:accesspoint:access-point-name"), "").c_str());
18391956
}
1840-
18411957
}

aws-cpp-sdk-s3/include/aws/s3/S3Endpoint.h

+5-2
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,18 @@ namespace S3Endpoint
2929
* @param arn The S3 Access Point ARN
3030
* @param regionNameOverride Override region name in ARN if it's not empty
3131
* @param useDualStack Using dual-stack endpoint if true
32+
* @param endpointOverride Override endpoint if it's not empty
3233
*/
33-
AWS_S3_API Aws::String ForAccessPointArn(const S3ARN& arn, const Aws::String& regionNameOverride = "", bool useDualStack = false);
34+
AWS_S3_API Aws::String ForAccessPointArn(const S3ARN& arn, const Aws::String& regionNameOverride = "", bool useDualStack = false, const Aws::String& endpointOverride = "");
3435

3536
/**
3637
* Compute endpoint based on Outposts ARN.
3738
* @param arn The S3 Outposts ARN
3839
* @param regionNameOverride Override region name in ARN if it's not empty
40+
* @param useDualStack Using dual-stack endpoint if true
41+
* @param endpointOverride Override endpoint if it's not empty
3942
*/
40-
AWS_S3_API Aws::String ForOutpostsArn(const S3ARN& arn, const Aws::String& regionNameOverride = "");
43+
AWS_S3_API Aws::String ForOutpostsArn(const S3ARN& arn, const Aws::String& regionNameOverride = "", bool useDualStack = false, const Aws::String& endpointOverride = "");
4144
} // namespace S3Endpoint
4245
} // namespace S3
4346
} // namespace Aws

aws-cpp-sdk-s3/source/S3Client.cpp

+13-8
Original file line numberDiff line numberDiff line change
@@ -4013,6 +4013,12 @@ Aws::String S3Client::GeneratePresignedUrlWithSSEC(const Aws::String& bucket, co
40134013

40144014
ComputeEndpointOutcome S3Client::ComputeEndpointString(const Aws::String& bucketOrArn) const
40154015
{
4016+
if (m_useDualStack && m_useCustomEndpoint)
4017+
{
4018+
return ComputeEndpointOutcome(Aws::Client::AWSError<S3Errors>(S3Errors::VALIDATION, "VALIDATION",
4019+
"Dual-stack endpoint is incompatible with a custom endpoint override.", false));
4020+
}
4021+
40164022
Aws::StringStream ss;
40174023
ss << m_scheme << "://";
40184024
Aws::String bucket = bucketOrArn;
@@ -4021,12 +4027,6 @@ ComputeEndpointOutcome S3Client::ComputeEndpointString(const Aws::String& bucket
40214027

40224028
if (arn)
40234029
{
4024-
if (m_useCustomEndpoint)
4025-
{
4026-
return ComputeEndpointOutcome(Aws::Client::AWSError<S3Errors>(S3Errors::VALIDATION, "VALIDATION",
4027-
"Custom endpoint is not compatible with Access Point ARN or Outposts ARN in Bucket field.", false));
4028-
}
4029-
40304030
if (!m_useVirtualAddressing)
40314031
{
40324032
return ComputeEndpointOutcome(Aws::Client::AWSError<S3Errors>(S3Errors::VALIDATION, "VALIDATION",
@@ -4041,7 +4041,7 @@ ComputeEndpointOutcome S3Client::ComputeEndpointString(const Aws::String& bucket
40414041
signerRegion = m_useArnRegion ? arn.GetRegion() : signerRegion;
40424042
if (arn.GetResourceType() == ARNResourceType::ACCESSPOINT)
40434043
{
4044-
ss << S3Endpoint::ForAccessPointArn(arn, m_useArnRegion ? "" : m_region, m_useDualStack);
4044+
ss << S3Endpoint::ForAccessPointArn(arn, m_useArnRegion ? "" : m_region, m_useDualStack, m_useCustomEndpoint ? m_baseUri : "");
40454045
return ComputeEndpointOutcome(ComputeEndpointResult(ss.str(), signerRegion, SERVICE_NAME));
40464046
}
40474047
else if (arn.GetResourceType() == ARNResourceType::OUTPOST)
@@ -4051,7 +4051,7 @@ ComputeEndpointOutcome S3Client::ComputeEndpointString(const Aws::String& bucket
40514051
return ComputeEndpointOutcome(Aws::Client::AWSError<S3Errors>(S3Errors::VALIDATION, "VALIDATION",
40524052
"Outposts Access Points do not support dualstack right now.", false));
40534053
}
4054-
ss << S3Endpoint::ForOutpostsArn(arn, m_useArnRegion ? "" : m_region);
4054+
ss << S3Endpoint::ForOutpostsArn(arn, m_useArnRegion ? "" : m_region, m_useDualStack, m_useCustomEndpoint ? m_baseUri : "");
40554055
return ComputeEndpointOutcome(ComputeEndpointResult(ss.str(), signerRegion, "s3-outposts"));
40564056
}
40574057
}
@@ -4074,6 +4074,11 @@ ComputeEndpointOutcome S3Client::ComputeEndpointString(const Aws::String& bucket
40744074

40754075
ComputeEndpointOutcome S3Client::ComputeEndpointString() const
40764076
{
4077+
if (m_useDualStack && m_useCustomEndpoint)
4078+
{
4079+
return ComputeEndpointOutcome(Aws::Client::AWSError<S3Errors>(S3Errors::VALIDATION, "VALIDATION",
4080+
"Dual-stack endpoint is incompatible with a custom endpoint override.", false));
4081+
}
40774082
Aws::StringStream ss;
40784083
ss << m_scheme << "://" << m_baseUri;
40794084
return ComputeEndpointOutcome(ComputeEndpointResult(ss.str(), Aws::Region::ComputeSignerRegion(m_region), SERVICE_NAME));

aws-cpp-sdk-s3/source/S3Endpoint.cpp

+20-4
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,19 @@ namespace S3Endpoint
2727
static const int US_EAST_1_HASH = Aws::Utils::HashingUtils::HashString("us-east-1");
2828
static const int AWS_GLOBAL_HASH = Aws::Utils::HashingUtils::HashString("aws-global");
2929

30-
Aws::String ForAccessPointArn(const S3ARN& arn, const Aws::String& regionNameOverride, bool useDualStack)
30+
Aws::String ForAccessPointArn(const S3ARN& arn, const Aws::String& regionNameOverride, bool useDualStack, const Aws::String& endpointOverride)
3131
{
32+
Aws::StringStream ss;
33+
34+
if (!endpointOverride.empty())
35+
{
36+
ss << arn.GetResourceId() << "-" << arn.GetAccountId() << "." << endpointOverride;
37+
return ss.str();
38+
}
39+
3240
const Aws::String& region = regionNameOverride.empty() ? arn.GetRegion() : regionNameOverride;
3341
auto hash = Aws::Utils::HashingUtils::HashString(region.c_str());
3442

35-
Aws::StringStream ss;
3643
ss << arn.GetResourceId() << "-" << arn.GetAccountId() << ".s3-accesspoint.";
3744
if (useDualStack)
3845
{
@@ -48,12 +55,21 @@ namespace S3Endpoint
4855
return ss.str();
4956
}
5057

51-
Aws::String ForOutpostsArn(const S3ARN& arn, const Aws::String& regionNameOverride)
58+
Aws::String ForOutpostsArn(const S3ARN& arn, const Aws::String& regionNameOverride, bool useDualStack, const Aws::String& endpointOverride)
5259
{
60+
AWS_UNREFERENCED_PARAM(useDualStack);
61+
assert(!useDualStack);
62+
Aws::StringStream ss;
63+
64+
if (!endpointOverride.empty())
65+
{
66+
ss << arn.GetSubResourceId() << "-" << arn.GetAccountId() << "." << arn.GetResourceId() << "." << endpointOverride;
67+
return ss.str();
68+
}
69+
5370
const Aws::String& region = regionNameOverride.empty() ? arn.GetRegion() : regionNameOverride;
5471
auto hash = Aws::Utils::HashingUtils::HashString(region.c_str());
5572

56-
Aws::StringStream ss;
5773
ss << arn.GetSubResourceId() << "-" << arn.GetAccountId() << "." << arn.GetResourceId() << ".s3-outposts." << region << "." << "amazonaws.com";
5874

5975
if (hash == CN_NORTH_1_HASH || hash == CN_NORTHWEST_1_HASH)

0 commit comments

Comments
 (0)