Skip to content

Commit

Permalink
feat: add support for defining IAM Permissions Boundary for Project (a…
Browse files Browse the repository at this point in the history
…ws-amplify#7144)

Adds a new advanced project configuration option to specify a permissions boundary that will be applied to all IAM roles in the project. This is broken into 3 components:
1. add a permissions boundary state manager to amplify-cli-core
2. add a prompt to amplify configure project that writes the boundary to the state manager
3. adds a permissions boundary template modifier to the pre-push cloudformation transformer that reads the value from the state manager and applies it to cfn templates
  • Loading branch information
edwardfoyle authored Jun 3, 2021
1 parent 2423e2e commit acf031b
Show file tree
Hide file tree
Showing 26 changed files with 757 additions and 45 deletions.
70 changes: 50 additions & 20 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1121,30 +1121,38 @@ jobs:
environment:
TEST_SUITE: src/__tests__/migration/node.function.test.ts
CLI_REGION: eu-west-2
iam-permissions-boundary-amplify_e2e_tests:
working_directory: ~/repo
docker: *ref_1
resource_class: large
steps: *ref_4
environment:
TEST_SUITE: src/__tests__/iam-permissions-boundary.test.ts
CLI_REGION: eu-central-1
function_5-amplify_e2e_tests:
working_directory: ~/repo
docker: *ref_1
resource_class: large
steps: *ref_4
environment:
TEST_SUITE: src/__tests__/function_5.test.ts
CLI_REGION: eu-central-1
CLI_REGION: ap-northeast-1
configure-project-amplify_e2e_tests:
working_directory: ~/repo
docker: *ref_1
resource_class: large
steps: *ref_4
environment:
TEST_SUITE: src/__tests__/configure-project.test.ts
CLI_REGION: ap-northeast-1
CLI_REGION: ap-southeast-1
api_4-amplify_e2e_tests:
working_directory: ~/repo
docker: *ref_1
resource_class: large
steps: *ref_4
environment:
TEST_SUITE: src/__tests__/api_4.test.ts
CLI_REGION: ap-southeast-1
CLI_REGION: ap-southeast-2
schema-iterative-update-4-amplify_e2e_tests_pkg_linux:
working_directory: ~/repo
docker: *ref_1
Expand Down Expand Up @@ -1805,6 +1813,16 @@ jobs:
TEST_SUITE: src/__tests__/migration/node.function.test.ts
CLI_REGION: eu-west-2
steps: *ref_5
iam-permissions-boundary-amplify_e2e_tests_pkg_linux:
working_directory: ~/repo
docker: *ref_1
resource_class: large
environment:
AMPLIFY_DIR: /home/circleci/repo/out
AMPLIFY_PATH: /home/circleci/repo/out/amplify-pkg-linux
TEST_SUITE: src/__tests__/iam-permissions-boundary.test.ts
CLI_REGION: eu-central-1
steps: *ref_5
function_5-amplify_e2e_tests_pkg_linux:
working_directory: ~/repo
docker: *ref_1
Expand All @@ -1813,7 +1831,7 @@ jobs:
AMPLIFY_DIR: /home/circleci/repo/out
AMPLIFY_PATH: /home/circleci/repo/out/amplify-pkg-linux
TEST_SUITE: src/__tests__/function_5.test.ts
CLI_REGION: eu-central-1
CLI_REGION: ap-northeast-1
steps: *ref_5
configure-project-amplify_e2e_tests_pkg_linux:
working_directory: ~/repo
Expand All @@ -1823,7 +1841,7 @@ jobs:
AMPLIFY_DIR: /home/circleci/repo/out
AMPLIFY_PATH: /home/circleci/repo/out/amplify-pkg-linux
TEST_SUITE: src/__tests__/configure-project.test.ts
CLI_REGION: ap-northeast-1
CLI_REGION: ap-southeast-1
steps: *ref_5
api_4-amplify_e2e_tests_pkg_linux:
working_directory: ~/repo
Expand All @@ -1833,7 +1851,7 @@ jobs:
AMPLIFY_DIR: /home/circleci/repo/out
AMPLIFY_PATH: /home/circleci/repo/out/amplify-pkg-linux
TEST_SUITE: src/__tests__/api_4.test.ts
CLI_REGION: ap-southeast-1
CLI_REGION: ap-southeast-2
steps: *ref_5
workflows:
version: 2
Expand Down Expand Up @@ -1948,19 +1966,19 @@ workflows:
- predictions-amplify_e2e_tests
- schema-predictions-amplify_e2e_tests
- amplify-configure-amplify_e2e_tests
- function_5-amplify_e2e_tests
- iam-permissions-boundary-amplify_e2e_tests
- containers-api-amplify_e2e_tests
- interactions-amplify_e2e_tests
- datastore-modelgen-amplify_e2e_tests
- configure-project-amplify_e2e_tests
- function_5-amplify_e2e_tests
- schema-iterative-update-2-amplify_e2e_tests
- schema-data-access-patterns-amplify_e2e_tests
- init-special-case-amplify_e2e_tests
- api_4-amplify_e2e_tests
- auth_1-amplify_e2e_tests
- configure-project-amplify_e2e_tests
- feature-flags-amplify_e2e_tests
- schema-versioned-amplify_e2e_tests
- plugin-amplify_e2e_tests
- api_4-amplify_e2e_tests
- done_with_pkg_linux_e2e_tests:
requires:
- schema-key-amplify_e2e_tests_pkg_linux
Expand All @@ -1978,19 +1996,19 @@ workflows:
- predictions-amplify_e2e_tests_pkg_linux
- schema-predictions-amplify_e2e_tests_pkg_linux
- amplify-configure-amplify_e2e_tests_pkg_linux
- function_5-amplify_e2e_tests_pkg_linux
- iam-permissions-boundary-amplify_e2e_tests_pkg_linux
- containers-api-amplify_e2e_tests_pkg_linux
- interactions-amplify_e2e_tests_pkg_linux
- datastore-modelgen-amplify_e2e_tests_pkg_linux
- configure-project-amplify_e2e_tests_pkg_linux
- function_5-amplify_e2e_tests_pkg_linux
- schema-iterative-update-2-amplify_e2e_tests_pkg_linux
- schema-data-access-patterns-amplify_e2e_tests_pkg_linux
- init-special-case-amplify_e2e_tests_pkg_linux
- api_4-amplify_e2e_tests_pkg_linux
- auth_1-amplify_e2e_tests_pkg_linux
- configure-project-amplify_e2e_tests_pkg_linux
- feature-flags-amplify_e2e_tests_pkg_linux
- schema-versioned-amplify_e2e_tests_pkg_linux
- plugin-amplify_e2e_tests_pkg_linux
- api_4-amplify_e2e_tests_pkg_linux
- amplify_migration_tests_latest:
context:
- amplify-ecr-image-pull
Expand Down Expand Up @@ -2345,7 +2363,7 @@ workflows:
filters: *ref_9
requires:
- auth_4-amplify_e2e_tests
- function_5-amplify_e2e_tests:
- iam-permissions-boundary-amplify_e2e_tests:
context: *ref_7
post-steps: *ref_8
filters: *ref_9
Expand Down Expand Up @@ -2405,7 +2423,7 @@ workflows:
filters: *ref_9
requires:
- migration-api-key-migration1-amplify_e2e_tests
- configure-project-amplify_e2e_tests:
- function_5-amplify_e2e_tests:
context: *ref_7
post-steps: *ref_8
filters: *ref_9
Expand Down Expand Up @@ -2465,7 +2483,7 @@ workflows:
filters: *ref_9
requires:
- layer-amplify_e2e_tests
- api_4-amplify_e2e_tests:
- configure-project-amplify_e2e_tests:
context: *ref_7
post-steps: *ref_8
filters: *ref_9
Expand Down Expand Up @@ -2525,6 +2543,12 @@ workflows:
filters: *ref_9
requires:
- auth_3-amplify_e2e_tests
- api_4-amplify_e2e_tests:
context: *ref_7
post-steps: *ref_8
filters: *ref_9
requires:
- auth_1-amplify_e2e_tests
- schema-iterative-update-4-amplify_e2e_tests_pkg_linux:
context: &ref_10
- amplify-ecr-image-pull
Expand Down Expand Up @@ -2783,7 +2807,7 @@ workflows:
filters: *ref_12
requires:
- auth_4-amplify_e2e_tests_pkg_linux
- function_5-amplify_e2e_tests_pkg_linux:
- iam-permissions-boundary-amplify_e2e_tests_pkg_linux:
context: *ref_10
post-steps: *ref_11
filters: *ref_12
Expand Down Expand Up @@ -2847,7 +2871,7 @@ workflows:
filters: *ref_12
requires:
- migration-api-key-migration1-amplify_e2e_tests_pkg_linux
- configure-project-amplify_e2e_tests_pkg_linux:
- function_5-amplify_e2e_tests_pkg_linux:
context: *ref_10
post-steps: *ref_11
filters: *ref_12
Expand Down Expand Up @@ -2911,7 +2935,7 @@ workflows:
filters: *ref_12
requires:
- layer-amplify_e2e_tests_pkg_linux
- api_4-amplify_e2e_tests_pkg_linux:
- configure-project-amplify_e2e_tests_pkg_linux:
context: *ref_10
post-steps: *ref_11
filters: *ref_12
Expand Down Expand Up @@ -2975,3 +2999,9 @@ workflows:
filters: *ref_12
requires:
- auth_3-amplify_e2e_tests_pkg_linux
- api_4-amplify_e2e_tests_pkg_linux:
context: *ref_10
post-steps: *ref_11
filters: *ref_12
requires:
- auth_1-amplify_e2e_tests_pkg_linux
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { getPermissionsBoundaryArn, setPermissionsBoundaryArn } from '..';
import { stateManager } from '../state-manager';

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

const testEnv = 'testEnv';

const objKey = 'PermissionsBoundaryPolicyArn';

const stateManager_mock = stateManager as jest.Mocked<typeof stateManager>;
stateManager_mock.getLocalEnvInfo.mockReturnValue({
envName: testEnv,
});

const testArn = 'testArn';

const tpi_stub = {
[testEnv]: {
awscloudformation: {
[objKey]: testArn,
},
},
};

describe('get permissions boundary arn', () => {
beforeEach(jest.clearAllMocks);
it('gets arn from team provider info file', () => {
stateManager_mock.getTeamProviderInfo.mockReturnValueOnce(tpi_stub);
expect(getPermissionsBoundaryArn()).toEqual(testArn);
});

it('gets arn from preInitTeamProviderInfo', () => {
// setup
setPermissionsBoundaryArn(testArn, testEnv, tpi_stub);

// test
expect(getPermissionsBoundaryArn()).toEqual(testArn);

// reset
setPermissionsBoundaryArn(testArn, testEnv);
});

it('returns undefined if no value found', () => {
expect(getPermissionsBoundaryArn()).toBeUndefined();
});
});

describe('set permissions boundary arn', () => {
beforeEach(jest.clearAllMocks);
it('sets the ARN value in tpi file if specified', () => {
stateManager_mock.getTeamProviderInfo.mockReturnValueOnce({});
setPermissionsBoundaryArn(testArn);
expect(stateManager_mock.setTeamProviderInfo.mock.calls[0][1][testEnv].awscloudformation[objKey]).toEqual(testArn);
});

it('sets the ARN for the specified env', () => {
stateManager_mock.getTeamProviderInfo.mockReturnValueOnce({});
setPermissionsBoundaryArn(testArn, 'otherenv');
expect(stateManager_mock.setTeamProviderInfo.mock.calls[0][1].otherenv.awscloudformation[objKey]).toEqual(testArn);
});

it('removes the ARN value if not specified', () => {
stateManager_mock.getTeamProviderInfo.mockReturnValueOnce(tpi_stub);
setPermissionsBoundaryArn();
expect(stateManager_mock.setTeamProviderInfo.mock.calls[0][1][testEnv].awscloudformation).toBeDefined();
expect(stateManager_mock.setTeamProviderInfo.mock.calls[0][1][testEnv].awscloudformation[objKey]).toBeUndefined();
});

it('if tpi object specified, sets arn in object and sets global preInitTeamProviderInfo', () => {
const tpi: Record<string, any> = {};
setPermissionsBoundaryArn(testArn, undefined, tpi);
expect(tpi[testEnv].awscloudformation[objKey]).toEqual(testArn);
expect(getPermissionsBoundaryArn()).toEqual(testArn);
delete (global as any).preInitTeamProviderInfo;
});
});
3 changes: 2 additions & 1 deletion packages/amplify-cli-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './cliContext';
export * from './cliContextEnvironmentProvider';
export * from './cliEnvironmentProvider';
export * from './feature-flags';
export * from './permissionsBoundaryState';
export * from './jsonUtilities';
export * from './jsonValidationError';
export * from './serviceSelection';
Expand Down Expand Up @@ -182,7 +183,7 @@ interface AmplifyToolkit {
getResourceStatus: (category?: $TSAny, resourceName?: $TSAny, providerName?: $TSAny, filteredResources?: $TSAny) => $TSAny;
getResourceOutputs: () => $TSAny;
getWhen: () => $TSAny;
inputValidation: (input: $TSAny) => $TSAny;
inputValidation: (input: $TSAny) => (value: $TSAny) => boolean | string;
listCategories: () => $TSAny;
makeId: (n?: number) => string;
openEditor: () => $TSAny;
Expand Down
56 changes: 56 additions & 0 deletions packages/amplify-cli-core/src/permissionsBoundaryState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { stateManager } from './state-manager';
import _ from 'lodash';
import { $TSObject } from '.';

let preInitTeamProviderInfo: any;

export const getPermissionsBoundaryArn = (env?: string): string | undefined => {
try {
const tpi = preInitTeamProviderInfo ?? stateManager.getTeamProviderInfo();
// if the pre init team-provider-info only has one env (which should always be the case), default to that one
if (preInitTeamProviderInfo && Object.keys(preInitTeamProviderInfo).length === 1 && !env) {
env = Object.keys(preInitTeamProviderInfo)[0];
}
return _.get(tpi, teamProviderInfoObjectPath(env)) as string | undefined;
} catch {
// uninitialized project
return undefined;
}
};

/**
* Stores the permissions boundary ARN in team-provider-info
* If teamProviderInfo is not specified, the file is read, updated and written back to disk
* If teamProviderInfo is specified, then this function assumes that the env is not initialized
* In this case, the teamProviderInfo object is updated but not written to disk. Instead "preInitTeamProviderInfo" is set
* so that subsequent calls to getPermissionsBoundaryArn will return the permissions boundary arn of the pre-initialized env
* @param arn The permissions boundary arn. If undefined or empty, the permissions boundary is removed
* @param env The Amplify env to update. If not specified, defaults to the current checked out environment
* @param teamProviderInfo The team-provider-info object to update
*/
export const setPermissionsBoundaryArn = (arn?: string, env?: string, teamProviderInfo?: $TSObject): void => {
let tpiGetter = () => stateManager.getTeamProviderInfo();
let tpiSetter = (tpi: $TSObject) => {
stateManager.setTeamProviderInfo(undefined, tpi);
preInitTeamProviderInfo = undefined;
};
if (teamProviderInfo) {
tpiGetter = () => teamProviderInfo;
tpiSetter = (tpi: $TSObject) => {
preInitTeamProviderInfo = tpi;
};
}
const tpi = tpiGetter();
if (!arn) {
_.unset(tpi, teamProviderInfoObjectPath(env));
} else {
_.set(tpi, teamProviderInfoObjectPath(env), arn);
}
tpiSetter(tpi);
};

const teamProviderInfoObjectPath = (env?: string) => [
env || (stateManager.getLocalEnvInfo().envName as string),
'awscloudformation',
'PermissionsBoundaryPolicyArn',
];
4 changes: 4 additions & 0 deletions packages/amplify-cli/src/commands/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ function displayHelp(context) {
name: 'import --name <env-name> --config <provider-configs> [--awsInfo <aws-configs>]',
description: 'Imports an already existing Amplify project environment stack to your local backend',
},
{
name: 'update [--permissions-boundary <IAM Policy ARN>]',
description: 'Update the environment configuration',
},
{
name: 'remove <env-name>',
description: 'Removes an environment from the Amplify project',
Expand Down
6 changes: 6 additions & 0 deletions packages/amplify-cli/src/commands/env/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { $TSContext } from 'amplify-cli-core';
import { executeProviderCommand } from '../../extensions/amplify-helpers/get-provider-plugins';

export const run = async (context: $TSContext) => {
await executeProviderCommand(context, 'updateEnv');
};
Loading

0 comments on commit acf031b

Please sign in to comment.