diff --git a/gems/aws-sdk-core/CHANGELOG.md b/gems/aws-sdk-core/CHANGELOG.md index ae402985ba4..eea4fac2df4 100644 --- a/gems/aws-sdk-core/CHANGELOG.md +++ b/gems/aws-sdk-core/CHANGELOG.md @@ -1,6 +1,8 @@ Unreleased Changes ------------------ +* Issue - Additional metrics collection for credentials in the User-Agent plugin. + 3.222.1 (2025-03-28) ------------------ diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/assume_role_credentials.rb b/gems/aws-sdk-core/lib/aws-sdk-core/assume_role_credentials.rb index ad9e6a4df6f..e7f6c6c8889 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/assume_role_credentials.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/assume_role_credentials.rb @@ -50,6 +50,7 @@ def initialize(options = {}) end @client = client_opts[:client] || STS::Client.new(client_opts) @async_refresh = true + @metrics = ['CREDENTIALS_STS_ASSUME_ROLE'] super end diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/assume_role_web_identity_credentials.rb b/gems/aws-sdk-core/lib/aws-sdk-core/assume_role_web_identity_credentials.rb index c07828f32a1..bcf0554ba17 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/assume_role_web_identity_credentials.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/assume_role_web_identity_credentials.rb @@ -61,6 +61,7 @@ def initialize(options = {}) @assume_role_web_identity_params[:role_session_name] = _session_name end @client = client_opts[:client] || STS::Client.new(client_opts.merge(credentials: nil)) + @metrics = ['CREDENTIALS_STS_ASSUME_ROLE_WEB_ID'] super end diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider.rb b/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider.rb index 7f3a20c5708..ccf066b8b85 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider.rb @@ -9,6 +9,10 @@ module CredentialProvider # @return [Time] attr_reader :expiration + # @api private + # Returns UserAgent metrics for credentials. + attr_accessor :metrics + # @return [Boolean] def set? !!@credentials && @credentials.set? diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb b/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb index 8f41f12219f..2efebeaaef1 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/credential_provider_chain.rb @@ -42,12 +42,14 @@ def providers def static_credentials(options) if options[:config] - Credentials.new( + creds = Credentials.new( options[:config].access_key_id, options[:config].secret_access_key, options[:config].session_token, account_id: options[:config].account_id ) + creds.metrics = ['CREDENTIALS_PROFILE'] + creds end end @@ -76,7 +78,9 @@ def static_profile_assume_role_credentials(options) def static_profile_credentials(options) if options[:config] && options[:config].profile - SharedCredentials.new(profile_name: options[:config].profile) + creds = SharedCredentials.new(profile_name: options[:config].profile) + creds.metrics = ['CREDENTIALS_PROFILE'] + creds end rescue Errors::NoSuchProfileError nil @@ -85,7 +89,11 @@ def static_profile_credentials(options) def static_profile_process_credentials(options) if Aws.shared_config.config_enabled? && options[:config] && options[:config].profile process_provider = Aws.shared_config.credential_process(profile: options[:config].profile) - ProcessCredentials.new([process_provider]) if process_provider + if process_provider + creds = ProcessCredentials.new([process_provider]) + creds.metrics << 'CREDENTIALS_PROFILE_PROCESS' + creds + end end rescue Errors::NoSuchProfileError nil @@ -96,12 +104,14 @@ def env_credentials(_options) secret = %w[AWS_SECRET_ACCESS_KEY AMAZON_SECRET_ACCESS_KEY AWS_SECRET_KEY] token = %w[AWS_SESSION_TOKEN AMAZON_SESSION_TOKEN] account_id = %w[AWS_ACCOUNT_ID] - Credentials.new( + creds = Credentials.new( envar(key), envar(secret), envar(token), account_id: envar(account_id) ) + creds.metrics = ['CREDENTIALS_ENV_VARS'] + creds end def envar(keys) @@ -117,7 +127,9 @@ def determine_profile_name(options) def shared_credentials(options) profile_name = determine_profile_name(options) - SharedCredentials.new(profile_name: profile_name) + creds = SharedCredentials.new(profile_name: profile_name) + creds.metrics = ['CREDENTIALS_PROFILE'] + creds rescue Errors::NoSuchProfileError nil end @@ -126,7 +138,11 @@ def process_credentials(options) profile_name = determine_profile_name(options) if Aws.shared_config.config_enabled? process_provider = Aws.shared_config.credential_process(profile: profile_name) - ProcessCredentials.new([process_provider]) if process_provider + if process_provider + creds = ProcessCredentials.new([process_provider]) + creds.metrics << 'CREDENTIALS_PROFILE_PROCESS' + creds + end end rescue Errors::NoSuchProfileError nil @@ -156,7 +172,11 @@ def assume_role_web_identity_credentials(options) role_session_name: ENV['AWS_ROLE_SESSION_NAME'] } cfg[:region] = region if region - AssumeRoleWebIdentityCredentials.new(cfg) + Aws::Plugins::UserAgent.metric('CREDENTIALS_ENV_VARS_STS_WEB_ID_TOKEN') do + creds = AssumeRoleWebIdentityCredentials.new(cfg) + creds.metrics << 'CREDENTIALS_ENV_VARS_STS_WEB_ID_TOKEN' + creds + end elsif Aws.shared_config.config_enabled? profile = options[:config].profile if options[:config] Aws.shared_config.assume_role_web_identity_credentials_from_config( diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/credentials.rb b/gems/aws-sdk-core/lib/aws-sdk-core/credentials.rb index 93bf3a0026f..865e22800ed 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/credentials.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/credentials.rb @@ -14,6 +14,7 @@ def initialize(access_key_id, secret_access_key, session_token = nil, @secret_access_key = secret_access_key @session_token = session_token @account_id = kwargs[:account_id] + @metrics = ['CREDENTIALS_CODE'] end # @return [String] @@ -28,6 +29,11 @@ def initialize(access_key_id, secret_access_key, session_token = nil, # @return [String, nil] attr_reader :account_id + # @api private + # Returns the credentials source. Used for tracking credentials + # related UserAgent metrics. + attr_accessor :metrics + # @return [Credentials] def credentials self diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/ecs_credentials.rb b/gems/aws-sdk-core/lib/aws-sdk-core/ecs_credentials.rb index 002ee55c067..97ff646ba7b 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/ecs_credentials.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/ecs_credentials.rb @@ -77,6 +77,7 @@ def initialize(options = {}) @http_debug_output = options[:http_debug_output] @backoff = backoff(options[:backoff]) @async_refresh = false + @metrics = ['CREDENTIALS_HTTP'] super end diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb b/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb index d1b7d337ed8..be47a8f87e6 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/instance_profile_credentials.rb @@ -90,6 +90,7 @@ def initialize(options = {}) @token = nil @no_refresh_until = nil @async_refresh = false + @metrics = ['CREDENTIALS_IMDS'] super end diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/plugins/client_metrics_plugin.rb b/gems/aws-sdk-core/lib/aws-sdk-core/plugins/client_metrics_plugin.rb index 4b63fd4c6d8..b11230ddd00 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/plugins/client_metrics_plugin.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/plugins/client_metrics_plugin.rb @@ -180,7 +180,6 @@ def call(context) complete_opts = { latency: end_time - start_time, attempt_count: context.retries + 1, - user_agent: context.http_request.headers["user-agent"], final_error_retryable: final_error_retryable, final_http_status_code: context.http_response.status_code, final_aws_exception: final_aws_exception, diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/plugins/sign.rb b/gems/aws-sdk-core/lib/aws-sdk-core/plugins/sign.rb index 80d98bca931..eba2235498f 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/plugins/sign.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/plugins/sign.rb @@ -41,6 +41,7 @@ def self.signer_for(auth_scheme, config, sigv4_region_override = nil, sigv4_cred class Handler < Seahorse::Client::Handler def call(context) # Skip signing if using sigv2 signing from s3_signer in S3 + credentials = nil unless v2_signing?(context.config) signer = Sign.signer_for( context[:auth_scheme], @@ -48,13 +49,20 @@ def call(context) context[:sigv4_region], context[:sigv4_credentials] ) + credentials = signer.credentials if signer.is_a?(SignatureV4) signer.sign(context) end - @handler.call(context) + with_metrics(credentials) { @handler.call(context) } end private + def with_metrics(credentials, &block) + return block.call unless credentials&.respond_to?(:metrics) + + Aws::Plugins::UserAgent.metric(*credentials.metrics, &block) + end + def v2_signing?(config) # 's3' is legacy signing, 'v4' is default config.respond_to?(:signature_version) && @@ -92,6 +100,8 @@ def sign_event(*args) # @api private class SignatureV4 + attr_reader :signer + def initialize(auth_scheme, config, sigv4_overrides = {}) scheme_name = auth_scheme['name'] @@ -155,6 +165,10 @@ def sign_event(*args) @signer.sign_event(*args) end + def credentials + @signer.credentials_provider + end + private def apply_authtype(context, req) diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/plugins/user_agent.rb b/gems/aws-sdk-core/lib/aws-sdk-core/plugins/user_agent.rb index 2d1e6e8ee5a..5c1c3faada4 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/plugins/user_agent.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/plugins/user_agent.rb @@ -34,7 +34,27 @@ class UserAgent < Seahorse::Client::Plugin "FLEXIBLE_CHECKSUMS_REQ_WHEN_SUPPORTED" : "Z", "FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED" : "a", "FLEXIBLE_CHECKSUMS_RES_WHEN_SUPPORTED" : "b", - "FLEXIBLE_CHECKSUMS_RES_WHEN_REQUIRED" : "c" + "FLEXIBLE_CHECKSUMS_RES_WHEN_REQUIRED" : "c", + "DDB_MAPPER": "d", + "CREDENTIALS_CODE" : "e", + "CREDENTIALS_ENV_VARS" : "g", + "CREDENTIALS_ENV_VARS_STS_WEB_ID_TOKEN" : "h", + "CREDENTIALS_STS_ASSUME_ROLE" : "i", + "CREDENTIALS_STS_ASSUME_ROLE_WEB_ID" : "k", + "CREDENTIALS_PROFILE" : "n", + "CREDENTIALS_PROFILE_SOURCE_PROFILE" : "o", + "CREDENTIALS_PROFILE_NAMED_PROVIDER" : "p", + "CREDENTIALS_PROFILE_STS_WEB_ID_TOKEN" : "q", + "CREDENTIALS_PROFILE_SSO" : "r", + "CREDENTIALS_SSO" : "s", + "CREDENTIALS_PROFILE_SSO_LEGACY" : "t", + "CREDENTIALS_SSO_LEGACY" : "u", + "CREDENTIALS_PROFILE_PROCESS" : "v", + "CREDENTIALS_PROCESS" : "w", + "CREDENTIALS_HTTP" : "z", + "CREDENTIALS_IMDS" : "0", + "SSO_LOGIN_DEVICE" : "1", + "SSO_LOGIN_AUTH" : "2" } METRICS @@ -196,7 +216,8 @@ def metric_metadata end end - handler(Handler, step: :sign, priority: 97) + # Priority set to 5 in order to add user agent as late as possible after signing + handler(Handler, step: :sign, priority: 5) end end end diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/process_credentials.rb b/gems/aws-sdk-core/lib/aws-sdk-core/process_credentials.rb index 55cc42ba8e5..9c60aab0a4c 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/process_credentials.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/process_credentials.rb @@ -36,7 +36,7 @@ def initialize(process) @process = process @credentials = credentials_from_process @async_refresh = false - + @metrics = ['CREDENTIALS_PROCESS'] super end diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/shared_config.rb b/gems/aws-sdk-core/lib/aws-sdk-core/shared_config.rb index 1fea8bdbccd..140650e985a 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/shared_config.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/shared_config.rb @@ -138,7 +138,11 @@ def assume_role_web_identity_credentials_from_config(opts = {}) role_session_name: entry['role_session_name'] } cfg[:region] = opts[:region] if opts[:region] - AssumeRoleWebIdentityCredentials.new(cfg) + with_metrics('CREDENTIALS_PROFILE_STS_WEB_ID_TOKEN') do + creds = AssumeRoleWebIdentityCredentials.new(cfg) + creds.metrics << 'CREDENTIALS_PROFILE_STS_WEB_ID_TOKEN' + creds + end end end end @@ -255,8 +259,8 @@ def assume_role_from_profile(cfg, profile, opts, chain_config) 'provide only source_profile or credential_source, not both.' elsif opts[:source_profile] opts[:visited_profiles] ||= Set.new - opts[:credentials] = resolve_source_profile(opts[:source_profile], opts) - if opts[:credentials] + provider = resolve_source_profile(opts[:source_profile], opts) + if provider && (opts[:credentials] = provider.credentials) opts[:role_session_name] ||= prof_cfg['role_session_name'] opts[:role_session_name] ||= 'default_session' opts[:role_arn] ||= prof_cfg['role_arn'] @@ -265,17 +269,28 @@ def assume_role_from_profile(cfg, profile, opts, chain_config) opts[:serial_number] ||= prof_cfg['mfa_serial'] opts[:profile] = opts.delete(:source_profile) opts.delete(:visited_profiles) - AssumeRoleCredentials.new(opts) + + metrics = provider.metrics + if provider.is_a?(AssumeRoleCredentials) + opts[:credentials] = provider + metrics.delete('CREDENTIALS_STS_ASSUME_ROLE') + else + metrics << 'CREDENTIALS_PROFILE_SOURCE_PROFILE' + end + # Set the original credentials metrics to [] to prevent duplicate metrics during sign plugin + opts[:credentials].metrics = [] + with_metrics(metrics) do + creds = AssumeRoleCredentials.new(opts) + creds.metrics.push(*metrics) + creds + end else raise Errors::NoSourceProfileError, "Profile #{profile} has a role_arn, and source_profile, but the"\ ' source_profile does not have credentials.' end elsif credential_source - opts[:credentials] = credentials_from_source( - credential_source, - chain_config - ) + opts[:credentials] = credentials_from_source(credential_source, chain_config) if opts[:credentials] opts[:role_session_name] ||= prof_cfg['role_session_name'] opts[:role_session_name] ||= 'default_session' @@ -284,7 +299,16 @@ def assume_role_from_profile(cfg, profile, opts, chain_config) opts[:external_id] ||= prof_cfg['external_id'] opts[:serial_number] ||= prof_cfg['mfa_serial'] opts.delete(:source_profile) # Cleanup - AssumeRoleCredentials.new(opts) + + metrics = opts[:credentials].metrics + metrics << 'CREDENTIALS_PROFILE_NAMED_PROVIDER' + # Set the original credentials metrics to [] to prevent duplicate metrics during sign plugin + opts[:credentials].metrics = [] + with_metrics(metrics) do + creds = AssumeRoleCredentials.new(opts) + creds.metrics.push(*metrics) + creds + end else raise Errors::NoSourceCredentials, "Profile #{profile} could not get source credentials from"\ @@ -312,12 +336,24 @@ def resolve_source_profile(profile, opts = {}) elsif profile_config && profile_config['source_profile'] opts.delete(:source_profile) assume_role_credentials_from_config(opts.merge(profile: profile)) - elsif (provider = assume_role_web_identity_credentials_from_config(opts.merge(profile: profile))) - provider.credentials if provider.credentials.set? + elsif (provider = assume_role_web_identity_credentials_from_config_with_metrics(opts.merge(profile: profile))) + provider if provider.credentials.set? elsif (provider = assume_role_process_credentials_from_config(profile)) - provider.credentials if provider.credentials.set? - elsif (provider = sso_credentials_from_config(profile: profile)) - provider.credentials if provider.credentials.set? + provider if provider.credentials.set? + elsif (provider = sso_credentials_from_config_with_metrics(profile)) + provider if provider.credentials.set? + end + end + + def assume_role_web_identity_credentials_from_config_with_metrics(opts) + with_metrics('CREDENTIALS_PROFILE_SOURCE_PROFILE') do + assume_role_web_identity_credentials_from_config(opts) + end + end + + def sso_credentials_from_config_with_metrics(profile) + with_metrics('CREDENTIALS_PROFILE_SOURCE_PROFILE') do + sso_credentials_from_config(profile: profile) end end @@ -342,7 +378,11 @@ def assume_role_process_credentials_from_config(profile) if @parsed_config credential_process ||= @parsed_config.fetch(profile, {})['credential_process'] end - ProcessCredentials.new([credential_process]) if credential_process + if credential_process + creds = ProcessCredentials.new([credential_process]) + creds.metrics << 'CREDENTIALS_PROFILE_PROCESS' + creds + end end def credentials_from_shared(profile, _opts) @@ -386,13 +426,18 @@ def sso_credentials_from_profile(cfg, profile) sso_start_url = prof_config['sso_start_url'] end - SSOCredentials.new( - sso_account_id: prof_config['sso_account_id'], - sso_role_name: prof_config['sso_role_name'], - sso_session: prof_config['sso_session'], - sso_region: sso_region, - sso_start_url: sso_start_url + metric = prof_config['sso_session'] ? 'CREDENTIALS_PROFILE_SSO' : 'CREDENTIALS_PROFILE_SSO_LEGACY' + with_metrics(metric) do + creds = SSOCredentials.new( + sso_account_id: prof_config['sso_account_id'], + sso_role_name: prof_config['sso_role_name'], + sso_session: prof_config['sso_session'], + sso_region: sso_region, + sso_start_url: sso_start_url ) + creds.metrics << metric + creds + end end end @@ -420,6 +465,7 @@ def credentials_from_profile(prof_config) prof_config['aws_session_token'], account_id: prof_config['aws_account_id'] ) + creds.metrics = ['CREDENTIALS_PROFILE'] creds if creds.set? end @@ -480,5 +526,9 @@ def sso_session(cfg, profile, sso_session_name) sso_session end + + def with_metrics(metrics, &block) + Aws::Plugins::UserAgent.metric(*metrics, &block) + end end end diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/shared_credentials.rb b/gems/aws-sdk-core/lib/aws-sdk-core/shared_credentials.rb index 9508080f19f..31fb1f7030d 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/shared_credentials.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/shared_credentials.rb @@ -40,6 +40,7 @@ def initialize(options = {}) ) @credentials = config.credentials(profile: @profile_name) end + @metrics = ['CREDENTIALS_CODE'] end # @return [String] diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/sso_credentials.rb b/gems/aws-sdk-core/lib/aws-sdk-core/sso_credentials.rb index 3c4785f5c8c..59389a4f7e4 100644 --- a/gems/aws-sdk-core/lib/aws-sdk-core/sso_credentials.rb +++ b/gems/aws-sdk-core/lib/aws-sdk-core/sso_credentials.rb @@ -91,6 +91,7 @@ def initialize(options = {}) client_opts[:credentials] = nil @client = Aws::SSO::Client.new(client_opts) end + @metrics = ['CREDENTIALS_SSO'] else # legacy behavior missing_keys = LEGACY_REQUIRED_OPTS.select { |k| options[k].nil? } unless missing_keys.empty? @@ -111,6 +112,7 @@ def initialize(options = {}) client_opts[:credentials] = nil @client = options[:client] || Aws::SSO::Client.new(client_opts) + @metrics = ['CREDENTIALS_SSO_LEGACY'] end @async_refresh = true diff --git a/gems/aws-sdk-core/spec/aws/credential_provider_chain_spec.rb b/gems/aws-sdk-core/spec/aws/credential_provider_chain_spec.rb index 89debf334b8..737494beb71 100644 --- a/gems/aws-sdk-core/spec/aws/credential_provider_chain_spec.rb +++ b/gems/aws-sdk-core/spec/aws/credential_provider_chain_spec.rb @@ -59,6 +59,10 @@ def validate_credentials(expected_creds) expect(creds.account_id).to eq(expected_creds[:account_id]) end + def validate_metrics(*expected_metrics) + expect(credentials.metrics).to eq(expected_metrics) + end + let(:config) do double('config', access_key_id: nil, @@ -79,11 +83,13 @@ def validate_credentials(expected_creds) before(:each) do allow(InstanceProfileCredentials).to receive(:new).and_return(mock_instance_creds) + allow(mock_instance_creds).to receive(:metrics).and_return(['CREDENTIALS_IMDS']) end it 'hydrates credentials from config options' do expected_creds = with_config_credentials validate_credentials(expected_creds) + validate_metrics('CREDENTIALS_PROFILE') end it 'hydrates credentials from ENV with prefix AWS_' do @@ -93,6 +99,7 @@ def validate_credentials(expected_creds) ENV['AWS_SESSION_TOKEN'] = expected_creds[:session_token] ENV['AWS_ACCOUNT_ID'] = expected_creds[:account_id] validate_credentials(expected_creds) + validate_metrics('CREDENTIALS_ENV_VARS') end it 'hydrates credentials from ENV with prefix AMAZON_' do @@ -101,6 +108,7 @@ def validate_credentials(expected_creds) ENV['AMAZON_SECRET_ACCESS_KEY'] = expected_creds[:secret_access_key] ENV['AMAZON_SESSION_TOKEN'] = expected_creds[:session_token] validate_credentials(expected_creds) + validate_metrics('CREDENTIALS_ENV_VARS') end it 'hydrates credentials from ENV at AWS_ACCESS_KEY & AWS_SECRET_KEY' do @@ -108,6 +116,7 @@ def validate_credentials(expected_creds) ENV['AWS_ACCESS_KEY'] = expected_creds[:access_key_id] ENV['AWS_SECRET_KEY'] = expected_creds[:secret_access_key] validate_credentials(expected_creds) + validate_metrics('CREDENTIALS_ENV_VARS') end it 'hydrates credentials from ENV at AWS_ACCESS_KEY_ID & AWS_SECRET_KEY' do @@ -115,27 +124,29 @@ def validate_credentials(expected_creds) ENV['AWS_ACCESS_KEY_ID'] = expected_creds[:access_key_id] ENV['AWS_SECRET_KEY'] = expected_creds[:secret_access_key] validate_credentials(expected_creds) + validate_metrics('CREDENTIALS_ENV_VARS') end it 'hydrates credentials from the instance profile service' do expect(mock_instance_creds).to receive(:set?).and_return(true) expect(credentials).to be(mock_instance_creds) + validate_metrics('CREDENTIALS_IMDS') end it 'hydrates credentials from ECS when AWS_CONTAINER_CREDENTIALS_RELATIVE_URI is set' do ENV['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'] = 'test_uri' - mock_ecs_creds = double('ECSCredentials') + mock_ecs_creds = double('ECSCredentials', metrics: ['CREDENTIALS_HTTP'], set?: true) expect(ECSCredentials).to receive(:new).and_return(mock_ecs_creds) - expect(mock_ecs_creds).to receive(:set?).and_return(true) expect(credentials).to be(mock_ecs_creds) + validate_metrics('CREDENTIALS_HTTP') end it 'hydrates credentials from ECS when AWS_CONTAINER_CREDENTIALS_FULL_URI is set' do ENV['AWS_CONTAINER_CREDENTIALS_FULL_URI'] = 'test_uri' - mock_ecs_creds = double('ECSCredentials') + mock_ecs_creds = double('ECSCredentials', metrics: ['CREDENTIALS_HTTP'], set?: true) expect(ECSCredentials).to receive(:new).and_return(mock_ecs_creds) - expect(mock_ecs_creds).to receive(:set?).and_return(true) expect(credentials).to be(mock_ecs_creds) + validate_metrics('CREDENTIALS_HTTP') end describe 'with config set to nil' do @@ -161,6 +172,7 @@ def validate_credentials(expected_creds) expected_creds = with_shared_credentials('test') ENV['AWS_DEFAULT_PROFILE'] = expected_creds[:profile_name] validate_credentials(expected_creds) + validate_metrics('CREDENTIALS_PROFILE') end it 'returns credentials from proper profile when config is set' do @@ -168,6 +180,7 @@ def validate_credentials(expected_creds) allow(config).to receive(:profile).and_return(expected_creds[:profile_name]) ENV['AWS_DEFAULT_PROFILE'] = 'BAD_PROFILE' validate_credentials(expected_creds) + validate_metrics('CREDENTIALS_PROFILE') end end @@ -177,12 +190,14 @@ def validate_credentials(expected_creds) ENV['AWS_DEFAULT_PROFILE'] = shared_creds[:profile_name] expected_creds = with_env_credentials validate_credentials(expected_creds) + validate_metrics('CREDENTIALS_ENV_VARS') end it 'hydrates credentials from config over ENV' do env_creds = with_env_credentials expected_creds = with_config_credentials validate_credentials(expected_creds) + validate_metrics('CREDENTIALS_PROFILE') end it 'hydrates credentials from profile when config set over ENV' do @@ -190,6 +205,7 @@ def validate_credentials(expected_creds) allow(config).to receive(:profile).and_return(expected_creds[:profile_name]) env_creds = with_env_credentials validate_credentials(expected_creds) + validate_metrics('CREDENTIALS_PROFILE') end end end diff --git a/gems/aws-sdk-core/spec/aws/credential_resolution_chain_spec.rb b/gems/aws-sdk-core/spec/aws/credential_resolution_chain_spec.rb index f628d9ddc1c..e141482d286 100644 --- a/gems/aws-sdk-core/spec/aws/credential_resolution_chain_spec.rb +++ b/gems/aws-sdk-core/spec/aws/credential_resolution_chain_spec.rb @@ -31,7 +31,6 @@ module Aws before(:each) do allow(InstanceProfileCredentials).to receive(:new).and_return(mock_instance_creds) - expect_any_instance_of(ProcessCredentials).not_to receive(:warn) end @@ -59,6 +58,7 @@ module Aws region: 'us-east-1' ) expect(client.config.credentials.access_key_id).to eq('ACCESS_DIRECT') + expect(metric_values(client.config.credentials.metrics)).to include('n') end it 'prefers assume role credentials when profile explicitly set over ENV credentials' do @@ -78,9 +78,33 @@ module Aws profile: 'assumerole_sc', region: 'us-east-1' ) expect(client.config.credentials.credentials.access_key_id).to eq('AR_AKID') + expect(metric_values(client.config.credentials.metrics)).to include('o', 'n', 'i') + end + + it 'emits correct UserAgent metrics during STS call for assume role credentials' do + stub_const( + 'ENV', + 'AWS_ACCESS_KEY_ID' => 'AKID_ENV_STUB', + 'AWS_SECRET_ACCESS_KEY' => 'SECRET_ENV_STUB' + ) + assume_role_stub( + 'arn:aws:iam::123456789012:role/foo', + 'ACCESS_KEY_1', # from 'fooprofile' + 'AR_AKID', + 'AR_SECRET', + 'AR_TOKEN' + ) + expect_any_instance_of(STS::Client).to receive(:assume_role).and_wrap_original do |m, *args| + resp = m.call(*args) + expect(metrics_from_user_agent_header(resp)).to include('o', 'n') + resp + end + ApiHelper.sample_rest_xml::Client.new( + profile: 'assumerole_sc', region: 'us-east-1' + ) end - it 'prefers assume role web identity over sso' do + it 'prefers assume role web identity from profile over sso' do assume_role_web_identity_stub( 'arn:aws:iam::123456789012:role/foo', 'AR_AKID', @@ -93,22 +117,70 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('AR_AKID') + expect(metric_values(client.config.credentials.metrics)).to include('q', 'k') end - it 'prefers sso credentials over assume role' do - expect(SSOCredentials).to receive(:new).with( - sso_start_url: 'START_URL', - sso_region: 'us-east-1', - sso_account_id: 'SSO_ACCOUNT_ID', - sso_role_name: 'SSO_ROLE_NAME', - sso_session: 'sso-test-session' - ).and_return( - double( - 'creds', - set?: true, - credentials: double(access_key_id: 'SSO_AKID') - ) + it 'emits correct UserAgent metrics during STS call for assume role web identity from profile' do + assume_role_web_identity_stub( + 'arn:aws:iam::123456789012:role/foo', + 'AR_AKID', + 'AR_SECRET', + 'AR_TOKEN' + ) + expect_any_instance_of(STS::Client).to receive(:assume_role_with_web_identity).and_wrap_original do |m, *args| + resp = m.call(*args) + expect(metrics_from_user_agent_header(resp)).to include('q') + resp + end + ApiHelper.sample_rest_xml::Client.new( + profile: 'ar_web_identity', region: 'us-east-1' + ) + end + + it 'prefers assume role web identity from ENV over sso' do + stub_const( + 'ENV', + 'AWS_ROLE_ARN' => 'arn:aws:iam::123456789012:role/foo', + 'AWS_WEB_IDENTITY_TOKEN_FILE' => 'my-token.jwt' + ) + assume_role_web_identity_stub( + 'arn:aws:iam::123456789012:role/foo', + 'AR_AKID', + 'AR_SECRET', + 'AR_TOKEN' + ) + client = ApiHelper.sample_rest_xml::Client.new( + region: 'us-east-1' + ) + expect(client.config.credentials.credentials.access_key_id) + .to eq('AR_AKID') + expect(metric_values(client.config.credentials.metrics)).to include('h', 'k') + end + + it 'emits correct UserAgent metrics during STS call for assume role web identity from env' do + stub_const( + 'ENV', + 'AWS_ROLE_ARN' => 'arn:aws:iam::123456789012:role/foo', + 'AWS_WEB_IDENTITY_TOKEN_FILE' => 'my-token.jwt' + ) + assume_role_web_identity_stub( + 'arn:aws:iam::123456789012:role/foo', + 'AR_AKID', + 'AR_SECRET', + 'AR_TOKEN' + ) + expect_any_instance_of(STS::Client).to receive(:assume_role_with_web_identity).and_wrap_original do |m, *args| + resp = m.call(*args) + expect(metrics_from_user_agent_header(resp)).to include('h') + resp + end + ApiHelper.sample_rest_xml::Client.new( + region: 'us-east-1' ) + end + + it 'prefers sso credentials over assume role' do + sso_stub client = ApiHelper.sample_rest_xml::Client.new( profile: 'sso_creds', token_provider: nil @@ -116,67 +188,59 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('SSO_AKID') + expect(metric_values(client.config.credentials.metrics)).to include('r', 's') end - it 'loads SSO credentials from a legacy profile' do - expect(SSOCredentials).to receive(:new).with( - sso_start_url: 'START_URL', - sso_region: 'us-east-1', - sso_account_id: 'SSO_ACCOUNT_ID', - sso_role_name: 'SSO_ROLE_NAME', - sso_session: nil - ).and_return( - double( - 'creds', - set?: true, - credentials: double(access_key_id: 'SSO_AKID') - ) + it 'emits correct UserAgent metrics during SSO call for SSO' do + sso_stub + expect_any_instance_of(SSO::Client).to receive(:get_role_credentials).and_wrap_original do |m, *args| + resp = m.call(*args) + expect(metrics_from_user_agent_header(resp)).to include('r') + resp + end + ApiHelper.sample_rest_xml::Client.new( + profile: 'sso_creds', + token_provider: nil ) + end + + it 'loads SSO credentials from a legacy profile' do + legacy_sso_stub client = ApiHelper.sample_rest_xml::Client.new( profile: 'sso_creds_legacy' ) expect( client.config.credentials.credentials.access_key_id ).to eq('SSO_AKID') + expect(metric_values(client.config.credentials.metrics)).to include('t', 'u') end - it 'loads SSO credentials from a mixed legacy profile when values match' do - expect(SSOCredentials).to receive(:new).with( - sso_start_url: 'START_URL', - sso_region: 'us-east-1', - sso_account_id: 'SSO_ACCOUNT_ID', - sso_role_name: 'SSO_ROLE_NAME', - sso_session: 'sso-test-session' - ).and_return( - double( - 'creds', - set?: true, - credentials: double(access_key_id: 'SSO_AKID') - ) + it 'emits correct UserAgent metrics during SSO call for legacy SSO' do + legacy_sso_stub + expect_any_instance_of(SSO::Client).to receive(:get_role_credentials).and_wrap_original do |m, *args| + resp = m.call(*args) + expect(metrics_from_user_agent_header(resp)).to include('t') + resp + end + ApiHelper.sample_rest_xml::Client.new( + profile: 'sso_creds_legacy' ) + end + + it 'loads SSO credentials from a mixed legacy profile when values match' do + sso_stub client = ApiHelper.sample_rest_xml::Client.new( profile: 'sso_creds_mixed_legacy', - token_provider: nil, + token_provider: nil ) expect( client.config.credentials.credentials.access_key_id ).to eq('SSO_AKID') + expect(metric_values(client.config.credentials.metrics)).to include('r', 's') end it 'loads SSO credentials from when the session name has quotes' do - expect(SSOCredentials).to receive(:new).with( - sso_start_url: 'START_URL', - sso_region: 'us-east-1', - sso_account_id: 'SSO_ACCOUNT_ID', - sso_role_name: 'SSO_ROLE_NAME', - sso_session: 'sso test session' - ).and_return( - double( - 'creds', - set?: true, - credentials: double(access_key_id: 'SSO_AKID') - ) - ) + sso_stub client = ApiHelper.sample_rest_xml::Client.new( profile: 'sso_creds_session_with_quotes', token_provider: nil @@ -184,6 +248,7 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('SSO_AKID') + expect(metric_values(client.config.credentials.metrics)).to include('r', 's') end it 'raises when attempting to load an incomplete SSO Profile' do @@ -229,6 +294,7 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('AR_AKID') + expect(metric_values(client.config.credentials.metrics)).to include('o', 'n', 'i') sts_client = client.config.credentials.client expect( @@ -243,6 +309,7 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('ACCESS_KEY_CRD') + expect(metric_values(client.config.credentials.metrics)).to include('n') end it 'will source static credentials from shared config after shared credentials' do @@ -252,6 +319,7 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('ACCESS_KEY_SC1') + expect(metric_values(client.config.credentials.metrics)).to include('n') end it 'prefers process credentials over metadata credentials' do @@ -261,6 +329,7 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('AK_PROC1') + expect(metric_values(client.config.credentials.metrics)).to include('v', 'w') end it 'prefers direct credentials over process credentials when profile not set' do @@ -275,6 +344,7 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('AKID_ENV_STUB') + expect(metric_values(client.config.credentials.metrics)).to include('g') end it 'prefers process credentials from direct profile over env' do @@ -289,9 +359,10 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('AK_PROC1') + expect(metric_values(client.config.credentials.metrics)).to include('v', 'w') end - it 'attempts to fetch metadata credentials last' do + it 'attempts to fetch metadata credentials last using IMDS' do allow(InstanceProfileCredentials).to receive(:new).and_call_original stub_request(:put, 'http://169.254.169.254/latest/api/token') @@ -326,6 +397,29 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('akid-md') + expect(metric_values(client.config.credentials.metrics)).to include('0') + end + + it 'attempts to fetch metadata credentials last using ECS' do + path = '/latest/credentials?id=foobarbaz' + resp = <<-JSON.strip + { + "RoleArn" : "arn:aws:iam::123456789012:role/BarFooRole", + "AccessKeyId" : "ACCESS_KEY_ECS", + "SecretAccessKey" : "secret", + "Token" : "session-token", + "Expiration" : "#{(Time.now.utc + 3600).strftime('%Y-%m-%dT%H:%M:%SZ')}" + } + JSON + stub_const('ENV', 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI' => path) + stub_request(:get, "http://169.254.170.2#{path}") + .to_return(status: 200, body: resp) + client = ApiHelper.sample_rest_xml::Client.new( + profile: 'nonexistent', + region: 'us-east-1' + ) + expect(client.config.credentials.credentials.access_key_id).to eq('ACCESS_KEY_ECS') + expect(metric_values(client.config.credentials.metrics)).to include('z') end describe 'Assume Role Resolution' do @@ -365,6 +459,7 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('AR_AKID') + expect(metric_values(client.config.credentials.metrics)).to include('o', 'q', 'k', 'i') sts_client = client.config.credentials.client expect( @@ -372,6 +467,35 @@ module Aws ).to eq('us-east-1') end + it 'emits correct UserAgent metrics during STS calls for :source_profile from assume_role_web_identity' do + assume_role_web_identity_stub( + 'arn:aws:iam::123456789012:role/foo', + 'AR_AKID_WEB', + 'AR_SECRET', + 'AR_TOKEN' + ) + assume_role_stub( + 'arn:aws:iam::123456789012:role/bar', + 'AR_AKID_WEB', # from web_only + 'AR_AKID', + 'AR_SECRET', + 'AR_TOKEN' + ) + expect_any_instance_of(STS::Client).to receive(:assume_role_with_web_identity).and_wrap_original do |m, *args| + resp = m.call(*args) + expect(metrics_from_user_agent_header(resp)).to include('o', 'q') + resp + end + expect_any_instance_of(STS::Client).to receive(:assume_role).and_wrap_original do |m, *args| + resp = m.call(*args) + expect(metrics_from_user_agent_header(resp)).to include('o', 'q', 'k') + resp + end + ApiHelper.sample_rest_xml::Client.new( + profile: 'ar_web_src', region: 'us-east-1' + ) + end + it 'supports :source_profile from process credentials' do assume_role_stub( 'arn:aws:iam::123456789012:role/foo', @@ -387,25 +511,29 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('AK_PROC1') + expect(metric_values(client.config.credentials.metrics)).to include('o', 'v', 'w', 'i') end - it 'supports :source_profile from sso credentials' do - expect(SSOCredentials).to receive(:new).with( - sso_start_url: 'START_URL', - sso_region: 'us-east-1', - sso_account_id: 'SSO_ACCOUNT_ID', - sso_role_name: 'SSO_ROLE_NAME', - sso_session: 'sso-test-session' - ).and_return( - double( - 'SSOCreds', - set?: true, - credentials: Credentials.new('SSO_AKID', 'sak') - ) + it 'emits correct UserAgent metrics during STS calls for :source_profile from process credentials' do + assume_role_stub( + 'arn:aws:iam::123456789012:role/foo', + 'AK_PROC1', + 'AK_PROC1', + 'SECRET_AK_PROC1', + 'TOKEN_PROC1' + ) + expect_any_instance_of(STS::Client).to receive(:assume_role).and_wrap_original do |m, *args| + resp = m.call(*args) + expect(metrics_from_user_agent_header(resp)).to include('o', 'v', 'w') + resp + end + ApiHelper.sample_rest_xml::Client.new( + profile: 'creds_from_sc_process', region: 'us-east-1' ) + end - allow(SSOTokenProvider).to receive(:new) - .and_return(double('SSOToken', set?: true)) + it 'supports :source_profile from sso credentials' do + sso_stub assume_role_stub( 'arn:aws:iam::123456789012:role/foo', @@ -421,6 +549,77 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('AR_AKID') + expect(metric_values(client.config.credentials.metrics)).to include('o', 'r', 's', 'i') + end + + it 'emits correct UserAgent metrics during service calls for :source_profile from sso credentials' do + sso_stub + + assume_role_stub( + 'arn:aws:iam::123456789012:role/foo', + 'SSO_AKID', + 'AR_AKID', + 'SECRET_AK', + 'TOKEN' + ) + expect_any_instance_of(SSO::Client).to receive(:get_role_credentials).and_wrap_original do |m, *args| + resp = m.call(*args) + expect(metrics_from_user_agent_header(resp)).to include('o', 'r') + resp + end + expect_any_instance_of(STS::Client).to receive(:assume_role).and_wrap_original do |m, *args| + resp = m.call(*args) + expect(metrics_from_user_agent_header(resp)).to include('o', 'r', 's') + resp + end + ApiHelper.sample_rest_xml::Client.new( + profile: 'ar_sso_src', region: 'us-east-1' + ) + end + + it 'supports :source_profile from legacy sso credentials' do + legacy_sso_stub + + assume_role_stub( + 'arn:aws:iam::123456789012:role/foo', + 'SSO_AKID', + 'AR_AKID', + 'SECRET_AK', + 'TOKEN' + ) + + client = ApiHelper.sample_rest_xml::Client.new( + profile: 'ar_sso_legacy_src', region: 'us-east-1' + ) + expect( + client.config.credentials.credentials.access_key_id + ).to eq('AR_AKID') + expect(metric_values(client.config.credentials.metrics)).to include('o', 't', 'u', 'i') + end + + it 'emits correct UserAgent metrics during STS calls for :source_profile from legacy sso credentials' do + legacy_sso_stub + + assume_role_stub( + 'arn:aws:iam::123456789012:role/foo', + 'SSO_AKID', + 'AR_AKID', + 'SECRET_AK', + 'TOKEN' + ) + expect_any_instance_of(SSO::Client).to receive(:get_role_credentials).and_wrap_original do |m, *args| + resp = m.call(*args) + expect(metrics_from_user_agent_header(resp)).to include('o', 't') + resp + end + expect_any_instance_of(STS::Client).to receive(:assume_role).and_wrap_original do |m, *args| + resp = m.call(*args) + expect(metrics_from_user_agent_header(resp)).to include('o', 't', 'u') + resp + end + ApiHelper.sample_rest_xml::Client.new( + profile: 'ar_sso_legacy_src', region: 'us-east-1' + ) end it 'supports assume role chaining' do @@ -446,6 +645,33 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('AK_2') + expect(metric_values(client.config.credentials.metrics)).to include('o', 'n', 'i') + end + + it 'emits correct UserAgent metrics during STS calls for assume role chaining' do + assume_role_stub( + 'arn:aws:iam::123456789012:role/role_b', + 'ACCESS_KEY_BASE', + 'AK_1', + 'SECRET_AK_1', + 'TOKEN_1' + ) + + assume_role_stub( + 'arn:aws:iam::123456789012:role/role_a', + 'AK_1', + 'AK_2', + 'SECRET_AK_2', + 'TOKEN_2' + ) + allow_any_instance_of(STS::Client).to receive(:assume_role).and_wrap_original do |m, *args| + resp = m.call(*args) + expect(metrics_from_user_agent_header(resp)).to include('o', 'n') + resp + end + ApiHelper.sample_rest_xml::Client.new( + profile: 'assume_role_chain_b', region: 'us-east-1' + ) end it 'uses source credentials when source and static are both set' do @@ -463,6 +689,7 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('AK_2') + expect(metric_values(client.config.credentials.metrics)).to include('o', 'n', 'i') end it 'uses static credentials when the profile self references' do @@ -480,6 +707,7 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('AK_2') + expect(metric_values(client.config.credentials.metrics)).to include('o', 'n', 'i') end it 'raises if there is a loop in chained profiles' do @@ -490,7 +718,6 @@ module Aws end.to raise_error(Errors::SourceProfileCircularReferenceError) end - it 'raises if credential_source is present but invalid' do expect do ApiHelper.sample_rest_xml::Client.new( @@ -521,6 +748,7 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('AR_AKID') + expect(metric_values(client.config.credentials.metrics)).to include('o', 'n', 'i') end it 'will then try to assume a role from shared config' do @@ -537,6 +765,7 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('AR_AKID') + expect(metric_values(client.config.credentials.metrics)).to include('o', 'n', 'i') end it 'assumes a role from config using source in shared credentials' do @@ -553,6 +782,7 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('AR_AKID') + expect(metric_values(client.config.credentials.metrics)).to include('o', 'n', 'i') end it 'allows region to be resolved when unspecified' do @@ -573,6 +803,7 @@ module Aws expect( credentials.credentials.access_key_id ).to eq('AR_AKID') + expect(metric_values(credentials.metrics)).to include('o', 'n', 'i') end end @@ -617,6 +848,52 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('AR_AKID') + expect(metric_values(client.config.credentials.metrics)).to include('p', '0', 'i') + end + + it 'emits correct UserAgent metrics during STS calls for EC2 Instance Metadata as a source' do + allow(InstanceProfileCredentials).to receive(:new).and_call_original + + profile = 'ar_ec2_src' + resp = <<-JSON.strip + { + "Code" : "Success", + "LastUpdated" : "2013-11-22T20:03:48Z", + "Type" : "AWS-HMAC", + "AccessKeyId" : "ACCESS_KEY_EC2", + "SecretAccessKey" : "secret", + "Token" : "session-token", + "Expiration" : "#{(Time.now.utc + 3600).strftime('%Y-%m-%dT%H:%M:%SZ')}" + } + JSON + assume_role_stub( + 'arn:aws:iam::123456789012:role/foo', + 'ACCESS_KEY_EC2', + 'AR_AKID', + 'AR_SECRET', + 'AR_TOKEN' + ) + stub_request(:put, 'http://169.254.169.254/latest/api/token') + .to_return( + status: 200, + body: "my-token\n", + headers: { 'x-aws-ec2-metadata-token-ttl-seconds' => '21600' } + ) + stub_request(:get, 'http://169.254.169.254/latest/meta-data/iam/security-credentials/') + .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) + .to_return(status: 200, body: "profile-name\n") + stub_request(:get, 'http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name') + .with(headers: { 'x-aws-ec2-metadata-token' => 'my-token' }) + .to_return(status: 200, body: resp) + expect_any_instance_of(STS::Client).to receive(:assume_role).and_wrap_original do |m, *args| + resp = m.call(*args) + expect(metrics_from_user_agent_header(resp)).to include('p', '0') + resp + end + ApiHelper.sample_rest_xml::Client.new( + profile: profile, + region: 'us-east-1' + ) end it 'can assume a role with ECS Credentials as a source' do @@ -631,8 +908,7 @@ module Aws "Expiration" : "#{(Time.now.utc + 3600).strftime('%Y-%m-%dT%H:%M:%SZ')}" } JSON - stub_const('ENV', - 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI' => path) + stub_const('ENV', 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI' => path) stub_request(:get, "http://169.254.170.2#{path}") .to_return(status: 200, body: resp) assume_role_stub( @@ -649,6 +925,40 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('AR_AKID') + expect(metric_values(client.config.credentials.metrics)).to include('p', 'z', 'i') + end + + it 'emits correct UserAgent metrics during STS calls for ECS Credentials as a source' do + profile = 'ar_ecs_src' + path = '/latest/credentials?id=foobarbaz' + resp = <<-JSON.strip + { + "RoleArn" : "arn:aws:iam::123456789012:role/BarFooRole", + "AccessKeyId" : "ACCESS_KEY_ECS", + "SecretAccessKey" : "secret", + "Token" : "session-token", + "Expiration" : "#{(Time.now.utc + 3600).strftime('%Y-%m-%dT%H:%M:%SZ')}" + } + JSON + stub_const('ENV', 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI' => path) + stub_request(:get, "http://169.254.170.2#{path}") + .to_return(status: 200, body: resp) + assume_role_stub( + 'arn:aws:iam::123456789012:role/foo', + 'ACCESS_KEY_ECS', + 'AR_AKID', + 'AR_SECRET', + 'AR_TOKEN' + ) + expect_any_instance_of(STS::Client).to receive(:assume_role).and_wrap_original do |m, *args| + resp = m.call(*args) + expect(metrics_from_user_agent_header(resp)).to include('p', 'z') + resp + end + ApiHelper.sample_rest_xml::Client.new( + profile: profile, + region: 'us-east-1' + ) end end @@ -678,6 +988,7 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('ACCESS_DIRECT') + expect(metric_values(client.config.credentials.metrics)).to include('n') end it 'prefers ENV credentials over shared config when profile not set' do @@ -692,6 +1003,7 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('AKID_ENV_STUB') + expect(metric_values(client.config.credentials.metrics)).to include('g') end it 'prefers config from profile over ENV credentials when profile is set on client' do @@ -706,6 +1018,7 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('ACCESS_KEY_1') + expect(metric_values(client.config.credentials.metrics)).to include('n') end it 'will not load credentials from shared config' do @@ -757,6 +1070,7 @@ module Aws expect( client.config.credentials.credentials.access_key_id ).to eq('akid-md') + expect(metric_values(client.config.credentials.metrics)).to include('0') end end @@ -813,10 +1127,57 @@ def assume_role_web_identity_stub(role_arn, access_key, secret_key, token) RESP end + def sso_stub + token = double('token', token: 'token') + token_provider = double('token_provider', token: token, set?: true) + allow(Aws::SSOTokenProvider).to receive(:new).and_return(token_provider) + stub_request(:get, 'https://portal.sso.us-east-1.amazonaws.com/federation/credentials?account_id=SSO_ACCOUNT_ID&role_name=SSO_ROLE_NAME') + .to_return(body: <<-RESP) + { + "roleCredentials": { + "accessKeyId": "SSO_AKID", + "secretAccessKey": "secret_key", + "sessionToken": "token", + "expiration": #{(Time.now + 3600).to_i * 1000} + } + } + RESP + end + + def mock_sso_cached_token + cached_token = { + 'accessToken' => 'legacy_token', + 'expiresAt' => Time.now + 3600 + } + start_url_sha1 = OpenSSL::Digest::SHA1.hexdigest('START_URL'.encode('utf-8')) + allow(Dir).to receive(:home).and_return('HOME') + path = File.join(Dir.home, '.aws', 'sso', 'cache', "#{start_url_sha1}.json") + + allow(File).to receive(:read).and_call_original + allow(File).to receive(:read).with(path).and_return( + JSON.dump(cached_token) + ) + end + + def legacy_sso_stub + mock_sso_cached_token + sso_stub + end + def stub_token_file(token) allow(File).to receive(:exist?).with('my-token.jwt').and_return(true) allow(File).to receive(:read).and_call_original allow(File).to receive(:read).with('my-token.jwt').and_return(token) end + + def metrics_from_user_agent_header(resp) + header = resp.context.http_request.headers['User-Agent'] + # Parse list of metrics from User-Agent header + header.match(%r{ m/([A-Za-z0-9+-,]+)})[1].split(',') + end + + def metric_values(metrics) + metrics.map { |metric| Aws::Plugins::UserAgent::METRICS[metric] } + end end end diff --git a/gems/aws-sdk-core/spec/aws/plugins/client_metrics_spec.rb b/gems/aws-sdk-core/spec/aws/plugins/client_metrics_spec.rb index bc40fb5d102..a6b46d43f7f 100644 --- a/gems/aws-sdk-core/spec/aws/plugins/client_metrics_spec.rb +++ b/gems/aws-sdk-core/spec/aws/plugins/client_metrics_spec.rb @@ -174,7 +174,6 @@ module Plugins expect(api_call.latency).to be_a_kind_of(Integer) expect(api_call.client_id).to eq('') expect(api_call.region).to eq('us-stubbed-1') - expect(api_call.user_agent).to match(/^aws-sdk-ruby3/) expect(api_call.final_http_status_code).to eq(200) end end @@ -194,7 +193,6 @@ module Plugins expect(attempt.version).to be_a_kind_of(Integer) expect(attempt.fqdn).to eq('s3.us-stubbed-1.amazonaws.com') expect(attempt.region).to eq('us-stubbed-1') - expect(attempt.user_agent).to match(/^aws-sdk-ruby3/) expect(attempt.access_key).to eq('stubbed-akid') expect(attempt.http_status_code).to eq(200) expect(attempt.request_latency).to be_a_kind_of(Integer) diff --git a/gems/aws-sdk-core/spec/aws/plugins/sign_spec.rb b/gems/aws-sdk-core/spec/aws/plugins/sign_spec.rb index 18df297647c..2c456b67e5d 100644 --- a/gems/aws-sdk-core/spec/aws/plugins/sign_spec.rb +++ b/gems/aws-sdk-core/spec/aws/plugins/sign_spec.rb @@ -99,7 +99,7 @@ def call(context) end it 'raises an error when attempting to sign a request w/out credentials' do - client = TestClient.new(client_options.merge(credentials: nil) ) + client = TestClient.new(client_options.merge(credentials: nil)) expect { client.operation }.to raise_error(Errors::MissingCredentialsError) @@ -202,6 +202,13 @@ def call(context) to eq (now.utc + 1000).strftime("%Y%m%dT%H%M%SZ") end end + + it 'retrieves the credential provider metrics' do + creds = Aws::Credentials.new('akid', 'secret') + client = TestClient.new(client_options.merge(credentials: creds)) + expect(creds).to receive(:metrics) + client.operation + end end context 'sigv4a' do diff --git a/gems/aws-sdk-core/spec/fixtures/credentials/mock_shared_config b/gems/aws-sdk-core/spec/fixtures/credentials/mock_shared_config index 9c19c758d79..7b2ee6dc0a1 100644 --- a/gems/aws-sdk-core/spec/fixtures/credentials/mock_shared_config +++ b/gems/aws-sdk-core/spec/fixtures/credentials/mock_shared_config @@ -173,6 +173,10 @@ sso_account_id = 123456789012 source_profile = sso_creds role_arn = arn:aws:iam::123456789012:role/bar +[profile ar_sso_legacy_src] +source_profile = sso_creds_legacy +role_arn = arn:aws:iam::123456789012:role/bar + [profile sso_creds_session_with_quotes] sso_account_id = SSO_ACCOUNT_ID sso_role_name = SSO_ROLE_NAME diff --git a/gems/aws-sdk-core/spec/sigv4_helper.rb b/gems/aws-sdk-core/spec/sigv4_helper.rb index 62236a6b73a..c909eae3bc0 100644 --- a/gems/aws-sdk-core/spec/sigv4_helper.rb +++ b/gems/aws-sdk-core/spec/sigv4_helper.rb @@ -20,6 +20,7 @@ def expect_auth(auth_scheme, region: nil, credentials: nil) expect(Aws::Sigv4::Signer).to receive(:new) .with(hash_including(signing_algorithm: :sigv4a, region: region)) .and_return(signer) + expect(signer).to receive(:credentials_provider).and_return(credentials) end m.call(*args)