Repository: aws/aws-sdk-js
Pull request #1795 (2017-11-10 23:39:58)

 Remove localhost restriction for ECS credential provider
Allow ECS credential provider to access non-local hosts for https protocol.


Patch:
diff --git a/.changes/next-release/feature-ECS-a2369f59.json b/.changes/next-release/feature-ECS-a2369f59.json
new file mode 100644
index 000000000..b4d1045ad
--- /dev/null
+++ b/.changes/next-release/feature-ECS-a2369f59.json
@@ -0,0 +1,5 @@
+{
+  "type": "feature",
+  "category": "ECS",
+  "description": "Remove localhost restriction for ECS credential provider"
+}
diff --git a/lib/credentials/ecs_credentials.js b/lib/credentials/ecs_credentials.js
index 61a98ce7b..b52a9bdde 100644
--- a/lib/credentials/ecs_credentials.js
+++ b/lib/credentials/ecs_credentials.js
@@ -2,6 +2,7 @@ var AWS = require('../core'),
   ENV_RELATIVE_URI = 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI',
   ENV_FULL_URI = 'AWS_CONTAINER_CREDENTIALS_FULL_URI',
   ENV_AUTH_TOKEN = 'AWS_CONTAINER_AUTHORIZATION_TOKEN',
+  FULL_URI_UNRESTRICTED_PROTOCOLS = ['https:'],
   FULL_URI_ALLOWED_PROTOCOLS = ['http:', 'https:'],
   FULL_URI_ALLOWED_HOSTNAMES = ['localhost', '127.0.0.1'],
   RELATIVE_URI_HOST = '169.254.170.2';
@@ -82,11 +83,12 @@ AWS.ECSCredentials = AWS.util.inherit(AWS.Credentials, {
           );
         }
 
-        if (FULL_URI_ALLOWED_HOSTNAMES.indexOf(parsed.hostname) < 0) {
+        if (FULL_URI_UNRESTRICTED_PROTOCOLS.indexOf(parsed.protocol) < 0 &&
+            FULL_URI_ALLOWED_HOSTNAMES.indexOf(parsed.hostname) < 0) {
           throw AWS.util.error(
-            new Error('Unsupported hostname: AWS.ECSCredentials supports '
-              + FULL_URI_ALLOWED_HOSTNAMES.join(',') + ' only; '
-              + parsed.hostname + ' requested.'),
+            new Error('Unsupported hostname: AWS.ECSCredentials only supports '
+              + FULL_URI_ALLOWED_HOSTNAMES.join(',') + ' for ' + parsed.protocol + '; '
+              + parsed.protocol + parsed.hostname + ' requested.'),
             { code: 'ECSCredentialsProviderFailure' }
           );
         }
@@ -109,8 +111,25 @@ AWS.ECSCredentials = AWS.util.inherit(AWS.Credentials, {
    * @api private
    */
   credsFormatIsValid: function credsFormatIsValid(credData) {
-    return (!!credData.AccessKeyId && !!credData.SecretAccessKey &&
-      !!credData.Token && !!credData.Expiration);
+    return (!!credData.accessKeyId && !!credData.secretAccessKey &&
+      !!credData.sessionToken && !!credData.expireTime);
+  },
+
+  /**
+   * @api private
+   */
+  formatCreds: function formatCreds(credData) {
+    if (!!credData.credentials) {
+      credData = credData.credentials;
+    }
+
+    return {
+      expired: false,
+      accessKeyId: credData.accessKeyId || credData.AccessKeyId,
+      secretAccessKey: credData.secretAccessKey || credData.SecretAccessKey,
+      sessionToken: credData.sessionToken || credData.Token,
+      expireTime: new Date(credData.expiration || credData.Expiration)
+    };
   },
 
   /**
@@ -183,15 +202,8 @@ AWS.ECSCredentials = AWS.util.inherit(AWS.Credentials, {
       if (!err) {
         try {
           data = JSON.parse(data);
-          if (self.credsFormatIsValid(data)) {
-            var creds = {
-              expired: false,
-              accessKeyId: data.AccessKeyId,
-              secretAccessKey: data.SecretAccessKey,
-              sessionToken: data.Token,
-              expireTime: new Date(data.Expiration)
-            };
-          } else {
+          var creds = self.formatCreds(data);
+          if (!self.credsFormatIsValid(creds)) {
             throw AWS.util.error(
               new Error('Response data is not in valid format'),
               { code: 'ECSCredentialsProviderFailure' }


Test patch:
diff --git a/test/credentials.spec.js b/test/credentials.spec.js
index 55f97022a..9d72b59ca 100644
--- a/test/credentials.spec.js
+++ b/test/credentials.spec.js
@@ -602,7 +602,7 @@
       });
     });
     describe('AWS.ECSCredentials', function() {
-      var creds, mockEndpoint, responseData;
+      var creds, mockEndpoint, responseData, responseDataNew;
       creds = null;
       responseData = {
         AccessKeyId: 'KEY',
@@ -610,6 +610,14 @@
         Token: 'TOKEN',
         Expiration: (new Date(0)).toISOString()
       };
+      responseDataNew = {
+        credentials: {
+          accessKeyId: 'KEY',
+          secretAccessKey: 'SECRET',
+          sessionToken: 'TOKEN',
+          expiration: (new Date(0)).toISOString()
+        }
+      };
       beforeEach(function() {
         creds = new AWS.ECSCredentials({
           host: 'host'
@@ -745,11 +753,57 @@
         it(
           'throws an error when the full URI environment variable contains a URI with an unsupported protocol',
           function () {
-            process.env['AWS_CONTAINER_CREDENTIALS_FULL_URI'] = 'https://s3-us-west-2.amazonaws.com/bucket/credentials';
+            process.env['AWS_CONTAINER_CREDENTIALS_FULL_URI'] = 'http://s3-us-west-2.amazonaws.com/bucket/credentials';
             expect(creds.getECSFullUri.bind(creds))
               .to.throw(/Unsupported hostname/);
           }
         );
+
+        it(
+          'returns a full URI when the full URI environment variable is set to a non-localhost https URI',
+          function () {
+            process.env['AWS_CONTAINER_CREDENTIALS_FULL_URI'] = 'https://s3-us-west-2.amazonaws.com/bucket/credentials';
+            expect(creds.getECSFullUri())
+              .to.equal('https://s3-us-west-2.amazonaws.com/bucket/credentials');
+          }
+        );
+      });
+
+      describe('formatCreds', function() {
+        var formattedCreds;
+
+        it('removes invalid keys', function() {
+          var invalidData;
+          invalidData = {
+            accessKeyId: 'KEY',
+            InvalidKey: 'someValue'
+          };
+          formattedCreds = creds.formatCreds(invalidData);
+          expect(formattedCreds.InvalidKey).to.be.undefined;
+          expect(formattedCreds.accessKeyId).to.be["KEY"];
+        });
+        it('renames valid keys', function() {
+          formattedCreds = creds.formatCreds(responseData);
+          expect(formattedCreds.accessKeyId).to.be["KEY"];
+          expect(formattedCreds.secretAccessKey).to.be["SECRET"];
+          expect(formattedCreds.sessionToken).to.be["TOKEN"];
+        });
+        it('restructures valid creds', function() {
+          var validData;
+          validData = {
+            credentials: {
+              accessKeyId: 'KEY',
+              secretAccessKey: 'SECRET',
+              sessionToken: 'TOKEN',
+              expiration: (new Date(0)).toISOString()
+            }
+          };
+          formattedCreds = creds.formatCreds(validData);
+          expect(formattedCreds.accessKeyId).to.be["KEY"];
+          expect(formattedCreds.secretAccessKey).to.be["SECRET"];
+          expect(formattedCreds.sessionToken).to.be["TOKEN"];
+          expect(formattedCreds.expireTime).to.eql(new Date(0));
+        });
       });
 
       describe('credsFormatIsValid', function() {
@@ -762,8 +816,9 @@
           };
           expect(creds.credsFormatIsValid(incompleteData)).to.be["false"];
         });
-        it('returns true when data has all required properties', function() {
-          expect(creds.credsFormatIsValid(responseData)).to.be["true"];
+        it('returns true when formatted data has all required properties', function() {
+          expect(creds.credsFormatIsValid(responseData)).to.be["false"];
+          expect(creds.credsFormatIsValid(creds.formatCreds(responseData))).to.be["true"];
         });
       });