-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(libstore): add AWS CRT-based credential infrastructure #14135
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| #include "nix/store/aws-creds.hh" | ||
|
|
||
| #if NIX_WITH_S3_SUPPORT | ||
|
|
||
| # include <aws/crt/Types.h> | ||
| # include "nix/store/s3-url.hh" | ||
| # include "nix/util/finally.hh" | ||
| # include "nix/util/logging.hh" | ||
| # include "nix/util/url.hh" | ||
| # include "nix/util/util.hh" | ||
|
|
||
| # include <aws/crt/Api.h> | ||
| # include <aws/crt/auth/Credentials.h> | ||
| # include <aws/crt/io/Bootstrap.h> | ||
|
|
||
| # include <boost/unordered/concurrent_flat_map.hpp> | ||
|
|
||
| # include <chrono> | ||
| # include <future> | ||
| # include <memory> | ||
| # include <unistd.h> | ||
|
|
||
| namespace nix { | ||
|
|
||
| namespace { | ||
|
|
||
| static void initAwsCrt() | ||
| { | ||
| struct CrtWrapper | ||
| { | ||
| Aws::Crt::ApiHandle apiHandle; | ||
|
|
||
| CrtWrapper() | ||
| { | ||
| apiHandle.InitializeLogging(Aws::Crt::LogLevel::Warn, static_cast<FILE *>(nullptr)); | ||
| } | ||
|
|
||
| ~CrtWrapper() | ||
| { | ||
| try { | ||
| // CRITICAL: Clear credential provider cache BEFORE AWS CRT shuts down | ||
| // This ensures all providers (which hold references to ClientBootstrap) | ||
| // are destroyed while AWS CRT is still valid | ||
| clearAwsCredentialsCache(); | ||
| // Now it's safe for ApiHandle destructor to run | ||
| } catch (...) { | ||
| ignoreExceptionInDestructor(); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| static CrtWrapper crt; | ||
| } | ||
|
|
||
| static AwsCredentials getCredentialsFromProvider(std::shared_ptr<Aws::Crt::Auth::ICredentialsProvider> provider) | ||
| { | ||
| if (!provider || !provider->IsValid()) { | ||
| throw AwsAuthError("AWS credential provider is invalid"); | ||
| } | ||
|
|
||
| auto prom = std::make_shared<std::promise<AwsCredentials>>(); | ||
| auto fut = prom->get_future(); | ||
|
|
||
| provider->GetCredentials([prom](std::shared_ptr<Aws::Crt::Auth::Credentials> credentials, int errorCode) { | ||
| if (errorCode != 0 || !credentials) { | ||
| prom->set_exception( | ||
| std::make_exception_ptr(AwsAuthError("Failed to resolve AWS credentials: error code %d", errorCode))); | ||
| } else { | ||
| auto accessKeyId = Aws::Crt::ByteCursorToStringView(credentials->GetAccessKeyId()); | ||
| auto secretAccessKey = Aws::Crt::ByteCursorToStringView(credentials->GetSecretAccessKey()); | ||
| auto sessionToken = Aws::Crt::ByteCursorToStringView(credentials->GetSessionToken()); | ||
|
|
||
| std::optional<std::string> sessionTokenStr; | ||
| if (!sessionToken.empty()) { | ||
| sessionTokenStr = std::string(sessionToken.data(), sessionToken.size()); | ||
| } | ||
|
|
||
| prom->set_value(AwsCredentials( | ||
| std::string(accessKeyId.data(), accessKeyId.size()), | ||
| std::string(secretAccessKey.data(), secretAccessKey.size()), | ||
| sessionTokenStr)); | ||
| } | ||
| }); | ||
|
|
||
| // AWS CRT GetCredentials is asynchronous and only guarantees the callback will be | ||
| // invoked if the initial call returns success. There's no documented timeout mechanism, | ||
| // so we add a timeout to prevent indefinite hanging if the callback is never called. | ||
| auto timeout = std::chrono::seconds(30); | ||
| if (fut.wait_for(timeout) == std::future_status::timeout) { | ||
| throw AwsAuthError( | ||
| "Timeout waiting for AWS credentials (%d seconds)", | ||
| std::chrono::duration_cast<std::chrono::seconds>(timeout).count()); | ||
| } | ||
|
|
||
| return fut.get(); // This will throw if set_exception was called | ||
| } | ||
|
|
||
| // Global credential provider cache using boost's concurrent map | ||
| // Key: profile name (empty string for default profile) | ||
| using CredentialProviderCache = | ||
| boost::concurrent_flat_map<std::string, std::shared_ptr<Aws::Crt::Auth::ICredentialsProvider>>; | ||
|
|
||
| static CredentialProviderCache credentialProviderCache; | ||
lovesegfault marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| } // anonymous namespace | ||
|
|
||
| AwsCredentials getAwsCredentials(const std::string & profile) | ||
| { | ||
| // Get or create credential provider with caching | ||
| std::shared_ptr<Aws::Crt::Auth::ICredentialsProvider> provider; | ||
|
|
||
| // Try to find existing provider | ||
| credentialProviderCache.visit(profile, [&](const auto & pair) { provider = pair.second; }); | ||
|
|
||
| if (!provider) { | ||
| // Create new provider if not found | ||
| debug( | ||
| "[pid=%d] creating new AWS credential provider for profile '%s'", | ||
| getpid(), | ||
| profile.empty() ? "(default)" : profile.c_str()); | ||
|
|
||
| try { | ||
| initAwsCrt(); | ||
|
|
||
| if (profile.empty()) { | ||
| Aws::Crt::Auth::CredentialsProviderChainDefaultConfig config; | ||
| config.Bootstrap = Aws::Crt::ApiHandle::GetOrCreateStaticDefaultClientBootstrap(); | ||
| provider = Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderChainDefault(config); | ||
| } else { | ||
| Aws::Crt::Auth::CredentialsProviderProfileConfig config; | ||
| config.Bootstrap = Aws::Crt::ApiHandle::GetOrCreateStaticDefaultClientBootstrap(); | ||
| // This is safe because the underlying C library will copy this string | ||
| // c.f. https://github.com/awslabs/aws-c-auth/blob/main/source/credentials_provider_profile.c#L220 | ||
| config.ProfileNameOverride = Aws::Crt::ByteCursorFromCString(profile.c_str()); | ||
| provider = Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderProfile(config); | ||
| } | ||
| } catch (Error & e) { | ||
| e.addTrace( | ||
| {}, | ||
| "while creating AWS credentials provider for %s", | ||
| profile.empty() ? "default profile" : fmt("profile '%s'", profile)); | ||
| throw; | ||
| } | ||
|
|
||
| if (!provider) { | ||
| throw AwsAuthError( | ||
| "Failed to create AWS credentials provider for %s", | ||
| profile.empty() ? "default profile" : fmt("profile '%s'", profile)); | ||
| } | ||
|
|
||
| // Insert into cache (try_emplace is thread-safe and won't overwrite if another thread added it) | ||
| credentialProviderCache.try_emplace(profile, provider); | ||
| } | ||
|
|
||
| return getCredentialsFromProvider(provider); | ||
| } | ||
|
|
||
| void invalidateAwsCredentials(const std::string & profile) | ||
| { | ||
| credentialProviderCache.erase(profile); | ||
| } | ||
|
|
||
| void clearAwsCredentialsCache() | ||
| { | ||
| credentialProviderCache.clear(); | ||
| } | ||
|
|
||
| AwsCredentials preResolveAwsCredentials(const ParsedS3URL & s3Url) | ||
| { | ||
| std::string profile = s3Url.profile.value_or(""); | ||
|
|
||
| // Get credentials (automatically cached) | ||
| return getAwsCredentials(profile); | ||
| } | ||
|
|
||
| } // namespace nix | ||
|
|
||
| #endif | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| #pragma once | ||
| ///@file | ||
| #include "nix/store/config.hh" | ||
|
|
||
| #if NIX_WITH_S3_SUPPORT | ||
|
|
||
| # include "nix/store/s3-url.hh" | ||
| # include "nix/util/error.hh" | ||
|
|
||
| # include <memory> | ||
| # include <optional> | ||
| # include <string> | ||
|
|
||
| namespace nix { | ||
|
|
||
| /** | ||
| * AWS credentials obtained from credential providers | ||
| */ | ||
| struct AwsCredentials | ||
| { | ||
| std::string accessKeyId; | ||
| std::string secretAccessKey; | ||
| std::optional<std::string> sessionToken; | ||
|
|
||
| AwsCredentials( | ||
| const std::string & accessKeyId, | ||
| const std::string & secretAccessKey, | ||
| const std::optional<std::string> & sessionToken = std::nullopt) | ||
| : accessKeyId(accessKeyId) | ||
| , secretAccessKey(secretAccessKey) | ||
| , sessionToken(sessionToken) | ||
| { | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Exception thrown when AWS authentication fails | ||
| */ | ||
| MakeError(AwsAuthError, Error); | ||
|
|
||
| /** | ||
| * Get AWS credentials for the given profile. | ||
| * This function automatically caches credential providers to avoid | ||
| * creating multiple providers for the same profile. | ||
| * | ||
| * @param profile The AWS profile name (empty string for default profile) | ||
| * @return AWS credentials | ||
| * @throws AwsAuthError if credentials cannot be resolved | ||
| */ | ||
| AwsCredentials getAwsCredentials(const std::string & profile = ""); | ||
|
|
||
| /** | ||
| * Invalidate cached credentials for a profile (e.g., on authentication failure). | ||
| * The next request for this profile will create a new provider. | ||
| * | ||
| * @param profile The AWS profile name to invalidate | ||
| */ | ||
| void invalidateAwsCredentials(const std::string & profile); | ||
|
|
||
| /** | ||
| * Clear all cached credential providers. | ||
| * Typically called during application cleanup. | ||
| */ | ||
| void clearAwsCredentialsCache(); | ||
|
|
||
| /** | ||
| * Pre-resolve AWS credentials for S3 URLs. | ||
| * Used to cache credentials in parent process before forking. | ||
| */ | ||
| AwsCredentials preResolveAwsCredentials(const ParsedS3URL & s3Url); | ||
|
|
||
| } // namespace nix | ||
| #endif |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.