From 0633c6b32173d2cd4b7283c6e0f46b4b09bbaf03 Mon Sep 17 00:00:00 2001
From: Ben Limmer <630449+blimmer@users.noreply.github.com>
Date: Sat, 25 Mar 2023 16:58:29 -0600
Subject: [PATCH] feat: initial implementation (#1)
---
.github/workflows/release.yml | 2 +-
.projen/tasks.json | 3 +-
.projenrc.js | 6 +-
.vscode/settings.json | 3 +
API.md | 448 ++++++++++++++++++++++++++++++
README.md | 103 ++++++-
package.json | 4 +-
src/CircleCiOidcProvider.ts | 50 ++++
src/CircleCiOidcRole.ts | 90 ++++++
src/index.ts | 7 +-
test/CircleCiOidcProvider.test.ts | 28 ++
test/CircleCiOidcRole.test.ts | 147 ++++++++++
test/hello.test.ts | 5 -
13 files changed, 879 insertions(+), 17 deletions(-)
create mode 100644 .vscode/settings.json
create mode 100644 API.md
create mode 100644 src/CircleCiOidcProvider.ts
create mode 100644 src/CircleCiOidcRole.ts
create mode 100644 test/CircleCiOidcProvider.test.ts
create mode 100644 test/CircleCiOidcRole.test.ts
delete mode 100644 test/hello.test.ts
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 0475824..69fe79d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -71,7 +71,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_REF: ${{ github.ref }}
- run: errout=$(mktemp); gh release create $(cat dist/releasetag.txt) -R $GITHUB_REPOSITORY -F dist/changelog.md -t $(cat dist/releasetag.txt) --target $GITHUB_REF 2> $errout && true; exitcode=$?; if [ $exitcode -ne 0 ] && ! grep -q "Release.tag_name already exists" $errout; then cat $errout; exit $exitcode; fi
+ run: errout=$(mktemp); gh release create $(cat dist/releasetag.txt) -R $GITHUB_REPOSITORY -F dist/changelog.md -t $(cat dist/releasetag.txt) --target $GITHUB_REF -p 2> $errout && true; exitcode=$?; if [ $exitcode -ne 0 ] && ! grep -q "Release.tag_name already exists" $errout; then cat $errout; exit $exitcode; fi
release_npm:
name: Publish to npm
needs: release
diff --git a/.projen/tasks.json b/.projen/tasks.json
index 57e5a46..f865c6f 100644
--- a/.projen/tasks.json
+++ b/.projen/tasks.json
@@ -178,7 +178,8 @@
"name": "release",
"description": "Prepare a release from \"main\" branch",
"env": {
- "RELEASE": "true"
+ "RELEASE": "true",
+ "PRERELEASE": "beta"
},
"steps": [
{
diff --git a/.projenrc.js b/.projenrc.js
index a42f0d5..715e905 100644
--- a/.projenrc.js
+++ b/.projenrc.js
@@ -4,8 +4,10 @@ const project = new awscdk.AwsCdkConstructLibrary({
authorAddress: 'hello@benlimmer.com',
cdkVersion: '2.1.0',
defaultReleaseBranch: 'main',
- name: 'circleci-oidc',
- repositoryUrl: 'https://github.com/blimmer/circleci-oidc.git',
+ name: '@blimmer/cdk-circleci-oidc',
+ repositoryUrl: 'https://github.com/blimmer/cdk-circleci-oidc.git',
+
+ prerelease: 'beta',
// deps: [], /* Runtime dependencies of this module. */
// description: undefined, /* The description is just a string that helps people understand the purpose of the package. */
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..30fd6c0
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "jest.jestCommandLine": "yarn projen test"
+}
diff --git a/API.md b/API.md
new file mode 100644
index 0000000..8871564
--- /dev/null
+++ b/API.md
@@ -0,0 +1,448 @@
+# API Reference
+
+## Constructs
+
+### CircleCiOidcProvider
+
+This construct creates a CircleCI ODIC provider to allow AWS access from CircleCI jobs.
+
+You'll need to instantiate
+this construct once per AWS account you want to use CircleCI OIDC with.
+
+To create a role that can be assumed by CircleCI jobs, use the `CircleCiOidcRole` construct.
+
+#### Initializers
+
+```typescript
+import { CircleCiOidcProvider } from '@blimmer/cdk-circleci-oidc'
+
+new CircleCiOidcProvider(scope: Construct, id: string, props: CircleCiOidcProviderProps)
+```
+
+| **Name** | **Type** | **Description** |
+| --- | --- | --- |
+| scope
| constructs.Construct
| *No description.* |
+| id
| string
| *No description.* |
+| props
| CircleCiOidcProviderProps
| *No description.* |
+
+---
+
+##### `scope`Required
+
+- *Type:* constructs.Construct
+
+---
+
+##### `id`Required
+
+- *Type:* string
+
+---
+
+##### `props`Required
+
+- *Type:* CircleCiOidcProviderProps
+
+---
+
+#### Methods
+
+| **Name** | **Description** |
+| --- | --- |
+| toString
| Returns a string representation of this construct. |
+
+---
+
+##### `toString`
+
+```typescript
+public toString(): string
+```
+
+Returns a string representation of this construct.
+
+#### Static Functions
+
+| **Name** | **Description** |
+| --- | --- |
+| isConstruct
| Checks if `x` is a construct. |
+
+---
+
+##### ~~`isConstruct`~~
+
+```typescript
+import { CircleCiOidcProvider } from '@blimmer/cdk-circleci-oidc'
+
+CircleCiOidcProvider.isConstruct(x: any)
+```
+
+Checks if `x` is a construct.
+
+###### `x`Required
+
+- *Type:* any
+
+Any object.
+
+---
+
+#### Properties
+
+| **Name** | **Type** | **Description** |
+| --- | --- | --- |
+| node
| constructs.Node
| The tree node. |
+| organizationId
| string
| *No description.* |
+| provider
| aws-cdk-lib.aws_iam.CfnOIDCProvider
| *No description.* |
+
+---
+
+##### `node`Required
+
+```typescript
+public readonly node: Node;
+```
+
+- *Type:* constructs.Node
+
+The tree node.
+
+---
+
+##### `organizationId`Required
+
+```typescript
+public readonly organizationId: string;
+```
+
+- *Type:* string
+
+---
+
+##### `provider`Required
+
+```typescript
+public readonly provider: CfnOIDCProvider;
+```
+
+- *Type:* aws-cdk-lib.aws_iam.CfnOIDCProvider
+
+---
+
+
+### CircleCiOidcRole
+
+This construct creates a CircleCI ODIC provider to allow AWS access from CircleCI jobs.
+
+You'll need to instantiate
+this construct once per AWS account you want to use CircleCI OIDC with.
+
+To create a role that can be assumed by CircleCI jobs, use the `CircleCiOidcRole` construct.
+
+#### Initializers
+
+```typescript
+import { CircleCiOidcRole } from '@blimmer/cdk-circleci-oidc'
+
+new CircleCiOidcRole(scope: Construct, id: string, props: CircleCiOidcRoleProps)
+```
+
+| **Name** | **Type** | **Description** |
+| --- | --- | --- |
+| scope
| constructs.Construct
| *No description.* |
+| id
| string
| *No description.* |
+| props
| CircleCiOidcRoleProps
| *No description.* |
+
+---
+
+##### `scope`Required
+
+- *Type:* constructs.Construct
+
+---
+
+##### `id`Required
+
+- *Type:* string
+
+---
+
+##### `props`Required
+
+- *Type:* CircleCiOidcRoleProps
+
+---
+
+#### Methods
+
+| **Name** | **Description** |
+| --- | --- |
+| toString
| Returns a string representation of this construct. |
+
+---
+
+##### `toString`
+
+```typescript
+public toString(): string
+```
+
+Returns a string representation of this construct.
+
+#### Static Functions
+
+| **Name** | **Description** |
+| --- | --- |
+| isConstruct
| Checks if `x` is a construct. |
+
+---
+
+##### ~~`isConstruct`~~
+
+```typescript
+import { CircleCiOidcRole } from '@blimmer/cdk-circleci-oidc'
+
+CircleCiOidcRole.isConstruct(x: any)
+```
+
+Checks if `x` is a construct.
+
+###### `x`Required
+
+- *Type:* any
+
+Any object.
+
+---
+
+#### Properties
+
+| **Name** | **Type** | **Description** |
+| --- | --- | --- |
+| node
| constructs.Node
| The tree node. |
+| role
| aws-cdk-lib.aws_iam.Role
| *No description.* |
+
+---
+
+##### `node`Required
+
+```typescript
+public readonly node: Node;
+```
+
+- *Type:* constructs.Node
+
+The tree node.
+
+---
+
+##### `role`Required
+
+```typescript
+public readonly role: Role;
+```
+
+- *Type:* aws-cdk-lib.aws_iam.Role
+
+---
+
+
+## Structs
+
+### CircleCiOidcProviderProps
+
+#### Initializer
+
+```typescript
+import { CircleCiOidcProviderProps } from '@blimmer/cdk-circleci-oidc'
+
+const circleCiOidcProviderProps: CircleCiOidcProviderProps = { ... }
+```
+
+#### Properties
+
+| **Name** | **Type** | **Description** |
+| --- | --- | --- |
+| organizationId
| string
| The ID of your CircleCI organization. |
+| circleCiOidcThumbprints
| string[]
| The OIDC thumbprints used by the provider. |
+
+---
+
+##### `organizationId`Required
+
+```typescript
+public readonly organizationId: string;
+```
+
+- *Type:* string
+
+The ID of your CircleCI organization.
+
+This is typically in a UUID format. You can find this ID in the CircleCI
+dashboard UI under the "Organization Settings" tab.
+
+---
+
+##### `circleCiOidcThumbprints`Optional
+
+```typescript
+public readonly circleCiOidcThumbprints: string[];
+```
+
+- *Type:* string[]
+
+The OIDC thumbprints used by the provider.
+
+You should not need to provide this value unless CircleCI suddenly
+rotates their OIDC thumbprints (e.g., in response to a security incident).
+
+If you do need to generate this thumbprint, you can follow the instructions here:
+https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html
+
+---
+
+### CircleCiOidcRoleProps
+
+#### Initializer
+
+```typescript
+import { CircleCiOidcRoleProps } from '@blimmer/cdk-circleci-oidc'
+
+const circleCiOidcRoleProps: CircleCiOidcRoleProps = { ... }
+```
+
+#### Properties
+
+| **Name** | **Type** | **Description** |
+| --- | --- | --- |
+| circleCiOidcProvider
| CircleCiOidcProvider \| ManualCircleCiOidcProviderProps
| *No description.* |
+| circleCiProjectIds
| string[]
| Provide the UUID(s) of the CircleCI project(s) you want to be allowed to use this role. |
+| description
| string
| *No description.* |
+| inlinePolicies
| {[ key: string ]: aws-cdk-lib.aws_iam.PolicyDocument}
| *No description.* |
+| managedPolicies
| aws-cdk-lib.aws_iam.IManagedPolicy[]
| *No description.* |
+| roleName
| string
| You can pass an explicit role name if you'd like, since you need to reference the Role ARN within your CircleCI configuration. |
+
+---
+
+##### `circleCiOidcProvider`Required
+
+```typescript
+public readonly circleCiOidcProvider: CircleCiOidcProvider | ManualCircleCiOidcProviderProps;
+```
+
+- *Type:* CircleCiOidcProvider | ManualCircleCiOidcProviderProps
+
+---
+
+##### `circleCiProjectIds`Optional
+
+```typescript
+public readonly circleCiProjectIds: string[];
+```
+
+- *Type:* string[]
+- *Default:* All CircleCI projects in the provider's organization
+
+Provide the UUID(s) of the CircleCI project(s) you want to be allowed to use this role.
+
+If you don't provide this
+value, the role will be allowed to be assumed by any CircleCI project in your organization. You can find a
+project's ID in the CircleCI dashboard UI under the "Project Settings" tab. It's usually in a UUID format.
+
+---
+
+##### `description`Optional
+
+```typescript
+public readonly description: string;
+```
+
+- *Type:* string
+
+---
+
+##### `inlinePolicies`Optional
+
+```typescript
+public readonly inlinePolicies: {[ key: string ]: PolicyDocument};
+```
+
+- *Type:* {[ key: string ]: aws-cdk-lib.aws_iam.PolicyDocument}
+
+---
+
+##### `managedPolicies`Optional
+
+```typescript
+public readonly managedPolicies: IManagedPolicy[];
+```
+
+- *Type:* aws-cdk-lib.aws_iam.IManagedPolicy[]
+
+---
+
+##### `roleName`Optional
+
+```typescript
+public readonly roleName: string;
+```
+
+- *Type:* string
+- *Default:* CloudFormation will auto-generate you a role name
+
+You can pass an explicit role name if you'd like, since you need to reference the Role ARN within your CircleCI configuration.
+
+---
+
+### ManualCircleCiOidcProviderProps
+
+If you're using the {@link CircleCiOidcProvider} construct, pass it instead of these manually-defined props.
+
+#### Initializer
+
+```typescript
+import { ManualCircleCiOidcProviderProps } from '@blimmer/cdk-circleci-oidc'
+
+const manualCircleCiOidcProviderProps: ManualCircleCiOidcProviderProps = { ... }
+```
+
+#### Properties
+
+| **Name** | **Type** | **Description** |
+| --- | --- | --- |
+| organizationId
| string
| The ID of your CircleCI organization. |
+| provider
| aws-cdk-lib.aws_iam.IOpenIdConnectProvider
| The CircleCI OIDC provider. |
+
+---
+
+##### `organizationId`Required
+
+```typescript
+public readonly organizationId: string;
+```
+
+- *Type:* string
+
+The ID of your CircleCI organization.
+
+This is typically in a UUID format. You can find this ID in the CircleCI
+dashboard UI under the "Organization Settings" tab.
+
+---
+
+##### `provider`Required
+
+```typescript
+public readonly provider: IOpenIdConnectProvider;
+```
+
+- *Type:* aws-cdk-lib.aws_iam.IOpenIdConnectProvider
+
+The CircleCI OIDC provider.
+
+You can either manually create it or import it.
+
+---
+
+
+
diff --git a/README.md b/README.md
index dcf15b5..c971b6f 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,104 @@
# CircleCI OIDC
-TODO
+This repository contains constructs to communicate between CircleCI and AWS via an Open ID Connect (OIDC) provider.
+The process is described in [this CircleCI blog post](https://circleci.com/blog/openid-connect-identity-tokens/).
+
+## Security Benefits
+
+By using the OpenID Connect provider, you can communicate with AWS from CircleCI without saving static credentials
+(e.g., `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`) in your CircleCI project settings or a context. Removing
+static credentials, especially in light of the early 2023 [breach](https://circleci.com/blog/jan-4-2023-incident-report/),
+is a best practice for security.
+
+## Quick Start
+
+Install the package:
+
+```bash
+npm install @blimmer/cdk-circleci-oidc
+
+or
+
+yarn add @blimmer/cdk-circleci-oidc
+```
+
+Then, create the provider and role(s).
+
+```typescript
+import { Stack, StackProps } from 'aws-cdk-lib';
+import { CircleCiOidcProvider, CircleCiOidcRole } from '@blimmer/cdk-circleci-oidc';
+import { Construct } from 'constructs';
+import { ManagedPolicy, PolicyStatement } from 'aws-cdk-lib/aws-iam';
+import { Bucket } from 'aws-cdk-lib/aws-s3';
+
+export class CircleCiStack extends Stack {
+ readonly provider: CircleCiOidcProvider; // export for use in other stacks
+
+ constructor(scope: Construct, id: string, props?: StackProps) {
+ super(scope, id, props);
+
+ this.provider = new CircleCiOidcProvider(this, 'OidcProvider', {
+ // Find your organization ID in the CircleCI dashboard under "Organization Settings"
+ organizationId: '11111111-2222-3333-4444-555555555555',
+ });
+
+ const myCircleCiRole = new CircleCiOidcRole(this, 'MyCircleCiRole', {
+ provider: this.provider,
+ roleName: "MyCircleCiRole",
+
+ // Pass some managed policies to the role
+ managedPolicies: [
+ ManagedPolicy.fromAwsManagedPolicyName('AmazonS3ReadOnlyAccess'),
+ ],
+ })
+
+ // You can also access the role from the construct. This allows adding roles and using `grant` methods after the
+ // construct has been created.
+ myCircleCiRole.role.addToPolicy(new PolicyStatement({
+ actions: ['s3:ListAllMyBuckets'],
+ resources: ['*'],
+ }));
+
+ const bucket = new Bucket(this, 'MyBucket');
+ bucket.grantRead(myCircleCiRole.role);
+ }
+}
+```
+
+Now, in your `.circleci/config.yml` file, you can use the [AWS CLI Orb](https://circleci.com/developer/orbs/orb/circleci/aws-cli)
+to assume your new role.
+
+```yaml
+version: 2.1
+
+orbs:
+ aws-cli: circleci/aws-cli@3.1.4 # https://circleci.com/developer/orbs/orb/circleci/aws-cli
+
+workflows:
+ version: 2
+ build:
+ jobs:
+ - oidc-job:
+ context: oidc-assumption # You _must_ use a context, even if it doesn't contain any secrets (see https://circleci.com/docs/openid-connect-tokens/#openid-connect-id-token-availability)
+
+jobs:
+ oidc-job:
+ docker:
+ - image: cimg/base:stable
+ steps:
+ - checkout
+ # https://circleci.com/developer/orbs/orb/circleci/aws-cli#commands-setup
+ - aws-cli/setup:
+ role-arn: 'arn:aws:iam::123456789101:role/MyCircleCiRole'
+ - run:
+ name: List S3 Buckets
+ command: aws s3 ls
+```
+
+## Usage
+
+For detailed API docs, see [API.md](/API.md).
+
+## Contributing
+
+Contributions, issues, and feedback are welcome!
diff --git a/package.json b/package.json
index 410996b..9e43c05 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
{
- "name": "circleci-oidc",
+ "name": "@blimmer/cdk-circleci-oidc",
"repository": {
"type": "git",
- "url": "https://github.com/blimmer/circleci-oidc.git"
+ "url": "https://github.com/blimmer/cdk-circleci-oidc.git"
},
"scripts": {
"build": "npx projen build",
diff --git a/src/CircleCiOidcProvider.ts b/src/CircleCiOidcProvider.ts
new file mode 100644
index 0000000..b6c93da
--- /dev/null
+++ b/src/CircleCiOidcProvider.ts
@@ -0,0 +1,50 @@
+import { CfnOIDCProvider } from 'aws-cdk-lib/aws-iam';
+import { Construct } from 'constructs';
+
+export interface CircleCiOidcProviderProps {
+ /**
+ * The ID of your CircleCI organization. This is typically in a UUID format. You can find this ID in the CircleCI
+ * dashboard UI under the "Organization Settings" tab.
+ */
+ readonly organizationId: string;
+
+ /**
+ * The OIDC thumbprints used by the provider. You should not need to provide this value unless CircleCI suddenly
+ * rotates their OIDC thumbprints (e.g., in response to a security incident).
+ *
+ * If you do need to generate this thumbprint, you can follow the instructions here:
+ * https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html
+ */
+ readonly circleCiOidcThumbprints?: string[];
+}
+
+/**
+ * This construct creates a CircleCI ODIC provider to allow AWS access from CircleCI jobs. You'll need to instantiate
+ * this construct once per AWS account you want to use CircleCI OIDC with.
+ *
+ * To create a role that can be assumed by CircleCI jobs, use the `CircleCiOidcRole` construct.
+ */
+export class CircleCiOidcProvider extends Construct {
+ public readonly provider: CfnOIDCProvider;
+ public readonly organizationId: string;
+
+ constructor(scope: Construct, id: string, props: CircleCiOidcProviderProps) {
+ super(scope, id);
+
+ const {
+ organizationId,
+ circleCiOidcThumbprints = ['9e99a48a9960b14926bb7f3b02e22da2b0ab7280'],
+ } = props;
+
+ // The L2 construct uses a Custom Resource, which is slow and has a few known issues
+ // (see https://github.com/aws/aws-cdk/issues/21197#issuecomment-1312843734)
+ // Therefore, we use the L1 OIDC provider construct directly instead.
+ this.provider = new CfnOIDCProvider(this, 'CircleCiOidcProvider', {
+ url: `https://oidc.circleci.com/org/${organizationId}`,
+ clientIdList: [organizationId],
+ thumbprintList: circleCiOidcThumbprints,
+ });
+
+ this.organizationId = organizationId;
+ }
+}
diff --git a/src/CircleCiOidcRole.ts b/src/CircleCiOidcRole.ts
new file mode 100644
index 0000000..d9d4d25
--- /dev/null
+++ b/src/CircleCiOidcRole.ts
@@ -0,0 +1,90 @@
+import { Condition, IManagedPolicy, IOpenIdConnectProvider, OpenIdConnectPrincipal, OpenIdConnectProvider, PolicyDocument, Role } from 'aws-cdk-lib/aws-iam';
+import { Construct } from 'constructs';
+import { CircleCiOidcProvider } from './CircleCiOidcProvider';
+
+/**
+ * If you're using the {@link CircleCiOidcProvider} construct, pass it instead of these manually-defined props.
+ */
+export interface ManualCircleCiOidcProviderProps {
+ /**
+ * The CircleCI OIDC provider. You can either manually create it or import it.
+ */
+ readonly provider: IOpenIdConnectProvider;
+
+ /**
+ * The ID of your CircleCI organization. This is typically in a UUID format. You can find this ID in the CircleCI
+ * dashboard UI under the "Organization Settings" tab.
+ */
+ readonly organizationId: string;
+}
+
+export interface CircleCiOidcRoleProps {
+ readonly circleCiOidcProvider: CircleCiOidcProvider | ManualCircleCiOidcProviderProps;
+
+ /**
+ * Provide the UUID(s) of the CircleCI project(s) you want to be allowed to use this role. If you don't provide this
+ * value, the role will be allowed to be assumed by any CircleCI project in your organization. You can find a
+ * project's ID in the CircleCI dashboard UI under the "Project Settings" tab. It's usually in a UUID format.
+ *
+ * @default - All CircleCI projects in the provider's organization
+ */
+ readonly circleCiProjectIds?: string[];
+
+ /**
+ * You can pass an explicit role name if you'd like, since you need to reference the Role ARN within your CircleCI
+ * configuration.
+ *
+ * @default - CloudFormation will auto-generate you a role name
+ */
+ readonly roleName?: string;
+
+ readonly managedPolicies?: IManagedPolicy[];
+ readonly inlinePolicies?: {
+ [name: string]: PolicyDocument;
+ };
+ readonly description?: string;
+}
+
+/**
+ * This construct creates a CircleCI ODIC provider to allow AWS access from CircleCI jobs. You'll need to instantiate
+ * this construct once per AWS account you want to use CircleCI OIDC with.
+ *
+ * To create a role that can be assumed by CircleCI jobs, use the `CircleCiOidcRole` construct.
+ */
+export class CircleCiOidcRole extends Construct {
+ readonly role: Role;
+
+ constructor(scope: Construct, id: string, props: CircleCiOidcRoleProps) {
+ super(scope, id);
+
+ const { circleCiProjectIds, circleCiOidcProvider, ...roleProps } = props;
+ const { provider, organizationId } = this.extractOpenIdConnectProvider(circleCiOidcProvider);
+ const oidcUrl = `oidc.circleci.com/org/${organizationId}`;
+
+ this.role = new Role(this, 'CircleCiOidcRole', {
+ assumedBy: new OpenIdConnectPrincipal(provider, {
+ StringEquals: { [`${oidcUrl}:aud`]: organizationId },
+ ...this.generateProjectCondition(oidcUrl, organizationId, circleCiProjectIds),
+ }),
+ ...roleProps,
+ });
+ }
+
+ private extractOpenIdConnectProvider(provider: CircleCiOidcProvider | ManualCircleCiOidcProviderProps) {
+ if (provider instanceof CircleCiOidcProvider) {
+ return { provider: OpenIdConnectProvider.fromOpenIdConnectProviderArn(this, 'ImportOidcProvider', provider.provider.attrArn), organizationId: provider.organizationId };
+ } else {
+ return provider;
+ }
+ }
+
+ private generateProjectCondition(oidcUrl: string, organizationId: string, circleCiProjectIds?: string[]): Condition {
+ if (!circleCiProjectIds || circleCiProjectIds.length === 0) {
+ return {};
+ }
+
+ return {
+ StringLike: { [`${oidcUrl}:sub`]: circleCiProjectIds.map((projectId) => `org/${organizationId}/project/${projectId}/*`) },
+ };
+ }
+}
diff --git a/src/index.ts b/src/index.ts
index fb2fabc..4d11816 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,5 +1,2 @@
-export class Hello {
- public sayHello() {
- return 'hello, world!';
- }
-}
+export * from './CircleCiOidcProvider';
+export * from './CircleCiOidcRole';
diff --git a/test/CircleCiOidcProvider.test.ts b/test/CircleCiOidcProvider.test.ts
new file mode 100644
index 0000000..a6bc305
--- /dev/null
+++ b/test/CircleCiOidcProvider.test.ts
@@ -0,0 +1,28 @@
+import { App, Stack } from 'aws-cdk-lib';
+import { Template } from 'aws-cdk-lib/assertions';
+import { CircleCiOidcProvider } from '../src';
+
+describe('CircleCiOidcProvider', () => {
+ it('uses the organization ID as the client ID', () => {
+ const app = new App();
+ const stack = new Stack(app, 'TestStack');
+ new CircleCiOidcProvider(stack, 'CircleCiOidcProvider', {
+ organizationId: '1234',
+ });
+
+ Template.fromStack(stack).hasResourceProperties('AWS::IAM::OIDCProvider', {
+ ClientIdList: ['1234'],
+ });
+ });
+ it('uses a default thumbprint list', () => {
+ const app = new App();
+ const stack = new Stack(app, 'TestStack');
+ new CircleCiOidcProvider(stack, 'CircleCiOidcProvider', {
+ organizationId: '1234',
+ });
+
+ Template.fromStack(stack).hasResourceProperties('AWS::IAM::OIDCProvider', {
+ ThumbprintList: ['9e99a48a9960b14926bb7f3b02e22da2b0ab7280'],
+ });
+ });
+});
diff --git a/test/CircleCiOidcRole.test.ts b/test/CircleCiOidcRole.test.ts
new file mode 100644
index 0000000..c5a9904
--- /dev/null
+++ b/test/CircleCiOidcRole.test.ts
@@ -0,0 +1,147 @@
+import { App, Stack } from 'aws-cdk-lib';
+import { Match, Template } from 'aws-cdk-lib/assertions';
+import { OpenIdConnectProvider } from 'aws-cdk-lib/aws-iam';
+import { Queue } from 'aws-cdk-lib/aws-sqs';
+import { CircleCiOidcProvider, CircleCiOidcRole } from '../src';
+
+describe('CircleCiOidcRole', () => {
+ it('uses the organization ID and arn from the CircleCiOidcProvider construct', () => {
+ const app = new App();
+ const stack = new Stack(app, 'TestStack');
+ const provider = new CircleCiOidcProvider(stack, 'CircleCiOidcProvider', {
+ organizationId: '1234',
+ });
+ new CircleCiOidcRole(stack, 'CircleCiOidcRole', {
+ circleCiOidcProvider: provider,
+ });
+
+ Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', {
+ AssumeRolePolicyDocument: {
+ Statement: [
+ Match.objectLike({
+ Effect: 'Allow',
+ Action: 'sts:AssumeRoleWithWebIdentity',
+ Condition: {
+ StringEquals: {
+ 'oidc.circleci.com/org/1234:aud': '1234',
+ },
+ },
+ Principal: {
+ Federated: {
+ 'Fn::GetAtt': [
+ 'CircleCiOidcProviderBE49A2E7',
+ 'Arn',
+ ],
+ },
+ },
+ }),
+ ],
+ },
+ });
+ });
+
+ it('allows passing a provider arn and organization id', () => {
+ const app = new App();
+ const stack = new Stack(app, 'TestStack');
+ new CircleCiOidcRole(stack, 'CircleCiOidcRole', {
+ circleCiOidcProvider: { provider: OpenIdConnectProvider.fromOpenIdConnectProviderArn(stack, 'ImportProvider', 'arn:aws:iam::12345678910:oidc-provider/circleci'), organizationId: '1234' },
+ });
+
+ Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', {
+ AssumeRolePolicyDocument: {
+ Statement: [
+ Match.objectLike({
+ Effect: 'Allow',
+ Action: 'sts:AssumeRoleWithWebIdentity',
+ Condition: {
+ StringEquals: {
+ 'oidc.circleci.com/org/1234:aud': '1234',
+ },
+ },
+ Principal: {
+ Federated: 'arn:aws:iam::12345678910:oidc-provider/circleci',
+ },
+ }),
+ ],
+ },
+ });
+ });
+
+ it('allows limiting the role to specific CircleCI projects', () => {
+ const app = new App();
+ const stack = new Stack(app, 'TestStack');
+ const provider = new CircleCiOidcProvider(stack, 'CircleCiOidcProvider', {
+ organizationId: '1234',
+ });
+ new CircleCiOidcRole(stack, 'CircleCiOidcRole', {
+ circleCiOidcProvider: provider,
+ circleCiProjectIds: ['1234', '5678'],
+ });
+
+ Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', {
+ AssumeRolePolicyDocument: {
+ Statement: [
+ Match.objectLike({
+ Effect: 'Allow',
+ Action: 'sts:AssumeRoleWithWebIdentity',
+ Condition: {
+ StringEquals: {
+ 'oidc.circleci.com/org/1234:aud': '1234',
+ },
+ StringLike: {
+ 'oidc.circleci.com/org/1234:sub': [
+ 'org/1234/project/1234/*',
+ 'org/1234/project/5678/*',
+ ],
+ },
+ },
+ }),
+ ],
+ },
+ });
+ });
+
+ it('allows adding to the role', () => {
+ const app = new App();
+ const stack = new Stack(app, 'TestStack');
+ const provider = new CircleCiOidcProvider(stack, 'CircleCiOidcProvider', {
+ organizationId: '1234',
+ });
+ const { role } = new CircleCiOidcRole(stack, 'CircleCiOidcRole', {
+ circleCiOidcProvider: provider,
+ });
+
+ const queue = new Queue(stack, 'Queue');
+ queue.grantConsumeMessages(role);
+
+ Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', {
+ // Attached to the role
+ Roles: [
+ {
+ Ref: 'CircleCiOidcRoleDC0C8DDB',
+ },
+ ],
+ PolicyDocument: {
+ Statement: [
+ // Granted access to the queue
+ {
+ Effect: 'Allow',
+ Action: [
+ 'sqs:ReceiveMessage',
+ 'sqs:ChangeMessageVisibility',
+ 'sqs:GetQueueUrl',
+ 'sqs:DeleteMessage',
+ 'sqs:GetQueueAttributes',
+ ],
+ Resource: {
+ 'Fn::GetAtt': [
+ 'Queue4A7E3555',
+ 'Arn',
+ ],
+ },
+ },
+ ],
+ },
+ });
+ });
+});
diff --git a/test/hello.test.ts b/test/hello.test.ts
deleted file mode 100644
index acbacd4..0000000
--- a/test/hello.test.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { Hello } from '../src';
-
-test('hello', () => {
- expect(new Hello().sayHello()).toBe('hello, world!');
-});
\ No newline at end of file