diff --git a/packages/@aws-cdk/aws-codebuild/lib/source.ts b/packages/@aws-cdk/aws-codebuild/lib/source.ts index 3a93de4b3806a..d25bd0db3f34d 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/source.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/source.ts @@ -1,7 +1,7 @@ import codecommit = require('@aws-cdk/aws-codecommit'); import iam = require('@aws-cdk/aws-iam'); import s3 = require('@aws-cdk/aws-s3'); -import cdk = require('@aws-cdk/cdk'); +import { SecretValue } from '@aws-cdk/cdk'; import { CfnProject } from './codebuild.generated'; import { Project } from './project'; @@ -213,7 +213,7 @@ export interface GitHubSourceProps extends GitBuildSourceProps { * Note that you need to give CodeBuild permissions to your GitHub account in order for the token to work. * That is a one-time operation that can be done through the AWS Console for CodeBuild. */ - readonly oauthToken: string; + readonly oauthToken: SecretValue; /** * Whether to create a webhook that will trigger a build every time a commit is pushed to the GitHub repository. @@ -236,13 +236,12 @@ export interface GitHubSourceProps extends GitBuildSourceProps { export class GitHubSource extends GitBuildSource { public readonly type: SourceType = SourceType.GitHub; private readonly httpsCloneUrl: string; - private readonly oauthToken: string; + private readonly oauthToken: SecretValue; private readonly reportBuildStatus: boolean; private readonly webhook?: boolean; constructor(props: GitHubSourceProps) { super(props); - cdk.Secret.assertSafeSecret(props.oauthToken, 'oauthToken'); this.httpsCloneUrl = `https://github.com/${props.owner}/${props.repo}.git`; this.oauthToken = props.oauthToken; this.webhook = props.webhook; @@ -278,7 +277,7 @@ export interface GitHubEnterpriseSourceProps extends GitBuildSourceProps { /** * The OAuth token used to authenticate when cloning the git repository. */ - readonly oauthToken: string; + readonly oauthToken: SecretValue; /** * Whether to ignore SSL errors when connecting to the repository. @@ -294,12 +293,11 @@ export interface GitHubEnterpriseSourceProps extends GitBuildSourceProps { export class GitHubEnterpriseSource extends GitBuildSource { public readonly type: SourceType = SourceType.GitHubEnterprise; private readonly httpsCloneUrl: string; - private readonly oauthToken: string; + private readonly oauthToken: SecretValue; private readonly ignoreSslErrors?: boolean; constructor(props: GitHubEnterpriseSourceProps) { super(props); - cdk.Secret.assertSafeSecret(props.oauthToken, 'oauthToken'); this.httpsCloneUrl = props.httpsCloneUrl; this.oauthToken = props.oauthToken; this.ignoreSslErrors = props.ignoreSslErrors; diff --git a/packages/@aws-cdk/aws-codebuild/test/test.project.ts b/packages/@aws-cdk/aws-codebuild/test/test.project.ts index 4324abae0372d..8e7728d71e463 100644 --- a/packages/@aws-cdk/aws-codebuild/test/test.project.ts +++ b/packages/@aws-cdk/aws-codebuild/test/test.project.ts @@ -1,6 +1,7 @@ import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import assets = require('@aws-cdk/assets'); import cdk = require('@aws-cdk/cdk'); +import { SecretValue } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import codebuild = require('../lib'); @@ -58,7 +59,7 @@ export = { owner: 'testowner', repo: 'testrepo', cloneDepth: 3, - oauthToken: cdk.Secret.plainText("test_oauth_token"), + oauthToken: SecretValue.plainText("test_oauth_token"), }) }); @@ -88,7 +89,7 @@ export = { source: new codebuild.GitHubSource({ owner: 'testowner', repo: 'testrepo', - oauthToken: cdk.Secret.plainText("test_oauth_token"), + oauthToken: SecretValue.plainText("test_oauth_token"), reportBuildStatus: false, }) }); @@ -112,7 +113,7 @@ export = { source: new codebuild.GitHubSource({ owner: 'testowner', repo: 'testrepo', - oauthToken: cdk.Secret.plainText("test_oauth_token"), + oauthToken: SecretValue.plainText("test_oauth_token"), webhook: true, }) }); @@ -138,7 +139,7 @@ export = { httpsCloneUrl: 'https://github.testcompany.com/testowner/testrepo', ignoreSslErrors: true, cloneDepth: 4, - oauthToken: cdk.Secret.plainText("test_oauth_token"), + oauthToken: SecretValue.plainText("test_oauth_token"), }) }); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/alexa-ask/deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/alexa-ask/deploy-action.ts index e53ad6a31af77..0316382db7a9b 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/alexa-ask/deploy-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/alexa-ask/deploy-action.ts @@ -1,5 +1,5 @@ import codepipeline = require('@aws-cdk/aws-codepipeline'); -import cdk = require('@aws-cdk/cdk'); +import { SecretValue } from '@aws-cdk/cdk'; /** * Construction properties of the {@link AlexaSkillDeployAction Alexa deploy Action}. @@ -13,12 +13,12 @@ export interface AlexaSkillDeployActionProps extends codepipeline.CommonActionPr /** * The client secret of the developer console token */ - readonly clientSecret: string; + readonly clientSecret: SecretValue; /** * The refresh token of the developer console token */ - readonly refreshToken: string; + readonly refreshToken: SecretValue; /** * The Alexa skill id @@ -41,9 +41,6 @@ export interface AlexaSkillDeployActionProps extends codepipeline.CommonActionPr */ export class AlexaSkillDeployAction extends codepipeline.DeployAction { constructor(props: AlexaSkillDeployActionProps) { - cdk.Secret.assertSafeSecret(props.clientSecret, 'clientSecret'); - cdk.Secret.assertSafeSecret(props.refreshToken, 'refreshToken'); - super({ ...props, artifactBounds: { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/github/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/github/source-action.ts index 9f6e3bf661aa3..a0f2543908fa9 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/github/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/github/source-action.ts @@ -1,5 +1,5 @@ import codepipeline = require('@aws-cdk/aws-codepipeline'); -import cdk = require('@aws-cdk/cdk'); +import { SecretValue } from '@aws-cdk/cdk'; /** * Construction properties of the {@link GitHubSourceAction GitHub source action}. @@ -36,7 +36,7 @@ export interface GitHubSourceActionProps extends codepipeline.CommonActionProps * const oauth = new secretsmanager.SecretString(this, 'GitHubOAuthToken', { secretId: 'my-github-token' }); * new GitHubSource(this, 'GitHubAction', { oauthToken: oauth.value, ... }); */ - readonly oauthToken: string; + readonly oauthToken: SecretValue; /** * Whether AWS CodePipeline should poll for source changes. @@ -54,8 +54,6 @@ export class GitHubSourceAction extends codepipeline.SourceAction { private readonly props: GitHubSourceActionProps; constructor(props: GitHubSourceActionProps) { - cdk.Secret.assertSafeSecret(props.oauthToken, 'oauthToken'); - super({ ...props, owner: 'ThirdParty', @@ -64,7 +62,7 @@ export class GitHubSourceAction extends codepipeline.SourceAction { Owner: props.owner, Repo: props.repo, Branch: props.branch || "master", - OAuthToken: props.oauthToken, + OAuthToken: props.oauthToken.toString(), PollForSourceChanges: props.pollForSourceChanges || false, }, outputArtifactName: props.outputArtifactName @@ -78,7 +76,7 @@ export class GitHubSourceAction extends codepipeline.SourceAction { new codepipeline.CfnWebhook(info.scope, 'WebhookResource', { authentication: 'GITHUB_HMAC', authenticationConfiguration: { - secretToken: this.props.oauthToken, + secretToken: this.props.oauthToken.toString(), }, filters: [ { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-alexa-deploy.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-alexa-deploy.ts index eb207c1f3bb55..236337acce16f 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-alexa-deploy.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-alexa-deploy.ts @@ -1,15 +1,15 @@ import codepipeline = require('@aws-cdk/aws-codepipeline'); import s3 = require('@aws-cdk/aws-s3'); -import cdk = require('@aws-cdk/cdk'); +import { App, RemovalPolicy, SecretValue, Stack } from '@aws-cdk/cdk'; import cpactions = require('../lib'); -const app = new cdk.App(); +const app = new App(); -const stack = new cdk.Stack(app, 'aws-cdk-codepipeline-alexa-deploy'); +const stack = new Stack(app, 'aws-cdk-codepipeline-alexa-deploy'); const bucket = new s3.Bucket(stack, 'PipelineBucket', { versioned: true, - removalPolicy: cdk.RemovalPolicy.Destroy, + removalPolicy: RemovalPolicy.Destroy, }); const sourceAction = new cpactions.S3SourceAction({ actionName: 'Source', @@ -30,8 +30,8 @@ const deployStage = { runOrder: 1, inputArtifact: sourceAction.outputArtifact, clientId: 'clientId', - clientSecret: cdk.Secret.plainText('clientSecret'), - refreshToken: cdk.Secret.plainText('refreshToken'), + clientSecret: SecretValue.plainText('clientSecret'), + refreshToken: SecretValue.plainText('refreshToken'), skillId: 'amzn1.ask.skill.12345678-1234-1234-1234-123456789012', }), ], diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/test.pipeline.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/test.pipeline.ts index 6d5a6347e21f2..610b9ab454820 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/test.pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/test.pipeline.ts @@ -5,7 +5,7 @@ import codepipeline = require('@aws-cdk/aws-codepipeline'); import lambda = require('@aws-cdk/aws-lambda'); import s3 = require('@aws-cdk/aws-s3'); import sns = require('@aws-cdk/aws-sns'); -import cdk = require('@aws-cdk/cdk'); +import { App, CfnParameter, SecretValue, Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import cpactions = require('../lib'); @@ -13,7 +13,7 @@ import cpactions = require('../lib'); export = { 'basic pipeline'(test: Test) { - const stack = new cdk.Stack(); + const stack = new Stack(); const repository = new codecommit.Repository(stack, 'MyRepo', { repositoryName: 'my-repo', @@ -50,7 +50,7 @@ export = { }, 'Tokens can be used as physical names of the Pipeline'(test: Test) { - const stack = new cdk.Stack(undefined, 'StackName'); + const stack = new Stack(undefined, 'StackName'); new codepipeline.Pipeline(stack, 'Pipeline', { pipelineName: stack.stackName, @@ -66,9 +66,9 @@ export = { }, 'github action uses ThirdParty owner'(test: Test) { - const stack = new cdk.Stack(); + const stack = new Stack(); - const secret = new cdk.CfnParameter(stack, 'GitHubToken', { type: 'String', default: 'my-token' }); + const secret = new CfnParameter(stack, 'GitHubToken', { type: 'String', default: 'my-token' }); const p = new codepipeline.Pipeline(stack, 'P'); @@ -80,7 +80,7 @@ export = { runOrder: 8, outputArtifactName: 'A', branch: 'branch', - oauthToken: secret.stringValue, + oauthToken: SecretValue.plainText(secret.stringValue), owner: 'foo', repo: 'bar' }), @@ -163,7 +163,7 @@ export = { }, 'onStateChange'(test: Test) { - const stack = new cdk.Stack(); + const stack = new Stack(); const topic = new sns.Topic(stack, 'Topic'); @@ -256,7 +256,7 @@ export = { 'manual approval Action': { 'allows passing an SNS Topic when constructing it'(test: Test) { - const stack = new cdk.Stack(); + const stack = new Stack(); const topic = new sns.Topic(stack, 'Topic'); const manualApprovalAction = new cpactions.ManualApprovalAction({ actionName: 'Approve', @@ -273,7 +273,7 @@ export = { 'PipelineProject': { 'with a custom Project Name': { 'sets the source and artifacts to CodePipeline'(test: Test) { - const stack = new cdk.Stack(); + const stack = new Stack(); new codebuild.PipelineProject(stack, 'MyProject', { projectName: 'MyProject', @@ -307,7 +307,7 @@ export = { }, 'Lambda PipelineInvokeAction can be used to invoke Lambda functions from a CodePipeline'(test: Test) { - const stack = new cdk.Stack(); + const stack = new Stack(); const lambdaFun = new lambda.Function(stack, 'Function', { code: new lambda.InlineCode('bla'), @@ -437,7 +437,7 @@ export = { 'CodeCommit Action': { 'does not poll for changes by default'(test: Test) { - const stack = new cdk.Stack(); + const stack = new Stack(); const sourceAction = new cpactions.CodeCommitSourceAction({ actionName: 'stage', outputArtifactName: 'SomeArtifact', @@ -450,7 +450,7 @@ export = { }, 'does not poll for source changes when explicitly set to false'(test: Test) { - const stack = new cdk.Stack(); + const stack = new Stack(); const sourceAction = new cpactions.CodeCommitSourceAction({ actionName: 'stage', outputArtifactName: 'SomeArtifact', @@ -469,9 +469,9 @@ export = { const pipelineRegion = 'us-west-2'; const pipelineAccount = '123'; - const app = new cdk.App(); + const app = new App(); - const stack = new cdk.Stack(app, 'TestStack', { + const stack = new Stack(app, 'TestStack', { env: { region: pipelineRegion, account: pipelineAccount, @@ -582,12 +582,12 @@ export = { }, }; -function stageForTesting(stack: cdk.Stack): codepipeline.IStage { +function stageForTesting(stack: Stack): codepipeline.IStage { const pipeline = new codepipeline.Pipeline(stack, 'pipeline'); return pipeline.addStage({ name: 'stage' }); } -function repositoryForTesting(stack: cdk.Stack): codecommit.Repository { +function repositoryForTesting(stack: Stack): codecommit.Repository { return new codecommit.Repository(stack, 'Repository', { repositoryName: 'Repository' }); diff --git a/packages/@aws-cdk/aws-iam/lib/user.ts b/packages/@aws-cdk/aws-iam/lib/user.ts index 3c9a04e49a7af..8613922888a03 100644 --- a/packages/@aws-cdk/aws-iam/lib/user.ts +++ b/packages/@aws-cdk/aws-iam/lib/user.ts @@ -1,4 +1,4 @@ -import { Construct } from '@aws-cdk/cdk'; +import { Construct, SecretValue } from '@aws-cdk/cdk'; import { Group } from './group'; import { CfnUser } from './iam.generated'; import { IIdentity } from './identity-base'; @@ -50,9 +50,12 @@ export interface UserProps { * The password for the user. This is required so the user can access the * AWS Management Console. * + * You can use `SecretValue.plainText` to specify a password in plain text or + * use `secretsmanager.Secret.import` to reference a secret in Secrets Manager. + * * @default User won't be able to access the management console without a password. */ - readonly password?: string; + readonly password?: SecretValue; /** * Specifies whether the user is required to set a new password the next @@ -147,7 +150,7 @@ export class User extends Construct implements IIdentity { private parseLoginProfile(props: UserProps): CfnUser.LoginProfileProperty | undefined { if (props.password) { return { - password: props.password, + password: props.password.toString(), passwordResetRequired: props.passwordResetRequired }; } diff --git a/packages/@aws-cdk/aws-iam/test/example.attaching.lit.ts b/packages/@aws-cdk/aws-iam/test/example.attaching.lit.ts index 78480ec54b3a8..1f01efb1a5004 100644 --- a/packages/@aws-cdk/aws-iam/test/example.attaching.lit.ts +++ b/packages/@aws-cdk/aws-iam/test/example.attaching.lit.ts @@ -1,4 +1,5 @@ import cdk = require('@aws-cdk/cdk'); +import { SecretValue } from '@aws-cdk/cdk'; import { Group, Policy, User } from '../lib'; export class ExampleConstruct extends cdk.Construct { @@ -6,7 +7,7 @@ export class ExampleConstruct extends cdk.Construct { super(scope, id); /// !show - const user = new User(this, 'MyUser', { password: '1234' }); + const user = new User(this, 'MyUser', { password: SecretValue.plainText('1234') }); const group = new Group(this, 'MyGroup'); const policy = new Policy(this, 'MyPolicy'); diff --git a/packages/@aws-cdk/aws-iam/test/integ.user.ts b/packages/@aws-cdk/aws-iam/test/integ.user.ts index ea1b19acd4429..e21c468177554 100644 --- a/packages/@aws-cdk/aws-iam/test/integ.user.ts +++ b/packages/@aws-cdk/aws-iam/test/integ.user.ts @@ -1,4 +1,4 @@ -import { App, Stack } from "@aws-cdk/cdk"; +import { App, SecretValue, Stack } from "@aws-cdk/cdk"; import { User } from "../lib"; const app = new App(); @@ -7,7 +7,7 @@ const stack = new Stack(app, 'aws-cdk-iam-user'); new User(stack, 'MyUser', { userName: 'benisrae', - password: '1234', + password: SecretValue.plainText('1234'), passwordResetRequired: true }); diff --git a/packages/@aws-cdk/aws-iam/test/test.user.ts b/packages/@aws-cdk/aws-iam/test/test.user.ts index 4982a39749ce6..d483669635c26 100644 --- a/packages/@aws-cdk/aws-iam/test/test.user.ts +++ b/packages/@aws-cdk/aws-iam/test/test.user.ts @@ -1,4 +1,4 @@ -import { App, Stack } from '@aws-cdk/cdk'; +import { App, SecretValue, Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { User } from '../lib'; @@ -17,7 +17,7 @@ export = { const app = new App(); const stack = new Stack(app, 'MyStack'); new User(stack, 'MyUser', { - password: '1234' + password: SecretValue.plainText('1234') }); test.deepEqual(app.synthesizeStack(stack.name).template, { Resources: { MyUserDC45028B: diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index 24d547b0262c1..0caedce93e1e7 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -273,8 +273,12 @@ export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseClu port: props.port, dbClusterParameterGroupName: props.parameterGroup && props.parameterGroup.parameterGroupName, // Admin - masterUsername: secret ? secret.jsonFieldValue('username') : props.masterUser.username, - masterUserPassword: secret ? secret.jsonFieldValue('password') : props.masterUser.password, + masterUsername: secret ? secret.secretJsonValue('username').toString() : props.masterUser.username, + masterUserPassword: secret + ? secret.secretJsonValue('password').toString() + : (props.masterUser.password + ? props.masterUser.password.toString() + : undefined), backupRetentionPeriod: props.backup && props.backup.retentionDays, preferredBackupWindow: props.backup && props.backup.preferredWindow, preferredMaintenanceWindow: props.preferredMaintenanceWindow, diff --git a/packages/@aws-cdk/aws-rds/lib/props.ts b/packages/@aws-cdk/aws-rds/lib/props.ts index 002039d05c8bf..0a414f1e3c834 100644 --- a/packages/@aws-cdk/aws-rds/lib/props.ts +++ b/packages/@aws-cdk/aws-rds/lib/props.ts @@ -1,5 +1,6 @@ import ec2 = require('@aws-cdk/aws-ec2'); import kms = require('@aws-cdk/aws-kms'); +import { SecretValue } from '@aws-cdk/cdk'; /** * The engine for the database cluster @@ -74,7 +75,7 @@ export interface Login { * * @default a Secrets Manager generated password */ - readonly password?: string; + readonly password?: SecretValue; /** * KMS encryption key to encrypt the generated secret. diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster.ts b/packages/@aws-cdk/aws-rds/test/integ.cluster.ts index 281f22ed4e89b..a6eea21795b90 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster.ts @@ -1,6 +1,7 @@ import ec2 = require('@aws-cdk/aws-ec2'); import kms = require('@aws-cdk/aws-kms'); import cdk = require('@aws-cdk/cdk'); +import { SecretValue } from '@aws-cdk/cdk'; import { DatabaseCluster, DatabaseClusterEngine } from '../lib'; import { ClusterParameterGroup } from '../lib/cluster-parameter-group'; @@ -20,7 +21,7 @@ const cluster = new DatabaseCluster(stack, 'Database', { engine: DatabaseClusterEngine.Aurora, masterUser: { username: 'admin', - password: '7959866cacc02c2d243ecfe177464fe6', + password: SecretValue.plainText('7959866cacc02c2d243ecfe177464fe6'), }, instanceProps: { instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small), diff --git a/packages/@aws-cdk/aws-rds/test/test.cluster.ts b/packages/@aws-cdk/aws-rds/test/test.cluster.ts index ce5d39e104573..124efb19cf9bb 100644 --- a/packages/@aws-cdk/aws-rds/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/test.cluster.ts @@ -2,6 +2,7 @@ import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; import ec2 = require('@aws-cdk/aws-ec2'); import kms = require('@aws-cdk/aws-kms'); import cdk = require('@aws-cdk/cdk'); +import { SecretValue } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import { ClusterParameterGroup, DatabaseCluster, DatabaseClusterEngine } from '../lib'; @@ -16,7 +17,7 @@ export = { engine: DatabaseClusterEngine.Aurora, masterUser: { username: 'admin', - password: 'tooshort', + password: SecretValue.plainText('tooshort'), }, instanceProps: { instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small), @@ -53,7 +54,7 @@ export = { engine: DatabaseClusterEngine.Aurora, masterUser: { username: 'admin', - password: 'tooshort', + password: SecretValue.plainText('tooshort'), }, instanceProps: { instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small), @@ -79,7 +80,7 @@ export = { instances: 1, masterUser: { username: 'admin', - password: 'tooshort', + password: SecretValue.plainText('tooshort'), }, instanceProps: { instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small), @@ -115,7 +116,7 @@ export = { instances: 1, masterUser: { username: 'admin', - password: 'tooshort', + password: SecretValue.plainText('tooshort'), }, instanceProps: { instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small), @@ -153,7 +154,7 @@ export = { engine: DatabaseClusterEngine.Aurora, masterUser: { username: 'admin', - password: 'tooshort', + password: SecretValue.plainText('tooshort'), }, instanceProps: { instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small), diff --git a/packages/@aws-cdk/aws-rds/test/test.rotation-single-user.ts b/packages/@aws-cdk/aws-rds/test/test.rotation-single-user.ts index 078f94fb1cee8..0c64f5759707a 100644 --- a/packages/@aws-cdk/aws-rds/test/test.rotation-single-user.ts +++ b/packages/@aws-cdk/aws-rds/test/test.rotation-single-user.ts @@ -2,6 +2,7 @@ import { expect, haveResource } from '@aws-cdk/assert'; import ec2 = require('@aws-cdk/aws-ec2'); import secretsmanager = require('@aws-cdk/aws-secretsmanager'); import cdk = require('@aws-cdk/cdk'); +import { SecretValue } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import rds = require('../lib'); @@ -167,7 +168,7 @@ export = { engine: rds.DatabaseClusterEngine.AuroraMysql, masterUser: { username: 'admin', - password: 'tooshort' + password: SecretValue.plainText('tooshort') }, instanceProps: { instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small), diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/index.ts b/packages/@aws-cdk/aws-secretsmanager/lib/index.ts index ab6379c2d85a8..79568e657c24c 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/index.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/index.ts @@ -1,5 +1,4 @@ export * from './secret'; -export * from './secret-string'; export * from './rotation-schedule'; // AWS::SecretsManager CloudFormation Resources: diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret-string.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret-string.ts deleted file mode 100644 index f87633887117c..0000000000000 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret-string.ts +++ /dev/null @@ -1,75 +0,0 @@ -import cdk = require('@aws-cdk/cdk'); - -/** - * Properties for a SecretString - */ -export interface SecretStringProps { - /** - * Unique identifier or ARN of the secret - */ - readonly secretId: string; - - /** - * Specifies the secret version that you want to retrieve by the staging label attached to the version. - * - * Can specify at most one of versionId and versionStage. - * - * @default AWSCURRENT - */ - readonly versionStage?: string; - - /** - * Specifies the unique identifier of the version of the secret that you want to use in stack operations. - * - * Can specify at most one of versionId and versionStage. - * - * @default AWSCURRENT - */ - readonly versionId?: string; -} - -/** - * References a secret string in Secrets Manager - * - * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secret.html - */ -export class SecretString extends cdk.DynamicReference { - constructor(scope: cdk.Construct, id: string, private readonly props: SecretStringProps) { - super(scope, id, { - service: cdk.DynamicReferenceService.SecretsManager, - referenceKey: '', - }); - - // If we don't validate this here it will lead to a very unclear - // error message in CloudFormation, so better do it. - if (!props.secretId) { - throw new Error('SecretString: secretId cannot be empty'); - } - } - - /** - * Return the full value of the secret - */ - public get stringValue(): string { - return this.resolveStringForJsonKey(''); - } - - /** - * Interpret the secret as a JSON object and return a field's value from it - */ - public jsonFieldValue(key: string) { - return this.resolveStringForJsonKey(key); - } - - private resolveStringForJsonKey(jsonKey: string) { - const parts = [ - this.props.secretId, - 'SecretString', - jsonKey, - this.props.versionStage || '', - this.props.versionId || '' - ]; - - return this.makeResolveValue(cdk.DynamicReferenceService.SecretsManager, parts.join(':')); - } -} diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts index 74661d1ccc1d2..4c7d14021d7a9 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts @@ -1,14 +1,13 @@ import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); -import cdk = require('@aws-cdk/cdk'); +import { Construct, IConstruct, SecretValue } from '@aws-cdk/cdk'; import { RotationSchedule, RotationScheduleOptions } from './rotation-schedule'; -import { SecretString } from './secret-string'; import secretsmanager = require('./secretsmanager.generated'); /** * A secret in AWS Secrets Manager. */ -export interface ISecret extends cdk.IConstruct { +export interface ISecret extends IConstruct { /** * The customer-managed encryption key that is used to encrypt this secret, if any. When not specified, the default * KMS key for the account and region is being used. @@ -21,21 +20,14 @@ export interface ISecret extends cdk.IConstruct { readonly secretArn: string; /** - * Returns a SecretString corresponding to this secret. - * - * SecretString represents the value of the Secret. - */ - readonly secretString: SecretString; - - /** - * Retrieve the value of the Secret, as a string. + * Retrieve the value of the stored secret as a `SecretValue`. */ - readonly stringValue: string; + readonly secretValue: SecretValue; /** - * Interpret the secret as a JSON object and return a field's value from it + * Interpret the secret as a JSON object and return a field's value from it as a `SecretValue`. */ - jsonFieldValue(key: string): string; + secretJsonValue(key: string): SecretValue; /** * Exports this secret. @@ -87,7 +79,7 @@ export interface SecretProps { * A name for the secret. Note that deleting secrets from SecretsManager does not happen immediately, but after a 7 to * 30 days blackout period. During that period, it is not possible to create another secret that shares the same name. * - * @default a name is generated by CloudFormation. + * @default - a name is generated by CloudFormation. */ readonly name?: string; } @@ -110,12 +102,10 @@ export interface SecretImportProps { /** * The common behavior of Secrets. Users should not use this class directly, and instead use ``Secret``. */ -export abstract class SecretBase extends cdk.Construct implements ISecret { +export abstract class SecretBase extends Construct implements ISecret { public abstract readonly encryptionKey?: kms.IEncryptionKey; public abstract readonly secretArn: string; - private _secretString?: SecretString; - public abstract export(): SecretImportProps; public grantRead(grantee: iam.IGrantable, versionStages?: string[]): iam.Grant { @@ -143,17 +133,12 @@ export abstract class SecretBase extends cdk.Construct implements ISecret { return result; } - public get secretString() { - this._secretString = this._secretString || new SecretString(this, 'SecretString', { secretId: this.secretArn }); - return this._secretString; - } - - public get stringValue() { - return this.secretString.stringValue; + public get secretValue() { + return this.secretJsonValue(''); } - public jsonFieldValue(key: string): string { - return this.secretString.jsonFieldValue(key); + public secretJsonValue(jsonField: string) { + return SecretValue.secretsManager(this.secretArn, { jsonField }); } public addRotationSchedule(id: string, options: RotationScheduleOptions): RotationSchedule { @@ -175,14 +160,14 @@ export class Secret extends SecretBase { * @param id the ID of the imported Secret in the construct tree. * @param props the attributes of the imported secret. */ - public static import(scope: cdk.Construct, id: string, props: SecretImportProps): ISecret { + public static import(scope: Construct, id: string, props: SecretImportProps): ISecret { return new ImportedSecret(scope, id, props); } public readonly encryptionKey?: kms.IEncryptionKey; public readonly secretArn: string; - constructor(scope: cdk.Construct, id: string, props: SecretProps = {}) { + constructor(scope: Construct, id: string, props: SecretProps = {}) { super(scope, id); if (props.generateSecretString && @@ -289,7 +274,7 @@ export class AttachedSecret extends SecretBase implements ISecret { public readonly encryptionKey?: kms.IEncryptionKey; public readonly secretArn: string; - constructor(scope: cdk.Construct, id: string, props: AttachedSecretProps) { + constructor(scope: Construct, id: string, props: AttachedSecretProps) { super(scope, id); const attachment = new secretsmanager.CfnSecretTargetAttachment(this, 'Resource', { @@ -393,7 +378,7 @@ class ImportedSecret extends SecretBase { public readonly encryptionKey?: kms.IEncryptionKey; public readonly secretArn: string; - constructor(scope: cdk.Construct, id: string, private readonly props: SecretImportProps) { + constructor(scope: Construct, id: string, private readonly props: SecretImportProps) { super(scope, id); this.encryptionKey = props.encryptionKey; diff --git a/packages/@aws-cdk/aws-secretsmanager/test/example.app-with-secret.lit.ts b/packages/@aws-cdk/aws-secretsmanager/test/example.app-with-secret.lit.ts index bcaca6be74842..f5b15b4a6b2b5 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/example.app-with-secret.lit.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/example.app-with-secret.lit.ts @@ -7,14 +7,14 @@ class ExampleStack extends cdk.Stack { super(scope, id); /// !show - const loginSecret = new secretsmanager.SecretString(this, 'Secret', { - secretId: 'SomeLogin' + const loginSecret = secretsmanager.Secret.import(this, 'Secret', { + secretArn: 'SomeLogin' }); new iam.User(this, 'User', { // Get the 'password' field from the secret that looks like // { "username": "XXXX", "password": "YYYY" } - password: loginSecret.jsonFieldValue('password') + password: loginSecret.secretJsonValue('password') }); /// !hide diff --git a/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.ts b/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.ts index 171426e6ba45c..4553d0042c87f 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.ts @@ -14,7 +14,7 @@ class SecretsManagerStack extends cdk.Stack { secret.grantRead(role); new iam.User(this, 'User', { - password: secret.stringValue + password: secret.secretValue }); // Templated secret @@ -26,8 +26,8 @@ class SecretsManagerStack extends cdk.Stack { }); new iam.User(this, 'OtherUser', { - userName: templatedSecret.jsonFieldValue('username'), - password: templatedSecret.jsonFieldValue('password') + userName: templatedSecret.secretJsonValue('username').toString(), + password: templatedSecret.secretJsonValue('password') }); /// !hide } diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.secret-string.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.secret-string.ts deleted file mode 100644 index acb2087bb74aa..0000000000000 --- a/packages/@aws-cdk/aws-secretsmanager/test/test.secret-string.ts +++ /dev/null @@ -1,49 +0,0 @@ -import cdk = require('@aws-cdk/cdk'); -import { Test } from 'nodeunit'; -import secretsmanager = require('../lib'); - -export = { - 'can reference Secrets Manager Value'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const ref = new secretsmanager.SecretString(stack, 'Ref', { - secretId: 'SomeSecret', - }); - - // THEN - test.equal(ref.node.resolve(ref.stringValue), '{{resolve:secretsmanager:SomeSecret:SecretString:::}}'); - - test.done(); - }, - - 'can reference jsonkey in Secrets Manager Value'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const ref = new secretsmanager.SecretString(stack, 'Ref', { - secretId: 'SomeSecret', - }); - - // THEN - test.equal(ref.node.resolve(ref.jsonFieldValue('subkey')), '{{resolve:secretsmanager:SomeSecret:SecretString:subkey::}}'); - - test.done(); - }, - - 'empty secretId will throw'(test: Test) { - // GIVEN - const stack = new cdk.Stack(); - - // WHEN - test.throws(() => { - new secretsmanager.SecretString(stack, 'Ref', { - secretId: '', - }); - }, /secretId cannot be empty/); - - test.done(); - }, -}; diff --git a/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts b/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts index d8d6435ef7d59..91f361f756d53 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/test.secret.ts @@ -3,6 +3,7 @@ import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); import lambda = require('@aws-cdk/aws-lambda'); import cdk = require('@aws-cdk/cdk'); +import { SecretValue, Stack } from '@aws-cdk/cdk'; import { Test } from 'nodeunit'; import secretsmanager = require('../lib'); @@ -261,7 +262,7 @@ export = { test.done(); }, - 'toSecretString'(test: Test) { + 'secretValue'(test: Test) { // GIVEN const stack = new cdk.Stack(); const key = new kms.EncryptionKey(stack, 'KMS'); @@ -271,7 +272,7 @@ export = { new cdk.CfnResource(stack, 'FakeResource', { type: 'CDK::Phony::Resource', properties: { - value: secret.stringValue + value: secret.secretValue } }); @@ -302,6 +303,8 @@ export = { // THEN test.equals(secret.secretArn, secretArn); test.same(secret.encryptionKey, encryptionKey); + test.deepEqual(stack.node.resolve(secret.secretValue), '{{resolve:secretsmanager:arn::of::a::secret:SecretString:::}}'); + test.deepEqual(stack.node.resolve(secret.secretJsonValue('password')), '{{resolve:secretsmanager:arn::of::a::secret:SecretString:password::}}'); test.done(); }, @@ -388,6 +391,19 @@ export = { } }), /`secretStringTemplate`.+`generateStringKey`/); + test.done(); + }, + + 'equivalence of SecretValue and Secret.import'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const imported = secretsmanager.Secret.import(stack, 'Imported', { secretArn: 'my-secret-arn' }).secretJsonValue('password'); + const value = SecretValue.secretsManager('my-secret-arn', { jsonField: 'password' }); + + // THEN + test.deepEqual(stack.node.resolve(imported), stack.node.resolve(value)); test.done(); } }; diff --git a/packages/@aws-cdk/aws-ssm/lib/parameter-store-string.ts b/packages/@aws-cdk/aws-ssm/lib/parameter-store-string.ts index 3cce5e00254e6..b47940dfc6d86 100644 --- a/packages/@aws-cdk/aws-ssm/lib/parameter-store-string.ts +++ b/packages/@aws-cdk/aws-ssm/lib/parameter-store-string.ts @@ -48,11 +48,8 @@ export class ParameterStoreString extends cdk.Construct { this.stringValue = param.stringValue; } else { // Use a dynamic reference - const dynRef = new cdk.DynamicReference(this, 'Reference', { - service: cdk.DynamicReferenceService.Ssm, - referenceKey: `${props.parameterName}:${props.version}`, - }); - this.stringValue = dynRef.stringValue; + const dynRef = new cdk.CfnDynamicReference(cdk.CfnDynamicReferenceService.Ssm, `${props.parameterName}:${props.version}`); + this.stringValue = dynRef.toString(); } } } @@ -80,12 +77,9 @@ export interface ParameterStoreSecureStringProps { * * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html */ -export class ParameterStoreSecureString extends cdk.DynamicReference { - constructor(scope: cdk.Construct, id: string, props: ParameterStoreSecureStringProps) { - super(scope, id, { - service: cdk.DynamicReferenceService.SsmSecure, - referenceKey: `${props.parameterName}:${props.version}`, - }); +export class ParameterStoreSecureString extends cdk.CfnDynamicReference { + constructor(props: ParameterStoreSecureStringProps) { + super(cdk.CfnDynamicReferenceService.SsmSecure, `${props.parameterName}:${props.version}`); // If we don't validate this here it will lead to a very unclear // error message in CloudFormation, so better do it. diff --git a/packages/@aws-cdk/aws-ssm/test/integ.parameter-store-string.lit.ts b/packages/@aws-cdk/aws-ssm/test/integ.parameter-store-string.lit.ts index 261ec9363cf86..a00e6575511b0 100644 --- a/packages/@aws-cdk/aws-ssm/test/integ.parameter-store-string.lit.ts +++ b/packages/@aws-cdk/aws-ssm/test/integ.parameter-store-string.lit.ts @@ -26,10 +26,10 @@ class UsingStack extends cdk.Stack { // Retrieve a specific version of the secret (SecureString) parameter. // 'version' is always required. - const secretValue = new ssm.ParameterStoreSecureString(this, 'SecretValue', { + const secretValue = new ssm.ParameterStoreSecureString({ parameterName: '/My/Secret/Parameter', version: 5 - }).stringValue; + }); /// !hide new cdk.CfnOutput(this, 'TheValue', { value: stringValue }); diff --git a/packages/@aws-cdk/aws-ssm/test/test.parameter-store-string.ts b/packages/@aws-cdk/aws-ssm/test/test.parameter-store-string.ts index fd226f7ad8b81..1ee43b2df1f0b 100644 --- a/packages/@aws-cdk/aws-ssm/test/test.parameter-store-string.ts +++ b/packages/@aws-cdk/aws-ssm/test/test.parameter-store-string.ts @@ -49,13 +49,13 @@ export = { const stack = new cdk.Stack(); // WHEN - const ref = new ssm.ParameterStoreSecureString(stack, 'Ref', { + const ref = new ssm.ParameterStoreSecureString({ parameterName: '/some/key', version: 123 }); // THEN - test.equal(ref.node.resolve(ref.stringValue), '{{resolve:ssm-secure:/some/key:123}}'); + test.equal(stack.node.resolve(ref), '{{resolve:ssm-secure:/some/key:123}}'); test.done(); }, diff --git a/packages/@aws-cdk/cdk/README.md b/packages/@aws-cdk/cdk/README.md index 0aef94964ed42..6c085700c021e 100644 --- a/packages/@aws-cdk/cdk/README.md +++ b/packages/@aws-cdk/cdk/README.md @@ -1,6 +1,6 @@ ## AWS Cloud Development Kit Core Library -This library includes the basic building blocks of +This library includes the basic building blocks of the [AWS Cloud Development Kit](https://github.com/awslabs/aws-cdk) (AWS CDK). ## Aspects @@ -185,3 +185,26 @@ const vpc = new ec2.VpcNetwork(this, 'MyVpc', { ... }); vpc.node.apply(new cdk.Tag('MyKey', 'MyValue', { priority: 2 })); // ... snip ``` + +## Secrets + +To help avoid accidental storage of secrets as plain text we use the `SecretValue` type to +represent secrets. + +The best practice is to store secrets in AWS Secrets Manager and reference them using `SecretValue.secretsManager`: + +```ts +const secret = SecretValue.secretsManager('secretId', { + jsonField: 'password' // optional: key of a JSON field to retrieve (defaults to all content), + versionId: 'id' // optional: id of the version (default AWSCURRENT) + versionStage: 'stage' // optional: version stage name (default AWSCURRENT) +}); +``` + +Using AWS Secrets Manager is the recommended way to reference secrets in a CDK app. +However, `SecretValue` supports the following additional options: + + * `SecretValue.plainText(secret)`: stores the secret as plain text in your app and the resulting template (not recommended). + * `SecretValue.ssmSecure(param, version)`: refers to a secret stored as a SecureString in the SSM Parameter Store. + * `SecretValue.cfnParameter(param)`: refers to a secret passed through a CloudFormation parameter (must have `NoEcho: true`). + * `SecretValue.cfnDynamicReference(dynref)`: refers to a secret described by a CloudFormation dynamic reference (used by `ssmSecure` and `secretsManager`). diff --git a/packages/@aws-cdk/cdk/lib/cfn-dynamic-reference.ts b/packages/@aws-cdk/cdk/lib/cfn-dynamic-reference.ts new file mode 100644 index 0000000000000..4f3c18495916d --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/cfn-dynamic-reference.ts @@ -0,0 +1,50 @@ +import { Token } from "./token"; + +/** + * Properties for a Dynamic Reference + */ +export interface DynamicReferenceProps { + /** + * The service to retrieve the dynamic reference from + */ + readonly service: CfnDynamicReferenceService; + + /** + * The reference key of the dynamic reference + */ + readonly referenceKey: string; +} + +/** + * References a dynamically retrieved value + * + * This is a Construct so that subclasses will (eventually) be able to attach + * metadata to themselves without having to change call signatures. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html + */ +export class CfnDynamicReference extends Token { + constructor(service: CfnDynamicReferenceService, key: string) { + super(() => '{{resolve:' + service + ':' + key + '}}'); + } +} + +/** + * The service to retrieve the dynamic reference from + */ +export enum CfnDynamicReferenceService { + /** + * Plaintext value stored in AWS Systems Manager Parameter Store + */ + Ssm = 'ssm', + + /** + * Secure string stored in AWS Systems Manager Parameter Store + */ + SsmSecure = 'ssm-secure', + + /** + * Secret stored in AWS Secrets Manager + */ + SecretsManager = 'secretsmanager', +} diff --git a/packages/@aws-cdk/cdk/lib/cfn-parameter.ts b/packages/@aws-cdk/cdk/lib/cfn-parameter.ts index b8477b4b9ab1f..ad5f84d7a91c5 100644 --- a/packages/@aws-cdk/cdk/lib/cfn-parameter.ts +++ b/packages/@aws-cdk/cdk/lib/cfn-parameter.ts @@ -86,6 +86,11 @@ export class CfnParameter extends CfnRefElement { */ public stringListValue: string[]; + /** + * Indicates if this parameter has "NoEcho" set. + */ + public readonly noEcho: boolean; + private properties: CfnParameterProps; /** @@ -102,6 +107,7 @@ export class CfnParameter extends CfnRefElement { this.value = this.referenceToken; this.stringValue = this.value.toString(); this.stringListValue = this.value.toList(); + this.noEcho = props.noEcho || false; } /** diff --git a/packages/@aws-cdk/cdk/lib/dynamic-reference.ts b/packages/@aws-cdk/cdk/lib/dynamic-reference.ts deleted file mode 100644 index 0cc42f48be353..0000000000000 --- a/packages/@aws-cdk/cdk/lib/dynamic-reference.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Construct } from "./construct"; -import { Token } from "./token"; - -/** - * Properties for a Dynamic Reference - */ -export interface DynamicReferenceProps { - /** - * The service to retrieve the dynamic reference from - */ - readonly service: DynamicReferenceService; - - /** - * The reference key of the dynamic reference - */ - readonly referenceKey: string; -} - -/** - * References a dynamically retrieved value - * - * This is a Construct so that subclasses will (eventually) be able to attach - * metadata to themselves without having to change call signatures. - * - * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html - */ -export class DynamicReference extends Construct { - private _value: string; - - constructor(scope: Construct, id: string, props: DynamicReferenceProps) { - super(scope, id); - - this._value = this.makeResolveValue(props.service, props.referenceKey); - } - - /** - * The value of this dynamic reference - */ - public get stringValue(): string { - return this._value; - } - - /** - * Make a dynamic reference Token value - * - * This is a value (similar to CDK Tokens) that will be substituted by - * CloudFormation before executing the changeset. - */ - protected makeResolveValue(service: DynamicReferenceService, referenceKey: string) { - const resolveString = '{{resolve:' + service + ':' + referenceKey + '}}'; - - // We don't strictly need to Tokenize a string here, but we do it anyway to be perfectly - // clear that DynamicReference.value is unparseable in CDK apps. - return new Token(resolveString).toString(); - } -} - -/** - * The service to retrieve the dynamic reference from - */ -export enum DynamicReferenceService { - /** - * Plaintext value stored in AWS Systems Manager Parameter Store - */ - Ssm = 'ssm', - - /** - * Secure string stored in AWS Systems Manager Parameter Store - */ - SsmSecure = 'ssm-secure', - - /** - * Secret stored in AWS Secrets Manager - */ - SecretsManager = 'secretsmanager', -} diff --git a/packages/@aws-cdk/cdk/lib/index.ts b/packages/@aws-cdk/cdk/lib/index.ts index 05ba430ed0dc4..dee345c7d033d 100644 --- a/packages/@aws-cdk/cdk/lib/index.ts +++ b/packages/@aws-cdk/cdk/lib/index.ts @@ -23,7 +23,7 @@ export * from './resource-policy'; export * from './cfn-rule'; export * from './stack'; export * from './cfn-element'; -export * from './dynamic-reference'; +export * from './cfn-dynamic-reference'; export * from './tag'; export * from './removal-policy'; export * from './arn'; @@ -33,5 +33,5 @@ export * from './context'; export * from './environment'; export * from './runtime'; -export * from './secret'; +export * from './secret-value'; export * from './synthesis'; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/secret-value.ts b/packages/@aws-cdk/cdk/lib/secret-value.ts new file mode 100644 index 0000000000000..e5b12581eda2e --- /dev/null +++ b/packages/@aws-cdk/cdk/lib/secret-value.ts @@ -0,0 +1,126 @@ +import { CfnDynamicReference, CfnDynamicReferenceService } from './cfn-dynamic-reference'; +import { CfnParameter } from './cfn-parameter'; +import { Token } from './token'; + +/** + * Work with secret values in the CDK + * + * Secret values in the CDK (such as those retrieved from SecretsManager) are + * represented as regular strings, just like other values that are only + * available at deployment time. + * + * To help you avoid accidental mistakes which would lead to you putting your + * secret values directly into a CloudFormation template, constructs that take + * secret values will not allow you to pass in a literal secret value. They do + * so by calling `Secret.assertSafeSecret()`. + * + * You can escape the check by calling `Secret.plainTex()`, but doing + * so is highly discouraged. + */ +export class SecretValue extends Token { + /** + * Construct a literal secret value for use with secret-aware constructs + * + * *Do not use this method for any secrets that you care about.* + * + * The only reasonable use case for using this method is when you are testing. + */ + public static plainText(secret: string): SecretValue { + return new SecretValue(secret); + } + + /** + * Creates a `SecretValue` with a value which is dynamically loaded from AWS Secrets Manager. + * @param secretId The ID or ARN of the secret + * @param options Options + */ + public static secretsManager(secretId: string, options: SecretsManagerSecretOptions = { }): SecretValue { + if (!secretId) { + throw new Error(`secretId cannot be empty`); + } + + const parts = [ + secretId, + 'SecretString', + options.jsonField || '', + options.versionStage || '', + options.versionId || '' + ]; + + const dyref = new CfnDynamicReference(CfnDynamicReferenceService.SecretsManager, parts.join(':')); + return this.cfnDynamicReference(dyref); + } + + /** + * Use a secret value stored from a Systems Manager (SSM) parameter. + * + * @param parameterName The name of the parameter in the Systems Manager + * Parameter Store. The parameter name is case-sensitive. + * + * @param version An integer that specifies the version of the parameter to + * use. You must specify the exact version. You cannot currently specify that + * AWS CloudFormation use the latest version of a parameter. + */ + public static ssmSecure(parameterName: string, version: string): SecretValue { + const parts = [ parameterName, version ]; + return this.cfnDynamicReference(new CfnDynamicReference(CfnDynamicReferenceService.SsmSecure, parts.join(':'))); + } + + /** + * Obtain the secret value through a CloudFormation dynamic reference. + * + * If possible, use `SecretValue.ssmSecure` or `SecretValue.secretsManager` directly. + * + * @param ref The dynamic reference to use. + */ + public static cfnDynamicReference(ref: CfnDynamicReference) { + return new SecretValue(() => ref.toString()); + } + + /** + * Obtain the secret value through a CloudFormation parameter. + * + * Generally, this is not a recommended approach. AWS Secrets Manager is the + * recommended way to reference secrets. + * + * @param param The CloudFormation parameter to use. + */ + public static cfnParameter(param: CfnParameter) { + if (!param.noEcho) { + throw new Error(`CloudFormation parameter must be configured with "NoEcho"`); + } + + return new SecretValue(param.value); + } +} + +/** + * Options for referencing a secret value from Secrets Manager. + */ +export interface SecretsManagerSecretOptions { + /** + * Specified the secret version that you want to retrieve by the staging label attached to the version. + * + * Can specify at most one of `versionId` and `versionStage`. + * + * @default AWSCURRENT + */ + readonly versionStage?: string; + + /** + * Specifies the unique identifier of the version of the secret you want to use. + * + * Can specify at most one of `versionId` and `versionStage`. + * + * @default AWSCURRENT + */ + readonly versionId?: string; + + /** + * The key of a JSON field to retrieve. This can only be used if the secret + * stores a JSON object. + * + * @default - returns all the content stored in the Secrets Manager secret. + */ + readonly jsonField?: string; +} diff --git a/packages/@aws-cdk/cdk/lib/secret.ts b/packages/@aws-cdk/cdk/lib/secret.ts deleted file mode 100644 index cea6503934d22..0000000000000 --- a/packages/@aws-cdk/cdk/lib/secret.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Token } from "./token"; -import { unresolved } from "./unresolved"; - -/** - * Work with secret values in the CDK - * - * Secret values in the CDK (such as those retrieved from SecretsManager) are - * represented as regular strings, just like other values that are only - * available at deployment time. - * - * To help you avoid accidental mistakes which would lead to you putting your - * secret values directly into a CloudFormation template, constructs that take - * secret values will not allow you to pass in a literal secret value. They do - * so by calling `Secret.assertSafeSecret()`. - * - * You can escape the check by calling `Secret.plainTex()`, but doing - * so is highly discouraged. - */ -export class Secret { - /** - * Validate that a given secret value is not a literal - * - * If the value is a literal, throw an error. - */ - public static assertSafeSecret(secretValue: string, parameterName?: string) { - if (!unresolved(secretValue)) { - const theParameter = parameterName ? `'${parameterName}'` : 'The value'; - - // tslint:disable-next-line:max-line-length - throw new Error(`${theParameter} should be a secret. Store it in SecretsManager or Systems Manager Parameter Store and retrieve it from there. Secret.plainTex() can be used to bypass this check, but do so for testing purposes only.`); - } - } - - /** - * Construct a literal secret value for use with secret-aware constructs - * - * *Do not use this method for any secrets that you care about.* - * - * The only reasonable use case for using this method is when you are testing. - */ - public static plainText(secret: string): string { - return new Token(() => secret).toString(); - } - - private constructor() { - } -} \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/lib/token.ts b/packages/@aws-cdk/cdk/lib/token.ts index 44032b91a6e0c..4ce9792079931 100644 --- a/packages/@aws-cdk/cdk/lib/token.ts +++ b/packages/@aws-cdk/cdk/lib/token.ts @@ -1,5 +1,6 @@ import { IConstruct } from "./construct"; import { TOKEN_MAP } from "./encoding"; +import { unresolved } from './unresolved'; /** * If objects has a function property by this name, they will be considered tokens, and this @@ -18,6 +19,16 @@ export const RESOLVE_METHOD = 'resolve'; * semantics. */ export class Token { + /** + * Returns true if obj is a token (i.e. has the resolve() method or is a string + * that includes token markers), or it's a listifictaion of a Token string. + * + * @param obj The object to test. + */ + public static unresolved(obj: any): boolean { + return unresolved(obj); + } + private tokenStringification?: string; private tokenListification?: string[]; diff --git a/packages/@aws-cdk/cdk/lib/unresolved.ts b/packages/@aws-cdk/cdk/lib/unresolved.ts index 1262f6f86c0fd..06cb599149351 100644 --- a/packages/@aws-cdk/cdk/lib/unresolved.ts +++ b/packages/@aws-cdk/cdk/lib/unresolved.ts @@ -6,6 +6,7 @@ import { RESOLVE_METHOD } from "./token"; * that includes token markers), or it's a listifictaion of a Token string. * * @param obj The object to test. + * @deprecated use `Token.unresolved` */ export function unresolved(obj: any): boolean { if (typeof(obj) === 'string') { diff --git a/packages/@aws-cdk/cdk/test/test.dynamic-reference.ts b/packages/@aws-cdk/cdk/test/test.dynamic-reference.ts index fa82494a33abe..b16b999f9527e 100644 --- a/packages/@aws-cdk/cdk/test/test.dynamic-reference.ts +++ b/packages/@aws-cdk/cdk/test/test.dynamic-reference.ts @@ -1,5 +1,5 @@ import { Test } from 'nodeunit'; -import { DynamicReference, DynamicReferenceService, Stack } from '../lib'; +import { CfnDynamicReference, CfnDynamicReferenceService, Stack } from '../lib'; export = { 'can create dynamic references with service and key with colons'(test: Test) { @@ -7,13 +7,10 @@ export = { const stack = new Stack(); // WHEN - const ref = new DynamicReference(stack, 'Ref', { - service: DynamicReferenceService.Ssm, - referenceKey: 'a:b:c', - }); + const ref = new CfnDynamicReference(CfnDynamicReferenceService.Ssm, 'a:b:c'); // THEN - test.equal(stack.node.resolve(ref.stringValue), '{{resolve:ssm:a:b:c}}'); + test.equal(stack.node.resolve(ref), '{{resolve:ssm:a:b:c}}'); test.done(); }, diff --git a/packages/@aws-cdk/cdk/test/test.secret-value.ts b/packages/@aws-cdk/cdk/test/test.secret-value.ts new file mode 100644 index 0000000000000..5d3e54f013f30 --- /dev/null +++ b/packages/@aws-cdk/cdk/test/test.secret-value.ts @@ -0,0 +1,97 @@ +import { Test } from 'nodeunit'; +import { CfnDynamicReference, CfnDynamicReferenceService, CfnParameter, SecretValue, Stack } from '../lib'; + +export = { + 'plainText'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const v = SecretValue.plainText('this just resolves to a string'); + + // THEN + test.deepEqual(stack.node.resolve(v), 'this just resolves to a string'); + test.done(); + }, + + 'secretsManager'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const v = SecretValue.secretsManager('secret-id', { + jsonField: 'json-key', + versionId: 'version-id', + versionStage: 'version-stage' + }); + + // THEN + test.deepEqual(stack.node.resolve(v), '{{resolve:secretsmanager:secret-id:SecretString:json-key:version-stage:version-id}}'); + test.done(); + }, + + 'secretsManager with defaults'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const v = SecretValue.secretsManager('secret-id'); + + // THEN + test.deepEqual(stack.node.resolve(v), '{{resolve:secretsmanager:secret-id:SecretString:::}}'); + test.done(); + }, + + 'secretsManager with an empty ID'(test: Test) { + test.throws(() => SecretValue.secretsManager(''), /secretId cannot be empty/); + test.done(); + }, + + 'ssmSecure'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const v = SecretValue.ssmSecure('param-name', 'param-version'); + + // THEN + test.deepEqual(stack.node.resolve(v), '{{resolve:ssm-secure:param-name:param-version}}'); + test.done(); + }, + + 'cfnDynamicReference'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const v = SecretValue.cfnDynamicReference(new CfnDynamicReference(CfnDynamicReferenceService.Ssm, 'foo:bar')); + + // THEN + test.deepEqual(stack.node.resolve(v), '{{resolve:ssm:foo:bar}}'); + test.done(); + }, + + 'cfnParameter (with NoEcho)'(test: Test) { + // GIVEN + const stack = new Stack(); + const p = new CfnParameter(stack, 'MyParam', { type: 'String', noEcho: true }); + + // WHEN + const v = SecretValue.cfnParameter(p); + + // THEN + test.deepEqual(stack.node.resolve(v), { Ref: 'MyParam' }); + test.done(); + }, + + 'fails if cfnParameter does not have NoEcho'(test: Test) { + // GIVEN + const stack = new Stack(); + const p = new CfnParameter(stack, 'MyParam', { type: 'String' }); + + // THEN + test.throws(() => SecretValue.cfnParameter(p), /CloudFormation parameter must be configured with "NoEcho"/); + test.done(); + } + +}; \ No newline at end of file diff --git a/packages/@aws-cdk/cdk/test/test.secret.ts b/packages/@aws-cdk/cdk/test/test.secret.ts deleted file mode 100644 index 936e036540260..0000000000000 --- a/packages/@aws-cdk/cdk/test/test.secret.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Test } from 'nodeunit'; -import cdk = require('../lib'); - -export = { - 'throws on literal value'(test: Test) { - test.throws(() => { - cdk.Secret.assertSafeSecret('bla'); - }, /should be a secret/); - test.done(); - }, - - 'can bypass'(test: Test) { - cdk.Secret.assertSafeSecret(cdk.Secret.plainText('bla')); - test.done(); - }, -}; \ No newline at end of file