Skip to content

Commit

Permalink
feat: Custom policies IAM Policies for Lambda and Containers (#8068)
Browse files Browse the repository at this point in the history
* Custom policy implementation

* feat: add custom policies file to function and API container

add custom policies file to function and API container, merge the custom policies to CFN template,
validation for regex of resources and actions in the custom policies file

* feat: changes for first PR

* feat: Some changes according to the PR comments

1. Add Json Schema to validate the customers input 2. some minor changes related to format issue 3.
error handle

* feat: replace env to current env in the resource when checkout and add env, and push

* feat: e2e test and replacing env

* feat: Minor changes for env replacement

* feat: remove changing env between env

* feat: Add cloudform type for type safety, move validation to provider-cloudformation, validation

* feat: remove some unused function and import, change regex for resource

* feat: Some changes according to the PR comment

* feat: changes according to PR comments

* feat: remove unused import

* feat: remove previous unused code

* feat: Changes according to PR comments

* feat: some changes according to PR comments

* feat: work on PR comments

* feat: rebase for conflict

* feat: rebase for failure of hooksmanager test failed

* feat: unit test

* feat: fix fail test

* feat: change default template of custom policies

* feat: fix failed test

* feat: PR comments

* feat: pr comments

* feat: fix failed test

* feat: PR comments from ED

Co-authored-by: Lu Han <lhnamz@amazon.com>
  • Loading branch information
ammarkarachi and luhanamz authored Oct 4, 2021
1 parent 3168885 commit 3e1ce0d
Show file tree
Hide file tree
Showing 20 changed files with 579 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { DEPLOYMENT_MECHANISM } from './base-api-stack';
import { GitHubSourceActionInfo } from './pipeline-with-awaiter';
import { API_TYPE, IMAGE_SOURCE_TYPE, ResourceDependency, ServiceConfiguration } from './service-walkthroughs/containers-walkthrough';
import { ApiResource, generateContainersArtifacts } from './utils/containers-artifacts';
import { createDefaultCustomPoliciesFile, pathManager } from 'amplify-cli-core';

export const addResource = async (
serviceWalkthroughPromise: Promise<ServiceConfiguration>,
Expand Down Expand Up @@ -96,6 +97,10 @@ export const addResource = async (

}

createDefaultCustomPoliciesFile(category, resourceName);

const customPoliciesPath = pathManager.getCustomPoliciesPath(category, resourceName);

context.print.success(`Successfully added resource ${resourceName} locally.`);
context.print.info('');
context.print.success('Next steps:');
Expand All @@ -111,6 +116,7 @@ export const addResource = async (
context.print.info(
`- Amplify CLI infers many configuration settings from the "docker-compose.yaml" file. Learn more: docs.amplify.aws/cli/usage/containers`,
);
context.print.info(`- To access AWS resources outside of this Amplify app, edit the ${customPoliciesPath}`);
context.print.info('- Run "amplify push" to build and deploy your image');

return resourceName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
saveMutableState,
updateLayerArtifacts,
} from './utils/storeResources';
import { createDefaultCustomPoliciesFile } from 'amplify-cli-core';

/**
* Entry point for creating a new function
Expand Down Expand Up @@ -112,18 +113,23 @@ export async function addFunctionResource(

await createFunctionResources(context, completeParams);

createDefaultCustomPoliciesFile(category, completeParams.resourceName);

if (!completeParams.skipEdit) {
await openEditor(context, category, completeParams.resourceName, completeParams.functionTemplate);
}

const { print } = context;

const customPoliciesPath = pathManager.getCustomPoliciesPath(category, completeParams.resourceName);

print.success(`Successfully added resource ${completeParams.resourceName} locally.`);
print.info('');
print.success('Next steps:');
print.info(`Check out sample function code generated in <project-dir>/amplify/backend/function/${completeParams.resourceName}/src`);
print.info('"amplify function build" builds all of your functions currently in the project');
print.info('"amplify mock function <functionName>" runs your function locally');
print.info(`To access AWS resources outside of this Amplify app, edit the ${customPoliciesPath}`);
print.info('"amplify push" builds all of your local backend resources and provisions them in the cloud');
print.info(
'"amplify publish" builds all of your local backend and front-end resources (if you added hosting category) and provisions them in the cloud',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {createDefaultCustomPoliciesFile} from '../customPoliciesUtils'
import { JSONUtilities } from '..';
import { pathManager, PathConstants } from '../state-manager';
import path from 'path';

describe('Custom policies util test', () => {

jest.mock('../state-manager');

const testCategoryName = 'function';
const testResourceName = 'functionTest';
const expectedFilePath = path.join(__dirname, 'testFiles', 'custom-policies-test', testCategoryName, testResourceName, PathConstants.CustomPoliciesFilename);
jest.spyOn(pathManager, 'getCustomPoliciesPath').mockReturnValue(expectedFilePath);

beforeEach(jest.clearAllMocks);

test('Write default custom policy file to the specified resource name', () => {

createDefaultCustomPoliciesFile(testCategoryName, testResourceName);

const data = JSONUtilities.readJson(expectedFilePath);

expect(data).toMatchObject([
{
Action: [],
Resource: []
}
]);

})
})
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ pathManager_mock.getHooksDirPath.mockReturnValue(testProjectHooksDirPath);
stateManager_mock.getHooksConfigJson.mockReturnValueOnce({ extensions: { py: { runtime: 'python3' } } });

jest.mock('execa');
jest.mock('process');
jest.mock('../../state-manager');
jest.mock('which', () => ({
sync: jest.fn().mockImplementation(runtimeName => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{
"Action": [],
"Resource": []
}
]
64 changes: 64 additions & 0 deletions packages/amplify-cli-core/src/customPoliciesUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Fn, IAM } from 'cloudform-types';
import { JSONUtilities, pathManager } from '.';

export type CustomIAMPolicies = CustomIAMPolicy[];

export type CustomIAMPolicy = {
Action: string[];
Effect: string;
Resource: string[];
}

export const CustomIAMPoliciesSchema = {
type : 'array',
minItems: 1,
items: {
type: 'object',
properties: {
Action: { type: 'array', items: { type: 'string' }, minItems: 1, nullable: false },
Resource: { type: 'array', items: { type: 'string' }, minItems: 1, nullable: false }
},
optionalProperties: {
Effect: { type: 'string', enum:['Allow', 'Deny'], default: 'Allow' },
},
required: ['Resource', 'Action'],
additionalProperties: true
},
additionalProperties: false
}

export const customExecutionPolicyForFunction = new IAM.Policy({
PolicyName: 'custom-lambda-execution-policy',
Roles: [
Fn.Ref('LambdaExecutionRole')
],
PolicyDocument: {
Version: '2012-10-17',
Statement: []
}
}).dependsOn(['LambdaExecutionRole']);

export const customExecutionPolicyForContainer = new IAM.Policy({
PolicyDocument: {
Statement: [
],
Version: '2012-10-17'
},
PolicyName: 'CustomExecutionPolicyForContainer',
Roles: [
]
});

export function createDefaultCustomPoliciesFile(categoryName: string, resourceName: string) {
const customPoliciesPath = pathManager.getCustomPoliciesPath(categoryName, resourceName);
const defaultCustomPolicies = [
{
Action: [],
Resource: []
}
]
JSONUtilities.writeJson(customPoliciesPath, defaultCustomPolicies);
}



2 changes: 2 additions & 0 deletions packages/amplify-cli-core/src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export class AngularConfigNotFoundError extends Error {}
export class AppIdMismatchError extends Error {}
export class UnrecognizedFrameworkError extends Error {}
export class ConfigurationError extends Error {}
export class CustomPoliciesFormatError extends Error {}

export class NotInitializedError extends Error {
public constructor() {
super();
Expand Down
2 changes: 2 additions & 0 deletions packages/amplify-cli-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export * from './cliGetCategories';
export * from './cliRemoveResourcePrompt';
export * from './cliViewAPI';
export * from './hooks';
export * from './cliViewAPI';
export * from './customPoliciesUtils'

// Temporary types until we can finish full type definition across the whole CLI

Expand Down
5 changes: 5 additions & 0 deletions packages/amplify-cli-core/src/state-manager/pathManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export const PathConstants = {
CLIJsonWithEnvironmentFileName: (env: string) => `cli.${env}.json`,

CfnFileName: (resourceName: string) => `${resourceName}-awscloudformation-template.json`,

CustomPoliciesFilename: 'custom-policies.json',
};

export class PathManager {
Expand Down Expand Up @@ -148,6 +150,9 @@ export class PathManager {

getDotAWSDirPath = (): string => path.normalize(path.join(homedir(), PathConstants.DotAWSDirName));

getCustomPoliciesPath = (category: string, resourceName: string): string =>
path.join(this.getResourceDirectoryPath(undefined, category, resourceName), PathConstants.CustomPoliciesFilename);

getAWSCredentialsFilePath = (): string => path.normalize(path.join(this.getDotAWSDirPath(), PathConstants.AWSCredentials));

getAWSConfigFilePath = (): string => path.normalize(path.join(this.getDotAWSDirPath(), PathConstants.AWSConfig));
Expand Down
19 changes: 16 additions & 3 deletions packages/amplify-cli-core/src/state-manager/stateManager.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import _ from 'lodash';
import { $TSAny, $TSMeta, $TSTeamProviderInfo, DeploymentSecrets, HooksConfig, PathConstants } from '..';
import { SecretFileMode } from '../cliConstants';
import { PathConstants, pathManager } from './pathManager';
import { $TSMeta, $TSTeamProviderInfo, $TSAny, DeploymentSecrets, HooksConfig } from '..';
import { JSONUtilities } from '../jsonUtilities';
import { SecretFileMode } from '../cliConstants';
import { HydrateTags, ReadTags, Tag } from '../tags';
import { pathManager } from './pathManager';
import { CustomIAMPolicies } from '../customPoliciesUtils';


export type GetOptions<T> = {
throwIfNotExist?: boolean;
Expand Down Expand Up @@ -76,6 +78,15 @@ export class StateManager {
return this.getData<$TSTeamProviderInfo>(filePath, mergedOptions);
};

getCustomPolicies = (categoryName: string, resourceName: string): CustomIAMPolicies | undefined => {
const filePath = pathManager.getCustomPoliciesPath(categoryName, resourceName);
try{
return JSONUtilities.readJson<CustomIAMPolicies>(filePath);
} catch(err) {
return undefined;
}
};

localEnvInfoExists = (projectPath?: string): boolean => this.doesExist(pathManager.getLocalEnvFilePath, projectPath);

getLocalEnvInfo = (projectPath?: string, options?: GetOptions<$TSAny>): $TSAny => {
Expand Down Expand Up @@ -368,3 +379,5 @@ export class StateManager {
}

export const stateManager = new StateManager();


Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { onCategoryOutputsChange } from './on-category-outputs-change';
import { initializeEnv } from '../../initialize-env';
import { getProviderPlugins } from './get-provider-plugins';
import { getEnvInfo } from './get-env-info';
import { EnvironmentDoesNotExistError, exitOnNextTick, stateManager, $TSAny, $TSContext } from 'amplify-cli-core';
import { EnvironmentDoesNotExistError, exitOnNextTick, stateManager, $TSAny, $TSContext, CustomPoliciesFormatError } from 'amplify-cli-core';
import { printer } from 'amplify-prompts';

export async function pushResources(
context: $TSContext,
Expand Down Expand Up @@ -76,8 +77,9 @@ export async function pushResources(
await onCategoryOutputsChange(context, currentAmplifyMeta);
} catch (err) {
// Handle the errors and print them nicely for the user.
context.print.error(`\n${err.message}`);

if (!(err instanceof CustomPoliciesFormatError)) {
printer.error(`\n${err.message}`);
}
throw err;
}
} else {
Expand Down
26 changes: 26 additions & 0 deletions packages/amplify-e2e-core/src/categories/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,3 +567,29 @@ export function addRestContainerApi(projectDir: string) {
});
});
}

export function addRestContainerApiForCustomPolicies(projectDir: string, settings: { name: string }) {
return new Promise<void>((resolve, reject) => {
spawn(getCLIPath(), ['add', 'api'], { cwd: projectDir, stripColors: true })
.wait('Please select from one of the below mentioned services:')
.sendKeyDown()
.sendCarriageReturn()
.wait('Which service would you like to use')
.sendKeyDown()
.sendCarriageReturn()
.wait('Provide a friendly name for your resource to be used as a label for this category in the project:')
.send(settings.name)
.sendCarriageReturn()
.wait('What image would you like to use')
.sendKeyDown()
.sendCarriageReturn()
.wait('When do you want to build & deploy the Fargate task')
.sendCarriageReturn()
.wait('Do you want to restrict API access')
.sendConfirmNo()
.wait('Select which container is the entrypoint')
.sendCarriageReturn()
.wait('"amplify publish" will build all your local backend and frontend resources')
.run((err: Error) => err ? reject(err) : resolve());
});
}
4 changes: 4 additions & 0 deletions packages/amplify-e2e-core/src/utils/projectMeta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ function getProjectMeta(projectRoot: string) {
const metaFilePath: string = path.join(projectRoot, 'amplify', '#current-cloud-backend', 'amplify-meta.json');
return JSON.parse(fs.readFileSync(metaFilePath, 'utf8'));
}
function getCustomPoliciesPath(projectRoot: string, category: string, resourceName: string): string {
return path.join(projectRoot, 'amplify', 'backend', category, resourceName, 'custom-policies.json');
}

function getProjectTags(projectRoot: string) {
const projectTagsFilePath: string = path.join(projectRoot, 'amplify', '#current-cloud-backend', 'tags.json');
Expand Down Expand Up @@ -167,4 +170,5 @@ export {
getCloudBackendConfig,
setTeamProviderInfo,
getLocalEnvInfo,
getCustomPoliciesPath,
};
12 changes: 12 additions & 0 deletions packages/amplify-e2e-tests/functions/get-ssm-parameter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const aws = require('aws-sdk');

exports.handler = async event => {
const { secretName} = event;
const { Parameter } = await new aws.SSM()
.getParameter({
Name: secretName,
WithDecryption: true,
})
.promise();
return Parameter;
};
Loading

0 comments on commit 3e1ce0d

Please sign in to comment.