Skip to content

Commit faa0c06

Browse files
authored
feat(iam): SAML identity provider (#13393)
L2 for [`AWS::IAM::SAMLProvider`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-samlprovider.html). Also add derived classes for federated principals. Closes #5320 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 90dbfb5 commit faa0c06

File tree

10 files changed

+473
-1
lines changed

10 files changed

+473
-1
lines changed

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,47 @@ const provider = new iam.OpenIdConnectProvider(this, 'MyProvider', {
364364
const principal = new iam.OpenIdConnectPrincipal(provider);
365365
```
366366

367+
## SAML provider
368+
369+
An IAM SAML 2.0 identity provider is an entity in IAM that describes an external
370+
identity provider (IdP) service that supports the SAML 2.0 (Security Assertion
371+
Markup Language 2.0) standard. You use an IAM identity provider when you want
372+
to establish trust between a SAML-compatible IdP such as Shibboleth or Active
373+
Directory Federation Services and AWS, so that users in your organization can
374+
access AWS resources. IAM SAML identity providers are used as principals in an
375+
IAM trust policy.
376+
377+
```ts
378+
new iam.SamlProvider(this, 'Provider', {
379+
metadataDocument: iam.SamlMetadataDocument.fromFile('/path/to/saml-metadata-document.xml'),
380+
});
381+
```
382+
383+
The `SamlPrincipal` class can be used as a principal with a `SamlProvider`:
384+
385+
```ts
386+
const provider = new iam.SamlProvider(this, 'Provider', {
387+
metadataDocument: iam.SamlMetadataDocument.fromFile('/path/to/saml-metadata-document.xml'),
388+
});
389+
const principal = new iam.SamlPrincipal(provider, {
390+
StringEquals: {
391+
'SAML:iss': 'issuer',
392+
},
393+
});
394+
```
395+
396+
When creating a role for programmatic and AWS Management Console access, use the `SamlConsolePrincipal`
397+
class:
398+
399+
```ts
400+
const provider = new iam.SamlProvider(this, 'Provider', {
401+
metadataDocument: iam.SamlMetadataDocument.fromFile('/path/to/saml-metadata-document.xml'),
402+
});
403+
new iam.Role(this, 'Role', {
404+
assumedBy: new iam.SamlConsolePrincipal(provider),
405+
});
406+
```
407+
367408
## Users
368409

369410
IAM manages users for your AWS account. To create a new user:

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export * from './grant';
1212
export * from './unknown-principal';
1313
export * from './oidc-provider';
1414
export * from './permissions-boundary';
15+
export * from './saml-provider';
1516

1617
// AWS::IAM CloudFormation Resources:
1718
export * from './iam.generated';

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as cdk from '@aws-cdk/core';
22
import { Default, RegionInfo } from '@aws-cdk/region-info';
33
import { IOpenIdConnectProvider } from './oidc-provider';
44
import { Condition, Conditions, PolicyStatement } from './policy-statement';
5+
import { ISamlProvider } from './saml-provider';
56
import { mergePrincipal } from './util';
67

78
/**
@@ -493,6 +494,38 @@ export class OpenIdConnectPrincipal extends WebIdentityPrincipal {
493494
}
494495
}
495496

497+
/**
498+
* Principal entity that represents a SAML federated identity provider
499+
*/
500+
export class SamlPrincipal extends FederatedPrincipal {
501+
constructor(samlProvider: ISamlProvider, conditions: Conditions) {
502+
super(samlProvider.samlProviderArn, conditions, 'sts:AssumeRoleWithSAML');
503+
}
504+
505+
public toString() {
506+
return `SamlPrincipal(${this.federated})`;
507+
}
508+
}
509+
510+
/**
511+
* Principal entity that represents a SAML federated identity provider for
512+
* programmatic and AWS Management Console access.
513+
*/
514+
export class SamlConsolePrincipal extends SamlPrincipal {
515+
constructor(samlProvider: ISamlProvider, conditions: Conditions = {}) {
516+
super(samlProvider, {
517+
...conditions,
518+
StringEquals: {
519+
'SAML:aud': 'https://signin.aws.amazon.com/saml',
520+
},
521+
});
522+
}
523+
524+
public toString() {
525+
return `SamlConsolePrincipal(${this.federated})`;
526+
}
527+
}
528+
496529
/**
497530
* Use the AWS account into which a stack is deployed as the principal entity in a policy
498531
*/
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import * as fs from 'fs';
2+
import { IResource, Resource, Token } from '@aws-cdk/core';
3+
import { Construct } from 'constructs';
4+
import { CfnSAMLProvider } from './iam.generated';
5+
6+
/**
7+
* A SAML provider
8+
*/
9+
export interface ISamlProvider extends IResource {
10+
/**
11+
* The Amazon Resource Name (ARN) of the provider
12+
*
13+
* @attribute
14+
*/
15+
readonly samlProviderArn: string;
16+
}
17+
18+
/**
19+
* Properties for a SAML provider
20+
*/
21+
export interface SamlProviderProps {
22+
/**
23+
* The name of the provider to create.
24+
*
25+
* This parameter allows a string of characters consisting of upper and
26+
* lowercase alphanumeric characters with no spaces. You can also include
27+
* any of the following characters: _+=,.@-
28+
*
29+
* Length must be between 1 and 128 characters.
30+
*
31+
* @default - a CloudFormation generated name
32+
*/
33+
readonly name?: string;
34+
35+
/**
36+
* An XML document generated by an identity provider (IdP) that supports
37+
* SAML 2.0. The document includes the issuer's name, expiration information,
38+
* and keys that can be used to validate the SAML authentication response
39+
* (assertions) that are received from the IdP. You must generate the metadata
40+
* document using the identity management software that is used as your
41+
* organization's IdP.
42+
*/
43+
readonly metadataDocument: SamlMetadataDocument;
44+
}
45+
46+
/**
47+
* A SAML metadata document
48+
*/
49+
export abstract class SamlMetadataDocument {
50+
/**
51+
* Create a SAML metadata document from a XML string
52+
*/
53+
public static fromXml(xml: string): SamlMetadataDocument {
54+
return { xml };
55+
}
56+
57+
/**
58+
* Create a SAML metadata document from a XML file
59+
*/
60+
public static fromFile(path: string): SamlMetadataDocument {
61+
return { xml: fs.readFileSync(path, 'utf-8') };
62+
}
63+
64+
/**
65+
* The XML content of the metadata document
66+
*/
67+
public abstract readonly xml: string;
68+
}
69+
70+
/**
71+
* A SAML provider
72+
*/
73+
export class SamlProvider extends Resource implements ISamlProvider {
74+
/**
75+
* Import an existing provider
76+
*/
77+
public static fromSamlProviderArn(scope: Construct, id: string, samlProviderArn: string): ISamlProvider {
78+
class Import extends Resource implements ISamlProvider {
79+
public readonly samlProviderArn = samlProviderArn;
80+
}
81+
return new Import(scope, id);
82+
}
83+
84+
public readonly samlProviderArn: string;
85+
86+
constructor(scope: Construct, id: string, props: SamlProviderProps) {
87+
super(scope, id);
88+
89+
if (props.name && !Token.isUnresolved(props.name) && !/^[\w+=,.@-]{1,128}$/.test(props.name)) {
90+
throw new Error('Invalid SAML provider name. The name must be a string of characters consisting of upper and lowercase alphanumeric characters with no spaces. You can also include any of the following characters: _+=,.@-. Length must be between 1 and 128 characters.');
91+
}
92+
93+
const samlProvider = new CfnSAMLProvider(this, 'Resource', {
94+
name: this.physicalName,
95+
samlMetadataDocument: props.metadataDocument.xml,
96+
});
97+
98+
this.samlProviderArn = samlProvider.ref;
99+
}
100+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
"from-signature:@aws-cdk/aws-iam.Role.fromRoleArn",
106106
"construct-interface-extends-iconstruct:@aws-cdk/aws-iam.IManagedPolicy",
107107
"props-physical-name:@aws-cdk/aws-iam.OpenIdConnectProviderProps",
108+
"props-physical-name:@aws-cdk/aws-iam.SamlProviderProps",
108109
"resource-interface-extends-resource:@aws-cdk/aws-iam.IManagedPolicy",
109110
"docs-public-apis:@aws-cdk/aws-iam.IUser"
110111
]

packages/@aws-cdk/aws-iam/test/integ.saml-provider.expected.json

Lines changed: 34 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as path from 'path';
2+
import { App, Stack, StackProps } from '@aws-cdk/core';
3+
import { Construct } from 'constructs';
4+
import * as iam from '../lib';
5+
6+
class TestStack extends Stack {
7+
constructor(scope: Construct, id: string, props?: StackProps) {
8+
super(scope, id, props);
9+
10+
const provider = new iam.SamlProvider(this, 'Provider', {
11+
metadataDocument: iam.SamlMetadataDocument.fromFile(path.join(__dirname, 'saml-metadata-document.xml')),
12+
});
13+
14+
new iam.Role(this, 'Role', {
15+
assumedBy: new iam.SamlConsolePrincipal(provider),
16+
});
17+
}
18+
}
19+
20+
const app = new App();
21+
new TestStack(app, 'cdk-saml-provider');
22+
app.synth();

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

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,42 @@ test('use OpenID Connect principal from provider', () => {
127127

128128
// THEN
129129
expect(stack.resolve(principal.federated)).toStrictEqual({ Ref: 'MyProvider730BA1C8' });
130-
});
130+
});
131+
132+
test('SAML principal', () => {
133+
// GIVEN
134+
const stack = new Stack();
135+
const provider = new iam.SamlProvider(stack, 'MyProvider', {
136+
metadataDocument: iam.SamlMetadataDocument.fromXml('document'),
137+
});
138+
139+
// WHEN
140+
const principal = new iam.SamlConsolePrincipal(provider);
141+
new iam.Role(stack, 'Role', {
142+
assumedBy: principal,
143+
});
144+
145+
// THEN
146+
expect(stack.resolve(principal.federated)).toStrictEqual({ Ref: 'MyProvider730BA1C8' });
147+
expect(stack).toHaveResource('AWS::IAM::Role', {
148+
AssumeRolePolicyDocument: {
149+
Statement: [
150+
{
151+
Action: 'sts:AssumeRoleWithSAML',
152+
Condition: {
153+
StringEquals: {
154+
'SAML:aud': 'https://signin.aws.amazon.com/saml',
155+
},
156+
},
157+
Effect: 'Allow',
158+
Principal: {
159+
Federated: {
160+
Ref: 'MyProvider730BA1C8',
161+
},
162+
},
163+
},
164+
],
165+
Version: '2012-10-17',
166+
},
167+
});
168+
});

0 commit comments

Comments
 (0)