UNPKG

@aws-amplify/amplify-category-api

Version:
581 lines 28.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.openConsole = exports.getIAMPolicies = exports.migrate = exports.updateWalkthrough = exports.serviceWalkthrough = void 0; const os_1 = __importDefault(require("os")); const amplify_cli_core_1 = require("@aws-amplify/amplify-cli-core"); const amplify_prompts_1 = require("@aws-amplify/amplify-prompts"); const inquirer_1 = __importDefault(require("inquirer")); const lodash_1 = __importDefault(require("lodash")); const uuid_1 = require("uuid"); const category_constants_1 = require("../../../category-constants"); const apigw_input_state_1 = require("../apigw-input-state"); const cdk_stack_builder_1 = require("../cdk-stack-builder"); const apigw_defaults_1 = require("../default-values/apigw-defaults"); const rest_api_path_utils_1 = require("../utils/rest-api-path-utils"); const category = amplify_cli_core_1.AmplifyCategories.API; const serviceName = amplify_cli_core_1.AmplifySupportedService.APIGW; const elasticContainerServiceName = 'ElasticContainer'; async function serviceWalkthrough(context) { const allDefaultValues = (0, apigw_defaults_1.getAllDefaults)(context.amplify.getProjectDetails()); const resourceName = await askApiName(context, allDefaultValues.resourceName); const answers = { paths: {}, resourceName, dependsOn: undefined }; return pathFlow(context, answers); } exports.serviceWalkthrough = serviceWalkthrough; async function updateWalkthrough(context) { const { allResources } = await context.amplify.getResourceStatus(); const allDefaultValues = (0, apigw_defaults_1.getAllDefaults)(context.amplify.getProjectDetails()); const resources = allResources .filter((resource) => resource.service === serviceName && resource.mobileHubMigrated !== true) .map((resource) => resource.resourceName); if (resources.length === 0) { const errMessage = 'No REST API resource to update. Use "amplify add api" command to create a new REST API'; amplify_prompts_1.printer.error(errMessage); await context.usageData.emitError(new amplify_cli_core_1.ResourceDoesNotExistError(errMessage)); (0, amplify_cli_core_1.exitOnNextTick)(0); return; } let answers = { paths: [], }; const selectedApiName = await amplify_prompts_1.prompter.pick('Select the REST API you want to update:', resources); let updateApiOperation = await amplify_prompts_1.prompter.pick('What would you like to do?', [ { name: 'Add another path', value: 'add' }, { name: 'Update path', value: 'update' }, { name: 'Remove path', value: 'remove' }, ]); if (context.input.command === 'add') { updateApiOperation = 'add'; } if (selectedApiName === category_constants_1.ADMIN_QUERIES_NAME) { const errMessage = `The Admin Queries API is maintained through the Auth category and should be updated using 'amplify update auth' command`; amplify_prompts_1.printer.warn(errMessage); await context.usageData.emitError(new amplify_cli_core_1.ResourceDoesNotExistError(errMessage)); (0, amplify_cli_core_1.exitOnNextTick)(0); } const projRoot = amplify_cli_core_1.pathManager.findProjectRoot(); if (!amplify_cli_core_1.stateManager.resourceInputsJsonExists(projRoot, category, selectedApiName)) { await migrate(context, projRoot, selectedApiName); if (!amplify_cli_core_1.stateManager.resourceInputsJsonExists(projRoot, category, selectedApiName)) { (0, amplify_cli_core_1.exitOnNextTick)(0); } } const parameters = amplify_cli_core_1.stateManager.getResourceInputsJson(projRoot, category, selectedApiName); parameters.resourceName = selectedApiName; Object.assign(allDefaultValues, parameters); answers = { ...answers, ...parameters }; [answers.uuid] = (0, uuid_1.v4)().split('-'); const pathNames = Object.keys(answers.paths); let updatedResult = {}; switch (updateApiOperation) { case 'add': { updatedResult = pathFlow(context, answers); break; } case 'remove': { const pathToRemove = await inquirer_1.default.prompt({ name: 'path', message: 'Select the path to remove', type: 'list', choices: pathNames, }); delete answers.paths[pathToRemove.path]; const { dependsOn, functionArns } = await findDependsOn(answers.paths); answers.dependsOn = dependsOn; answers.functionArns = functionArns; updatedResult = { answers }; break; } case 'update': { const pathToEdit = await inquirer_1.default.prompt({ name: 'pathName', message: 'Select the path to edit', type: 'list', choices: pathNames, }); const currentPath = answers.paths[pathToEdit.pathName]; delete answers.paths[pathToEdit.pathName]; updatedResult = pathFlow(context, answers, currentPath); break; } default: { throw new Error(`Unrecognized API update operation "${updateApiOperation}"`); } } return updatedResult; } exports.updateWalkthrough = updateWalkthrough; async function pathFlow(context, answers, currentPath) { const pathsAnswer = await askPaths(context, answers, currentPath); return { answers: pathsAnswer }; } async function askApiName(context, defaultResourceName) { const apiNameValidator = (input) => { const amplifyValidatorOutput = context.amplify.inputValidation({ validation: { operator: 'regex', value: '^[a-zA-Z0-9]+$', onErrorMsg: 'Resource name should be alphanumeric', }, required: true, })(input); const adminQueriesName = 'AdminQueries'; if (input === adminQueriesName) { return `${adminQueriesName} is a reserved name for REST API resources for use by the auth category. Run "amplify update auth" to create an Admin Queries API.`; } let uniqueCheck = false; try { uniqueCheck = (0, amplify_cli_core_1.isResourceNameUnique)(category, input); } catch (e) { return e.message || e; } return typeof amplifyValidatorOutput === 'string' ? amplifyValidatorOutput : uniqueCheck; }; const resourceName = await amplify_prompts_1.prompter.input('Provide a friendly name for your resource to be used as a label for this category in the project:', { initial: defaultResourceName, validate: apiNameValidator }); return resourceName; } async function askPermissions(context, answers, currentPath) { var _a, _b, _c, _d, _e; while (true) { const apiAccess = await amplify_prompts_1.prompter.yesOrNo('Restrict API access?', ((_a = currentPath === null || currentPath === void 0 ? void 0 : currentPath.permissions) === null || _a === void 0 ? void 0 : _a.setting) !== cdk_stack_builder_1.PermissionSetting.OPEN); if (!apiAccess) { return { setting: cdk_stack_builder_1.PermissionSetting.OPEN }; } const userPoolGroupList = context.amplify.getUserPoolGroupList(); let permissionSelected = 'Auth/Guest Users'; const permissions = {}; if (userPoolGroupList.length > 0) { do { if (permissionSelected === 'Learn more') { amplify_prompts_1.printer.blankLine(); amplify_prompts_1.printer.info('You can restrict access using CRUD policies for Authenticated Users, Guest Users, or on individual Group that users belong to' + ' in a User Pool. If a user logs into your application and is not a member of any group they will use policy set for ' + '“Authenticated Users”, however if they belong to a group they will only get the policy associated with that specific group.'); amplify_prompts_1.printer.blankLine(); } const permissionSelection = await amplify_prompts_1.prompter.pick('Restrict access by:', [ 'Auth/Guest Users', 'Individual Groups', 'Both', 'Learn more', ]); permissionSelected = permissionSelection; } while (permissionSelected === 'Learn more'); } if (permissionSelected === 'Both' || permissionSelected === 'Auth/Guest Users') { const permissionSetting = await amplify_prompts_1.prompter.pick('Who should have access?', [ { name: 'Authenticated users only', value: cdk_stack_builder_1.PermissionSetting.PRIVATE, }, { name: 'Authenticated and Guest users', value: cdk_stack_builder_1.PermissionSetting.PROTECTED, }, ], { initial: ((_b = currentPath === null || currentPath === void 0 ? void 0 : currentPath.permissions) === null || _b === void 0 ? void 0 : _b.setting) === cdk_stack_builder_1.PermissionSetting.PROTECTED ? 1 : 0 }); permissions.setting = permissionSetting; let { permissions: { auth: authPermissions }, } = currentPath || { permissions: { auth: [] } }; let { permissions: { guest: unauthPermissions }, } = currentPath || { permissions: { guest: [] } }; if (permissionSetting === cdk_stack_builder_1.PermissionSetting.PRIVATE) { permissions.auth = await askCRUD('Authenticated', authPermissions); const apiRequirements = { authSelections: 'identityPoolAndUserPool' }; await ensureAuth(context, apiRequirements, answers.resourceName); } if (permissionSetting === cdk_stack_builder_1.PermissionSetting.PROTECTED) { permissions.auth = await askCRUD('Authenticated', authPermissions); permissions.guest = await askCRUD('Guest', unauthPermissions); const apiRequirements = { authSelections: 'identityPoolAndUserPool', allowUnauthenticatedIdentities: true }; await ensureAuth(context, apiRequirements, answers.resourceName); } } if (permissionSelected === 'Both' || permissionSelected === 'Individual Groups') { const apiRequirements = { authSelections: 'identityPoolAndUserPool' }; await ensureAuth(context, apiRequirements, answers.resourceName); const authResourceName = getAuthResourceName(); answers.authResourceName = authResourceName; let defaultSelectedGroups = []; if ((_c = currentPath === null || currentPath === void 0 ? void 0 : currentPath.permissions) === null || _c === void 0 ? void 0 : _c.groups) { defaultSelectedGroups = Object.keys(currentPath.permissions.groups); } let selectedUserPoolGroupList = await amplify_prompts_1.prompter.pick('Select groups:', userPoolGroupList, { initial: (0, amplify_prompts_1.byValues)(defaultSelectedGroups)(userPoolGroupList), returnSize: 'many', pickAtLeast: 1, }); if (selectedUserPoolGroupList && !Array.isArray(selectedUserPoolGroupList)) { selectedUserPoolGroupList = [selectedUserPoolGroupList]; } for (const selectedUserPoolGroup of selectedUserPoolGroupList) { let defaults = []; if ((_e = (_d = currentPath === null || currentPath === void 0 ? void 0 : currentPath.permissions) === null || _d === void 0 ? void 0 : _d.groups) === null || _e === void 0 ? void 0 : _e[selectedUserPoolGroup]) { defaults = currentPath.permissions.groups[selectedUserPoolGroup]; } if (!permissions.groups) { permissions.groups = {}; } permissions.groups[selectedUserPoolGroup] = await askCRUD(selectedUserPoolGroup, defaults); } if (!permissions.setting) { permissions.setting = cdk_stack_builder_1.PermissionSetting.PRIVATE; } } return permissions; } } async function ensureAuth(context, apiRequirements, resourceName) { const checkResult = await context.amplify.invokePluginMethod(context, 'auth', undefined, 'checkRequirements', [ apiRequirements, context, 'api', resourceName, ]); if (checkResult.authImported === true && checkResult.errors && checkResult.errors.length > 0) { throw new Error(checkResult.errors.join(os_1.default.EOL)); } if (checkResult.errors && checkResult.errors.length > 0) { amplify_prompts_1.printer.warn(checkResult.errors.join(os_1.default.EOL)); } if (!checkResult.authEnabled || !checkResult.requirementsMet) { try { await context.amplify.invokePluginMethod(context, 'auth', undefined, 'externalAuthEnable', [ context, amplify_cli_core_1.AmplifyCategories.API, resourceName, apiRequirements, ]); } catch (error) { amplify_prompts_1.printer.error(error); throw error; } } } async function askCRUD(userType, permissions = []) { const crudOptions = [cdk_stack_builder_1.CrudOperation.CREATE, cdk_stack_builder_1.CrudOperation.READ, cdk_stack_builder_1.CrudOperation.UPDATE, cdk_stack_builder_1.CrudOperation.DELETE]; const crudAnswers = await amplify_prompts_1.prompter.pick(`What permissions do you want to grant to ${userType} users?`, crudOptions, { returnSize: 'many', initial: (0, amplify_prompts_1.byValues)(permissions), pickAtLeast: 1, }); return crudAnswers; } async function askPaths(context, answers, currentPath) { const existingFunctions = functionsExist(); let defaultFunctionType = 'newFunction'; const defaultChoice = { name: 'Create a new Lambda function', value: defaultFunctionType, }; const choices = [defaultChoice]; if (existingFunctions) { choices.push({ name: 'Use a Lambda function already added in the current Amplify project', value: 'projectFunction', }); } const paths = answers.paths; let addAnotherPath; do { let pathName; let isPathValid; do { pathName = await amplify_prompts_1.prompter.input('Provide a path (e.g., /book/{isbn}):', { initial: currentPath ? currentPath.name : '/items', validate: rest_api_path_utils_1.validatePathName, }); const overlapCheckResult = (0, rest_api_path_utils_1.checkForPathOverlap)(pathName, Object.keys(paths)); if (overlapCheckResult === false) { isPathValid = true; } else { const higherOrderPath = overlapCheckResult.higherOrderPath; const lowerOrderPath = overlapCheckResult.lowerOrderPath; isPathValid = await amplify_prompts_1.prompter.confirmContinue(`The path ${lowerOrderPath} overlaps with ${higherOrderPath}. Users authorized to access ${higherOrderPath} will also have access` + ` to ${lowerOrderPath}. Are you sure you want to continue?`); } } while (!isPathValid); const functionType = await amplify_prompts_1.prompter.pick('Choose a Lambda source', choices, { initial: choices.indexOf(defaultChoice) }); let path = { name: pathName }; let lambda; do { lambda = await askLambdaSource(context, functionType, pathName, currentPath); } while (!lambda); const permissions = await askPermissions(context, answers, currentPath); path = { ...path, ...lambda, permissions }; paths[pathName] = path; if (currentPath) { break; } addAnotherPath = await amplify_prompts_1.prompter.confirmContinue('Do you want to add another path?'); } while (addAnotherPath); const { dependsOn, functionArns } = await findDependsOn(paths); return { paths, dependsOn, resourceName: answers.resourceName, functionArns }; } async function findDependsOn(paths) { var _a; const dependsOn = []; const functionArns = []; for (const path of Object.values(paths)) { if (path.lambdaFunction && !path.lambdaArn) { if (!dependsOn.find((func) => func.resourceName === path.lambdaFunction)) { dependsOn.push({ category: 'function', resourceName: path.lambdaFunction, attributes: ['Name', 'Arn'], }); } } if (!functionArns.find((func) => func.lambdaFunction === path.lambdaFunction)) { functionArns.push({ lambdaFunction: path.lambdaFunction, lambdaArn: path.lambdaArn, }); } if ((_a = path === null || path === void 0 ? void 0 : path.permissions) === null || _a === void 0 ? void 0 : _a.groups) { const userPoolGroups = Object.keys(path.permissions.groups); if (userPoolGroups.length > 0) { const authResourceName = getAuthResourceName(); if (!dependsOn.find((resource) => resource.resourceName === authResourceName)) { dependsOn.push({ category: 'auth', resourceName: authResourceName, attributes: ['UserPoolId'], }); } userPoolGroups.forEach((group) => { if (!dependsOn.find((resource) => resource.attributes[0] === `${group}GroupRole`)) { dependsOn.push({ category: 'auth', resourceName: 'userPoolGroups', attributes: [`${group}GroupRole`], }); } }); } } } return { dependsOn, functionArns }; } function getAuthResourceName() { const meta = amplify_cli_core_1.stateManager.getMeta(); const authResources = (Object.entries(meta === null || meta === void 0 ? void 0 : meta.auth) || []).filter(([_, resource]) => resource.service === amplify_cli_core_1.AmplifySupportedService.COGNITO); if (authResources.length === 0) { throw new Error('No auth resource found. Add it using amplify add auth'); } const [authResourceName] = authResources[0]; return authResourceName; } function functionsExist() { const meta = amplify_cli_core_1.stateManager.getMeta(); if (!meta.function) { return false; } const functionResources = meta.function; const lambdaFunctions = []; Object.keys(functionResources).forEach((resourceName) => { if (functionResources[resourceName].service === amplify_cli_core_1.AmplifySupportedService.LAMBDA) { lambdaFunctions.push(resourceName); } }); if (lambdaFunctions.length === 0) { return false; } return true; } async function askLambdaSource(context, functionType, path, currentPath) { switch (functionType) { case 'arn': return askLambdaArn(context, currentPath); case 'projectFunction': return askLambdaFromProject(currentPath); case 'newFunction': return newLambdaFunction(context, path); default: throw new Error('Type not supported'); } } async function newLambdaFunction(context, path) { let params = { functionTemplate: { parameters: { path, expressPath: (0, rest_api_path_utils_1.formatCFNPathParamsForExpressJs)(path), }, }, }; const resourceName = await context.amplify.invokePluginMethod(context, amplify_cli_core_1.AmplifyCategories.FUNCTION, undefined, 'add', [ context, 'awscloudformation', amplify_cli_core_1.AmplifySupportedService.LAMBDA, params, ]); amplify_prompts_1.printer.success('Succesfully added the Lambda function locally'); return { lambdaFunction: resourceName }; } async function askLambdaFromProject(currentPath) { const meta = amplify_cli_core_1.stateManager.getMeta(); const lambdaFunctions = []; Object.keys((meta === null || meta === void 0 ? void 0 : meta.function) || {}).forEach((resourceName) => { if (meta.function[resourceName].service === amplify_cli_core_1.AmplifySupportedService.LAMBDA) { lambdaFunctions.push(resourceName); } }); const lambdaFunction = await amplify_prompts_1.prompter.pick('Choose the Lambda function to invoke by this path', lambdaFunctions, { initial: currentPath ? lambdaFunctions.indexOf(currentPath.lambdaFunction) : 0, }); return { lambdaFunction }; } async function askLambdaArn(context, currentPath) { const lambdaFunctions = await context.amplify.executeProviderUtils(context, 'awscloudformation', 'getLambdaFunctions'); const lambdaOptions = lambdaFunctions.map((lambdaFunction) => ({ value: lambdaFunction.FunctionArn, name: `${lambdaFunction.FunctionName} (${lambdaFunction.FunctionArn})`, })); if (lambdaOptions.length === 0) { amplify_prompts_1.printer.error('You do not have any Lambda functions configured for the selected Region'); return null; } const lambdaCloudOptionQuestion = { type: 'list', name: 'lambdaChoice', message: 'Select a Lambda function', choices: lambdaOptions, default: currentPath && currentPath.lambdaFunction ? `${currentPath.lambdaFunction}` : `${lambdaOptions[0].value}`, }; let lambdaOption; while (!lambdaOption) { try { lambdaOption = await inquirer_1.default.prompt([lambdaCloudOptionQuestion]); } catch (err) { amplify_prompts_1.printer.error('Select a Lambda Function'); } } const lambdaCloudOptionAnswer = lambdaFunctions.find((lambda) => lambda.FunctionArn === lambdaOption.lambdaChoice); return { lambdaArn: lambdaCloudOptionAnswer.FunctionArn, lambdaFunction: lambdaCloudOptionAnswer.FunctionName, }; } async function migrate(context, projectPath, resourceName) { var _a, _b; const apigwInputState = new apigw_input_state_1.ApigwInputState(context, resourceName); if (resourceName === category_constants_1.ADMIN_QUERIES_NAME) { const meta = amplify_cli_core_1.stateManager.getMeta(); const adminQueriesDependsOn = lodash_1.default.get(meta, [amplify_cli_core_1.AmplifyCategories.API, category_constants_1.ADMIN_QUERIES_NAME, 'dependsOn'], undefined); if (!adminQueriesDependsOn) { throw new Error('Failed to migrate Admin Queries API. Could not find expected information in amplify-meta.json.'); } const functionName = (_b = (_a = adminQueriesDependsOn.filter((dependency) => dependency.category === amplify_cli_core_1.AmplifyCategories.FUNCTION)) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.resourceName; const adminQueriesProps = { apiName: resourceName, authResourceName: getAuthResourceName(), functionName, dependsOn: adminQueriesDependsOn, }; return apigwInputState.migrateAdminQueries(adminQueriesProps); } return apigwInputState.migrateApigwResource(resourceName); } exports.migrate = migrate; function getIAMPolicies(resourceName, crudOptions) { let policy = {}; const actions = []; crudOptions.forEach((crudOption) => { switch (crudOption) { case cdk_stack_builder_1.CrudOperation.CREATE: actions.push('apigateway:POST', 'apigateway:PUT'); break; case cdk_stack_builder_1.CrudOperation.UPDATE: actions.push('apigateway:PATCH'); break; case cdk_stack_builder_1.CrudOperation.READ: actions.push('apigateway:GET', 'apigateway:HEAD', 'apigateway:OPTIONS'); break; case cdk_stack_builder_1.CrudOperation.DELETE: actions.push('apigateway:DELETE'); break; default: amplify_prompts_1.printer.info(`${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', 'ApiId']; return { policy, attributes }; } exports.getIAMPolicies = getIAMPolicies; const openConsole = async (context) => { const amplifyMeta = amplify_cli_core_1.stateManager.getMeta(); const categoryAmplifyMeta = amplifyMeta[category]; const { Region } = amplifyMeta.providers.awscloudformation; const restApis = Object.keys(categoryAmplifyMeta).filter((resourceName) => { const resource = categoryAmplifyMeta[resourceName]; return (resource.output && (resource.service === serviceName || (resource.service === elasticContainerServiceName && resource.apiType === 'REST'))); }); if (restApis) { let url; const selectedApi = await amplify_prompts_1.prompter.pick('Select the API', restApis); const selectedResource = categoryAmplifyMeta[selectedApi]; if (selectedResource.service === serviceName) { const { output: { ApiId }, } = selectedResource; url = `https://${Region}.console.aws.amazon.com/apigateway/home?region=${Region}#/apis/${ApiId}/resources/`; } else { const { output: { PipelineName, ServiceName, ClusterName }, } = selectedResource; const codePipeline = 'CodePipeline'; const elasticContainer = 'ElasticContainer'; const selectedConsole = await amplify_prompts_1.prompter.pick('Which console do you want to open?', [ { name: 'Elastic Container Service (Deployed container status)', value: elasticContainer, }, { name: 'CodePipeline (Container build status)', value: codePipeline, }, ]); if (selectedConsole === elasticContainer) { url = `https://console.aws.amazon.com/ecs/home?region=${Region}#/clusters/${ClusterName}/services/${ServiceName}/details`; } else if (selectedConsole === codePipeline) { url = `https://${Region}.console.aws.amazon.com/codesuite/codepipeline/pipelines/${PipelineName}/view`; } else { amplify_prompts_1.printer.error('Option not available'); return; } } await (0, amplify_cli_core_1.open)(url, { wait: false }); } else { amplify_prompts_1.printer.error('There are no REST APIs pushed to the cloud'); } }; exports.openConsole = openConsole; //# sourceMappingURL=apigw-walkthrough.js.map