Skip to content

Commit d3df40f

Browse files
authored
feat(iam): implement IGrantable to Policy and ManagedPolicy (#22712)
Fixes #10308 ---- ### All Submissions: * [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) ### Adding new Unconventional Dependencies: * [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies) ### New Features * [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)? * [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 7f6b892 commit d3df40f

24 files changed

+805
-42
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ declare const table: dynamodb.Table;
6060
table.grant(fn, 'dynamodb:PutItem');
6161
```
6262

63-
The `grant*` methods accept an `IGrantable` object. This interface is implemented by IAM principal resources (groups, users and roles) and resources that assume a role such as a Lambda function, EC2 instance or a Codebuild project.
63+
The `grant*` methods accept an `IGrantable` object. This interface is implemented by IAM principal resources (groups, users and roles), policies, managed policies and resources that assume a role such as a Lambda function, EC2 instance or a Codebuild project.
6464

6565
You can find which `grant*` methods exist for a resource in the [AWS CDK API Reference](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-construct-library.html).
6666

packages/@aws-cdk/aws-iam/lib/managed-policy.ts

+33-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { IGroup } from './group';
55
import { CfnManagedPolicy } from './iam.generated';
66
import { PolicyDocument } from './policy-document';
77
import { PolicyStatement } from './policy-statement';
8+
import { AddToPrincipalPolicyResult, IGrantable, IPrincipal, PrincipalPolicyFragment } from './principals';
89
import { undefinedIfEmpty } from './private/util';
910
import { IRole } from './role';
1011
import { IUser } from './user';
@@ -100,7 +101,7 @@ export interface ManagedPolicyProps {
100101
* Managed policy
101102
*
102103
*/
103-
export class ManagedPolicy extends Resource implements IManagedPolicy {
104+
export class ManagedPolicy extends Resource implements IManagedPolicy, IGrantable {
104105
/**
105106
* Import a customer managed policy from the managedPolicyName.
106107
*
@@ -202,6 +203,8 @@ export class ManagedPolicy extends Resource implements IManagedPolicy {
202203
*/
203204
public readonly path: string;
204205

206+
public readonly grantPrincipal: IPrincipal;
207+
205208
private readonly roles = new Array<IRole>();
206209
private readonly users = new Array<IUser>();
207210
private readonly groups = new Array<IGroup>();
@@ -263,6 +266,8 @@ export class ManagedPolicy extends Resource implements IManagedPolicy {
263266
props.statements.forEach(p => this.addStatements(p));
264267
}
265268

269+
this.grantPrincipal = new ManagedPolicyGrantPrincipal(this);
270+
266271
this.node.addValidation({ validate: () => this.validateManagedPolicy() });
267272
}
268273

@@ -316,3 +321,30 @@ export class ManagedPolicy extends Resource implements IManagedPolicy {
316321
return result;
317322
}
318323
}
324+
325+
class ManagedPolicyGrantPrincipal implements IPrincipal {
326+
public readonly assumeRoleAction = 'sts:AssumeRole';
327+
public readonly grantPrincipal: IPrincipal;
328+
public readonly principalAccount?: string;
329+
330+
constructor(private _managedPolicy: ManagedPolicy) {
331+
this.grantPrincipal = this;
332+
this.principalAccount = _managedPolicy.env.account;
333+
}
334+
335+
public get policyFragment(): PrincipalPolicyFragment {
336+
// This property is referenced to add policy statements as a resource-based policy.
337+
// We should fail because a managed policy cannot be used as a principal of a policy document.
338+
// cf. https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html#Principal_specifying
339+
throw new Error(`Cannot use a ManagedPolicy '${this._managedPolicy.node.path}' as the 'Principal' or 'NotPrincipal' in an IAM Policy`);
340+
}
341+
342+
public addToPolicy(statement: PolicyStatement): boolean {
343+
return this.addToPrincipalPolicy(statement).statementAdded;
344+
}
345+
346+
public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult {
347+
this._managedPolicy.addStatements(statement);
348+
return { statementAdded: true, policyDependable: this._managedPolicy };
349+
}
350+
}

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

+33-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { IGroup } from './group';
44
import { CfnPolicy } from './iam.generated';
55
import { PolicyDocument } from './policy-document';
66
import { PolicyStatement } from './policy-statement';
7+
import { AddToPrincipalPolicyResult, IGrantable, IPrincipal, PrincipalPolicyFragment } from './principals';
78
import { generatePolicyName, undefinedIfEmpty } from './private/util';
89
import { IRole } from './role';
910
import { IUser } from './user';
@@ -100,7 +101,7 @@ export interface PolicyProps {
100101
* Policies](http://docs.aws.amazon.com/IAM/latest/UserGuide/policies_overview.html)
101102
* in the IAM User Guide guide.
102103
*/
103-
export class Policy extends Resource implements IPolicy {
104+
export class Policy extends Resource implements IPolicy, IGrantable {
104105

105106
/**
106107
* Import a policy in this app based on its name
@@ -118,6 +119,8 @@ export class Policy extends Resource implements IPolicy {
118119
*/
119120
public readonly document = new PolicyDocument();
120121

122+
public readonly grantPrincipal: IPrincipal;
123+
121124
private readonly _policyName: string;
122125
private readonly roles = new Array<IRole>();
123126
private readonly users = new Array<IUser>();
@@ -178,6 +181,8 @@ export class Policy extends Resource implements IPolicy {
178181
props.statements.forEach(p => this.addStatements(p));
179182
}
180183

184+
this.grantPrincipal = new PolicyGrantPrincipal(this);
185+
181186
this.node.addValidation({ validate: () => this.validatePolicy() });
182187
}
183188

@@ -260,3 +265,30 @@ export class Policy extends Resource implements IPolicy {
260265
return this.groups.length + this.users.length + this.roles.length > 0;
261266
}
262267
}
268+
269+
class PolicyGrantPrincipal implements IPrincipal {
270+
public readonly assumeRoleAction = 'sts:AssumeRole';
271+
public readonly grantPrincipal: IPrincipal;
272+
public readonly principalAccount?: string;
273+
274+
constructor(private _policy: Policy) {
275+
this.grantPrincipal = this;
276+
this.principalAccount = _policy.env.account;
277+
}
278+
279+
public get policyFragment(): PrincipalPolicyFragment {
280+
// This property is referenced to add policy statements as a resource-based policy.
281+
// We should fail because a policy cannot be used as a principal of a policy document.
282+
// cf. https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html#Principal_specifying
283+
throw new Error(`Cannot use a Policy '${this._policy.node.path}' as the 'Principal' or 'NotPrincipal' in an IAM Policy`);
284+
}
285+
286+
public addToPolicy(statement: PolicyStatement): boolean {
287+
return this.addToPrincipalPolicy(statement).statementAdded;
288+
}
289+
290+
public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult {
291+
this._policy.addStatements(statement);
292+
return { statementAdded: true, policyDependable: this._policy };
293+
}
294+
}

packages/@aws-cdk/aws-iam/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
"devDependencies": {
8383
"@aws-cdk/assertions": "0.0.0",
8484
"@aws-cdk/cdk-build-tools": "0.0.0",
85+
"@aws-cdk/integ-tests": "0.0.0",
8586
"@aws-cdk/integ-runner": "0.0.0",
8687
"@aws-cdk/integ-tests": "0.0.0",
8788
"@aws-cdk/cfn2ts": "0.0.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"version": "21.0.0",
3+
"files": {
4+
"21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": {
5+
"source": {
6+
"path": "ManagedPolicyIntegDefaultTestDeployAssert27007DC6.template.json",
7+
"packaging": "file"
8+
},
9+
"destinations": {
10+
"current_account-current_region": {
11+
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
12+
"objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json",
13+
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
14+
}
15+
}
16+
}
17+
},
18+
"dockerImages": {}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"Parameters": {
3+
"BootstrapVersion": {
4+
"Type": "AWS::SSM::Parameter::Value<String>",
5+
"Default": "/cdk-bootstrap/hnb659fds/version",
6+
"Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"
7+
}
8+
},
9+
"Rules": {
10+
"CheckBootstrapVersion": {
11+
"Assertions": [
12+
{
13+
"Assert": {
14+
"Fn::Not": [
15+
{
16+
"Fn::Contains": [
17+
[
18+
"1",
19+
"2",
20+
"3",
21+
"4",
22+
"5"
23+
],
24+
{
25+
"Ref": "BootstrapVersion"
26+
}
27+
]
28+
}
29+
]
30+
},
31+
"AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."
32+
}
33+
]
34+
}
35+
}
36+
}

packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/aws-cdk-iam-managed-policy.assets.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
{
2-
"version": "20.0.0",
2+
"version": "21.0.0",
33
"files": {
4-
"368422c5e22c8b7707ddebb8f45bea20bd1c541adb0778d8e86c2e1854900523": {
4+
"df45fa697f19036987d5bfe10fdcfba6de6d5d93be8e406edfc43fcc13fedc33": {
55
"source": {
66
"path": "aws-cdk-iam-managed-policy.template.json",
77
"packaging": "file"
88
},
99
"destinations": {
1010
"current_account-current_region": {
1111
"bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
12-
"objectKey": "368422c5e22c8b7707ddebb8f45bea20bd1c541adb0778d8e86c2e1854900523.json",
12+
"objectKey": "df45fa697f19036987d5bfe10fdcfba6de6d5d93be8e406edfc43fcc13fedc33.json",
1313
"assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
1414
}
1515
}

packages/@aws-cdk/aws-iam/test/integ.managed-policy.js.snapshot/aws-cdk-iam-managed-policy.template.json

+52
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@
3131
"Action": "sqs:SendMessage",
3232
"Effect": "Allow",
3333
"Resource": "*"
34+
},
35+
{
36+
"Action": "sts:AssumeRole",
37+
"Effect": "Allow",
38+
"Resource": {
39+
"Fn::GetAtt": [
40+
"Role1ABCC5F0",
41+
"Arn"
42+
]
43+
}
3444
}
3545
],
3646
"Version": "2012-10-17"
@@ -54,13 +64,55 @@
5464
"Action": "lambda:InvokeFunction",
5565
"Effect": "Allow",
5666
"Resource": "*"
67+
},
68+
{
69+
"Action": "iam:*",
70+
"Effect": "Allow",
71+
"Resource": {
72+
"Fn::GetAtt": [
73+
"Role1ABCC5F0",
74+
"Arn"
75+
]
76+
}
5777
}
5878
],
5979
"Version": "2012-10-17"
6080
},
6181
"Description": "",
6282
"Path": "/"
6383
}
84+
},
85+
"Role1ABCC5F0": {
86+
"Type": "AWS::IAM::Role",
87+
"Properties": {
88+
"AssumeRolePolicyDocument": {
89+
"Statement": [
90+
{
91+
"Action": "sts:AssumeRole",
92+
"Effect": "Allow",
93+
"Principal": {
94+
"AWS": {
95+
"Fn::Join": [
96+
"",
97+
[
98+
"arn:",
99+
{
100+
"Ref": "AWS::Partition"
101+
},
102+
":iam::",
103+
{
104+
"Ref": "AWS::AccountId"
105+
},
106+
":root"
107+
]
108+
]
109+
}
110+
}
111+
}
112+
],
113+
"Version": "2012-10-17"
114+
}
115+
}
64116
}
65117
},
66118
"Parameters": {
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"version":"20.0.0"}
1+
{"version":"21.0.0"}
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
{
2-
"version": "20.0.0",
2+
"version": "21.0.0",
33
"testCases": {
4-
"integ.managed-policy": {
4+
"ManagedPolicyInteg/DefaultTest": {
55
"stacks": [
66
"aws-cdk-iam-managed-policy"
77
],
8-
"diffAssets": false,
9-
"stackUpdateWorkflow": true
8+
"assertionStack": "ManagedPolicyInteg/DefaultTest/DeployAssert",
9+
"assertionStackName": "ManagedPolicyIntegDefaultTestDeployAssert27007DC6"
1010
}
11-
},
12-
"synthContext": {},
13-
"enableLookups": false
11+
}
1412
}

0 commit comments

Comments
 (0)