From b152131e17f8a64fd37c4f458ae9c942c90e3f46 Mon Sep 17 00:00:00 2001 From: Duncan Boynton Date: Tue, 30 Jul 2024 21:21:01 -0700 Subject: [PATCH 1/8] implemented refreshing credentials in SharedCredentials --- gems/aws-sdk-core/CHANGELOG.md | 2 + .../lib/aws-sdk-core/shared_credentials.rb | 98 ++++++++++++------ .../spec/aws/shared_credentials_spec.rb | 99 +++++++++++++------ 3 files changed, 140 insertions(+), 59 deletions(-) diff --git a/gems/aws-sdk-core/CHANGELOG.md b/gems/aws-sdk-core/CHANGELOG.md index 6987342bafe..0f48a9c4433 100644 --- a/gems/aws-sdk-core/CHANGELOG.md +++ b/gems/aws-sdk-core/CHANGELOG.md @@ -1,6 +1,8 @@ Unreleased Changes ------------------ +* Feature - Added support for opt-in credential refreshing in `SharedCredentials`. This allows users to enable or disable automatic credential refreshing by specifying the `:enable_refresh` option. + 3.201.3 (2024-07-23) ------------------ 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 836dd3f6273..af3b3385166 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 @@ -1,17 +1,18 @@ # frozen_string_literal: true require_relative 'ini_parser' +require_relative 'refreshing_credentials' module Aws class SharedCredentials - include CredentialProvider + include RefreshingCredentials # @api private KEY_MAP = { 'aws_access_key_id' => 'access_key_id', 'aws_secret_access_key' => 'secret_access_key', - 'aws_session_token' => 'session_token', + 'aws_session_token' => 'session_token' } # Constructs a new SharedCredentials object. This will load static @@ -25,37 +26,37 @@ class SharedCredentials # You may access the resolved credentials through # `client.config.credentials`. # - # @option [String] :path Path to the shared file. Defaults + # @option [String] :path Path to the shared file. Defaults # to "#{Dir.home}/.aws/credentials". # # @option [String] :profile_name Defaults to 'default' or # `ENV['AWS_PROFILE']`. # + # @option [Integer] :refresh_interval The duration (in seconds) before the + # credentials are considered near expiration and should be refreshed. + # Defaults to 10 minutes (600 seconds). + # + # @option [Boolean] :enable_refresh If true, the credentials will be automatically + # refreshed when they are near expiration. Defaults to false. + # def initialize(options = {}) - shared_config = Aws.shared_config - @path = options[:path] - @path ||= shared_config.credentials_path - @profile_name = options[:profile_name] - @profile_name ||= ENV['AWS_PROFILE'] - @profile_name ||= shared_config.profile_name - if @path && @path == shared_config.credentials_path - @credentials = shared_config.credentials(profile: @profile_name) - else - config = SharedConfig.new( - credentials_path: @path, - profile_name: @profile_name - ) - @credentials = config.credentials(profile: @profile_name) - end + @enable_refresh = options[:enable_refresh] || false # Refresh disabled by default + @refresh_interval = options[:refresh_interval] || 10 * 60 # Default refresh interval: 10 minutes + @path = options[:path] || Aws.shared_config.credentials_path + @profile_name = options[:profile_name] || ENV['AWS_PROFILE'] || Aws.shared_config.profile_name + + super(options) # This will call RefreshingCredentials#initialize + + refresh # Initially load credentials end - # @return [String] + # @return [String] The path to the credentials file attr_reader :path - # @return [String] + # @return [String] The name of the profile being used attr_reader :profile_name - # @return [Credentials] + # @return [Credentials] The loaded credentials attr_reader :credentials # @api private @@ -63,19 +64,58 @@ def inspect parts = [ self.class.name, "profile_name=#{profile_name.inspect}", - "path=#{path.inspect}", + "path=#{path.inspect}" ] "#<#{parts.join(' ')}>" end - # @deprecated This method is no longer used. - # @return [Boolean] Returns `true` if a credential file - # exists and has appropriate read permissions at {#path}. - # @note This method does not indicate if the file found at {#path} - # will be parsable, only if it can be read. - def loadable? - !path.nil? && File.exist?(path) && File.readable?(path) + # Refreshes credentials from the shared credentials file. + def refresh + shared_config = Aws.shared_config + + if @path && @path == shared_config.credentials_path + # Use credentials from shared config if paths match + @credentials = shared_config.credentials(profile: @profile_name) + else + # Load credentials from specified path and profile + config = SharedConfig.new( + credentials_path: @path, + profile_name: @profile_name + ) + @credentials = config.credentials(profile: @profile_name) + end + + if @credentials && @credentials.set? + # Credentials successfully loaded and set + @access_key_id = @credentials.access_key_id + @secret_access_key = @credentials.secret_access_key + @session_token = @credentials.session_token + @expiration = Time.now + @refresh_interval + else + # Incomplete or missing credentials + @credentials = nil + @access_key_id = nil + @secret_access_key = nil + @session_token = nil + @expiration = nil + end + end + + # For testing purposes to check the refresh logic + # This method triggers the internal refresh logic as if the credentials + # were near expiration. + def force_refresh_check + refresh_if_near_expiration! if @enable_refresh end + private + + def sync_expiration_length + @refresh_interval || self.class::SYNC_EXPIRATION_LENGTH + end + + def async_expiration_length + @refresh_interval || self.class::ASYNC_EXPIRATION_LENGTH + end end end diff --git a/gems/aws-sdk-core/spec/aws/shared_credentials_spec.rb b/gems/aws-sdk-core/spec/aws/shared_credentials_spec.rb index f05d08eea77..ef5a27a26cc 100644 --- a/gems/aws-sdk-core/spec/aws/shared_credentials_spec.rb +++ b/gems/aws-sdk-core/spec/aws/shared_credentials_spec.rb @@ -4,24 +4,23 @@ module Aws describe SharedCredentials do - before(:each) do stub_const('ENV', {}) allow(Dir).to receive(:home).and_raise(ArgumentError) end - let(:mock_credential_file) { + let(:mock_credential_file) do File.expand_path(File.join(File.dirname(__FILE__), - '..', 'fixtures', 'credentials', 'mock_shared_credentials')) - } + '..', 'fixtures', 'credentials', 'mock_shared_credentials')) + end - let(:mock_config_file) { + let(:mock_config_file) do File.expand_path(File.join(File.dirname(__FILE__), - '..', 'fixtures', 'credentials', 'mock_shared_config')) - } + '..', 'fixtures', 'credentials', 'mock_shared_config')) + end it 'reads the correct default credentials from a credentials file' do - creds = SharedCredentials.new(path:mock_credential_file).credentials + creds = SharedCredentials.new(path: mock_credential_file).credentials expect(creds.access_key_id).to eq('ACCESS_KEY_0') expect(creds.secret_access_key).to eq('SECRET_KEY_0') expect(creds.session_token).to eq('TOKEN_0') @@ -29,7 +28,7 @@ module Aws it 'supports fetching profiles from ENV' do stub_const('ENV', { 'AWS_PROFILE' => 'barprofile' }) - creds = SharedCredentials.new(path:mock_credential_file).credentials + creds = SharedCredentials.new(path: mock_credential_file).credentials expect(creds.access_key_id).to eq('ACCESS_KEY_2') expect(creds.secret_access_key).to eq('SECRET_KEY_2') expect(creds.session_token).to eq('TOKEN_2') @@ -39,34 +38,35 @@ module Aws stub_const('ENV', { 'AWS_PROFILE' => 'barporfile' }) creds = SharedCredentials.new( path: mock_credential_file, - profile_name: 'fooprofile').credentials + profile_name: 'fooprofile' + ).credentials expect(creds.access_key_id).to eq('ACCESS_KEY_1') expect(creds.secret_access_key).to eq('SECRET_KEY_1') expect(creds.session_token).to eq('TOKEN_1') end it 'raises when a path does not exist' do - msg = /^Profile `doesnotexist' not found in \/no\/file\/here/ - expect { + msg = %r{^Profile `doesnotexist' not found in /no/file/here} + expect do SharedCredentials.new( path: '/no/file/here', profile_name: 'doesnotexist' ) - }.to raise_error(Errors::NoSuchProfileError, msg) + end.to raise_error(Errors::NoSuchProfileError, msg) end it 'raises when a profile does not exist' do msg = /^Profile `doesnotexist' not found in .+mock_shared_credentials/ - expect { + expect do SharedCredentials.new( path: mock_credential_file, profile_name: 'doesnotexist' ) - }.to raise_error(Errors::NoSuchProfileError, msg) + end.to raise_error(Errors::NoSuchProfileError, msg) end it 'is set when credentials is valid' do - creds = SharedCredentials.new(path:mock_credential_file) + creds = SharedCredentials.new(path: mock_credential_file) expect(creds.set?).to eq(true) end @@ -79,13 +79,13 @@ module Aws end it 'supports inline comments with the profile' do - file = <<-FILE -[default] # comment -aws_access_key_id=commented-akid -aws_secret_access_key=commented-secret + file = <<~FILE + [default] # comment + aws_access_key_id=commented-akid + aws_secret_access_key=commented-secret FILE allow(File).to receive(:read).and_return(file) - creds = SharedCredentials.new(path:mock_credential_file).credentials + creds = SharedCredentials.new(path: mock_credential_file).credentials expect(creds.access_key_id).to eq('commented-akid') expect(creds.secret_access_key).to eq('commented-secret') end @@ -95,11 +95,11 @@ module Aws config_enabled: true, credentials_path: mock_credential_file, config_path: mock_config_file, - profile_name: "creds_from_cfg" + profile_name: 'creds_from_cfg' ) creds = SharedCredentials.new.credentials - expect(creds.access_key_id).to eq("ACCESS_KEY_SC0") - expect(creds.secret_access_key).to eq("SECRET_KEY_SC0") + expect(creds.access_key_id).to eq('ACCESS_KEY_SC0') + expect(creds.secret_access_key).to eq('SECRET_KEY_SC0') end it 'properly falls back when credentials incomplete' do @@ -107,11 +107,11 @@ module Aws config_enabled: true, credentials_path: mock_credential_file, config_path: mock_config_file, - profile_name: "incomplete_cred" + profile_name: 'incomplete_cred' ) creds = SharedCredentials.new.credentials - expect(creds.access_key_id).to eq("ACCESS_KEY_SC1") - expect(creds.secret_access_key).to eq("SECRET_KEY_SC1") + expect(creds.access_key_id).to eq('ACCESS_KEY_SC1') + expect(creds.secret_access_key).to eq('SECRET_KEY_SC1') end it 'will source from credentials over config' do @@ -121,8 +121,8 @@ module Aws config_path: mock_config_file ) creds = SharedCredentials.new.credentials - expect(creds.access_key_id).to eq("ACCESS_KEY_0") - expect(creds.secret_access_key).to eq("SECRET_KEY_0") + expect(creds.access_key_id).to eq('ACCESS_KEY_0') + expect(creds.secret_access_key).to eq('SECRET_KEY_0') end it 'will ignore incomplete credentials' do @@ -130,11 +130,50 @@ module Aws config_enabled: true, credentials_path: mock_credential_file, config_path: mock_config_file, - profile_name: "incomplete_cfg" + profile_name: 'incomplete_cfg' ) creds = SharedCredentials.new.credentials expect(creds).to eq(nil) end + # Refreshing credentials tests + + it 'refreshes credentials when near expiration' do + refresh_interval = 2 # Short interval for testing + creds = SharedCredentials.new(path: mock_credential_file, refresh_interval: refresh_interval, + enable_refresh: true) + + initial_access_key = creds.credentials.access_key_id + allow(creds).to receive(:refresh).and_call_original + + # Simulate time passing to near expiration + sleep(refresh_interval + 1) + + creds.force_refresh_check + refreshed_access_key = creds.credentials.access_key_id + expect(creds).to have_received(:refresh).at_least(:once) + expect(refreshed_access_key).to eq('ACCESS_KEY_0') + end + + it 'does not refresh credentials if not near expiration' do + refresh_interval = 10 # Longer interval for testing + creds = SharedCredentials.new(path: mock_credential_file, refresh_interval: refresh_interval, + enable_refresh: true) + + allow(creds).to receive(:refresh).and_call_original + + # Check credentials before expiration + initial_access_key = creds.credentials.access_key_id + + sleep(2) + + creds.force_refresh_check + + # Check credentials to ensure they are not refreshed + refreshed_access_key = creds.credentials.access_key_id + + expect(creds).to have_received(:refresh).once # Ensure refresh was called only once (during initialization) + expect(initial_access_key).to eq(refreshed_access_key) + end end end From 5eb4ab90c48771a329da1b28b467326e379b5047 Mon Sep 17 00:00:00 2001 From: Duncan Boynton Date: Mon, 5 Aug 2024 09:42:48 -0700 Subject: [PATCH 2/8] most comments addressed --- .../lib/aws-sdk-core/shared_credentials.rb | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) 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 af3b3385166..4e1f47fcd5f 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 @@ -69,6 +69,7 @@ def inspect "#<#{parts.join(' ')}>" end + # Refreshes credentials from the shared credentials file. # Refreshes credentials from the shared credentials file. def refresh shared_config = Aws.shared_config @@ -85,20 +86,8 @@ def refresh @credentials = config.credentials(profile: @profile_name) end - if @credentials && @credentials.set? - # Credentials successfully loaded and set - @access_key_id = @credentials.access_key_id - @secret_access_key = @credentials.secret_access_key - @session_token = @credentials.session_token - @expiration = Time.now + @refresh_interval - else - # Incomplete or missing credentials - @credentials = nil - @access_key_id = nil - @secret_access_key = nil - @session_token = nil - @expiration = nil - end + # Set expiration time if credentials are present + @expiration = @credentials ? (Time.now + @refresh_interval) : nil end # For testing purposes to check the refresh logic From 50a6b9ca6709b79eb95961e8d16da1d07b32bea6 Mon Sep 17 00:00:00 2001 From: Duncan Boynton Date: Mon, 5 Aug 2024 09:44:00 -0700 Subject: [PATCH 3/8] remove force_refresh_check --- .../lib/aws-sdk-core/shared_credentials.rb | 10 +--------- .../spec/aws/shared_credentials_spec.rb | 13 ++++++++----- 2 files changed, 9 insertions(+), 14 deletions(-) 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 4e1f47fcd5f..6b339375f78 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 @@ -69,7 +69,6 @@ def inspect "#<#{parts.join(' ')}>" end - # Refreshes credentials from the shared credentials file. # Refreshes credentials from the shared credentials file. def refresh shared_config = Aws.shared_config @@ -90,13 +89,6 @@ def refresh @expiration = @credentials ? (Time.now + @refresh_interval) : nil end - # For testing purposes to check the refresh logic - # This method triggers the internal refresh logic as if the credentials - # were near expiration. - def force_refresh_check - refresh_if_near_expiration! if @enable_refresh - end - private def sync_expiration_length @@ -107,4 +99,4 @@ def async_expiration_length @refresh_interval || self.class::ASYNC_EXPIRATION_LENGTH end end -end +end \ No newline at end of file diff --git a/gems/aws-sdk-core/spec/aws/shared_credentials_spec.rb b/gems/aws-sdk-core/spec/aws/shared_credentials_spec.rb index ef5a27a26cc..e85e50729b1 100644 --- a/gems/aws-sdk-core/spec/aws/shared_credentials_spec.rb +++ b/gems/aws-sdk-core/spec/aws/shared_credentials_spec.rb @@ -146,10 +146,10 @@ module Aws initial_access_key = creds.credentials.access_key_id allow(creds).to receive(:refresh).and_call_original - # Simulate time passing to near expiration - sleep(refresh_interval + 1) + # Mock expiration logic by setting expiration time in the past + allow(creds).to receive(:near_expiration?).and_return(true) - creds.force_refresh_check + creds.refresh! refreshed_access_key = creds.credentials.access_key_id expect(creds).to have_received(:refresh).at_least(:once) expect(refreshed_access_key).to eq('ACCESS_KEY_0') @@ -167,7 +167,10 @@ module Aws sleep(2) - creds.force_refresh_check + # Mock expiration logic by ensuring it's not near expiration + allow(creds).to receive(:near_expiration?).and_return(false) + + creds.refresh! # Check credentials to ensure they are not refreshed refreshed_access_key = creds.credentials.access_key_id @@ -176,4 +179,4 @@ module Aws expect(initial_access_key).to eq(refreshed_access_key) end end -end +end \ No newline at end of file From 6e6ec7330915673ae64da9f6f0f256adf7c7b4f1 Mon Sep 17 00:00:00 2001 From: Duncan Boynton Date: Mon, 5 Aug 2024 09:53:25 -0700 Subject: [PATCH 4/8] fixing enablement logic --- .../lib/aws-sdk-core/shared_credentials.rb | 4 ++++ .../spec/aws/shared_credentials_spec.rb | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) 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 6b339375f78..f322ca05cb1 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 @@ -91,6 +91,10 @@ def refresh private + def refresh_if_necessary + refresh! if @enable_refresh && near_expiration? + end + def sync_expiration_length @refresh_interval || self.class::SYNC_EXPIRATION_LENGTH end diff --git a/gems/aws-sdk-core/spec/aws/shared_credentials_spec.rb b/gems/aws-sdk-core/spec/aws/shared_credentials_spec.rb index e85e50729b1..7159abb82ee 100644 --- a/gems/aws-sdk-core/spec/aws/shared_credentials_spec.rb +++ b/gems/aws-sdk-core/spec/aws/shared_credentials_spec.rb @@ -142,8 +142,6 @@ module Aws refresh_interval = 2 # Short interval for testing creds = SharedCredentials.new(path: mock_credential_file, refresh_interval: refresh_interval, enable_refresh: true) - - initial_access_key = creds.credentials.access_key_id allow(creds).to receive(:refresh).and_call_original # Mock expiration logic by setting expiration time in the past @@ -165,8 +163,6 @@ module Aws # Check credentials before expiration initial_access_key = creds.credentials.access_key_id - sleep(2) - # Mock expiration logic by ensuring it's not near expiration allow(creds).to receive(:near_expiration?).and_return(false) @@ -178,5 +174,19 @@ module Aws expect(creds).to have_received(:refresh).once # Ensure refresh was called only once (during initialization) expect(initial_access_key).to eq(refreshed_access_key) end + + it 'does not refresh credentials when enable_refresh is false' do + refresh_interval = 2 + creds = SharedCredentials.new(path: mock_credential_file, refresh_interval: refresh_interval, + enable_refresh: false) + + allow(creds).to receive(:refresh).and_call_original + + # Mock expiration logic by setting expiration time in the past + allow(creds).to receive(:near_expiration?).and_return(true) + + creds.refresh! + expect(creds).to have_received(:refresh).once # Ensure refresh was called only once (during initialization) + end end end \ No newline at end of file From 4cab1bcd6fdb4609715086ee8ccbdb4c188e8144 Mon Sep 17 00:00:00 2001 From: Duncan Boynton Date: Mon, 5 Aug 2024 10:00:03 -0700 Subject: [PATCH 5/8] removed redundant refresh call --- gems/aws-sdk-core/lib/aws-sdk-core/shared_credentials.rb | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) 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 f322ca05cb1..d449547435e 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 @@ -45,9 +45,7 @@ def initialize(options = {}) @path = options[:path] || Aws.shared_config.credentials_path @profile_name = options[:profile_name] || ENV['AWS_PROFILE'] || Aws.shared_config.profile_name - super(options) # This will call RefreshingCredentials#initialize - - refresh # Initially load credentials + super(options) end # @return [String] The path to the credentials file @@ -91,10 +89,6 @@ def refresh private - def refresh_if_necessary - refresh! if @enable_refresh && near_expiration? - end - def sync_expiration_length @refresh_interval || self.class::SYNC_EXPIRATION_LENGTH end From bc66d27770144b85e4f95a80c92c4eacffdfbd66 Mon Sep 17 00:00:00 2001 From: Duncan Boynton Date: Mon, 5 Aug 2024 10:02:52 -0700 Subject: [PATCH 6/8] actually fixed enablement refresh logic --- gems/aws-sdk-core/lib/aws-sdk-core/shared_credentials.rb | 4 ++++ 1 file changed, 4 insertions(+) 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 d449547435e..a50887c4e02 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 @@ -89,6 +89,10 @@ def refresh private + def refresh_if_necessary + refresh! if @enable_refresh && near_expiration? + end + def sync_expiration_length @refresh_interval || self.class::SYNC_EXPIRATION_LENGTH end From 0591535ebea2e3c3a72c919adc340721b66b3ea5 Mon Sep 17 00:00:00 2001 From: Duncan Boynton Date: Mon, 5 Aug 2024 10:16:34 -0700 Subject: [PATCH 7/8] tying together refresh being enabled and desired interval --- gems/aws-sdk-core/lib/aws-sdk-core/shared_credentials.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 a50887c4e02..37a319144ae 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 @@ -55,7 +55,10 @@ def initialize(options = {}) attr_reader :profile_name # @return [Credentials] The loaded credentials - attr_reader :credentials + def credentials + refresh_if_necessary + @credentials + end # @api private def inspect @@ -90,7 +93,7 @@ def refresh private def refresh_if_necessary - refresh! if @enable_refresh && near_expiration? + refresh! if @enable_refresh && near_expiration?(@refresh_interval) end def sync_expiration_length From 60a38be40a70aa6c2f8dd000421a3107cde7323d Mon Sep 17 00:00:00 2001 From: Duncan Boynton Date: Wed, 7 Aug 2024 09:59:17 -0700 Subject: [PATCH 8/8] fixing credential refreshing + test --- .../lib/aws-sdk-core/shared_credentials.rb | 77 ++++++++----------- .../spec/aws/shared_credentials_spec.rb | 18 ++--- 2 files changed, 39 insertions(+), 56 deletions(-) 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 37a319144ae..fb6d1a0f158 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 @@ -8,13 +8,6 @@ class SharedCredentials include CredentialProvider include RefreshingCredentials - # @api private - KEY_MAP = { - 'aws_access_key_id' => 'access_key_id', - 'aws_secret_access_key' => 'secret_access_key', - 'aws_session_token' => 'session_token' - } - # Constructs a new SharedCredentials object. This will load static # (access_key_id, secret_access_key and session_token) AWS access # credentials from an ini file, which supports profiles. The default @@ -26,26 +19,26 @@ class SharedCredentials # You may access the resolved credentials through # `client.config.credentials`. # - # @option [String] :path Path to the shared file. Defaults + # @option [String] :path Path to the shared file. Defaults # to "#{Dir.home}/.aws/credentials". # # @option [String] :profile_name Defaults to 'default' or # `ENV['AWS_PROFILE']`. # - # @option [Integer] :refresh_interval The duration (in seconds) before the - # credentials are considered near expiration and should be refreshed. - # Defaults to 10 minutes (600 seconds). - # - # @option [Boolean] :enable_refresh If true, the credentials will be automatically - # refreshed when they are near expiration. Defaults to false. - # def initialize(options = {}) + shared_config = Aws.shared_config @enable_refresh = options[:enable_refresh] || false # Refresh disabled by default @refresh_interval = options[:refresh_interval] || 10 * 60 # Default refresh interval: 10 minutes - @path = options[:path] || Aws.shared_config.credentials_path - @profile_name = options[:profile_name] || ENV['AWS_PROFILE'] || Aws.shared_config.profile_name + @path = options[:path] || shared_config.credentials_path + @profile_name = options[:profile_name] || ENV['AWS_PROFILE'] || shared_config.profile_name + @next_refresh_time = nil # Internal variable to track refresh timing + + # Initialize the mutex for thread-safety + super(options) # Call super to initialize RefreshingCredentials - super(options) + @credentials = load_credentials + + refresh if @enable_refresh end # @return [String] The path to the credentials file @@ -54,10 +47,12 @@ def initialize(options = {}) # @return [String] The name of the profile being used attr_reader :profile_name - # @return [Credentials] The loaded credentials - def credentials - refresh_if_necessary - @credentials + # Override of near_expiration to decide when to refresh + def near_expiration?(expiration_length = 0) + return false unless @enable_refresh + + # Consider "near expiration" if the internal next_refresh_time has passed + @next_refresh_time.nil? || @next_refresh_time <= Time.now + expiration_length end # @api private @@ -70,38 +65,26 @@ def inspect "#<#{parts.join(' ')}>" end - # Refreshes credentials from the shared credentials file. def refresh - shared_config = Aws.shared_config + @credentials = load_credentials + + # Set next refresh time if credentials are present + # This is for internal tracking, and won't affect the actual credentials' expiration + @next_refresh_time = @credentials ? (Time.now + @refresh_interval) : nil + end - if @path && @path == shared_config.credentials_path - # Use credentials from shared config if paths match - @credentials = shared_config.credentials(profile: @profile_name) + private + + def load_credentials + if @path && @path == Aws.shared_config.credentials_path + Aws.shared_config.credentials(profile: @profile_name) else - # Load credentials from specified path and profile config = SharedConfig.new( credentials_path: @path, profile_name: @profile_name ) - @credentials = config.credentials(profile: @profile_name) + config.credentials(profile: @profile_name) end - - # Set expiration time if credentials are present - @expiration = @credentials ? (Time.now + @refresh_interval) : nil - end - - private - - def refresh_if_necessary - refresh! if @enable_refresh && near_expiration?(@refresh_interval) - end - - def sync_expiration_length - @refresh_interval || self.class::SYNC_EXPIRATION_LENGTH - end - - def async_expiration_length - @refresh_interval || self.class::ASYNC_EXPIRATION_LENGTH end end -end \ No newline at end of file +end diff --git a/gems/aws-sdk-core/spec/aws/shared_credentials_spec.rb b/gems/aws-sdk-core/spec/aws/shared_credentials_spec.rb index 7159abb82ee..320a4798434 100644 --- a/gems/aws-sdk-core/spec/aws/shared_credentials_spec.rb +++ b/gems/aws-sdk-core/spec/aws/shared_credentials_spec.rb @@ -155,23 +155,23 @@ module Aws it 'does not refresh credentials if not near expiration' do refresh_interval = 10 # Longer interval for testing - creds = SharedCredentials.new(path: mock_credential_file, refresh_interval: refresh_interval, - enable_refresh: true) - - allow(creds).to receive(:refresh).and_call_original + creds = SharedCredentials.new( + path: mock_credential_file, + refresh_interval: refresh_interval, + enable_refresh: true + ) - # Check credentials before expiration + # Capture the initial access key initial_access_key = creds.credentials.access_key_id - # Mock expiration logic by ensuring it's not near expiration allow(creds).to receive(:near_expiration?).and_return(false) + # This will force a refresh! Call creds.refresh! - # Check credentials to ensure they are not refreshed refreshed_access_key = creds.credentials.access_key_id - expect(creds).to have_received(:refresh).once # Ensure refresh was called only once (during initialization) + # Check that the access key has not changed, meaning no refresh occurred if not near expiration expect(initial_access_key).to eq(refreshed_access_key) end @@ -189,4 +189,4 @@ module Aws expect(creds).to have_received(:refresh).once # Ensure refresh was called only once (during initialization) end end -end \ No newline at end of file +end