Skip to content

Commit

Permalink
feat(aws-iam): configure ExternalId for Role (#1359)
Browse files Browse the repository at this point in the history
Supply the `externalId` prop to add the appropriate
`{ StringEquals: { "sts:ExternalId": "...the id..." }}` to a
Role's `AssumeRolePolicyDocument`.

Fixes #235.
  • Loading branch information
rix0rrr authored and RomainMuller committed Dec 14, 2018
1 parent d20938c commit 3d200c9
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 6 deletions.
8 changes: 8 additions & 0 deletions packages/@aws-cdk/aws-iam/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ Managed policies can be attached using `xxx.attachManagedPolicy(arn)`:

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

### Configuring an ExternalId

If you need to create roles that will be assumed by 3rd parties, it is generally a good idea to [require an `ExternalId`
to assume them](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html). Configuring
an `ExternalId` works like this:

[supplying an external ID](test/example.external-id.lit.ts)

### Features

* Policy name uniqueness is enforced. If two policies by the same name are attached to the same
Expand Down
26 changes: 21 additions & 5 deletions packages/@aws-cdk/aws-iam/lib/role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ export interface RoleProps {
*/
assumedBy: PolicyPrincipal;

/**
* ID that the role assumer needs to provide when assuming this role
*
* If the configured and provided external IDs do not match, the
* AssumeRole operation will fail.
*
* @default No external ID required
*/
externalId?: string;

/**
* A list of ARNs for managed policies associated with this role.
* You can add managed policies later using `attachManagedPolicy(arn)`.
Expand Down Expand Up @@ -120,7 +130,7 @@ export class Role extends Construct implements IRole {
constructor(parent: Construct, name: string, props: RoleProps) {
super(parent, name);

this.assumeRolePolicy = createAssumeRolePolicy(props.assumedBy);
this.assumeRolePolicy = createAssumeRolePolicy(props.assumedBy, props.externalId);
this.managedPolicyArns = props.managedPolicyArns || [ ];

validateMaxSessionDuration(props.maxSessionDurationSec);
Expand Down Expand Up @@ -194,11 +204,17 @@ export interface IRole extends IPrincipal, IDependable {
readonly roleArn: string;
}

function createAssumeRolePolicy(principal: PolicyPrincipal) {
return new PolicyDocument()
.addStatement(new PolicyStatement()
function createAssumeRolePolicy(principal: PolicyPrincipal, externalId?: string) {
const statement = new PolicyStatement();
statement
.addPrincipal(principal)
.addAction(principal.assumeRoleAction));
.addAction(principal.assumeRoleAction);

if (externalId !== undefined) {
statement.addCondition('StringEquals', { 'sts:ExternalId': externalId });
}

return new PolicyDocument().addStatement(statement);
}

function validateMaxSessionDuration(duration?: number) {
Expand Down
17 changes: 17 additions & 0 deletions packages/@aws-cdk/aws-iam/test/example.external-id.lit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import cdk = require('@aws-cdk/cdk');
import iam = require('../lib');

export class ExampleConstruct extends cdk.Construct {
constructor(parent: cdk.Construct, id: string) {
super(parent, id);

/// !show
const role = new iam.Role(this, 'MyRole', {
assumedBy: new iam.AccountPrincipal('123456789012'),
externalId: 'SUPPLY-ME',
});
/// !hide

Array.isArray(role);
}
}
37 changes: 37 additions & 0 deletions packages/@aws-cdk/aws-iam/test/integ.role.expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,43 @@
}
]
}
},
"TestRole25D98AB21": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "supply-me"
}
},
"Effect": "Allow",
"Principal": {
"AWS": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::",
{
"Ref": "AWS::AccountId"
},
":root"
]
]
}
}
}
],
"Version": "2012-10-17"
}
}
}
}
}
8 changes: 7 additions & 1 deletion packages/@aws-cdk/aws-iam/test/integ.role.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { App, Stack } from "@aws-cdk/cdk";
import { Policy, PolicyStatement, Role, ServicePrincipal } from "../lib";
import { AccountRootPrincipal, Policy, PolicyStatement, Role, ServicePrincipal } from "../lib";

const app = new App();

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

// Role with an external ID
new Role(stack, 'TestRole2', {
assumedBy: new AccountRootPrincipal(),
externalId: 'supply-me',
});

app.run();
30 changes: 30 additions & 0 deletions packages/@aws-cdk/aws-iam/test/test.role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,36 @@ export = {
test.done();
},

'can supply externalId'(test: Test) {
// GIVEN
const stack = new Stack();

// WHEN
new Role(stack, 'MyRole', {
assumedBy: new ServicePrincipal('sns.amazonaws.com'),
externalId: 'SomeSecret',
});

// THEN
expect(stack).to(haveResource('AWS::IAM::Role', {
AssumeRolePolicyDocument: {
Statement: [
{
Action: "sts:AssumeRole",
Condition: {
StringEquals: { "sts:ExternalId": "SomeSecret" }
},
Effect: "Allow",
Principal: { Service: "sns.amazonaws.com" }
}
],
Version: "2012-10-17"
}
}));

test.done();
},

'policy is created automatically when permissions are added'(test: Test) {
const stack = new Stack();

Expand Down

0 comments on commit 3d200c9

Please sign in to comment.