-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(iam): Role.withoutPolicyUpdates()
#5569
Changes from all commits
c571295
e017fb6
f81c312
7349f2a
6e0d1e3
fe49f05
10c3eb0
1973409
334cb8c
39de5a1
717d052
8ef7ab5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { DependableTrait } from '@aws-cdk/core'; | ||
import { Grant } from '../grant'; | ||
import { IManagedPolicy } from '../managed-policy'; | ||
import { Policy } from '../policy'; | ||
import { PolicyStatement } from '../policy-statement'; | ||
import { IPrincipal } from '../principals'; | ||
import { IRole } from '../role'; | ||
|
||
/** | ||
* An immutable wrapper around an IRole | ||
* | ||
* This wrapper ignores all mutating operations, like attaching policies or | ||
* adding policy statements. | ||
* | ||
* Useful in cases where you want to turn off CDK's automatic permissions | ||
* management, and instead have full control over all permissions. | ||
* | ||
* Note: if you want to ignore all mutations for an externally defined role | ||
* which was imported into the CDK with {@link Role.fromRoleArn}, you don't have to use this class - | ||
* simply pass the property mutable = false when calling {@link Role.fromRoleArn}. | ||
*/ | ||
export class ImmutableRole implements IRole { | ||
public readonly assumeRoleAction = this.role.assumeRoleAction; | ||
public readonly policyFragment = this.role.policyFragment; | ||
public readonly grantPrincipal = this.role.grantPrincipal; | ||
public readonly roleArn = this.role.roleArn; | ||
public readonly roleName = this.role.roleName; | ||
public readonly node = this.role.node; | ||
public readonly stack = this.role.stack; | ||
|
||
constructor(private readonly role: IRole) { | ||
// implement IDependable privately | ||
DependableTrait.implement(this, { | ||
dependencyRoots: [ role ] | ||
}); | ||
} | ||
|
||
public attachInlinePolicy(_policy: Policy): void { | ||
// do nothing | ||
} | ||
|
||
public addManagedPolicy(_policy: IManagedPolicy): void { | ||
// do nothing | ||
} | ||
|
||
public addToPolicy(_statement: PolicyStatement): boolean { | ||
// Not really added, but for the purposes of consumer code pretend that it was. | ||
return true; | ||
} | ||
|
||
public grant(grantee: IPrincipal, ...actions: string[]): Grant { | ||
return this.role.grant(grantee, ...actions); | ||
} | ||
|
||
public grantPassRole(grantee: IPrincipal): Grant { | ||
return this.role.grantPassRole(grantee); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ import { Policy } from './policy'; | |
import { PolicyDocument } from './policy-document'; | ||
import { PolicyStatement } from './policy-statement'; | ||
import { ArnPrincipal, IPrincipal, PrincipalPolicyFragment } from './principals'; | ||
import { ImmutableRole } from './private/immutable-role'; | ||
import { AttachedPolicies } from './util'; | ||
|
||
export interface RoleProps { | ||
|
@@ -162,16 +163,32 @@ export class Role extends Resource implements IRole { | |
? resourceName.slice('service-role/'.length) | ||
: resourceName; | ||
|
||
abstract class Import extends Resource implements IRole { | ||
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 = roleName; | ||
private readonly attachedPolicies = new AttachedPolicies(); | ||
private defaultPolicy?: Policy; | ||
|
||
public abstract addToPolicy(statement: PolicyStatement): boolean; | ||
public addToPolicy(statement: PolicyStatement): boolean { | ||
if (!this.defaultPolicy) { | ||
this.defaultPolicy = new Policy(this, 'Policy'); | ||
this.attachInlinePolicy(this.defaultPolicy); | ||
} | ||
this.defaultPolicy.addStatements(statement); | ||
return true; | ||
} | ||
|
||
public attachInlinePolicy(policy: Policy): void { | ||
const policyAccount = Stack.of(policy).account; | ||
|
||
public abstract attachInlinePolicy(policy: Policy): void; | ||
if (accountsAreEqualOrOneIsUnresolved(policyAccount, roleAccount)) { | ||
this.attachedPolicies.attach(policy); | ||
policy.attachToRole(this); | ||
} | ||
} | ||
|
||
public addManagedPolicy(_policy: IManagedPolicy): void { | ||
// FIXME: Add warning that we're ignoring this | ||
|
@@ -199,44 +216,11 @@ export class Role extends Resource implements IRole { | |
|
||
const roleAccount = parsedArn.account; | ||
|
||
class MutableImport extends Import { | ||
private readonly attachedPolicies = new AttachedPolicies(); | ||
private defaultPolicy?: Policy; | ||
|
||
public addToPolicy(statement: PolicyStatement): boolean { | ||
if (!this.defaultPolicy) { | ||
this.defaultPolicy = new Policy(this, 'Policy'); | ||
this.attachInlinePolicy(this.defaultPolicy); | ||
} | ||
this.defaultPolicy.addStatements(statement); | ||
return true; | ||
} | ||
|
||
public attachInlinePolicy(policy: Policy): void { | ||
const policyAccount = Stack.of(policy).account; | ||
|
||
if (accountsAreEqualOrOneIsUnresolved(policyAccount, roleAccount)) { | ||
this.attachedPolicies.attach(policy); | ||
policy.attachToRole(this); | ||
} | ||
} | ||
} | ||
|
||
class ImmutableImport extends Import { | ||
public addToPolicy(_statement: PolicyStatement): boolean { | ||
return true; | ||
} | ||
|
||
public attachInlinePolicy(_policy: Policy): void { | ||
// do nothing | ||
} | ||
} | ||
|
||
const scopeAccount = scopeStack.account; | ||
|
||
return options.mutable !== false && accountsAreEqualOrOneIsUnresolved(scopeAccount, roleAccount) | ||
? new MutableImport(scope, id) | ||
: new ImmutableImport(scope, id); | ||
? new Import(scope, id) | ||
: new ImmutableRole(new Import(scope, id)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this be |
||
|
||
function accountsAreEqualOrOneIsUnresolved(account1: string | undefined, | ||
account2: string | undefined): boolean { | ||
|
@@ -385,6 +369,19 @@ export class Role extends Resource implements IRole { | |
public grantPassRole(identity: IPrincipal) { | ||
return this.grant(identity, 'iam:PassRole'); | ||
} | ||
|
||
/** | ||
* Return a copy of this Role object whose Policies will not be updated | ||
* | ||
* Use the object returned by this method if you want this Role to be used by | ||
* a construct without it automatically updating the Role's Policies. | ||
* | ||
* If you do, you are responsible for adding the correct statements to the | ||
* Role's policies yourself. | ||
*/ | ||
public withoutPolicyUpdates(): IRole { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Going back to our conversation, why can't this be in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
So either we make There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure. It’s not that critical. |
||
return new ImmutableRole(this); | ||
} | ||
} | ||
|
||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import '@aws-cdk/assert/jest'; | ||
import { Stack } from '@aws-cdk/core'; | ||
import * as iam from '../lib'; | ||
|
||
// tslint:disable:object-literal-key-quotes | ||
|
||
describe('ImmutableRole', () => { | ||
let stack: Stack; | ||
let mutableRole: iam.Role; | ||
let immutableRole: iam.IRole; | ||
|
||
beforeEach(() => { | ||
stack = new Stack(); | ||
mutableRole = new iam.Role(stack, 'MutableRole', { | ||
assumedBy: new iam.AnyPrincipal(), | ||
}); | ||
immutableRole = mutableRole.withoutPolicyUpdates(); | ||
}); | ||
|
||
test('ignores calls to attachInlinePolicy', () => { | ||
const user = new iam.User(stack, 'User'); | ||
const policy = new iam.Policy(stack, 'Policy', { | ||
statements: [new iam.PolicyStatement({ | ||
resources: ['*'], | ||
actions: ['s3:*'], | ||
})], | ||
users: [user], | ||
}); | ||
|
||
immutableRole.attachInlinePolicy(policy); | ||
|
||
expect(stack).toHaveResource('AWS::IAM::Policy', { | ||
"PolicyDocument": { | ||
"Statement": [ | ||
{ | ||
"Action": "s3:*", | ||
"Resource": "*", | ||
"Effect": "Allow", | ||
}, | ||
], | ||
"Version": "2012-10-17", | ||
}, | ||
"PolicyName": "Policy23B91518", | ||
"Users": [ | ||
{ | ||
"Ref": "User00B015A1", | ||
}, | ||
], | ||
}); | ||
}); | ||
|
||
test('ignores calls to addManagedPolicy', () => { | ||
mutableRole.addManagedPolicy({ managedPolicyArn: 'Arn1' }); | ||
|
||
immutableRole.addManagedPolicy({ managedPolicyArn: 'Arn2' }); | ||
|
||
expect(stack).toHaveResourceLike('AWS::IAM::Role', { | ||
"ManagedPolicyArns": [ | ||
'Arn1', | ||
], | ||
}); | ||
}); | ||
|
||
test('ignores calls to addToPolicy', () => { | ||
mutableRole.addToPolicy(new iam.PolicyStatement({ | ||
resources: ['*'], | ||
actions: ['s3:*'], | ||
})); | ||
|
||
immutableRole.addToPolicy(new iam.PolicyStatement({ | ||
resources: ['*'], | ||
actions: ['iam:*'], | ||
})); | ||
|
||
expect(stack).toHaveResourceLike('AWS::IAM::Policy', { | ||
"PolicyDocument": { | ||
"Statement": [ | ||
{ | ||
"Resource": "*", | ||
"Action": "s3:*", | ||
"Effect": "Allow", | ||
}, | ||
], | ||
}, | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.