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

misc: things missed in initial release #1

Merged
merged 1 commit into from
Oct 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
[![npm (scoped)](https://img.shields.io/npm/v/@catnekaise/actions-constructs?style=flat-square)](https://www.npmjs.com/package/@catnekaise/actions-constructs)
[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/catnekaise/actions-constructs?sort=semver&style=flat-square)](https://github.com/catnekaise/actions-constructs/releases)

# Actions Constructs
A set of AWS CDK Constructs for integrating GitHub Actions and AWS.

### GitHub Actions ABAC in AWS
At the time of writing, the constructs and utilities of this library relates to `GitHub Actions attribute based access control in AWS` and how to make this as easy as possible using AWS CDK.

## Install

```bash
npm install @catnekaise/actions-constructs
```

## ActionsIdentityPool
Use this construct to create an `Amazon Cognito Identity Pool` that enables GitHub Actions [OpenID Connect identities](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect) to request temporary AWS Credentials. The temporary AWS Credentials will have principal/session tags corresponding with [access token claims](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token) which enables attribute based access control (ABAC) to AWS resources based on these claims.

Expand Down
146 changes: 133 additions & 13 deletions docs/actions-identity-pool/util/iam-resource-path.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,153 @@
# IAM Resource Path Builder
Use this to build an IAM Policy resource path consisting of text and principalTags. It will use the tag names provided to the Identity Pool when builder is created using `pool.util.iamResourcePath`.

## Usage
### Background
When you write a resource path for an AWS IAM policy that uses many GitHub Actions claims such as `arn:aws:s3:::my-bucket/${aws:principalTag/repo}/${aws:principalTag/env}/${aws:principalTag/run}/${aws:principalTag/attempt}/*`, you have to make sure of:

- Using Correct Syntax
- Using correct tag names and updating the resource path string if you change any tag name
- Not using claims that was not mapped in the Identity Pool

Using this utility, it allows you to work with the [original claim names](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token) of a GitHub Actions access token claim and the utility will:

- Build a string with the correct syntax
- Use the tag name you specified in `ClaimMapping`, and reflect updated tag names you make in `ClaimMapping`
- Throw an error if you attempt to use a claim that you did not include in `ClaimMapping`

## Basic Usage
See examples below and view source. If using this please provide feedback.

```typescript
declare const pool: ActionsIdentityPool;
declare const role: iam.Role;
import { ActionsIdentityPoolBasic, ClaimMapping } from '@catnekaise/actions-constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';

const claimMapping = ClaimMapping.fromClaimsWithTagName({
repository: 'repo',
job_workflow_ref: 'job',
actor: 'user',
});

const pool = new ActionsIdentityPoolBasic(stack, 'Pool', {
claimMapping,
principalClaimRequirements: {
repository: {
condition: 'StringLike',
values: [`${githubOrganization}/*`],
},
},
});

const role = pool.authenticatedRole;

const bucket = new s3.Bucket(this, 'Bucket', { name: 'my-bucket' });

// Inline use. .toString() is automatically used
// permission granted at object prefix = /${aws:principalTag/repo}/*
bucket.grantRead(role, pool.util.iamResourcePath.value('repository', '*'));

// Create a new builder
const builder = pool.util.iamResourcePath;

const value = builder.text(bucket.bucketArn).claim('repository').text('*').toString();
// resource string = arn:aws:s3:::my-bucket/${aws:principalTag/repo}/*
const value = pool.util.iamResourcePath.text(bucket.bucketArn).claim('repository').text('*').toString();

role.addToPolicy(new iam.PolicyStatement({
effect: Effect.ALLOW,
actions: ['s3:GetObject'],
resources: [value],
}));
```

## Usage - Separate Stack
The same builder can be created in a separate stack where the Identity Pool was not created.

```typescript
import { ActionsIdentityIamResourcePathBuilder, ClaimMapping } from '@catnekaise/actions-constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';

const claimMapping = ClaimMapping.fromClaimsWithTagName({
repository: 'repo',
job_workflow_ref: 'job',
actor: 'user',
});

// Same as calling `pool.util.iamResourcePath`.
const builder = ActionsIdentityIamResourcePathBuilder.fromClaimMapping(claimMapping);

declare const role: iam.Role;

const bucket = new s3.Bucket(this, 'Bucket', { name: 'my-bucket' });

// permission granted at object prefix = /${aws:principalTag/repo}/*
bucket.grantRead(role, builder.value('repository', '*'));
```

### Claim, Value, Text
Three methods of input exists, `.claim`, `.value` and `.text`.

- Using `.claim` will throw error unless the provided value matches a [GitHub Actions access token claim](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token).
- Using `.value` allows mixing both GitHub Actions access token claims and text.
- Using `.text` will not transform values as `.text` does not care about mapped or unmapped claims
- Values provided to `.claim` and values matching claims provided to `.value` will be transformed into `${aws:principalTag/tag_name}`.
- When a claim is provided to either `.claim` or `.value` that was not mapped in `ClaimMapping`, an error is thrown

```typescript
import { ActionsIdentityIamResourcePathBuilder, ClaimMapping } from '@catnekaise/actions-constructs';
import * as s3 from 'aws-cdk-lib/aws-s3';

const claimMapping = ClaimMapping.fromClaimsWithTagName({
repository: 'repo',
job_workflow_ref: 'job',
actor: 'user',
});

const builder = ActionsIdentityIamResourcePathBuilder.fromClaimMapping(claimMapping);

declare const role: iam.Role;

const bucket = new s3.Bucket(this, 'Bucket', { name: 'my-bucket' });

// permission granted at object prefix = /${aws:principalTag/repo}/*
bucket.grantRead(role, builder.claim('repository').text('*'));

// permission granted at object prefix = /${aws:principalTag/repo}/*
bucket.grantRead(role, builder.value('repository', '*'));

// permission granted at object prefix = /repository/*
bucket.grantRead(role, builder.text('repository', '*'));

// Throws an error as "environment" was not a claim mapped in "ClaimMapping"
bucket.grantRead(role, builder.claim('environment').text('*'));

// Does NOT throws an error as .text() does not care about "ClaimMapping"
// permission granted at object prefix = /environment/*
bucket.grantRead(role, builder.text('environment').text('*'));
```

### Immutable
The builder is immutable.

```typescript
import { ActionsIdentityIamResourcePathBuilder, ClaimMapping } from '@catnekaise/actions-constructs';

const claimMapping = ClaimMapping.fromClaimsWithTagName({
repository: 'repo',
job_workflow_ref: 'job',
actor: 'user',
});

// builder string value = ''
let builder = ActionsIdentityIamResourcePathBuilder.fromClaimMapping(claimMapping);

// builder string value = ''
builder.claim('repository');

// builder string value = '${aws:principalTag/repo}'
builder = builder.claim('repository');

// builder string value = '${aws:principalTag/repo}'
// builder2 string value = '${aws:principalTag/repo}/foo'
let builder2 = builder.text('foo');

// arn:aws:s3:::my-bucket/${aws:principalTag/repo}/${aws:principalTag/env}/${aws:principalTag/run}/${aws:principalTag/attempt}/*
const value2 = builder.value(bucket.bucketArn, 'repository', 'environment', 'run_id', 'run_attempt', '*').toString();
// builder string value = '${aws:principalTag/repo}/${aws:principalTag/user}'
builder = builder.claim('actor');

// .text() always renders the value provided
// arn:aws:s3:::my-bucket/repository_environment_job_workflow_ref
const value3 = builder.text(bucket.bucketArn, 'repository', 'environment', 'job_workflow_ref').toString('_');
// builder2 string value = '${aws:principalTag/repo}/foo/${aws:principalTag/user}'
builder2 = builder2.claim('actor');
```
3 changes: 0 additions & 3 deletions src/identity-pool/claims.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/** @internal **/
export const GitHubActionClaims = [
'jti',
'sub',
Expand Down Expand Up @@ -54,10 +53,8 @@ export interface MappedClaim {
readonly claim: GhaClaim;
}

/** @internal **/
export type PartialGhaClaims = Partial<{ [K in typeof GitHubActionClaims[number]]: string }>;

/** @internal **/
export function createMappedClaims(claims: GhaClaim[] | PartialGhaClaims): MappedClaim[] {

const mappedClaims: MappedClaim[] = [];
Expand Down
20 changes: 20 additions & 0 deletions test/iam-resource-path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,24 @@ describe('IAM Resource Path', () => {

});

it('should be immutable builders', () => {

const builder = ActionsIdentityIamResourcePathBuilder.fromClaimMapping(ClaimMapping.fromClaimsWithTagName({
repository: 'repo',
repository_owner: 'owner',
repository_owner_id: 'ownerId',
}));

const builder2 = builder.claim('repository_owner');
builder2.text('omitted');

let builder3 = builder2.value('repository');
builder3 = builder3.text('*');

expect(builder.toString()).toEqual('');
expect(builder2.toString()).toEqual('${aws:principalTag/owner}');
expect(builder3.toString()).toEqual('${aws:principalTag/owner}/${aws:principalTag/repo}/*');

});

});