From c63600d346210c7319c59bd99abfd3a7ee8a6950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Mon, 24 Nov 2025 21:06:59 +0100 Subject: [PATCH 1/7] libstore: add AWS SSO support for S3 authentication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This enables seamless AWS SSO authentication for S3 binary caches without requiring users to manually export credentials. This adds SSO support by calling aws_credentials_provider_new_sso() from the C library directly. It builds a custom credential chain: Env → SSO → Profile → IMDS The SSO provider requires a TLS context for HTTPS connections to SSO endpoints, which is created once and shared across all providers. --- src/libstore/aws-creds.cc | 101 +++++++++++++++++++++++++++++++++++--- src/libstore/meson.build | 2 + 2 files changed, 96 insertions(+), 7 deletions(-) diff --git a/src/libstore/aws-creds.cc b/src/libstore/aws-creds.cc index dfdd81abbc4..322fada9950 100644 --- a/src/libstore/aws-creds.cc +++ b/src/libstore/aws-creds.cc @@ -13,6 +13,9 @@ # include # include +// C library headers for SSO provider support +# include + # include # include @@ -30,6 +33,48 @@ AwsAuthError::AwsAuthError(int errorCode) namespace { +/** + * Helper function to wrap a C credentials provider in the C++ interface. + * This replicates the static s_CreateWrappedProvider from aws-crt-cpp. + */ +static std::shared_ptr createWrappedProvider( + aws_credentials_provider * rawProvider, Aws::Crt::Allocator * allocator = Aws::Crt::ApiAllocator()) +{ + if (rawProvider == nullptr) { + return nullptr; + } + + auto provider = Aws::Crt::MakeShared(allocator, rawProvider, allocator); + return std::static_pointer_cast(provider); +} + +/** + * Create an SSO credentials provider using the C library directly. + * The C++ wrapper doesn't expose SSO, so we call the C library and wrap the result. + * Returns nullptr if SSO provider creation fails (e.g., profile doesn't have SSO config). + */ +static std::shared_ptr createSSOProvider( + const std::string & profileName, + Aws::Crt::Io::ClientBootstrap * bootstrap, + Aws::Crt::Io::TlsContext * tlsContext, + Aws::Crt::Allocator * allocator = Aws::Crt::ApiAllocator()) +{ + aws_credentials_provider_sso_options options; + AWS_ZERO_STRUCT(options); + + options.bootstrap = bootstrap->GetUnderlyingHandle(); + options.tls_ctx = tlsContext ? tlsContext->GetUnderlyingHandle() : nullptr; + options.profile_name_override = aws_byte_cursor_from_c_str(profileName.c_str()); + + // Create the SSO provider - will return nullptr if SSO isn't configured for this profile + auto * rawProvider = aws_credentials_provider_new_sso(allocator, &options); + if (!rawProvider) { + return nullptr; + } + + return createWrappedProvider(rawProvider, allocator); +} + static AwsCredentials getCredentialsFromProvider(std::shared_ptr provider) { if (!provider || !provider->IsValid()) { @@ -91,6 +136,12 @@ class AwsCredentialProviderImpl : public AwsCredentialProvider logLevel = Aws::Crt::LogLevel::Warn; } apiHandle.InitializeLogging(logLevel, stderr); + + // Create a shared TLS context for SSO (required for HTTPS connections) + auto allocator = Aws::Crt::ApiAllocator(); + auto tlsCtxOptions = Aws::Crt::Io::TlsContextOptions::InitDefaultClient(allocator); + tlsContext = + std::make_shared(tlsCtxOptions, Aws::Crt::Io::TlsMode::CLIENT, allocator); } AwsCredentials getCredentialsRaw(const std::string & profile); @@ -111,6 +162,7 @@ class AwsCredentialProviderImpl : public AwsCredentialProvider private: Aws::Crt::ApiHandle apiHandle; + std::shared_ptr tlsContext; boost::concurrent_flat_map> credentialProviderCache; }; @@ -123,18 +175,53 @@ AwsCredentialProviderImpl::createProviderForProfile(const std::string & profile) getpid(), profile.empty() ? "(default)" : profile.c_str()); + auto bootstrap = Aws::Crt::ApiHandle::GetOrCreateStaticDefaultClientBootstrap(); + + // If no profile specified, use the default chain if (profile.empty()) { Aws::Crt::Auth::CredentialsProviderChainDefaultConfig config; - config.Bootstrap = Aws::Crt::ApiHandle::GetOrCreateStaticDefaultClientBootstrap(); + config.Bootstrap = bootstrap; return Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderChainDefault(config); } - 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()); - return Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderProfile(config); + // For named profiles, build a custom credential chain: Environment → SSO → Profile → IMDS + // The SSO provider will gracefully fail if SSO isn't configured for this profile, + // and the chain will move on to the next provider. + Aws::Crt::Auth::CredentialsProviderChainConfig chainConfig; + auto allocator = Aws::Crt::ApiAllocator(); + + // 1. Environment variables (highest priority) + auto envProvider = Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderEnvironment(allocator); + if (envProvider) { + chainConfig.Providers.push_back(envProvider); + } + + // 2. SSO provider (try it, will fail gracefully if not configured) + auto ssoProvider = createSSOProvider(profile, bootstrap, tlsContext.get(), allocator); + if (ssoProvider) { + debug("[pid=%d] added SSO provider to credential chain for profile '%s'", getpid(), profile.c_str()); + chainConfig.Providers.push_back(ssoProvider); + } + + // 3. Profile provider (for static credentials) + Aws::Crt::Auth::CredentialsProviderProfileConfig profileConfig; + profileConfig.Bootstrap = bootstrap; + profileConfig.ProfileNameOverride = Aws::Crt::ByteCursorFromCString(profile.c_str()); + auto profileProvider = + Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderProfile(profileConfig, allocator); + if (profileProvider) { + chainConfig.Providers.push_back(profileProvider); + } + + // 4. IMDS provider (for EC2 instances, lowest priority) + Aws::Crt::Auth::CredentialsProviderImdsConfig imdsConfig; + imdsConfig.Bootstrap = bootstrap; + auto imdsProvider = Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderImds(imdsConfig, allocator); + if (imdsProvider) { + chainConfig.Providers.push_back(imdsProvider); + } + + return Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderChain(chainConfig, allocator); } AwsCredentials AwsCredentialProviderImpl::getCredentialsRaw(const std::string & profile) diff --git a/src/libstore/meson.build b/src/libstore/meson.build index d8927c3a6c3..088d8bcbb59 100644 --- a/src/libstore/meson.build +++ b/src/libstore/meson.build @@ -160,6 +160,8 @@ if s3_aws_auth.enabled() deps_other += aws_crt_cpp aws_c_common = cxx.find_library('aws-c-common', required : true) deps_other += aws_c_common + aws_c_auth = cxx.find_library('aws-c-auth', required : true) + deps_other += aws_c_auth endif configdata_pub.set('NIX_WITH_AWS_AUTH', s3_aws_auth.enabled().to_int()) From f8034e61b191e76c5a05c6ca25d4aea3f234e26f Mon Sep 17 00:00:00 2001 From: Bernardo Meurer Date: Tue, 25 Nov 2025 13:59:25 -0500 Subject: [PATCH 2/7] refactor(libstore/aws-creds): improve error handling and logging Add validation for TLS context and client bootstrap initialization, with appropriate error messages when these fail. The TLS context failure is now a warning that gracefully disables SSO, while bootstrap failure throws since it's required for all providers. --- src/libstore/aws-creds.cc | 56 ++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/src/libstore/aws-creds.cc b/src/libstore/aws-creds.cc index 322fada9950..d84d57c8b6d 100644 --- a/src/libstore/aws-creds.cc +++ b/src/libstore/aws-creds.cc @@ -142,6 +142,10 @@ class AwsCredentialProviderImpl : public AwsCredentialProvider auto tlsCtxOptions = Aws::Crt::Io::TlsContextOptions::InitDefaultClient(allocator); tlsContext = std::make_shared(tlsCtxOptions, Aws::Crt::Io::TlsMode::CLIENT, allocator); + if (!tlsContext || !*tlsContext) { + warn("failed to create TLS context for AWS SSO; SSO authentication will be unavailable"); + tlsContext = nullptr; + } } AwsCredentials getCredentialsRaw(const std::string & profile); @@ -176,6 +180,9 @@ AwsCredentialProviderImpl::createProviderForProfile(const std::string & profile) profile.empty() ? "(default)" : profile.c_str()); auto bootstrap = Aws::Crt::ApiHandle::GetOrCreateStaticDefaultClientBootstrap(); + if (!bootstrap) { + throw AwsAuthError("failed to create AWS client bootstrap"); + } // If no profile specified, use the default chain if (profile.empty()) { @@ -190,36 +197,41 @@ AwsCredentialProviderImpl::createProviderForProfile(const std::string & profile) Aws::Crt::Auth::CredentialsProviderChainConfig chainConfig; auto allocator = Aws::Crt::ApiAllocator(); + auto addProviderToChain = [&](std::string_view name, auto createProvider) { + if (auto provider = createProvider()) { + chainConfig.Providers.push_back(provider); + debug("Added AWS %s Credential Provider to chain for profile '%s'", name, profile); + } else { + debug("Skipped AWS %s Credential Provider for profile '%s'", name, profile); + } + }; + // 1. Environment variables (highest priority) - auto envProvider = Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderEnvironment(allocator); - if (envProvider) { - chainConfig.Providers.push_back(envProvider); - } + addProviderToChain("Environment", [&]() { + return Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderEnvironment(allocator); + }); // 2. SSO provider (try it, will fail gracefully if not configured) - auto ssoProvider = createSSOProvider(profile, bootstrap, tlsContext.get(), allocator); - if (ssoProvider) { - debug("[pid=%d] added SSO provider to credential chain for profile '%s'", getpid(), profile.c_str()); - chainConfig.Providers.push_back(ssoProvider); + if (tlsContext) { + addProviderToChain("SSO", [&]() { return createSSOProvider(profile, bootstrap, tlsContext.get(), allocator); }); + } else { + debug("Skipped AWS SSO Credential Provider for profile '%s': TLS context unavailable", profile); } // 3. Profile provider (for static credentials) - Aws::Crt::Auth::CredentialsProviderProfileConfig profileConfig; - profileConfig.Bootstrap = bootstrap; - profileConfig.ProfileNameOverride = Aws::Crt::ByteCursorFromCString(profile.c_str()); - auto profileProvider = - Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderProfile(profileConfig, allocator); - if (profileProvider) { - chainConfig.Providers.push_back(profileProvider); - } + addProviderToChain("Profile", [&]() { + Aws::Crt::Auth::CredentialsProviderProfileConfig profileConfig; + profileConfig.Bootstrap = bootstrap; + profileConfig.ProfileNameOverride = Aws::Crt::ByteCursorFromCString(profile.c_str()); + return Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderProfile(profileConfig, allocator); + }); // 4. IMDS provider (for EC2 instances, lowest priority) - Aws::Crt::Auth::CredentialsProviderImdsConfig imdsConfig; - imdsConfig.Bootstrap = bootstrap; - auto imdsProvider = Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderImds(imdsConfig, allocator); - if (imdsProvider) { - chainConfig.Providers.push_back(imdsProvider); - } + addProviderToChain("IMDS", [&]() { + Aws::Crt::Auth::CredentialsProviderImdsConfig imdsConfig; + imdsConfig.Bootstrap = bootstrap; + return Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderImds(imdsConfig, allocator); + }); return Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderChain(chainConfig, allocator); } From 078ef24735d66214f818db9467c0f220bbf8396a Mon Sep 17 00:00:00 2001 From: Bernardo Meurer Date: Tue, 25 Nov 2025 17:34:16 -0500 Subject: [PATCH 3/7] fix(libstore/aws-creds): add STS support for default profile The default (empty) profile case was using CreateCredentialsProviderChainDefault which didn't properly support role_arn/source_profile based role assumption via STS because TLS context wasn't being passed to the Profile provider. This change unifies the credential chain for all profiles (default and named), ensuring: - Consistent behavior between default and named profiles - Proper TLS context is passed for STS operations - SSO support works for both cases --- src/libstore/aws-creds.cc | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/libstore/aws-creds.cc b/src/libstore/aws-creds.cc index d84d57c8b6d..f1385a71b8c 100644 --- a/src/libstore/aws-creds.cc +++ b/src/libstore/aws-creds.cc @@ -184,25 +184,19 @@ AwsCredentialProviderImpl::createProviderForProfile(const std::string & profile) throw AwsAuthError("failed to create AWS client bootstrap"); } - // If no profile specified, use the default chain - if (profile.empty()) { - Aws::Crt::Auth::CredentialsProviderChainDefaultConfig config; - config.Bootstrap = bootstrap; - return Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderChainDefault(config); - } - - // For named profiles, build a custom credential chain: Environment → SSO → Profile → IMDS - // The SSO provider will gracefully fail if SSO isn't configured for this profile, - // and the chain will move on to the next provider. + // Build a custom credential chain: Environment → SSO → Profile → IMDS + // This works for both default and named profiles, ensuring consistent behavior + // including SSO support and proper TLS context for STS-based role assumption. Aws::Crt::Auth::CredentialsProviderChainConfig chainConfig; auto allocator = Aws::Crt::ApiAllocator(); + const char * profileName = profile.empty() ? "(default)" : profile.c_str(); auto addProviderToChain = [&](std::string_view name, auto createProvider) { if (auto provider = createProvider()) { chainConfig.Providers.push_back(provider); - debug("Added AWS %s Credential Provider to chain for profile '%s'", name, profile); + debug("Added AWS %s Credential Provider to chain for profile '%s'", name, profileName); } else { - debug("Skipped AWS %s Credential Provider for profile '%s'", name, profile); + debug("Skipped AWS %s Credential Provider for profile '%s'", name, profileName); } }; @@ -215,14 +209,17 @@ AwsCredentialProviderImpl::createProviderForProfile(const std::string & profile) if (tlsContext) { addProviderToChain("SSO", [&]() { return createSSOProvider(profile, bootstrap, tlsContext.get(), allocator); }); } else { - debug("Skipped AWS SSO Credential Provider for profile '%s': TLS context unavailable", profile); + debug("Skipped AWS SSO Credential Provider for profile '%s': TLS context unavailable", profileName); } - // 3. Profile provider (for static credentials) + // 3. Profile provider (for static credentials and role_arn/source_profile with STS) addProviderToChain("Profile", [&]() { Aws::Crt::Auth::CredentialsProviderProfileConfig profileConfig; profileConfig.Bootstrap = bootstrap; - profileConfig.ProfileNameOverride = Aws::Crt::ByteCursorFromCString(profile.c_str()); + profileConfig.TlsContext = tlsContext.get(); + if (!profile.empty()) { + profileConfig.ProfileNameOverride = Aws::Crt::ByteCursorFromCString(profile.c_str()); + } return Aws::Crt::Auth::CredentialsProvider::CreateCredentialsProviderProfile(profileConfig, allocator); }); From 35f59f1741f7d551dc3b3b8969bd9134cce3c7bb Mon Sep 17 00:00:00 2001 From: Bernardo Meurer Date: Tue, 25 Nov 2025 14:20:38 -0500 Subject: [PATCH 4/7] chore(libstore/aws-creds): remove unused includes --- src/libstore/aws-creds.cc | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/libstore/aws-creds.cc b/src/libstore/aws-creds.cc index f1385a71b8c..a7753d705b3 100644 --- a/src/libstore/aws-creds.cc +++ b/src/libstore/aws-creds.cc @@ -4,10 +4,7 @@ # include # 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 # include From 1a2e90b16958c3da348e7d8e31e6ca30c42f7bf5 Mon Sep 17 00:00:00 2001 From: Bernardo Meurer Date: Tue, 25 Nov 2025 15:46:47 -0500 Subject: [PATCH 5/7] test(s3-binary-cache-store): add profile support for setup_for_s3 --- tests/nixos/s3-binary-cache-store.nix | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/nixos/s3-binary-cache-store.nix b/tests/nixos/s3-binary-cache-store.nix index a2ba1dae6c7..1645d8a8dad 100644 --- a/tests/nixos/s3-binary-cache-store.nix +++ b/tests/nixos/s3-binary-cache-store.nix @@ -147,7 +147,7 @@ in else: machine.fail(f"nix path-info {pkg}") - def setup_s3(populate_bucket=[], public=False, versioned=False): + def setup_s3(populate_bucket=[], public=False, versioned=False, profiles=None): """ Decorator that creates/destroys a unique bucket for each test. Optionally pre-populates bucket with specified packages. @@ -157,6 +157,10 @@ in populate_bucket: List of packages to upload before test runs public: If True, make the bucket publicly accessible versioned: If True, enable versioning on the bucket before populating + profiles: Dict of AWS profiles to create, e.g.: + {"valid": {"access_key": "...", "secret_key": "..."}, + "invalid": {"access_key": "WRONG", "secret_key": "WRONG"}} + Profiles are created on the client machine at /root/.aws/credentials """ def decorator(test_func): def wrapper(): @@ -167,6 +171,15 @@ in server.succeed(f"mc anonymous set download minio/{bucket}") if versioned: server.succeed(f"mc version enable minio/{bucket}") + if profiles: + # Build credentials file content + creds_content = "" + for name, creds in profiles.items(): + creds_content += f"[{name}]\n" + creds_content += f"aws_access_key_id = {creds['access_key']}\n" + creds_content += f"aws_secret_access_key = {creds['secret_key']}\n\n" + client.succeed("mkdir -p /root/.aws") + client.succeed(f"cat > /root/.aws/credentials << 'AWSCREDS'\n{creds_content}AWSCREDS") if populate_bucket: store_url = make_s3_url(bucket) for pkg in populate_bucket: @@ -174,6 +187,9 @@ in test_func(bucket) finally: server.succeed(f"mc rb --force minio/{bucket}") + # Clean up AWS profiles if created + if profiles: + client.succeed("rm -rf /root/.aws") # Clean up client store - only delete if path exists for pkg in PKGS.values(): client.succeed(f"[ ! -e {pkg} ] || nix store delete --ignore-liveness {pkg}") From c1a1b439536ad5e1e5ef33e069e898b68a6eb838 Mon Sep 17 00:00:00 2001 From: Bernardo Meurer Date: Tue, 25 Nov 2025 15:47:27 -0500 Subject: [PATCH 6/7] test(s3-binary-cache-store): clear credential cache between tests --- tests/nixos/s3-binary-cache-store.nix | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/nixos/s3-binary-cache-store.nix b/tests/nixos/s3-binary-cache-store.nix index 1645d8a8dad..0e01d0a0134 100644 --- a/tests/nixos/s3-binary-cache-store.nix +++ b/tests/nixos/s3-binary-cache-store.nix @@ -164,6 +164,15 @@ in """ def decorator(test_func): def wrapper(): + # Restart nix-daemon on both machines to clear the credential provider cache. + # The AwsCredentialProviderImpl singleton persists in the daemon process, + # and its cache can cause credentials from previous tests to be reused. + # We reset-failed first to avoid systemd's start rate limiting. + server.succeed("systemctl reset-failed nix-daemon.service nix-daemon.socket") + server.succeed("systemctl restart nix-daemon") + client.succeed("systemctl reset-failed nix-daemon.service nix-daemon.socket") + client.succeed("systemctl restart nix-daemon") + bucket = str(uuid.uuid4()) server.succeed(f"mc mb minio/{bucket}") try: From 2e45b8822556bc897fd16b8ffd81956f02f5c1f1 Mon Sep 17 00:00:00 2001 From: Bernardo Meurer Date: Tue, 25 Nov 2025 15:49:17 -0500 Subject: [PATCH 7/7] test(s3-binary-cache-store): test profiles and provider chain --- tests/nixos/s3-binary-cache-store.nix | 105 ++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/tests/nixos/s3-binary-cache-store.nix b/tests/nixos/s3-binary-cache-store.nix index 0e01d0a0134..154c1fb1b8b 100644 --- a/tests/nixos/s3-binary-cache-store.nix +++ b/tests/nixos/s3-binary-cache-store.nix @@ -789,6 +789,108 @@ in print(" ✓ Compressed log uploaded with multipart") + @setup_s3( + populate_bucket=[PKGS['A']], + profiles={ + "valid": {"access_key": ACCESS_KEY, "secret_key": SECRET_KEY}, + "invalid": {"access_key": "INVALIDKEY", "secret_key": "INVALIDSECRET"}, + } + ) + def test_profile_credentials(bucket): + """Test that profile-based credentials work without environment variables""" + print("\n=== Testing Profile-Based Credentials ===") + + store_url = make_s3_url(bucket, profile="valid") + + # Verify store info works with profile credentials (no env vars) + client.succeed(f"HOME=/root nix store info --store '{store_url}' >&2") + print(" ✓ nix store info works with profile credentials") + + # Verify we can copy from the store using profile + verify_packages_in_store(client, PKGS['A'], should_exist=False) + client.succeed(f"HOME=/root nix copy --no-check-sigs --from '{store_url}' {PKGS['A']}") + verify_packages_in_store(client, PKGS['A']) + print(" ✓ nix copy works with profile credentials") + + # Clean up the package we just copied so we can test invalid profile + client.succeed(f"nix store delete --ignore-liveness {PKGS['A']}") + verify_packages_in_store(client, PKGS['A'], should_exist=False) + + # Verify invalid profile fails when trying to copy + invalid_url = make_s3_url(bucket, profile="invalid") + client.fail(f"HOME=/root nix copy --no-check-sigs --from '{invalid_url}' {PKGS['A']} 2>&1") + print(" ✓ Invalid profile credentials correctly rejected") + + @setup_s3( + populate_bucket=[PKGS['A']], + profiles={ + "wrong": {"access_key": "WRONGKEY", "secret_key": "WRONGSECRET"}, + } + ) + def test_env_vars_precedence(bucket): + """Test that environment variables take precedence over profile credentials""" + print("\n=== Testing Environment Variables Precedence ===") + + # Use profile with wrong credentials, but provide correct creds via env vars + store_url = make_s3_url(bucket, profile="wrong") + + # Ensure package is not in client store + verify_packages_in_store(client, PKGS['A'], should_exist=False) + + # This should succeed because env vars (correct) override profile (wrong) + output = client.succeed( + f"HOME=/root {ENV_WITH_CREDS} nix copy --no-check-sigs --debug --from '{store_url}' {PKGS['A']} 2>&1" + ) + print(" ✓ nix copy succeeded with env vars overriding wrong profile") + + # Verify the credential chain shows Environment provider was added + if "Added AWS Environment Credential Provider" not in output: + print("Debug output:") + print(output) + raise Exception("Expected Environment provider to be added to chain") + print(" ✓ Environment provider added to credential chain") + + # Clean up the package so we can test again without env vars + client.succeed(f"nix store delete --ignore-liveness {PKGS['A']}") + verify_packages_in_store(client, PKGS['A'], should_exist=False) + + # Without env vars, same URL should fail (proving profile creds are actually wrong) + client.fail(f"HOME=/root nix copy --no-check-sigs --from '{store_url}' {PKGS['A']} 2>&1") + print(" ✓ Without env vars, wrong profile credentials correctly fail") + + @setup_s3( + populate_bucket=[PKGS['A']], + profiles={ + "testprofile": {"access_key": ACCESS_KEY, "secret_key": SECRET_KEY}, + } + ) + def test_credential_provider_chain(bucket): + """Test that debug logging shows which providers are added to the chain""" + print("\n=== Testing Credential Provider Chain Logging ===") + + store_url = make_s3_url(bucket, profile="testprofile") + + output = client.succeed( + f"HOME=/root nix store info --debug --store '{store_url}' 2>&1" + ) + + # For a named profile, we expect to see these providers in the chain + expected_providers = ["Environment", "Profile", "IMDS"] + for provider in expected_providers: + msg = f"Added AWS {provider} Credential Provider to chain for profile 'testprofile'" + if msg not in output: + print("Debug output:") + print(output) + raise Exception(f"Expected to find: {msg}") + print(f" ✓ {provider} provider added to chain") + + # SSO should be skipped (no SSO config for this profile) + if "Skipped AWS SSO Credential Provider for profile 'testprofile'" not in output: + print("Debug output:") + print(output) + raise Exception("Expected SSO provider to be skipped") + print(" ✓ SSO provider correctly skipped (not configured)") + # ============================================================================ # Main Test Execution # ============================================================================ @@ -822,6 +924,9 @@ in test_multipart_upload_basic() test_multipart_threshold() test_multipart_with_log_compression() + test_profile_credentials() + test_env_vars_precedence() + test_credential_provider_chain() print("\n" + "="*80) print("✓ All S3 Binary Cache Store Tests Passed!")