diff --git a/packages/@aws-cdk/aws-eks/README.md b/packages/@aws-cdk/aws-eks/README.md index dd41a74f879d3..4f9103c9504fa 100644 --- a/packages/@aws-cdk/aws-eks/README.md +++ b/packages/@aws-cdk/aws-eks/README.md @@ -518,9 +518,37 @@ cluster.addCapacity('BottlerocketNodes', { To define only Bottlerocket capacity in your cluster, set `defaultCapacity` to `0` when you define the cluster as described above. -Please note Bottlerocket does not allow to customize bootstrap options and `bootstrapOptions` properties is not supported when you create the `Bottlerocket` capacity. +Please note Bottlerocket does not allow to customize bootstrap options and `bootstrapOptions` properties is not supported when you create the `Bottlerocket` capacity. +### Service Accounts +With services account you can provide Kubernetes Pods access to AWS resources. + +```ts +// add service account +const serviceAccount = cluster.addServiceAccount('MyServiceAccount'); + +const bucket = new Bucket(this, 'Bucket'); +bucket.grantReadWrite(serviceAccount); + +cluster.addResource('mypod', { + apiVersion: 'v1', + kind: 'Pod', + metadata: { name: 'mypod' }, + spec: { + containers: [ + { + name: 'hello', + image: 'paulbouwer/hello-kubernetes:1.5', + ports: [ { containerPort: 8080 } ], + serviceAccountName: serviceAccount.serviceAccountName + } + ] + } +}); +``` + +> Warning: Currently there are no condition set on the IAM Role which results that there are no restrictions on other pods to assume the role. This will be improved in the near future. ### Roadmap diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts index 45ec209a0c15c..47449325d4304 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts @@ -198,6 +198,7 @@ export class ClusterResourceHandler extends ResourceHandler { Endpoint: cluster.endpoint, Arn: cluster.arn, CertificateAuthorityData: cluster.certificateAuthority?.data, + OpenIdConnectIssuerUrl: cluster.identity?.oidc?.issuer, }, }; } diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts index c7e17fe25d308..e4c8f13917e8c 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts @@ -18,6 +18,7 @@ export class ClusterResource extends Construct { public readonly attrEndpoint: string; public readonly attrArn: string; public readonly attrCertificateAuthorityData: string; + public readonly attrOpenIdConnectIssuerUrl: string; public readonly ref: string; /** @@ -124,6 +125,7 @@ export class ClusterResource extends Construct { this.attrEndpoint = Token.asString(resource.getAtt('Endpoint')); this.attrArn = Token.asString(resource.getAtt('Arn')); this.attrCertificateAuthorityData = Token.asString(resource.getAtt('CertificateAuthorityData')); + this.attrOpenIdConnectIssuerUrl = Token.asString(resource.getAtt('OpenIdConnectIssuerUrl')); } /** diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index e5dd1719c736d..296e7beb07fe4 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -12,6 +12,7 @@ import { KubernetesPatch } from './k8s-patch'; import { KubernetesResource } from './k8s-resource'; import { KubectlProvider } from './kubectl-provider'; import { Nodegroup, NodegroupOptions } from './managed-nodegroup'; +import { ServiceAccount, ServiceAccountOptions } from './service-account'; import { LifecycleLabel, renderAmazonLinuxUserData, renderBottlerocketUserData } from './user-data'; // defaults are based on https://eksctl.io @@ -342,6 +343,8 @@ export class Cluster extends Resource implements ICluster { */ private _awsAuth?: AwsAuth; + private _openIdConnectProvider?: iam.OpenIdConnectProvider; + private _spotInterruptHandler?: HelmChart; private readonly version: string | undefined; @@ -617,6 +620,48 @@ export class Cluster extends Resource implements ICluster { return this._awsAuth; } + /** + * If this cluster is kubectl-enabled, returns the OpenID Connect issuer url. + * This is because the values is only be retrieved by the API and not exposed + * by CloudFormation. If this cluster is not kubectl-enabled (i.e. uses the + * stock `CfnCluster`), this is `undefined`. + * @attribute + */ + public get clusterOpenIdConnectIssuerUrl(): string { + if (!this._clusterResource) { + throw new Error('unable to obtain OpenID Connect issuer URL. Cluster must be kubectl-enabled'); + } + + return this._clusterResource.attrOpenIdConnectIssuerUrl; + } + + /** + * An `OpenIdConnectProvider` resource associated with this cluster, and which can be used + * to link this cluster to AWS IAM. + * + * A provider will only be defined if this property is accessed (lazy initialization). + */ + public get openIdConnectProvider() { + if (!this.kubectlEnabled) { + throw new Error('Cannot specify a OpenID Connect Provider if kubectl is disabled'); + } + + if (!this._openIdConnectProvider) { + this._openIdConnectProvider = new iam.OpenIdConnectProvider(this, 'OpenIdConnectProvider', { + url: this.clusterOpenIdConnectIssuerUrl, + clientIds: [ 'sts.amazonaws.com' ], + /** + * For some reason EKS isn't validating the root certificate but a intermediat certificate + * which is one level up in the tree. Because of the a constant thumbprint value has to be + * stated with this OpenID Connect provider. The certificate thumbprint is the same for all the regions. + */ + thumbprints: [ '9e99a48a9960b14926bb7f3b02e22da2b0ab7280' ], + }); + } + + return this._openIdConnectProvider; + } + /** * Defines a Kubernetes resource in this cluster. * @@ -657,6 +702,19 @@ export class Cluster extends Resource implements ICluster { }); } + /** + * Adds a service account to this cluster. + * + * @param id the id of this service account + * @param options service account options + */ + public addServiceAccount(id: string, options: ServiceAccountOptions) { + return new ServiceAccount(this, id, { + ...options, + cluster: this, + }); + } + /** * Returns the role ARN for the cluster creation role for kubectl-enabled * clusters. diff --git a/packages/@aws-cdk/aws-eks/lib/index.ts b/packages/@aws-cdk/aws-eks/lib/index.ts index da92e2173d09e..5e1009d98eec7 100644 --- a/packages/@aws-cdk/aws-eks/lib/index.ts +++ b/packages/@aws-cdk/aws-eks/lib/index.ts @@ -7,4 +7,5 @@ export * from './helm-chart'; export * from './k8s-patch'; export * from './k8s-resource'; export * from './fargate-cluster'; +export * from './service-account'; export * from './managed-nodegroup'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/lib/service-account.ts b/packages/@aws-cdk/aws-eks/lib/service-account.ts new file mode 100644 index 0000000000000..93d43f700139b --- /dev/null +++ b/packages/@aws-cdk/aws-eks/lib/service-account.ts @@ -0,0 +1,95 @@ +import { AddToPrincipalPolicyResult, IPrincipal, IRole, OpenIdConnectPrincipal, PolicyStatement, PrincipalPolicyFragment, Role } from '@aws-cdk/aws-iam'; +import { Construct } from '@aws-cdk/core'; +import { Cluster } from './cluster'; + +/** + * Options for `ServiceAccount` + */ +export interface ServiceAccountOptions { + /** + * The name of the service account. + * @default - If no name is given, it will use the id of the resource. + */ + readonly name?: string; + + /** + * The namespace of the service account. + * @default "default" + */ + readonly namespace?: string; +} + +/** + * Properties for defining service accounts + */ +export interface ServiceAccountProps extends ServiceAccountOptions { + /** + * The cluster to apply the patch to. + * [disable-awslint:ref-via-interface] + */ + readonly cluster: Cluster; +} + +/** + * Service Account + */ +export class ServiceAccount extends Construct implements IPrincipal { + + /** + * The role which is linked to the service account. + */ + public readonly role: IRole; + + public readonly assumeRoleAction: string; + public readonly grantPrincipal: IPrincipal; + public readonly policyFragment: PrincipalPolicyFragment; + + /** + * The name of the service account. + */ + public readonly serviceAccountName: string; + + /** + * The namespace where the service account is located in. + */ + public readonly serviceAccountNamespace: string; + + constructor(scope: Construct, id: string, props: ServiceAccountProps) { + super(scope, id); + + const { cluster } = props; + this.serviceAccountName = props.name ?? this.node.uniqueId.toLowerCase(); + this.serviceAccountNamespace = props.namespace ?? 'default'; + + this.role = new Role(this, 'Role', { + assumedBy: new OpenIdConnectPrincipal(cluster.openIdConnectProvider), + }); + + this.assumeRoleAction = this.role.assumeRoleAction; + this.grantPrincipal = this.role.grantPrincipal; + this.policyFragment = this.role.policyFragment; + + cluster.addResource('ServiceAccount', { + apiVersion: 'v1', + kind: 'ServiceAccount', + metadata: { + name: this.serviceAccountName, + namespace: this.serviceAccountNamespace, + labels: { + 'app.kubernetes.io/name': this.serviceAccountName, + }, + annotations: { + 'eks.amazonaws.com/role-arn': this.role.roleArn, + }, + }, + }); + } + + public addToPolicy(statement: PolicyStatement): boolean { + return this.role.addToPolicy(statement); + } + + public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult { + return this.role.addToPrincipalPolicy(statement); + } +} diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json index 67ad7d6a50a2c..982a27933f700 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json @@ -2224,7 +2224,7 @@ }, "/", { - "Ref": "AssetParameters222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2S3BucketC839B0E2" + "Ref": "AssetParameters01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9S3BucketDF419A16" }, "/", { @@ -2234,7 +2234,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2S3VersionKeyEEF27FE8" + "Ref": "AssetParameters01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9S3VersionKeyAA30989B" } ] } @@ -2247,7 +2247,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2S3VersionKeyEEF27FE8" + "Ref": "AssetParameters01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9S3VersionKeyAA30989B" } ] } @@ -2257,11 +2257,11 @@ ] }, "Parameters": { - "referencetoawscdkeksclustertestAssetParameters80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5S3Bucket278A73D2Ref": { - "Ref": "AssetParameters80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5S3Bucket91F3EC34" + "referencetoawscdkeksclustertestAssetParameters35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebeS3Bucket21CE03E4Ref": { + "Ref": "AssetParameters35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebeS3BucketE9BEFBC2" }, - "referencetoawscdkeksclustertestAssetParameters80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5S3VersionKeyD7A198A8Ref": { - "Ref": "AssetParameters80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5S3VersionKey29EF2E8E" + "referencetoawscdkeksclustertestAssetParameters35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebeS3VersionKey7161DBC6Ref": { + "Ref": "AssetParameters35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebeS3VersionKeyC7391006" }, "referencetoawscdkeksclustertestAssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3BucketC7CBF350Ref": { "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C" @@ -2406,17 +2406,17 @@ } }, "Parameters": { - "AssetParameters80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5S3Bucket91F3EC34": { + "AssetParameters35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebeS3BucketE9BEFBC2": { "Type": "String", - "Description": "S3 bucket for asset \"80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5\"" + "Description": "S3 bucket for asset \"35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebe\"" }, - "AssetParameters80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5S3VersionKey29EF2E8E": { + "AssetParameters35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebeS3VersionKeyC7391006": { "Type": "String", - "Description": "S3 key for asset version \"80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5\"" + "Description": "S3 key for asset version \"35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebe\"" }, - "AssetParameters80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5ArtifactHash2145581C": { + "AssetParameters35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebeArtifactHash058BD37E": { "Type": "String", - "Description": "Artifact hash for asset \"80599c1e26718262302e982159fe61aff7a2bdd392db341e7e99cbfbf84a0be5\"" + "Description": "Artifact hash for asset \"35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebe\"" }, "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C": { "Type": "String", @@ -2442,17 +2442,17 @@ "Type": "String", "Description": "Artifact hash for asset \"a6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afb\"" }, - "AssetParameters222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2S3BucketC839B0E2": { + "AssetParameters01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9S3BucketDF419A16": { "Type": "String", - "Description": "S3 bucket for asset \"222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2\"" + "Description": "S3 bucket for asset \"01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9\"" }, - "AssetParameters222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2S3VersionKeyEEF27FE8": { + "AssetParameters01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9S3VersionKeyAA30989B": { "Type": "String", - "Description": "S3 key for asset version \"222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2\"" + "Description": "S3 key for asset version \"01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9\"" }, - "AssetParameters222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2ArtifactHashAF96C5C2": { + "AssetParameters01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9ArtifactHash93C28EE4": { "Type": "String", - "Description": "Artifact hash for asset \"222a8e375c233b55d0c3e20bc9ac98ffc8e51132f82b0f12f3cd1b7b22c562c2\"" + "Description": "Artifact hash for asset \"01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9\"" }, "AssetParameters36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbcS3Bucket2D824DEF": { "Type": "String", diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts b/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts index 9b9baaafab141..311116611a58f 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts @@ -99,6 +99,7 @@ export = { Endpoint: 'http://endpoint', Arn: 'arn:cluster-arn', CertificateAuthorityData: 'certificateAuthority-data', + OpenIdConnectIssuerUrl: undefined, }, }); test.done(); @@ -420,6 +421,7 @@ export = { Endpoint: 'http://endpoint', Arn: 'arn:cluster-arn', CertificateAuthorityData: 'certificateAuthority-data', + OpenIdConnectIssuerUrl: undefined, }, }); test.done(); diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster.ts b/packages/@aws-cdk/aws-eks/test/test.cluster.ts index 7ea3985ed4224..c99324b0a8ea1 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster.ts @@ -334,6 +334,7 @@ export = { test.throws(() => cluster.addCapacity('boo', { instanceType: new ec2.InstanceType('r5d.24xlarge'), mapRole: true }), /Cannot map instance IAM role to RBAC if kubectl is disabled for the cluster/); test.throws(() => new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }), /Unable to perform this operation since kubectl is not enabled for this cluster/); + test.throws(() => cluster.openIdConnectProvider, /Cannot specify a OpenID Connect Provider if kubectl is disabled/); test.done(); }, @@ -1133,4 +1134,36 @@ export = { })); test.done(); }, + 'if openIDConnectProvider a new OpenIDConnectProvider resource is created and exposed'(test: Test) { + // GIVEN + const { stack } = testFixtureNoVpc(); + const cluster = new eks.Cluster(stack, 'Cluster', { defaultCapacity: 0 }); + + // WHEN + const provider = cluster.openIdConnectProvider; + + // THEN + test.equal(provider, cluster.openIdConnectProvider, 'openIdConnect provider is different and created more than once.'); + expect(stack).to(haveResource('Custom::AWSCDKOpenIdConnectProvider', { + ServiceToken: { + 'Fn::GetAtt': [ + 'CustomAWSCDKOpenIdConnectProviderCustomResourceProviderHandlerF2C543E0', + 'Arn', + ], + }, + ClientIDList: [ + 'sts.amazonaws.com', + ], + ThumbprintList: [ + '9e99a48a9960b14926bb7f3b02e22da2b0ab7280', + ], + Url: { + 'Fn::GetAtt': [ + 'Cluster9EE0221C', + 'OpenIdConnectIssuerUrl', + ], + }, + })); + test.done(); + }, }}; diff --git a/packages/@aws-cdk/aws-eks/test/test.service-account.ts b/packages/@aws-cdk/aws-eks/test/test.service-account.ts new file mode 100644 index 0000000000000..9a3467023b47c --- /dev/null +++ b/packages/@aws-cdk/aws-eks/test/test.service-account.ts @@ -0,0 +1,61 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import * as iam from '@aws-cdk/aws-iam'; +import { Test } from 'nodeunit'; +import * as eks from '../lib'; +import { testFixtureCluster } from './util'; + +// tslint:disable:max-line-length + +export = { + 'add Service Account': { + 'defaults should have default namespace and lowercase unique id'(test: Test) { + // GIVEN + const { stack, cluster } = testFixtureCluster(); + + // WHEN + new eks.ServiceAccount(stack, 'MyServiceAccount', { cluster }); + + // THEN + expect(stack).to(haveResource(eks.KubernetesResource.RESOURCE_TYPE, { + ServiceToken: { + 'Fn::GetAtt': [ + 'awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B', + 'Outputs.StackawscdkawseksKubectlProviderframeworkonEvent8897FD9BArn', + ], + }, + Manifest: { + 'Fn::Join': [ + '', + [ + '[{\"apiVersion\":\"v1\",\"kind\":\"ServiceAccount\",\"metadata\":{\"name\":\"stackmyserviceaccount58b9529e\",\"namespace\":\"default\",\"labels\":{\"app.kubernetes.io/name\":\"stackmyserviceaccount58b9529e\"},\"annotations\":{\"eks.amazonaws.com/role-arn\":\"', + { + 'Fn::GetAtt': [ + 'MyServiceAccountRoleB41709FF', + 'Arn', + ], + }, + '\"}}}]', + ], + ], + }, + })); + expect(stack).to(haveResource(iam.CfnRole.CFN_RESOURCE_TYPE_NAME, { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRoleWithWebIdentity', + Effect: 'Allow', + Principal: { + Federated: { + Ref: 'ClusterOpenIdConnectProviderE7EB0530', + }, + }, + }, + ], + Version: '2012-10-17', + }, + })); + test.done(); + }, + }, +}; diff --git a/packages/@aws-cdk/aws-iam/README.md b/packages/@aws-cdk/aws-iam/README.md index 68b64d27c5024..1145fbe14f0f5 100644 --- a/packages/@aws-cdk/aws-iam/README.md +++ b/packages/@aws-cdk/aws-iam/README.md @@ -198,6 +198,17 @@ const principal = new iam.AccountPrincipal('123456789000') .withConditions({ StringEquals: { foo: "baz" } }); ``` +The `WebIdentityPrincipal` class can be used as a principal for web identities like +Cognito, Amazon, Google or Facebook, for example: + +```ts +const principal = new iam.WebIdentityPrincipal('cognito-identity.amazonaws.com') + .withConditions({ + "StringEquals": { "cognito-identity.amazonaws.com:aud": "us-east-2:12345678-abcd-abcd-abcd-123456" }, + "ForAnyValue:StringLike": {"cognito-identity.amazonaws.com:amr": "unauthenticated"} + }); +``` + ### Parsing JSON Policy Documents The `PolicyDocument.fromJson` and `PolicyStatement.fromJson` static methods can be used to parse JSON objects. For example: @@ -282,6 +293,16 @@ new cognito.CfnIdentityPool(this, 'IdentityPool', { }); ``` +The `OpenIdConnectPrincipal` class can be used as a principal used with a `OpenIdConnectProvider`, for example: + +```ts +const provider = new OpenIdConnectProvider(this, 'MyProvider', { + url: 'https://openid/connect', + clients: [ 'myclient1', 'myclient2' ] +}); +const principal = new iam.OpenIdConnectPrincipal(provider); +``` + ### Features * Policy name uniqueness is enforced. If two policies by the same name are attached to the same diff --git a/packages/@aws-cdk/aws-iam/lib/principals.ts b/packages/@aws-cdk/aws-iam/lib/principals.ts index fcf8db593bd35..15f39638f5509 100644 --- a/packages/@aws-cdk/aws-iam/lib/principals.ts +++ b/packages/@aws-cdk/aws-iam/lib/principals.ts @@ -1,5 +1,6 @@ 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 { mergePrincipal } from './util'; @@ -417,6 +418,55 @@ export class FederatedPrincipal extends PrincipalBase { } } +/** + * A principal that represents a federated identity provider as Web Identity such as Cognito, Amazon, + * Facebook, Google, etc. + */ +export class WebIdentityPrincipal extends FederatedPrincipal { + + /** + * + * @param identityProvider identity provider (i.e. 'cognito-identity.amazonaws.com' for users authenticated through Cognito) + * @param conditions The conditions under which the policy is in effect. + * See [the IAM documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html). + */ + constructor(identityProvider: string, conditions: Conditions = {}) { + super(identityProvider, conditions ?? {}, 'sts:AssumeRoleWithWebIdentity'); + } + + public get policyFragment(): PrincipalPolicyFragment { + return new PrincipalPolicyFragment({ Federated: [this.federated] }, this.conditions); + } + + public toString() { + return `WebIdentityPrincipal(${this.federated})`; + } +} + +/** + * A principal that represents a federated identity provider as from a OpenID Connect provider. + */ +export class OpenIdConnectPrincipal extends WebIdentityPrincipal { + + /** + * + * @param openIdConnectProvider OpenID Connect provider + * @param conditions The conditions under which the policy is in effect. + * See [the IAM documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html). + */ + constructor(openIdConnectProvider: IOpenIdConnectProvider, conditions: Conditions = {}) { + super(openIdConnectProvider.openIdConnectProviderArn, conditions ?? {}); + } + + public get policyFragment(): PrincipalPolicyFragment { + return new PrincipalPolicyFragment({ Federated: [this.federated] }, this.conditions); + } + + public toString() { + return `OpenIdConnectPrincipal(${this.federated})`; + } +} + /** * Use the AWS account into which a stack is deployed as the principal entity in a policy */ diff --git a/packages/@aws-cdk/aws-iam/test/principals.test.ts b/packages/@aws-cdk/aws-iam/test/principals.test.ts index a78d31bf9cea9..b1d7bc0d169f1 100644 --- a/packages/@aws-cdk/aws-iam/test/principals.test.ts +++ b/packages/@aws-cdk/aws-iam/test/principals.test.ts @@ -101,4 +101,30 @@ test('can have multiple principals the same conditions in the same statement', ( }), ], })); +}); + +test('use Web Identity principal', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const principal = new iam.WebIdentityPrincipal('cognito-identity.amazonaws.com'); + + // THEN + expect(stack.resolve(principal.federated)).toStrictEqual('cognito-identity.amazonaws.com'); + expect(stack.resolve(principal.assumeRoleAction)).toStrictEqual('sts:AssumeRoleWithWebIdentity'); +}); + +test('use OpenID Connect principal from provider', () => { + // GIVEN + const stack = new Stack(); + const provider = new iam.OpenIdConnectProvider(stack, 'MyProvider', { + url: 'https://openid-endpoint', + }); + + // WHEN + const principal = new iam.OpenIdConnectPrincipal(provider); + + // THEN + expect(stack.resolve(principal.federated)).toStrictEqual({ Ref: 'MyProvider730BA1C8' }); }); \ No newline at end of file