From fc4d2107036d4dbc12027dd110048715134f252e Mon Sep 17 00:00:00 2001 From: Jonathan Eskew Date: Wed, 12 Apr 2017 19:37:28 -0700 Subject: [PATCH] Allow profiles defined in ~/.aws/credentials to assume roles with source profiles defined in ~/.aws/config --- .../shared_ini_file_credentials.js | 38 +++-- lib/node_loader.js | 26 ++-- lib/shared_ini.js | 19 ++- lib/util.js | 17 ++- test/config.spec.coffee | 95 +++++++++--- test/credentials.spec.coffee | 144 ++++++++++++++++-- 6 files changed, 271 insertions(+), 68 deletions(-) diff --git a/lib/credentials/shared_ini_file_credentials.js b/lib/credentials/shared_ini_file_credentials.js index 40b9643743..ecd0c67f78 100644 --- a/lib/credentials/shared_ini_file_credentials.js +++ b/lib/credentials/shared_ini_file_credentials.js @@ -1,11 +1,8 @@ var AWS = require('../core'); var path = require('path'); +var SharedIniFile = require('../shared_ini'); var STS = require('../../clients/sts'); -var configOptInEnv = 'AWS_SDK_LOAD_CONFIG'; -var sharedFileEnv = 'AWS_SHARED_CREDENTIALS_FILE'; -var defaultProfile = 'default'; - /** * Represents credentials loaded from shared credentials file * (defaulting to ~/.aws/credentials or defined by the @@ -57,7 +54,7 @@ AWS.SharedIniFileCredentials = AWS.util.inherit(AWS.Credentials, { options = options || {}; this.filename = options.filename; - this.profile = options.profile || process.env.AWS_PROFILE || defaultProfile; + this.profile = options.profile || process.env.AWS_PROFILE || AWS.util.defaultProfile; this.disableAssumeRole = Boolean(options.disableAssumeRole); this.get(function() {}); }, @@ -76,19 +73,27 @@ AWS.SharedIniFileCredentials = AWS.util.inherit(AWS.Credentials, { refresh: function refresh(callback) { if (!callback) callback = function(err) { if (err) throw err; }; try { - var profile = {}; - if (process.env[configOptInEnv]) { - var config = new AWS.SharedIniFile({ + var profiles = {}; + if (process.env[AWS.util.configOptInEnv]) { + var config = new SharedIniFile({ isConfig: true, - filename: process.env.AWS_CONFIG_FILE + filename: process.env[AWS.util.sharedConfigFileEnv] }); - profile = AWS.util.merge(profile, config.getProfile(this.profile)); + for (var i = 0, availableProfiles = config.getProfiles(); i < availableProfiles.length; i++) { + profiles[availableProfiles[i]] = config.getProfile(availableProfiles[i]); + } } - var creds = new AWS.SharedIniFile({ + var creds = new SharedIniFile({ filename: this.filename || - (process.env[configOptInEnv] && process.env[sharedFileEnv]) + (process.env[AWS.util.configOptInEnv] && process.env[AWS.util.sharedCredentialsFileEnv]) }); - profile = AWS.util.merge(profile, creds.getProfile(this.profile)); + for (var i = 0, availableProfiles = creds.getProfiles(); i < availableProfiles.length; i++) { + profiles[availableProfiles[i]] = AWS.util.merge( + profiles[availableProfiles[i]] || {}, + creds.getProfile(availableProfiles[i]) + ); + } + var profile = profiles[this.profile] || {}; if (Object.keys(profile).length === 0) { throw AWS.util.error( @@ -98,7 +103,7 @@ AWS.SharedIniFileCredentials = AWS.util.inherit(AWS.Credentials, { } if (profile['role_arn']) { - this.loadRoleProfile(creds, profile, callback); + this.loadRoleProfile(profiles, profile, callback); return; } @@ -126,7 +131,8 @@ AWS.SharedIniFileCredentials = AWS.util.inherit(AWS.Credentials, { if (this.disableAssumeRole) { throw AWS.util.error( new Error('Role assumption profiles are disabled. ' + - 'Failed to load profile ' + this.profile), + 'Failed to load profile ' + this.profile + + ' from ' + creds.filename), { code: 'SharedIniFileCredentialsProviderFailure' } ); } @@ -144,7 +150,7 @@ AWS.SharedIniFileCredentials = AWS.util.inherit(AWS.Credentials, { ); } - var sourceProfile = creds.getProfile(sourceProfileName); + var sourceProfile = creds[sourceProfileName]; if (typeof sourceProfile !== 'object') { throw AWS.util.error( diff --git a/lib/node_loader.js b/lib/node_loader.js index b15d91d17e..4d260081ee 100644 --- a/lib/node_loader.js +++ b/lib/node_loader.js @@ -19,9 +19,6 @@ AWS.XML.Parser = require('./xml/node_parser'); // Load Node HTTP client require('./http/node'); -// Load Node shared ini file loader -require('./shared_ini'); - // Load custom credential providers require('./credentials/ec2_metadata_credentials'); require('./credentials/ecs_credentials'); @@ -29,6 +26,8 @@ require('./credentials/environment_credentials'); require('./credentials/file_system_credentials'); require('./credentials/shared_ini_file_credentials'); +var SharedIniFile = require('./shared_ini'); + // Setup default chain providers // If this changes, please update documentation for // AWS.CredentialProviderChain.defaultProviders in @@ -64,15 +63,18 @@ AWS.util.update(AWS.Config.prototype.keys, { region: function() { var env = process.env; var region = env.AWS_REGION || env.AMAZON_REGION; - if (!region && env.AWS_SDK_LOAD_CONFIG) { - var configFile = new AWS.SharedIniFile({ - isConfig: true, - filename: process.env.AWS_CONFIG_FILE - }); - var profile = configFile.getProfile( - env.AWS_PROFILE || AWS.util.defaultProfile - ); - region = profile && profile.region; + if (env[AWS.util.configOptInEnv]) { + var toCheck = [ + {filename: env[AWS.util.sharedCredentialsFileEnv]}, + {isConfig: true, filename: env[AWS.util.sharedConfigFileEnv]} + ]; + while (!region && toCheck.length) { + var configFile = new SharedIniFile(toCheck.shift()); + var profile = configFile.getProfile( + env.AWS_PROFILE || AWS.util.defaultProfile + ); + region = profile && profile.region; + } } return region; } diff --git a/lib/shared_ini.js b/lib/shared_ini.js index 0eae44a3f3..cc1bd84b80 100644 --- a/lib/shared_ini.js +++ b/lib/shared_ini.js @@ -5,18 +5,18 @@ var path = require('path'); /** * @api private */ -AWS.SharedIniFile = AWS.util.inherit({ +module.exports = AWS.util.inherit({ constructor: function SharedIniFile(options) { options = options || {}; - this.filename = options.filename; this.isConfig = options.isConfig === true; + this.filename = options.filename || this.getDefaultFilepath(); }, ensureFileLoaded: function loadFile() { if (!this.parsedContents) { this.parsedContents = AWS.util.ini.parse( - AWS.util.readFileSync(this.filename || this.getDefaultFilepath()) + AWS.util.readFileSync(this.filename) ); } }, @@ -55,5 +55,18 @@ AWS.SharedIniFile = AWS.util.inherit({ 'profile ' + profile : profile; return this.parsedContents[profileIndex]; + }, + + getProfiles: function loadProfileNames() { + this.ensureFileLoaded(); + var isConfig = this.isConfig; + + return Object.keys(this.parsedContents).map(function(profileName) { + if (isConfig) { + return profileName.replace(/^profile\s/, ''); + } + + return profileName; + }); } }); diff --git a/lib/util.js b/lib/util.js index 7ae93ae499..bc1a481443 100644 --- a/lib/util.js +++ b/lib/util.js @@ -897,7 +897,22 @@ var util = { /** * @api private */ - defaultProfile: 'default' + defaultProfile: 'default', + + /** + * @api private + */ + configOptInEnv: 'AWS_SDK_LOAD_CONFIG', + + /** + * @api private + */ + sharedCredentialsFileEnv: 'AWS_SHARED_CREDENTIALS_FILE', + + /** + * @api private + */ + sharedConfigFileEnv: 'AWS_CONFIG_FILE' }; module.exports = util; diff --git a/test/config.spec.coffee b/test/config.spec.coffee index 3f4f906136..1f79a5b117 100644 --- a/test/config.spec.coffee +++ b/test/config.spec.coffee @@ -1,5 +1,6 @@ helpers = require('./helpers') AWS = helpers.AWS +SharedIniFile = require('../lib/shared_ini') configure = (options) -> new AWS.Config(options) @@ -55,46 +56,98 @@ describe 'AWS.Config', -> os = require('os') helpers.spyOn(os, 'homedir').andReturn('/home/user') + it 'grabs region from shared credentials file if AWS_SDK_LOAD_CONFIG is set', -> + process.env.AWS_SDK_LOAD_CONFIG = '1' + helpers.spyOn(AWS.util, 'readFileSync').andCallFake (path) -> + if (path.match(/[\/\\]home[\/\\]user[\/\\].aws[\/\\]credentials/)) + ''' + [default] + region = us-west-2 + ''' + else + ''' + [default] + region = eu-east-1 + ''' + + config = new AWS.Config() + expect(config.region).to.equal('us-west-2') + + it 'loads file from path specified in AWS_SHARED_CREDENTIALS_FILE if AWS_SDK_LOAD_CONFIG is set', -> + process.env.AWS_SDK_LOAD_CONFIG = '1' + process.env.AWS_SHARED_CREDENTIALS_FILE = '/path/to/user/config/file' + helpers.spyOn(AWS.util, 'readFileSync').andCallFake (path) -> + if (path == '/path/to/user/config/file') + ''' + [default] + region = us-west-2 + ''' + else + ''' + [default] + region = eu-east-1 + ''' + + config = new AWS.Config() + expect(config.region).to.equal('us-west-2') + it 'grabs region from shared config if AWS_SDK_LOAD_CONFIG is set', -> process.env.AWS_SDK_LOAD_CONFIG = '1' - mock = ''' - [default] - region = us-west-2 - ''' - helpers.spyOn(AWS.util, 'readFileSync').andReturn(mock) + helpers.spyOn(AWS.util, 'readFileSync').andCallFake (path) -> + if (path.match(/[\/\\]home[\/\\]user[\/\\].aws[\/\\]config/)) + ''' + [default] + region = us-west-2 + ''' + else + ''' + [default] + aws_access_key_id = AKIAIOSFODNN7EXAMPLE + aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY + ''' config = new AWS.Config() expect(config.region).to.equal('us-west-2') - expect(AWS.util.readFileSync.calls[0].arguments[0]).to.match(/[\/\\]home[\/\\]user[\/\\].aws[\/\\]config/) it 'loads file from path specified in AWS_CONFIG_FILE if AWS_SDK_LOAD_CONFIG is set', -> process.env.AWS_SDK_LOAD_CONFIG = '1' process.env.AWS_CONFIG_FILE = '/path/to/user/config/file' - mock = ''' - [default] - region = us-west-2 - ''' - helpers.spyOn(AWS.util, 'readFileSync').andReturn(mock) + helpers.spyOn(AWS.util, 'readFileSync').andCallFake (path) -> + if (path == '/path/to/user/config/file') + ''' + [default] + region = us-west-2 + ''' + else + ''' + [default] + aws_access_key_id = AKIAIOSFODNN7EXAMPLE + aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY + ''' config = new AWS.Config() expect(config.region).to.equal('us-west-2') - expect(AWS.util.readFileSync.calls[0].arguments[0]).to.equal('/path/to/user/config/file') it 'uses the profile specified in AWS_PROFILE', -> process.env.AWS_SDK_LOAD_CONFIG = '1' process.env.AWS_PROFILE = 'foo' - mock = ''' - [default] - region = us-west-1 - - [profile foo] - region = us-west-2 - ''' - helpers.spyOn(AWS.util, 'readFileSync').andReturn(mock) + helpers.spyOn(AWS.util, 'readFileSync').andCallFake (path) -> + if (path.match(/[\/\\]home[\/\\]user[\/\\].aws[\/\\]config/)) + ''' + [default] + region = us-west-1 + + [profile foo] + region = us-west-2 + ''' + else + ''' + [default] + region = eu-east-1 + ''' config = new AWS.Config() expect(config.region).to.equal('us-west-2') - expect(AWS.util.readFileSync.calls[0].arguments[0]).to.match(/[\/\\]home[\/\\]user[\/\\].aws[\/\\]config/) it 'prefers AWS_REGION to the shared config file', -> process.env.AWS_REGION = 'us-east-1' diff --git a/test/credentials.spec.coffee b/test/credentials.spec.coffee index a580bf43be..4097ee1e0c 100644 --- a/test/credentials.spec.coffee +++ b/test/credentials.spec.coffee @@ -186,6 +186,7 @@ if AWS.util.isNode() new AWS.SharedIniFileCredentials() expect(os.homedir.calls.length).to.equal(1) + expect(AWS.util.readFileSync.calls.length).to.equal(1) expect(AWS.util.readFileSync.calls[0].arguments[0]).to.match(/\/foo\/bar\/baz[\/\\].aws[\/\\]credentials/) it 'throws an error if HOME/HOMEPATH/USERPROFILE are not set', -> @@ -196,24 +197,28 @@ if AWS.util.isNode() process.env.HOMEDRIVE = 'd:/' process.env.HOMEPATH = 'homepath' creds = new AWS.SharedIniFileCredentials() - creds.get(); + creds.get() + expect(AWS.util.readFileSync.calls.length).to.equal(1) expect(AWS.util.readFileSync.calls[0].arguments[0]).to.match(/d:[\/\\]homepath[\/\\].aws[\/\\]credentials/) it 'uses default HOMEDRIVE of C:/', -> process.env.HOMEPATH = 'homepath' creds = new AWS.SharedIniFileCredentials() - creds.get(); + creds.get() + expect(AWS.util.readFileSync.calls.length).to.equal(1) expect(AWS.util.readFileSync.calls[0].arguments[0]).to.match(/C:[\/\\]homepath[\/\\].aws[\/\\]credentials/) it 'uses USERPROFILE if HOME is not set', -> process.env.USERPROFILE = '/userprofile' creds = new AWS.SharedIniFileCredentials() - creds.get(); + creds.get() + expect(AWS.util.readFileSync.calls.length).to.equal(1) expect(AWS.util.readFileSync.calls[0].arguments[0]).to.match(/[\/\\]userprofile[\/\\].aws[\/\\]credentials/) it 'can override filename as a constructor argument', -> creds = new AWS.SharedIniFileCredentials(filename: '/etc/creds') - creds.get(); + creds.get() + expect(AWS.util.readFileSync.calls.length).to.equal(1) expect(AWS.util.readFileSync.calls[0].arguments[0]).to.equal('/etc/creds') describe 'loading', -> @@ -265,6 +270,36 @@ if AWS.util.isNode() validateCredentials(creds) expect(AWS.util.readFileSync.calls[0].arguments[0]).to.match(/[\/\\]home[\/\\]user[\/\\].aws[\/\\]config/) + it 'prefers credentials from ~/.aws/credentials if AWS_SDK_LOAD_CONFIG is set', -> + process.env.AWS_SDK_LOAD_CONFIG = '1' + mock = ''' + [default] + aws_access_key_id = akid + aws_secret_access_key = secret + aws_session_token = session + ''' + helpers.spyOn(AWS.util, 'readFileSync').andCallFake (path) -> + if path.match(/[\/\\]home[\/\\]user[\/\\].aws[\/\\]credentials/) + ''' + [default] + aws_access_key_id = akid + aws_secret_access_key = secret + aws_session_token = session + ''' + else + ''' + [default] + aws_access_key_id = AKIAIOSFODNN7EXAMPLE + aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY + ''' + + creds = new AWS.SharedIniFileCredentials() + creds.get(); + validateCredentials(creds) + expect(creds.accessKeyId).to.equal('akid') + expect(creds.secretAccessKey).to.equal('secret') + expect(creds.sessionToken).to.equal('session') + it 'loads credentials from ~/.aws/credentials if AWS_SDK_LOAD_CONFIG is not set', -> process.env.AWS_SHARED_CREDENTIALS_FILE = '/path/to/aws/credentials' mock = ''' @@ -362,9 +397,10 @@ if AWS.util.isNode() to.throw(/^Profile default not found/) describe 'loadRoleProfile', -> - beforeEach -> - process.env.HOME = '/home/user' - + beforeEach -> + os = require('os') + helpers.spyOn(os, 'homedir').andReturn('/home/user') + it 'will fail if assume role is disabled', -> mock = ''' [default] @@ -376,7 +412,7 @@ if AWS.util.isNode() creds = new AWS.SharedIniFileCredentials({disableAssumeRole: true}) creds.refresh (err) -> expect(err.message).to.match(/^Role assumption profiles are disabled. Failed to load profile default/) - + it 'will fail if no source profile is specified', -> mock = ''' [default] @@ -385,11 +421,11 @@ if AWS.util.isNode() role_arn = arn ''' helpers.spyOn(AWS.util, 'readFileSync').andReturn(mock) - + creds = new AWS.SharedIniFileCredentials() creds.refresh (err) -> expect(err.message).to.equal('source_profile is not set using profile default') - + it 'will fail if source profile config is not defined', -> mock = ''' [default] @@ -399,11 +435,11 @@ if AWS.util.isNode() source_profile = fake ''' helpers.spyOn(AWS.util, 'readFileSync').andReturn(mock) - + creds = new AWS.SharedIniFileCredentials() creds.refresh (err) -> expect(err.message).to.match(/source_profile fake using profile default does not exist/) - + it 'will fail if source profile config lacks credentials', -> mock = ''' [default] @@ -415,7 +451,7 @@ if AWS.util.isNode() aws_access_key_id = akid2 ''' helpers.spyOn(AWS.util, 'readFileSync').andReturn(mock) - + creds = new AWS.SharedIniFileCredentials() creds.refresh (err) -> expect(err.message).to.match(/Credentials not set in source_profile foo using profile default/) @@ -455,6 +491,84 @@ if AWS.util.isNode() expect(creds.expireTime).to.eql(new Date(0)) done() + it 'will assume a role from the credentials file whose source profile is defined in the config file', (done) -> + process.env.AWS_SDK_LOAD_CONFIG = '1' + helpers.spyOn(AWS.util, 'readFileSync').andCallFake (path) -> + if (path.match(/[\/\\]home[\/\\]user[\/\\].aws[\/\\]credentials/)) + ''' + [default] + aws_access_key_id = akid + aws_secret_access_key = secret + role_arn = arn + source_profile = foo + ''' + else + ''' + [profile foo] + aws_access_key_id = akid2 + aws_secret_access_key = secret2 + ''' + helpers.mockHttpResponse 200, {}, ''' + + + + KEY + SECRET + TOKEN + 1970-01-01T00:00:00.000Z + + + + ''' + debugger; + creds = new AWS.SharedIniFileCredentials() + expect(creds.roleArn).to.equal('arn') + creds.refresh (err) -> + expect(creds.accessKeyId).to.equal('KEY') + expect(creds.secretAccessKey).to.equal('SECRET') + expect(creds.sessionToken).to.equal('TOKEN') + expect(creds.expireTime).to.eql(new Date(0)) + done() + + it 'will assume a role from the config file whose source profile is defined in the credentials file', (done) -> + process.env.AWS_SDK_LOAD_CONFIG = '1' + helpers.spyOn(AWS.util, 'readFileSync').andCallFake (path) -> + if (path.match(/[\/\\]home[\/\\]user[\/\\].aws[\/\\]config/)) + ''' + [default] + aws_access_key_id = akid + aws_secret_access_key = secret + role_arn = arn + source_profile = foo + ''' + else + ''' + [foo] + aws_access_key_id = akid2 + aws_secret_access_key = secret2 + ''' + helpers.mockHttpResponse 200, {}, ''' + + + + KEY + SECRET + TOKEN + 1970-01-01T00:00:00.000Z + + + + ''' + debugger; + creds = new AWS.SharedIniFileCredentials() + expect(creds.roleArn).to.equal('arn') + creds.refresh (err) -> + expect(creds.accessKeyId).to.equal('KEY') + expect(creds.secretAccessKey).to.equal('SECRET') + expect(creds.sessionToken).to.equal('TOKEN') + expect(creds.expireTime).to.eql(new Date(0)) + done() + describe 'AWS.EC2MetadataCredentials', -> creds = null @@ -980,13 +1094,13 @@ describe 'AWS.CognitoIdentityCredentials', -> expect(creds.params.IdentityId).not.to.exist describe 'clearIdOnNotAuthorized', -> - + it 'should call clearCachedId if user is not authorized', -> clearCache = helpers.spyOn(creds,'clearCachedId') idErr = {code: 'NotAuthorizedException'} creds.clearIdOnNotAuthorized(idErr) expect(clearCache.calls.length).to.equal(1) - + it 'should not call clearCachedId if user is authorized', -> clearCache = helpers.spyOn(creds,'clearCachedId') idErr = {code: 'TEST'}