From fee247c74f54b050f7b7a6ea0733fbd08976f232 Mon Sep 17 00:00:00 2001 From: Kaustav Ghosh Date: Tue, 28 May 2019 21:06:38 -0700 Subject: [PATCH] feat: flow to add policies to access amplify resources from Lambda (#1462) * feat: flow to add policies to access amplify resources from Lambda * changes based on feedback * fix variable names * chnage var names to camel-case * fix error messages * fix region string issue * fix wording for resource selection * fix resource cyclic-dependency issues * change JSON.parse to context.amplify.readJSON * modify AppSync CRUD permissions --- packages/amplify-category-analytics/index.js | 32 ++ .../provider-utils/awscloudformation/index.js | 17 +- .../pinpoint-walkthrough.js | 54 ++- packages/amplify-category-api/index.js | 30 ++ .../provider-utils/awscloudformation/index.js | 16 +- .../service-walkthroughs/apigw-walkthrough.js | 57 ++- .../appSync-walkthrough.js | 47 ++- packages/amplify-category-auth/index.js | 31 ++ packages/amplify-category-auth/package.json | 1 + .../provider-utils/awscloudformation/index.js | 16 + .../service-walkthroughs/auth-questions.js | 135 +++++++ .../commands/function/update.js | 24 ++ packages/amplify-category-function/index.js | 30 ++ .../lambda-cloudformation-template.json.ejs | 15 +- .../function-template-dir/crud-app.js.ejs | 3 + .../{index.js => index.js.ejs} | 2 + .../serverless-app.js.ejs | 3 + .../provider-utils/awscloudformation/index.js | 73 +++- .../lambda-walkthrough.js | 367 +++++++++++++++++- packages/amplify-category-hosting/index.js | 18 + .../lib/category-manager.js | 48 +++ .../amplify-category-interactions/index.js | 31 ++ .../provider-utils/awscloudformation/index.js | 18 +- .../service-walkthroughs/lex-walkthrough.js | 53 ++- packages/amplify-category-storage/index.js | 30 ++ .../provider-utils/awscloudformation/index.js | 19 +- .../dynamoDb-walkthrough.js | 32 +- .../service-walkthroughs/s3-walkthrough.js | 47 ++- packages/amplify-category-xr/index.js | 19 + .../amplify-category-xr/lib/xr-manager.js | 47 +++ .../amplify-helpers/update-amplify-meta.js | 36 +- 31 files changed, 1330 insertions(+), 21 deletions(-) create mode 100644 packages/amplify-category-function/commands/function/update.js rename packages/amplify-category-function/provider-utils/awscloudformation/function-template-dir/{index.js => index.js.ejs} (89%) diff --git a/packages/amplify-category-analytics/index.js b/packages/amplify-category-analytics/index.js index 2d10ef181b1..4e02e657eb1 100644 --- a/packages/amplify-category-analytics/index.js +++ b/packages/amplify-category-analytics/index.js @@ -3,11 +3,43 @@ const { migrate, } = require('./provider-utils/awscloudformation/service-walkthroughs/pinpoint-walkthrough'); +const category = 'analytics'; + function console(context) { pinpointHelper.console(context); } +async function getPermissionPolicies(context, resourceOpsMapping) { + const amplifyMetaFilePath = context.amplify.pathManager.getAmplifyMetaFilePath(); + const amplifyMeta = context.amplify.readJsonFile(amplifyMetaFilePath); + const permissionPolicies = []; + const resourceAttributes = []; + + Object.keys(resourceOpsMapping).forEach((resourceName) => { + try { + const providerController = require(`./provider-utils/${amplifyMeta[category][resourceName].providerPlugin}/index`); + if (providerController) { + const { policy, attributes } = providerController.getPermissionPolicies( + context, + amplifyMeta[category][resourceName].service, + resourceName, + resourceOpsMapping[resourceName], + ); + permissionPolicies.push(policy); + resourceAttributes.push({ resourceName, attributes, category }); + } else { + context.print.error(`Provider not configured for ${category}: ${resourceName}`); + } + } catch (e) { + context.print.warning(`Could not get policies for ${category}: ${resourceName}`); + throw e; + } + }); + return { permissionPolicies, resourceAttributes }; +} + module.exports = { console, migrate, + getPermissionPolicies, }; diff --git a/packages/amplify-category-analytics/provider-utils/awscloudformation/index.js b/packages/amplify-category-analytics/provider-utils/awscloudformation/index.js index 871220965bc..cd4d1ccb3c1 100644 --- a/packages/amplify-category-analytics/provider-utils/awscloudformation/index.js +++ b/packages/amplify-category-analytics/provider-utils/awscloudformation/index.js @@ -1,4 +1,3 @@ - function addResource(context, category, service) { const serviceMetadata = context.amplify.readJsonFile(`${__dirname}/../supported-services.json`)[service]; const { defaultValuesFilename, serviceWalkthroughFilename } = serviceMetadata; @@ -9,4 +8,18 @@ function addResource(context, category, service) { return addWalkthrough(context, defaultValuesFilename, serviceMetadata); } -module.exports = { addResource }; +function getPermissionPolicies(context, service, resourceName, crudOptions) { + const serviceMetadata = context.amplify.readJsonFile(`${__dirname}/../supported-services.json`)[service]; + const { serviceWalkthroughFilename } = serviceMetadata; + const serviceWalkthroughSrc = `${__dirname}/service-walkthroughs/${serviceWalkthroughFilename}`; + const { getIAMPolicies } = require(serviceWalkthroughSrc); + + if (!getPermissionPolicies) { + context.print.info(`No policies found for ${resourceName}`); + return; + } + + return getIAMPolicies(resourceName, crudOptions); +} + +module.exports = { addResource, getPermissionPolicies }; diff --git a/packages/amplify-category-analytics/provider-utils/awscloudformation/service-walkthroughs/pinpoint-walkthrough.js b/packages/amplify-category-analytics/provider-utils/awscloudformation/service-walkthroughs/pinpoint-walkthrough.js index e9faabaef2f..4f6eff7ef48 100644 --- a/packages/amplify-category-analytics/provider-utils/awscloudformation/service-walkthroughs/pinpoint-walkthrough.js +++ b/packages/amplify-category-analytics/provider-utils/awscloudformation/service-walkthroughs/pinpoint-walkthrough.js @@ -343,4 +343,56 @@ function isRefNode(node, refName) { return false; } -module.exports = { addWalkthrough, migrate }; +function getIAMPolicies(resourceName, crudOptions) { + let policy = {}; + const actions = []; + + crudOptions.forEach((crudOption) => { + switch (crudOption) { + case 'create': actions.push( + 'mobiletargeting:Put*', + 'mobiletargeting:Create*', + 'mobiletargeting:Send*', + ); + break; + case 'update': actions.push('mobiletargeting:Update*'); + break; + case 'read': actions.push('mobiletargeting:Get*', 'mobiletargeting:List*'); + break; + case 'delete': actions.push('mobiletargeting:Delete*'); + break; + default: console.log(`${crudOption} not supported`); + } + }); + + policy = { + Effect: 'Allow', + Action: actions, + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:aws:mobiletargeting:', + { + Ref: `${category}${resourceName}Region`, + }, + ':', + { Ref: 'AWS::AccountId' }, + ':apps/', + { + Ref: `${category}${resourceName}Id`, + }, + ], + ], + }, + ], + }; + + const attributes = ['Id', 'Region']; + + return { policy, attributes }; +} + + +module.exports = { addWalkthrough, migrate, getIAMPolicies }; diff --git a/packages/amplify-category-api/index.js b/packages/amplify-category-api/index.js index 66b7f9afaeb..e0377b29a40 100644 --- a/packages/amplify-category-api/index.js +++ b/packages/amplify-category-api/index.js @@ -133,8 +133,38 @@ async function initEnv(context) { }); } +async function getPermissionPolicies(context, resourceOpsMapping) { + const amplifyMetaFilePath = context.amplify.pathManager.getAmplifyMetaFilePath(); + const amplifyMeta = context.amplify.readJsonFile(amplifyMetaFilePath); + const permissionPolicies = []; + const resourceAttributes = []; + + Object.keys(resourceOpsMapping).forEach((resourceName) => { + try { + const providerController = require(`./provider-utils/${amplifyMeta[category][resourceName].providerPlugin}/index`); + if (providerController) { + const { policy, attributes } = providerController.getPermissionPolicies( + context, + amplifyMeta[category][resourceName].service, + resourceName, + resourceOpsMapping[resourceName], + ); + permissionPolicies.push(policy); + resourceAttributes.push({ resourceName, attributes, category }); + } else { + context.print.error(`Provider not configured for ${category}: ${resourceName}`); + } + } catch (e) { + context.print.warning(`Could not get policies for ${category}: ${resourceName}`); + throw e; + } + }); + return { permissionPolicies, resourceAttributes }; +} + module.exports = { console, migrate, initEnv, + getPermissionPolicies, }; diff --git a/packages/amplify-category-api/provider-utils/awscloudformation/index.js b/packages/amplify-category-api/provider-utils/awscloudformation/index.js index 2a1adffc880..0236823e5a5 100644 --- a/packages/amplify-category-api/provider-utils/awscloudformation/index.js +++ b/packages/amplify-category-api/provider-utils/awscloudformation/index.js @@ -170,6 +170,20 @@ function addDatasource(context, category, datasource) { return serviceQuestions(context, defaultValuesFilename, serviceWalkthroughFilename); } +function getPermissionPolicies(context, service, resourceName, crudOptions) { + serviceMetadata = context.amplify.readJsonFile(`${__dirname}/../supported-services.json`)[service]; + const { serviceWalkthroughFilename } = serviceMetadata; + const serviceWalkthroughSrc = `${__dirname}/service-walkthroughs/${serviceWalkthroughFilename}`; + const { getIAMPolicies } = require(serviceWalkthroughSrc); + + if (!getPermissionPolicies) { + context.print.info(`No policies found for ${resourceName}`); + return; + } + + return getIAMPolicies(resourceName, crudOptions); +} + module.exports = { - addResource, updateResource, console, migrateResource, addDatasource, + addResource, updateResource, console, migrateResource, addDatasource, getPermissionPolicies, }; diff --git a/packages/amplify-category-api/provider-utils/awscloudformation/service-walkthroughs/apigw-walkthrough.js b/packages/amplify-category-api/provider-utils/awscloudformation/service-walkthroughs/apigw-walkthrough.js index 8bbee3b09ab..481592ce265 100644 --- a/packages/amplify-category-api/provider-utils/awscloudformation/service-walkthroughs/apigw-walkthrough.js +++ b/packages/amplify-category-api/provider-utils/awscloudformation/service-walkthroughs/apigw-walkthrough.js @@ -637,4 +637,59 @@ function convertToCRUD(privacy) { return privacy; } -module.exports = { serviceWalkthrough, updateWalkthrough, migrate }; + +function getIAMPolicies(resourceName, crudOptions) { + let policy = {}; + const actions = []; + + crudOptions.forEach((crudOption) => { + switch (crudOption) { + case 'create': actions.push( + 'apigateway:POST', + 'apigateway:PUT', + ); + break; + case 'update': actions.push('apigateway:PATCH'); + break; + case 'read': actions.push( + 'apigateway:GET', 'apigateway:HEAD', + 'apigateway:OPTIONS', + ); + break; + case 'delete': actions.push('apigateway:DELETE'); + break; + default: console.log(`${crudOption} not supported`); + } + }); + + policy = { + Effect: 'Allow', + Action: actions, + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:aws:apigateway:', + { + Ref: 'AWS::Region', + }, + '::/restapis/', + { + Ref: `${category}${resourceName}ApiName`, + }, + '/*', + ], + ], + }, + ], + }; + + const attributes = ['ApiName']; + + return { policy, attributes }; +} + +module.exports = { + serviceWalkthrough, updateWalkthrough, migrate, getIAMPolicies, +}; diff --git a/packages/amplify-category-api/provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough.js b/packages/amplify-category-api/provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough.js index 646fda456dd..b713abfb947 100644 --- a/packages/amplify-category-api/provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough.js +++ b/packages/amplify-category-api/provider-utils/awscloudformation/service-walkthroughs/appSync-walkthrough.js @@ -375,7 +375,52 @@ async function migrate(context) { await context.amplify.executeProviderUtils(context, 'awscloudformation', 'compileSchema', { noConfig: true, forceCompile: true, migrate: true }); } +function getIAMPolicies(resourceName, crudOptions) { + let policy = {}; + const actions = []; + + crudOptions.forEach((crudOption) => { + switch (crudOption) { + case 'create': actions.push('appsync:Create*', 'appsync:StartSchemaCreation', 'appsync:GraphQL'); + break; + case 'update': actions.push('appsync:Update*'); + break; + case 'read': actions.push('appsync:Get*', 'appsync:List*'); + break; + case 'delete': actions.push('appsync:Delete*'); + break; + default: console.log(`${crudOption} not supported`); + } + }); + + policy = { + Effect: 'Allow', + Action: actions, + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:aws:appsync:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':apis/', + { + Ref: `${category}${resourceName}GraphQLAPIIdOutput`, + }, + ], + ], + }, + ], + }; + + const attributes = ['GraphQLAPIIdOutput']; + + return { policy, attributes }; +} + module.exports = { - serviceWalkthrough, updateWalkthrough, openConsole, migrate, + serviceWalkthrough, updateWalkthrough, openConsole, migrate, getIAMPolicies, }; diff --git a/packages/amplify-category-auth/index.js b/packages/amplify-category-auth/index.js index 009273fb5fe..797781a549b 100644 --- a/packages/amplify-category-auth/index.js +++ b/packages/amplify-category-auth/index.js @@ -231,6 +231,36 @@ async function console(context) { }); } +async function getPermissionPolicies(context, resourceOpsMapping) { + const amplifyMetaFilePath = context.amplify.pathManager.getAmplifyMetaFilePath(); + const amplifyMeta = context.amplify.readJsonFile(amplifyMetaFilePath); + const permissionPolicies = []; + const resourceAttributes = []; + + Object.keys(resourceOpsMapping).forEach((resourceName) => { + try { + const providerController = require(`./provider-utils/${amplifyMeta[category][resourceName].providerPlugin}/index`); + if (providerController) { + const { policy, attributes } = providerController.getPermissionPolicies( + context, + amplifyMeta[category][resourceName].service, + resourceName, + resourceOpsMapping[resourceName], + ); + permissionPolicies.push(policy); + resourceAttributes.push({ resourceName, attributes, category }); + } else { + context.print.error(`Provider not configured for ${category}: ${resourceName}`); + } + } catch (e) { + context.print.warning(`Could not get policies for ${category}: ${resourceName}`); + throw e; + } + }); + return { permissionPolicies, resourceAttributes }; +} + + module.exports = { externalAuthEnable, checkRequirements, @@ -238,4 +268,5 @@ module.exports = { migrate, initEnv, console, + getPermissionPolicies, }; diff --git a/packages/amplify-category-auth/package.json b/packages/amplify-category-auth/package.json index bbf8ca04964..cd0774d10c0 100755 --- a/packages/amplify-category-auth/package.json +++ b/packages/amplify-category-auth/package.json @@ -17,6 +17,7 @@ "chalk-pipe": "^1.2.0", "eslint": "^4.19.1", "inquirer": "^6.0.0", + "fs-extra": "^7.0.0", "jest": "^23.5.0", "lodash": "^4.17.10", "opn": "^5.3.0", diff --git a/packages/amplify-category-auth/provider-utils/awscloudformation/index.js b/packages/amplify-category-auth/provider-utils/awscloudformation/index.js index fdcd7fbacbd..25635f30505 100644 --- a/packages/amplify-category-auth/provider-utils/awscloudformation/index.js +++ b/packages/amplify-category-auth/provider-utils/awscloudformation/index.js @@ -517,6 +517,21 @@ async function openIdentityPoolConsole(context, region, identityPoolId) { context.print.success(identityPoolConsoleUrl); } +function getPermissionPolicies(context, service, resourceName, crudOptions) { + serviceMetadata = context.amplify.readJsonFile(`${__dirname}/../supported-services.json`)[service]; + const { serviceWalkthroughFilename } = serviceMetadata; + const serviceWalkthroughSrc = `${__dirname}/service-walkthroughs/${serviceWalkthroughFilename}`; + const { getIAMPolicies } = require(serviceWalkthroughSrc); + + if (!getPermissionPolicies) { + context.print.info(`No policies found for ${resourceName}`); + return; + } + + return getIAMPolicies(resourceName, crudOptions); +} + + module.exports = { addResource, updateResource, @@ -526,4 +541,5 @@ module.exports = { copyCfnTemplate, migrate, console, + getPermissionPolicies, }; diff --git a/packages/amplify-category-auth/provider-utils/awscloudformation/service-walkthroughs/auth-questions.js b/packages/amplify-category-auth/provider-utils/awscloudformation/service-walkthroughs/auth-questions.js index 3e358829022..0f1ddaaf5bb 100644 --- a/packages/amplify-category-auth/provider-utils/awscloudformation/service-walkthroughs/auth-questions.js +++ b/packages/amplify-category-auth/provider-utils/awscloudformation/service-walkthroughs/auth-questions.js @@ -3,6 +3,8 @@ const chalk = require('chalk'); const chalkpipe = require('chalk-pipe'); const { authProviders, attributeProviderMap } = require('../assets/string-maps'); +const category = 'auth'; + async function serviceWalkthrough( context, @@ -365,9 +367,142 @@ function filterInput(input, updateFlow) { return false; } +function getIAMPolicies(resourceName, crudOptions) { + let policy = {}; + const actions = []; + + crudOptions.forEach((crudOption) => { + switch (crudOption) { + case 'create': actions.push( + 'cognito-idp:ConfirmSignUp', + 'cognito-idp:AdminCreateUser', + 'cognito-idp:CreateUserImportJob', + 'cognito-idp:AdminSetUserSettings', + 'cognito-idp:AdminLinkProviderForUser', + 'cognito-idp:CreateIdentityProvider', + 'cognito-idp:AdminConfirmSignUp', + 'cognito-idp:AdminDisableUser', + 'cognito-idp:AdminRemoveUserFromGroup', + 'cognito-idp:SetUserMFAPreference', + 'cognito-idp:SetUICustomization', + 'cognito-idp:SignUp', + 'cognito-idp:VerifyUserAttribute', + 'cognito-idp:SetRiskConfiguration', + 'cognito-idp:StartUserImportJob', + 'cognito-idp:AdminSetUserPassword', + 'cognito-idp:AssociateSoftwareToken', + 'cognito-idp:CreateResourceServer', + 'cognito-idp:RespondToAuthChallenge', + 'cognito-idp:CreateUserPoolClient', + 'cognito-idp:AdminUserGlobalSignOut', + 'cognito-idp:GlobalSignOut', + 'cognito-idp:AddCustomAttributes', + 'cognito-idp:CreateGroup', + 'cognito-idp:CreateUserPool', + 'cognito-idp:AdminForgetDevice', + 'cognito-idp:AdminAddUserToGroup', + 'cognito-idp:AdminRespondToAuthChallenge', + 'cognito-idp:ForgetDevice', + 'cognito-idp:CreateUserPoolDomain', + 'cognito-idp:AdminEnableUser', + 'cognito-idp:AdminUpdateDeviceStatus', + 'cognito-idp:StopUserImportJob', + 'cognito-idp:InitiateAuth', + 'cognito-idp:AdminInitiateAuth', + 'cognito-idp:AdminSetUserMFAPreference', + 'cognito-idp:ConfirmForgotPassword', + 'cognito-idp:SetUserSettings', + 'cognito-idp:VerifySoftwareToken', + 'cognito-idp:AdminDisableProviderForUser', + 'cognito-idp:SetUserPoolMfaConfig', + 'cognito-idp:ChangePassword', + 'cognito-idp:ConfirmDevice', + 'cognito-idp:AdminResetUserPassword', + 'cognito-idp:ResendConfirmationCode', + ); + break; + case 'update': actions.push( + 'cognito-idp:ForgotPassword', + 'cognito-idp:UpdateAuthEventFeedback', + 'cognito-idp:UpdateResourceServer', + 'cognito-idp:UpdateUserPoolClient', + 'cognito-idp:AdminUpdateUserAttributes', + 'cognito-idp:UpdateUserAttributes', + 'cognito-idp:UpdateUserPoolDomain', + 'cognito-idp:UpdateIdentityProvider', + 'cognito-idp:UpdateGroup', + 'cognito-idp:AdminUpdateAuthEventFeedback', + 'cognito-idp:UpdateDeviceStatus', + 'cognito-idp:UpdateUserPool', + ); + break; + case 'read': actions.push( + 'cognito-identity:Describe*', + 'cognito-identity:Get*', + 'cognito-identity:List*', + 'cognito-idp:Describe*', + 'cognito-idp:AdminGetDevice', + 'cognito-idp:AdminGetUser', + 'cognito-idp:AdminList*', + 'cognito-idp:List*', + 'cognito-sync:Describe*', + 'cognito-sync:Get*', + 'cognito-sync:List*', + 'iam:ListOpenIdConnectProviders', + 'iam:ListRoles', + 'sns:ListPlatformApplications', + ); + break; + case 'delete': actions.push( + 'cognito-idp:DeleteUserPoolDomain', + 'cognito-idp:DeleteResourceServer', + 'cognito-idp:DeleteGroup', + 'cognito-idp:AdminDeleteUserAttributes', + 'cognito-idp:DeleteUserPoolClient', + 'cognito-idp:DeleteUserAttributes', + 'cognito-idp:DeleteUserPool', + 'cognito-idp:AdminDeleteUser', + 'cognito-idp:DeleteIdentityProvider', + 'cognito-idp:DeleteUser', + ); + break; + default: console.log(`${crudOption} not supported`); + } + }); + + + policy = { + Effect: 'Allow', + Action: actions, + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:aws:cognito-idp::', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':userpool/', + { + Ref: `${category}${resourceName}UserPoolId`, + }, + ], + ], + }, + ], + }; + + const attributes = ['UserPoolId']; + + return { policy, attributes }; +} + + module.exports = { serviceWalkthrough, userPoolProviders, parseOAuthCreds, structureoAuthMetaData, + getIAMPolicies, }; diff --git a/packages/amplify-category-function/commands/function/update.js b/packages/amplify-category-function/commands/function/update.js new file mode 100644 index 00000000000..6850de3e250 --- /dev/null +++ b/packages/amplify-category-function/commands/function/update.js @@ -0,0 +1,24 @@ +const subcommand = 'update'; +const category = 'function'; + +module.exports = { + name: subcommand, + alias: ['configure'], + run: async (context) => { + const { amplify } = context; + const servicesMetadata = amplify.readJsonFile(`${__dirname}/../../provider-utils/supported-services.json`); + return amplify.serviceSelectionPrompt(context, category, servicesMetadata) + .then((result) => { + const providerController = require(`../../provider-utils/${result.providerName}/index`); + if (!providerController) { + context.print.error('Provider not configured for this category'); + return; + } + return providerController.updateResource(context, category, result.service); + }) + .then(() => context.print.success('Successfully updated resource')) + .catch((err) => { + context.print.error(err.stack); + }); + }, +}; diff --git a/packages/amplify-category-function/index.js b/packages/amplify-category-function/index.js index 3c9141b6df6..fa8f0760d33 100644 --- a/packages/amplify-category-function/index.js +++ b/packages/amplify-category-function/index.js @@ -47,9 +47,39 @@ async function migrate(context) { await Promise.all(migrateResourcePromises); } +async function getPermissionPolicies(context, resourceOpsMapping) { + const amplifyMetaFilePath = context.amplify.pathManager.getAmplifyMetaFilePath(); + const amplifyMeta = context.amplify.readJsonFile(amplifyMetaFilePath); + const permissionPolicies = []; + const resourceAttributes = []; + + Object.keys(resourceOpsMapping).forEach((resourceName) => { + try { + const providerController = require(`./provider-utils/${amplifyMeta[category][resourceName].providerPlugin}/index`); + if (providerController) { + const { policy, attributes } = providerController.getPermissionPolicies( + context, + amplifyMeta[category][resourceName].service, + resourceName, + resourceOpsMapping[resourceName], + ); + permissionPolicies.push(policy); + resourceAttributes.push({ resourceName, attributes, category }); + } else { + context.print.error(`Provider not configured for ${category}: ${resourceName}`); + } + } catch (e) { + context.print.warning(`Could not get policies for ${category}: ${resourceName}`); + throw e; + } + }); + return { permissionPolicies, resourceAttributes }; +} + module.exports = { add, console, migrate, + getPermissionPolicies, }; diff --git a/packages/amplify-category-function/provider-utils/awscloudformation/cloudformation-templates/lambda-cloudformation-template.json.ejs b/packages/amplify-category-function/provider-utils/awscloudformation/cloudformation-templates/lambda-cloudformation-template.json.ejs index 2fb4f677a53..6b00757598b 100644 --- a/packages/amplify-category-function/provider-utils/awscloudformation/cloudformation-templates/lambda-cloudformation-template.json.ejs +++ b/packages/amplify-category-function/provider-utils/awscloudformation/cloudformation-templates/lambda-cloudformation-template.json.ejs @@ -54,7 +54,7 @@ } ] }, - "Environment": {"Variables" : { "ENV": {"Ref": "env"}}}, + "Environment": {"Variables" : { "ENV": {"Ref": "env"}, "REGION": { "Ref": 'AWS::Region'}<% if (props.resourceProperties && props.resourceProperties.length > 0) { %>,<%- props.resourceProperties%> <% } %>}}, "Role": { "Fn::GetAtt" : ["LambdaExecutionRole", "Arn"] }, "Runtime": "nodejs8.10", "Timeout": "25" @@ -131,7 +131,20 @@ ] } } + }<% if (props.categoryPolicies) { %> + ,"AmplifyResourcesPolicy": { + "DependsOn": ["LambdaExecutionRole"], + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyName": "amplify-lambda-execution-policy", + "Roles": [{ "Ref": "LambdaExecutionRole" }], + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": <%- JSON.stringify(props.categoryPolicies) %> + } + } } + <% } %> }, "Outputs": { "Name": { diff --git a/packages/amplify-category-function/provider-utils/awscloudformation/function-template-dir/crud-app.js.ejs b/packages/amplify-category-function/provider-utils/awscloudformation/function-template-dir/crud-app.js.ejs index 8a36a2b7b18..589b891ec4e 100644 --- a/packages/amplify-category-function/provider-utils/awscloudformation/function-template-dir/crud-app.js.ejs +++ b/packages/amplify-category-function/provider-utils/awscloudformation/function-template-dir/crud-app.js.ejs @@ -5,6 +5,9 @@ Licensed under the Apache License, Version 2.0 (the "License"). You may not use or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ + +<%= props.topLevelComment %> + const AWS = require('aws-sdk') var awsServerlessExpressMiddleware = require('aws-serverless-express/middleware') var bodyParser = require('body-parser') diff --git a/packages/amplify-category-function/provider-utils/awscloudformation/function-template-dir/index.js b/packages/amplify-category-function/provider-utils/awscloudformation/function-template-dir/index.js.ejs similarity index 89% rename from packages/amplify-category-function/provider-utils/awscloudformation/function-template-dir/index.js rename to packages/amplify-category-function/provider-utils/awscloudformation/function-template-dir/index.js.ejs index 5473f6cb09e..4054a5302e7 100644 --- a/packages/amplify-category-function/provider-utils/awscloudformation/function-template-dir/index.js +++ b/packages/amplify-category-function/provider-utils/awscloudformation/function-template-dir/index.js.ejs @@ -1,3 +1,5 @@ +<%= props.topLevelComment %> + exports.handler = function (event, context) { //eslint-disable-line console.log(`value1 = ${event.key1}`); console.log(`value2 = ${event.key2}`); diff --git a/packages/amplify-category-function/provider-utils/awscloudformation/function-template-dir/serverless-app.js.ejs b/packages/amplify-category-function/provider-utils/awscloudformation/function-template-dir/serverless-app.js.ejs index 2c117a3f34c..dadffa3e008 100644 --- a/packages/amplify-category-function/provider-utils/awscloudformation/function-template-dir/serverless-app.js.ejs +++ b/packages/amplify-category-function/provider-utils/awscloudformation/function-template-dir/serverless-app.js.ejs @@ -6,6 +6,9 @@ or in the "license" file accompanying this file. This file is distributed on an See the License for the specific language governing permissions and limitations under the License. */ + +<%= props.topLevelComment %> + var express = require('express') var bodyParser = require('body-parser') var awsServerlessExpressMiddleware = require('aws-serverless-express/middleware') diff --git a/packages/amplify-category-function/provider-utils/awscloudformation/index.js b/packages/amplify-category-function/provider-utils/awscloudformation/index.js index 2de3e83d300..40f00b85dfb 100644 --- a/packages/amplify-category-function/provider-utils/awscloudformation/index.js +++ b/packages/amplify-category-function/provider-utils/awscloudformation/index.js @@ -2,6 +2,8 @@ const fs = require('fs-extra'); const path = require('path'); const inquirer = require('inquirer'); +const categoryName = 'function'; + let serviceMetadata; async function serviceQuestions(context, defaultValuesFilename, serviceWalkthroughFilename) { @@ -28,7 +30,7 @@ function copyCfnTemplate(context, category, options, cfnFilename) { copyJobs.push(...[ { dir: pluginDir, - template: 'function-template-dir/index.js', + template: 'function-template-dir/index.js.ejs', target: `${targetDir}/${category}/${options.resourceName}/src/index.js`, }, { @@ -97,6 +99,16 @@ function copyCfnTemplate(context, category, options, cfnFilename) { return context.amplify.copyBatch(context, copyJobs, options); } +function createParametersFile(context, parameters, resourceName) { + const parametersFileName = 'function-parameters.json'; + const projectBackendDirPath = context.amplify.pathManager.getBackendDirPath(); + const resourceDirPath = path.join(projectBackendDirPath, categoryName, resourceName); + fs.ensureDirSync(resourceDirPath); + const parametersFilePath = path.join(resourceDirPath, parametersFileName); + const jsonString = JSON.stringify(parameters, null, 4); + fs.writeFileSync(parametersFilePath, jsonString, 'utf8'); +} + async function addResource(context, category, service, options) { let answers; serviceMetadata = context.amplify.readJsonFile(`${__dirname}/../supported-services.json`)[service]; @@ -111,13 +123,52 @@ async function addResource(context, category, service, options) { answers = result; } - copyCfnTemplate(context, category, answers, cfnFilename); context.amplify.updateamplifyMetaAfterResourceAdd( category, answers.resourceName, options, ); + copyCfnTemplate(context, category, answers, cfnFilename); + if (answers.parameters) { + createParametersFile(context, answers.parameters, answers.resourceName); + } + + await openEditor(context, category, answers); + + return answers.resourceName; +} + +async function updateResource(context, category, service) { + let answers; + serviceMetadata = context.amplify.readJsonFile(`${__dirname}/../supported-services.json`)[service]; + const { serviceWalkthroughFilename } = serviceMetadata; + + const serviceWalkthroughSrc = `${__dirname}/service-walkthroughs/${serviceWalkthroughFilename}`; + const { updateWalkthrough } = require(serviceWalkthroughSrc); + + const result = await updateWalkthrough(context); + + if (result.answers) { + ({ answers } = result); + } else { + answers = result; + } + + if (result.dependsOn) { + context.amplify.updateamplifyMetaAfterResourceUpdate( + category, + answers.resourceName, + 'dependsOn', + result.dependsOn, + ); + } + + if (answers.parameters) { + createParametersFile(context, answers.parameters, answers.resourceName); + } + + await openEditor(context, category, answers); return answers.resourceName; @@ -213,5 +264,21 @@ function migrateResource(context, projectPath, service, resourceName) { return migrate(context, projectPath, resourceName); } +function getPermissionPolicies(context, service, resourceName, crudOptions) { + serviceMetadata = context.amplify.readJsonFile(`${__dirname}/../supported-services.json`)[service]; + const { serviceWalkthroughFilename } = serviceMetadata; + const serviceWalkthroughSrc = `${__dirname}/service-walkthroughs/${serviceWalkthroughFilename}`; + const { getIAMPolicies } = require(serviceWalkthroughSrc); + + if (!getPermissionPolicies) { + context.print.info(`No policies found for ${resourceName}`); + return; + } + + return getIAMPolicies(resourceName, crudOptions); +} + -module.exports = { addResource, invoke, migrateResource }; +module.exports = { + addResource, updateResource, invoke, migrateResource, getPermissionPolicies, +}; diff --git a/packages/amplify-category-function/provider-utils/awscloudformation/service-walkthroughs/lambda-walkthrough.js b/packages/amplify-category-function/provider-utils/awscloudformation/service-walkthroughs/lambda-walkthrough.js index ed8046605c2..1b2f7dbc98d 100644 --- a/packages/amplify-category-function/provider-utils/awscloudformation/service-walkthroughs/lambda-walkthrough.js +++ b/packages/amplify-category-function/provider-utils/awscloudformation/service-walkthroughs/lambda-walkthrough.js @@ -2,7 +2,9 @@ const fs = require('fs-extra'); const inquirer = require('inquirer'); const path = require('path'); -const category = 'function'; +const categoryName = 'function'; +const serviceName = 'Lambda'; +const functionParametersFileName = 'function-parameters.json'; const parametersFileName = 'parameters.json'; @@ -12,7 +14,8 @@ async function serviceWalkthrough(context, defaultValuesFilename, serviceMetadat const defaultValuesSrc = `${__dirname}/../default-values/${defaultValuesFilename}`; const { getAllDefaults } = require(defaultValuesSrc); const allDefaultValues = getAllDefaults(amplify.getProjectDetails()); - const dependsOn = []; + let dependsOn = []; + const parameters = {}; // Ask resource and Lambda function name const resourceQuestions = [ @@ -81,9 +84,312 @@ async function serviceWalkthrough(context, defaultValuesFilename, serviceMetadat } allDefaultValues.dependsOn = dependsOn; } + + let topLevelComment; + if (await context.amplify.confirmPrompt.run('Do you want to access other resources created in this project from your Lambda function?')) { + ({ topLevelComment } = await askExecRolePermissionsQuestions( + context, + allDefaultValues, + parameters, + )); + } + allDefaultValues.parameters = parameters; + allDefaultValues.topLevelComment = topLevelComment; + ({ dependsOn } = allDefaultValues); return { answers: allDefaultValues, dependsOn }; } +async function updateWalkthrough(context) { + const { allResources } = await context.amplify.getResourceStatus(); + const resources = allResources + .filter(resource => resource.service === serviceName) + .map(resource => resource.resourceName); + + if (resources.length === 0) { + context.print.error('No Lambda Functions resource to update. Please use "amplify add function" command to create a new Function'); + process.exit(0); + return; + } + + const resourceQuestion = [{ + name: 'resourceName', + message: 'Please select the Lambda Function you would want to update', + type: 'list', + choices: resources, + }]; + + const newParams = {}; + const answers = {}; + const currentDefaults = {}; + let dependsOn; + + const resourceAnswer = await inquirer.prompt(resourceQuestion); + answers.resourceName = resourceAnswer.resourceName; + + const projectBackendDirPath = context.amplify.pathManager.getBackendDirPath(); + const resourceDirPath = path.join( + projectBackendDirPath, categoryName, + resourceAnswer.resourceName, + ); + const parametersFilePath = path.join(resourceDirPath, functionParametersFileName); + let currentParameters; + try { + currentParameters = JSON.parse(fs.readFileSync(parametersFilePath)); + } catch (e) { + currentParameters = {}; + } + if (currentParameters.permissions) { + currentDefaults.categories = Object.keys(currentParameters.permissions); + currentDefaults.categoryPermissionMap = currentParameters.permissions; + } + + if (await context.amplify.confirmPrompt.run('Do you want to update permissions granted to this Lambda function to perform on other resources in your project?')) { + const { topLevelComment } = await askExecRolePermissionsQuestions( + context, + answers, newParams, + currentDefaults, + ); + + const cfnFileName = `${resourceAnswer.resourceName}-cloudformation-template.json`; + const cfnFilePath = path.join(resourceDirPath, cfnFileName); + const cfnContent = JSON.parse(fs.readFileSync(cfnFilePath)); + const dependsOnParams = { env: { Type: 'String' } }; + + Object.keys(answers.resourcePropertiesJSON).forEach((resourceProperty) => { + dependsOnParams[resourceProperty] = { + Type: 'String', + Default: resourceProperty, + }; + }); + cfnContent.Parameters = dependsOnParams; + + Object.assign(answers.resourcePropertiesJSON, { ENV: { Ref: 'env' }, REGION: { Ref: 'AWS::Region' } }); + + if (!cfnContent.Resources.AmplifyResourcesPolicy) { + cfnContent.Resources.AmplifyResourcesPolicy = { + DependsOn: [ + 'LambdaExecutionRole', + ], + Type: 'AWS::IAM::Policy', + Properties: { + PolicyName: 'amplify-lambda-execution-policy', + Roles: [ + { + Ref: 'LambdaExecutionRole', + }, + ], + PolicyDocument: { + Version: '2012-10-17', + Statement: [], + }, + }, + }; + } + + if (answers.categoryPolicies.length === 0) { + delete cfnContent.Resources.AmplifyResourcesPolicy; + } else { + cfnContent.Resources.AmplifyResourcesPolicy.Properties.PolicyDocument.Statement = + answers.categoryPolicies; + } + cfnContent.Resources.LambdaFunction.Properties.Environment.Variables = + answers.resourcePropertiesJSON; + // Update top level comment in app.js or index.js file + + const updateTopLevelComment = (filePath) => { + const commentRegex = /\/\* Amplify Params - DO NOT EDIT[a-zA-Z0-9\-\s._=]+Amplify Params - DO NOT EDIT \*\//; + let fileContents = fs.readFileSync(filePath).toString(); + const commentMatches = fileContents.match(commentRegex); + if (!commentMatches || commentMatches.length === 0) { + fileContents = topLevelComment + fileContents; + } else { + fileContents = fileContents.replace(commentRegex, topLevelComment); + } + fs.writeFileSync(filePath, fileContents); + }; + const appJSFilePath = path.join(resourceDirPath, 'src', 'app.js'); + const indexJSFilePath = path.join(resourceDirPath, 'src', 'index.js'); + if (fs.existsSync(appJSFilePath)) { + updateTopLevelComment(appJSFilePath); + } else if (fs.existsSync(indexJSFilePath)) { + updateTopLevelComment(indexJSFilePath); + } + + fs.writeFileSync(cfnFilePath, JSON.stringify(cfnContent, null, 4)); + answers.parameters = newParams; + ({ dependsOn } = answers); + if (!dependsOn) { + dependsOn = []; + } + } + return { answers, dependsOn }; +} + +async function askExecRolePermissionsQuestions( + context, + allDefaultValues, + parameters, currentDefaults, +) { + const amplifyMetaFilePath = context.amplify.pathManager.getAmplifyMetaFilePath(); + const amplifyMeta = JSON.parse(fs.readFileSync(amplifyMetaFilePath)); + + let categories = Object.keys(amplifyMeta); + categories = categories.filter(category => category !== 'providers'); + + const categoryPermissionQuestion = { + type: 'checkbox', + name: 'categories', + message: 'Select the category', + choices: categories, + default: (currentDefaults ? currentDefaults.categories : undefined), + }; + const capitalizeFirstLetter = str => str.charAt(0).toUpperCase() + str.slice(1); + const categoryPermissionAnswer = await inquirer.prompt([categoryPermissionQuestion]); + const selectedCategories = categoryPermissionAnswer.categories; + let categoryPolicies = []; + let resources = []; + const crudOptions = ['create', 'read', 'update', 'delete']; + parameters.permissions = {}; + + const categoryPlugins = context.amplify.getCategoryPlugins(context); + for (let i = 0; i < selectedCategories.length; i += 1) { + const category = selectedCategories[i]; + + const resourcesList = Object.keys(amplifyMeta[category]); + + if (resourcesList.length === 0) { + context.print.warning(`No resources found for ${category}`); + continue; + } + + + try { + const { getPermissionPolicies } = require(categoryPlugins[category]); + if (!getPermissionPolicies) { + context.print.warning(`Policies cannot be added for ${category}`); + continue; + } else { + let selectedResources = []; + + if (resourcesList.length === 1) { + context.print.info(`${capitalizeFirstLetter(category)} category has a resource called ${resourcesList[0]}`); + selectedResources = [resourcesList[0]]; + } else { + const resourceQuestion = { + type: 'checkbox', + name: 'resources', + message: `${capitalizeFirstLetter(category)} has ${resourcesList.length} resources in this project. Select the one you would like your Lambda to access`, + choices: resourcesList, + validate: (value) => { + if (value.length === 0) { + return 'You must select at least resource'; + } + return true; + }, + default: () => { + if (currentDefaults && currentDefaults.categoryPermissionMap && + currentDefaults.categoryPermissionMap[category]) { + return Object.keys(currentDefaults.categoryPermissionMap[category]); + } + }, + }; + + const resourceAnswer = await inquirer.prompt([resourceQuestion]); + selectedResources = resourceAnswer.resources; + } + + for (let j = 0; j < selectedResources.length; j += 1) { + const resourceName = selectedResources[j]; + const crudPermissionQuestion = { + type: 'checkbox', + name: 'crudOptions', + message: `Select the operations you want to permit for ${resourceName}`, + choices: crudOptions, + validate: (value) => { + if (value.length === 0) { + return 'You must select at least one operation'; + } + + return true; + }, + default: () => { + if (currentDefaults && currentDefaults.categoryPermissionMap[category] + && currentDefaults.categoryPermissionMap[category][resourceName]) { + return currentDefaults.categoryPermissionMap[category][resourceName]; + } + }, + }; + + const crudPermissionAnswer = await inquirer.prompt([crudPermissionQuestion]); + if (!parameters.permissions[category]) { + parameters.permissions[category] = {}; + } + parameters.permissions[category][resourceName] = crudPermissionAnswer.crudOptions; + } + if (selectedResources.length > 0) { + const { permissionPolicies, resourceAttributes } = + await getPermissionPolicies(context, parameters.permissions[category]); + categoryPolicies = categoryPolicies.concat(permissionPolicies); + resources = resources.concat(resourceAttributes); + } + } + } catch (e) { + context.print.warning(`Policies cannot be added for ${category}`); + context.print.info(e.stack); + } + } + + allDefaultValues.categoryPolicies = categoryPolicies; + const resourceProperties = []; + const resourcePropertiesJSON = {}; + const categoryMapping = {}; + resources.forEach((resource) => { + const { category, resourceName, attributes } = resource; + attributes.forEach((attribute) => { + const envName = `${category.toUpperCase()}_${resourceName.toUpperCase()}_${attribute.toUpperCase()}`; + const varName = `${category}${capitalizeFirstLetter(resourceName)}${capitalizeFirstLetter(attribute)}`; + const refName = `${category}${resourceName}${attribute}`; + + resourceProperties.push(`"${envName}": {"Ref": "${refName}"}`); + resourcePropertiesJSON[`${envName}`] = { Ref: `${category}${resourceName}${attribute}` }; + if (!categoryMapping[category]) { + categoryMapping[category] = []; + } + categoryMapping[category].push({ envName, varName }); + }); + if (!allDefaultValues.dependsOn) { + allDefaultValues.dependsOn = []; + } + allDefaultValues.dependsOn.push({ + category: resource.category, + resourceName: resource.resourceName, + attributes: resource.attributes, + }); + }); + + allDefaultValues.resourceProperties = resourceProperties.join(','); + allDefaultValues.resourcePropertiesJSON = resourcePropertiesJSON; + + context.print.info(''); + let topLevelComment = '/* Amplify Params - DO NOT EDIT\n'; + let terminalOutput = 'You can access the following resource attributes as environment variables from your Lambda function\n'; + terminalOutput += 'var environment = process.env.ENV\n'; + terminalOutput += 'var region = process.env.REGION\n'; + + Object.keys(categoryMapping).forEach((category) => { + if (categoryMapping[category].length > 0) { + categoryMapping[category].forEach((args) => { + terminalOutput += `var ${args.varName} = process.env.${args.envName}\n`; + }); + } + }); + + context.print.info(terminalOutput); + topLevelComment += `${terminalOutput}\nAmplify Params - DO NOT EDIT */`; + + return { topLevelComment }; +} + async function getTableParameters(context, dynamoAnswers) { if (dynamoAnswers.Arn) { // Looking for table parameters on DynamoDB public API const hashKey = dynamoAnswers.KeySchema.find(attr => attr.KeyType === 'HASH') || {}; @@ -208,7 +514,7 @@ async function askDynamoDBQuestions(context, inputs) { } function migrate(context, projectPath, resourceName) { - const resourceDirPath = path.join(projectPath, 'amplify', 'backend', category, resourceName); + const resourceDirPath = path.join(projectPath, 'amplify', 'backend', categoryName, resourceName); const cfnFilePath = path.join(resourceDirPath, `${resourceName}-cloudformation-template.json`); const oldCfn = context.amplify.readJsonFile(cfnFilePath); const newCfn = {}; @@ -286,4 +592,57 @@ function migrate(context, projectPath, resourceName) { fs.writeFileSync(cfnFilePath, jsonString, 'utf8'); } -module.exports = { serviceWalkthrough, migrate }; +function getIAMPolicies(resourceName, crudOptions) { + let policy = {}; + const actions = []; + + crudOptions.forEach((crudOption) => { + switch (crudOption) { + case 'create': actions.push( + 'lambda:Create*', + 'lambda:Put*', + 'lambda:Add*', + ); + break; + case 'update': actions.push('lambda:Update*'); + break; + case 'read': actions.push('lambda:Get*', 'lambda:List*', 'lambda:Invoke*'); + break; + case 'delete': actions.push('lambda:Delete*', 'lambda:Remove*'); + break; + default: console.log(`${crudOption} not supported`); + } + }); + + policy = { + Effect: 'Allow', + Action: actions, + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:aws:lambda:', + { + Ref: 'AWS::Region', + }, + ':', + { Ref: 'AWS::AccountId' }, + ':function', + { + Ref: `${categoryName}${resourceName}Name`, + }, + ], + ], + }, + ], + }; + + const attributes = ['Name']; + + return { policy, attributes }; +} + +module.exports = { + serviceWalkthrough, updateWalkthrough, migrate, getIAMPolicies, +}; diff --git a/packages/amplify-category-hosting/index.js b/packages/amplify-category-hosting/index.js index a5fed33e689..9db29cf8a8d 100644 --- a/packages/amplify-category-hosting/index.js +++ b/packages/amplify-category-hosting/index.js @@ -2,6 +2,8 @@ const inquirer = require('inquirer'); const sequential = require('promise-sequential'); const categoryManager = require('./lib/category-manager'); +const category = 'hosting'; + async function add(context) { const { availableServices, @@ -108,10 +110,26 @@ async function migrate(context) { await categoryManager.migrate(context); } +async function getPermissionPolicies(context, resourceOpsMapping) { + const permissionPolicies = []; + const resourceAttributes = []; + + Object.keys(resourceOpsMapping).forEach((resourceName) => { + const { policy, attributes } = categoryManager.getIAMPolicies( + resourceName, + resourceOpsMapping[resourceName], + ); + permissionPolicies.push(policy); + resourceAttributes.push({ resourceName, attributes, category }); + }); + return { permissionPolicies, resourceAttributes }; +} + module.exports = { add, configure, publish, console, migrate, + getPermissionPolicies, }; diff --git a/packages/amplify-category-hosting/lib/category-manager.js b/packages/amplify-category-hosting/lib/category-manager.js index 46c974c70b8..dc2f8d4907c 100644 --- a/packages/amplify-category-hosting/lib/category-manager.js +++ b/packages/amplify-category-hosting/lib/category-manager.js @@ -4,6 +4,8 @@ const sequential = require('promise-sequential'); const constants = require('./constants'); const supportedServices = require('./supported-services'); +const category = 'hosting'; + function getAvailableServices(context) { const availableServices = []; const projectConfig = context.amplify.getProjectConfig(); @@ -74,8 +76,54 @@ async function migrate(context) { await sequential(migrationTasks); } + +function getIAMPolicies(resourceName, crudOptions) { + let policy = {}; + let actions = new Set(); + + crudOptions.forEach((crudOption) => { + switch (crudOption) { + case 'create': actions.add('s3:PutObject'); + break; + case 'update': actions.add('s3:PutObject'); + break; + case 'read': actions.add('s3:GetObject'); actions.add('s3:ListBucket'); + break; + case 'delete': actions.add('s3:DeleteObject'); + break; + default: console.log(`${crudOption} not supported`); + } + }); + + actions = Array.from(actions); + policy = { + Effect: 'Allow', + Action: actions, + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:aws:s3:::', + { + Ref: `${category}${resourceName}HostingBucketName`, + }, + '/*', + ], + ], + }, + ], + }; + + const attributes = ['HostingBucketName']; + + return { policy, attributes }; +} + + module.exports = { getCategoryStatus, runServiceAction, migrate, + getIAMPolicies, }; diff --git a/packages/amplify-category-interactions/index.js b/packages/amplify-category-interactions/index.js index aae2e08de69..3c5c3dba5cd 100644 --- a/packages/amplify-category-interactions/index.js +++ b/packages/amplify-category-interactions/index.js @@ -29,6 +29,37 @@ async function migrate(context) { await Promise.all(migrateResourcePromises); } +async function getPermissionPolicies(context, resourceOpsMapping) { + const amplifyMetaFilePath = context.amplify.pathManager.getAmplifyMetaFilePath(); + const amplifyMeta = context.amplify.readJsonFile(amplifyMetaFilePath); + const permissionPolicies = []; + const resourceAttributes = []; + + Object.keys(resourceOpsMapping).forEach((resourceName) => { + try { + const providerController = require(`./provider-utils/${amplifyMeta[category][resourceName].providerPlugin}/index`); + if (providerController) { + const { policy, attributes } = providerController.getPermissionPolicies( + context, + amplifyMeta[category][resourceName].service, + resourceName, + resourceOpsMapping[resourceName], + ); + permissionPolicies.push(policy); + resourceAttributes.push({ resourceName, attributes, category }); + } else { + context.print.error(`Provider not configured for ${category}: ${resourceName}`); + } + } catch (e) { + context.print.warning(`Could not get policies for ${category}: ${resourceName}`); + throw e; + } + }); + return { permissionPolicies, resourceAttributes }; +} + + module.exports = { migrate, + getPermissionPolicies, }; diff --git a/packages/amplify-category-interactions/provider-utils/awscloudformation/index.js b/packages/amplify-category-interactions/provider-utils/awscloudformation/index.js index 500f9b25143..3e4dd5636c8 100644 --- a/packages/amplify-category-interactions/provider-utils/awscloudformation/index.js +++ b/packages/amplify-category-interactions/provider-utils/awscloudformation/index.js @@ -133,5 +133,21 @@ async function migrateResource(context, projectPath, service, resourceName) { return await migrate(context, projectPath, resourceName); } +function getPermissionPolicies(context, service, resourceName, crudOptions) { + serviceMetadata = context.amplify.readJsonFile(`${__dirname}/../supported-services.json`)[service]; + const { serviceWalkthroughFilename } = serviceMetadata; + const serviceWalkthroughSrc = `${__dirname}/service-walkthroughs/${serviceWalkthroughFilename}`; + const { getIAMPolicies } = require(serviceWalkthroughSrc); + + if (!getPermissionPolicies) { + context.print.info(`No policies found for ${resourceName}`); + return; + } + + return getIAMPolicies(resourceName, crudOptions); +} + -module.exports = { addResource, updateResource, migrateResource }; +module.exports = { + addResource, updateResource, migrateResource, getPermissionPolicies, +}; diff --git a/packages/amplify-category-interactions/provider-utils/awscloudformation/service-walkthroughs/lex-walkthrough.js b/packages/amplify-category-interactions/provider-utils/awscloudformation/service-walkthroughs/lex-walkthrough.js index 1c7d6c19b5a..22a22b3edc0 100644 --- a/packages/amplify-category-interactions/provider-utils/awscloudformation/service-walkthroughs/lex-walkthrough.js +++ b/packages/amplify-category-interactions/provider-utils/awscloudformation/service-walkthroughs/lex-walkthrough.js @@ -761,4 +761,55 @@ async function migrate(context, projectPath, resourceName) { fs.writeFileSync(cfnParametersFilePath, jsonString, 'utf8'); } -module.exports = { addWalkthrough, updateWalkthrough, migrate }; +function getIAMPolicies(resourceName, crudOptions) { + let policy = {}; + const actions = []; + + crudOptions.forEach((crudOption) => { + switch (crudOption) { + case 'create': actions.push( + 'lex:Create*', + 'lex:Post*', + ); + break; + case 'update': actions.push('lex:Put*'); + break; + case 'read': actions.push('lex:Get*'); + break; + case 'delete': actions.push('lex:Delete*'); + break; + default: console.log(`${crudOption} not supported`); + } + }); + + policy = { + Effect: 'Allow', + Action: actions, + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:aws:lex:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':bot:', + { + Ref: `${category}${resourceName}BotName`, + }, + ':*', + ], + ], + }, + ], + }; + + const attributes = ['BotName']; + + return { policy, attributes }; +} + +module.exports = { + addWalkthrough, updateWalkthrough, migrate, getIAMPolicies, +}; diff --git a/packages/amplify-category-storage/index.js b/packages/amplify-category-storage/index.js index fb251d7aa08..47865b7e72c 100644 --- a/packages/amplify-category-storage/index.js +++ b/packages/amplify-category-storage/index.js @@ -47,8 +47,38 @@ async function migrate(context) { await Promise.all(migrateResourcePromises); } +async function getPermissionPolicies(context, resourceOpsMapping) { + const amplifyMetaFilePath = context.amplify.pathManager.getAmplifyMetaFilePath(); + const amplifyMeta = context.amplify.readJsonFile(amplifyMetaFilePath); + const permissionPolicies = []; + const resourceAttributes = []; + + Object.keys(resourceOpsMapping).forEach((resourceName) => { + try { + const providerController = require(`./provider-utils/${amplifyMeta[category][resourceName].providerPlugin}/index`); + if (providerController) { + const { policy, attributes } = providerController.getPermissionPolicies( + context, + amplifyMeta[category][resourceName].service, + resourceName, + resourceOpsMapping[resourceName], + ); + permissionPolicies.push(policy); + resourceAttributes.push({ resourceName, attributes, category }); + } else { + context.print.error(`Provider not configured for ${category}: ${resourceName}`); + } + } catch (e) { + context.print.warning(`Could not get policies for ${category}: ${resourceName}`); + throw e; + } + }); + return { permissionPolicies, resourceAttributes }; +} + module.exports = { add, console, migrate, + getPermissionPolicies, }; diff --git a/packages/amplify-category-storage/provider-utils/awscloudformation/index.js b/packages/amplify-category-storage/provider-utils/awscloudformation/index.js index c139cfb58e0..2878459dfd9 100644 --- a/packages/amplify-category-storage/provider-utils/awscloudformation/index.js +++ b/packages/amplify-category-storage/provider-utils/awscloudformation/index.js @@ -1,4 +1,3 @@ - function addResource(context, category, service, options) { const serviceMetadata = context.amplify.readJsonFile(`${__dirname}/../supported-services.json`)[service]; const { defaultValuesFilename, serviceWalkthroughFilename } = serviceMetadata; @@ -44,5 +43,21 @@ function migrateResource(context, projectPath, service, resourceName) { return migrate(context, projectPath, resourceName); } +function getPermissionPolicies(context, service, resourceName, crudOptions) { + const serviceMetadata = context.amplify.readJsonFile(`${__dirname}/../supported-services.json`)[service]; + const { serviceWalkthroughFilename } = serviceMetadata; + const serviceWalkthroughSrc = `${__dirname}/service-walkthroughs/${serviceWalkthroughFilename}`; + const { getIAMPolicies } = require(serviceWalkthroughSrc); + + if (!getPermissionPolicies) { + context.print.info(`No policies found for ${resourceName}`); + return; + } + + return getIAMPolicies(resourceName, crudOptions); +} + -module.exports = { addResource, updateResource, migrateResource }; +module.exports = { + addResource, updateResource, migrateResource, getPermissionPolicies, +}; diff --git a/packages/amplify-category-storage/provider-utils/awscloudformation/service-walkthroughs/dynamoDb-walkthrough.js b/packages/amplify-category-storage/provider-utils/awscloudformation/service-walkthroughs/dynamoDb-walkthrough.js index 0cb22058b76..e122d3ea160 100644 --- a/packages/amplify-category-storage/provider-utils/awscloudformation/service-walkthroughs/dynamoDb-walkthrough.js +++ b/packages/amplify-category-storage/provider-utils/awscloudformation/service-walkthroughs/dynamoDb-walkthrough.js @@ -460,4 +460,34 @@ function migrate(context, projectPath, resourceName) { fs.writeFileSync(cfnFilePath, jsonString, 'utf8'); } -module.exports = { addWalkthrough, updateWalkthrough, migrate }; +function getIAMPolicies(resourceName, crudOptions) { + let policy = {}; + const actions = []; + + crudOptions.forEach((crudOption) => { + switch (crudOption) { + case 'create': actions.push('dynamodb:Put*', 'dynamodb:Create*', 'dynamodb:BatchWriteItem'); + break; + case 'update': actions.push('dynamodb:Update*', 'dynamodb:RestoreTable*'); + break; + case 'read': actions.push('dynamodb:Get*', 'dynamodb:BatchGetItem', 'dynamodb:List*', 'dynamodb:Describe*', 'dynamodb:Scan', 'dynamodb:Query'); + break; + case 'delete': actions.push('dynamodb:Delete*'); + break; + default: console.log(`${crudOption} not supported`); + } + }); + + policy = { + Effect: 'Allow', + Action: actions, + Resource: [{ Ref: `${category}${resourceName}Arn` }], + }; + const attributes = ['Name', 'Arn']; + + return { policy, attributes }; +} + +module.exports = { + addWalkthrough, updateWalkthrough, migrate, getIAMPolicies, +}; diff --git a/packages/amplify-category-storage/provider-utils/awscloudformation/service-walkthroughs/s3-walkthrough.js b/packages/amplify-category-storage/provider-utils/awscloudformation/service-walkthroughs/s3-walkthrough.js index 77e1a470623..2d393de3aad 100644 --- a/packages/amplify-category-storage/provider-utils/awscloudformation/service-walkthroughs/s3-walkthrough.js +++ b/packages/amplify-category-storage/provider-utils/awscloudformation/service-walkthroughs/s3-walkthrough.js @@ -384,5 +384,50 @@ function convertToCRUD(parameters, answers) { } } +function getIAMPolicies(resourceName, crudOptions) { + let policy = {}; + let actions = new Set(); -module.exports = { addWalkthrough, updateWalkthrough, migrate }; + crudOptions.forEach((crudOption) => { + switch (crudOption) { + case 'create': actions.add('s3:PutObject'); + break; + case 'update': actions.add('s3:PutObject'); + break; + case 'read': actions.add('s3:GetObject'); actions.add('s3:ListBucket'); + break; + case 'delete': actions.add('s3:DeleteObject'); + break; + default: console.log(`${crudOption} not supported`); + } + }); + + actions = Array.from(actions); + policy = { + Effect: 'Allow', + Action: actions, + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:aws:s3:::', + { + Ref: `${category}${resourceName}BucketName`, + }, + '/*', + ], + ], + }, + ], + }; + + const attributes = ['BucketName']; + + return { policy, attributes }; +} + + +module.exports = { + addWalkthrough, updateWalkthrough, migrate, getIAMPolicies, +}; diff --git a/packages/amplify-category-xr/index.js b/packages/amplify-category-xr/index.js index 68c8daaeff2..132d83bc74a 100644 --- a/packages/amplify-category-xr/index.js +++ b/packages/amplify-category-xr/index.js @@ -70,7 +70,26 @@ async function initEnv(context) { } } +async function getPermissionPolicies(context, resourceOpsMapping) { + const permissionPolicies = []; + const resourceAttributes = []; + + Object.keys(resourceOpsMapping).forEach((resourceName) => { + const { policy, attributes } = xrManager.getIAMPolicies( + context, + resourceName, + resourceOpsMapping[resourceName], + ); + permissionPolicies.push(policy); + resourceAttributes.push({ resourceName, attributes, category: XR_CATEGORY_NAME }); + }); + + return { permissionPolicies, resourceAttributes }; +} + + module.exports = { console, initEnv, + getPermissionPolicies, }; diff --git a/packages/amplify-category-xr/lib/xr-manager.js b/packages/amplify-category-xr/lib/xr-manager.js index fe67e981ccc..c3fa32bdc65 100644 --- a/packages/amplify-category-xr/lib/xr-manager.js +++ b/packages/amplify-category-xr/lib/xr-manager.js @@ -258,6 +258,52 @@ function isSceneNameValid(sceneName) { /^[a-zA-Z0-9]+$/i.test(sceneName); } +function getIAMPolicies(context, resourceName, crudOptions) { + let policy = {}; + let actions = new Set(); + + crudOptions.forEach((crudOption) => { + switch (crudOption) { + case 'create': actions.add('sumerian:Login'); + break; + case 'read': actions.add('sumerian:ViewRelease'); + break; + default: context.print.warning(`${crudOption} operation is not supported for ${resourceName}`); + } + }); + + actions = Array.from(actions); + policy = { + Effect: 'Allow', + Action: actions, + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:aws:sumerian:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':project:', + { + Ref: `${constants.CategoryName}${resourceName}projectName`, + }, + ], + ], + }, + ], + }; + + const attributes = ['projectName', 'sceneId']; + + return { policy, attributes }; +} + module.exports = { isXRSetup, ensureSetup, @@ -267,4 +313,5 @@ module.exports = { addSceneConfig, remove, console, + getIAMPolicies, }; diff --git a/packages/amplify-cli/src/extensions/amplify-helpers/update-amplify-meta.js b/packages/amplify-cli/src/extensions/amplify-helpers/update-amplify-meta.js index 24fe0b8db27..f6a26dd422a 100644 --- a/packages/amplify-cli/src/extensions/amplify-helpers/update-amplify-meta.js +++ b/packages/amplify-cli/src/extensions/amplify-helpers/update-amplify-meta.js @@ -73,6 +73,10 @@ function moveBackendResourcesToCurrentCloudBackend(resources) { function updateamplifyMetaAfterResourceAdd(category, resourceName, options) { const amplifyMetaFilePath = pathManager.getAmplifyMetaFilePath(); const amplifyMeta = readJsonFile(amplifyMetaFilePath); + if (options.dependsOn) { + checkForCyclicDependencies(category, resourceName, options.dependsOn); + } + if (!amplifyMeta[category]) { amplifyMeta[category] = {}; } @@ -110,7 +114,9 @@ function updateamplifyMetaAfterResourceUpdate(category, resourceName, attribute, const amplifyMetaFilePath = pathManager.getAmplifyMetaFilePath(); // let amplifyCloudMetaFilePath = pathManager.getCurentAmplifyMetaFilePath(); const currentTimestamp = new Date(); - + if (attribute === 'dependsOn') { + checkForCyclicDependencies(category, resourceName, value); + } updateAwsMetaFile( amplifyMetaFilePath, category, @@ -206,6 +212,34 @@ function updateamplifyMetaAfterResourceDelete(category, resourceName) { filesystem.remove(resourceDir); } +function checkForCyclicDependencies(category, resourceName, dependsOn) { + const amplifyMetaFilePath = pathManager.getAmplifyMetaFilePath(); + const amplifyMeta = readJsonFile(amplifyMetaFilePath); + let cyclicDependency = false; + + if (dependsOn) { + dependsOn.forEach((resource) => { + if (resource.category === category && resource.resourceName === resourceName) { + cyclicDependency = true; + } + const dependsOnResourceDependency = + amplifyMeta[resource.category][resource.resourceName].dependsOn; + if (dependsOnResourceDependency) { + dependsOnResourceDependency.forEach((dependsOnResource) => { + if (dependsOnResource.category === category && + dependsOnResource.resourceName === resourceName) { + cyclicDependency = true; + } + }); + } + }); + } + + if (cyclicDependency === true) { + throw new Error(`Cannot add ${resourceName} due to a cyclic dependency`); + } +} + module.exports = { updateamplifyMetaAfterResourceAdd,