serverless/lib/cli/interactive-setup/aws-credentials.js
2021-06-28 15:19:55 +02:00

447 lines
14 KiB
JavaScript

'use strict';
const chalk = require('chalk');
const _ = require('lodash');
const inquirer = require('@serverless/utils/inquirer');
const memoizee = require('memoizee');
const AWS = require('aws-sdk');
const awsCredentials = require('../../plugins/aws/utils/credentials');
const { confirm, doesServiceInstanceHaveLinkedProvider } = require('./utils');
const openBrowser = require('../../utils/openBrowser');
const ServerlessError = require('../../serverless-error');
const resolveStage = require('../../utils/resolve-stage');
const resolveRegion = require('../../utils/resolve-region');
const isValidAwsAccessKeyId = RegExp.prototype.test.bind(/^[A-Z0-9]{10,}$/);
const isValidAwsSecretAccessKey = RegExp.prototype.test.bind(/^[a-zA-Z0-9/+]{10,}$/);
const { getPlatformClientWithAccessKey } = require('@serverless/dashboard-plugin/lib/clientUtils');
const isAuthenticated = require('@serverless/dashboard-plugin/lib/isAuthenticated');
const CREDENTIALS_SETUP_CHOICE = {
LOCAL: '_local_',
CREATE_PROVIDER: '_create_provider_',
SKIP: '_skip_',
};
const getProviderLinkUid = ({ app, service, stage, region }) =>
`appName|${app}|serviceName|${service}|stage|${stage}|region|${region}`;
const getSdkInstance = memoizee(
async (orgName) => {
return getPlatformClientWithAccessKey(orgName);
},
{ promise: true }
);
const getOrgUidByName = memoizee(
async (orgName) => {
const sdk = await getSdkInstance(orgName);
let organization;
try {
organization = await sdk.organizations.get({ orgName });
} catch (err) {
if (err.statusCode && err.statusCode >= 500) {
throw new ServerlessError(
'Dashboard service is currently unavailable, please try again later',
'DASHBOARD_UNAVAILABLE'
);
}
throw err;
}
return organization.orgUid;
},
{ promise: true }
);
const getProviders = memoizee(
async (orgName) => {
const sdk = await getSdkInstance(orgName);
const orgUid = await getOrgUidByName(orgName);
let providers;
try {
providers = await sdk.getProviders(orgUid);
} catch (err) {
if (err.statusCode && err.statusCode >= 500) {
throw new ServerlessError(
'Dashboard service is currently unavailable, please try again later',
'DASHBOARD_UNAVAILABLE'
);
}
throw err;
}
return providers.result;
},
{
promise: true,
}
);
const awsAccessKeyIdInput = async ({ stepHistory }) => {
const accessKeyId = (
await inquirer.prompt({
message: 'AWS Access Key Id:',
type: 'input',
name: 'accessKeyId',
validate: (input) => {
if (isValidAwsAccessKeyId(input.trim())) return true;
return 'AWS Access Key Id seems not valid.\n Expected something like AKIAIOSFODNN7EXAMPLE';
},
})
).accessKeyId.trim();
stepHistory.set('accessKeyId', '_user_provided_');
return accessKeyId;
};
const awsSecretAccessKeyInput = async ({ stepHistory }) => {
const secretAccessKey = (
await inquirer.prompt({
message: 'AWS Secret Access Key:',
type: 'input',
name: 'secretAccessKey',
validate: (input) => {
if (isValidAwsSecretAccessKey(input.trim())) return true;
return (
'AWS Secret Access Key seems not valid.\n' +
' Expected something like wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
);
},
})
).secretAccessKey.trim();
stepHistory.set('secretAccessKey', '_user_provided_');
return secretAccessKey;
};
const credentialsSetupChoice = async (context, providers) => {
let credentialsSetupChoices = [];
let message = 'No AWS credentials found, what credentials do you want to use?';
if (providers) {
// This is situation where we know that user has decided to link his service to an org
const hasExistingProviders = Boolean(providers.length);
if (hasExistingProviders) {
message = 'What credentials do you want to use?';
}
const createAccessRoleName = hasExistingProviders
? 'Create a new AWS Access Role provider'
: 'AWS Access Role (most secure)';
const formatProviderName = (provider) => {
if (provider.providerType === 'roleArn') {
return `${provider.alias} (${provider.providerDetails.roleArn})`;
}
// Otherwise its `accessKey`-based provider
return `${provider.alias} (${provider.providerDetails.accessKeyId})`;
};
credentialsSetupChoices = [
...providers.map((provider) => ({
name: formatProviderName(provider),
value: provider.providerUid,
})),
{ name: createAccessRoleName, value: CREDENTIALS_SETUP_CHOICE.CREATE_PROVIDER },
];
}
credentialsSetupChoices.push(
{ name: 'Local AWS Access Keys', value: CREDENTIALS_SETUP_CHOICE.LOCAL },
{ name: 'Skip', value: CREDENTIALS_SETUP_CHOICE.SKIP }
);
const result = (
await inquirer.prompt({
message,
type: 'list',
name: 'credentialsSetupChoice',
choices: credentialsSetupChoices,
})
).credentialsSetupChoice;
context.stepHistory.set(
'credentialsSetupChoice',
result.startsWith('_') ? result : '_user_provided_'
);
return result;
};
const steps = {
writeOnSetupSkip: () =>
process.stdout.write(`\nYou can setup your AWS account later. More details available here:
http://slss.io/aws-creds-setup\n`),
ensureAwsAccount: async ({ stepHistory }) => {
if (await confirm('Do you have an AWS account?', { name: 'hasAwsAccount' })) return;
openBrowser('https://portal.aws.amazon.com/billing/signup');
await inquirer.prompt({
message: 'Press Enter to continue after creating an AWS account',
name: 'createAwsAccountPrompt',
});
stepHistory.set('createAwsAccountPrompt', true);
},
ensureAwsCredentials: async ({ options, configuration, stepHistory }) => {
const region = options.region || configuration.provider.region || 'us-east-1';
openBrowser(
`https://console.aws.amazon.com/iam/home?region=${region}#/users$new?step=final&accessKey&userNames=serverless&permissionType=policies&policies=arn:aws:iam::aws:policy%2FAdministratorAccess`
);
await inquirer.prompt({
message: 'Press Enter to continue after creating an AWS user with access keys',
name: 'generateAwsCredsPrompt',
});
stepHistory.set('generateAwsCredsPrompt', true);
},
inputAwsCredentials: async (context) => {
const accessKeyId = await awsAccessKeyIdInput(context);
const secretAccessKey = await awsSecretAccessKeyInput(context);
await awsCredentials.saveFileProfiles(new Map([['default', { accessKeyId, secretAccessKey }]]));
process.stdout.write(
`\n${chalk.green(
`AWS credentials saved on your machine at ${chalk.bold(
process.platform === 'win32' ? '%userprofile%\\.aws\\credentials' : '~/.aws/credentials'
)}. Go there to change them at any time.`
)}\n`
);
},
handleProviderCreation: async ({ configuration: { org: orgName }, stepHistory }) => {
const providersUrl = `https://app.serverless.com/${orgName}/settings/providers?source=cli&providerId=new&provider=aws`;
openBrowser(chalk.bold.white(providersUrl));
process.stdout.write(
'To learn more about providers, visit: http://slss.io/add-providers-dashboard\n'
);
process.stdout.write('\nWaiting for creation of AWS Access Role provider...\n');
let onEvent;
let showSkipPromptTimeout;
const p = new Promise((resolve) => {
let inquirerPrompt;
const timeoutDuration = 1000 * 30; // 30 seconds
showSkipPromptTimeout = setTimeout(() => {
inquirerPrompt = inquirer.prompt({
message:
'\n [If you approached an issue when setting up a provider, you may press Enter to skip this step]',
name: 'skipProviderSetup',
});
inquirerPrompt.then(() => {
stepHistory.set('skipProviderSetup', true);
resolve(null);
});
}, timeoutDuration);
onEvent = (event) => {
if (inquirerPrompt) {
// Disable inquirer prompt asking to skip without setting provider
inquirerPrompt.ui.close();
}
clearTimeout(showSkipPromptTimeout);
resolve(event);
};
});
// Listen for `provider.created` event to detect creation of new provider
const sdk = await getSdkInstance(orgName);
try {
await sdk.connect({
orgName,
onEvent,
filter: {
events: ['provider.created'],
},
});
} catch (err) {
// Ensure that prompt timeout is cleared in case of error
clearTimeout(showSkipPromptTimeout);
if (err.statusCode && err.statusCode >= 500) {
throw new ServerlessError(
'Dashboard service is currently unavailable, please try again later',
'DASHBOARD_UNAVAILABLE'
);
}
throw err;
}
let maybeEvent;
try {
maybeEvent = await p;
} finally {
sdk.disconnect();
}
if (maybeEvent) {
process.stdout.write(
`\n${chalk.green('AWS Access Role provider was successfully created')}\n`
);
return maybeEvent.data.object.provider_uid;
}
process.stdout.write(
'\nSkipping credentials provider setup. You can still setup credentials provider later.\n'
);
return null;
},
linkProviderToServiceInstance: async ({ providerUid, configuration, options }) => {
const { app, service, org } = configuration;
const stage = resolveStage({ configuration, options });
const region = resolveRegion({ configuration, options });
const sdk = await getSdkInstance(org);
const linkType = 'instance';
const linkUid = getProviderLinkUid({ app, service, region, stage });
let orgUid;
try {
orgUid = await getOrgUidByName(org);
} catch (err) {
if (err.code === 'DASHBOARD_UNAVAILABLE') {
process.stdout.write(`\n${chalk.yellow(err.message)}\n`);
return false;
}
throw err;
}
try {
await sdk.createProviderLink(orgUid, linkType, linkUid, providerUid);
return true;
} catch (err) {
if (err.statusCode && err.statusCode >= 500) {
process.stdout.write(
`\n${chalk.yellow(
'Dashboard service is currently unavailable, please try again later'
)}\n`
);
return false;
}
throw err;
}
},
};
module.exports = {
async isApplicable(context) {
const { configuration, history, options } = context;
if (
_.get(configuration, 'provider') !== 'aws' &&
_.get(configuration, 'provider.name') !== 'aws'
) {
context.inapplicabilityReasonCode = 'NON_AWS_PROVIDER';
return false;
}
if (new AWS.S3().config.credentials) {
context.inapplicabilityReasonCode = 'LOCAL_CREDENTIALS_CONFIGURED';
return false;
}
if ((await awsCredentials.resolveFileProfiles()).size) {
context.inapplicabilityReasonCode = 'LOCAL_CREDENTIAL_PROFILES_CONFIGURED';
return false;
}
const orgName = configuration.org;
if (orgName && isAuthenticated()) {
let providers;
try {
providers = await getProviders(orgName);
} catch (err) {
if (err.code === 'DASHBOARD_UNAVAILABLE') {
process.stdout.write(`\n${chalk.yellow(err.message)}\n`);
return false;
}
throw err;
}
const hasDefaultProvider = providers.some((provider) => provider.isDefault);
if (hasDefaultProvider) {
context.inapplicabilityReasonCode = 'DEFAULT_PROVIDER_CONFIGURED';
return false;
}
// For situation where it is invoked for already existing service
// We need to check if service already has a linked provider
if (
providers &&
!history.has('service') &&
(await doesServiceInstanceHaveLinkedProvider({ configuration, options }))
) {
context.inapplicabilityReasonCode = 'LINKED_PROVIDER_CONFIGURED';
return false;
}
}
return true;
},
async run(context) {
let providers;
// It is possible that user decides to not configure org for his service and
// we still should allow setup of local credentials in such case
if (context.configuration.org && isAuthenticated()) {
try {
providers = await getProviders(context.configuration.org);
} catch (err) {
if (err.code === 'DASHBOARD_UNAVAILABLE') {
process.stdout.write(`\n${chalk.yellow(err.message)}\n`);
return;
}
throw err;
}
}
const credentialsSetupChoiceAnswer = await credentialsSetupChoice(context, providers);
if (credentialsSetupChoiceAnswer === CREDENTIALS_SETUP_CHOICE.CREATE_PROVIDER) {
try {
const createdProviderUid = await steps.handleProviderCreation(context);
const hadExistingProviders = Boolean(providers.length);
const shouldLinkProvider = createdProviderUid && hadExistingProviders;
if (shouldLinkProvider) {
// This is situation where user decided to create a new provider and already had previous providers setup
// In this case, we want to setup an explicit link between provider and service as the newly created provider
// might not be the default one.
await steps.linkProviderToServiceInstance({
configuration: context.configuration,
providerUid: createdProviderUid,
options: context.options,
});
}
return;
} catch (err) {
if (err.code === 'DASHBOARD_UNAVAILABLE') {
process.stdout.write(`\n${chalk.yellow(err.message)}\n`);
return;
}
throw err;
}
} else if (credentialsSetupChoiceAnswer === CREDENTIALS_SETUP_CHOICE.SKIP) {
steps.writeOnSetupSkip();
return;
} else if (credentialsSetupChoiceAnswer === CREDENTIALS_SETUP_CHOICE.LOCAL) {
await steps.ensureAwsAccount(context);
await steps.ensureAwsCredentials(context);
await steps.inputAwsCredentials(context);
return;
}
// Otherwise user selected an existing provider
const linked = await steps.linkProviderToServiceInstance({
configuration: context.configuration,
providerUid: credentialsSetupChoiceAnswer,
options: context.options,
});
if (linked) {
process.stdout.write(
`\n${chalk.green('Selected provider was successfully linked to your service')}\n`
);
}
},
steps,
configuredQuestions: [
'credentialsSetupChoice',
'hasAwsAccount',
'createAwsAccountPrompt',
'generateAwsCredsPrompt',
'accessKeyId',
'secretAccessKey',
'skipProviderSetup',
],
};