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): SAML identity provider #13393

Merged
merged 4 commits into from
Mar 8, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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: fs.readFileSync('/path/to/saml-metadata-document.xml', 'utf-8'),
});
```

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

```ts
const provider = new iam.SamlProvider(this, 'Provider', {
metadataDocument: fs.readFileSync('/path/to/saml-metadata-document.xml', 'utf-8'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we abstract this away? I don't think anybody would want to inline the XML and read from a file would be the default. So what about providing only a file name to the L2?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In most cases you are right but it could be returned by a custom resource?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what about a SamlMetadata class that has fromInline and fromFile?

});
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: fs.readFileSync('/path/to/saml-metadata-document.xml', 'utf-8'),
});
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
75 changes: 75 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,75 @@
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: 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,
});

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.

23 changes: 23 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,23 @@
import * as fs from 'fs';
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: fs.readFileSync(path.join(__dirname, 'saml-metadata-document.xml'), 'utf-8'),
});

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: '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