Skip to content

Commit

Permalink
feat(code pipeline): cross-account support: wrap artifacts stores
Browse files Browse the repository at this point in the history
Use new construct `ArtifactStore` to represent pipeline artifact store.
If this construct has `artifactsKeyAlias` set than use it in output
template as encryption key.

For cross-account deployments artifacts must by encrypted and replicated
using key to which foreign account has an access, to. By default
`aws/s3` key is used which has limited support for setting resource policy.

In addition it’s hard to pass KMS keys between stacks, as KMS keys
are identified by freshly generated UUID, so it’s impossible to generate
scaffold stack and import / export values from it - physical
ARN should be passed to simplify things, so alias is used instead of
KMS key.

This change itself only enables customers to build cross-account pipelines,
but doesn’t support them in automatic managing or creation of keys
(subsequent changes will give more support)
  • Loading branch information
Radoslaw Smogura committed Jan 17, 2019
1 parent 3270b47 commit 8b160bb
Show file tree
Hide file tree
Showing 13 changed files with 527 additions and 61 deletions.
217 changes: 217 additions & 0 deletions packages/@aws-cdk/aws-codepipeline/lib/artifacts-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import * as cdk from '@aws-cdk/cdk';

import * as iam from '@aws-cdk/aws-iam';
import * as kms from '@aws-cdk/aws-kms';
import * as s3 from '@aws-cdk/aws-s3';

export interface IArtifactsStore {
/**
* Bucket to store artifacts in given region
*/
readonly bucket: s3.IBucket;

/**
* **(Experimental)** The alias of KMS key (or KMS key itself)
* used to encrypt artifacts. In case of cross-account pipelines
* specifying this value may be required, as artifacts should be
* encrypted with KMS key which is allowed to be used in foreign account.
*
* This feature is experimental. It may be removed, or work improperly.
* Known limitations:
* * if cross-region replication is used, build actions should use this key;
* * no permissions is set for KMS key, nor on roles policy.
*/
readonly artifactsKeyAliasArn?: string;

// For cross region / account calling this methods requires special care
// like for (S3 ones). There's a risk that pipeline.role will be passed,
// but it's in stack created after artifact's store stack - service like KMS will reject
// creation of such resource policy.
grantRead(identity?: iam.IPrincipal): void;
grantReadWrite(identity?: iam.IPrincipal): void;
}

export interface ArtifactsStoreProps {
/**
* Bucket to store artifacts in given region
*/
bucket?: s3.IBucket;

/**
* **(Experimental)** The KMS key to manage to encrypt artifacts.
* If this value is set *CDK* will manage policy for this key.
*
* **This key is not used to set encryption, but `artifactsKeyAlias`, thus
* `artifactsKeyAlias` should correspond to `artifactsKey`**
*/
managedArtifactsKey?: kms.EncryptionKey;

/**
* **(Experimental)** Alias of `key` (or key itself) to use when encrypting artifacts in store.
*
* This value is optional, however when set consider setting encryption
* key on `Project` as pipeline can fail.
*
* @see IArtifactsStore
*/
artifactsKeyAliasArn?: string;
}

/**
* Represents artifacts store.
*
* Artifacts store is composed from bucket and eventually KMS key (or alias), which is used to encrypt or
* decrypt artifacts.
*/
export class ArtifactsStore extends cdk.Construct implements IArtifactsStore {
/**
* Bucket to store artifacts in given region
*/
public readonly bucket: s3.IBucket;

/**
* The name of bucket used to store artifacts.
* If store has been created within stack with known account and region
* this value will fully represent physical name, and should not contain
* pseudo parameters.
*/
public get bucketName() { return this._bucketName; }

/**
* Encryption key used to encrypt artifacts. In this class
* this attribute represent physical key created in stack.
*/
public readonly artifactsKey?: kms.EncryptionKey;

/**
* Artifacts encryption key alias ARN. ARN is synthesized
* from account number, region, and alias name.
*/
public readonly artifactsKeyAliasArn?: string;

protected _bucketName: string;
/**
* Constructs new artifacts store with given properties.
* **Consider using `fromBaseName`**
*/
constructor(scope: cdk.Construct, id: string, props: ArtifactsStoreProps) {
super(scope, id);
this.bucket = props.bucket || new s3.Bucket(this, 'Bucket');
this._bucketName = this.bucket.bucketName;
this.artifactsKey = props.managedArtifactsKey;
this.artifactsKeyAliasArn = props.artifactsKeyAliasArn;
}

/**
* Converts store to `ImportedArtifactsStore` which will have `parent`.
*/
public asImportedStore(parent: cdk.Construct, id: string): ImportedArtifactsStore {
return new ImportedArtifactsStore(parent, id, {
artifactsKeyAliasArn: this.artifactsKeyAliasArn,
bucketName: this.bucketName
});
}

public grantRead(identity?: iam.IPrincipal): void {
if (!identity) {
return;
}
this.bucket.grantRead(identity);
}

public grantReadWrite(identity?: iam.IPrincipal) {
if (!identity) {
return;
}
this.bucket.grantReadWrite(identity);
}

protected grantKmsActions(identity: iam.IPrincipal | iam.ArnPrincipal, kmsActions: string[]) {
if (this.artifactsKey) {
let principal;
if (identity instanceof iam.ArnPrincipal) {
principal = identity;
} else {
principal = (identity as iam.IPrincipal).principal;
}

this.artifactsKey.addToResourcePolicy(new iam.PolicyStatement()
.addActions(...kmsActions)
// Can't use role generated by pipeline, as KMS validates if role exists, so
// only root principal can be specified
// TODO Narrow actions & narrow access with IAM tags (PR to be sent)
.addPrincipal(principal)
.addAllResources()
);
}
}
}

/**
* Represents configuration of artifact store used for cross-region and cross account replication of deployment artifacts.
*
* Artifacts store is a set of AWS artifacts (like buckets and KMS keys) which are used by pipeline to store
* input and output to and from actions.
*/
export interface ImportedArtifactsStoreProps {
/**
* The name of the S3 Bucket used for replicating the Pipeline's artifacts into the region.
*/
bucketName: string;

/**
* Encryption key used to encrypt artifacts, it can represent key ARN or it can be an alias to key.
*/
artifactsKeyAliasArn?: string;
}

/**
* Represents imported artifacts store.
*/
export class ImportedArtifactsStore extends cdk.Construct implements IArtifactsStore {
public readonly bucket: s3.IBucket;

public readonly artifactsKeyAliasArn?: string;

// public readonly artifactKeyTag?: string; // For managing keys by tags

constructor(scope: cdk.Construct, id: string, props: ImportedArtifactsStoreProps) {
super(scope, id);

this.bucket = s3.Bucket.import(this, `${id}-Bucket`, {
bucketName: props.bucketName
});

this.artifactsKeyAliasArn = props.artifactsKeyAliasArn;
}

public grantRead(identity?: iam.IPrincipal): void {
if (!identity) {
return;
}
this.bucket.grantRead(identity);
}

public grantReadWrite(identity?: iam.IPrincipal) {
if (!identity) {
return;
}
this.bucket.grantReadWrite(identity);
}

protected grantKmsActions(identity: iam.IPrincipal, kmsActions: string[]) {
// In this case we only update the principal, however argument
if (this.artifactsKeyAliasArn) {
if (identity instanceof iam.ArnPrincipal) {
// Nothing to do, as it's imported store, so can't update KMS policy.
return;
}

(identity as iam.IPrincipal).addToPolicy(new iam.PolicyStatement()
.addActions(...kmsActions)
// TODO Nice to introduce tag base permission management
.addAllResources()
);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import s3 = require('@aws-cdk/aws-s3');
import cdk = require('@aws-cdk/cdk');
import crypto = require('crypto');
import { ImportedArtifactsStore } from './artifacts-store';

/**
* Construction properties for {@link CrossRegionScaffoldStack}.
Expand Down Expand Up @@ -44,6 +45,12 @@ export class CrossRegionScaffoldStack extends cdk.Stack {
});
this.replicationBucketName = replicationBucketName;
}

public asImportedStore(parent: cdk.Construct, id: string) {
return new ImportedArtifactsStore(parent, id, {
bucketName: this.replicationBucketName
});
}
}

function generateStackName(props: CrossRegionScaffoldStackProps): string {
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-codepipeline/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './artifacts-store';
export * from './cross-region-scaffold-stack';
export * from './github-source-action';
export * from './jenkins-actions';
Expand Down
Loading

0 comments on commit 8b160bb

Please sign in to comment.