Skip to content

Commit

Permalink
feat(secretsmanager): create secret with secretObjectValue (#21091)
Browse files Browse the repository at this point in the history
A common use case is to create key/value secrets where the values could
be either strings _or_ other secret values. Currently this is possible,
but the user experience is not great. This PR introduces a new input
prop `secretObjectValue` which is of type `{ [key: string]: SecretValue }`.

For example, you can now create a JSON secret:
```ts
new secretsmanager.Secret(stack, 'JSONSecret', {
  secretObjectValue: {
    username: SecretValue.unsafePlainText(user.userName), // intrinsic reference, not exposed as plaintext
    database: SecretValue.unsafePlainText('foo'), // rendered as plain text, but not a secret
    password: accessKey.secretAccessKey, // SecretValue
  },
});
```

I've also updated the docs to better reflect what `unsafe` means given
this new context.

fixes #20461


----

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md)

### Adding new Unconventional Dependencies:

* [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies)

### New Features

* [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)?
	* [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
corymhall committed Jul 13, 2022
1 parent 541ce1b commit 5f0eff2
Show file tree
Hide file tree
Showing 13 changed files with 291 additions and 40 deletions.
33 changes: 33 additions & 0 deletions packages/@aws-cdk/aws-secretsmanager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,3 +252,36 @@ Alternatively, use `addReplicaRegion()`:
const secret = new secretsmanager.Secret(this, 'Secret');
secret.addReplicaRegion('eu-west-1');
```

## Creating JSON Secrets

Sometimes it is necessary to create a secret in SecretsManager that contains a JSON object.
For example:

```json
{
"username": "myUsername",
"database": "foo",
"password": "mypassword"
}
```

In order to create this type of secret, use the `secretObjectValue` input prop.

```ts
const user = new iam.User(stack, 'User');
const accessKey = new iam.AccessKey(stack, 'AccessKey', { user });
declare const stack: Stack;

new secretsmanager.Secret(stack, 'Secret', {
secretObjectValue: {
username: SecretValue.unsafePlainText(user.userName),
database: SecretValue.unsafePlainText('foo'),
password: accessKey.secretAccessKey,
},
})
```

In this case both the `username` and `database` are not a `Secret` so `SecretValue.unsafePlainText` needs to be used.
This means that they will be rendered as plain text in the template, but in this case neither of those
are actual "secrets".
58 changes: 51 additions & 7 deletions packages/@aws-cdk/aws-secretsmanager/lib/secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,44 @@ export interface SecretProps {
* to the CloudFormation template (via the AWS Console, SDKs, or CLI).
*
* Specifies text data that you want to encrypt and store in this new version of the secret.
* May be a simple string value, or a string representation of a JSON structure.
* May be a simple string value. To provide a string representation of JSON structure, use {@link SecretProps.secretObjectValue} instead.
*
* Only one of `secretStringBeta1`, `secretStringValue`, and `generateSecretString` can be provided.
* Only one of `secretStringBeta1`, `secretStringValue`, 'secretObjectValue', and `generateSecretString` can be provided.
*
* @default - SecretsManager generates a new secret value.
*/
readonly secretStringValue?: SecretValue;

/**
* Initial value for a JSON secret
*
* **NOTE:** *It is **highly** encouraged to leave this field undefined and allow SecretsManager to create the secret value.
* The secret object -- if provided -- will be included in the output of the cdk as part of synthesis,
* and will appear in the CloudFormation template in the console. This can be secure(-ish) if that value is merely reference to
* another resource (or one of its attributes), but if the value is a plaintext string, it will be visible to anyone with access
* to the CloudFormation template (via the AWS Console, SDKs, or CLI).
*
* Specifies a JSON object that you want to encrypt and store in this new version of the secret.
* To specify a simple string value instead, use {@link SecretProps.secretStringValue}
*
* Only one of `secretStringBeta1`, `secretStringValue`, 'secretObjectValue', and `generateSecretString` can be provided.
*
* @example
* declare const user: iam.User;
* declare const accessKey: iam.AccessKey;
* declare const stack: Stack;
* new secretsmanager.Secret(stack, 'JSONSecret', {
* secretObjectValue: {
* username: SecretValue.unsafePlainText(user.userName), // intrinsic reference, not exposed as plaintext
* database: SecretValue.unsafePlainText('foo'), // rendered as plain text, but not a secret
* password: accessKey.secretAccessKey, // SecretValue
* },
* });
*
* @default - SecretsManager generates a new secret value.
*/
readonly secretObjectValue?: { [key: string]: SecretValue };

/**
* Policy to apply when the secret is removed from this stack.
*
Expand Down Expand Up @@ -233,7 +263,7 @@ export class SecretStringValueBeta1 {
* // Creates a new IAM user, access and secret keys, and stores the secret access key in a Secret.
* const user = new iam.User(this, 'User');
* const accessKey = new iam.AccessKey(this, 'AccessKey', { user });
* const secret = new secrets.Secret(this, 'Secret', {
* const secret = new secretsmanager.Secret(this, 'Secret', {
* secretStringValue: accessKey.secretAccessKey,
* });
* ```
Expand Down Expand Up @@ -582,11 +612,17 @@ export class Secret extends SecretBase {
throw new Error('`secretStringTemplate` and `generateStringKey` must be specified together.');
}

if ((props.generateSecretString ? 1 : 0) + (props.secretStringBeta1 ? 1 : 0) + (props.secretStringValue ? 1 : 0) > 1) {
throw new Error('Cannot specify more than one of `generateSecretString`, `secretStringValue`, and `secretStringBeta1`.');
if ((props.generateSecretString ? 1 : 0)
+ (props.secretStringBeta1 ? 1 : 0)
+ (props.secretStringValue ? 1 : 0)
+ (props.secretObjectValue ? 1 : 0)
> 1) {
throw new Error('Cannot specify more than one of `generateSecretString`, `secretStringValue`, `secretObjectValue`, and `secretStringBeta1`.');
}

const secretString = props.secretStringValue?.unsafeUnwrap() ?? props.secretStringBeta1?.secretValue();
const secretString = props.secretObjectValue
? this.resolveSecretObjectValue(props.secretObjectValue)
: props.secretStringValue?.unsafeUnwrap() ?? props.secretStringBeta1?.secretValue();

const resource = new secretsmanager.CfnSecret(this, 'Resource', {
description: props.description,
Expand Down Expand Up @@ -627,6 +663,14 @@ export class Secret extends SecretBase {
this.excludeCharacters = props.generateSecretString?.excludeCharacters;
}

private resolveSecretObjectValue(secretObject: { [key: string]: SecretValue }): string {
const resolvedObject: { [key: string]: string } = {};
for (const [key, value] of Object.entries(secretObject)) {
resolvedObject[key] = value.unsafeUnwrap();
}
return JSON.stringify(resolvedObject);
}

/**
* Adds a target attachment to the secret.
*
Expand Down Expand Up @@ -968,4 +1012,4 @@ function attachmentTargetTypeToString(x: AttachmentTargetType): string {
case AttachmentTargetType.DOCDB_DB_CLUSTER:
return 'AWS::DocDB::DBCluster';
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Fixture with packages imported, but nothing else
import { Construct } from 'constructs';
import { Duration, Stack } from '@aws-cdk/core';
import { Duration, Stack, SecretValue } from '@aws-cdk/core';
import * as secretsmanager from '@aws-cdk/aws-secretsmanager';
import * as kms from '@aws-cdk/aws-kms';
import * as iam from '@aws-cdk/aws-iam';
Expand All @@ -12,4 +12,4 @@ class Fixture extends Stack {

/// here
}
}
}
11 changes: 11 additions & 0 deletions packages/@aws-cdk/aws-secretsmanager/test/integ.secret.lit.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as iam from '@aws-cdk/aws-iam';
import * as cdk from '@aws-cdk/core';
import { SecretValue } from '@aws-cdk/core';
import * as secretsmanager from '../lib';

class SecretsManagerStack extends cdk.Stack {
Expand Down Expand Up @@ -37,10 +38,20 @@ class SecretsManagerStack extends cdk.Stack {
new secretsmanager.Secret(this, 'PredefinedSecret', {
secretStringValue: accessKey.secretAccessKey,
});

// JSON secret
new secretsmanager.Secret(this, 'JSONSecret', {
secretObjectValue: {
username: SecretValue.unsafePlainText(user.userName),
database: SecretValue.unsafePlainText('foo'),
password: accessKey.secretAccessKey,
},
});
/// !hide
}
}

const app = new cdk.App();
new SecretsManagerStack(app, 'Integ-SecretsManager-Secret');

app.synth();

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,32 @@
},
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete"
},
"JSONSecret6FE68AEF": {
"Type": "AWS::SecretsManager::Secret",
"Properties": {
"SecretString": {
"Fn::Join": [
"",
[
"{\"username\":\"",
{
"Ref": "User00B015A1"
},
"\",\"database\":\"foo\",\"password\":\"",
{
"Fn::GetAtt": [
"AccessKeyE6B25659",
"SecretAccessKey"
]
},
"\"}"
]
]
}
},
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"version":"17.0.0"}
{"version":"20.0.0"}
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
{
"version": "18.0.0",
"version": "20.0.0",
"testCases": {
"aws-secretsmanager/test/integ.secret.lit": {
"SecretTest/DefaultTest": {
"stacks": [
"Integ-SecretsManager-Secret"
],
"diffAssets": false,
"stackUpdateWorkflow": true
"assertionStack": "SecretTestDefaultTestDeployAssert519F6A06"
}
},
"synthContext": {},
"enableLookups": false
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "17.0.0",
"version": "20.0.0",
"artifacts": {
"Tree": {
"type": "cdk:tree",
Expand Down Expand Up @@ -62,9 +62,24 @@
"type": "aws:cdk:logicalId",
"data": "PredefinedSecret660AF4EC"
}
],
"/Integ-SecretsManager-Secret/JSONSecret/Resource": [
{
"type": "aws:cdk:logicalId",
"data": "JSONSecret6FE68AEF"
}
]
},
"displayName": "Integ-SecretsManager-Secret"
},
"SecretTestDefaultTestDeployAssert519F6A06": {
"type": "aws:cloudformation:stack",
"environment": "aws://unknown-account/unknown-region",
"properties": {
"templateFile": "SecretTestDefaultTestDeployAssert519F6A06.template.json",
"validateOnSynth": false
},
"displayName": "SecretTest/DefaultTest/DeployAssert"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"path": "Tree",
"constructInfo": {
"fqn": "constructs.Construct",
"version": "10.0.9"
"version": "10.1.33"
}
},
"Integ-SecretsManager-Secret": {
Expand Down Expand Up @@ -301,12 +301,90 @@
"fqn": "@aws-cdk/aws-secretsmanager.Secret",
"version": "0.0.0"
}
},
"JSONSecret": {
"id": "JSONSecret",
"path": "Integ-SecretsManager-Secret/JSONSecret",
"children": {
"Resource": {
"id": "Resource",
"path": "Integ-SecretsManager-Secret/JSONSecret/Resource",
"attributes": {
"aws:cdk:cloudformation:type": "AWS::SecretsManager::Secret",
"aws:cdk:cloudformation:props": {
"secretString": {
"Fn::Join": [
"",
[
"{\"username\":\"",
{
"Ref": "User00B015A1"
},
"\",\"database\":\"foo\",\"password\":\"",
{
"Fn::GetAtt": [
"AccessKeyE6B25659",
"SecretAccessKey"
]
},
"\"}"
]
]
}
}
},
"constructInfo": {
"fqn": "@aws-cdk/aws-secretsmanager.CfnSecret",
"version": "0.0.0"
}
}
},
"constructInfo": {
"fqn": "@aws-cdk/aws-secretsmanager.Secret",
"version": "0.0.0"
}
}
},
"constructInfo": {
"fqn": "@aws-cdk/core.Stack",
"version": "0.0.0"
}
},
"SecretTest": {
"id": "SecretTest",
"path": "SecretTest",
"children": {
"DefaultTest": {
"id": "DefaultTest",
"path": "SecretTest/DefaultTest",
"children": {
"Default": {
"id": "Default",
"path": "SecretTest/DefaultTest/Default",
"constructInfo": {
"fqn": "constructs.Construct",
"version": "10.1.33"
}
},
"DeployAssert": {
"id": "DeployAssert",
"path": "SecretTest/DefaultTest/DeployAssert",
"constructInfo": {
"fqn": "@aws-cdk/core.Stack",
"version": "0.0.0"
}
}
},
"constructInfo": {
"fqn": "@aws-cdk/integ-tests.IntegTestCase",
"version": "0.0.0"
}
}
},
"constructInfo": {
"fqn": "@aws-cdk/integ-tests.IntegTest",
"version": "0.0.0"
}
}
},
"constructInfo": {
Expand Down

0 comments on commit 5f0eff2

Please sign in to comment.