Skip to content
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): customize IAM role creation behavior #22856

Merged
merged 6 commits into from
Nov 11, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions packages/@aws-cdk/aws-iam/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,120 @@ const role = iam.Role.fromRoleArn(this, 'Role', 'arn:aws:iam::123456789012:role/
});
```

### Customizing role creation

It is best practice to allow CDK to manage IAM roles and permissions, but if you are using CDK in
an environment where role creation is not allowed, or needs to be managed through a process outside
of the CDK application, you can prevent CDK from creating roles by using the `customizeRoles` method.

```ts
declare const stack: Stack;
iam.Role.customizeRoles(stack);
```

This will prevent CDK from creating any IAM roles or policies with the `stack` scope. It will also
fail synthesis and generate a policy report to the cloud assembly (i.e. cdk.out) with the name
`iam-policy-report.txt`. The report will contain a list of IAM roles that would have been created
along with the associated IAM permissions. This report can be used to create the roles with the
appropriate permissions outside of CDK. Once those roles have been created, their names can
be added to the `usePrecreatedRoles` property.

```ts
declare const app: App;
const stack = new Stack(app, 'MyStack');
iam.Role.customizeRoles(stack, {
usePrecreatedRoles: {
'MyStack/MyRole': 'my-precreated-role-name',
},
});

new iam.Role(stack, 'MyRole', {
assumedBy: new iam.ServicePrincipal('sns.amazonaws.com'),
});
```

If any IAM policies reference deploy time values (i.e. ARN of a resource that hasn't been created
yet) you will have to modify the generated report to be more generic. For example, given the
following CDK code:

```ts
declare const app: App;
const stack = new Stack(app, 'MyStack');
iam.Role.customizeRoles(stack);

const fn = new lambda.Function(stack, 'MyLambda', {
code: new lambda.InlineCode('foo'),
handler: 'index.handler',
runtime: lambda.Runtime.NODEJS_14_X,
});

const bucket = new s3.Bucket(stack, 'Bucket');
bucket.grantRead(fn);
```

The following report will be generated.

```txt
<missing role> (MyStack/MyLambda/ServiceRole)

AssumeRole Policy:
[
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
]

Managed Policy ARNs:
[
"arn:(PARTITION):iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
]

Managed Policies Statements:
NONE

Identity Policy Statements:
[
{
"Action": [
"s3:GetObject*",
"s3:GetBucket*",
"s3:List*"
],
"Effect": "Allow",
"Resource": [
"(MyStack/Bucket/Resource.Arn)",
"(MyStack/Bucket/Resource.Arn)/*"
]
}
]
```

You would then need to create the role with the inline & managed policies in the report and then
come back and update the `customizeRoles` with the role name.

```ts
declare const app: App;
const stack = new Stack(app, 'MyStack');
iam.Role.customizeRoles(stack, {
usePrecreatedRoles: {
'MyStack/MyLambda/ServiceRole': 'my-role-name',
}
});
```

It is also possible to generate the report _without_ preventing the role/policy creation.

```ts
declare const stack: Stack;
iam.Role.customizeRoles(stack, {
preventSynthesis: false,
});
```

## Configuring an ExternalId

If you need to create Roles that will be assumed by third parties, it is generally a good idea to [require an `ExternalId`
Expand Down
53 changes: 35 additions & 18 deletions packages/@aws-cdk/aws-iam/lib/managed-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { IGroup } from './group';
import { CfnManagedPolicy } from './iam.generated';
import { PolicyDocument } from './policy-document';
import { PolicyStatement } from './policy-statement';
import { PolicySynthesizer, getCustomizeRolesConfig } from './private/precreated-role';
import { IRole } from './role';
import { IUser } from './user';
import { undefinedIfEmpty } from './util';
Expand Down Expand Up @@ -204,6 +205,7 @@ export class ManagedPolicy extends Resource implements IManagedPolicy {
private readonly roles = new Array<IRole>();
private readonly users = new Array<IUser>();
private readonly groups = new Array<IGroup>();
private readonly _precreatedPolicy?: IManagedPolicy;

constructor(scope: Construct, id: string, props: ManagedPolicyProps = {}) {
super(scope, id, {
Expand All @@ -217,15 +219,33 @@ export class ManagedPolicy extends Resource implements IManagedPolicy {
this.document = props.document;
}

const resource = new CfnManagedPolicy(this, 'Resource', {
policyDocument: this.document,
managedPolicyName: this.physicalName,
description: this.description,
path: this.path,
roles: undefinedIfEmpty(() => this.roles.map(r => r.roleName)),
users: undefinedIfEmpty(() => this.users.map(u => u.userName)),
groups: undefinedIfEmpty(() => this.groups.map(g => g.groupName)),
});
const config = getCustomizeRolesConfig(this);
const _precreatedPolicy = ManagedPolicy.fromManagedPolicyName(this, 'Imported'+id, id);
this.managedPolicyName = id;
this.managedPolicyArn = _precreatedPolicy.managedPolicyArn;
if (config.enabled) {
this._precreatedPolicy = _precreatedPolicy;
}
if (!config.preventSynthesis) {
const resource = new CfnManagedPolicy(this, 'Resource', {
policyDocument: this.document,
managedPolicyName: this.physicalName,
description: this.description,
path: this.path,
roles: undefinedIfEmpty(() => this.roles.map(r => r.roleName)),
users: undefinedIfEmpty(() => this.users.map(u => u.userName)),
groups: undefinedIfEmpty(() => this.groups.map(g => g.groupName)),
});

// arn:aws:iam::123456789012:policy/teststack-CreateTestDBPolicy-16M23YE3CS700
this.managedPolicyName = this.getResourceNameAttribute(Stack.of(this).splitArn(resource.ref, ArnFormat.SLASH_RESOURCE_NAME).resourceName!);
this.managedPolicyArn = this.getResourceArnAttribute(resource.ref, {
region: '', // IAM is global in each partition
service: 'iam',
resource: 'policy',
resourceName: this.physicalName,
});
}

if (props.users) {
props.users.forEach(u => this.attachToUser(u));
Expand All @@ -243,15 +263,6 @@ export class ManagedPolicy extends Resource implements IManagedPolicy {
props.statements.forEach(p => this.addStatements(p));
}

// arn:aws:iam::123456789012:policy/teststack-CreateTestDBPolicy-16M23YE3CS700
this.managedPolicyName = this.getResourceNameAttribute(Stack.of(this).splitArn(resource.ref, ArnFormat.SLASH_RESOURCE_NAME).resourceName!);
this.managedPolicyArn = this.getResourceArnAttribute(resource.ref, {
region: '', // IAM is global in each partition
service: 'iam',
resource: 'policy',
resourceName: this.physicalName,
});

this.node.addValidation({ validate: () => this.validateManagedPolicy() });
}

Expand Down Expand Up @@ -296,6 +307,12 @@ export class ManagedPolicy extends Resource implements IManagedPolicy {

result.push(...this.document.validateForIdentityPolicy());

if (result.length === 0 && this._precreatedPolicy) {
PolicySynthesizer.getOrCreate(this).addManagedPolicy(this.node.path, {
policyStatements: this.document.toJSON()?.Statement,
roles: this.roles.map(role => role.node.path),
});
}
return result;
}
}
88 changes: 88 additions & 0 deletions packages/@aws-cdk/aws-iam/lib/private/imported-role.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Resource, Token, TokenComparison, Annotations } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { Grant } from '../grant';
import { IManagedPolicy } from '../managed-policy';
import { Policy } from '../policy';
import { PolicyStatement } from '../policy-statement';
import { IComparablePrincipal, IPrincipal, ArnPrincipal, AddToPrincipalPolicyResult, PrincipalPolicyFragment } from '../principals';
import { IRole, FromRoleArnOptions } from '../role';
import { AttachedPolicies } from '../util';

export interface ImportedRoleProps extends FromRoleArnOptions {
readonly roleArn: string;
readonly roleName: string;
readonly account?: string;
}

export class ImportedRole extends Resource implements IRole, IComparablePrincipal {
public readonly grantPrincipal: IPrincipal = this;
public readonly principalAccount?: string;
public readonly assumeRoleAction: string = 'sts:AssumeRole';
public readonly policyFragment: PrincipalPolicyFragment;
public readonly roleArn: string;
public readonly roleName: string;
private readonly attachedPolicies = new AttachedPolicies();
private readonly defaultPolicyName?: string;
private defaultPolicy?: Policy;

constructor(scope: Construct, id: string, props: ImportedRoleProps) {
super(scope, id, {
account: props.account,
});

this.roleArn = props.roleArn;
this.roleName = props.roleName;
this.policyFragment = new ArnPrincipal(this.roleArn).policyFragment;
this.defaultPolicyName = props.defaultPolicyName;
this.principalAccount = props.account;
}

public addToPolicy(statement: PolicyStatement): boolean {
return this.addToPrincipalPolicy(statement).statementAdded;
}

public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult {
if (!this.defaultPolicy) {
this.defaultPolicy = new Policy(this, this.defaultPolicyName ?? 'Policy');
this.attachInlinePolicy(this.defaultPolicy);
}
this.defaultPolicy.addStatements(statement);
return { statementAdded: true, policyDependable: this.defaultPolicy };
}

public attachInlinePolicy(policy: Policy): void {
const thisAndPolicyAccountComparison = Token.compareStrings(this.env.account, policy.env.account);
const equalOrAnyUnresolved = thisAndPolicyAccountComparison === TokenComparison.SAME ||
thisAndPolicyAccountComparison === TokenComparison.BOTH_UNRESOLVED ||
thisAndPolicyAccountComparison === TokenComparison.ONE_UNRESOLVED;
if (equalOrAnyUnresolved) {
this.attachedPolicies.attach(policy);
policy.attachToRole(this);
}
}

public addManagedPolicy(policy: IManagedPolicy): void {
Annotations.of(this).addWarning(`Not adding managed policy: ${policy.managedPolicyArn} to imported role: ${this.roleName}`);
}

public grantPassRole(identity: IPrincipal): Grant {
return this.grant(identity, 'iam:PassRole');
}

public grantAssumeRole(identity: IPrincipal): Grant {
return this.grant(identity, 'sts:AssumeRole');
}

public grant(grantee: IPrincipal, ...actions: string[]): Grant {
return Grant.addToPrincipal({
grantee,
actions,
resourceArns: [this.roleArn],
scope: this,
});
}

public dedupeString(): string | undefined {
return `ImportedRole:${this.roleArn}`;
}
}
Loading