Skip to content

Instantly share code, notes, and snippets.

@thomasmichaelwallace
Last active October 5, 2021 11:56
Show Gist options
  • Save thomasmichaelwallace/5ef97b1fbadf8df2bca21bebafd2dd7e to your computer and use it in GitHub Desktop.
Save thomasmichaelwallace/5ef97b1fbadf8df2bca21bebafd2dd7e to your computer and use it in GitHub Desktop.
serverless framework credential_process patch

About

This gist provides a patch-package patch that tracks the serverless framework and provides a fix for #4838 (support for credential_process).

We use AWS SSO at devicepilot, and credentials_process provides a convenient way to polyfill the aws-sdk (and serverless framework) with support for it (see: https://github.com/benkehoe/aws-sso-credential-process).

There's still some work to be done refactoring the serverless framework before a PR for this patch can be accepted. However, as I've already committed to using and maintaining this patch in production, I thought I would make it publicly available for the impatient.

Until it is accepted as a PR, I'll maintain this patch against a relatively recent version of the framework. I can't commit to always tracking the latest version, however it's likely to be compatible with newer (and not-too-older) versions because the touched files do not change much.

Instructions

  • Ensure you are using serverless v2.4.0, v2.22.0, v2.30.3, v.2.52.1, v.2.61.0 (your milage may vary, but it probably will work, with older/newer versions)
  • Install and setup patch-package in your project: https://github.com/ds300/patch-package#set-up
  • Copy the serverless+2.4.0.patch or serverless+2.22.0.patch patch (whichever matches your version) into (project root)/patches/serverless+2._.0.patch (using the raw button is probably the easiest way to do this)
  • Run yarn or npm install to patch the above changes
  • You should now be able to use the serverless framework with credentials_process.
diff --git a/node_modules/serverless/lib/plugins/aws/invokeLocal/index.js b/node_modules/serverless/lib/plugins/aws/invokeLocal/index.js
index 570f566..a7b3bff 100644
--- a/node_modules/serverless/lib/plugins/aws/invokeLocal/index.js
+++ b/node_modules/serverless/lib/plugins/aws/invokeLocal/index.js
@@ -124,18 +124,25 @@ class AwsInvokeLocal {
getCredentialEnvVars() {
const credentialEnvVars = {};
const { credentials } = this.provider.getCredentials();
- if (credentials) {
- if (credentials.accessKeyId) {
- credentialEnvVars.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
- }
- if (credentials.secretAccessKey) {
- credentialEnvVars.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey;
- }
- if (credentials.sessionToken) {
- credentialEnvVars.AWS_SESSION_TOKEN = credentials.sessionToken;
- }
- }
- return credentialEnvVars;
+ return credentials
+ .getPromise()
+ .then(() => {
+ if (credentials.accessKeyId) {
+ credentialEnvVars.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
+ }
+ if (credentials.secretAccessKey) {
+ credentialEnvVars.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey;
+ }
+ if (credentials.sessionToken) {
+ credentialEnvVars.AWS_SESSION_TOKEN = credentials.sessionToken;
+ }
+ return credentialEnvVars;
+ })
+ .catch(() => {
+ // do not throw if provider credentials fail to resolve,
+ // the user may be relying on functionEnvVars instead.
+ return credentialEnvVars;
+ });
}
getConfiguredEnvVars() {
@@ -167,7 +174,7 @@ class AwsInvokeLocal {
NODE_PATH: '/var/runtime:/var/task:/var/runtime/node_modules',
};
- const credentialEnvVars = this.getCredentialEnvVars();
+ const credentialEnvVars = await this.getCredentialEnvVars();
// profile override from config
const profileOverride = this.provider.getProfile();
@@ -472,7 +479,7 @@ class AwsInvokeLocal {
AWS_LAMBDA_FUNCTION_NAME: lambdaName,
AWS_LAMBDA_FUNCTION_MEMORY_SIZE: memorySize,
};
- const credentialEnvVars = this.getCredentialEnvVars();
+ const credentialEnvVars = await this.getCredentialEnvVars();
const configuredEnvVars = this.getConfiguredEnvVars();
const envVarsFromOptions = this.getEnvVarsFromOptions();
const envVars = _.merge(
diff --git a/node_modules/serverless/lib/plugins/aws/lib/awsCredentials.js b/node_modules/serverless/lib/plugins/aws/lib/awsCredentials.js
new file mode 100644
index 0000000..eb6f09b
--- /dev/null
+++ b/node_modules/serverless/lib/plugins/aws/lib/awsCredentials.js
@@ -0,0 +1,90 @@
+'use strict';
+
+const AWS = require('aws-sdk');
+const readline = require('readline');
+
+/*
+ * The aws-sdk-js provides a built in mechanism for resolving credentials from multiple sources:
+ * https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CredentialProviderChain.html
+ * However, the credential resolution for the serverless framework differs significantly from the
+ * AWS default provider chain (e.g. credentials and provides set by the yaml).
+ *
+ * This class allows us to define a more flexible order (see AwsProvider.getCredentials()),
+ * while still using the aws-sdk-js supported framework; so we can more readily support future
+ * ways of resolving credentials.
+ *
+ * Until https://github.com/aws/aws-sdk-js/issues/3122 is resolved, extending the
+ * AWS.CredentialProviderChain does not result in AWS.Credentials that refresh using the chain.
+ * Therefore we must extend AWS.Credentials directly and provide a refresh method that
+ * resolves the chain ourselves.
+ */
+module.exports = class AwsCredentials extends AWS.Credentials {
+ constructor() {
+ super();
+ this.chain = new AWS.CredentialProviderChain([]); // providers are added explicitly
+ }
+
+ refresh(callback) {
+ this.chain.resolve((err, res) => {
+ if (err) {
+ callback(err);
+ } else {
+ AWS.Credentials.call(this, res);
+ callback();
+ }
+ });
+ }
+
+ /**
+ * Add credentials, if present and valid, from provider config
+ * @param credentials The credentials to test for validity
+ */
+ addConfig(credentials) {
+ if (credentials) {
+ if (
+ credentials.accessKeyId &&
+ credentials.accessKeyId !== 'undefined' &&
+ ((credentials.secretAccessKey && credentials.secretAccessKey !== 'undefined') ||
+ (credentials.sessionToken && credentials.sessionToken !== 'undefined'))
+ ) {
+ this.chain.providers.push(new AWS.Credentials(credentials));
+ }
+ }
+ }
+
+ /**
+ * Add credentials, if present, from the environment
+ * @param prefix The environment variable prefix to use in extracting credentials
+ */
+ addEnvironment(prefix) {
+ if (prefix) {
+ const environmentCredentials = new AWS.EnvironmentCredentials(prefix);
+ this.chain.providers.push(environmentCredentials);
+ }
+ }
+
+ /**
+ * Add credentials from a profile
+ * @param profile The profile to load credentials from
+ */
+ addProfile(profile) {
+ if (profile) {
+ const params = { profile };
+ if (process.env.AWS_SHARED_CREDENTIALS_FILE) {
+ params.filename = process.env.AWS_SHARED_CREDENTIALS_FILE;
+ }
+
+ // Setup a MFA callback for asking the code from the user.
+ params.tokenCodeFn = (mfaSerial, callback) => {
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
+ rl.question(`Enter MFA code for ${mfaSerial}: `, (answer) => {
+ rl.close();
+ callback(null, answer);
+ });
+ };
+
+ this.chain.providers.push(() => new AWS.SharedIniFileCredentials(params));
+ this.chain.providers.push(() => new AWS.ProcessCredentials(params));
+ }
+ }
+};
diff --git a/node_modules/serverless/lib/plugins/aws/provider.js b/node_modules/serverless/lib/plugins/aws/provider.js
index b9f5b46..b93146b 100644
--- a/node_modules/serverless/lib/plugins/aws/provider.js
+++ b/node_modules/serverless/lib/plugins/aws/provider.js
@@ -11,8 +11,8 @@ const https = require('https');
const fs = require('fs');
const objectHash = require('object-hash');
const PromiseQueue = require('promise-queue');
+const AwsCredentials = require('./lib/awsCredentials');
const getS3EndpointForRegion = require('./utils/getS3EndpointForRegion');
-const readline = require('readline');
const { ALB_LISTENER_REGEXP } = require('./package/compile/events/alb/lib/validate');
const memoizeeMethods = require('memoizee/methods');
const d = require('d');
@@ -65,101 +65,6 @@ const MAX_RETRIES = (() => {
return userValue >= 0 ? userValue : 4;
})();
-const impl = {
- /**
- * Determine whether the given credentials are valid. It turned out that detecting invalid
- * credentials was more difficult than detecting the positive cases we know about. Hooray for
- * whack-a-mole!
- * @param credentials The credentials to test for validity
- * @return {boolean} Whether the given credentials were valid
- */
- validCredentials: (credentials) => {
- let result = false;
- if (credentials) {
- if (
- // valid credentials loaded
- (credentials.accessKeyId &&
- credentials.accessKeyId !== 'undefined' &&
- credentials.secretAccessKey &&
- credentials.secretAccessKey !== 'undefined') ||
- // a role to assume has been successfully loaded, the associated STS request has been
- // sent, and the temporary credentials will be asynchronously delivered.
- credentials.roleArn
- ) {
- result = true;
- }
- }
- return result;
- },
- /**
- * Add credentials, if present, to the given results
- * @param results The results to add the given credentials to if they are valid
- * @param credentials The credentials to validate and add to the results if valid
- */
- addCredentials: (results, credentials) => {
- if (impl.validCredentials(credentials)) {
- results.credentials = credentials; // eslint-disable-line no-param-reassign
- }
- },
- /**
- * Add credentials, if present, from the environment
- * @param results The results to add environment credentials to
- * @param prefix The environment variable prefix to use in extracting credentials
- */
- addEnvironmentCredentials: (results, prefix) => {
- if (prefix) {
- const environmentCredentials = new AWS.EnvironmentCredentials(prefix);
- impl.addCredentials(results, environmentCredentials);
- }
- },
- /**
- * Add credentials from a profile, if the profile and credentials for it exists
- * @param results The results to add profile credentials to
- * @param profile The profile to load credentials from
- */
- addProfileCredentials: (results, profile) => {
- if (profile) {
- const params = { profile };
- if (process.env.AWS_SHARED_CREDENTIALS_FILE) {
- params.filename = process.env.AWS_SHARED_CREDENTIALS_FILE;
- }
-
- // Setup a MFA callback for asking the code from the user.
- params.tokenCodeFn = (mfaSerial, callback) => {
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
- rl.question(`Enter MFA code for ${mfaSerial}: `, (answer) => {
- rl.close();
- callback(null, answer);
- });
- };
-
- const profileCredentials = new AWS.SharedIniFileCredentials(params);
- if (
- !(
- profileCredentials.accessKeyId ||
- profileCredentials.sessionToken ||
- profileCredentials.roleArn
- )
- ) {
- throw new Error(`Profile ${profile} does not exist`);
- }
-
- impl.addCredentials(results, profileCredentials);
- }
- },
- /**
- * Add credentials, if present, from a profile that is specified within the environment
- * @param results The prefix of the profile's declaration in the environment
- * @param prefix The prefix for the environment variable
- */
- addEnvironmentProfile: (results, prefix) => {
- if (prefix) {
- const profile = process.env[`${prefix}_PROFILE`];
- impl.addProfileCredentials(results, profile);
- }
- },
-};
-
const baseAlbAuthorizerProperties = {
onUnauthenticatedRequest: { enum: ['allow', 'authenticate', 'deny'] },
requestExtraParams: {
@@ -1298,14 +1203,14 @@ class AwsProvider {
* @prop [options.region] - Specify when to request to different region
*/
request(service, method, params, options) {
- const credentials = Object.assign({}, this.getCredentials());
- credentials.region = this.getRegion();
+ const that = this;
// Make sure options is an object (honors wrong calls of request)
const requestOptions = _.isObject(options) ? options : {};
const shouldCache = _.get(requestOptions, 'useCache', false);
- const paramsWithRegion = _.merge({}, params, {
- region: _.get(options, 'region'),
- });
+ const { credentials } = that.getCredentials();
+ const region = requestOptions.region || that.getRegion();
+ const serviceParams = { credentials, region };
+ const paramsWithRegion = { ...params, region };
const paramsHash = objectHash.sha1(paramsWithRegion);
const BASE_BACKOFF = 5000;
const persistentRequest = (f) =>
@@ -1352,8 +1257,20 @@ class AwsProvider {
}
// Support S3 Transfer Acceleration
- if (this.canUseS3TransferAcceleration(service, method)) {
- this.enableS3TransferAcceleration(credentials);
+ if (service === 'S3') {
+ // Support S3 Transfer Acceleration
+ if (this.canUseS3TransferAcceleration(service, method)) {
+ this.enableS3TransferAcceleration(serviceParams);
+ }
+
+ const deploymentBucketObject = this.serverless.service.provider.deploymentBucketObject;
+ if (
+ deploymentBucketObject &&
+ deploymentBucketObject.serverSideEncryption &&
+ deploymentBucketObject.serverSideEncryption === 'aws:kms'
+ ) {
+ serviceParams.signatureVersion = 'v4';
+ }
}
if (shouldCache) {
@@ -1365,11 +1282,8 @@ class AwsProvider {
const request = this.requestQueue.add(() =>
persistentRequest(() => {
- if (options && options.region) {
- credentials.region = options.region;
- }
- const Service = _.get(this.sdk, service);
- const awsService = new Service(credentials);
+ const Service = _.get(that.sdk, service);
+ const awsService = new Service(serviceParams);
const req = awsService[method](params);
// TODO: Add listeners, put Debug statements here...
@@ -1434,50 +1348,37 @@ class AwsProvider {
/**
* Fetch credentials directly or using a profile from serverless yml configuration or from the
* well known environment variables
- * @returns {{region: *}}
+ * If using outside of AWS, you are expected to use credentials.get/Promise to ensure they have
+ * not expired (https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Credentials.html#getPromise-property)
+ * @returns {credentials: AWS.Credentials, region: string}
*/
getCredentials() {
if (this.cachedCredentials) {
// We have already created the credentials object once, so return it.
return this.cachedCredentials;
}
- const result = {};
- const stageUpper = this.getStage() ? this.getStage().toUpperCase() : null;
+ const credentials = new AwsCredentials();
+ const stageUpperCase = this.getStage() ? this.getStage().toUpperCase() : null;
- // add specified credentials, overriding with more specific declarations
- const awsDefaultProfile = process.env.AWS_DEFAULT_PROFILE || 'default';
- try {
- impl.addProfileCredentials(result, awsDefaultProfile);
- } catch (err) {
- if (err.message !== `Profile ${awsDefaultProfile} does not exist`) {
- throw err;
- }
- }
- impl.addCredentials(result, this.serverless.service.provider.credentials); // config creds
+ // AwsCredentials returns the first credentials to resolve, so add from most-ot-least specific:
+ credentials.addProfile(this.options['aws-profile']); // CLI option profile
+
+ credentials.addProfile(process.env[`AWS_${stageUpperCase}_PROFILE`]); // stage specific creds
+ credentials.addEnvironment(`AWS_${stageUpperCase}`);
+
+ credentials.addProfile(process.env.AWS_PROFILE); // creds for all stages
+ credentials.addEnvironment('AWS');
if (this.serverless.service.provider.profile && !this.options['aws-profile']) {
// config profile
- impl.addProfileCredentials(result, this.serverless.service.provider.profile);
- }
- impl.addEnvironmentCredentials(result, 'AWS'); // creds for all stages
- impl.addEnvironmentProfile(result, 'AWS');
- impl.addEnvironmentCredentials(result, `AWS_${stageUpper}`); // stage specific creds
- impl.addEnvironmentProfile(result, `AWS_${stageUpper}`);
- if (this.options['aws-profile']) {
- impl.addProfileCredentials(result, this.options['aws-profile']); // CLI option profile
- }
-
- const deploymentBucketObject = this.serverless.service.provider.deploymentBucketObject;
- if (
- deploymentBucketObject &&
- deploymentBucketObject.serverSideEncryption &&
- deploymentBucketObject.serverSideEncryption === 'aws:kms'
- ) {
- result.signatureVersion = 'v4';
+ credentials.addProfile(this.serverless.service.provider.profile);
}
+ credentials.addConfig(this.serverless.service.provider.credentials); // config creds
+ credentials.addProfile(process.env.AWS_DEFAULT_PROFILE || 'default');
// Store the credentials to avoid creating them again (messes up MFA).
- this.cachedCredentials = result;
- return result;
+ const region = this.getRegion();
+ this.cachedCredentials = { credentials, region };
+ return this.cachedCredentials;
}
canUseS3TransferAcceleration(service, method) {
@@ -1510,9 +1411,9 @@ class AwsProvider {
delete this.options['aws-s3-accelerate'];
}
- enableS3TransferAcceleration(credentials) {
+ enableS3TransferAcceleration(serviceParams) {
this.serverless.cli.log('Using S3 Transfer Acceleration Endpoint...');
- credentials.useAccelerateEndpoint = true; // eslint-disable-line no-param-reassign
+ serviceParams.useAccelerateEndpoint = true; // eslint-disable-line no-param-reassign
}
getValues(source, objectPaths) {
diff --git a/node_modules/serverless/lib/plugins/aws/invokeLocal/index.js b/node_modules/serverless/lib/plugins/aws/invokeLocal/index.js
index fc6352b..870c8af 100644
--- a/node_modules/serverless/lib/plugins/aws/invokeLocal/index.js
+++ b/node_modules/serverless/lib/plugins/aws/invokeLocal/index.js
@@ -123,18 +123,25 @@ class AwsInvokeLocal {
getCredentialEnvVars() {
const credentialEnvVars = {};
const { credentials } = this.provider.getCredentials();
- if (credentials) {
- if (credentials.accessKeyId) {
- credentialEnvVars.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
- }
- if (credentials.secretAccessKey) {
- credentialEnvVars.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey;
- }
- if (credentials.sessionToken) {
- credentialEnvVars.AWS_SESSION_TOKEN = credentials.sessionToken;
- }
- }
- return credentialEnvVars;
+ return credentials
+ .getPromise()
+ .then(() => {
+ if (credentials.accessKeyId) {
+ credentialEnvVars.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
+ }
+ if (credentials.secretAccessKey) {
+ credentialEnvVars.AWS_SECRET_ACCESS_KEY = credentials.secretAccessKey;
+ }
+ if (credentials.sessionToken) {
+ credentialEnvVars.AWS_SESSION_TOKEN = credentials.sessionToken;
+ }
+ return credentialEnvVars;
+ })
+ .catch(() => {
+ // do not throw if provider credentials fail to resolve,
+ // the user may be relying on functionEnvVars instead.
+ return credentialEnvVars;
+ });
}
getConfiguredEnvVars() {
@@ -166,7 +173,7 @@ class AwsInvokeLocal {
NODE_PATH: '/var/runtime:/var/task:/var/runtime/node_modules',
};
- const credentialEnvVars = this.getCredentialEnvVars();
+ const credentialEnvVars = await this.getCredentialEnvVars();
// profile override from config
const profileOverride = this.provider.getProfile();
@@ -471,7 +478,7 @@ class AwsInvokeLocal {
AWS_LAMBDA_FUNCTION_NAME: lambdaName,
AWS_LAMBDA_FUNCTION_MEMORY_SIZE: memorySize,
};
- const credentialEnvVars = this.getCredentialEnvVars();
+ const credentialEnvVars = await this.getCredentialEnvVars();
const configuredEnvVars = this.getConfiguredEnvVars();
const envVarsFromOptions = this.getEnvVarsFromOptions();
const envVars = _.merge(
diff --git a/node_modules/serverless/lib/plugins/aws/lib/awsCredentials.js b/node_modules/serverless/lib/plugins/aws/lib/awsCredentials.js
new file mode 100644
index 0000000..eb6f09b
--- /dev/null
+++ b/node_modules/serverless/lib/plugins/aws/lib/awsCredentials.js
@@ -0,0 +1,90 @@
+'use strict';
+
+const AWS = require('aws-sdk');
+const readline = require('readline');
+
+/*
+ * The aws-sdk-js provides a built in mechanism for resolving credentials from multiple sources:
+ * https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CredentialProviderChain.html
+ * However, the credential resolution for the serverless framework differs significantly from the
+ * AWS default provider chain (e.g. credentials and provides set by the yaml).
+ *
+ * This class allows us to define a more flexible order (see AwsProvider.getCredentials()),
+ * while still using the aws-sdk-js supported framework; so we can more readily support future
+ * ways of resolving credentials.
+ *
+ * Until https://github.com/aws/aws-sdk-js/issues/3122 is resolved, extending the
+ * AWS.CredentialProviderChain does not result in AWS.Credentials that refresh using the chain.
+ * Therefore we must extend AWS.Credentials directly and provide a refresh method that
+ * resolves the chain ourselves.
+ */
+module.exports = class AwsCredentials extends AWS.Credentials {
+ constructor() {
+ super();
+ this.chain = new AWS.CredentialProviderChain([]); // providers are added explicitly
+ }
+
+ refresh(callback) {
+ this.chain.resolve((err, res) => {
+ if (err) {
+ callback(err);
+ } else {
+ AWS.Credentials.call(this, res);
+ callback();
+ }
+ });
+ }
+
+ /**
+ * Add credentials, if present and valid, from provider config
+ * @param credentials The credentials to test for validity
+ */
+ addConfig(credentials) {
+ if (credentials) {
+ if (
+ credentials.accessKeyId &&
+ credentials.accessKeyId !== 'undefined' &&
+ ((credentials.secretAccessKey && credentials.secretAccessKey !== 'undefined') ||
+ (credentials.sessionToken && credentials.sessionToken !== 'undefined'))
+ ) {
+ this.chain.providers.push(new AWS.Credentials(credentials));
+ }
+ }
+ }
+
+ /**
+ * Add credentials, if present, from the environment
+ * @param prefix The environment variable prefix to use in extracting credentials
+ */
+ addEnvironment(prefix) {
+ if (prefix) {
+ const environmentCredentials = new AWS.EnvironmentCredentials(prefix);
+ this.chain.providers.push(environmentCredentials);
+ }
+ }
+
+ /**
+ * Add credentials from a profile
+ * @param profile The profile to load credentials from
+ */
+ addProfile(profile) {
+ if (profile) {
+ const params = { profile };
+ if (process.env.AWS_SHARED_CREDENTIALS_FILE) {
+ params.filename = process.env.AWS_SHARED_CREDENTIALS_FILE;
+ }
+
+ // Setup a MFA callback for asking the code from the user.
+ params.tokenCodeFn = (mfaSerial, callback) => {
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
+ rl.question(`Enter MFA code for ${mfaSerial}: `, (answer) => {
+ rl.close();
+ callback(null, answer);
+ });
+ };
+
+ this.chain.providers.push(() => new AWS.SharedIniFileCredentials(params));
+ this.chain.providers.push(() => new AWS.ProcessCredentials(params));
+ }
+ }
+};
diff --git a/node_modules/serverless/lib/plugins/aws/provider.js b/node_modules/serverless/lib/plugins/aws/provider.js
index bad1a05..91fd185 100644
--- a/node_modules/serverless/lib/plugins/aws/provider.js
+++ b/node_modules/serverless/lib/plugins/aws/provider.js
@@ -5,9 +5,9 @@ const BbPromise = require('bluebird');
const _ = require('lodash');
const naming = require('./lib/naming.js');
const fs = require('fs');
+const AwsCredentials = require('./lib/awsCredentials');
const getS3EndpointForRegion = require('./utils/getS3EndpointForRegion');
const memoizeeMethods = require('memoizee/methods');
-const readline = require('readline');
const reportDeprecatedProperties = require('../../utils/report-deprecated-properties');
const { ALB_LISTENER_REGEXP } = require('./package/compile/events/alb/lib/validate');
const d = require('d');
@@ -56,104 +56,6 @@ const apiGatewayUsagePlan = {
additionalProperties: false,
};
-const impl = {
- /**
- * Determine whether the given credentials are valid. It turned out that detecting invalid
- * credentials was more difficult than detecting the positive cases we know about. Hooray for
- * whack-a-mole!
- * @param credentials The credentials to test for validity
- * @return {boolean} Whether the given credentials were valid
- */
- validCredentials: (credentials) => {
- let result = false;
- if (credentials) {
- if (
- // valid credentials loaded
- (credentials.accessKeyId &&
- credentials.accessKeyId !== 'undefined' &&
- credentials.secretAccessKey &&
- credentials.secretAccessKey !== 'undefined') ||
- // a role to assume has been successfully loaded, the associated STS request has been
- // sent, and the temporary credentials will be asynchronously delivered.
- credentials.roleArn
- ) {
- result = true;
- }
- }
- return result;
- },
- /**
- * Add credentials, if present, to the given results
- * @param results The results to add the given credentials to if they are valid
- * @param credentials The credentials to validate and add to the results if valid
- */
- addCredentials: (results, credentials) => {
- if (impl.validCredentials(credentials)) {
- results.credentials = credentials; // eslint-disable-line no-param-reassign
- }
- },
- /**
- * Add credentials, if present, from the environment
- * @param results The results to add environment credentials to
- * @param prefix The environment variable prefix to use in extracting credentials
- */
- addEnvironmentCredentials: (results, prefix) => {
- if (prefix) {
- const environmentCredentials = new AWS.EnvironmentCredentials(prefix);
- impl.addCredentials(results, environmentCredentials);
- }
- },
- /**
- * Add credentials from a profile, if the profile and credentials for it exists
- * @param results The results to add profile credentials to
- * @param profile The profile to load credentials from
- */
- addProfileCredentials: (results, profile) => {
- if (profile) {
- const params = { profile };
- if (process.env.AWS_SHARED_CREDENTIALS_FILE) {
- params.filename = process.env.AWS_SHARED_CREDENTIALS_FILE;
- }
-
- // Setup a MFA callback for asking the code from the user.
- params.tokenCodeFn = (mfaSerial, callback) => {
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
- rl.question(`Enter MFA code for ${mfaSerial}: `, (answer) => {
- rl.close();
- callback(null, answer);
- });
- };
-
- const profileCredentials = new AWS.SharedIniFileCredentials(params);
- if (
- !(
- profileCredentials.accessKeyId ||
- profileCredentials.sessionToken ||
- profileCredentials.roleArn
- )
- ) {
- throw new ServerlessError(
- `AWS profile "${profile}" doesn't seem to be configured`,
- 'UNRECOGNIZED_AWS_PROFILE'
- );
- }
-
- impl.addCredentials(results, profileCredentials);
- }
- },
- /**
- * Add credentials, if present, from a profile that is specified within the environment
- * @param results The prefix of the profile's declaration in the environment
- * @param prefix The prefix for the environment variable
- */
- addEnvironmentProfile: (results, prefix) => {
- if (prefix) {
- const profile = process.env[`${prefix}_PROFILE`];
- impl.addProfileCredentials(results, profile);
- }
- },
-};
-
const baseAlbAuthorizerProperties = {
onUnauthenticatedRequest: { enum: ['allow', 'authenticate', 'deny'] },
requestExtraParams: {
@@ -1321,12 +1223,11 @@ class AwsProvider {
}
const requestOptions = _.isObject(options) ? options : {};
const shouldCache = _.get(requestOptions, 'useCache', false);
- // Copy is required as the credentials may be modified during the request
- const credentials = Object.assign({}, this.getCredentials());
+ const { credentials } = await this.getCredentials();
const serviceOptions = {
name: service,
params: {
- ...credentials,
+ credentials,
region: _.get(requestOptions, 'region', this.getRegion()),
isS3TransferAccelerationEnabled: this.isS3TransferAccelerationEnabled(),
},
@@ -1337,48 +1238,37 @@ class AwsProvider {
/**
* Fetch credentials directly or using a profile from serverless yml configuration or from the
* well known environment variables
- * @returns {{region: *}}
+ * If using outside of AWS, you are expected to use credentials.get/Promise to ensure they have
+ * not expired (https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Credentials.html#getPromise-property)
+ * @returns {credentials: AWS.Credentials, region: string}
*/
getCredentials() {
if (this.cachedCredentials) {
// We have already created the credentials object once, so return it.
return this.cachedCredentials;
}
- const result = {};
- const stageUpper = this.getStage() ? this.getStage().toUpperCase() : null;
+ const credentials = new AwsCredentials();
+ const stageUpperCase = this.getStage() ? this.getStage().toUpperCase() : null;
- // add specified credentials, overriding with more specific declarations
- const awsDefaultProfile = process.env.AWS_DEFAULT_PROFILE || 'default';
- try {
- impl.addProfileCredentials(result, awsDefaultProfile);
- } catch (err) {
- if (err.code !== 'UNRECOGNIZED_AWS_PROFILE') throw err;
- }
- impl.addCredentials(result, this.serverless.service.provider.credentials); // config creds
+ // AwsCredentials returns the first credentials to resolve, so add from most-ot-least specific:
+ credentials.addProfile(this.options['aws-profile']); // CLI option profile
+
+ credentials.addProfile(process.env[`AWS_${stageUpperCase}_PROFILE`]); // stage specific creds
+ credentials.addEnvironment(`AWS_${stageUpperCase}`);
+
+ credentials.addProfile(process.env.AWS_PROFILE); // creds for all stages
+ credentials.addEnvironment('AWS');
if (this.serverless.service.provider.profile && !this.options['aws-profile']) {
// config profile
- impl.addProfileCredentials(result, this.serverless.service.provider.profile);
- }
- impl.addEnvironmentCredentials(result, 'AWS'); // creds for all stages
- impl.addEnvironmentProfile(result, 'AWS');
- impl.addEnvironmentCredentials(result, `AWS_${stageUpper}`); // stage specific creds
- impl.addEnvironmentProfile(result, `AWS_${stageUpper}`);
- if (this.options['aws-profile']) {
- impl.addProfileCredentials(result, this.options['aws-profile']); // CLI option profile
- }
-
- const deploymentBucketObject = this.serverless.service.provider.deploymentBucketObject;
- if (
- deploymentBucketObject &&
- deploymentBucketObject.serverSideEncryption &&
- deploymentBucketObject.serverSideEncryption === 'aws:kms'
- ) {
- result.signatureVersion = 'v4';
+ credentials.addProfile(this.serverless.service.provider.profile);
}
+ credentials.addConfig(this.serverless.service.provider.credentials); // config creds
+ credentials.addProfile(process.env.AWS_DEFAULT_PROFILE || 'default');
// Store the credentials to avoid creating them again (messes up MFA).
- this.cachedCredentials = result;
- return result;
+ const region = this.getRegion();
+ this.cachedCredentials = { credentials, region };
+ return this.cachedCredentials;
}
// This function will be used to block the addition of transfer acceleration options
diff --git a/node_modules/serverless/lib/plugins/aws/invokeLocal/index.js b/node_modules/serverless/lib/plugins/aws/invokeLocal/index.js
index 123e8e1..5e099e3 100644
--- a/node_modules/serverless/lib/plugins/aws/invokeLocal/index.js
+++ b/node_modules/serverless/lib/plugins/aws/invokeLocal/index.js
@@ -15,6 +15,7 @@ const download = require('download');
const { ensureDir } = require('fs-extra');
const cachedir = require('cachedir');
const decompress = require('decompress');
+const { v4: uuidv4 } = require('uuid');
const dirExists = require('../../../utils/fs/dirExists');
const fileExists = require('../../../utils/fs/fileExists');
const isStandalone = require('../../../utils/isStandaloneExecutable');
@@ -126,10 +127,11 @@ class AwsInvokeLocal {
});
}
- getCredentialEnvVars() {
+ async getCredentialEnvVars() {
const credentialEnvVars = {};
const { credentials } = this.provider.getCredentials();
if (credentials) {
+ await credentials.getPromise().catch(() => null); // best effort
if (credentials.accessKeyId) {
credentialEnvVars.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
}
@@ -149,64 +151,61 @@ class AwsInvokeLocal {
return _.merge(providerEnvVars, functionEnvVars);
}
- loadEnvVars() {
- return BbPromise.try(() => {
- const lambdaName = this.options.functionObj.name;
- const memorySize =
- Number(this.options.functionObj.memorySize) ||
- Number(this.serverless.service.provider.memorySize) ||
- 1024;
-
- const lambdaDefaultEnvVars = {
- LANG: 'en_US.UTF-8',
- LD_LIBRARY_PATH:
- '/usr/local/lib64/node-v4.3.x/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib', // eslint-disable-line max-len
- LAMBDA_TASK_ROOT: '/var/task',
- LAMBDA_RUNTIME_DIR: '/var/runtime',
- AWS_REGION: this.provider.getRegion(),
- AWS_DEFAULT_REGION: this.provider.getRegion(),
- AWS_LAMBDA_LOG_GROUP_NAME: this.provider.naming.getLogGroupName(lambdaName),
- AWS_LAMBDA_LOG_STREAM_NAME: '2016/12/02/[$LATEST]f77ff5e4026c45bda9a9ebcec6bc9cad',
- AWS_LAMBDA_FUNCTION_NAME: lambdaName,
- AWS_LAMBDA_FUNCTION_MEMORY_SIZE: memorySize,
- AWS_LAMBDA_FUNCTION_VERSION: '$LATEST',
- NODE_PATH: '/var/runtime:/var/task:/var/runtime/node_modules',
- };
+ async loadEnvVars() {
+ const lambdaName = this.options.functionObj.name;
+ const memorySize =
+ Number(this.options.functionObj.memorySize) ||
+ Number(this.serverless.service.provider.memorySize) ||
+ 1024;
+
+ const lambdaDefaultEnvVars = {
+ LANG: 'en_US.UTF-8',
+ LD_LIBRARY_PATH:
+ '/usr/local/lib64/node-v4.3.x/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib', // eslint-disable-line max-len
+ LAMBDA_TASK_ROOT: '/var/task',
+ LAMBDA_RUNTIME_DIR: '/var/runtime',
+ AWS_REGION: this.provider.getRegion(),
+ AWS_DEFAULT_REGION: this.provider.getRegion(),
+ AWS_LAMBDA_LOG_GROUP_NAME: this.provider.naming.getLogGroupName(lambdaName),
+ AWS_LAMBDA_LOG_STREAM_NAME: '2016/12/02/[$LATEST]f77ff5e4026c45bda9a9ebcec6bc9cad',
+ AWS_LAMBDA_FUNCTION_NAME: lambdaName,
+ AWS_LAMBDA_FUNCTION_MEMORY_SIZE: memorySize,
+ AWS_LAMBDA_FUNCTION_VERSION: '$LATEST',
+ NODE_PATH: '/var/runtime:/var/task:/var/runtime/node_modules',
+ };
- const credentialEnvVars = this.getCredentialEnvVars();
+ const credentialEnvVars = await this.getCredentialEnvVars();
- // profile override from config
- const profileOverride = this.provider.getProfile();
- if (profileOverride) {
- lambdaDefaultEnvVars.AWS_PROFILE = profileOverride;
- }
+ // profile override from config
+ const profileOverride = this.provider.getProfile();
+ if (profileOverride) {
+ lambdaDefaultEnvVars.AWS_PROFILE = profileOverride;
+ }
- const configuredEnvVars = this.getConfiguredEnvVars();
+ const configuredEnvVars = this.getConfiguredEnvVars();
- const promises = Object.entries(configuredEnvVars).map(async ([name, value]) => {
- if (!_.isObject(value)) return;
- try {
- if (value['Fn::ImportValue']) {
- configuredEnvVars[name] = await resolveCfImportValue(
- this.provider,
- value['Fn::ImportValue']
- );
- } else if (value.Ref) {
- configuredEnvVars[name] = await resolveCfRefValue(this.provider, value.Ref);
- } else {
- throw new Error(`Unsupported format: ${inspect(value)}`);
- }
- } catch (error) {
- throw new this.serverless.classes.Error(
- `Could not resolve "${name}" environment variable: ${error.message}`
+ const promises = Object.entries(configuredEnvVars).map(async ([name, value]) => {
+ if (!_.isObject(value)) return;
+ try {
+ if (value['Fn::ImportValue']) {
+ configuredEnvVars[name] = await resolveCfImportValue(
+ this.provider,
+ value['Fn::ImportValue']
);
+ } else if (value.Ref) {
+ configuredEnvVars[name] = await resolveCfRefValue(this.provider, value.Ref);
+ } else {
+ throw new Error(`Unsupported format: ${inspect(value)}`);
}
- });
-
- return BbPromise.all(promises).then(() => {
- _.merge(process.env, lambdaDefaultEnvVars, credentialEnvVars, configuredEnvVars);
- });
+ } catch (error) {
+ throw new this.serverless.classes.Error(
+ `Could not resolve "${name}" environment variable: ${error.message}`
+ );
+ }
});
+
+ await Promise.all(promises);
+ _.merge(process.env, lambdaDefaultEnvVars, credentialEnvVars, configuredEnvVars);
}
invokeLocal() {
@@ -466,77 +465,72 @@ class AwsInvokeLocal {
return this.serverless.pluginManager.spawn('package');
}
- invokeLocalDocker() {
+ async invokeLocalDocker() {
const handler = this.options.functionObj.handler;
const runtime = this.getRuntime();
- return this.ensurePackage()
- .then(() => this.checkDockerDaemonStatus())
- .then(() =>
- BbPromise.all([
- this.checkDockerImage(`lambci/lambda:${runtime}`).then(exists => {
- return exists ? {} : this.pullDockerImage();
- }),
- this.getLayerPaths().then(layerPaths => this.buildDockerImage(layerPaths)),
- this.extractArtifact(),
- ]).then(results => {
- const imageName = results[1];
- const artifactPath = results[2];
-
- const lambdaName = this.options.functionObj.name;
- const memorySize =
- Number(this.options.functionObj.memorySize) ||
- Number(this.serverless.service.provider.memorySize) ||
- 1024;
- const lambdaDefaultEnvVars = {
- AWS_REGION: this.provider.getRegion(),
- AWS_DEFAULT_REGION: this.provider.getRegion(),
- AWS_LAMBDA_LOG_GROUP_NAME: this.provider.naming.getLogGroupName(lambdaName),
- AWS_LAMBDA_FUNCTION_NAME: lambdaName,
- AWS_LAMBDA_FUNCTION_MEMORY_SIZE: memorySize,
- };
- const credentialEnvVars = this.getCredentialEnvVars();
- const configuredEnvVars = this.getConfiguredEnvVars();
- const envVarsFromOptions = this.getEnvVarsFromOptions();
- const envVars = _.merge(
- lambdaDefaultEnvVars,
- credentialEnvVars,
- configuredEnvVars,
- envVarsFromOptions
- );
- const envVarsDockerArgs = _.flatMap(envVars, (value, key) => [
- '--env',
- `${key}=${value}`,
- ]);
-
- const dockerArgsFromOptions = this.getDockerArgsFromOptions();
- const dockerArgs = [
- 'run',
- '--rm',
- '-v',
- `${artifactPath}:/var/task:ro,delegated`,
- ].concat(envVarsDockerArgs, dockerArgsFromOptions, [
- imageName,
- handler,
- JSON.stringify(this.options.data),
- ]);
-
- return spawnExt('docker', dockerArgs).then(
- ({ stdBuffer }) => {
- if (stdBuffer) {
- process.stdout.write(stdBuffer);
- }
- return imageName;
- },
- ({ code, stdBuffer }) => {
- if (stdBuffer) {
- process.stdout.write(stdBuffer);
- }
- throw new Error(`Failed to run docker for ${runtime} image (exit code ${code}})`);
- }
- );
- })
- );
+ await this.ensurePackage();
+ await this.checkDockerDaemonStatus();
+ const results = await Promise.all([
+ this.checkDockerImage(`lambci/lambda:${runtime}`).then(exists => {
+ return exists ? {} : this.pullDockerImage();
+ }),
+ this.getLayerPaths().then(layerPaths => this.buildDockerImage(layerPaths)),
+ this.extractArtifact(),
+ ]);
+
+ const imageName = results[1];
+ const artifactPath = results[2];
+
+ const lambdaName = this.options.functionObj.name;
+ const memorySize =
+ Number(this.options.functionObj.memorySize) ||
+ Number(this.serverless.service.provider.memorySize) ||
+ 1024;
+ const lambdaDefaultEnvVars = {
+ AWS_REGION: this.provider.getRegion(),
+ AWS_DEFAULT_REGION: this.provider.getRegion(),
+ AWS_LAMBDA_LOG_GROUP_NAME: this.provider.naming.getLogGroupName(lambdaName),
+ AWS_LAMBDA_FUNCTION_NAME: lambdaName,
+ AWS_LAMBDA_FUNCTION_MEMORY_SIZE: memorySize,
+ };
+ const credentialEnvVars = await this.getCredentialEnvVars();
+ const configuredEnvVars = this.getConfiguredEnvVars();
+ const envVarsFromOptions = this.getEnvVarsFromOptions();
+ const envVars = _.merge(
+ lambdaDefaultEnvVars,
+ credentialEnvVars,
+ configuredEnvVars,
+ envVarsFromOptions
+ );
+ const envVarsDockerArgs = _.flatMap(envVars, (value, key) => ['--env', `${key}=${value}`]);
+
+ const dockerArgsFromOptions = this.getDockerArgsFromOptions();
+ const dockerArgs = [
+ 'run',
+ '--rm',
+ '-v',
+ `${artifactPath}:/var/task:ro,delegated`,
+ ].concat(envVarsDockerArgs, dockerArgsFromOptions, [
+ imageName,
+ handler,
+ JSON.stringify(this.options.data),
+ ]);
+
+ return spawnExt('docker', dockerArgs).then(
+ ({ stdBuffer }) => {
+ if (stdBuffer) {
+ process.stdout.write(stdBuffer);
+ }
+ return imageName;
+ },
+ ({ code, stdBuffer }) => {
+ if (stdBuffer) {
+ process.stdout.write(stdBuffer);
+ }
+ throw new Error(`Failed to run docker for ${runtime} image (exit code ${code}})`);
+ }
+ );
}
getDockerArgsFromOptions() {
@@ -834,7 +828,7 @@ class AwsInvokeLocal {
Number(this.serverless.service.provider.timeout) ||
6;
let context = {
- awsRequestId: 'id',
+ awsRequestId: uuidv4(),
invokeid: 'id',
logGroupName: this.provider.naming.getLogGroupName(this.options.functionObj.name),
logStreamName: '2015/09/22/[HEAD]13370a84ca4ed8b77c427af260',
diff --git a/node_modules/serverless/lib/plugins/aws/invokeLocal/index.test.js b/node_modules/serverless/lib/plugins/aws/invokeLocal/index.test.js
index 9474818..091717a 100644
--- a/node_modules/serverless/lib/plugins/aws/invokeLocal/index.test.js
+++ b/node_modules/serverless/lib/plugins/aws/invokeLocal/index.test.js
@@ -68,7 +68,7 @@ describe('AwsInvokeLocal', () => {
serverless.processedInput = { commands: ['invoke'] };
provider = new AwsProvider(serverless, options);
provider.cachedCredentials = {
- credentials: { accessKeyId: 'foo', secretAccessKey: 'bar' },
+ credentials: { getPromise: async () => null, accessKeyId: 'foo', secretAccessKey: 'bar' },
};
serverless.setProvider('aws', provider);
awsInvokeLocal = new AwsInvokeLocal(serverless, options);
@@ -291,18 +291,19 @@ describe('AwsInvokeLocal', () => {
});
describe('#getCredentialEnvVars()', () => {
- it('returns empty object when credentials is not set', () => {
+ it('returns empty object when credentials is not set', async () => {
provider.cachedCredentials = null;
serverless.service.provider.credentials = null;
- const credentialEnvVars = awsInvokeLocal.getCredentialEnvVars();
+ const credentialEnvVars = await awsInvokeLocal.getCredentialEnvVars();
expect(credentialEnvVars).to.be.eql({});
});
- it('returns credential env vars from cached credentials', () => {
+ it('returns credential env vars from cached credentials', async () => {
provider.cachedCredentials = {
credentials: {
+ getPromise: async () => {},
accessKeyId: 'ID',
secretAccessKey: 'SECRET',
sessionToken: 'TOKEN',
@@ -310,7 +311,7 @@ describe('AwsInvokeLocal', () => {
};
serverless.service.provider.credentials = null;
- const credentialEnvVars = awsInvokeLocal.getCredentialEnvVars();
+ const credentialEnvVars = await awsInvokeLocal.getCredentialEnvVars();
expect(credentialEnvVars).to.be.eql({
AWS_ACCESS_KEY_ID: 'ID',
@@ -319,15 +320,16 @@ describe('AwsInvokeLocal', () => {
});
});
- it('returns credential env vars from credentials config', () => {
+ it('returns credential env vars from credentials config', async () => {
provider.cachedCredentials = null;
serverless.service.provider.credentials = {
+ getPromise: async () => {},
accessKeyId: 'ID',
secretAccessKey: 'SECRET',
sessionToken: 'TOKEN',
};
- const credentialEnvVars = awsInvokeLocal.getCredentialEnvVars();
+ const credentialEnvVars = await awsInvokeLocal.getCredentialEnvVars();
expect(credentialEnvVars).to.be.eql({
AWS_ACCESS_KEY_ID: 'ID',
@@ -401,6 +403,7 @@ describe('AwsInvokeLocal', () => {
it('it should set credential env vars #1', () => {
provider.cachedCredentials = {
credentials: {
+ getPromise: async () => null,
accessKeyId: 'ID',
secretAccessKey: 'SECRET',
},
@@ -416,6 +419,7 @@ describe('AwsInvokeLocal', () => {
it('it should set credential env vars #2', () => {
provider.cachedCredentials = {
credentials: {
+ getPromise: async () => null,
sessionToken: 'TOKEN',
},
};
@@ -1079,7 +1083,7 @@ describe('AwsInvokeLocal', () => {
() => {
log.debug('test target %o', serverless.cli.consoleLog.lastCall.args);
const result = JSON.parse(serverless.cli.consoleLog.lastCall.args[0]);
- expect(result.deadlineMs).to.be.closeTo(Date.now() + 6000, 1000);
+ expect(result.deadlineMs).to.be.closeTo(Date.now() + 6000, 2000);
},
error => {
if (error.code === 'ENOENT' && error.path === 'ruby') {
diff --git a/node_modules/serverless/lib/plugins/aws/provider/awsCredentials.js b/node_modules/serverless/lib/plugins/aws/provider/awsCredentials.js
new file mode 100644
index 0000000..dda61ea
--- /dev/null
+++ b/node_modules/serverless/lib/plugins/aws/provider/awsCredentials.js
@@ -0,0 +1,75 @@
+'use strict';
+
+const AWS = require('aws-sdk');
+const readline = require('readline');
+
+const notEmpty = s => typeof s === 'string' && s.trim().length > 0;
+
+module.exports = class AwsCredentials extends AWS.Credentials {
+ constructor() {
+ super();
+ this.chain = new AWS.CredentialProviderChain([]); // providers are added explicitly
+ }
+
+ refresh(callback) {
+ this.chain.resolve((err, res) => {
+ if (err) {
+ callback(err);
+ } else {
+ AWS.Credentials.call(this, res);
+ callback();
+ }
+ });
+ }
+
+ /**
+ * Add credentials, if present and valid, from provider config
+ * @param credentials The credentials to test for validity
+ */
+ addConfig(credentials) {
+ if (credentials) {
+ if (
+ notEmpty(credentials.accessKeyId) &&
+ (notEmpty(credentials.secretAccessKey) || notEmpty(credentials.sessionToken))
+ ) {
+ this.chain.providers.push(new AWS.Credentials(credentials));
+ }
+ }
+ }
+
+ /**
+ * Add credentials, if present, from the environment
+ * @param prefix The environment variable prefix to use in extracting credentials
+ */
+ addEnvironment(prefix) {
+ if (prefix) {
+ const environmentCredentials = new AWS.EnvironmentCredentials(prefix);
+ this.chain.providers.push(environmentCredentials);
+ }
+ }
+
+ /**
+ * Add credentials from a profile
+ * @param profile The profile to load credentials from
+ */
+ addProfile(profile) {
+ if (profile) {
+ const params = { profile };
+ if (process.env.AWS_SHARED_CREDENTIALS_FILE) {
+ params.filename = process.env.AWS_SHARED_CREDENTIALS_FILE;
+ }
+
+ // Setup a MFA callback for asking the code from the user.
+ params.tokenCodeFn = (mfaSerial, callback) => {
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
+ rl.question(`Enter MFA code for ${mfaSerial}: `, answer => {
+ rl.close();
+ callback(null, answer);
+ });
+ };
+
+ this.chain.providers.push(() => new AWS.SharedIniFileCredentials(params));
+ this.chain.providers.push(() => new AWS.ProcessCredentials(params));
+ }
+ }
+};
diff --git a/node_modules/serverless/lib/plugins/aws/provider/awsProvider.js b/node_modules/serverless/lib/plugins/aws/provider/awsProvider.js
index f4eb592..3426f94 100644
--- a/node_modules/serverless/lib/plugins/aws/provider/awsProvider.js
+++ b/node_modules/serverless/lib/plugins/aws/provider/awsProvider.js
@@ -11,17 +11,45 @@ const https = require('https');
const fs = require('fs');
const objectHash = require('object-hash');
const PromiseQueue = require('promise-queue');
+const AwsCredentials = require('./awsCredentials');
const getS3EndpointForRegion = require('../utils/getS3EndpointForRegion');
-const readline = require('readline');
const { ALB_LISTENER_REGEXP } = require('../package/compile/events/alb/lib/validate');
const path = require('path');
const isLambdaArn = RegExp.prototype.test.bind(/^arn:[^:]+:lambda:/);
+function caseInsensitive(str) {
+ return { type: 'string', regexp: new RegExp(`^${str}$`, 'i').toString() };
+}
+
const constants = {
providerName: 'aws',
};
+const apiGatewayUsagePlan = {
+ type: 'object',
+ properties: {
+ quota: {
+ type: 'object',
+ properties: {
+ limit: { type: 'integer', minimum: 0 },
+ offset: { type: 'integer', minimum: 0 },
+ period: { enum: ['DAY', 'WEEK', 'MONTH'] },
+ },
+ additionalProperties: false,
+ },
+ throttle: {
+ type: 'object',
+ properties: {
+ burstLimit: { type: 'integer', minimum: 0 },
+ rateLimit: { type: 'integer', minimum: 0 },
+ },
+ additionalProperties: false,
+ },
+ },
+ additionalProperties: false,
+};
+
PromiseQueue.configure(BbPromise.Promise);
const MAX_RETRIES = (() => {
@@ -29,101 +57,6 @@ const MAX_RETRIES = (() => {
return userValue >= 0 ? userValue : 4;
})();
-const impl = {
- /**
- * Determine whether the given credentials are valid. It turned out that detecting invalid
- * credentials was more difficult than detecting the positive cases we know about. Hooray for
- * whak-a-mole!
- * @param credentials The credentials to test for validity
- * @return {boolean} Whether the given credentials were valid
- */
- validCredentials: credentials => {
- let result = false;
- if (credentials) {
- if (
- // valid credentials loaded
- (credentials.accessKeyId &&
- credentials.accessKeyId !== 'undefined' &&
- credentials.secretAccessKey &&
- credentials.secretAccessKey !== 'undefined') ||
- // a role to assume has been successfully loaded, the associated STS request has been
- // sent, and the temporary credentials will be asynchronously delivered.
- credentials.roleArn
- ) {
- result = true;
- }
- }
- return result;
- },
- /**
- * Add credentials, if present, to the given results
- * @param results The results to add the given credentials to if they are valid
- * @param credentials The credentials to validate and add to the results if valid
- */
- addCredentials: (results, credentials) => {
- if (impl.validCredentials(credentials)) {
- results.credentials = credentials; // eslint-disable-line no-param-reassign
- }
- },
- /**
- * Add credentials, if present, from the environment
- * @param results The results to add environment credentials to
- * @param prefix The environment variable prefix to use in extracting credentials
- */
- addEnvironmentCredentials: (results, prefix) => {
- if (prefix) {
- const environmentCredentials = new AWS.EnvironmentCredentials(prefix);
- impl.addCredentials(results, environmentCredentials);
- }
- },
- /**
- * Add credentials from a profile, if the profile and credentials for it exists
- * @param results The results to add profile credentials to
- * @param profile The profile to load credentials from
- */
- addProfileCredentials: (results, profile) => {
- if (profile) {
- const params = { profile };
- if (process.env.AWS_SHARED_CREDENTIALS_FILE) {
- params.filename = process.env.AWS_SHARED_CREDENTIALS_FILE;
- }
-
- // Setup a MFA callback for asking the code from the user.
- params.tokenCodeFn = (mfaSerial, callback) => {
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
- rl.question(`Enter MFA code for ${mfaSerial}: `, answer => {
- rl.close();
- callback(null, answer);
- });
- };
-
- const profileCredentials = new AWS.SharedIniFileCredentials(params);
- if (
- !(
- profileCredentials.accessKeyId ||
- profileCredentials.sessionToken ||
- profileCredentials.roleArn
- )
- ) {
- throw new Error(`Profile ${profile} does not exist`);
- }
-
- impl.addCredentials(results, profileCredentials);
- }
- },
- /**
- * Add credentials, if present, from a profile that is specified within the environment
- * @param results The prefix of the profile's declaration in the environment
- * @param prefix The prefix for the environment variable
- */
- addEnvironmentProfile: (results, prefix) => {
- if (prefix) {
- const profile = process.env[`${prefix}_PROFILE`];
- impl.addProfileCredentials(results, profile);
- }
- },
-};
-
const baseAlbAuthorizerProperties = {
onUnauthenticatedRequest: { enum: ['allow', 'authenticate', 'deny'] },
requestExtraParams: {
@@ -173,10 +106,6 @@ const cognitoAlbAuthorizer = {
};
class AwsProvider {
- static getProviderName() {
- return constants.providerName;
- }
-
constructor(serverless, options) {
this.naming = { provider: this };
this.options = options;
@@ -202,7 +131,7 @@ class AwsProvider {
pattern: '^[a-zA-Z0-9._\\-]+$',
},
awsArn: {
- oneOf: [
+ anyOf: [
{ $ref: '#/definitions/awsArnString' },
{ $ref: '#/definitions/awsCfFunction' },
],
@@ -212,7 +141,7 @@ class AwsProvider {
pattern: '^arn:',
},
awsCfFunction: {
- oneOf: [
+ anyOf: [
{ $ref: '#/definitions/awsCfImport' },
{ $ref: '#/definitions/awsCfJoin' },
{ $ref: '#/definitions/awsCfGetAtt' },
@@ -250,7 +179,7 @@ class AwsProvider {
required: ['Fn::ImportValue'],
},
awsCfInstruction: {
- oneOf: [{ type: 'string', minLength: 1 }, { $ref: '#/definitions/awsCfFunction' }],
+ anyOf: [{ type: 'string', minLength: 1 }, { $ref: '#/definitions/awsCfFunction' }],
},
awsCfJoin: {
type: 'object',
@@ -259,7 +188,7 @@ class AwsProvider {
type: 'array',
minItems: 2,
maxItems: 2,
- items: [{ type: 'string', minLength: 1 }, { type: 'array' }],
+ items: [{ type: 'string' }, { type: 'array' }],
additionalItems: false,
},
},
@@ -282,6 +211,58 @@ class AwsProvider {
required: ['Fn::Sub'],
additionalProperties: false,
},
+ awsIamPolicyAction: { type: 'array', items: { type: 'string' } },
+ awsIamPolicyPrincipal: {
+ anyOf: [
+ { const: '*' },
+ {
+ type: 'object',
+ properties: {
+ AWS: {
+ anyOf: [
+ { const: '*' },
+ { type: 'array', items: { $ref: '#/definitions/awsArn' } },
+ ],
+ },
+ Federated: { type: 'array', items: { type: 'string' } },
+ Service: { type: 'array', items: { type: 'string' } },
+ CanonicalUser: { type: 'array', items: { type: 'string' } },
+ },
+ additionalProperties: false,
+ },
+ ],
+ },
+ awsIamPolicyResource: {
+ anyOf: [
+ { const: '*' },
+ { $ref: '#/definitions/awsArn' },
+ { type: 'array', items: { $ref: '#/definitions/awsArn' } },
+ ],
+ },
+ // Definition of Statement taken from https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_grammar.html#policies-grammar-bnf
+ awsIamPolicyStatements: {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ Sid: { type: 'string' },
+ Effect: { enum: ['Allow', 'Deny'] },
+ Action: { $ref: '#/definitions/awsIamPolicyAction' },
+ NotAction: { $ref: '#/definitions/awsIamPolicyAction' },
+ Principal: { $ref: '#/definitions/awsIamPolicyPrincipal' },
+ NotPrincipal: { $ref: '#/definitions/awsIamPolicyPrincipal' },
+ Resource: { $ref: '#/definitions/awsIamPolicyResource' },
+ NotResource: { $ref: '#/definitions/awsIamPolicyResource' },
+ Condition: { type: 'object' },
+ },
+ additionalProperties: false,
+ allOf: [
+ { required: ['Effect'] },
+ { oneOf: [{ required: ['Action'] }, { required: ['NotAction'] }] },
+ { oneOf: [{ required: ['Resource'] }, { required: ['NotResource'] }] },
+ ],
+ },
+ },
awsLambdaEnvironment: {
type: 'object',
patternProperties: {
@@ -339,10 +320,12 @@ class AwsProvider {
},
},
},
- awsResourceCondition: { type: 'string' },
- awsResourceDependsOn: {
- oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
+ awsLogGroupName: {
+ type: 'string',
+ pattern: '^[/#A-Za-z0-9-_.]+$',
},
+ awsResourceCondition: { type: 'string' },
+ awsResourceDependsOn: { type: 'array', items: { type: 'string' } },
awsResourceProperties: {
Properties: { type: 'object' },
CreationPolicy: { type: 'object' },
@@ -356,22 +339,89 @@ class AwsProvider {
awsResourceTags: {
type: 'object',
patternProperties: {
- '^(?!aws:)[\\w./=+-]{1,128}$': {
+ '^(?!aws:)[\\w./=+:-]{1,128}$': {
type: 'string',
- pattern: '^(?!aws:)[\\w./=+-]*$',
maxLength: 256,
},
},
additionalProperties: false,
},
- awsLogGroupName: {
+ awsS3BucketName: {
type: 'string',
- pattern: '^[/#A-Za-z0-9-_.]+$',
+ // pattern sourced from https://stackoverflow.com/questions/50480924/regex-for-s3-bucket-name
+ pattern:
+ '(?!^(\\d{1,3}\\.){3}\\d{1,3}$)(^(([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])\\.)*([a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])$)',
+ minLength: 3,
+ maxLength: 63,
},
},
provider: {
properties: {
- apiGateway: { type: 'object', properties: { websocketApiId: { type: 'string' } } },
+ apiGateway: {
+ type: 'object',
+ properties: {
+ apiKeySourceType: {
+ anyOf: ['HEADER', 'AUTHORIZER'].map(caseInsensitive),
+ },
+ binaryMediaTypes: {
+ type: 'array',
+ items: { type: 'string', pattern: '^\\S+\\/\\S+$' },
+ },
+ description: { type: 'string' },
+ metrics: { type: 'boolean' },
+ minimumCompressionSize: { type: 'integer', minimum: 0, maximum: 10485760 },
+ restApiId: { $ref: '#/definitions/awsCfInstruction' },
+ restApiResources: {
+ anyOf: [
+ {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ path: { type: 'string' },
+ resourceId: { type: 'string' },
+ },
+ required: [],
+ additionalProperties: false,
+ },
+ },
+ { type: 'object' },
+ ],
+ },
+ restApiRootResourceId: { $ref: '#/definitions/awsCfInstruction' },
+ shouldStartNameWithService: { const: true },
+ websocketApiId: { $ref: '#/definitions/awsCfInstruction' },
+ },
+ additionalProperties: false,
+ },
+ apiKeys: {
+ type: 'array',
+ items: {
+ anyOf: [
+ { type: 'string' },
+ {
+ type: 'object',
+ properties: {
+ name: { type: 'string' },
+ value: { type: 'string' },
+ description: { type: 'string' },
+ customerId: { type: 'string' },
+ },
+ anyOf: [{ required: ['name'] }, { required: ['value'] }],
+ additionalProperties: false,
+ },
+ {
+ type: 'object',
+ maxProperties: 1,
+ additionalProperties: {
+ type: 'array',
+ items: { type: 'string' },
+ },
+ },
+ ],
+ },
+ },
+ apiName: { type: 'string' },
alb: {
type: 'object',
properties: {
@@ -379,12 +429,37 @@ class AwsProvider {
authorizers: {
type: 'object',
additionalProperties: {
- oneOf: [oidcAlbAuthorizer, cognitoAlbAuthorizer],
+ anyOf: [oidcAlbAuthorizer, cognitoAlbAuthorizer],
},
},
},
additionalProperties: false,
},
+ cfnRole: { $ref: '#/definitions/awsArn' },
+ deploymentBucket: {
+ anyOf: [
+ { $ref: '#/definitions/awsS3BucketName' },
+ {
+ type: 'object',
+ properties: {
+ name: { $ref: '#/definitions/awsS3BucketName' },
+ maxPreviousDeploymentArtifacts: { type: 'integer', minimum: 0 },
+ serverSideEncryption: { enum: ['AES256', 'aws:kms'] },
+ sseCustomerAlgorithim: { type: 'string' },
+ sseCustomerKey: { type: 'string' },
+ sseCustomerKeyMD5: { type: 'string' },
+ sseKMSKeyId: { type: 'string' },
+ tags: { $ref: '#/definitions/awsResourceTags' },
+ blockPublicAccess: { type: 'boolean' },
+ },
+ additionalProperties: false,
+ },
+ ],
+ },
+ deploymentPrefix: { type: 'string' },
+ endpointType: {
+ anyOf: ['REGIONAL', 'EDGE', 'PRIVATE'].map(caseInsensitive),
+ },
environment: { $ref: '#/definitions/awsLambdaEnvironment' },
httpApi: {
type: 'object',
@@ -398,12 +473,11 @@ class AwsProvider {
identitySource: { $ref: '#/definitions/awsCfInstruction' },
issuerUrl: { $ref: '#/definitions/awsCfInstruction' },
audience: {
- oneOf: [
+ anyOf: [
{ $ref: '#/definitions/awsCfInstruction' },
{
type: 'array',
items: { $ref: '#/definitions/awsCfInstruction' },
- additionalItems: false,
},
],
},
@@ -413,24 +487,16 @@ class AwsProvider {
},
},
cors: {
- oneOf: [
+ anyOf: [
{ type: 'boolean' },
{
type: 'object',
properties: {
allowCredentials: { type: 'boolean' },
- allowedHeaders: {
- oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
- },
- allowedMethods: {
- oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
- },
- allowedOrigins: {
- oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
- },
- exposedResponseHeaders: {
- oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }],
- },
+ allowedHeaders: { type: 'array', items: { type: 'string' } },
+ allowedMethods: { type: 'array', items: { type: 'string' } },
+ allowedOrigins: { type: 'array', items: { type: 'string' } },
+ exposedResponseHeaders: { type: 'array', items: { type: 'string' } },
maxAge: { type: 'integer', minimum: 0 },
},
additionalProperties: false,
@@ -438,7 +504,7 @@ class AwsProvider {
],
},
id: {
- oneOf: [
+ anyOf: [
{ type: 'string' },
{ $ref: '#/definitions/awsCfImportLocallyResolvable' },
],
@@ -448,13 +514,19 @@ class AwsProvider {
},
additionalProperties: false,
},
+ iamManagedPolicies: { type: 'array', items: { $ref: '#/definitions/awsArnString' } },
+ iamRoleStatements: { $ref: '#/definitions/awsIamPolicyStatements' },
kmsKeyArn: { $ref: '#/definitions/awsKmsArn' },
layers: { $ref: '#/definitions/awsLambdaLayers' },
+ logRetentionInDays: {
+ enum: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653],
+ },
logs: {
type: 'object',
properties: {
+ frameworkLambda: { type: 'boolean' },
httpApi: {
- oneOf: [
+ anyOf: [
{ type: 'boolean' },
{
type: 'object',
@@ -465,8 +537,26 @@ class AwsProvider {
},
],
},
+ restApi: {
+ anyOf: [
+ { type: 'boolean' },
+ {
+ type: 'object',
+ properties: {
+ accessLogging: { type: 'boolean' },
+ executionLogging: { type: 'boolean' },
+ format: { type: 'string' },
+ fullExecutionData: { type: 'boolean' },
+ level: { enum: ['INFO', 'ERROR'] },
+ role: { $ref: '#/definitions/awsArn' },
+ roleManagedExternally: { type: 'boolean' },
+ },
+ additionalProperties: false,
+ },
+ ],
+ },
websocket: {
- oneOf: [
+ anyOf: [
{ type: 'boolean' },
{
type: 'object',
@@ -480,14 +570,81 @@ class AwsProvider {
},
},
memorySize: { $ref: '#/definitions/awsLambdaMemorySize' },
- resourcePolicy: {
+ notificationArns: { type: 'array', items: { $ref: '#/definitions/awsArnString' } },
+ profile: { type: 'string' },
+ region: {
+ enum: [
+ 'us-east-1',
+ 'us-east-2',
+ 'us-gov-east-1',
+ 'us-gov-west-1',
+ 'us-west-1',
+ 'us-west-2',
+ 'af-south-1',
+ 'ap-east-1',
+ 'ap-northeast-1',
+ 'ap-northeast-2',
+ 'ap-northeast-3',
+ 'ap-south-1',
+ 'ap-southeast-1',
+ 'ap-southeast-2',
+ 'ca-central-1',
+ 'cn-north-1',
+ 'cn-northwest-1',
+ 'eu-central-1',
+ 'eu-north-1',
+ 'eu-south-1',
+ 'eu-west-1',
+ 'eu-west-2',
+ 'eu-west-3',
+ 'me-south-1',
+ 'sa-east-1',
+ ],
+ },
+ resourcePolicy: { $ref: '#/definitions/awsIamPolicyStatements' },
+ role: { $ref: '#/definitions/awsLambdaRole' },
+ rolePermissionsBoundary: { $ref: '#/definitions/awsArnString' },
+ rollbackConfiguration: {
+ type: 'object',
+ properties: {
+ RollbackTriggers: {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ Arn: { $ref: '#/definitions/awsArnString' },
+ Type: { const: 'AWS::CloudWatch::Alarm' },
+ },
+ additionalProperties: false,
+ required: ['Arn', 'Type'],
+ },
+ },
+ MonitoringTimeInMinutes: { type: 'integer', minimum: 0 },
+ },
+ additionalProperties: false,
+ },
+ runtime: { $ref: '#/definitions/awsLambdaRuntime' },
+ stage: { type: 'string' },
+ stackName: {
+ type: 'string',
+ pattern: '^[a-zA-Z][a-zA-Z0-9-]*$',
+ maxLength: 128,
+ },
+ stackParameters: {
type: 'array',
items: {
type: 'object',
+ properties: {
+ ParameterKey: { type: 'string' },
+ ParameterValue: { type: 'string' },
+ UsePreviousValue: { type: 'boolean' },
+ ResolvedValue: { type: 'string' },
+ },
+ additionalProperties: false,
},
},
- role: { $ref: '#/definitions/awsLambdaRole' },
- runtime: { $ref: '#/definitions/awsLambdaRuntime' },
+ stackPolicy: { $ref: '#/definitions/awsIamPolicyStatements' },
+ stackTags: { $ref: '#/definitions/awsResourceTags' },
tags: { $ref: '#/definitions/awsResourceTags' },
timeout: { $ref: '#/definitions/awsLambdaTimeout' },
tracing: {
@@ -498,7 +655,24 @@ class AwsProvider {
},
additionalProperties: false,
},
+ usagePlan: {
+ anyOf: [
+ apiGatewayUsagePlan,
+ {
+ type: 'array',
+ items: {
+ type: 'object',
+ additionalProperties: apiGatewayUsagePlan,
+ maxProperties: 1,
+ },
+ },
+ ],
+ },
vpc: { $ref: '#/definitions/awsLambdaVpcConfig' },
+ vpcEndpointIds: {
+ type: 'array',
+ items: { $ref: '#/definitions/awsCfInstruction' },
+ },
versionFunctions: { $ref: '#/definitions/awsLambdaVersionning' },
websocketsApiName: { type: 'string' },
websocketsApiRouteSelectionExpression: { type: 'string' },
@@ -510,13 +684,21 @@ class AwsProvider {
condition: { $ref: '#/definitions/awsResourceCondition' },
dependsOn: { $ref: '#/definitions/awsResourceDependsOn' },
description: { type: 'string', maxLength: 256 },
+ destinations: {
+ type: 'object',
+ properties: {
+ onSuccess: { type: 'string', minLength: 1 },
+ onFailure: { type: 'string', minLength: 1 },
+ },
+ additionalProperties: false,
+ },
disableLogs: { type: 'boolean' },
environment: { $ref: '#/definitions/awsLambdaEnvironment' },
fileSystemConfig: {
type: 'object',
properties: {
arn: {
- oneOf: [
+ anyOf: [
{
type: 'string',
pattern:
@@ -535,9 +717,11 @@ class AwsProvider {
handler: { type: 'string' },
kmsKeyArn: { $ref: '#/definitions/awsKmsArn' },
layers: { $ref: '#/definitions/awsLambdaLayers' },
+ maximumEventAge: { type: 'integer', minimum: 60, maximum: 21600 },
+ maximumRetryAttempts: { type: 'integer', minimum: 0, maximum: 2 },
memorySize: { $ref: '#/definitions/awsLambdaMemorySize' },
onError: {
- oneOf: [
+ anyOf: [
{ type: 'string', pattern: '^arn:aws[a-z-]*:sns' },
{ $ref: '#/definitions/awsCfFunction' },
],
@@ -564,6 +748,47 @@ class AwsProvider {
},
additionalProperties: false,
},
+ layers: {
+ type: 'object',
+ additionalProperties: {
+ type: 'object',
+ properties: {
+ allowedAccounts: {
+ type: 'array',
+ items: {
+ type: 'string',
+ pattern: '^(\\d{12}|\\*|arn:(aws[a-zA-Z-]*):iam::\\d{12}:root)$',
+ },
+ },
+ compatibleRuntimes: {
+ type: 'array',
+ items: { $ref: '#/definitions/awsLambdaRuntime' },
+ maxItems: 5,
+ },
+ description: { type: 'string', maxLength: 256 },
+ licenseInfo: { type: 'string', maxLength: 512 },
+ name: {
+ type: 'string',
+ minLength: 1,
+ maxLength: 140,
+ pattern:
+ '^((arn:[a-zA-Z0-9-]+:lambda:[a-zA-Z0-9-]+:\\d{12}:layer:[a-zA-Z0-9-_]+)|[a-zA-Z0-9-_]+)$',
+ },
+ package: {
+ type: 'object',
+ properties: {
+ artifact: { type: 'string' },
+ exclude: { type: 'array', items: { type: 'string' } },
+ include: { type: 'array', items: { type: 'string' } },
+ },
+ additionalProperties: false,
+ },
+ path: { type: 'string' },
+ retain: { type: 'boolean' },
+ },
+ additionalProperties: false,
+ },
+ },
resources: {
properties: {
AWSTemplateFormatVersion: {
@@ -597,6 +822,17 @@ class AwsProvider {
// See also https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html
Resources: {
type: 'object',
+ properties: {
+ 'Fn::Transform': {
+ type: 'object',
+ properties: {
+ Name: { type: 'string' },
+ Parameters: { type: 'object' },
+ },
+ required: ['Name'],
+ additionalProperties: false,
+ },
+ },
patternProperties: {
'^[a-zA-Z0-9]{1,255}$': {
type: 'object',
@@ -718,6 +954,10 @@ class AwsProvider {
}
}
+ static getProviderName() {
+ return constants.providerName;
+ }
+
/**
* Execute an AWS request by calling the AWS SDK
* @param {string} service - Service name
@@ -729,14 +969,13 @@ class AwsProvider {
*/
request(service, method, params, options) {
const that = this;
- const credentials = Object.assign({}, that.getCredentials());
- credentials.region = this.getRegion();
// Make sure options is an object (honors wrong calls of request)
const requestOptions = _.isObject(options) ? options : {};
const shouldCache = _.get(requestOptions, 'useCache', false);
- const paramsWithRegion = _.merge({}, params, {
- region: _.get(options, 'region'),
- });
+ const { credentials } = that.getCredentials();
+ const region = requestOptions.region || that.getRegion();
+ const serviceParams = { credentials, region };
+ const paramsWithRegion = { ...params, region };
const paramsHash = objectHash.sha1(paramsWithRegion);
const BASE_BACKOFF = 5000;
const persistentRequest = f =>
@@ -783,8 +1022,20 @@ class AwsProvider {
}
// Support S3 Transfer Acceleration
- if (this.canUseS3TransferAcceleration(service, method)) {
- this.enableS3TransferAcceleration(credentials);
+ if (service === 'S3') {
+ // Support S3 Transfer Acceleration
+ if (this.canUseS3TransferAcceleration(service, method)) {
+ this.enableS3TransferAcceleration(serviceParams);
+ }
+
+ const deploymentBucketObject = this.serverless.service.provider.deploymentBucketObject;
+ if (
+ deploymentBucketObject &&
+ deploymentBucketObject.serverSideEncryption &&
+ deploymentBucketObject.serverSideEncryption === 'aws:kms'
+ ) {
+ serviceParams.signatureVersion = 'v4';
+ }
}
if (shouldCache) {
@@ -796,11 +1047,8 @@ class AwsProvider {
const request = this.requestQueue.add(() =>
persistentRequest(() => {
- if (options && options.region) {
- credentials.region = options.region;
- }
const Service = _.get(that.sdk, service);
- const awsService = new Service(credentials);
+ const awsService = new Service(serviceParams);
const req = awsService[method](params);
// TODO: Add listeners, put Debug statements here...
@@ -812,7 +1060,7 @@ class AwsProvider {
req.send(cb);
});
return promise.catch(err => {
- let message = err.message != null ? err.message : err.code;
+ let message = err.message != null ? err.message : String(err.code);
if (message.startsWith('Missing credentials in config')) {
// Credentials error
// If failed at last resort (EC2 Metadata check) expose a meaningful error
@@ -865,49 +1113,37 @@ class AwsProvider {
/**
* Fetch credentials directly or using a profile from serverless yml configuration or from the
* well known environment variables
- * @returns {{region: *}}
+ * If using outside of AWS, you are expected to use credentials.get/Promise to ensure they have
+ * not expired (https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Credentials.html#getPromise-property)
+ * @returns {credentials: AWS.Credentials, region: string}
*/
getCredentials() {
if (this.cachedCredentials) {
// We have already created the credentials object once, so return it.
return this.cachedCredentials;
}
- const result = {};
- const stageUpper = this.getStage() ? this.getStage().toUpperCase() : null;
-
- // add specified credentials, overriding with more specific declarations
- try {
- impl.addProfileCredentials(result, 'default');
- } catch (err) {
- if (err.message !== 'Profile default does not exist') {
- throw err;
- }
- }
- impl.addCredentials(result, this.serverless.service.provider.credentials); // config creds
+ const credentials = new AwsCredentials();
+ const stageUpperCase = this.getStage() ? this.getStage().toUpperCase() : null;
+
+ // add credentials to resolve from most-to-least specific
+ credentials.addProfile(this.options['aws-profile']); // CLI option profile
+
+ credentials.addProfile(process.env[`AWS_${stageUpperCase}_PROFILE`]); // stage specific creds
+ credentials.addEnvironment(`AWS_${stageUpperCase}`);
+
+ credentials.addProfile(process.env.AWS_PROFILE); // creds for all stages
+ credentials.addEnvironment('AWS');
if (this.serverless.service.provider.profile && !this.options['aws-profile']) {
// config profile
- impl.addProfileCredentials(result, this.serverless.service.provider.profile);
- }
- impl.addEnvironmentCredentials(result, 'AWS'); // creds for all stages
- impl.addEnvironmentProfile(result, 'AWS');
- impl.addEnvironmentCredentials(result, `AWS_${stageUpper}`); // stage specific creds
- impl.addEnvironmentProfile(result, `AWS_${stageUpper}`);
- if (this.options['aws-profile']) {
- impl.addProfileCredentials(result, this.options['aws-profile']); // CLI option profile
- }
-
- const deploymentBucketObject = this.serverless.service.provider.deploymentBucketObject;
- if (
- deploymentBucketObject &&
- deploymentBucketObject.serverSideEncryption &&
- deploymentBucketObject.serverSideEncryption === 'aws:kms'
- ) {
- result.signatureVersion = 'v4';
+ credentials.addProfile(this.serverless.service.provider.profile);
}
+ credentials.addConfig(this.serverless.service.provider.credentials); // config creds
+ credentials.addProfile(process.env.AWS_DEFAULT_PROFILE || 'default');
// Store the credentials to avoid creating them again (messes up MFA).
- this.cachedCredentials = result;
- return result;
+ const region = this.getRegion();
+ this.cachedCredentials = { credentials, region };
+ return this.cachedCredentials;
}
canUseS3TransferAcceleration(service, method) {
@@ -940,9 +1176,9 @@ class AwsProvider {
delete this.options['aws-s3-accelerate'];
}
- enableS3TransferAcceleration(credentials) {
+ enableS3TransferAcceleration(serviceParams) {
this.serverless.cli.log('Using S3 Transfer Acceleration Endpoint...');
- credentials.useAccelerateEndpoint = true; // eslint-disable-line no-param-reassign
+ serviceParams.useAccelerateEndpoint = true; // eslint-disable-line no-param-reassign
}
getValues(source, objectPaths) {
@@ -951,6 +1187,7 @@ class AwsProvider {
value: _.get(source, objectPath.join('.')),
}));
}
+
firstValue(values) {
return values.reduce((result, current) => {
return result.value ? result : current;
@@ -965,6 +1202,7 @@ class AwsProvider {
]);
return this.firstValue(values);
}
+
getRegion() {
const defaultRegion = 'us-east-1';
const regionSourceValue = this.getRegionSourceValue();
@@ -975,6 +1213,7 @@ class AwsProvider {
const values = this.getValues(this, [['serverless', 'service', 'provider', 'runtime']]);
return this.firstValue(values);
}
+
getRuntime(runtime) {
const defaultRuntime = 'nodejs12.x';
const runtimeSourceValue = this.getRuntimeSourceValue();
@@ -991,6 +1230,7 @@ class AwsProvider {
const firstVal = this.firstValue(values);
return firstVal ? firstVal.value : null;
}
+
getProfile() {
return this.getProfileSourceValue();
}
@@ -1077,17 +1317,7 @@ class AwsProvider {
}
getLogRetentionInDays() {
- if (!this.serverless.service.provider.logRetentionInDays) {
- return null;
- }
- const rawRetentionInDays = this.serverless.service.provider.logRetentionInDays;
- const retentionInDays = parseInt(rawRetentionInDays, 10);
- if (retentionInDays > 0) {
- return retentionInDays;
- }
-
- const errorMessage = `logRetentionInDays should be an integer over 0 but ${rawRetentionInDays}`;
- throw new this.serverless.classes.Error(errorMessage);
+ return this.serverless.service.provider.logRetentionInDays;
}
getStageSourceValue() {
@@ -1098,6 +1328,7 @@ class AwsProvider {
]);
return this.firstValue(values);
}
+
getStage() {
const defaultStage = 'dev';
const stageSourceValue = this.getStageSourceValue();
@@ -1174,10 +1405,6 @@ class AwsProvider {
return this.serverless.service.provider.apiGateway.restApiResources;
}
- if (typeof this.serverless.service.provider.apiGateway.restApiResources !== 'object') {
- throw new Error('REST API resource must be an array of object');
- }
-
return Object.keys(this.serverless.service.provider.apiGateway.restApiResources).map(key => ({
path: key,
resourceId: this.serverless.service.provider.apiGateway.restApiResources[key],
diff --git a/node_modules/serverless/lib/plugins/aws/provider/awsProvider.test.js b/node_modules/serverless/lib/plugins/aws/provider/awsProvider.test.js
index 03d44a1..34198a5 100644
--- a/node_modules/serverless/lib/plugins/aws/provider/awsProvider.test.js
+++ b/node_modules/serverless/lib/plugins/aws/provider/awsProvider.test.js
@@ -59,7 +59,7 @@ describe('AwsProvider', () => {
});
it('should have no AWS logger', () => {
- expect(awsProvider.sdk.config.logger).to.be.null;
+ expect(awsProvider.sdk.config.logger == null).to.be.true;
});
it('should set AWS logger', () => {
@@ -699,6 +699,43 @@ describe('AwsProvider', () => {
});
});
+ it('should set the signatureVersion to v4 if the serverSideEncryption is aws:kms', () => {
+ // mocking S3 for testing
+ class FakeS3 {
+ constructor(config) {
+ this.config = config;
+ }
+
+ putObject() {
+ return {
+ send: cb => cb(null, { config: this.config }),
+ };
+ }
+ }
+ awsProvider.sdk = {
+ S3: FakeS3,
+ };
+ awsProvider.serverless.service.environment = {
+ vars: {},
+ stages: {
+ dev: {
+ vars: {
+ profile: 'default',
+ },
+ regions: {},
+ },
+ },
+ };
+
+ awsProvider.serverless.service.provider.deploymentBucketObject = {
+ serverSideEncryption: 'aws:kms',
+ };
+
+ return awsProvider.request('S3', 'putObject', {}).then(data => {
+ expect(data.config.signatureVersion).to.equal('v4');
+ });
+ });
+
describe('using the request cache', () => {
it('should call correct aws method', () => {
// mocking CF for testing
@@ -836,25 +873,38 @@ describe('AwsProvider', () => {
});
describe('STS tokens', () => {
- let newAwsProvider;
- let originalProviderProfile;
- let originalEnvironmentVariables;
const relevantEnvironment = {
AWS_SHARED_CREDENTIALS_FILE: getTmpFilePath('credentials'),
};
+ let newAwsProvider;
+
+ let originalProviderProfile;
+ let originalEnvironmentVariables;
+
beforeEach(() => {
originalProviderProfile = serverless.service.provider.profile;
originalEnvironmentVariables = replaceEnv(relevantEnvironment);
+ // fake an asynchronous credential process:
+ const credProcessCode = `
+ const credentials = {
+ Version: 1,
+ AccessKeyId: 'async_access_key_id',
+ SessionToken: 'async_session_token',
+ }
+ console.log(JSON.stringify(credentials));
+ `;
+ const nodePath = process.execPath;
+ const credProcessPath = getTmpFilePath('script.js');
+ serverless.utils.writeFileSync(credProcessPath, credProcessCode);
serverless.utils.writeFileSync(
relevantEnvironment.AWS_SHARED_CREDENTIALS_FILE,
- '[default]\n' +
- 'aws_access_key_id = 1111\n' +
- 'aws_secret_access_key = 22222\n' +
+ '[no_default]\n' +
+ 'aws_access_key_id = default_access_key_id\n' +
+ 'aws_secret_access_key = default_aws_secret_access_key\n' +
'\n' +
- '[async]\n' +
- 'role_arn = arn:123\n' +
- 'source_profile = default'
+ '[async_profile]\n' +
+ `credential_process = ${nodePath} ${credProcessPath}\n`
);
newAwsProvider = new AwsProvider(serverless, options);
});
@@ -864,17 +914,21 @@ describe('AwsProvider', () => {
serverless.service.provider.profile = originalProviderProfile;
});
- it('should retain reference to STS tokens when updated via SDK', () => {
- const expectedToken = '123';
+ it('should retain reference to STS tokens when updated via SDK', async () => {
+ const expectedToken = '_sts_refreshed_access_token_';
+
+ serverless.service.provider.profile = 'async_profile';
+ const { credentials } = newAwsProvider.getCredentials();
+ await credentials.getPromise();
- serverless.service.provider.profile = 'async';
- const startToken = newAwsProvider.getCredentials().credentials.sessionToken;
+ const startToken = credentials.sessionToken;
+ expect(startToken).to.equal('async_session_token');
expect(startToken).to.not.equal(expectedToken);
class FakeCloudFormation {
- constructor(credentials) {
+ constructor(config) {
// Not sure where the the SDK resolves the STS, so for the test it's here
- this.credentials = credentials;
+ this.credentials = config;
this.credentials.credentials.sessionToken = expectedToken;
}
@@ -933,11 +987,35 @@ describe('AwsProvider', () => {
region: 'testregion',
};
const fakeCredentials = {
- accessKeyId: 'AABBCCDDEEFF',
- secretAccessKey: '0123456789876543',
- sessionToken: '981237917391273918273918723987129837129873',
- roleArn: 'a:role:arn',
- sourceProfile: 'notDefaultTemporary',
+ yaml: {
+ accessKeyId: 'yaml_aws_access_key_id',
+ secretAccessKey: 'yaml_aws_secret_access_key',
+ sessionToken: 'yaml_aws_session_token',
+ },
+ default: {
+ accessKeyId: 'default_access_key_id',
+ secretAccessKey: 'default_aws_secret_access_key',
+ },
+ otherDefault: {
+ name: 'other_default',
+ accessKeyId: 'other_default_access_key_id',
+ secretAccessKey: 'other_default_aws_secret_access_key',
+ },
+ aProfile: {
+ name: 'a_profile',
+ accessKeyId: 'a_profile_access_key_id',
+ secretAccessKey: 'a_profile_aws_secret_access_key',
+ },
+ anotherProfile: {
+ name: 'another_profile',
+ accessKeyId: 'another_profile_access_key_id',
+ secretAccessKey: 'another_profile_aws_secret_access_key',
+ },
+ asyncProfile: {
+ name: 'async_profile',
+ accessKeyId: 'async_access_key_id',
+ sessionToken: 'async_session_token',
+ },
};
let originalProviderCredentials;
@@ -948,21 +1026,31 @@ describe('AwsProvider', () => {
originalProviderCredentials = serverless.service.provider.credentials;
originalProviderProfile = serverless.service.provider.profile;
originalEnvironmentVariables = replaceEnv(relevantEnvironment);
+ // fake a credential process provider
+ const credProcessCode = `
+ const credentials = {
+ Version: 1,
+ AccessKeyId: '${fakeCredentials.asyncProfile.accessKeyId}',
+ SessionToken: '${fakeCredentials.asyncProfile.sessionToken}',
+ }
+ console.log(JSON.stringify(credentials));
+ `;
+ const nodePath = process.execPath;
+ const credProcessPath = getTmpFilePath('script.js');
+ serverless.utils.writeFileSync(credProcessPath, credProcessCode);
// make temporary credentials file
serverless.utils.writeFileSync(
relevantEnvironment.AWS_SHARED_CREDENTIALS_FILE,
- '[notDefault]\n' +
- `aws_access_key_id = ${fakeCredentials.accessKeyId}\n` +
- `aws_secret_access_key = ${fakeCredentials.secretAccessKey}\n` +
+ `[${fakeCredentials.aProfile.name}]\n` +
+ `aws_access_key_id = ${fakeCredentials.aProfile.accessKeyId}\n` +
+ `aws_secret_access_key = ${fakeCredentials.aProfile.secretAccessKey}\n` +
'\n' +
- '[notDefaultTemporary]\n' +
- `aws_access_key_id = ${fakeCredentials.accessKeyId}\n` +
- `aws_secret_access_key = ${fakeCredentials.secretAccessKey}\n` +
- `aws_session_token = ${fakeCredentials.sessionToken}\n` +
+ `[${fakeCredentials.anotherProfile.name}]\n` +
+ `aws_access_key_id = ${fakeCredentials.anotherProfile.accessKeyId}\n` +
+ `aws_secret_access_key = ${fakeCredentials.anotherProfile.secretAccessKey}\n` +
'\n' +
- '[notDefaultAsync]\n' +
- `role_arn = ${fakeCredentials.roleArn}\n` +
- `source_profile = ${fakeCredentials.sourceProfile}\n`
+ `[${fakeCredentials.asyncProfile.name}]\n` +
+ `credential_process = ${nodePath} ${credProcessPath}\n`
);
newAwsProvider = new AwsProviderProxyquired(serverless, newOptions);
});
@@ -973,92 +1061,100 @@ describe('AwsProvider', () => {
serverless.service.provider.credentials = originalProviderCredentials;
});
- it('should not set credentials if credentials is an empty object', () => {
+ it('should not set credentials from provider if credentials is an empty object', async () => {
serverless.service.provider.credentials = {};
- const credentials = newAwsProvider.getCredentials();
- expect(credentials).to.eql({});
+ const { credentials } = newAwsProvider.getCredentials();
+ await credentials.getPromise().catch(() => null); // no valid credentials
+ expect(credentials.accessKeyId).to.be.undefined;
+ expect(credentials.secretAccessKey).to.be.undefined;
+ expect(credentials.sessionToken).to.be.undefined;
});
- it('should not set credentials if credentials has undefined values', () => {
+ it('should not set credentials from provider if credentials has undefined values', async () => {
serverless.service.provider.credentials = {
accessKeyId: undefined,
secretAccessKey: undefined,
sessionToken: undefined,
};
- const credentials = newAwsProvider.getCredentials();
- expect(credentials).to.eql({});
+ const { credentials } = newAwsProvider.getCredentials();
+ await credentials.getPromise().catch(() => null); // no valid credentials
+ expect(credentials.accessKeyId).to.be.undefined;
+ expect(credentials.secretAccessKey).to.be.undefined;
+ expect(credentials.sessionToken).to.be.undefined;
});
- it('should not set credentials if credentials has empty string values', () => {
+ it('should not set credentials from provider if credentials has empty string values', async () => {
serverless.service.provider.credentials = {
accessKeyId: '',
secretAccessKey: '',
sessionToken: '',
};
- const credentials = newAwsProvider.getCredentials();
- expect(credentials).to.eql({});
+ const { credentials } = newAwsProvider.getCredentials();
+ await credentials.getPromise().catch(() => null); // no valid credentials
+ expect(credentials.accessKeyId).to.be.undefined;
+ expect(credentials.secretAccessKey).to.be.undefined;
+ expect(credentials.sessionToken).to.be.undefined;
});
- it('should get credentials from provider declared credentials', () => {
+ it('should get credentials from provider declared secret access key credentials', async () => {
serverless.service.provider.credentials = {
- accessKeyId: 'accessKeyId',
- secretAccessKey: 'secretAccessKey',
- sessionToken: 'sessionToken',
+ accessKeyId: fakeCredentials.yaml.accessKeyId,
+ secretAccessKey: fakeCredentials.yaml.secretAccessKey,
};
- const credentials = newAwsProvider.getCredentials();
- expect(credentials.credentials).to.deep.eql(serverless.service.provider.credentials);
- });
-
- it('should load profile credentials from AWS_SHARED_CREDENTIALS_FILE', () => {
- serverless.service.provider.profile = 'notDefault';
- const credentials = newAwsProvider.getCredentials();
- expect(credentials.credentials.profile).to.equal(serverless.service.provider.profile);
- expect(credentials.credentials.accessKeyId).to.equal(fakeCredentials.accessKeyId);
- expect(credentials.credentials.secretAccessKey).to.equal(fakeCredentials.secretAccessKey);
- expect(credentials.credentials.sessionToken).to.equal(undefined);
+ const { credentials } = newAwsProvider.getCredentials();
+ await credentials.getPromise();
+ expect(credentials.accessKeyId).to.equal(fakeCredentials.yaml.accessKeyId);
+ expect(credentials.secretAccessKey).to.equal(fakeCredentials.yaml.secretAccessKey);
+ expect(credentials.sessionToken).to.be.undefined;
});
- it('should load async profiles properly', () => {
- serverless.service.provider.profile = 'notDefaultAsync';
- const credentials = newAwsProvider.getCredentials();
- expect(credentials.credentials.roleArn).to.equal(fakeCredentials.roleArn);
- });
-
- it('should throw an error if a non-existent profile is set', () => {
- serverless.service.provider.profile = 'not-a-defined-profile';
- expect(() => {
- newAwsProvider.getCredentials();
- }).to.throw(Error);
+ it('should get credentials from provider declared session token credentials', async () => {
+ serverless.service.provider.credentials = {
+ accessKeyId: fakeCredentials.yaml.accessKeyId,
+ sessionToken: fakeCredentials.yaml.sessionToken,
+ };
+ const { credentials } = newAwsProvider.getCredentials();
+ await credentials.getPromise();
+ expect(credentials.accessKeyId).to.equal(fakeCredentials.yaml.accessKeyId);
+ expect(credentials.secretAccessKey).to.be.undefined;
+ expect(credentials.sessionToken).to.equal(fakeCredentials.yaml.sessionToken);
});
- it('should not set credentials if empty profile is set', () => {
- serverless.service.provider.profile = '';
- const credentials = newAwsProvider.getCredentials();
- expect(credentials).to.eql({});
+ it('should return the region', async () => {
+ serverless.service.provider.credentials = {
+ accessKeyId: fakeCredentials.yaml.accessKeyId,
+ secretAccessKey: fakeCredentials.yaml.secretAccessKey,
+ };
+ const { region } = newAwsProvider.getCredentials();
+ expect(region).to.equal('testregion');
});
- it('should not set credentials if profile is not set', () => {
- serverless.service.provider.profile = undefined;
- const credentials = newAwsProvider.getCredentials();
- expect(credentials).to.eql({});
+ it('should load profile credentials from AWS_SHARED_CREDENTIALS_FILE', async () => {
+ serverless.service.provider.profile = fakeCredentials.aProfile.name;
+ const { credentials } = newAwsProvider.getCredentials();
+ await credentials.getPromise();
+ expect(credentials.accessKeyId).to.equal(fakeCredentials.aProfile.accessKeyId);
+ expect(credentials.secretAccessKey).to.equal(fakeCredentials.aProfile.secretAccessKey);
});
- it('should not set credentials if empty profile is set', () => {
- serverless.service.provider.profile = '';
- const credentials = newAwsProvider.getCredentials();
- expect(credentials).to.eql({});
+ it('should load async profiles properly', async () => {
+ serverless.service.provider.profile = fakeCredentials.asyncProfile.name;
+ const { credentials } = newAwsProvider.getCredentials();
+ await credentials.getPromise();
+ expect(credentials.accessKeyId).to.equal(fakeCredentials.asyncProfile.accessKeyId);
+ expect(credentials.sessionToken).to.equal(fakeCredentials.asyncProfile.sessionToken);
});
- it('should get credentials from provider declared temporary profile', () => {
- serverless.service.provider.profile = 'notDefaultTemporary';
- const credentials = newAwsProvider.getCredentials();
- expect(credentials.credentials.profile).to.equal(serverless.service.provider.profile);
- expect(credentials.credentials.accessKeyId).to.equal(fakeCredentials.accessKeyId);
- expect(credentials.credentials.secretAccessKey).to.equal(fakeCredentials.secretAccessKey);
- expect(credentials.credentials.sessionToken).to.equal(fakeCredentials.sessionToken);
+ it('should throw an error if a non-existent profile is set', done => {
+ serverless.service.provider.profile = 'not-a-defined-profile';
+ const { credentials } = newAwsProvider.getCredentials();
+ credentials.get(err => {
+ expect(err).not.to.be.undefined;
+ done();
+ });
});
- it('should get credentials from environment declared for-all-stages credentials', () => {
+ it('should get credentials from environment declared for-all-stages credentials', async () => {
const testVal = {
accessKeyId: 'accessKeyId',
secretAccessKey: 'secretAccessKey',
@@ -1068,76 +1164,117 @@ describe('AwsProvider', () => {
process.env.AWS_SECRET_ACCESS_KEY = testVal.secretAccessKey;
process.env.AWS_SESSION_TOKEN = testVal.sessionToken;
- const credentials = newAwsProvider.getCredentials();
- expect(credentials.credentials.accessKeyId).to.equal(testVal.accessKeyId);
- expect(credentials.credentials.secretAccessKey).to.equal(testVal.secretAccessKey);
- expect(credentials.credentials.sessionToken).to.equal(testVal.sessionToken);
+ const { credentials } = newAwsProvider.getCredentials();
+ await credentials.getPromise();
+ expect(credentials.accessKeyId).to.equal(testVal.accessKeyId);
+ expect(credentials.secretAccessKey).to.equal(testVal.secretAccessKey);
+ expect(credentials.sessionToken).to.equal(testVal.sessionToken);
});
- it('should get credentials from environment declared stage specific credentials', () => {
+ it('should get credentials from environment declared stage specific credentials', async () => {
const testVal = {
- accessKeyId: 'accessKeyId',
- secretAccessKey: 'secretAccessKey',
- sessionToken: 'sessionToken',
+ accessKeyId: 'stageAccessKeyId',
+ secretAccessKey: 'stageSecretAccessKey',
+ sessionToken: 'stageSessionToken',
};
process.env.AWS_TESTSTAGE_ACCESS_KEY_ID = testVal.accessKeyId;
process.env.AWS_TESTSTAGE_SECRET_ACCESS_KEY = testVal.secretAccessKey;
process.env.AWS_TESTSTAGE_SESSION_TOKEN = testVal.sessionToken;
- const credentials = newAwsProvider.getCredentials();
- expect(credentials.credentials.accessKeyId).to.equal(testVal.accessKeyId);
- expect(credentials.credentials.secretAccessKey).to.equal(testVal.secretAccessKey);
- expect(credentials.credentials.sessionToken).to.equal(testVal.sessionToken);
+ const { credentials } = newAwsProvider.getCredentials();
+ await credentials.getPromise();
+ expect(credentials.accessKeyId).to.equal(testVal.accessKeyId);
+ expect(credentials.secretAccessKey).to.equal(testVal.secretAccessKey);
+ expect(credentials.sessionToken).to.equal(testVal.sessionToken);
});
- it('should get credentials from environment declared for-all-stages profile', () => {
- process.env.AWS_PROFILE = 'notDefault';
- const credentials = newAwsProvider.getCredentials();
- expect(credentials.credentials.profile).to.equal('notDefault');
+ it('should get credentials from environment declared for-all-stages profile', async () => {
+ process.env.AWS_PROFILE = fakeCredentials.aProfile.name;
+ const { credentials } = newAwsProvider.getCredentials();
+ await credentials.getPromise();
+ expect(credentials.accessKeyId).to.equal(fakeCredentials.aProfile.accessKeyId);
+ expect(credentials.secretAccessKey).to.equal(fakeCredentials.aProfile.secretAccessKey);
});
- it('should get credentials from environment declared stage-specific profile', () => {
- process.env.AWS_TESTSTAGE_PROFILE = 'notDefault';
- const credentials = newAwsProvider.getCredentials();
- expect(credentials.credentials.profile).to.equal('notDefault');
+ it('should get credentials from environment declared from preferred stage-specific profile', async () => {
+ process.env.AWS_PROFILE = fakeCredentials.anotherProfile.name;
+ process.env.AWS_TESTSTAGE_PROFILE = fakeCredentials.aProfile.name;
+ const { credentials } = newAwsProvider.getCredentials();
+ await credentials.getPromise();
+ expect(credentials.accessKeyId).to.equal(fakeCredentials.aProfile.accessKeyId);
+ expect(credentials.secretAccessKey).to.equal(fakeCredentials.aProfile.secretAccessKey);
});
- it('should get credentials when profile is provied via --aws-profile option', () => {
- process.env.AWS_PROFILE = 'notDefaultTemporary';
- newAwsProvider.options['aws-profile'] = 'notDefault';
+ it('should get credentials when profile is provided via --aws-profile option', async () => {
+ process.env.AWS_PROFILE = fakeCredentials.anotherProfile.name;
+ newAwsProvider.options['aws-profile'] = fakeCredentials.aProfile.name;
- const credentials = newAwsProvider.getCredentials();
- expect(credentials.credentials.profile).to.equal('notDefault');
+ const { credentials } = newAwsProvider.getCredentials();
+ await credentials.getPromise();
+ expect(credentials.accessKeyId).to.equal(fakeCredentials.aProfile.accessKeyId);
+ expect(credentials.secretAccessKey).to.equal(fakeCredentials.aProfile.secretAccessKey);
});
- it('should get credentials when profile is provied via --aws-profile option even if profile is defined in serverless.yml', () => {
+ it('should get credentials when profile is provided via --aws-profile option even if profile is defined in serverless.yml', async () => {
// eslint-disable-line max-len
- process.env.AWS_PROFILE = 'notDefaultTemporary';
- newAwsProvider.options['aws-profile'] = 'notDefault';
+ process.env.AWS_PROFILE = fakeCredentials.anotherProfile.name;
+ serverless.service.provider.profile = fakeCredentials.anotherProfile.name;
- serverless.service.provider.profile = 'notDefaultTemporary2';
+ newAwsProvider.options['aws-profile'] = fakeCredentials.aProfile.name;
- const credentials = newAwsProvider.getCredentials();
- expect(credentials.credentials.profile).to.equal('notDefault');
+ const { credentials } = newAwsProvider.getCredentials();
+ await credentials.getPromise();
+ expect(credentials.accessKeyId).to.equal(fakeCredentials.aProfile.accessKeyId);
+ expect(credentials.secretAccessKey).to.equal(fakeCredentials.aProfile.secretAccessKey);
});
- it('should get credentials when profile is provied via process.env.AWS_PROFILE even if profile is defined in serverless.yml', () => {
+ it('should get credentials when profile is provided via process.env.AWS_PROFILE even if profile is defined in serverless.yml', async () => {
// eslint-disable-line max-len
- process.env.AWS_PROFILE = 'notDefault';
+ process.env.AWS_PROFILE = fakeCredentials.aProfile.name;
- serverless.service.provider.profile = 'notDefaultTemporary';
+ serverless.service.provider.profile = fakeCredentials.anotherProfile.name;
- const credentials = newAwsProvider.getCredentials();
- expect(credentials.credentials.profile).to.equal('notDefault');
+ const { credentials } = newAwsProvider.getCredentials();
+ await credentials.getPromise();
+ expect(credentials.accessKeyId).to.equal(fakeCredentials.aProfile.accessKeyId);
+ expect(credentials.secretAccessKey).to.equal(fakeCredentials.aProfile.secretAccessKey);
});
- it('should set the signatureVersion to v4 if the serverSideEncryption is aws:kms', () => {
- newAwsProvider.serverless.service.provider.deploymentBucketObject = {
- serverSideEncryption: 'aws:kms',
- };
+ it('should get credentials from process.env.AWS_DEFAULT_PROFILE, not "default"', async () => {
+ process.env.AWS_DEFAULT_PROFILE = fakeCredentials.otherDefault.name;
+ newAwsProvider.options['aws-profile'] = undefined;
+
+ serverless.utils.appendFileSync(
+ relevantEnvironment.AWS_SHARED_CREDENTIALS_FILE,
+ '[default]\n' +
+ `aws_access_key_id = ${fakeCredentials.default.accessKeyId}\n` +
+ `aws_secret_access_key = ${fakeCredentials.default.secretAccessKey}\n` +
+ '\n' +
+ `[${fakeCredentials.otherDefault.name}]\n` +
+ `aws_access_key_id = ${fakeCredentials.otherDefault.accessKeyId}\n` +
+ `aws_secret_access_key = ${fakeCredentials.otherDefault.secretAccessKey}\n`
+ );
+
+ const { credentials } = newAwsProvider.getCredentials();
+ await credentials.getPromise();
+ expect(credentials.accessKeyId).to.equal(fakeCredentials.otherDefault.accessKeyId);
+ expect(credentials.secretAccessKey).to.equal(fakeCredentials.otherDefault.secretAccessKey);
+ });
+
+ it('should get "default" credentials in lieu of anything else', async () => {
+ newAwsProvider.options['aws-profile'] = undefined;
+
+ serverless.utils.appendFileSync(
+ relevantEnvironment.AWS_SHARED_CREDENTIALS_FILE,
+ '[default]\n' +
+ `aws_access_key_id = ${fakeCredentials.default.accessKeyId}\n` +
+ `aws_secret_access_key = ${fakeCredentials.default.secretAccessKey}\n`
+ );
- const credentials = newAwsProvider.getCredentials();
- expect(credentials.signatureVersion).to.equal('v4');
+ const { credentials } = newAwsProvider.getCredentials();
+ await credentials.getPromise();
+ expect(credentials.accessKeyId).to.equal(fakeCredentials.default.accessKeyId);
+ expect(credentials.secretAccessKey).to.equal(fakeCredentials.default.secretAccessKey);
});
});
diff --git a/node_modules/serverless/lib/plugins/create/templates/aws-csharp/obj/Debug/netcoreapp3.1/.NETCoreApp,Version=v3.1.AssemblyAttributes.cs b/node_modules/serverless/lib/plugins/create/templates/aws-csharp/obj/Debug/netcoreapp3.1/.NETCoreApp,Version=v3.1.AssemblyAttributes.cs
new file mode 100644
index 0000000..03fd1de
--- /dev/null
+++ b/node_modules/serverless/lib/plugins/create/templates/aws-csharp/obj/Debug/netcoreapp3.1/.NETCoreApp,Version=v3.1.AssemblyAttributes.cs
@@ -0,0 +1,4 @@
+// <autogenerated />
+using System;
+using System.Reflection;
+[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v3.1", FrameworkDisplayName = "")]
diff --git a/node_modules/serverless/lib/plugins/create/templates/aws-csharp/obj/Debug/netcoreapp3.1/aws-csharp.AssemblyInfo.cs b/node_modules/serverless/lib/plugins/create/templates/aws-csharp/obj/Debug/netcoreapp3.1/aws-csharp.AssemblyInfo.cs
new file mode 100644
index 0000000..158b34a
--- /dev/null
+++ b/node_modules/serverless/lib/plugins/create/templates/aws-csharp/obj/Debug/netcoreapp3.1/aws-csharp.AssemblyInfo.cs
@@ -0,0 +1,23 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+using System;
+using System.Reflection;
+
+[assembly: System.Reflection.AssemblyCompanyAttribute("CsharpHandlers")]
+[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
+[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
+[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
+[assembly: System.Reflection.AssemblyProductAttribute("CsharpHandlers")]
+[assembly: System.Reflection.AssemblyTitleAttribute("CsharpHandlers")]
+[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
+
+// Generated by the MSBuild WriteCodeFragment class.
+
diff --git a/node_modules/serverless/lib/plugins/create/templates/aws-csharp/obj/Debug/netcoreapp3.1/aws-csharp.AssemblyInfoInputs.cache b/node_modules/serverless/lib/plugins/create/templates/aws-csharp/obj/Debug/netcoreapp3.1/aws-csharp.AssemblyInfoInputs.cache
new file mode 100644
index 0000000..77f182d
--- /dev/null
+++ b/node_modules/serverless/lib/plugins/create/templates/aws-csharp/obj/Debug/netcoreapp3.1/aws-csharp.AssemblyInfoInputs.cache
@@ -0,0 +1 @@
+a49277692051e4f935ec0560b75544b62478532d
diff --git a/node_modules/serverless/lib/plugins/create/templates/aws-csharp/obj/Debug/netcoreapp3.1/aws-csharp.csprojAssemblyReference.cache b/node_modules/serverless/lib/plugins/create/templates/aws-csharp/obj/Debug/netcoreapp3.1/aws-csharp.csprojAssemblyReference.cache
new file mode 100644
index 0000000..4a1a56a
Binary files /dev/null and b/node_modules/serverless/lib/plugins/create/templates/aws-csharp/obj/Debug/netcoreapp3.1/aws-csharp.csprojAssemblyReference.cache differ
diff --git a/node_modules/serverless/lib/plugins/create/templates/azure-csharp/obj/Debug/netcoreapp3.1/.NETCoreApp,Version=v3.1.AssemblyAttributes.cs b/node_modules/serverless/lib/plugins/create/templates/azure-csharp/obj/Debug/netcoreapp3.1/.NETCoreApp,Version=v3.1.AssemblyAttributes.cs
new file mode 100644
index 0000000..03fd1de
--- /dev/null
+++ b/node_modules/serverless/lib/plugins/create/templates/azure-csharp/obj/Debug/netcoreapp3.1/.NETCoreApp,Version=v3.1.AssemblyAttributes.cs
@@ -0,0 +1,4 @@
+// <autogenerated />
+using System;
+using System.Reflection;
+[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v3.1", FrameworkDisplayName = "")]
diff --git a/node_modules/serverless/lib/plugins/create/templates/azure-csharp/obj/Debug/netcoreapp3.1/azure-csharp.AssemblyInfo.cs b/node_modules/serverless/lib/plugins/create/templates/azure-csharp/obj/Debug/netcoreapp3.1/azure-csharp.AssemblyInfo.cs
new file mode 100644
index 0000000..12c1d63
--- /dev/null
+++ b/node_modules/serverless/lib/plugins/create/templates/azure-csharp/obj/Debug/netcoreapp3.1/azure-csharp.AssemblyInfo.cs
@@ -0,0 +1,23 @@
+//------------------------------------------------------------------------------
+// <auto-generated>
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+// </auto-generated>
+//------------------------------------------------------------------------------
+
+using System;
+using System.Reflection;
+
+[assembly: System.Reflection.AssemblyCompanyAttribute("azure-csharp")]
+[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
+[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
+[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
+[assembly: System.Reflection.AssemblyProductAttribute("azure-csharp")]
+[assembly: System.Reflection.AssemblyTitleAttribute("azure-csharp")]
+[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
+
+// Generated by the MSBuild WriteCodeFragment class.
+
diff --git a/node_modules/serverless/lib/plugins/create/templates/azure-csharp/obj/Debug/netcoreapp3.1/azure-csharp.AssemblyInfoInputs.cache b/node_modules/serverless/lib/plugins/create/templates/azure-csharp/obj/Debug/netcoreapp3.1/azure-csharp.AssemblyInfoInputs.cache
new file mode 100644
index 0000000..c0f76f8
--- /dev/null
+++ b/node_modules/serverless/lib/plugins/create/templates/azure-csharp/obj/Debug/netcoreapp3.1/azure-csharp.AssemblyInfoInputs.cache
@@ -0,0 +1 @@
+779601599c1884a13eb5f8cc85c28c97c14d133e
diff --git a/node_modules/serverless/lib/plugins/aws/invokeLocal/index.js b/node_modules/serverless/lib/plugins/aws/invokeLocal/index.js
index 2970e7e..45535e5 100644
--- a/node_modules/serverless/lib/plugins/aws/invokeLocal/index.js
+++ b/node_modules/serverless/lib/plugins/aws/invokeLocal/index.js
@@ -133,9 +133,14 @@ class AwsInvokeLocal {
}
}
- getCredentialEnvVars() {
+ async getCredentialEnvVars() {
const credentialEnvVars = {};
const { credentials } = this.provider.getCredentials();
+ if (credentials && credentials.getPromise) {
+ await credentials.getPromise().catch(() => {
+ /* ignore */
+ });
+ }
if (credentials) {
if (credentials.accessKeyId) {
credentialEnvVars.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
@@ -185,7 +190,7 @@ class AwsInvokeLocal {
NODE_PATH: '/var/runtime:/var/task:/var/runtime/node_modules',
};
- const credentialEnvVars = this.getCredentialEnvVars();
+ const credentialEnvVars = await this.getCredentialEnvVars();
// profile override from config
const profileOverride = this.provider.getProfile();
@@ -508,7 +513,7 @@ class AwsInvokeLocal {
AWS_LAMBDA_FUNCTION_NAME: lambdaName,
AWS_LAMBDA_FUNCTION_MEMORY_SIZE: memorySize,
};
- const credentialEnvVars = this.getCredentialEnvVars();
+ const credentialEnvVars = await this.getCredentialEnvVars();
const configuredEnvVars = this.getConfiguredEnvVars();
const envVarsFromOptions = this.getEnvVarsFromOptions();
const envVars = _.merge(
diff --git a/node_modules/serverless/lib/plugins/aws/lib/awsCredentials.js b/node_modules/serverless/lib/plugins/aws/lib/awsCredentials.js
new file mode 100644
index 0000000..eb6f09b
--- /dev/null
+++ b/node_modules/serverless/lib/plugins/aws/lib/awsCredentials.js
@@ -0,0 +1,90 @@
+'use strict';
+
+const AWS = require('aws-sdk');
+const readline = require('readline');
+
+/*
+ * The aws-sdk-js provides a built in mechanism for resolving credentials from multiple sources:
+ * https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CredentialProviderChain.html
+ * However, the credential resolution for the serverless framework differs significantly from the
+ * AWS default provider chain (e.g. credentials and provides set by the yaml).
+ *
+ * This class allows us to define a more flexible order (see AwsProvider.getCredentials()),
+ * while still using the aws-sdk-js supported framework; so we can more readily support future
+ * ways of resolving credentials.
+ *
+ * Until https://github.com/aws/aws-sdk-js/issues/3122 is resolved, extending the
+ * AWS.CredentialProviderChain does not result in AWS.Credentials that refresh using the chain.
+ * Therefore we must extend AWS.Credentials directly and provide a refresh method that
+ * resolves the chain ourselves.
+ */
+module.exports = class AwsCredentials extends AWS.Credentials {
+ constructor() {
+ super();
+ this.chain = new AWS.CredentialProviderChain([]); // providers are added explicitly
+ }
+
+ refresh(callback) {
+ this.chain.resolve((err, res) => {
+ if (err) {
+ callback(err);
+ } else {
+ AWS.Credentials.call(this, res);
+ callback();
+ }
+ });
+ }
+
+ /**
+ * Add credentials, if present and valid, from provider config
+ * @param credentials The credentials to test for validity
+ */
+ addConfig(credentials) {
+ if (credentials) {
+ if (
+ credentials.accessKeyId &&
+ credentials.accessKeyId !== 'undefined' &&
+ ((credentials.secretAccessKey && credentials.secretAccessKey !== 'undefined') ||
+ (credentials.sessionToken && credentials.sessionToken !== 'undefined'))
+ ) {
+ this.chain.providers.push(new AWS.Credentials(credentials));
+ }
+ }
+ }
+
+ /**
+ * Add credentials, if present, from the environment
+ * @param prefix The environment variable prefix to use in extracting credentials
+ */
+ addEnvironment(prefix) {
+ if (prefix) {
+ const environmentCredentials = new AWS.EnvironmentCredentials(prefix);
+ this.chain.providers.push(environmentCredentials);
+ }
+ }
+
+ /**
+ * Add credentials from a profile
+ * @param profile The profile to load credentials from
+ */
+ addProfile(profile) {
+ if (profile) {
+ const params = { profile };
+ if (process.env.AWS_SHARED_CREDENTIALS_FILE) {
+ params.filename = process.env.AWS_SHARED_CREDENTIALS_FILE;
+ }
+
+ // Setup a MFA callback for asking the code from the user.
+ params.tokenCodeFn = (mfaSerial, callback) => {
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
+ rl.question(`Enter MFA code for ${mfaSerial}: `, (answer) => {
+ rl.close();
+ callback(null, answer);
+ });
+ };
+
+ this.chain.providers.push(() => new AWS.SharedIniFileCredentials(params));
+ this.chain.providers.push(() => new AWS.ProcessCredentials(params));
+ }
+ }
+};
diff --git a/node_modules/serverless/lib/plugins/aws/provider.js b/node_modules/serverless/lib/plugins/aws/provider.js
index ae01298..ddc6d4b 100644
--- a/node_modules/serverless/lib/plugins/aws/provider.js
+++ b/node_modules/serverless/lib/plugins/aws/provider.js
@@ -5,9 +5,9 @@ const BbPromise = require('bluebird');
const _ = require('lodash');
const naming = require('./lib/naming.js');
const fs = require('fs');
+const AwsCredentials = require('./lib/awsCredentials');
const getS3EndpointForRegion = require('./utils/getS3EndpointForRegion');
const memoizeeMethods = require('memoizee/methods');
-const readline = require('readline');
const reportDeprecatedProperties = require('../../utils/report-deprecated-properties');
const { ALB_LISTENER_REGEXP } = require('./package/compile/events/alb/lib/validate');
const d = require('d');
@@ -57,104 +57,6 @@ const apiGatewayUsagePlan = {
additionalProperties: false,
};
-const impl = {
- /**
- * Determine whether the given credentials are valid. It turned out that detecting invalid
- * credentials was more difficult than detecting the positive cases we know about. Hooray for
- * whack-a-mole!
- * @param credentials The credentials to test for validity
- * @return {boolean} Whether the given credentials were valid
- */
- validCredentials: (credentials) => {
- let result = false;
- if (credentials) {
- if (
- // valid credentials loaded
- (credentials.accessKeyId &&
- credentials.accessKeyId !== 'undefined' &&
- credentials.secretAccessKey &&
- credentials.secretAccessKey !== 'undefined') ||
- // a role to assume has been successfully loaded, the associated STS request has been
- // sent, and the temporary credentials will be asynchronously delivered.
- credentials.roleArn
- ) {
- result = true;
- }
- }
- return result;
- },
- /**
- * Add credentials, if present, to the given results
- * @param results The results to add the given credentials to if they are valid
- * @param credentials The credentials to validate and add to the results if valid
- */
- addCredentials: (results, credentials) => {
- if (impl.validCredentials(credentials)) {
- results.credentials = credentials;
- }
- },
- /**
- * Add credentials, if present, from the environment
- * @param results The results to add environment credentials to
- * @param prefix The environment variable prefix to use in extracting credentials
- */
- addEnvironmentCredentials: (results, prefix) => {
- if (prefix) {
- const environmentCredentials = new AWS.EnvironmentCredentials(prefix);
- impl.addCredentials(results, environmentCredentials);
- }
- },
- /**
- * Add credentials from a profile, if the profile and credentials for it exists
- * @param results The results to add profile credentials to
- * @param profile The profile to load credentials from
- */
- addProfileCredentials: (results, profile) => {
- if (profile) {
- const params = { profile };
- if (process.env.AWS_SHARED_CREDENTIALS_FILE) {
- params.filename = process.env.AWS_SHARED_CREDENTIALS_FILE;
- }
-
- // Setup a MFA callback for asking the code from the user.
- params.tokenCodeFn = (mfaSerial, callback) => {
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
- rl.question(`Enter MFA code for ${mfaSerial}: `, (answer) => {
- rl.close();
- callback(null, answer);
- });
- };
-
- const profileCredentials = new AWS.SharedIniFileCredentials(params);
- if (
- !(
- profileCredentials.accessKeyId ||
- profileCredentials.sessionToken ||
- profileCredentials.roleArn
- )
- ) {
- throw new ServerlessError(
- `AWS profile "${profile}" doesn't seem to be configured`,
- 'UNRECOGNIZED_AWS_PROFILE'
- );
- }
-
- impl.addCredentials(results, profileCredentials);
- }
- },
- /**
- * Add credentials, if present, from a profile that is specified within the environment
- * @param results The prefix of the profile's declaration in the environment
- * @param prefix The prefix for the environment variable
- */
- addEnvironmentProfile: (results, prefix) => {
- if (prefix) {
- const profile = process.env[`${prefix}_PROFILE`];
- impl.addProfileCredentials(results, profile);
- }
- },
-};
-
const baseAlbAuthorizerProperties = {
onUnauthenticatedRequest: { enum: ['allow', 'authenticate', 'deny'] },
requestExtraParams: {
@@ -1443,8 +1345,7 @@ class AwsProvider {
}
const requestOptions = _.isObject(options) ? options : {};
const shouldCache = _.get(requestOptions, 'useCache', false);
- // Copy is required as the credentials may be modified during the request
- const credentials = Object.assign({}, this.getCredentials());
+ const credentials = await this.getCredentials();
const serviceOptions = {
name: service,
params: {
@@ -1457,36 +1358,36 @@ class AwsProvider {
}
/**
- * Fetch credentials directly or using a profile from serverless yml configuration or from the
- * well known environment variables
- * @returns {{region: *}}
+ * If using outside of AWS, you are expected to use credentials.get/Promise to ensure they have
+ * not expired (https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Credentials.html#getPromise-property)
+ * @returns {credentials: AWS.Credentials, region: string}
*/
getCredentials() {
if (this.cachedCredentials) {
// We have already created the credentials object once, so return it.
return this.cachedCredentials;
}
- const result = {};
const stageUpper = this.getStage() ? this.getStage().toUpperCase() : null;
- // add specified credentials, overriding with more specific declarations
- const awsDefaultProfile = process.env.AWS_DEFAULT_PROFILE || 'default';
- try {
- impl.addProfileCredentials(result, awsDefaultProfile);
- } catch (err) {
- if (err.code !== 'UNRECOGNIZED_AWS_PROFILE') throw err;
- }
+ const credentials = new AwsCredentials();
+ // AwsCredentials returns the first credentials to resolve, so add from most-to-least specific:
+ credentials.addProfile(this.options['aws-profile']); // CLI option profile
+
+ credentials.addProfile(process.env[`AWS_${stageUpper}_PROFILE`]); // stage specific creds
+ credentials.addEnvironment(`AWS_${stageUpper}`);
+
+ credentials.addProfile(process.env.AWS_PROFILE); // creds for all stages
+ credentials.addEnvironment('AWS');
+
if (this.serverless.service.provider.profile && !this.options['aws-profile']) {
- // config profile
- impl.addProfileCredentials(result, this.serverless.service.provider.profile);
- }
- impl.addEnvironmentCredentials(result, 'AWS'); // creds for all stages
- impl.addEnvironmentProfile(result, 'AWS');
- impl.addEnvironmentCredentials(result, `AWS_${stageUpper}`); // stage specific creds
- impl.addEnvironmentProfile(result, `AWS_${stageUpper}`);
- if (this.options['aws-profile']) {
- impl.addProfileCredentials(result, this.options['aws-profile']); // CLI option profile
+ credentials.addProfile(this.serverless.service.provider.profile);
}
+ credentials.addConfig(this.serverless.service.provider.credentials); // config creds
+ credentials.addProfile(process.env.AWS_DEFAULT_PROFILE || 'default');
+
+ // Store the credentials to avoid creating them again (messes up MFA).
+ const region = this.getRegion();
+ this.cachedCredentials = { credentials, region };
const deploymentBucketObject = this.serverless.service.provider.deploymentBucketObject;
if (
@@ -1494,12 +1395,10 @@ class AwsProvider {
deploymentBucketObject.serverSideEncryption &&
deploymentBucketObject.serverSideEncryption === 'aws:kms'
) {
- result.signatureVersion = 'v4';
+ this.cachedCredentials.signatureVersion = 'v4';
}
- // Store the credentials to avoid creating them again (messes up MFA).
- this.cachedCredentials = result;
- return result;
+ return this.cachedCredentials;
}
// This function will be used to block the addition of transfer acceleration options
diff --git a/node_modules/serverless/lib/plugins/aws/invokeLocal/index.js b/node_modules/serverless/lib/plugins/aws/invokeLocal/index.js
index 7936e76..b93fd85 100644
--- a/node_modules/serverless/lib/plugins/aws/invokeLocal/index.js
+++ b/node_modules/serverless/lib/plugins/aws/invokeLocal/index.js
@@ -134,9 +134,14 @@ class AwsInvokeLocal {
}
}
- getCredentialEnvVars() {
+ async getCredentialEnvVars() {
const credentialEnvVars = {};
const { credentials } = this.provider.getCredentials();
+ if (credentials && credentials.getPromise) {
+ await credentials.getPromise().catch(() => {
+ /* ignore */
+ });
+ }
if (credentials) {
if (credentials.accessKeyId) {
credentialEnvVars.AWS_ACCESS_KEY_ID = credentials.accessKeyId;
@@ -186,7 +191,7 @@ class AwsInvokeLocal {
NODE_PATH: '/var/runtime:/var/task:/var/runtime/node_modules',
};
- const credentialEnvVars = this.getCredentialEnvVars();
+ const credentialEnvVars = await this.getCredentialEnvVars();
// profile override from config
const profileOverride = this.provider.getProfile();
@@ -509,7 +514,7 @@ class AwsInvokeLocal {
AWS_LAMBDA_FUNCTION_NAME: lambdaName,
AWS_LAMBDA_FUNCTION_MEMORY_SIZE: memorySize,
};
- const credentialEnvVars = this.getCredentialEnvVars();
+ const credentialEnvVars = await this.getCredentialEnvVars();
const configuredEnvVars = this.getConfiguredEnvVars();
const envVarsFromOptions = this.getEnvVarsFromOptions();
const envVars = _.merge(
diff --git a/node_modules/serverless/lib/plugins/aws/lib/awsCredentials.js b/node_modules/serverless/lib/plugins/aws/lib/awsCredentials.js
new file mode 100644
index 0000000..eb6f09b
--- /dev/null
+++ b/node_modules/serverless/lib/plugins/aws/lib/awsCredentials.js
@@ -0,0 +1,90 @@
+'use strict';
+
+const AWS = require('aws-sdk');
+const readline = require('readline');
+
+/*
+ * The aws-sdk-js provides a built in mechanism for resolving credentials from multiple sources:
+ * https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/CredentialProviderChain.html
+ * However, the credential resolution for the serverless framework differs significantly from the
+ * AWS default provider chain (e.g. credentials and provides set by the yaml).
+ *
+ * This class allows us to define a more flexible order (see AwsProvider.getCredentials()),
+ * while still using the aws-sdk-js supported framework; so we can more readily support future
+ * ways of resolving credentials.
+ *
+ * Until https://github.com/aws/aws-sdk-js/issues/3122 is resolved, extending the
+ * AWS.CredentialProviderChain does not result in AWS.Credentials that refresh using the chain.
+ * Therefore we must extend AWS.Credentials directly and provide a refresh method that
+ * resolves the chain ourselves.
+ */
+module.exports = class AwsCredentials extends AWS.Credentials {
+ constructor() {
+ super();
+ this.chain = new AWS.CredentialProviderChain([]); // providers are added explicitly
+ }
+
+ refresh(callback) {
+ this.chain.resolve((err, res) => {
+ if (err) {
+ callback(err);
+ } else {
+ AWS.Credentials.call(this, res);
+ callback();
+ }
+ });
+ }
+
+ /**
+ * Add credentials, if present and valid, from provider config
+ * @param credentials The credentials to test for validity
+ */
+ addConfig(credentials) {
+ if (credentials) {
+ if (
+ credentials.accessKeyId &&
+ credentials.accessKeyId !== 'undefined' &&
+ ((credentials.secretAccessKey && credentials.secretAccessKey !== 'undefined') ||
+ (credentials.sessionToken && credentials.sessionToken !== 'undefined'))
+ ) {
+ this.chain.providers.push(new AWS.Credentials(credentials));
+ }
+ }
+ }
+
+ /**
+ * Add credentials, if present, from the environment
+ * @param prefix The environment variable prefix to use in extracting credentials
+ */
+ addEnvironment(prefix) {
+ if (prefix) {
+ const environmentCredentials = new AWS.EnvironmentCredentials(prefix);
+ this.chain.providers.push(environmentCredentials);
+ }
+ }
+
+ /**
+ * Add credentials from a profile
+ * @param profile The profile to load credentials from
+ */
+ addProfile(profile) {
+ if (profile) {
+ const params = { profile };
+ if (process.env.AWS_SHARED_CREDENTIALS_FILE) {
+ params.filename = process.env.AWS_SHARED_CREDENTIALS_FILE;
+ }
+
+ // Setup a MFA callback for asking the code from the user.
+ params.tokenCodeFn = (mfaSerial, callback) => {
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
+ rl.question(`Enter MFA code for ${mfaSerial}: `, (answer) => {
+ rl.close();
+ callback(null, answer);
+ });
+ };
+
+ this.chain.providers.push(() => new AWS.SharedIniFileCredentials(params));
+ this.chain.providers.push(() => new AWS.ProcessCredentials(params));
+ }
+ }
+};
diff --git a/node_modules/serverless/lib/plugins/aws/provider.js b/node_modules/serverless/lib/plugins/aws/provider.js
index dff500a..a8820c8 100644
--- a/node_modules/serverless/lib/plugins/aws/provider.js
+++ b/node_modules/serverless/lib/plugins/aws/provider.js
@@ -4,10 +4,10 @@ const AWS = require('aws-sdk');
const BbPromise = require('bluebird');
const _ = require('lodash');
const naming = require('./lib/naming.js');
+const AwsCredentials = require('./lib/awsCredentials');
const fsp = require('fs').promises;
const getS3EndpointForRegion = require('./utils/getS3EndpointForRegion');
const memoizeeMethods = require('memoizee/methods');
-const readline = require('readline');
const { ALB_LISTENER_REGEXP } = require('./package/compile/events/alb/lib/validate');
const d = require('d');
const path = require('path');
@@ -56,104 +56,6 @@ const apiGatewayUsagePlan = {
additionalProperties: false,
};
-const impl = {
- /**
- * Determine whether the given credentials are valid. It turned out that detecting invalid
- * credentials was more difficult than detecting the positive cases we know about. Hooray for
- * whack-a-mole!
- * @param credentials The credentials to test for validity
- * @return {boolean} Whether the given credentials were valid
- */
- validCredentials: (credentials) => {
- let result = false;
- if (credentials) {
- if (
- // valid credentials loaded
- (credentials.accessKeyId &&
- credentials.accessKeyId !== 'undefined' &&
- credentials.secretAccessKey &&
- credentials.secretAccessKey !== 'undefined') ||
- // a role to assume has been successfully loaded, the associated STS request has been
- // sent, and the temporary credentials will be asynchronously delivered.
- credentials.roleArn
- ) {
- result = true;
- }
- }
- return result;
- },
- /**
- * Add credentials, if present, to the given results
- * @param results The results to add the given credentials to if they are valid
- * @param credentials The credentials to validate and add to the results if valid
- */
- addCredentials: (results, credentials) => {
- if (impl.validCredentials(credentials)) {
- results.credentials = credentials;
- }
- },
- /**
- * Add credentials, if present, from the environment
- * @param results The results to add environment credentials to
- * @param prefix The environment variable prefix to use in extracting credentials
- */
- addEnvironmentCredentials: (results, prefix) => {
- if (prefix) {
- const environmentCredentials = new AWS.EnvironmentCredentials(prefix);
- impl.addCredentials(results, environmentCredentials);
- }
- },
- /**
- * Add credentials from a profile, if the profile and credentials for it exists
- * @param results The results to add profile credentials to
- * @param profile The profile to load credentials from
- */
- addProfileCredentials: (results, profile) => {
- if (profile) {
- const params = { profile };
- if (process.env.AWS_SHARED_CREDENTIALS_FILE) {
- params.filename = process.env.AWS_SHARED_CREDENTIALS_FILE;
- }
-
- // Setup a MFA callback for asking the code from the user.
- params.tokenCodeFn = (mfaSerial, callback) => {
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
- rl.question(`Enter MFA code for ${mfaSerial}: `, (answer) => {
- rl.close();
- callback(null, answer);
- });
- };
-
- const profileCredentials = new AWS.SharedIniFileCredentials(params);
- if (
- !(
- profileCredentials.accessKeyId ||
- profileCredentials.sessionToken ||
- profileCredentials.roleArn
- )
- ) {
- throw new ServerlessError(
- `AWS profile "${profile}" doesn't seem to be configured`,
- 'UNRECOGNIZED_AWS_PROFILE'
- );
- }
-
- impl.addCredentials(results, profileCredentials);
- }
- },
- /**
- * Add credentials, if present, from a profile that is specified within the environment
- * @param results The prefix of the profile's declaration in the environment
- * @param prefix The prefix for the environment variable
- */
- addEnvironmentProfile: (results, prefix) => {
- if (prefix) {
- const profile = process.env[`${prefix}_PROFILE`];
- impl.addProfileCredentials(results, profile);
- }
- },
-};
-
const baseAlbAuthorizerProperties = {
onUnauthenticatedRequest: { enum: ['allow', 'authenticate', 'deny'] },
requestExtraParams: {
@@ -1448,8 +1350,7 @@ class AwsProvider {
}
const requestOptions = _.isObject(options) ? options : {};
const shouldCache = _.get(requestOptions, 'useCache', false);
- // Copy is required as the credentials may be modified during the request
- const credentials = Object.assign({}, this.getCredentials());
+ const credentials = await this.getCredentials();
const serviceOptions = {
name: service,
params: {
@@ -1462,36 +1363,36 @@ class AwsProvider {
}
/**
- * Fetch credentials directly or using a profile from serverless yml configuration or from the
- * well known environment variables
- * @returns {{region: *}}
+ * If using outside of AWS, you are expected to use credentials.get/Promise to ensure they have
+ * not expired (https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Credentials.html#getPromise-property)
+ * @returns {credentials: AWS.Credentials, region: string}
*/
getCredentials() {
if (this.cachedCredentials) {
// We have already created the credentials object once, so return it.
return this.cachedCredentials;
}
- const result = {};
const stageUpper = this.getStage() ? this.getStage().toUpperCase() : null;
- // add specified credentials, overriding with more specific declarations
- const awsDefaultProfile = process.env.AWS_DEFAULT_PROFILE || 'default';
- try {
- impl.addProfileCredentials(result, awsDefaultProfile);
- } catch (err) {
- if (err.code !== 'UNRECOGNIZED_AWS_PROFILE') throw err;
- }
+ const credentials = new AwsCredentials();
+ // AwsCredentials returns the first credentials to resolve, so add from most-to-least specific:
+ credentials.addProfile(this.options['aws-profile']); // CLI option profile
+
+ credentials.addProfile(process.env[`AWS_${stageUpper}_PROFILE`]); // stage specific creds
+ credentials.addEnvironment(`AWS_${stageUpper}`);
+
+ credentials.addProfile(process.env.AWS_PROFILE); // creds for all stages
+ credentials.addEnvironment('AWS');
+
if (this.serverless.service.provider.profile && !this.options['aws-profile']) {
- // config profile
- impl.addProfileCredentials(result, this.serverless.service.provider.profile);
- }
- impl.addEnvironmentCredentials(result, 'AWS'); // creds for all stages
- impl.addEnvironmentProfile(result, 'AWS');
- impl.addEnvironmentCredentials(result, `AWS_${stageUpper}`); // stage specific creds
- impl.addEnvironmentProfile(result, `AWS_${stageUpper}`);
- if (this.options['aws-profile']) {
- impl.addProfileCredentials(result, this.options['aws-profile']); // CLI option profile
+ credentials.addProfile(this.serverless.service.provider.profile);
}
+ credentials.addConfig(this.serverless.service.provider.credentials); // config creds
+ credentials.addProfile(process.env.AWS_DEFAULT_PROFILE || 'default');
+
+ // Store the credentials to avoid creating them again (messes up MFA).
+ const region = this.getRegion();
+ this.cachedCredentials = { credentials, region };
const deploymentBucketObject = this.serverless.service.provider.deploymentBucketObject;
if (
@@ -1499,12 +1400,10 @@ class AwsProvider {
deploymentBucketObject.serverSideEncryption &&
deploymentBucketObject.serverSideEncryption === 'aws:kms'
) {
- result.signatureVersion = 'v4';
+ this.cachedCredentials.signatureVersion = 'v4';
}
- // Store the credentials to avoid creating them again (messes up MFA).
- this.cachedCredentials = result;
- return result;
+ return this.cachedCredentials;
}
// This function will be used to block the addition of transfer acceleration options
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment