Skip to content

Commit

Permalink
Refactor and cleanup.
Browse files Browse the repository at this point in the history
  • Loading branch information
Ross Ragsdale committed Nov 19, 2020
1 parent 8810503 commit 1c4309c
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 70 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { constructCFModelTableNameComponent, constructCFModelTableArnComponent } from '../utils/cloudformationHelpers';
import inquirer from 'inquirer';
import inquirer, { CheckboxQuestion, DistinctChoice } from 'inquirer';
import path from 'path';
import * as TransformPackage from 'graphql-transformer-core';
import _, { get } from 'lodash';
import _, { get, capitalize, concat, isEmpty } from 'lodash';
import { topLevelCommentPrefix, topLevelCommentSuffix, envVarPrintoutPrefix, CRUDOperation, GraphQLOperation } from '../../../constants';
import { ServiceName } from '../utils/constants';
import {
Expand Down Expand Up @@ -30,22 +30,17 @@ export const askExecRolePermissionsQuestions = async (
// retrieve api's AppSync resource name for conditional logic
// in blending appsync @model-backed dynamoDB tables into storage category flow
const appsyncResourceName = getAppSyncResourceName();

// if there is api category AppSync resource and no storage category, add it back to selection
// since storage category is responsible for managing appsync @model-backed dynamoDB table permissions
if (!categories.includes('storage') && appsyncResourceName !== undefined) {
categories.push('storage');
}

const categoryPermissionQuestion = {
type: 'checkbox',
name: 'categories',
message: 'Select the category',
choices: categories,
default: fetchPermissionCategories(currentPermissionMap),
};
const capitalizeFirstLetter = str => str.charAt(0).toUpperCase() + str.slice(1);
const categoryPermissionAnswer = await inquirer.prompt([categoryPermissionQuestion]);
const selectedCategories = categoryPermissionAnswer.categories as any[];
const categoryPermissionQuestion = selectCategories(categories, currentPermissionMap);
const categoryPermissionAnswer = await inquirer.prompt(categoryPermissionQuestion);
const selectedCategories = categoryPermissionAnswer.categories as string[];

let categoryPolicies = [];
let resources = [];
const crudOptions = _.values(CRUDOperation);
Expand Down Expand Up @@ -75,42 +70,27 @@ export const askExecRolePermissionsQuestions = async (
);
}

if (resourcesList.length === 0) {
if (isEmpty(resourcesList)) {
context.print.warning(`No resources found for ${category}`);
continue;
}

try {
let selectedResources: any = [];

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: fetchPermissionResourcesForCategory(currentPermissionMap, category),
};

let selectedResources = [];
if (resourcesList.length > 1) {
// There's a few resources in this category. Let's pick some.
const resourceQuestion = selectResourcesInCategory(resourcesList, currentPermissionMap, category);
const resourceAnswer = await inquirer.prompt([resourceQuestion]);
selectedResources = resourceAnswer.resources;
selectedResources = concat(resourceAnswer.resources);
} else {
// There's only one resource in the category. Let's use that.
selectedResources = concat(resourcesList);
}

for (let resourceName of selectedResources) {
// If the resource is AppSync, use GraphQL operations for permission policies.
// Otherwise, default to CRUD permissions.
const serviceType = get(context.amplify.getProjectMeta(), [category, resourceName, 'service']);
const serviceType = get(amplifyMeta, [category, resourceName, 'service']);
let options;
switch (serviceType) {
case 'AppSync':
Expand All @@ -121,32 +101,18 @@ export const askExecRolePermissionsQuestions = async (
break;
}

if (
// In case of some resources they are not in the meta file so check for resource existence as well
amplifyMeta[category] &&
amplifyMeta[category][resourceName] &&
amplifyMeta[category][resourceName].mobileHubMigrated === true
) {
// In case of some resources they are not in the meta file so check for resource existence as well
const isMobileHubImportedResource = get(amplifyMeta, [category, resourceName, 'mobileHubMigrated'], false);
if (isMobileHubImportedResource) {
context.print.warning(`Policies cannot be added for ${category}/${resourceName}, since it is a MobileHub imported resource.`);
continue;
} else {
const permissionQuestion = {
type: 'checkbox',
name: 'options',
message: `Select the operations you want to permit for ${resourceName}`,
choices: options,
validate: value => {
if (value.length === 0) {
return 'You must select at least one operation';
}
return true;
},
default: fetchPermissionsForResourceInCategory(currentPermissionMap, category, resourceName, serviceType),
};

const resourceType = get(amplifyMeta, [category, resourceName, 'service']);
const currentPermissions = fetchPermissionsForResourceInCategory(currentPermissionMap, category, resourceName, serviceType);
const permissionQuestion = selectPermissions(options, currentPermissions, resourceType);
const permissionAnswer = await inquirer.prompt([permissionQuestion]);

const resourcePolicy: any = permissionAnswer.options;

// overload options when user selects graphql @model-backing DynamoDB table
// as there is no actual storage category resource where getPermissionPolicies can derive service and provider
if (resourceName.endsWith(appsyncTableSuffix)) {
Expand Down Expand Up @@ -281,3 +247,41 @@ export const askExecRolePermissionsQuestions = async (
export type ExecRolePermissionsResponse = Required<
Pick<FunctionParameters, 'categoryPolicies' | 'environmentMap' | 'topLevelComment' | 'dependsOn' | 'mutableParametersState'>
>;

// Inquirer Questions

const selectResourcesInCategory = (choices: DistinctChoice<any>[], currentPermissionMap: any, category: any): CheckboxQuestion => ({
type: 'checkbox',
name: 'resources',
message: `${capitalize(category)} has ${choices.length} resources in this project. Select the one you would like your Lambda to access`,
choices,
validate: answers => {
if (isEmpty(answers)) 'You must select at least one resource';
return true;
},
default: fetchPermissionResourcesForCategory(currentPermissionMap, category),
});

const selectCategories = (choices: DistinctChoice<any>[], currentPermissionMap: object): CheckboxQuestion => ({
type: 'checkbox',
name: 'categories',
message: 'Select the categories you want this function to have access to.',
choices,
validate: answers => {
if (isEmpty(answers)) 'You must select at least one category';
return true;
},
default: fetchPermissionCategories(currentPermissionMap),
});

const selectPermissions = (choices: DistinctChoice<any>[], currentPermissions: any, resourceType: any) => ({
type: 'checkbox',
name: 'options',
message: `Select the operations you want to permit for ${resourceType}`,
choices,
validate: answers => {
if (isEmpty(answers)) 'You must select at least one operation';
return true;
},
default: currentPermissions,
});
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export async function createWalkthrough(
// list out the advanced settings before asking whether to configure them
context.print.info('');
context.print.success('Available advanced settings:');
advancedSettingsList.forEach(setting => context.print.info('- '.concat(setting)));
advancedSettingsList.forEach(setting => context.print.info(setting));
context.print.info('');

// ask whether to configure advanced settings
Expand Down Expand Up @@ -96,7 +96,8 @@ function provideInformation(context, lambdaToUpdate, functionRuntime, currentPar
currentCategoryPermissions.forEach(category => {
const currentResources = fetchPermissionResourcesForCategory(currentParameters.permissions, category);
currentResources.forEach(resource => {
const currentPermissions = fetchPermissionsForResourceInCategory(currentParameters.permissions, category, resource);
const serviceType = _.get(context.amplify.getProjectMeta(), [category, resource, 'service']);
const currentPermissions = fetchPermissionsForResourceInCategory(currentParameters.permissions, category, resource, serviceType);
const formattedCurrentPermissions = ' ('.concat(currentPermissions.join(', ').concat(')'));
context.print.info('- '.concat(resource).concat(formattedCurrentPermissions));
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ export const layerParametersFileName = 'layer-parameters.json';
export const parametersFileName = 'parameters.json';
export const provider = 'awscloudformation';
export const appsyncTableSuffix = '@model(appsync)';
export const resourceAccessSetting = 'Resource access permissions';
export const cronJobSetting = 'Scheduled recurring invocation';
export const lambdaLayerSetting = 'Lambda layers configuration';
export const resourceAccessSetting = '🔒 Resource access permissions';
export const cronJobSetting = '⏰ Schedule recurring invocation';
export const lambdaLayerSetting = '📦 Lambda layers configuration';

export const enum ServiceName {
LambdaFunction = 'Lambda',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ export const fetchPermissionResourcesForCategory = (permissionMap, category: str
return _.keys(_.get(permissionMap, [category]));
};

export const fetchPermissionsForResourceInCategory = (permissionMap, category: string, resourceName: string, serviceType: string) => {
const permissions = _.get(permissionMap, [category, resourceName]);
export const fetchPermissionsForResourceInCategory = (
permissionMap: object = {},
category: string,
resourceName: string,
serviceType: string,
) => {
const permissions = _.get(permissionMap, [category, resourceName], []);
// BACKWARDS COMPATIBILITY:
// https://github.com/aws-amplify/amplify-cli/pull/5342
// If AppSync and CRUD permissions, update to GraphQL
if (serviceType == 'AppSync' && hasCRUDPermissions(permissions)) {
console.log('Backwards compatibility');
return updatePermissionsForBackwardsCompatibility(permissionMap, category, resourceName);
} else {
return permissions;
Expand Down
13 changes: 9 additions & 4 deletions packages/amplify-cli-core/src/state-manager/stateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,24 @@ export type GetOptions<T> = {
default?: T;
};

// TODO: where should this go?
interface ProjectMetadata {
category: {
resourceName: {};
};
}

export class StateManager {
metaFileExists = (projectPath?: string): boolean => fs.existsSync(pathManager.getAmplifyMetaFilePath(projectPath));

getMeta = (projectPath?: string, options?: GetOptions<$TSMeta>): $TSMeta => {
getMeta = (projectPath?: string, options?: GetOptions<$TSMeta>): ProjectMetadata => {
const filePath = pathManager.getAmplifyMetaFilePath(projectPath);
const mergedOptions = {
throwIfNotExist: true,
...options,
};

const data = this.getData<$TSMeta>(filePath, mergedOptions);

return data;
return this.getData<$TSMeta>(filePath, mergedOptions);
};

currentMetaFileExists = (projectPath?: string): boolean => fs.existsSync(pathManager.getCurrentAmplifyMetaFilePath(projectPath));
Expand Down

0 comments on commit 1c4309c

Please sign in to comment.