Skip to content

Commit 3d200c9

Browse files
rix0rrrRomainMuller
authored andcommitted
feat(aws-iam): configure ExternalId for Role (#1359)
Supply the `externalId` prop to add the appropriate `{ StringEquals: { "sts:ExternalId": "...the id..." }}` to a Role's `AssumeRolePolicyDocument`. Fixes #235.
1 parent d20938c commit 3d200c9

File tree

6 files changed

+120
-6
lines changed

6 files changed

+120
-6
lines changed

packages/@aws-cdk/aws-iam/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ Managed policies can be attached using `xxx.attachManagedPolicy(arn)`:
1414

1515
[attaching managed policies](test/example.managedpolicy.lit.ts)
1616

17+
### Configuring an ExternalId
18+
19+
If you need to create roles that will be assumed by 3rd parties, it is generally a good idea to [require an `ExternalId`
20+
to assume them](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html). Configuring
21+
an `ExternalId` works like this:
22+
23+
[supplying an external ID](test/example.external-id.lit.ts)
24+
1725
### Features
1826

1927
* Policy name uniqueness is enforced. If two policies by the same name are attached to the same

packages/@aws-cdk/aws-iam/lib/role.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ export interface RoleProps {
1414
*/
1515
assumedBy: PolicyPrincipal;
1616

17+
/**
18+
* ID that the role assumer needs to provide when assuming this role
19+
*
20+
* If the configured and provided external IDs do not match, the
21+
* AssumeRole operation will fail.
22+
*
23+
* @default No external ID required
24+
*/
25+
externalId?: string;
26+
1727
/**
1828
* A list of ARNs for managed policies associated with this role.
1929
* You can add managed policies later using `attachManagedPolicy(arn)`.
@@ -120,7 +130,7 @@ export class Role extends Construct implements IRole {
120130
constructor(parent: Construct, name: string, props: RoleProps) {
121131
super(parent, name);
122132

123-
this.assumeRolePolicy = createAssumeRolePolicy(props.assumedBy);
133+
this.assumeRolePolicy = createAssumeRolePolicy(props.assumedBy, props.externalId);
124134
this.managedPolicyArns = props.managedPolicyArns || [ ];
125135

126136
validateMaxSessionDuration(props.maxSessionDurationSec);
@@ -194,11 +204,17 @@ export interface IRole extends IPrincipal, IDependable {
194204
readonly roleArn: string;
195205
}
196206

197-
function createAssumeRolePolicy(principal: PolicyPrincipal) {
198-
return new PolicyDocument()
199-
.addStatement(new PolicyStatement()
207+
function createAssumeRolePolicy(principal: PolicyPrincipal, externalId?: string) {
208+
const statement = new PolicyStatement();
209+
statement
200210
.addPrincipal(principal)
201-
.addAction(principal.assumeRoleAction));
211+
.addAction(principal.assumeRoleAction);
212+
213+
if (externalId !== undefined) {
214+
statement.addCondition('StringEquals', { 'sts:ExternalId': externalId });
215+
}
216+
217+
return new PolicyDocument().addStatement(statement);
202218
}
203219

204220
function validateMaxSessionDuration(duration?: number) {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import cdk = require('@aws-cdk/cdk');
2+
import iam = require('../lib');
3+
4+
export class ExampleConstruct extends cdk.Construct {
5+
constructor(parent: cdk.Construct, id: string) {
6+
super(parent, id);
7+
8+
/// !show
9+
const role = new iam.Role(this, 'MyRole', {
10+
assumedBy: new iam.AccountPrincipal('123456789012'),
11+
externalId: 'SUPPLY-ME',
12+
});
13+
/// !hide
14+
15+
Array.isArray(role);
16+
}
17+
}

packages/@aws-cdk/aws-iam/test/integ.role.expected.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,43 @@
5858
}
5959
]
6060
}
61+
},
62+
"TestRole25D98AB21": {
63+
"Type": "AWS::IAM::Role",
64+
"Properties": {
65+
"AssumeRolePolicyDocument": {
66+
"Statement": [
67+
{
68+
"Action": "sts:AssumeRole",
69+
"Condition": {
70+
"StringEquals": {
71+
"sts:ExternalId": "supply-me"
72+
}
73+
},
74+
"Effect": "Allow",
75+
"Principal": {
76+
"AWS": {
77+
"Fn::Join": [
78+
"",
79+
[
80+
"arn:",
81+
{
82+
"Ref": "AWS::Partition"
83+
},
84+
":iam::",
85+
{
86+
"Ref": "AWS::AccountId"
87+
},
88+
":root"
89+
]
90+
]
91+
}
92+
}
93+
}
94+
],
95+
"Version": "2012-10-17"
96+
}
97+
}
6198
}
6299
}
63100
}

packages/@aws-cdk/aws-iam/test/integ.role.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { App, Stack } from "@aws-cdk/cdk";
2-
import { Policy, PolicyStatement, Role, ServicePrincipal } from "../lib";
2+
import { AccountRootPrincipal, Policy, PolicyStatement, Role, ServicePrincipal } from "../lib";
33

44
const app = new App();
55

@@ -15,4 +15,10 @@ const policy = new Policy(stack, 'HelloPolicy', { policyName: 'Default' });
1515
policy.addStatement(new PolicyStatement().addAction('ec2:*').addResource('*'));
1616
policy.attachToRole(role);
1717

18+
// Role with an external ID
19+
new Role(stack, 'TestRole2', {
20+
assumedBy: new AccountRootPrincipal(),
21+
externalId: 'supply-me',
22+
});
23+
1824
app.run();

packages/@aws-cdk/aws-iam/test/test.role.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,36 @@ export = {
2424
test.done();
2525
},
2626

27+
'can supply externalId'(test: Test) {
28+
// GIVEN
29+
const stack = new Stack();
30+
31+
// WHEN
32+
new Role(stack, 'MyRole', {
33+
assumedBy: new ServicePrincipal('sns.amazonaws.com'),
34+
externalId: 'SomeSecret',
35+
});
36+
37+
// THEN
38+
expect(stack).to(haveResource('AWS::IAM::Role', {
39+
AssumeRolePolicyDocument: {
40+
Statement: [
41+
{
42+
Action: "sts:AssumeRole",
43+
Condition: {
44+
StringEquals: { "sts:ExternalId": "SomeSecret" }
45+
},
46+
Effect: "Allow",
47+
Principal: { Service: "sns.amazonaws.com" }
48+
}
49+
],
50+
Version: "2012-10-17"
51+
}
52+
}));
53+
54+
test.done();
55+
},
56+
2757
'policy is created automatically when permissions are added'(test: Test) {
2858
const stack = new Stack();
2959

0 commit comments

Comments
 (0)