Skip to content

Commit 8a8a0c2

Browse files
authored
Merge pull request #14135 from lovesegfault/curl-based-s3-pieces
feat(libstore): add AWS CRT-based credential infrastructure
2 parents eb67b0d + a4e792c commit 8a8a0c2

File tree

4 files changed

+253
-0
lines changed

4 files changed

+253
-0
lines changed

src/libstore/aws-creds.cc

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
#include "nix/store/aws-creds.hh"
2+
3+
#if NIX_WITH_S3_SUPPORT
4+
5+
# include <aws/crt/Types.h>
6+
# include "nix/store/s3-url.hh"
7+
# include "nix/util/finally.hh"
8+
# include "nix/util/logging.hh"
9+
# include "nix/util/url.hh"
10+
# include "nix/util/util.hh"
11+
12+
# include <aws/crt/Api.h>
13+
# include <aws/crt/auth/Credentials.h>
14+
# include <aws/crt/io/Bootstrap.h>
15+
16+
# include <boost/unordered/concurrent_flat_map.hpp>
17+
18+
# include <chrono>
19+
# include <future>
20+
# include <memory>
21+
# include <unistd.h>
22+
23+
namespace nix {
24+
25+
namespace {
26+
27+
static void initAwsCrt()
28+
{
29+
struct CrtWrapper
30+
{
31+
Aws::Crt::ApiHandle apiHandle;
32+
33+
CrtWrapper()
34+
{
35+
apiHandle.InitializeLogging(Aws::Crt::LogLevel::Warn, static_cast<FILE *>(nullptr));
36+
}
37+
38+
~CrtWrapper()
39+
{
40+
try {
41+
// CRITICAL: Clear credential provider cache BEFORE AWS CRT shuts down
42+
// This ensures all providers (which hold references to ClientBootstrap)
43+
// are destroyed while AWS CRT is still valid
44+
clearAwsCredentialsCache();
45+
// Now it's safe for ApiHandle destructor to run
46+
} catch (...) {
47+
ignoreExceptionInDestructor();
48+
}
49+
}
50+
};
51+
52+
static CrtWrapper crt;
53+
}
54+
55+
static AwsCredentials getCredentialsFromProvider(std::shared_ptr<Aws::Crt::Auth::ICredentialsProvider> provider)
56+
{
57+
if (!provider || !provider->IsValid()) {
58+
throw AwsAuthError("AWS credential provider is invalid");
59+
}
60+
61+
auto prom = std::make_shared<std::promise<AwsCredentials>>();
62+
auto fut = prom->get_future();
63+
64+
provider->GetCredentials([prom](std::shared_ptr<Aws::Crt::Auth::Credentials> credentials, int errorCode) {
65+
if (errorCode != 0 || !credentials) {
66+
prom->set_exception(
67+
std::make_exception_ptr(AwsAuthError("Failed to resolve AWS credentials: error code %d", errorCode)));
68+
} else {
69+
auto accessKeyId = Aws::Crt::ByteCursorToStringView(credentials->GetAccessKeyId());
70+
auto secretAccessKey = Aws::Crt::ByteCursorToStringView(credentials->GetSecretAccessKey());
71+
auto sessionToken = Aws::Crt::ByteCursorToStringView(credentials->GetSessionToken());
72+
73+
std::optional<std::string> sessionTokenStr;
74+
if (!sessionToken.empty()) {
75+
sessionTokenStr = std::string(sessionToken.data(), sessionToken.size());
76+
}
77+
78+
prom->set_value(AwsCredentials(
79+
std::string(accessKeyId.data(), accessKeyId.size()),
80+
std::string(secretAccessKey.data(), secretAccessKey.size()),
81+
sessionTokenStr));
82+
}
83+
});
84+
85+
// AWS CRT GetCredentials is asynchronous and only guarantees the callback will be
86+
// invoked if the initial call returns success. There's no documented timeout mechanism,
87+
// so we add a timeout to prevent indefinite hanging if the callback is never called.
88+
auto timeout = std::chrono::seconds(30);
89+
if (fut.wait_for(timeout) == std::future_status::timeout) {
90+
throw AwsAuthError(
91+
"Timeout waiting for AWS credentials (%d seconds)",
92+
std::chrono::duration_cast<std::chrono::seconds>(timeout).count());
93+
}
94+
95+
return fut.get(); // This will throw if set_exception was called
96+
}
97+
98+
// Global credential provider cache using boost's concurrent map
99+
// Key: profile name (empty string for default profile)
100+
using CredentialProviderCache =
101+
boost::concurrent_flat_map<std::string, std::shared_ptr<Aws::Crt::Auth::ICredentialsProvider>>;
102+
103+
static CredentialProviderCache credentialProviderCache;
104+
105+
} // anonymous namespace
106+
107+
AwsCredentials getAwsCredentials(const std::string & profile)
108+
{
109+
// Get or create credential provider with caching
110+
std::shared_ptr<Aws::Crt::Auth::ICredentialsProvider> provider;
111+
112+
// Try to find existing provider
113+
credentialProviderCache.visit(profile, [&](const auto & pair) { provider = pair.second; });
114+
115+
if (!provider) {
116+
// Create new provider if not found
117+
debug(
118+
"[pid=%d] creating new AWS credential provider for profile '%s'",
119+
getpid(),
120+
profile.empty() ? "(default)" : profile.c_str());
121+
122+
try {
123+
initAwsCrt();
124+
125+
if (profile.empty()) {
126+
Aws::Crt::Auth::CredentialsProviderChainDefaultConfig config;
127+
config.Bootstrap = Aws::Crt::ApiHandle::GetOrCreateStaticDefaultClientBootstrap();
128+
provider = Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderChainDefault(config);
129+
} else {
130+
Aws::Crt::Auth::CredentialsProviderProfileConfig config;
131+
config.Bootstrap = Aws::Crt::ApiHandle::GetOrCreateStaticDefaultClientBootstrap();
132+
// This is safe because the underlying C library will copy this string
133+
// c.f. https://github.com/awslabs/aws-c-auth/blob/main/source/credentials_provider_profile.c#L220
134+
config.ProfileNameOverride = Aws::Crt::ByteCursorFromCString(profile.c_str());
135+
provider = Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderProfile(config);
136+
}
137+
} catch (Error & e) {
138+
e.addTrace(
139+
{},
140+
"while creating AWS credentials provider for %s",
141+
profile.empty() ? "default profile" : fmt("profile '%s'", profile));
142+
throw;
143+
}
144+
145+
if (!provider) {
146+
throw AwsAuthError(
147+
"Failed to create AWS credentials provider for %s",
148+
profile.empty() ? "default profile" : fmt("profile '%s'", profile));
149+
}
150+
151+
// Insert into cache (try_emplace is thread-safe and won't overwrite if another thread added it)
152+
credentialProviderCache.try_emplace(profile, provider);
153+
}
154+
155+
return getCredentialsFromProvider(provider);
156+
}
157+
158+
void invalidateAwsCredentials(const std::string & profile)
159+
{
160+
credentialProviderCache.erase(profile);
161+
}
162+
163+
void clearAwsCredentialsCache()
164+
{
165+
credentialProviderCache.clear();
166+
}
167+
168+
AwsCredentials preResolveAwsCredentials(const ParsedS3URL & s3Url)
169+
{
170+
std::string profile = s3Url.profile.value_or("");
171+
172+
// Get credentials (automatically cached)
173+
return getAwsCredentials(profile);
174+
}
175+
176+
} // namespace nix
177+
178+
#endif
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#pragma once
2+
///@file
3+
#include "nix/store/config.hh"
4+
5+
#if NIX_WITH_S3_SUPPORT
6+
7+
# include "nix/store/s3-url.hh"
8+
# include "nix/util/error.hh"
9+
10+
# include <memory>
11+
# include <optional>
12+
# include <string>
13+
14+
namespace nix {
15+
16+
/**
17+
* AWS credentials obtained from credential providers
18+
*/
19+
struct AwsCredentials
20+
{
21+
std::string accessKeyId;
22+
std::string secretAccessKey;
23+
std::optional<std::string> sessionToken;
24+
25+
AwsCredentials(
26+
const std::string & accessKeyId,
27+
const std::string & secretAccessKey,
28+
const std::optional<std::string> & sessionToken = std::nullopt)
29+
: accessKeyId(accessKeyId)
30+
, secretAccessKey(secretAccessKey)
31+
, sessionToken(sessionToken)
32+
{
33+
}
34+
};
35+
36+
/**
37+
* Exception thrown when AWS authentication fails
38+
*/
39+
MakeError(AwsAuthError, Error);
40+
41+
/**
42+
* Get AWS credentials for the given profile.
43+
* This function automatically caches credential providers to avoid
44+
* creating multiple providers for the same profile.
45+
*
46+
* @param profile The AWS profile name (empty string for default profile)
47+
* @return AWS credentials
48+
* @throws AwsAuthError if credentials cannot be resolved
49+
*/
50+
AwsCredentials getAwsCredentials(const std::string & profile = "");
51+
52+
/**
53+
* Invalidate cached credentials for a profile (e.g., on authentication failure).
54+
* The next request for this profile will create a new provider.
55+
*
56+
* @param profile The AWS profile name to invalidate
57+
*/
58+
void invalidateAwsCredentials(const std::string & profile);
59+
60+
/**
61+
* Clear all cached credential providers.
62+
* Typically called during application cleanup.
63+
*/
64+
void clearAwsCredentialsCache();
65+
66+
/**
67+
* Pre-resolve AWS credentials for S3 URLs.
68+
* Used to cache credentials in parent process before forking.
69+
*/
70+
AwsCredentials preResolveAwsCredentials(const ParsedS3URL & s3Url);
71+
72+
} // namespace nix
73+
#endif

src/libstore/include/nix/store/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ config_pub_h = configure_file(
1010
)
1111

1212
headers = [ config_pub_h ] + files(
13+
'aws-creds.hh',
1314
'binary-cache-store.hh',
1415
'build-result.hh',
1516
'build/derivation-builder.hh',

src/libstore/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ subdir('nix-meson-build-support/common')
268268
subdir('nix-meson-build-support/asan-options')
269269

270270
sources = files(
271+
'aws-creds.cc',
271272
'binary-cache-store.cc',
272273
'build-result.cc',
273274
'build/derivation-building-goal.cc',

0 commit comments

Comments
 (0)