diff --git a/.changes/next-release/bugfix-Credentials-6b971f33.json b/.changes/next-release/bugfix-Credentials-6b971f33.json new file mode 100644 index 0000000000..45dd83f666 --- /dev/null +++ b/.changes/next-release/bugfix-Credentials-6b971f33.json @@ -0,0 +1,5 @@ +{ + "type": "bugfix", + "category": "Credentials", + "description": "fix credential_process credential resolution failure if it is the only item in source_profile when used by role_arn" +} \ No newline at end of file diff --git a/lib/credentials/shared_ini_file_credentials.js b/lib/credentials/shared_ini_file_credentials.js index 47290be6e6..1c0629ceac 100644 --- a/lib/credentials/shared_ini_file_credentials.js +++ b/lib/credentials/shared_ini_file_credentials.js @@ -122,18 +122,48 @@ AWS.SharedIniFileCredentials = AWS.util.inherit(AWS.Credentials, { ); if (profile['role_arn'] && !preferStaticCredentialsToRoleArn) { - this.loadRoleProfile(profiles, profile, function(err, data) { - if (err) { - callback(err); - } else { - self.expired = false; - self.accessKeyId = data.Credentials.AccessKeyId; - self.secretAccessKey = data.Credentials.SecretAccessKey; - self.sessionToken = data.Credentials.SessionToken; - self.expireTime = data.Credentials.Expiration; - callback(null); + var sourceProfileName = profile['source_profile']; + var credentialsSetChain = [ + [AWS.SharedIniFileCredentials, function() { return false; }], + [AWS.ProcessCredentials, function() { + return !profiles[sourceProfileName] || + !profiles[sourceProfileName]['credential_process']; + }], + ]; + var i = credentialsSetChain.length; + var errs = []; + for (var index = 0; index < credentialsSetChain.length; index++) { + var credentials = credentialsSetChain[index][0], skip = credentialsSetChain[index][1]; + if (skip()) { + i--; + continue; + } + this.loadRoleProfile(profiles, profile, credentials, function(err, data) { + i--; + if (err) { + errs.push(err); + } else { + self.expired = false; + self.accessKeyId = data.Credentials.AccessKeyId; + self.secretAccessKey = data.Credentials.SecretAccessKey; + self.sessionToken = data.Credentials.SessionToken; + self.expireTime = data.Credentials.Expiration; + callback(null); + sourceProfileName = ''; + } + }); + if (!sourceProfileName) { + return; } - }); + } + (function() { + var j = setInterval(function() { + if (i <= 0) { + errs.forEach(function(err) { callback(err); }); + clearInterval(j); + } + }, 0); + })(); return; } @@ -176,7 +206,7 @@ AWS.SharedIniFileCredentials = AWS.util.inherit(AWS.Credentials, { /** * @api private */ - loadRoleProfile: function loadRoleProfile(creds, roleProfile, callback) { + loadRoleProfile: function loadRoleProfile(creds, roleProfile, credentials, callback) { if (this.disableAssumeRole) { throw AWS.util.error( new Error('Role assumption profiles are disabled. ' + @@ -232,7 +262,7 @@ AWS.SharedIniFileCredentials = AWS.util.inherit(AWS.Credentials, { ); } - var sourceCredentials = new AWS.SharedIniFileCredentials( + var sourceCredentials = new credentials( AWS.util.merge(this.options || {}, { profile: sourceProfileName, preferStaticCredentials: true diff --git a/test/credentials.spec.js b/test/credentials.spec.js index 6d290c6c28..4a6de9224b 100644 --- a/test/credentials.spec.js +++ b/test/credentials.spec.js @@ -1324,6 +1324,65 @@ const exp = require('constants'); }); }); + it('will use credential_process in the base profile', function(done) { + var mock = [ + '[default]', + 'role_arn = arn', + 'source_profile = base', + '[base]', + 'credential_process=federated_cli_mock', + ].join('\n'); + helpers.spyOn(AWS.util, 'readFileSync').andReturn(mock); + + var child_process = require('child_process'); + mockProcess = '{"Version": 1,"AccessKeyId": "akid","SecretAccessKey": "secret","SessionToken": "session","Expiration": ""}'; + helpers.spyOn(child_process, 'exec').andCallFake(function (_, _, cb) { + cb(undefined, mockProcess, undefined); + }); + + var expiration = new Date(Date.now() + 1000); + var STSPrototype = (new STS()).constructor.prototype; + var spy = helpers.spyOn(STSPrototype, 'assumeRole').andCallFake( + function(roleParams, callback) { + if (this.config.credentials.accessKeyId === 'akid' && + this.config.credentials.secretAccessKey === 'secret' && + this.config.credentials.sessionToken === 'session') { + callback(null, { + Credentials: { + AccessKeyId: 'KEY', + SecretAccessKey: 'SECRET', + SessionToken: 'TOKEN', + Expiration: expiration + } + }); + } + callback(new Error('INVALID CREDENTIAL')); + } + ); + + var creds = new AWS.SharedIniFileCredentials({ + callback: function (err) { + expect(err).to.be.null; + expect(spy.calls.length).to.equal(2); + expect(spy.calls[0].arguments[0].RoleArn).to.equal('arn'); + expect(spy.calls[0].object.config.credentials.profile).to.equal('base'); + expect(spy.calls[0].object.config.credentials.accessKeyId).to.be.undefined; + expect(spy.calls[0].object.config.credentials.secretAccessKey).to.be.undefined; + expect(spy.calls[0].object.config.credentials.sessionToken).to.be.undefined; + expect(spy.calls[1].arguments[0].RoleArn).to.equal('arn'); + expect(spy.calls[1].object.config.credentials.profile).to.equal('base'); + expect(spy.calls[1].object.config.credentials.accessKeyId).to.equal('akid'); + expect(spy.calls[1].object.config.credentials.secretAccessKey).to.equal('secret'); + expect(spy.calls[1].object.config.credentials.sessionToken).to.equal('session'); + expect(creds.accessKeyId).to.equal('KEY'); + expect(creds.secretAccessKey).to.equal('SECRET'); + expect(creds.sessionToken).to.equal('TOKEN'); + expect(creds.expireTime).to.equal(expiration); + done(); + } + }); + }); + describe('mfa serial callback', function() { beforeEach(function() {