diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index 783718303cea0..8817f44a80caf 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -559,6 +559,7 @@ export class Table extends Construct { private makeScalingRole(): iam.IRole { // Use a Service Linked Role. return iam.Role.import(this, 'ScalingRole', { + roleName: "DynamoDbAutoScalingRole", roleArn: cdk.ArnUtils.fromComponents({ // https://docs.aws.amazon.com/autoscaling/application/userguide/application-auto-scaling-service-linked-roles.html service: 'iam', diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index 351c90592a240..126aadc5be40b 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -227,6 +227,7 @@ export abstract class BaseService extends cdk.Construct private makeAutoScalingRole(): iam.IRole { // Use a Service Linked Role. return iam.Role.import(this, 'ScalingRole', { + roleName: 'ECSServiceAutoScalingRole', roleArn: cdk.ArnUtils.fromComponents({ service: 'iam', resource: 'role/aws-service-role/ecs.application-autoscaling.amazonaws.com', diff --git a/packages/@aws-cdk/aws-iam/README.md b/packages/@aws-cdk/aws-iam/README.md index edee5fdf403c0..aadbbd4fd10b2 100644 --- a/packages/@aws-cdk/aws-iam/README.md +++ b/packages/@aws-cdk/aws-iam/README.md @@ -16,6 +16,25 @@ Managed policies can be attached using `xxx.attachManagedPolicy(arn)`: ### Features - * Policy name uniqueness is enforced. If two policies by the same name are attached to the same +* Policy name uniqueness is enforced. If two policies by the same name are attached to the same principal, the attachment will fail. - * Policy names are not required - the CDK logical ID will be used and ensured to be unique. +* Policy names are not required - the CDK logical ID will be used and ensured to be unique. + +### Instance Profile + +[AWS::IAM::InstanceProfile](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-instanceprofile.html) + +```ts +import iam = require('@aws-cdk/aws-iam') + +const testRole = new iam.Role(stack, 'TestRole', { + assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com') +}); + +const instanceProfile = new iam.InstanceProfile(stack, 'InstanceProfile', { + instanceProfileName: "InstanceProfileName", + role: testRole, + path: "/" +}); + +``` \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/lib/index.ts b/packages/@aws-cdk/aws-iam/lib/index.ts index 2301ccd5b6ae8..fe072f94c254a 100644 --- a/packages/@aws-cdk/aws-iam/lib/index.ts +++ b/packages/@aws-cdk/aws-iam/lib/index.ts @@ -5,6 +5,8 @@ export * from './policy'; export * from './user'; export * from './group'; export * from './lazy-role'; +export * from './instance-profile'; +export * from './instance-profile-ref'; // AWS::IAM CloudFormation Resources: export * from './iam.generated'; diff --git a/packages/@aws-cdk/aws-iam/lib/instance-profile-ref.ts b/packages/@aws-cdk/aws-iam/lib/instance-profile-ref.ts new file mode 100644 index 0000000000000..632daa57b2952 --- /dev/null +++ b/packages/@aws-cdk/aws-iam/lib/instance-profile-ref.ts @@ -0,0 +1,110 @@ +import cdk = require('@aws-cdk/cdk'); +import { PolicyStatement } from './policy-document'; +import { IRole } from './role'; + +export interface InstanceProfileRefProps { + + /** + * The path associated with this instance profile. For information about IAM Instance Profiles, see + * Friendly Names and Paths in IAM User Guide. + * @link http://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html#Identifiers_FriendlyNames + * @default / By default, AWS CloudFormation specifies '/' for the path. + */ + path: string; + + /** + * The name of an existing IAM role to associate with this instance profile. + * Currently, you can assign a maximum of one role to an instance profile. + * @default Role a default Role with ServicePrincipal(ec2.amazonaws.com). + */ + role: IRole; + + /** + * The name of the instance profile that you want to create. + * This parameter allows (per its regex pattern) a string consisting of + * upper and lowercase alphanumeric characters with no spaces. + * You can also include any of the following characters: = , . @ - + * @default none instance profile name does not have a default value. + */ + instanceProfileName?: string; + +} + +export abstract class InstanceProfileRef extends cdk.Construct { + + /** + * Creates an IAM Instance Profile object which represents an + * instance profile not defined within this stack. + * + * `InstanceProfile.import(this, 'MyImportedInstanceProfile', {})` + * + * @param parent The parent construct + * @param name The name for the CloudFormation InstanceProfile Element. Note: this is not the same as the + * InstanceProfileName. + * @param ref A reference to an Instance Profile. Can be created manually (see example above) or + * obtained through a call to `instanceProfile.export()` + * @returns InstanceProfileRef + */ + public static import(parent: cdk.Construct, name: string, ref: InstanceProfileRefProps): InstanceProfileRef { + return new InstanceProfileRefImport(parent, name, ref); + } + + /** + * Path for the Instance Profile + * @link http://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html#Identifiers_FriendlyNames + * @default / If a path is not provided, CloudFormation defaults the path to '/'. + */ + public abstract readonly path: string; + + /** + * The name of an existing IAM role to associate with this instance profile. + * Currently, you can assign a maximum of one role to an instance profile. + * @default Role a default Role with ServicePrincipal(ec2.amazonaws.com). + */ + public abstract readonly role: IRole; + + /** + * The name of the instance profile that you want to create. + * This parameter allows (per its regex pattern) a string consisting of + * upper and lowercase alphanumeric characters with no spaces. + * You can also include any of the following characters: = , . @ - + * @default none instance profile name does not have a default value. + */ + public abstract readonly instanceProfileName?: string; + + /** + * Adds a PolicyStatement to the Role associated with this InstanceProfile + * @param statement the statement to add + */ + public abstract addToRolePolicy(statement: PolicyStatement): void; + + /** + * Exports this InstanceProfile + */ + public export(): InstanceProfileRefProps { + return { + path: this.path, + role: this.role, + instanceProfileName: this.instanceProfileName + }; + } + +} + +class InstanceProfileRefImport extends InstanceProfileRef { + public readonly path: string; + public readonly role: IRole; + public readonly instanceProfileName?: string; + + constructor(parent: cdk.Construct, name: string, props: InstanceProfileRefProps) { + super(parent, name); + this.path = props.path; + this.role = props.role; + this.instanceProfileName = props.instanceProfileName; + } + + public addToRolePolicy(_statement: PolicyStatement) { + // FIXME: Add warning that we're ignoring this + } + +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/lib/instance-profile.ts b/packages/@aws-cdk/aws-iam/lib/instance-profile.ts new file mode 100644 index 0000000000000..cc6e86e529beb --- /dev/null +++ b/packages/@aws-cdk/aws-iam/lib/instance-profile.ts @@ -0,0 +1,73 @@ +import { Construct } from '@aws-cdk/cdk'; +import { cloudformation } from './iam.generated'; +import { InstanceProfileRef } from './instance-profile-ref'; +import { PolicyStatement, ServicePrincipal } from './policy-document'; +import { IRole, Role } from './role'; +export interface InstanceProfileProps { + + /** + * The path associated with this instance profile. For information about IAM Instance Profiles, see + * Friendly Names and Paths in IAM User Guide. + * @link http://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html#Identifiers_FriendlyNames + * @default / By default, AWS CloudFormation specifies '/' for the path. + */ + path?: string; + + /** + * The name of an existing IAM role to associate with this instance profile. + * Currently, you can assign a maximum of one role to an instance profile. + * @default Role a default Role with ServicePrincipal(ec2.amazonaws.com). + */ + role?: IRole; + + /** + * The name of the instance profile that you want to create. + * This parameter allows (per its regex pattern) a string consisting of + * upper and lowercase alphanumeric characters with no spaces. + * You can also include any of the following characters: = , . @ - + * @default none instance profile name does not have a default value. + */ + instanceProfileName?: string; + +} + +/** + * IAM Instance Profile + * + * Defines an IAM Instance Profile that can be used with IAM roles for EC2 instances. + * @link https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-instanceprofile.html + */ +export class InstanceProfile extends InstanceProfileRef { + + public readonly path: string; + public readonly role: IRole; + public readonly instanceProfileName?: string; + + constructor(parent: Construct, name: string, props: InstanceProfileProps) { + super(parent, name); + + this.role = props.role || new Role(this, 'EC2Role', { + assumedBy: new ServicePrincipal('ec2.amazonaws.com') + }); + + this.path = props.path || "/"; + const resource = new cloudformation.InstanceProfileResource(this, 'Resource', { + roles: [ this.role.roleName ], + path: this.path, + instanceProfileName: props.instanceProfileName + }); + this.instanceProfileName = resource.instanceProfileName; + } + + /** + * Adds a PolicyStatement to the Role associated with this InstanceProfile + * @param statement the statement to add + */ + public addToRolePolicy(statement: PolicyStatement) { + if (!this.role) { + return; + } + this.role.addToPolicy(statement); + } + +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/lib/lazy-role.ts b/packages/@aws-cdk/aws-iam/lib/lazy-role.ts index e9dcd7e160404..659cb9473ff18 100644 --- a/packages/@aws-cdk/aws-iam/lib/lazy-role.ts +++ b/packages/@aws-cdk/aws-iam/lib/lazy-role.ts @@ -73,6 +73,13 @@ export class LazyRole extends cdk.Construct implements IRole { return this.instantiate().roleArn; } + /** + * Returns the Name of this role. + */ + public get roleName(): string { + return this.instantiate().roleName; + } + /** * Returns a Principal object representing the ARN of this role. */ diff --git a/packages/@aws-cdk/aws-iam/lib/role.ts b/packages/@aws-cdk/aws-iam/lib/role.ts index 65da3d1fe9cae..d433ef4fc8c8e 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, IDependable } from '@aws-cdk/cdk'; +import { ArnUtils, Construct, IDependable, Output, unresolved } from '@aws-cdk/cdk'; import { cloudformation } from './iam.generated'; import { IPrincipal, Policy } from './policy'; import { ArnPrincipal, PolicyDocument, PolicyPrincipal, PolicyStatement } from './policy-document'; @@ -182,6 +182,14 @@ export class Role extends Construct implements IRole { this.attachedPolicies.attach(policy); policy.attachToRole(this); } + + public export(): ImportedRoleProps { + return { + roleArn: new Output(this, 'RoleArn', { value: this.roleArn }).makeImportValue().toString(), + roleName: new Output(this, 'RoleName', { value: this.roleName }).makeImportValue().toString() + }; + } + } /** @@ -192,6 +200,11 @@ export interface IRole extends IPrincipal, IDependable { * Returns the ARN of this role. */ readonly roleArn: string; + + /** + * Returns this role's Name. + */ + readonly roleName: string; } function createAssumeRolePolicy(principal: PolicyPrincipal) { @@ -218,7 +231,12 @@ export interface ImportedRoleProps { /** * The role's ARN */ - roleArn: string; + roleArn?: string; + + /** + * The role's Name + */ + roleName?: string; } /** @@ -226,12 +244,32 @@ export interface ImportedRoleProps { */ class ImportedRole extends Construct implements IRole { public readonly roleArn: string; + public readonly roleName: string; public readonly principal: PolicyPrincipal; public readonly dependencyElements: IDependable[] = []; constructor(parent: Construct, id: string, props: ImportedRoleProps) { super(parent, id); - this.roleArn = props.roleArn; + if (props.roleArn) { + this.roleArn = props.roleArn; + } else { + if (!props.roleName) { + throw new Error('If "roleArn" is not specified, you must specify "roleName"'); + } + this.roleArn = ArnUtils.fromComponents({ + service: 'iam', + resource: 'role' + }); + } + + if (props.roleName) { + this.roleName = props.roleName; + } else { + if (unresolved(this.roleArn)) { + throw new Error('roleArn is a late-bound value, and therefore roleName is required'); + } + this.roleName = this.roleArn.split('/').slice(1).join('/'); + } this.principal = new ArnPrincipal(this.roleArn); } diff --git a/packages/@aws-cdk/aws-iam/test/test.instance-profile.ts b/packages/@aws-cdk/aws-iam/test/test.instance-profile.ts new file mode 100644 index 0000000000000..1af2850d5b17e --- /dev/null +++ b/packages/@aws-cdk/aws-iam/test/test.instance-profile.ts @@ -0,0 +1,292 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import { Construct } from '@aws-cdk/cdk'; +import { Stack } from '@aws-cdk/cdk'; +import assert = require('assert'); +import { Test } from 'nodeunit'; +import { InstanceProfile, + InstanceProfileRef, + PolicyStatement, + Role, + ServicePrincipal +} from '../lib'; + +/** + * Test Cases: + * parameters: role + * parameters: role, path + * parameters: role, path, instanceProfileName + * parameters: role, instanceProfileName + * parameters: path, instanceProfileName + * parameters: path + * parameters: instanceProfileName + * no parameters + * role other than ec2 + * path other than / + * addRoleToPolicy + * import/export + */ +export = { + 'parameters: role'(test: Test) { + const stack = new Stack(); + + const testEc2Role = new Role(stack, 'TestEC2Role', { + assumedBy: new ServicePrincipal('ec2.amazonaws.com') + }); + + new InstanceProfile(stack, 'TestEC2InstanceProfile', { + role: testEc2Role, + }); + expect(stack).to(haveResource('AWS::IAM::InstanceProfile', { + Roles: [ + { Ref: "TestEC2RoleBD27AEF4" } + ], + Path: '/' + })); + test.done(); + }, + + 'parameters: role, path'(test: Test) { + const stack = new Stack(); + + const testEc2Role = new Role(stack, 'TestEC2Role', { + assumedBy: new ServicePrincipal('ec2.amazonaws.com') + }); + + new InstanceProfile(stack, 'TestEC2InstanceProfile', { + role: testEc2Role, + path: "/" + }); + expect(stack).to(haveResource('AWS::IAM::InstanceProfile', { + Roles: [ + { Ref: "TestEC2RoleBD27AEF4" } + ], + Path: '/' + })); + test.done(); + }, + + 'parameters: role, path, instanceProfileName'(test: Test) { + const stack = new Stack(); + + const testEc2Role = new Role(stack, 'TestEC2Role', { + assumedBy: new ServicePrincipal('ec2.amazonaws.com') + }); + + new InstanceProfile(stack, 'TestEC2InstanceProfile', { + instanceProfileName: "TestInstanceProfile", + role: testEc2Role, + path: "/" + }); + expect(stack).to(haveResource('AWS::IAM::InstanceProfile', { + Roles: [ + { Ref: "TestEC2RoleBD27AEF4" } + ], + InstanceProfileName: 'TestInstanceProfile', + Path: '/' + })); + test.done(); + }, + + 'parameters: role, instanceProfileName'(test: Test) { + const stack = new Stack(); + + const testEc2Role = new Role(stack, 'TestEC2Role', { + assumedBy: new ServicePrincipal('ec2.amazonaws.com') + }); + + new InstanceProfile(stack, 'TestEC2InstanceProfile', { + instanceProfileName: "TestInstanceProfile", + role: testEc2Role, + }); + expect(stack).to(haveResource('AWS::IAM::InstanceProfile', { + Roles: [ + { Ref: "TestEC2RoleBD27AEF4" } + ], + InstanceProfileName: 'TestInstanceProfile', + Path: '/' + })); + test.done(); + }, + + 'parameters: path, instanceProfileName'(test: Test) { + const stack = new Stack(); + + new InstanceProfile(stack, 'TestEC2InstanceProfile', { + instanceProfileName: "TestInstanceProfile", + path: '/' + }); + expect(stack).to(haveResource('AWS::IAM::InstanceProfile', { + Roles: [ + { Ref: "TestEC2InstanceProfileEC2Role5106A065" } + ], + InstanceProfileName: 'TestInstanceProfile', + Path: '/' + })); + test.done(); + }, + + 'parameters: instanceProfileName'(test: Test) { + const stack = new Stack(); + + new InstanceProfile(stack, 'TestEC2InstanceProfile', { + instanceProfileName: "TestInstanceProfile" + }); + expect(stack).to(haveResource('AWS::IAM::InstanceProfile', { + Roles: [ + { Ref: "TestEC2InstanceProfileEC2Role5106A065" } + ], + InstanceProfileName: 'TestInstanceProfile', + Path: '/' + })); + test.done(); + }, + + 'parameters: path'(test: Test) { + const stack = new Stack(); + + new InstanceProfile(stack, 'TestEC2InstanceProfile', { + path: '/' + }); + expect(stack).to(haveResource('AWS::IAM::InstanceProfile', { + Roles: [ + { Ref: "TestEC2InstanceProfileEC2Role5106A065" } + ], + Path: '/' + })); + test.done(); + }, + + 'no parameters supplied'(test: Test) { + const stack = new Stack(); + + new InstanceProfile(stack, 'NakedTestEC2InstanceProfile', {}); + expect(stack).to(haveResource('AWS::IAM::InstanceProfile', { + Roles: [ + { Ref: "NakedTestEC2InstanceProfileEC2Role59DF0EED" } + ], + Path: '/' + })); + test.done(); + }, + + 'role other than ec2'(test: Test) { + const stack = new Stack(); + + const testEc2Role = new Role(stack, 'TestEC2SNSRole', { + assumedBy: new ServicePrincipal('sns.amazonaws.com') + }); + + new InstanceProfile(stack, 'TestEC2InstanceProfile', { + role: testEc2Role + }); + + expect(stack).to(haveResource('AWS::IAM::InstanceProfile', { + Roles: [ + { Ref: "TestEC2SNSRole3159CBC5" } + ], + Path: '/' + })); + test.done(); + }, + + 'custom path supplied'(test: Test) { + const stack = new Stack(); + + const testEc2Role = new Role(stack, 'TestEC2Role', { + assumedBy: new ServicePrincipal('ec2.amazonaws.com') + }); + + const customPath = '/TestEC2Role/CustomPath'; + + new InstanceProfile(stack, 'TestEC2InstanceProfile', { + role: testEc2Role, + path: customPath + }); + expect(stack).to(haveResource('AWS::IAM::InstanceProfile', { + Roles: [ + { Ref: "TestEC2RoleBD27AEF4" } + ], + Path: '/TestEC2Role/CustomPath' + })); + test.done(); + }, + + 'addToRolePolicy adds a policy'(test: Test) { + const stack = new Stack(); + + const testEc2Role = new Role(stack, 'TestEC2Role', { + assumedBy: new ServicePrincipal('ec2.amazonaws.com') + }); + + const instanceProfile = new InstanceProfile(stack, 'TestEC2InstanceProfile', { + role: testEc2Role + }); + + instanceProfile.addToRolePolicy( + new PolicyStatement().addAction('confirm:itsthesame') + ); + + expect(stack).to(haveResource('AWS::IAM::InstanceProfile', { + Roles: [ + { Ref: "TestEC2RoleBD27AEF4" } + ], + Path: '/' + })); + + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: "confirm:itsthesame", + Effect: "Allow" + } + ], + Version: "2012-10-17" + }, + PolicyName: "TestEC2RoleDefaultPolicyD19D9935", + Roles: [ + { Ref: "TestEC2RoleBD27AEF4" } + ] + })); + test.done(); + }, + 'import/export': { + 'default ec2 role created'(test: Test) { + const stack = new Stack(); + + new InstanceProfile(stack, 'TestEC2InstanceProfile', {}); + expect(stack).to(haveResource('AWS::IAM::InstanceProfile', { + Roles: [ + { Ref: "TestEC2InstanceProfileEC2Role5106A065" } + ], + Path: '/' + })); + test.done(); + }, + + 'instanceProfile.export() can be used to add Outputs to the stack and returns a InstanceProfileRef object'(test: Test) { + // GIVEN + const stack1 = new Stack(); + const stack2 = new Stack(); + const testInstanceProfile1 = newTestInstanceProfile(stack1, 'TestInstanceProfile1', "/TestInstanceProfilePath"); + + // WHEN + const props = testInstanceProfile1.export(); + const imported = InstanceProfileRef.import(stack2, 'Imported', props); + // test imported values are expected + assert.equal(imported.path, "/TestInstanceProfilePath", "imported path value was not an expected value"); + assert.equal(imported.uniqueId, "Imported", "unique id value was not an expected value"); + // test addRoleToPolicy on imported + imported.addToRolePolicy( + new PolicyStatement().addAction('confirm:itsthesame') + ); + test.done(); + }, + } +}; + +function newTestInstanceProfile(parent: Construct, name: string, pathName: string) { + return new InstanceProfile(parent, name, { + path: pathName + }); +} diff --git a/packages/@aws-cdk/aws-iam/test/test.role.ts b/packages/@aws-cdk/aws-iam/test/test.role.ts index 66209cfe1cb3d..14057a332e0c9 100644 --- a/packages/@aws-cdk/aws-iam/test/test.role.ts +++ b/packages/@aws-cdk/aws-iam/test/test.role.ts @@ -1,8 +1,9 @@ import { expect, haveResource } from '@aws-cdk/assert'; -import { Resource, Stack } from '@aws-cdk/cdk'; +import { resolve, Resource, Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { FederatedPrincipal, PolicyStatement, Role, ServicePrincipal } from '../lib'; +/* tslint:disable:no-console */ export = { 'default role'(test: Test) { const stack = new Stack(); @@ -24,6 +25,28 @@ export = { test.done(); }, + 'role with name'(test: Test) { + const stack = new Stack(); + + new Role(stack, 'MyRole', { + assumedBy: new ServicePrincipal('sns.amazonaws.com'), + roleName: "RollWithName" + }); + + expect(stack).toMatch({ Resources: + { MyRoleF48FFE04: + { Type: 'AWS::IAM::Role', + Properties: + { AssumeRolePolicyDocument: + { Statement: + [ { Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { Service: 'sns.amazonaws.com' } } ], + Version: '2012-10-17' }, + RoleName: "RollWithName" } } } }); + test.done(); + }, + 'policy is created automatically when permissions are added'(test: Test) { const stack = new Stack(); @@ -175,6 +198,55 @@ export = { test.done(); } - } + }, + 'import/export': { + + 'import with name'(test: Test) { + const stack = new Stack(); + const stack2 = new Stack(); + + const role = new Role(stack, 'TestRole', { + assumedBy: new ServicePrincipal('bla'), + roleName: 'RoleImportMe' + }); + + const role2 = Role.import(stack2, 'SecondStack', role.export()); + + test.deepEqual(resolve(role2.roleArn), { + 'Fn::ImportValue': 'TestRoleRoleArn4584711D' + }); + + test.deepEqual(resolve(role2.roleName), { + 'Fn::ImportValue': 'TestRoleRoleNameA06B1198' + }); + + test.done(); + } + }, + + 'import with arn'(test: Test) { + const stack = new Stack(); + const stack2 = new Stack(); + + new Role(stack, 'TestRole', { + assumedBy: new ServicePrincipal('bla') + }); + + const role2 = Role.import(stack2, 'SecondStack', { + roleArn: 'arn:aws:iam::123456789012:role/service-role/test-service-role' + }); + + test.deepEqual(resolve(role2.roleArn), 'arn:aws:iam::123456789012:role/service-role/test-service-role'); + test.deepEqual(resolve(role2.roleName), 'service-role/test-service-role'); + + test.done(); + }, + + 'fails with no arn or name'(test: Test) { + const stack = new Stack(); + test.throws(() => Role.import(stack, 'BadRole', {}), + /If "roleArn" is not specified, you must specify "roleName"/); + test.done(); + } };