Skip to content

Commit

Permalink
feat(iam): SAML identity provider (#13393)
Browse files Browse the repository at this point in the history
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*
  • Loading branch information
jogold authored Mar 8, 2021
1 parent 90dbfb5 commit faa0c06
Show file tree
Hide file tree
Showing 10 changed files with 473 additions and 1 deletion.
41 changes: 41 additions & 0 deletions packages/@aws-cdk/aws-iam/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,47 @@ const provider = new iam.OpenIdConnectProvider(this, 'MyProvider', {
const principal = new iam.OpenIdConnectPrincipal(provider);
```

## SAML provider

An IAM SAML 2.0 identity provider is an entity in IAM that describes an external
identity provider (IdP) service that supports the SAML 2.0 (Security Assertion
Markup Language 2.0) standard. You use an IAM identity provider when you want
to establish trust between a SAML-compatible IdP such as Shibboleth or Active
Directory Federation Services and AWS, so that users in your organization can
access AWS resources. IAM SAML identity providers are used as principals in an
IAM trust policy.

```ts
new iam.SamlProvider(this, 'Provider', {
metadataDocument: iam.SamlMetadataDocument.fromFile('/path/to/saml-metadata-document.xml'),
});
```

The `SamlPrincipal` class can be used as a principal with a `SamlProvider`:

```ts
const provider = new iam.SamlProvider(this, 'Provider', {
metadataDocument: iam.SamlMetadataDocument.fromFile('/path/to/saml-metadata-document.xml'),
});
const principal = new iam.SamlPrincipal(provider, {
StringEquals: {
'SAML:iss': 'issuer',
},
});
```

When creating a role for programmatic and AWS Management Console access, use the `SamlConsolePrincipal`
class:

```ts
const provider = new iam.SamlProvider(this, 'Provider', {
metadataDocument: iam.SamlMetadataDocument.fromFile('/path/to/saml-metadata-document.xml'),
});
new iam.Role(this, 'Role', {
assumedBy: new iam.SamlConsolePrincipal(provider),
});
```

## Users

IAM manages users for your AWS account. To create a new user:
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-iam/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from './grant';
export * from './unknown-principal';
export * from './oidc-provider';
export * from './permissions-boundary';
export * from './saml-provider';

// AWS::IAM CloudFormation Resources:
export * from './iam.generated';
33 changes: 33 additions & 0 deletions packages/@aws-cdk/aws-iam/lib/principals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as cdk from '@aws-cdk/core';
import { Default, RegionInfo } from '@aws-cdk/region-info';
import { IOpenIdConnectProvider } from './oidc-provider';
import { Condition, Conditions, PolicyStatement } from './policy-statement';
import { ISamlProvider } from './saml-provider';
import { mergePrincipal } from './util';

/**
Expand Down Expand Up @@ -493,6 +494,38 @@ export class OpenIdConnectPrincipal extends WebIdentityPrincipal {
}
}

/**
* Principal entity that represents a SAML federated identity provider
*/
export class SamlPrincipal extends FederatedPrincipal {
constructor(samlProvider: ISamlProvider, conditions: Conditions) {
super(samlProvider.samlProviderArn, conditions, 'sts:AssumeRoleWithSAML');
}

public toString() {
return `SamlPrincipal(${this.federated})`;
}
}

/**
* Principal entity that represents a SAML federated identity provider for
* programmatic and AWS Management Console access.
*/
export class SamlConsolePrincipal extends SamlPrincipal {
constructor(samlProvider: ISamlProvider, conditions: Conditions = {}) {
super(samlProvider, {
...conditions,
StringEquals: {
'SAML:aud': 'https://signin.aws.amazon.com/saml',
},
});
}

public toString() {
return `SamlConsolePrincipal(${this.federated})`;
}
}

/**
* Use the AWS account into which a stack is deployed as the principal entity in a policy
*/
Expand Down
100 changes: 100 additions & 0 deletions packages/@aws-cdk/aws-iam/lib/saml-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import * as fs from 'fs';
import { IResource, Resource, Token } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CfnSAMLProvider } from './iam.generated';

/**
* A SAML provider
*/
export interface ISamlProvider extends IResource {
/**
* The Amazon Resource Name (ARN) of the provider
*
* @attribute
*/
readonly samlProviderArn: string;
}

/**
* Properties for a SAML provider
*/
export interface SamlProviderProps {
/**
* The name of the provider to create.
*
* This parameter allows 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.
*
* @default - a CloudFormation generated name
*/
readonly name?: string;

/**
* An XML document generated by an identity provider (IdP) that supports
* SAML 2.0. The document includes the issuer's name, expiration information,
* and keys that can be used to validate the SAML authentication response
* (assertions) that are received from the IdP. You must generate the metadata
* document using the identity management software that is used as your
* organization's IdP.
*/
readonly metadataDocument: SamlMetadataDocument;
}

/**
* A SAML metadata document
*/
export abstract class SamlMetadataDocument {
/**
* Create a SAML metadata document from a XML string
*/
public static fromXml(xml: string): SamlMetadataDocument {
return { xml };
}

/**
* Create a SAML metadata document from a XML file
*/
public static fromFile(path: string): SamlMetadataDocument {
return { xml: fs.readFileSync(path, 'utf-8') };
}

/**
* The XML content of the metadata document
*/
public abstract readonly xml: string;
}

/**
* A SAML provider
*/
export class SamlProvider extends Resource implements ISamlProvider {
/**
* Import an existing provider
*/
public static fromSamlProviderArn(scope: Construct, id: string, samlProviderArn: string): ISamlProvider {
class Import extends Resource implements ISamlProvider {
public readonly samlProviderArn = samlProviderArn;
}
return new Import(scope, id);
}

public readonly samlProviderArn: string;

constructor(scope: Construct, id: string, props: SamlProviderProps) {
super(scope, id);

if (props.name && !Token.isUnresolved(props.name) && !/^[\w+=,.@-]{1,128}$/.test(props.name)) {
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.');
}

const samlProvider = new CfnSAMLProvider(this, 'Resource', {
name: this.physicalName,
samlMetadataDocument: props.metadataDocument.xml,
});

this.samlProviderArn = samlProvider.ref;
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-iam/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
"from-signature:@aws-cdk/aws-iam.Role.fromRoleArn",
"construct-interface-extends-iconstruct:@aws-cdk/aws-iam.IManagedPolicy",
"props-physical-name:@aws-cdk/aws-iam.OpenIdConnectProviderProps",
"props-physical-name:@aws-cdk/aws-iam.SamlProviderProps",
"resource-interface-extends-resource:@aws-cdk/aws-iam.IManagedPolicy",
"docs-public-apis:@aws-cdk/aws-iam.IUser"
]
Expand Down
34 changes: 34 additions & 0 deletions packages/@aws-cdk/aws-iam/test/integ.saml-provider.expected.json

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions packages/@aws-cdk/aws-iam/test/integ.saml-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as path from 'path';
import { App, Stack, StackProps } from '@aws-cdk/core';
import { Construct } from 'constructs';
import * as iam from '../lib';

class TestStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);

const provider = new iam.SamlProvider(this, 'Provider', {
metadataDocument: iam.SamlMetadataDocument.fromFile(path.join(__dirname, 'saml-metadata-document.xml')),
});

new iam.Role(this, 'Role', {
assumedBy: new iam.SamlConsolePrincipal(provider),
});
}
}

const app = new App();
new TestStack(app, 'cdk-saml-provider');
app.synth();
40 changes: 39 additions & 1 deletion packages/@aws-cdk/aws-iam/test/principals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,42 @@ test('use OpenID Connect principal from provider', () => {

// THEN
expect(stack.resolve(principal.federated)).toStrictEqual({ Ref: 'MyProvider730BA1C8' });
});
});

test('SAML principal', () => {
// GIVEN
const stack = new Stack();
const provider = new iam.SamlProvider(stack, 'MyProvider', {
metadataDocument: iam.SamlMetadataDocument.fromXml('document'),
});

// WHEN
const principal = new iam.SamlConsolePrincipal(provider);
new iam.Role(stack, 'Role', {
assumedBy: principal,
});

// THEN
expect(stack.resolve(principal.federated)).toStrictEqual({ Ref: 'MyProvider730BA1C8' });
expect(stack).toHaveResource('AWS::IAM::Role', {
AssumeRolePolicyDocument: {
Statement: [
{
Action: 'sts:AssumeRoleWithSAML',
Condition: {
StringEquals: {
'SAML:aud': 'https://signin.aws.amazon.com/saml',
},
},
Effect: 'Allow',
Principal: {
Federated: {
Ref: 'MyProvider730BA1C8',
},
},
},
],
Version: '2012-10-17',
},
});
});
Loading

0 comments on commit faa0c06

Please sign in to comment.