Skip to content

Commit 9f22b2f

Browse files
authored
feat(iam): session tagging (#17689)
To allow session tagging, the `sts:TagSession` permission needs to be added to the role's AssumeRolePolicyDocument. Introduce a new principal which enables this, and add a convenience method `.withSessionTags()` to the `PrincipalBase` class so all built-in principals will have this convenience method by default. To build this, we had to get rid of some cruft and assumptions around policy documents and statements, and defer more power to the `IPrincipal` objects themselves. In order not to break existing implementors, introduce a new interface `IAssumeRolePrincipal` which knows how to add itself to an AssumeRolePolicyDocument and gets complete freedom doing so. That same new interface could be used to lift some old limitations on `CompositePrincipal` so did that as well. Fixes #15908, closes #16725, fixes #2041, fixes #1578. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 465dabf commit 9f22b2f

17 files changed

+452
-186
lines changed

packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.executionrole.expected.json

+8-4
Original file line numberDiff line numberDiff line change
@@ -367,10 +367,14 @@
367367
"Action": "sts:AssumeRole",
368368
"Effect": "Allow",
369369
"Principal": {
370-
"Service": [
371-
"ecs.amazonaws.com",
372-
"ecs-tasks.amazonaws.com"
373-
]
370+
"Service": "ecs.amazonaws.com"
371+
}
372+
},
373+
{
374+
"Action": "sts:AssumeRole",
375+
"Effect": "Allow",
376+
"Principal": {
377+
"Service": "ecs-tasks.amazonaws.com"
374378
}
375379
}
376380
],

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

+23-5
Original file line numberDiff line numberDiff line change
@@ -209,13 +209,31 @@ The `WebIdentityPrincipal` class can be used as a principal for web identities l
209209
Cognito, Amazon, Google or Facebook, for example:
210210

211211
```ts
212-
const principal = new iam.WebIdentityPrincipal('cognito-identity.amazonaws.com')
213-
.withConditions({
214-
"StringEquals": { "cognito-identity.amazonaws.com:aud": "us-east-2:12345678-abcd-abcd-abcd-123456" },
215-
"ForAnyValue:StringLike": {"cognito-identity.amazonaws.com:amr": "unauthenticated" },
216-
});
212+
const principal = new iam.WebIdentityPrincipal('cognito-identity.amazonaws.com', {
213+
'StringEquals': { 'cognito-identity.amazonaws.com:aud': 'us-east-2:12345678-abcd-abcd-abcd-123456' },
214+
'ForAnyValue:StringLike': {'cognito-identity.amazonaws.com:amr': 'unauthenticated' },
215+
});
217216
```
218217

218+
If your identity provider is configured to assume a Role with [session
219+
tags](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html), you
220+
need to call `.withSessionTags()` to add the required permissions to the Role's
221+
policy document:
222+
223+
```ts
224+
new iam.Role(this, 'Role', {
225+
assumedBy: new iam.WebIdentityPrincipal('cognito-identity.amazonaws.com', {
226+
'StringEquals': {
227+
'cognito-identity.amazonaws.com:aud': 'us-east-2:12345678-abcd-abcd-abcd-123456',
228+
},
229+
'ForAnyValue:StringLike': {
230+
'cognito-identity.amazonaws.com:amr': 'unauthenticated',
231+
},
232+
}).withSessionTags(),
233+
});
234+
```
235+
236+
219237
## Parsing JSON Policy Documents
220238

221239
The `PolicyDocument.fromJson` and `PolicyStatement.fromJson` static methods can be used to parse JSON objects. For example:

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

+109-39
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import * as cdk from '@aws-cdk/core';
22
import { Default, FactName, RegionInfo } from '@aws-cdk/region-info';
33
import { IOpenIdConnectProvider } from './oidc-provider';
4+
import { PolicyDocument } from './policy-document';
45
import { Condition, Conditions, PolicyStatement } from './policy-statement';
6+
import { defaultAddPrincipalToAssumeRole } from './private/assume-role-policy';
57
import { ISamlProvider } from './saml-provider';
68
import { LITERAL_STRING_KEY, mergePrincipal } from './util';
79

@@ -68,6 +70,25 @@ export interface IPrincipal extends IGrantable {
6870
addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult;
6971
}
7072

73+
/**
74+
* A type of principal that has more control over its own representation in AssumeRolePolicyDocuments
75+
*
76+
* More complex types of identity providers need more control over Role's policy documents
77+
* than simply `{ Effect: 'Allow', Action: 'AssumeRole', Principal: <Whatever> }`.
78+
*
79+
* If that control is necessary, they can implement `IAssumeRolePrincipal` to get full
80+
* access to a Role's AssumeRolePolicyDocument.
81+
*/
82+
export interface IAssumeRolePrincipal extends IPrincipal {
83+
/**
84+
* Add the princpial to the AssumeRolePolicyDocument
85+
*
86+
* Add the statements to the AssumeRolePolicyDocument necessary to give this principal
87+
* permissions to assume the given role.
88+
*/
89+
addToAssumeRolePolicy(document: PolicyDocument): void;
90+
}
91+
7192
/**
7293
* Result of calling `addToPrincipalPolicy`
7394
*/
@@ -89,7 +110,7 @@ export interface AddToPrincipalPolicyResult {
89110
/**
90111
* Base class for policy principals
91112
*/
92-
export abstract class PrincipalBase implements IPrincipal {
113+
export abstract class PrincipalBase implements IAssumeRolePrincipal {
93114
public readonly grantPrincipal: IPrincipal = this;
94115
public readonly principalAccount: string | undefined = undefined;
95116

@@ -113,6 +134,14 @@ export abstract class PrincipalBase implements IPrincipal {
113134
return { statementAdded: false };
114135
}
115136

137+
public addToAssumeRolePolicy(document: PolicyDocument): void {
138+
// Default implementation of this protocol, compatible with the legacy behavior
139+
document.addStatements(new PolicyStatement({
140+
actions: [this.assumeRoleAction],
141+
principals: [this],
142+
}));
143+
}
144+
116145
public toString() {
117146
// This is a first pass to make the object readable. Descendant principals
118147
// should return something nicer.
@@ -138,9 +167,39 @@ export abstract class PrincipalBase implements IPrincipal {
138167
*
139168
* @returns a new PrincipalWithConditions object.
140169
*/
141-
public withConditions(conditions: Conditions): IPrincipal {
170+
public withConditions(conditions: Conditions): PrincipalBase {
142171
return new PrincipalWithConditions(this, conditions);
143172
}
173+
174+
/**
175+
* Returns a new principal using this principal as the base, with session tags enabled.
176+
*
177+
* @returns a new SessionTagsPrincipal object.
178+
*/
179+
public withSessionTags(): PrincipalBase {
180+
return new SessionTagsPrincipal(this);
181+
}
182+
}
183+
184+
/**
185+
* Base class for Principals that wrap other principals
186+
*/
187+
class PrincipalAdapter extends PrincipalBase {
188+
public readonly assumeRoleAction = this.wrapped.assumeRoleAction;
189+
public readonly principalAccount = this.wrapped.principalAccount;
190+
191+
constructor(protected readonly wrapped: IPrincipal) {
192+
super();
193+
}
194+
195+
public get policyFragment(): PrincipalPolicyFragment { return this.wrapped.policyFragment; }
196+
197+
addToPolicy(statement: PolicyStatement): boolean {
198+
return this.wrapped.addToPolicy(statement);
199+
}
200+
addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult {
201+
return this.wrapped.addToPrincipalPolicy(statement);
202+
}
144203
}
145204

146205
/**
@@ -149,15 +208,11 @@ export abstract class PrincipalBase implements IPrincipal {
149208
* For more information about conditions, see:
150209
* https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html
151210
*/
152-
export class PrincipalWithConditions implements IPrincipal {
153-
public readonly grantPrincipal: IPrincipal = this;
154-
public readonly assumeRoleAction: string = this.principal.assumeRoleAction;
211+
export class PrincipalWithConditions extends PrincipalAdapter {
155212
private additionalConditions: Conditions;
156213

157-
constructor(
158-
private readonly principal: IPrincipal,
159-
conditions: Conditions,
160-
) {
214+
constructor(principal: IPrincipal, conditions: Conditions) {
215+
super(principal);
161216
this.additionalConditions = conditions;
162217
}
163218

@@ -186,27 +241,15 @@ export class PrincipalWithConditions implements IPrincipal {
186241
* See [the IAM documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html).
187242
*/
188243
public get conditions() {
189-
return this.mergeConditions(this.principal.policyFragment.conditions, this.additionalConditions);
244+
return this.mergeConditions(this.wrapped.policyFragment.conditions, this.additionalConditions);
190245
}
191246

192247
public get policyFragment(): PrincipalPolicyFragment {
193-
return new PrincipalPolicyFragment(this.principal.policyFragment.principalJson, this.conditions);
194-
}
195-
196-
public get principalAccount(): string | undefined {
197-
return this.principal.principalAccount;
198-
}
199-
200-
public addToPolicy(statement: PolicyStatement): boolean {
201-
return this.addToPrincipalPolicy(statement).statementAdded;
202-
}
203-
204-
public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult {
205-
return this.principal.addToPrincipalPolicy(statement);
248+
return new PrincipalPolicyFragment(this.wrapped.policyFragment.principalJson, this.conditions);
206249
}
207250

208251
public toString() {
209-
return this.principal.toString();
252+
return this.wrapped.toString();
210253
}
211254

212255
/**
@@ -247,6 +290,30 @@ export class PrincipalWithConditions implements IPrincipal {
247290
}
248291
}
249292

293+
/**
294+
* Enables session tags on role assumptions from a principal
295+
*
296+
* For more information on session tags, see:
297+
* https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html
298+
*/
299+
export class SessionTagsPrincipal extends PrincipalAdapter {
300+
constructor(principal: IPrincipal) {
301+
super(principal);
302+
}
303+
304+
public addToAssumeRolePolicy(doc: PolicyDocument) {
305+
// Lazy import to avoid circular import dependencies during startup
306+
307+
// eslint-disable-next-line @typescript-eslint/no-require-imports
308+
const adapter: typeof import('./private/policydoc-adapter') = require('./private/policydoc-adapter');
309+
310+
defaultAddPrincipalToAssumeRole(this.wrapped, new adapter.MutatingPolicyDocumentAdapter(doc, (statement) => {
311+
statement.addActions('sts:TagSession');
312+
return statement;
313+
}));
314+
}
315+
}
316+
250317
/**
251318
* A collection of the fields in a PolicyStatement that can be used to identify a principal.
252319
*
@@ -441,6 +508,7 @@ export class FederatedPrincipal extends PrincipalBase {
441508
* @param federated federated identity provider (i.e. 'cognito-identity.amazonaws.com' for users authenticated through Cognito)
442509
* @param conditions The conditions under which the policy is in effect.
443510
* See [the IAM documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html).
511+
* @param sessionTags Whether to enable session tagging (see https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html)
444512
*/
445513
constructor(
446514
public readonly federated: string,
@@ -471,6 +539,7 @@ export class WebIdentityPrincipal extends FederatedPrincipal {
471539
* @param identityProvider identity provider (i.e. 'cognito-identity.amazonaws.com' for users authenticated through Cognito)
472540
* @param conditions The conditions under which the policy is in effect.
473541
* See [the IAM documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html).
542+
* @param sessionTags Whether to enable session tagging (see https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html)
474543
*/
475544
constructor(identityProvider: string, conditions: Conditions = {}) {
476545
super(identityProvider, conditions ?? {}, 'sts:AssumeRoleWithWebIdentity');
@@ -606,9 +675,9 @@ export class StarPrincipal extends PrincipalBase {
606675
*/
607676
export class CompositePrincipal extends PrincipalBase {
608677
public readonly assumeRoleAction: string;
609-
private readonly principals = new Array<PrincipalBase>();
678+
private readonly principals = new Array<IPrincipal>();
610679

611-
constructor(...principals: PrincipalBase[]) {
680+
constructor(...principals: IPrincipal[]) {
612681
super();
613682
if (principals.length === 0) {
614683
throw new Error('CompositePrincipals must be constructed with at least 1 Principal but none were passed.');
@@ -623,28 +692,29 @@ export class CompositePrincipal extends PrincipalBase {
623692
*
624693
* @param principals IAM principals that will be added to the composite principal
625694
*/
626-
public addPrincipals(...principals: PrincipalBase[]): this {
627-
for (const p of principals) {
628-
if (p.assumeRoleAction !== this.assumeRoleAction) {
629-
throw new Error(
630-
'Cannot add multiple principals with different "assumeRoleAction". ' +
631-
`Expecting "${this.assumeRoleAction}", got "${p.assumeRoleAction}"`);
632-
}
695+
public addPrincipals(...principals: IPrincipal[]): this {
696+
this.principals.push(...principals);
697+
return this;
698+
}
699+
700+
public addToAssumeRolePolicy(doc: PolicyDocument) {
701+
for (const p of this.principals) {
702+
defaultAddPrincipalToAssumeRole(p, doc);
703+
}
704+
}
633705

706+
public get policyFragment(): PrincipalPolicyFragment {
707+
// We only have a problem with conditions if we are trying to render composite
708+
// princpals into a single statement (which is when `policyFragment` would get called)
709+
for (const p of this.principals) {
634710
const fragment = p.policyFragment;
635711
if (fragment.conditions && Object.keys(fragment.conditions).length > 0) {
636712
throw new Error(
637713
'Components of a CompositePrincipal must not have conditions. ' +
638714
`Tried to add the following fragment: ${JSON.stringify(fragment)}`);
639715
}
640-
641-
this.principals.push(p);
642716
}
643717

644-
return this;
645-
}
646-
647-
public get policyFragment(): PrincipalPolicyFragment {
648718
const principalJson: { [key: string]: string[] } = {};
649719

650720
for (const p of this.principals) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { PolicyDocument } from '../policy-document';
2+
import { PolicyStatement } from '../policy-statement';
3+
import { IPrincipal, IAssumeRolePrincipal } from '../principals';
4+
5+
/**
6+
* Add a principal to an AssumeRolePolicyDocument in the right way
7+
*
8+
* Delegate to the principal if it can do the job itself, do a default job if it can't.
9+
*/
10+
export function defaultAddPrincipalToAssumeRole(principal: IPrincipal, doc: PolicyDocument) {
11+
if (isAssumeRolePrincipal(principal)) {
12+
// Principal knows how to add itself
13+
principal.addToAssumeRolePolicy(doc);
14+
} else {
15+
// Principal can't add itself, we do it for them
16+
doc.addStatements(new PolicyStatement({
17+
actions: [principal.assumeRoleAction],
18+
principals: [principal],
19+
}));
20+
}
21+
}
22+
23+
function isAssumeRolePrincipal(principal: IPrincipal): principal is IAssumeRolePrincipal {
24+
return !!(principal as IAssumeRolePrincipal).addToAssumeRolePolicy;
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { PolicyDocument } from '../policy-document';
2+
import { PolicyStatement } from '../policy-statement';
3+
4+
/**
5+
* A PolicyDocument adapter that can modify statements flowing through it
6+
*/
7+
export class MutatingPolicyDocumentAdapter extends PolicyDocument {
8+
constructor(private readonly wrapped: PolicyDocument, private readonly mutator: (s: PolicyStatement) => PolicyStatement) {
9+
super();
10+
}
11+
12+
public addStatements(...statements: PolicyStatement[]): void {
13+
for (const st of statements) {
14+
this.wrapped.addStatements(this.mutator(st));
15+
}
16+
}
17+
}

0 commit comments

Comments
 (0)