Skip to content

Commit

Permalink
Merge branch 'master' into deploy-role-external-id
Browse files Browse the repository at this point in the history
  • Loading branch information
mergify[bot] authored Jul 16, 2021
2 parents 9b1b0f2 + 349de7c commit b29a974
Show file tree
Hide file tree
Showing 20 changed files with 3,586 additions and 38 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@
"@aws-cdk/core/minimatch/**",
"@aws-cdk/cx-api/semver",
"@aws-cdk/cx-api/semver/**",
"@aws-cdk/pipelines/aws-sdk",
"@aws-cdk/pipelines/aws-sdk/**",
"@aws-cdk/yaml-cfn/yaml",
"@aws-cdk/yaml-cfn/yaml/**",
"aws-cdk-lib/@balena/dockerignore",
Expand Down
9 changes: 9 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,15 @@ new WebSocketStage(stack, 'mystage', {
});
```

To retrieve a websocket URL and a callback URL:

```ts
const webSocketURL = webSocketStage.url;
// wss://${this.api.apiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}
const callbackURL = webSocketURL.callbackUrl;
// https://${this.api.apiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}
```

To add any other route:

```ts
Expand Down
23 changes: 22 additions & 1 deletion packages/@aws-cdk/aws-apigatewayv2/lib/websocket/stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ export interface IWebSocketStage extends IStage {
* The API this stage is associated to.
*/
readonly api: IWebSocketApi;

/**
* The callback URL to this stage.
* You can use the callback URL to send messages to the client from the backend system.
* https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-basic-concept.html
* https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-how-to-call-websocket-api-connections.html
*/
readonly callbackUrl: string;
}

/**
Expand Down Expand Up @@ -57,6 +65,10 @@ export class WebSocketStage extends StageBase implements IWebSocketStage {
get url(): string {
throw new Error('url is not available for imported stages.');
}

get callbackUrl(): string {
throw new Error('callback url is not available for imported stages.');
}
}
return new Import(scope, id);
}
Expand Down Expand Up @@ -86,11 +98,20 @@ export class WebSocketStage extends StageBase implements IWebSocketStage {
}

/**
* The URL to this stage.
* The websocket URL to this stage.
*/
public get url(): string {
const s = Stack.of(this);
const urlPath = this.stageName;
return `wss://${this.api.apiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`;
}

/**
* The callback URL to this stage.
*/
public get callbackUrl(): string {
const s = Stack.of(this);
const urlPath = this.stageName;
return `https://${this.api.apiId}.execute-api.${s.region}.${s.urlSuffix}/${urlPath}`;
}
}
18 changes: 18 additions & 0 deletions packages/@aws-cdk/aws-apigatewayv2/test/websocket/stage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,23 @@ describe('WebSocketStage', () => {

// THEN
expect(imported.stageName).toEqual(stage.stageName);
expect(() => imported.url).toThrow();
expect(() => imported.callbackUrl).toThrow();
});

test('callback URL', () => {
// GIVEN
const stack = new Stack();
const api = new WebSocketApi(stack, 'Api');

// WHEN
const defaultStage = new WebSocketStage(stack, 'Stage', {
webSocketApi: api,
stageName: 'dev',
});

// THEN
expect(defaultStage.callbackUrl.endsWith('/dev')).toBe(true);
expect(defaultStage.callbackUrl.startsWith('https://')).toBe(true);
});
});
58 changes: 57 additions & 1 deletion packages/@aws-cdk/pipelines/ORIGINAL_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -495,4 +495,60 @@ const validationAction = new ShellScriptAction({
// 'test.js' was produced from 'test/test.ts' during the synth step
commands: ['node ./test.js'],
});
```
```

### Confirm permissions broadening

To keep tabs on the security impact of changes going out through your pipeline,
you can insert a security check before any stage deployment. This security check
will check if the upcoming deployment would add any new IAM permissions or
security group rules, and if so pause the pipeline and require you to confirm
the changes.

The security check will appear as two distinct actions in your pipeline: first
a CodeBuild project that runs `cdk diff` on the stage that's about to be deployed,
followed by a Manual Approval action that pauses the pipeline. If it so happens
that there no new IAM permissions or security group rules will be added by the deployment,
the manual approval step is automatically satisfied. The pipeline will look like this:

```txt
Pipeline
├── ...
├── MyApplicationStage
│   ├── MyApplicationSecurityCheck // Security Diff Action
│   ├── MyApplicationManualApproval // Manual Approval Action
│   ├── Stack.Prepare
│   └── Stack.Deploy
└── ...
```

You can enable the security check by passing `confirmBroadeningPermissions` to
`addApplicationStage`:

```ts
const stage = pipeline.addApplicationStage(new MyApplication(this, 'PreProd'), {
confirmBroadeningPermissions: true,
});
```

To get notified when there is a change that needs your manual approval,
create an SNS Topic, subscribe your own email address, and pass it in via
`securityNotificationTopic`:

```ts
import * as sns from '@aws-cdk/aws-sns';
import * as subscriptions from '@aws-cdk/aws-sns-subscriptions';
import * as pipelines from '@aws-cdk/pipelines';

const topic = new sns.Topic(this, 'SecurityChangesTopic');
topic.addSubscription(new subscriptions.EmailSubscription('[email protected]'));

const pipeline = new CdkPipeline(app, 'Pipeline', { /* ... */ });
const stage = pipeline.addApplicationStage(new MyApplication(this, 'PreProd'), {
confirmBroadeningPermissions: true,
securityNotificationTopic: topic,
});
```

**Note**: Manual Approvals notifications only apply when an application has security
check enabled.
64 changes: 63 additions & 1 deletion packages/@aws-cdk/pipelines/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -840,7 +840,7 @@ and orphan the old bucket. You should manually delete the orphaned bucket
after you are sure you have redeployed all CDK applications and there are no
more references to the old asset bucket.

## Security Tips
## Security Considerations

It's important to stay safe while employing Continuous Delivery. The CDK Pipelines
library comes with secure defaults to the best of our ability, but by its
Expand All @@ -862,6 +862,68 @@ We therefore expect you to mind the following:
changes can be deployed through git. Avoid the chances of credentials leaking
by not having them in the first place!

### Confirm permissions broadening

To keep tabs on the security impact of changes going out through your pipeline,
you can insert a security check before any stage deployment. This security check
will check if the upcoming deployment would add any new IAM permissions or
security group rules, and if so pause the pipeline and require you to confirm
the changes.

The security check will appear as two distinct actions in your pipeline: first
a CodeBuild project that runs `cdk diff` on the stage that's about to be deployed,
followed by a Manual Approval action that pauses the pipeline. If it so happens
that there no new IAM permissions or security group rules will be added by the deployment,
the manual approval step is automatically satisfied. The pipeline will look like this:

```txt
Pipeline
├── ...
├── MyApplicationStage
│   ├── MyApplicationSecurityCheck // Security Diff Action
│   ├── MyApplicationManualApproval // Manual Approval Action
│   ├── Stack.Prepare
│   └── Stack.Deploy
└── ...
```

You can insert the security check by using a `ConfirmPermissionsBroadening` step:

```ts
const stage = new MyApplicationStage(this, 'MyApplication');
pipeline.addStage(stage, {
pre: [
new ConfirmPermissionsBroadening('Check', { stage }),
],
});
```

To get notified when there is a change that needs your manual approval,
create an SNS Topic, subscribe your own email address, and pass it in as
as the `notificationTopic` property:

```ts
import * as sns from '@aws-cdk/aws-sns';
import * as subscriptions from '@aws-cdk/aws-sns-subscriptions';
import * as pipelines from '@aws-cdk/pipelines';

const topic = new sns.Topic(this, 'SecurityChangesTopic');
topic.addSubscription(new subscriptions.EmailSubscription('[email protected]'));

const stage = new MyApplicationStage(this, 'MyApplication');
pipeline.addStage(stage, {
pre: [
new ConfirmPermissionsBroadening('Check', {
stage,
notificationTopic: topic,
}),
],
});
```

**Note**: Manual Approvals notifications only apply when an application has security
check enabled.

## Troubleshooting

Here are some common errors you may encounter while using this library.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import * as cb from '@aws-cdk/aws-codebuild';
import * as cp from '@aws-cdk/aws-codepipeline';
import { Construct } from 'constructs';
import { PipelineBase } from '../main';
import { ArtifactMap } from './artifact-map';
import { CodeBuildOptions } from './codepipeline';
import { CodeBuildOptions, CodePipeline } from './codepipeline';

/**
* Options for the `CodePipelineActionFactory.produce()` method.
Expand Down Expand Up @@ -43,7 +42,7 @@ export interface ProduceActionOptions {
/**
* The pipeline the action is being generated for
*/
readonly pipeline: PipelineBase;
readonly pipeline: CodePipeline;

/**
* If this action factory creates a CodeBuild step, default options to inherit
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { IStage } from '@aws-cdk/aws-codepipeline';
import * as cpa from '@aws-cdk/aws-codepipeline-actions';
import * as sns from '@aws-cdk/aws-sns';
import { Stage } from '@aws-cdk/core';
import { Node } from 'constructs';
import { Step } from '../blueprint';
import { ApplicationSecurityCheck } from '../private/application-security-check';
import { CodePipeline } from './codepipeline';
import { CodePipelineActionFactoryResult, ICodePipelineActionFactory, ProduceActionOptions } from './codepipeline-action-factory';

/**
* Properties for a `PermissionsBroadeningCheck`
*/
export interface PermissionsBroadeningCheckProps {
/**
* The CDK Stage object to check the stacks of
*
* This should be the same Stage object you are passing to `addStage()`.
*/
readonly stage: Stage;

/**
* Topic to send notifications when a human needs to give manual confirmation
*
* @default - no notification
*/
readonly notificationTopic?: sns.ITopic
}

/**
* Pause the pipeline if a deployment would add IAM permissions or Security Group rules
*
* This step is only supported in CodePipeline pipelines.
*/
export class ConfirmPermissionsBroadening extends Step implements ICodePipelineActionFactory {
constructor(id: string, private readonly props: PermissionsBroadeningCheckProps) {
super(id);
}

public produceAction(stage: IStage, options: ProduceActionOptions): CodePipelineActionFactoryResult {
const sec = this.getOrCreateSecCheck(options.pipeline);
this.props.notificationTopic?.grantPublish(sec.cdkDiffProject);

const variablesNamespace = Node.of(this.props.stage).addr;

const approveActionName = `${options.actionName}.Confirm`;
stage.addAction(new cpa.CodeBuildAction({
runOrder: options.runOrder,
actionName: `${options.actionName}.Check`,
input: options.artifacts.toCodePipeline(options.pipeline.cloudAssemblyFileSet),
project: sec.cdkDiffProject,
variablesNamespace,
environmentVariables: {
STAGE_PATH: { value: Node.of(this.props.stage).path },
STAGE_NAME: { value: stage.stageName },
ACTION_NAME: { value: approveActionName },
...this.props.notificationTopic ? {
NOTIFICATION_ARN: { value: this.props.notificationTopic.topicArn },
NOTIFICATION_SUBJECT: { value: `Confirm permission broadening in ${this.props.stage.stageName}` },
} : {},
},
}));

stage.addAction(new cpa.ManualApprovalAction({
actionName: approveActionName,
runOrder: options.runOrder + 1,
additionalInformation: `#{${variablesNamespace}.MESSAGE}`,
externalEntityLink: `#{${variablesNamespace}.LINK}`,
}));

return { runOrdersConsumed: 2 };
}

private getOrCreateSecCheck(pipeline: CodePipeline): ApplicationSecurityCheck {
const id = 'PipelinesSecurityCheck';
const existing = Node.of(pipeline).tryFindChild(id);
if (existing) {
if (!(existing instanceof ApplicationSecurityCheck)) {
throw new Error(`Expected '${Node.of(existing).path}' to be 'ApplicationSecurityCheck' but was '${existing}'`);
}
return existing;
}

return new ApplicationSecurityCheck(pipeline, id, {
codePipeline: pipeline.pipeline,
});
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/pipelines/lib/codepipeline/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './artifact-map';
export * from './codebuild-step';
export * from './confirm-permissions-broadening';
export * from './codepipeline';
export * from './codepipeline-action-factory';
export * from './codepipeline-source';
Loading

0 comments on commit b29a974

Please sign in to comment.