From 87db7aa05d4de3f610a471732ab03f453a239cbe Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Fri, 6 Sep 2019 09:12:24 -0700 Subject: [PATCH] fix(iam): only attach policies to imported roles if the accounts match (#3716) We allow attaching policies to the imported role if the account passed in the ARN matches the account of the stack the policy belongs to, or if either one of those accounts was not specified (i.e., is env-agnostic), or if the ARN used for importing was dynamic. Fixes #2985 Fixes #3025 --- packages/@aws-cdk/aws-iam/lib/role.ts | 103 +++- packages/@aws-cdk/aws-iam/package.json | 1 + .../aws-iam/test/role.from-role-arn.test.ts | 533 ++++++++++++++++++ packages/@aws-cdk/aws-iam/test/role.test.ts | 40 -- 4 files changed, 610 insertions(+), 67 deletions(-) create mode 100644 packages/@aws-cdk/aws-iam/test/role.from-role-arn.test.ts diff --git a/packages/@aws-cdk/aws-iam/lib/role.ts b/packages/@aws-cdk/aws-iam/lib/role.ts index 54643646311fc..7fb0ceb982765 100644 --- a/packages/@aws-cdk/aws-iam/lib/role.ts +++ b/packages/@aws-cdk/aws-iam/lib/role.ts @@ -1,4 +1,4 @@ -import { Construct, Duration, Lazy, Resource, Stack } from '@aws-cdk/core'; +import { Construct, Duration, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; import { Grant } from './grant'; import { CfnRole } from './iam.generated'; import { IIdentity } from './identity-base'; @@ -123,6 +123,20 @@ export interface RoleProps { readonly maxSessionDuration?: Duration; } +/** + * Options allowing customizing the behavior of {@link Role.fromRoleArn}. + */ +export interface FromRoleArnOptions { + /** + * Whether the imported role can be modified by attaching policy resources to it. + * + * @default true + * + * @experimental + */ + readonly mutable?: boolean; +} + /** * IAM Role * @@ -130,22 +144,57 @@ export interface RoleProps { * the specified AWS service principal defined in `serviceAssumeRole`. */ export class Role extends Resource implements IRole { - /** - * Imports an external role by ARN + * Imports an external role by ARN. + * * @param scope construct scope * @param id construct id * @param roleArn the ARN of the role to import + * @param options allow customizing the behavior of the returned role */ - public static fromRoleArn(scope: Construct, id: string, roleArn: string): IRole { + public static fromRoleArn(scope: Construct, id: string, roleArn: string, options: FromRoleArnOptions = {}): IRole { + const scopeStack = Stack.of(scope); + const parsedArn = scopeStack.parseArn(roleArn); + const roleName = parsedArn.resourceName!; - class Import extends Resource implements IRole { + abstract class Import extends Resource implements IRole { public readonly grantPrincipal: IPrincipal = this; public readonly assumeRoleAction: string = 'sts:AssumeRole'; public readonly policyFragment = new ArnPrincipal(roleArn).policyFragment; public readonly roleArn = roleArn; - public readonly roleName = Stack.of(scope).parseArn(roleArn).resourceName!; + public readonly roleName = roleName; + + public abstract addToPolicy(statement: PolicyStatement): boolean; + + public abstract attachInlinePolicy(policy: Policy): void; + public addManagedPolicy(_policy: IManagedPolicy): void { + // FIXME: Add warning that we're ignoring this + } + + /** + * Grant permissions to the given principal to pass this role. + */ + public grantPassRole(identity: IPrincipal): Grant { + return this.grant(identity, 'iam:PassRole'); + } + + /** + * Grant the actions defined in actions to the identity Principal on this resource. + */ + public grant(grantee: IPrincipal, ...actions: string[]): Grant { + return Grant.addToPrincipal({ + grantee, + actions, + resourceArns: [this.roleArn], + scope: this, + }); + } + } + + const roleAccount = parsedArn.account; + + class MutableImport extends Import { private readonly attachedPolicies = new AttachedPolicies(); private defaultPolicy?: Policy; @@ -159,36 +208,36 @@ export class Role extends Resource implements IRole { } public attachInlinePolicy(policy: Policy): void { - this.attachedPolicies.attach(policy); - policy.attachToRole(this); - } + const policyAccount = Stack.of(policy).account; - public addManagedPolicy(_policy: IManagedPolicy): void { - // FIXME: Add warning that we're ignoring this + if (accountsAreEqualOrOneIsUnresolved(policyAccount, roleAccount)) { + this.attachedPolicies.attach(policy); + policy.attachToRole(this); + } } + } - /** - * Grant the actions defined in actions to the identity Principal on this resource. - */ - public grant(grantee: IPrincipal, ...actions: string[]): Grant { - return Grant.addToPrincipal({ - grantee, - actions, - resourceArns: [this.roleArn], - scope: this - }); + class ImmutableImport extends Import { + public addToPolicy(_statement: PolicyStatement): boolean { + return false; } - /** - * Grant permissions to the given principal to pass this role. - */ - public grantPassRole(identity: IPrincipal): Grant { - return this.grant(identity, 'iam:PassRole'); + public attachInlinePolicy(_policy: Policy): void { + // do nothing } } - return new Import(scope, id); + const scopeAccount = scopeStack.account; + + return options.mutable !== false && accountsAreEqualOrOneIsUnresolved(scopeAccount, roleAccount) + ? new MutableImport(scope, id) + : new ImmutableImport(scope, id); + function accountsAreEqualOrOneIsUnresolved(account1: string | undefined, + account2: string | undefined): boolean { + return Token.isUnresolved(account1) || Token.isUnresolved(account2) || + account1 === account2; + } } public readonly grantPrincipal: IPrincipal = this; diff --git a/packages/@aws-cdk/aws-iam/package.json b/packages/@aws-cdk/aws-iam/package.json index 0775b14db8e26..c699d7561c68d 100644 --- a/packages/@aws-cdk/aws-iam/package.json +++ b/packages/@aws-cdk/aws-iam/package.json @@ -96,6 +96,7 @@ }, "awslint": { "exclude": [ + "from-signature:@aws-cdk/aws-iam.Role.fromRoleArn", "construct-interface-extends-iconstruct:@aws-cdk/aws-iam.IManagedPolicy", "resource-interface-extends-resource:@aws-cdk/aws-iam.IManagedPolicy" ] diff --git a/packages/@aws-cdk/aws-iam/test/role.from-role-arn.test.ts b/packages/@aws-cdk/aws-iam/test/role.from-role-arn.test.ts new file mode 100644 index 0000000000000..4f69d8fde13f6 --- /dev/null +++ b/packages/@aws-cdk/aws-iam/test/role.from-role-arn.test.ts @@ -0,0 +1,533 @@ +import '@aws-cdk/assert/jest'; +import { App, CfnElement, Lazy, Stack } from "@aws-cdk/core"; +import { AnyPrincipal, ArnPrincipal, IRole, Policy, PolicyStatement, Role } from "../lib"; + +// tslint:disable:object-literal-key-quotes + +const roleAccount = '123456789012'; +const notRoleAccount = '012345678901'; + +describe('IAM Role.fromRoleArn', () => { + let app: App; + + beforeEach(() => { + app = new App(); + }); + + let roleStack: Stack; + let importedRole: IRole; + + describe('imported with a static ARN', () => { + const roleName = 'MyRole'; + + describe('into an env-agnostic stack', () => { + beforeEach(() => { + roleStack = new Stack(app, 'RoleStack'); + importedRole = Role.fromRoleArn(roleStack, 'ImportedRole', + `arn:aws:iam::${roleAccount}:role/${roleName}`); + }); + + test('correctly parses the imported role ARN', () => { + expect(importedRole.roleArn).toBe(`arn:aws:iam::${roleAccount}:role/${roleName}`); + }); + + test('correctly parses the imported role name', () => { + expect(importedRole.roleName).toBe(roleName); + }); + + describe('then adding a PolicyStatement to it', () => { + let addToPolicyResult: boolean; + + beforeEach(() => { + addToPolicyResult = importedRole.addToPolicy(somePolicyStatement()); + }); + + test('returns true', () => { + expect(addToPolicyResult).toBe(true); + }); + + test("generates a default Policy resource pointing at the imported role's physical name", () => { + assertRoleHasDefaultPolicy(roleStack, roleName); + }); + }); + + describe('then attaching a Policy to it', () => { + let policyStack: Stack; + + describe('that belongs to the same stack as the imported role', () => { + beforeEach(() => { + importedRole.attachInlinePolicy(somePolicy(roleStack, 'MyPolicy')); + }); + + test("correctly attaches the Policy to the imported role", () => { + assertRoleHasAttachedPolicy(roleStack, roleName, 'MyPolicy'); + }); + }); + + describe('that belongs to a different env-agnostic stack', () => { + beforeEach(() => { + policyStack = new Stack(app, 'PolicyStack'); + importedRole.attachInlinePolicy(somePolicy(policyStack, 'MyPolicy')); + }); + + test("correctly attaches the Policy to the imported role", () => { + assertRoleHasAttachedPolicy(policyStack, roleName, 'MyPolicy'); + }); + }); + + describe('that belongs to a targeted stack, with account set to', () => { + describe('the same account as in the ARN of the imported role', () => { + beforeEach(() => { + policyStack = new Stack(app, 'PolicyStack', { env: { account: roleAccount } }); + importedRole.attachInlinePolicy(somePolicy(policyStack, 'MyPolicy')); + }); + + test("correctly attaches the Policy to the imported role", () => { + assertRoleHasAttachedPolicy(policyStack, roleName, 'MyPolicy'); + }); + }); + + describe('a different account than in the ARN of the imported role', () => { + beforeEach(() => { + policyStack = new Stack(app, 'PolicyStack', { env: { account: notRoleAccount } }); + importedRole.attachInlinePolicy(somePolicy(policyStack, 'MyPolicy')); + }); + + test("does NOT attach the Policy to the imported role", () => { + assertPolicyDidNotAttachToRole(policyStack, 'MyPolicy'); + }); + }); + }); + }); + }); + + describe('into a targeted stack with account set to', () => { + describe('the same account as in the ARN the role was imported with', () => { + beforeEach(() => { + roleStack = new Stack(app, 'RoleStack', { env: { account: roleAccount } }); + importedRole = Role.fromRoleArn(roleStack, 'ImportedRole', + `arn:aws:iam::${roleAccount}:role/${roleName}`); + }); + + describe('then adding a PolicyStatement to it', () => { + let addToPolicyResult: boolean; + + beforeEach(() => { + addToPolicyResult = importedRole.addToPolicy(somePolicyStatement()); + }); + + test('returns true', () => { + expect(addToPolicyResult).toBe(true); + }); + + test("generates a default Policy resource pointing at the imported role's physical name", () => { + assertRoleHasDefaultPolicy(roleStack, roleName); + }); + }); + + describe('then attaching a Policy to it', () => { + describe('that belongs to the same stack as the imported role', () => { + beforeEach(() => { + importedRole.attachInlinePolicy(somePolicy(roleStack, 'MyPolicy')); + }); + + test("correctly attaches the Policy to the imported role", () => { + assertRoleHasAttachedPolicy(roleStack, roleName, 'MyPolicy'); + }); + }); + + describe('that belongs to an env-agnostic stack', () => { + let policyStack: Stack; + + beforeEach(() => { + policyStack = new Stack(app, 'PolicyStack'); + importedRole.attachInlinePolicy(somePolicy(policyStack, 'MyPolicy')); + }); + + test("correctly attaches the Policy to the imported role", () => { + assertRoleHasAttachedPolicy(policyStack, roleName, 'MyPolicy'); + }); + }); + + describe('that belongs to a targeted stack, with account set to', () => { + let policyStack: Stack; + + describe('the same account as in the imported role ARN and in the stack the imported role belongs to', () => { + beforeEach(() => { + policyStack = new Stack(app, 'PolicyStack', { env: { account: roleAccount } }); + importedRole.attachInlinePolicy(somePolicy(policyStack, 'MyPolicy')); + }); + + test("correctly attaches the Policy to the imported role", () => { + assertRoleHasAttachedPolicy(policyStack, roleName, 'MyPolicy'); + }); + }); + + describe('a different account than in the imported role ARN and in the stack the imported role belongs to', () => { + beforeEach(() => { + policyStack = new Stack(app, 'PolicyStack', { env: { account: notRoleAccount } }); + importedRole.attachInlinePolicy(somePolicy(policyStack, 'MyPolicy')); + }); + + test("does NOT attach the Policy to the imported role", () => { + assertPolicyDidNotAttachToRole(policyStack, 'MyPolicy'); + }); + }); + }); + }); + }); + + describe('a different account than in the ARN the role was imported with', () => { + beforeEach(() => { + roleStack = new Stack(app, 'RoleStack', { env: { account: notRoleAccount } }); + importedRole = Role.fromRoleArn(roleStack, 'ImportedRole', + `arn:aws:iam::${roleAccount}:role/${roleName}`); + }); + + describe('then adding a PolicyStatement to it', () => { + let addToPolicyResult: boolean; + + beforeEach(() => { + addToPolicyResult = importedRole.addToPolicy(somePolicyStatement()); + }); + + test('returns false', () => { + expect(addToPolicyResult).toBe(false); + }); + + test("does NOT generate a default Policy resource pointing at the imported role's physical name", () => { + expect(roleStack).not.toHaveResourceLike('AWS::IAM::Policy'); + }); + }); + + describe('then attaching a Policy to it', () => { + describe('that belongs to the same stack as the imported role', () => { + beforeEach(() => { + importedRole.attachInlinePolicy(somePolicy(roleStack, 'MyPolicy')); + }); + + test("does NOT attach the Policy to the imported role", () => { + assertPolicyDidNotAttachToRole(roleStack, 'MyPolicy'); + }); + }); + + describe('that belongs to an env-agnostic stack', () => { + let policyStack: Stack; + + beforeEach(() => { + policyStack = new Stack(app, 'PolicyStack'); + importedRole.attachInlinePolicy(somePolicy(policyStack, 'MyPolicy')); + }); + + test("does NOT attach the Policy to the imported role", () => { + assertPolicyDidNotAttachToRole(policyStack, 'MyPolicy'); + }); + }); + + describe('that belongs to a different targeted stack, with account set to', () => { + let policyStack: Stack; + + describe('the same account as in the ARN of the imported role', () => { + beforeEach(() => { + policyStack = new Stack(app, 'PolicyStack', { env: { account: roleAccount } }); + importedRole.attachInlinePolicy(somePolicy(policyStack, 'MyPolicy')); + }); + + test("does NOT attach the Policy to the imported role", () => { + assertPolicyDidNotAttachToRole(policyStack, 'MyPolicy'); + }); + }); + + describe('the same account as in the stack the imported role belongs to', () => { + beforeEach(() => { + policyStack = new Stack(app, 'PolicyStack', { env: { account: notRoleAccount } }); + importedRole.attachInlinePolicy(somePolicy(policyStack, 'MyPolicy')); + }); + + test("does NOT attach the Policy to the imported role", () => { + assertPolicyDidNotAttachToRole(policyStack, 'MyPolicy'); + }); + }); + + describe('a third account, different from both the role and scope stack accounts', () => { + beforeEach(() => { + policyStack = new Stack(app, 'PolicyStack', { env: { account: 'some-random-account' } }); + importedRole.attachInlinePolicy(somePolicy(policyStack, 'MyPolicy')); + }); + + test("does NOT attach the Policy to the imported role", () => { + assertPolicyDidNotAttachToRole(policyStack, 'MyPolicy'); + }); + }); + }); + }); + }); + }); + + describe('and with mutable=false', () => { + beforeEach(() => { + roleStack = new Stack(app, 'RoleStack'); + importedRole = Role.fromRoleArn(roleStack, 'ImportedRole', + `arn:aws:iam::${roleAccount}:role/${roleName}`, { mutable: false }); + }); + + describe('then adding a PolicyStatement to it', () => { + let addToPolicyResult: boolean; + + beforeEach(() => { + addToPolicyResult = importedRole.addToPolicy(somePolicyStatement()); + }); + + test('returns false', () => { + expect(addToPolicyResult).toBe(false); + }); + + test("does NOT generate a default Policy resource pointing at the imported role's physical name", () => { + expect(roleStack).not.toHaveResourceLike('AWS::IAM::Policy'); + }); + }); + + describe('then attaching a Policy to it', () => { + let policyStack: Stack; + + describe('that belongs to a stack with account equal to the account in the imported role ARN', () => { + beforeEach(() => { + policyStack = new Stack(app, 'PolicyStack', { env: { account : roleAccount } }); + importedRole.attachInlinePolicy(somePolicy(policyStack, 'MyPolicy')); + }); + + test("does NOT attach the Policy to the imported role", () => { + assertPolicyDidNotAttachToRole(policyStack, 'MyPolicy'); + }); + }); + }); + }); + }); + + describe('imported with a dynamic ARN', () => { + const dynamicValue = Lazy.stringValue({ produce: () => 'role-arn' }); + const roleName: any = { + "Fn::Select": [1, + { + "Fn::Split": ["/", + { + "Fn::Select": [5, + { "Fn::Split": [":", "role-arn"] }, + ] + }, + ], + }, + ], + }; + + describe('into an env-agnostic stack', () => { + beforeEach(() => { + roleStack = new Stack(app, 'RoleStack'); + importedRole = Role.fromRoleArn(roleStack, 'ImportedRole', dynamicValue); + }); + + test('correctly parses the imported role ARN', () => { + expect(importedRole.roleArn).toBe(dynamicValue); + }); + + test('correctly parses the imported role name', () => { + new Role(roleStack, 'AnyRole', { + roleName: 'AnyRole', + assumedBy: new ArnPrincipal(importedRole.roleName), + }); + + expect(roleStack).toHaveResourceLike('AWS::IAM::Role', { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": roleName, + }, + }, + ], + }, + }); + }); + + describe('then adding a PolicyStatement to it', () => { + let addToPolicyResult: boolean; + + beforeEach(() => { + addToPolicyResult = importedRole.addToPolicy(somePolicyStatement()); + }); + + test('returns true', () => { + expect(addToPolicyResult).toBe(true); + }); + + test("generates a default Policy resource pointing at the imported role's physical name", () => { + assertRoleHasDefaultPolicy(roleStack, roleName); + }); + }); + + describe('then attaching a Policy to it', () => { + let policyStack: Stack; + + describe('that belongs to the same stack as the imported role', () => { + beforeEach(() => { + importedRole.attachInlinePolicy(somePolicy(roleStack, 'MyPolicy')); + }); + + test("correctly attaches the Policy to the imported role", () => { + assertRoleHasAttachedPolicy(roleStack, roleName, 'MyPolicy'); + }); + }); + + describe('that belongs to a different env-agnostic stack', () => { + beforeEach(() => { + policyStack = new Stack(app, 'PolicyStack'); + importedRole.attachInlinePolicy(somePolicy(policyStack, 'MyPolicy')); + }); + + test("correctly attaches the Policy to the imported role", () => { + assertRoleHasAttachedPolicy(policyStack, roleName, 'MyPolicy'); + }); + }); + + describe('that belongs to a targeted stack', () => { + beforeEach(() => { + policyStack = new Stack(app, 'PolicyStack', { env: { account: roleAccount } }); + importedRole.attachInlinePolicy(somePolicy(policyStack, 'MyPolicy')); + }); + + test("correctly attaches the Policy to the imported role", () => { + assertRoleHasAttachedPolicy(policyStack, roleName, 'MyPolicy'); + }); + }); + }); + }); + + describe('into a targeted stack with account set', () => { + beforeEach(() => { + roleStack = new Stack(app, 'RoleStack', { env: { account: roleAccount } }); + importedRole = Role.fromRoleArn(roleStack, 'ImportedRole', dynamicValue); + }); + + describe('then adding a PolicyStatement to it', () => { + let addToPolicyResult: boolean; + + beforeEach(() => { + addToPolicyResult = importedRole.addToPolicy(somePolicyStatement()); + }); + + test('returns true', () => { + expect(addToPolicyResult).toBe(true); + }); + + test("generates a default Policy resource pointing at the imported role's physical name", () => { + assertRoleHasDefaultPolicy(roleStack, roleName); + }); + }); + + describe('then attaching a Policy to it', () => { + let policyStack: Stack; + + describe('that belongs to the same stack as the imported role', () => { + beforeEach(() => { + importedRole.attachInlinePolicy(somePolicy(roleStack, 'MyPolicy')); + }); + + test("correctly attaches the Policy to the imported role", () => { + assertRoleHasAttachedPolicy(roleStack, roleName, 'MyPolicy'); + }); + }); + + describe('that belongs to an env-agnostic stack', () => { + beforeEach(() => { + policyStack = new Stack(app, 'PolicyStack'); + importedRole.attachInlinePolicy(somePolicy(policyStack, 'MyPolicy')); + }); + + test("correctly attaches the Policy to the imported role", () => { + assertRoleHasAttachedPolicy(policyStack, roleName, 'MyPolicy'); + }); + }); + + describe('that belongs to a different targeted stack, with account set to', () => { + describe('the same account as the stack the role was imported into', () => { + beforeEach(() => { + policyStack = new Stack(app, 'PolicyStack', { env: { account: roleAccount } }); + importedRole.attachInlinePolicy(somePolicy(policyStack, 'MyPolicy')); + }); + + test("correctly attaches the Policy to the imported role", () => { + assertRoleHasAttachedPolicy(policyStack, roleName, 'MyPolicy'); + }); + }); + + describe('a different account than the stack the role was imported into', () => { + beforeEach(() => { + policyStack = new Stack(app, 'PolicyStack', { env: { account: notRoleAccount } }); + importedRole.attachInlinePolicy(somePolicy(policyStack, 'MyPolicy')); + }); + + test("correctly attaches the Policy to the imported role", () => { + assertRoleHasAttachedPolicy(policyStack, roleName, 'MyPolicy'); + }); + }); + }); + }); + }); + }); +}); + +function somePolicyStatement() { + return new PolicyStatement({ + actions: ['s3:*'], + resources: ['xyz'], + }); +} + +function somePolicy(policyStack: Stack, policyName: string) { + const someRole = new Role(policyStack, 'SomeExampleRole', { + assumedBy: new AnyPrincipal(), + }); + const roleResource = someRole.node.defaultChild as CfnElement; + roleResource.overrideLogicalId('SomeRole'); // force a particular logical ID in the Ref expression + + return new Policy(policyStack, 'MyPolicy', { + policyName, + statements: [somePolicyStatement()], + // need at least one of user/group/role, otherwise validation fails + roles: [someRole], + }); +} + +function assertRoleHasDefaultPolicy(stack: Stack, roleName: string) { + _assertStackContainsPolicyResource(stack, [roleName], undefined); +} + +function assertRoleHasAttachedPolicy(stack: Stack, roleName: string, attachedPolicyName: string) { + _assertStackContainsPolicyResource(stack, [{ Ref: 'SomeRole' }, roleName], attachedPolicyName); +} + +function assertPolicyDidNotAttachToRole(stack: Stack, policyName: string) { + _assertStackContainsPolicyResource(stack, [{ Ref: 'SomeRole' }], policyName); +} + +function _assertStackContainsPolicyResource(stack: Stack, roleNames: any[], nameOfPolicy: string | undefined) { + const expected: any = { + PolicyDocument: { + Statement: [ + { + Action: "s3:*", + Effect: "Allow", + Resource: "xyz", + }, + ], + }, + Roles: roleNames, + }; + if (nameOfPolicy) { + expected.PolicyName = nameOfPolicy; + } + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', expected); +} diff --git a/packages/@aws-cdk/aws-iam/test/role.test.ts b/packages/@aws-cdk/aws-iam/test/role.test.ts index d2331b701709a..9580b97556038 100644 --- a/packages/@aws-cdk/aws-iam/test/role.test.ts +++ b/packages/@aws-cdk/aws-iam/test/role.test.ts @@ -293,46 +293,6 @@ describe('IAM role', () => { }); }); - test('fromRoleArn', () => { - // GIVEN - const stack = new Stack(); - - // WHEN - const importedRole = Role.fromRoleArn(stack, 'ImportedRole', 'arn:aws:iam::123456789012:role/S3Access'); - - // THEN - expect(importedRole.roleArn).toEqual('arn:aws:iam::123456789012:role/S3Access'); - expect(importedRole.roleName).toEqual('S3Access'); - }); - - test('add policy to imported role', () => { - // GIVEN - const stack = new Stack(); - const importedRole = Role.fromRoleArn(stack, 'ImportedRole', 'arn:aws:iam::123456789012:role/MyRole'); - - // WHEN - importedRole.addToPolicy(new PolicyStatement({ - actions: ['s3:*'], - resources: ['xyz'] - })); - - // THEN - expect(stack).toHaveResource('AWS::IAM::Policy', { - PolicyDocument: { - Statement: [ - { - Action: "s3:*", - Effect: "Allow", - Resource: "xyz" - } - ], - Version: "2012-10-17" - }, - Roles: [ "MyRole" ] - }); - - }); - test('can supply permissions boundary managed policy', () => { // GIVEN const stack = new Stack();