From 9b1d44c56bfc89bd6faa465e2f4b34c8a1d7964e Mon Sep 17 00:00:00 2001 From: Markus Date: Thu, 7 Apr 2022 21:53:48 +0200 Subject: [PATCH] feat: use asset hash as versionId --- API.md | 354 +++--------------- README.md | 5 +- go.mod | 9 - lambda/__snapshots__/handler_json_test.snap | 4 + lambda/__snapshots__/handler_yaml_test.snap | 6 + lambda/__snapshots__/main_test.snap | 3 +- lambda/main.go | 50 ++- lambda/main_test.go | 3 +- src/index.ts | 88 ++++- .../SecretIntegration.assets.json | 10 +- .../SecretIntegration.template.json | 92 ++--- test/secret.test.ts | 2 +- 12 files changed, 240 insertions(+), 386 deletions(-) delete mode 100644 go.mod diff --git a/API.md b/API.md index 26b391b5..db8456e2 100644 --- a/API.md +++ b/API.md @@ -4,6 +4,8 @@ ### SopsSecret +- *Implements:* @aws-cdk/aws-secretsmanager.ISecret + A drop in replacement for the normal Secret, that is populated with the encrypted content of the given sops file. #### Initializers @@ -45,12 +47,11 @@ new SopsSecret(scope: Construct, id: string, props: SopsSecretProps) | **Name** | **Description** | | --- | --- | | toString | Returns a string representation of this construct. | -| applyRemovalPolicy | Apply the given removal policy to this resource. | -| addReplicaRegion | Adds a replica region for the secret. | | addRotationSchedule | Adds a rotation schedule to the secret. | -| addTargetAttachment | Adds a target attachment to the secret. | | addToResourcePolicy | Adds a statement to the IAM resource policy associated with this secret. | +| applyRemovalPolicy | Apply the given removal policy to this resource. | | attach | Attach a target to this secret. | +| currentVersionId | Returns the current versionId that was created via the SopsSync. | | denyAccountRootDelete | Denies the `DeleteSecret` action to all principals within the current account. | | grantRead | Grants reading the secret value to some role. | | grantWrite | Grants writing and updating the secret value to some role. | @@ -66,52 +67,6 @@ public toString(): string Returns a string representation of this construct. -##### `applyRemovalPolicy` - -```typescript -public applyRemovalPolicy(policy: RemovalPolicy): void -``` - -Apply the given removal policy to this resource. - -The Removal Policy controls what happens to this resource when it stops -being managed by CloudFormation, either because you've removed it from the -CDK application or because you've made a change that requires the resource -to be replaced. - -The resource can be deleted (`RemovalPolicy.DESTROY`), or left in your AWS -account for data recovery and cleanup later (`RemovalPolicy.RETAIN`). - -###### `policy`Required - -- *Type:* @aws-cdk/core.RemovalPolicy - ---- - -##### `addReplicaRegion` - -```typescript -public addReplicaRegion(region: string, encryptionKey?: IKey): void -``` - -Adds a replica region for the secret. - -###### `region`Required - -- *Type:* string - -The name of the region. - ---- - -###### `encryptionKey`Optional - -- *Type:* @aws-cdk/aws-kms.IKey - -The customer-managed encryption key to use for encrypting the secret value. - ---- - ##### `addRotationSchedule` ```typescript @@ -132,41 +87,43 @@ Adds a rotation schedule to the secret. --- -##### ~~`addTargetAttachment`~~ +##### `addToResourcePolicy` ```typescript -public addTargetAttachment(id: string, options: AttachedSecretOptions): SecretTargetAttachment +public addToResourcePolicy(statement: PolicyStatement): AddToResourcePolicyResult ``` -Adds a target attachment to the secret. - -###### `id`Required - -- *Type:* string +Adds a statement to the IAM resource policy associated with this secret. ---- +If this secret was created in this stack, a resource policy will be +automatically created upon the first call to `addToResourcePolicy`. If +the secret is imported, then this is a no-op. -###### `options`Required +###### `statement`Required -- *Type:* @aws-cdk/aws-secretsmanager.AttachedSecretOptions +- *Type:* @aws-cdk/aws-iam.PolicyStatement --- -##### `addToResourcePolicy` +##### `applyRemovalPolicy` ```typescript -public addToResourcePolicy(statement: PolicyStatement): AddToResourcePolicyResult +public applyRemovalPolicy(policy: RemovalPolicy): void ``` -Adds a statement to the IAM resource policy associated with this secret. +Apply the given removal policy to this resource. -If this secret was created in this stack, a resource policy will be -automatically created upon the first call to `addToResourcePolicy`. If -the secret is imported, then this is a no-op. +The Removal Policy controls what happens to this resource when it stops +being managed by CloudFormation, either because you've removed it from the +CDK application or because you've made a change that requires the resource +to be replaced. -###### `statement`Required +The resource can be deleted (`RemovalPolicy.DESTROY`), or left in your AWS +account for data recovery and cleanup later (`RemovalPolicy.RETAIN`). -- *Type:* @aws-cdk/aws-iam.PolicyStatement +###### `policy`Required + +- *Type:* @aws-cdk/core.RemovalPolicy --- @@ -182,10 +139,16 @@ Attach a target to this secret. - *Type:* @aws-cdk/aws-secretsmanager.ISecretAttachmentTarget -The target to attach. - --- +##### `currentVersionId` + +```typescript +public currentVersionId(): string +``` + +Returns the current versionId that was created via the SopsSync. + ##### `denyAccountRootDelete` ```typescript @@ -247,13 +210,6 @@ Interpret the secret as a JSON object and return a field's value from it as a `S | **Name** | **Description** | | --- | --- | | isConstruct | Return whether the given object is a Construct. | -| isResource | Check whether the given construct is a Resource. | -| fromSecretArn | *No description.* | -| fromSecretAttributes | Import an existing secret into the Stack. | -| fromSecretCompleteArn | Imports a secret by complete ARN. | -| fromSecretName | Imports a secret by secret name; | -| fromSecretNameV2 | Imports a secret by secret name. | -| fromSecretPartialArn | Imports a secret by partial ARN. | --- @@ -273,217 +229,19 @@ Return whether the given object is a Construct. --- -##### `isResource` - -```typescript -import { SopsSecret } from 'cdk-sops-secrets' - -SopsSecret.isResource(construct: IConstruct) -``` - -Check whether the given construct is a Resource. - -###### `construct`Required - -- *Type:* @aws-cdk/core.IConstruct - ---- - -##### ~~`fromSecretArn`~~ - -```typescript -import { SopsSecret } from 'cdk-sops-secrets' - -SopsSecret.fromSecretArn(scope: Construct, id: string, secretArn: string) -``` - -###### `scope`Required - -- *Type:* constructs.Construct - ---- - -###### `id`Required - -- *Type:* string - ---- - -###### `secretArn`Required - -- *Type:* string - ---- - -##### `fromSecretAttributes` - -```typescript -import { SopsSecret } from 'cdk-sops-secrets' - -SopsSecret.fromSecretAttributes(scope: Construct, id: string, attrs: SecretAttributes) -``` - -Import an existing secret into the Stack. - -###### `scope`Required - -- *Type:* constructs.Construct - -the scope of the import. - ---- - -###### `id`Required - -- *Type:* string - -the ID of the imported Secret in the construct tree. - ---- - -###### `attrs`Required - -- *Type:* @aws-cdk/aws-secretsmanager.SecretAttributes - -the attributes of the imported secret. - ---- - -##### `fromSecretCompleteArn` - -```typescript -import { SopsSecret } from 'cdk-sops-secrets' - -SopsSecret.fromSecretCompleteArn(scope: Construct, id: string, secretCompleteArn: string) -``` - -Imports a secret by complete ARN. - -The complete ARN is the ARN with the Secrets Manager-supplied suffix. - -###### `scope`Required - -- *Type:* constructs.Construct - ---- - -###### `id`Required - -- *Type:* string - ---- - -###### `secretCompleteArn`Required - -- *Type:* string - ---- - -##### ~~`fromSecretName`~~ - -```typescript -import { SopsSecret } from 'cdk-sops-secrets' - -SopsSecret.fromSecretName(scope: Construct, id: string, secretName: string) -``` - -Imports a secret by secret name; - -the ARN of the Secret will be set to the secret name. -A secret with this name must exist in the same account & region. - -###### `scope`Required - -- *Type:* constructs.Construct - ---- - -###### `id`Required - -- *Type:* string - ---- - -###### `secretName`Required - -- *Type:* string - ---- - -##### `fromSecretNameV2` - -```typescript -import { SopsSecret } from 'cdk-sops-secrets' - -SopsSecret.fromSecretNameV2(scope: Construct, id: string, secretName: string) -``` - -Imports a secret by secret name. - -A secret with this name must exist in the same account & region. -Replaces the deprecated `fromSecretName`. - -###### `scope`Required - -- *Type:* constructs.Construct - ---- - -###### `id`Required - -- *Type:* string - ---- - -###### `secretName`Required - -- *Type:* string - ---- - -##### `fromSecretPartialArn` - -```typescript -import { SopsSecret } from 'cdk-sops-secrets' - -SopsSecret.fromSecretPartialArn(scope: Construct, id: string, secretPartialArn: string) -``` - -Imports a secret by partial ARN. - -The partial ARN is the ARN without the Secrets Manager-supplied suffix. - -###### `scope`Required - -- *Type:* constructs.Construct - ---- - -###### `id`Required - -- *Type:* string - ---- - -###### `secretPartialArn`Required - -- *Type:* string - ---- - #### Properties | **Name** | **Type** | **Description** | | --- | --- | --- | | node | @aws-cdk/core.ConstructNode | The construct tree node associated with this construct. | | env | @aws-cdk/core.ResourceEnvironment | The environment this resource belongs to. | -| stack | @aws-cdk/core.Stack | The stack in which this resource is defined. | | secretArn | string | The ARN of the secret in AWS Secrets Manager. | | secretName | string | The name of the secret. | | secretValue | @aws-cdk/core.SecretValue | Retrieve the value of the stored secret as a `SecretValue`. | +| stack | @aws-cdk/core.Stack | The stack in which this resource is defined. | +| sync | SopsSync | *No description.* | | encryptionKey | @aws-cdk/aws-kms.IKey | The customer-managed encryption key that is used to encrypt this secret, if any. | | secretFullArn | string | The full ARN of the secret in AWS Secrets Manager, which is the ARN including the Secrets Manager-supplied 6-character suffix. | -| sync | SopsSync | *No description.* | --- @@ -518,18 +276,6 @@ that might be different than the stack they were imported into. --- -##### `stack`Required - -```typescript -public readonly stack: Stack; -``` - -- *Type:* @aws-cdk/core.Stack - -The stack in which this resource is defined. - ---- - ##### `secretArn`Required ```typescript @@ -572,6 +318,28 @@ Retrieve the value of the stored secret as a `SecretValue`. --- +##### `stack`Required + +```typescript +public readonly stack: Stack; +``` + +- *Type:* @aws-cdk/core.Stack + +The stack in which this resource is defined. + +--- + +##### `sync`Required + +```typescript +public readonly sync: SopsSync; +``` + +- *Type:* SopsSync + +--- + ##### `encryptionKey`Optional ```typescript @@ -601,16 +369,6 @@ This is equal to `secretArn` in most cases, but is undefined when a full ARN is --- -##### `sync`Required - -```typescript -public readonly sync: SopsSync; -``` - -- *Type:* SopsSync - ---- - ### SopsSync diff --git a/README.md b/README.md index 21da114c..4a1f7e0c 100644 --- a/README.md +++ b/README.md @@ -43,4 +43,7 @@ Other than that, or perhaps more importantly, my goal was to learn new things: # Other Tools like this -* [sops-secretsmanager-cdk](https://github.com/isotoma/sops-secretsmanager-cdk): Does nearly the same (really, found it after this was already done). Less options than this construct. \ No newline at end of file +The problem this Construct addresses is so good, already two other implementations exist: + +* [isotoma/sops-secretsmanager-cdk](https://github.com/isotoma/sops-secretsmanager-cdk): Does nearly the same. Uses CustomResource, wraps the sops cli, does not support flatten. Found it after I published my solution to npm :-/ +* [taimos/secretsmanager-versioning](https://github.com/taimos/secretsmanager-versioning): Different approach on the same problem. This is a cli tool with very nice integration into cdk and also handles git versioning information. \ No newline at end of file diff --git a/go.mod b/go.mod deleted file mode 100644 index f311056e..00000000 --- a/go.mod +++ /dev/null @@ -1,9 +0,0 @@ -module github.com/markussiebert/cdk-sops-secrets - -go 1.18 - -require ( - cdk-sops-secrets/lambda v0.0.0 -) - -replace cdk-sops-secrets/lambda => ./lambda \ No newline at end of file diff --git a/lambda/__snapshots__/handler_json_test.snap b/lambda/__snapshots__/handler_json_test.snap index e603c816..d5bd822c 100755 --- a/lambda/__snapshots__/handler_json_test.snap +++ b/lambda/__snapshots__/handler_json_test.snap @@ -10,6 +10,7 @@ [Test_FullWorkflow_Create_S3_JSON_Simple - 2] >>>SecretsManagerMockClient.PutSecretValue.Input { + ClientRequestToken: "", SecretId: "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", SecretString: "{\n \"key1\": \"value1\",\n \"key2\": 12345,\n \"key3\": false\n}" } @@ -40,6 +41,7 @@ nil [Test_FullWorkflow_Create_S3_JSON_Complex - 2] >>>SecretsManagerMockClient.PutSecretValue.Input { + ClientRequestToken: "", SecretId: "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", SecretString: "{\n \"and now\": {\n \"some\": [\n {\n \"basic\": false\n },\n {\n \"nested\": 12345\n },\n {\n \"type\": 1.2345\n },\n {\n \"tests\": \"Finish!\"\n }\n ]\n },\n \"some\": {\n \"deep\": {\n \"nested\": {\n \"arrays\": [\n \"with\",\n \"several\",\n {\n \"values\": {\n \"and\": \"objects\"\n }\n }\n ],\n \"object\": \"structure\"\n }\n },\n \"notsodeep\": \"struct\"\n }\n}" } @@ -70,6 +72,7 @@ nil [Test_FullWorkflow_Create_S3_JSON_Complex_StringifyValues - 2] >>>SecretsManagerMockClient.PutSecretValue.Input { + ClientRequestToken: "", SecretId: "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", SecretString: "{\n \"and now\": {\n \"some\": [\n {\n \"basic\": \"false\"\n },\n {\n \"nested\": \"12345\"\n },\n {\n \"type\": \"1.2345\"\n },\n {\n \"tests\": \"Finish!\"\n }\n ]\n },\n \"some\": {\n \"deep\": {\n \"nested\": {\n \"arrays\": [\n \"with\",\n \"several\",\n {\n \"values\": {\n \"and\": \"objects\"\n }\n }\n ],\n \"object\": \"structure\"\n }\n },\n \"notsodeep\": \"struct\"\n }\n}" } @@ -100,6 +103,7 @@ nil [Test_FullWorkflow_Create_S3_JSON_Complex_Flat - 2] >>>SecretsManagerMockClient.PutSecretValue.Input { + ClientRequestToken: "", SecretId: "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", SecretString: "{\n \"and now.some[0].basic\": false,\n \"and now.some[1].nested\": 12345,\n \"and now.some[2].type\": 1.2345,\n \"and now.some[3].tests\": \"Finish!\",\n \"some.deep.nested.arrays[0]\": \"with\",\n \"some.deep.nested.arrays[1]\": \"several\",\n \"some.deep.nested.arrays[2].values.and\": \"objects\",\n \"some.deep.nested.object\": \"structure\",\n \"some.notsodeep\": \"struct\"\n}" } diff --git a/lambda/__snapshots__/handler_yaml_test.snap b/lambda/__snapshots__/handler_yaml_test.snap index 97d3f15c..5f46a6ca 100755 --- a/lambda/__snapshots__/handler_yaml_test.snap +++ b/lambda/__snapshots__/handler_yaml_test.snap @@ -10,6 +10,7 @@ [Test_FullWorkflow_Create_S3_YAML_Simple - 2] >>>SecretsManagerMockClient.PutSecretValue.Input { + ClientRequestToken: "", SecretId: "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", SecretString: "key1: value1\nkey2: 12345\nkey3: false\n" } @@ -40,6 +41,7 @@ nil [Test_FullWorkflow_Create_S3_YAML_as_JSON_Simple - 2] >>>SecretsManagerMockClient.PutSecretValue.Input { + ClientRequestToken: "", SecretId: "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", SecretString: "{\n \"key1\": \"value1\",\n \"key2\": 12345,\n \"key3\": false\n}" } @@ -70,6 +72,7 @@ nil [Test_FullWorkflow_Create_S3_YAML_Complex - 2] >>>SecretsManagerMockClient.PutSecretValue.Input { + ClientRequestToken: "", SecretId: "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", SecretString: "and now:\n some:\n - basic: false\n - nested: 12345\n - type: 1.2345\n - tests: Finish!\nsome:\n deep:\n nested:\n arrays:\n - with\n - several\n - values:\n and: objects\n object: structure\n notsodeep: struct\n" } @@ -100,6 +103,7 @@ nil [Test_FullWorkflow_Create_S3_YAML_as_JSON_Complex - 2] >>>SecretsManagerMockClient.PutSecretValue.Input { + ClientRequestToken: "", SecretId: "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", SecretString: "{\n \"and now\": {\n \"some\": [\n {\n \"basic\": false\n },\n {\n \"nested\": 12345\n },\n {\n \"type\": 1.2345\n },\n {\n \"tests\": \"Finish!\"\n }\n ]\n },\n \"some\": {\n \"deep\": {\n \"nested\": {\n \"arrays\": [\n \"with\",\n \"several\",\n {\n \"values\": {\n \"and\": \"objects\"\n }\n }\n ],\n \"object\": \"structure\"\n }\n },\n \"notsodeep\": \"struct\"\n }\n}" } @@ -130,6 +134,7 @@ nil [Test_FullWorkflow_Create_S3_YAML_Complex_Flat - 2] >>>SecretsManagerMockClient.PutSecretValue.Input { + ClientRequestToken: "", SecretId: "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", SecretString: "and now.some[0].basic: false\nand now.some[1].nested: 12345\nand now.some[2].type: 1.2345\nand now.some[3].tests: Finish!\nsome.deep.nested.arrays[0]: with\nsome.deep.nested.arrays[1]: several\nsome.deep.nested.arrays[2].values.and: objects\nsome.deep.nested.object: structure\nsome.notsodeep: struct\n" } @@ -160,6 +165,7 @@ nil [Test_FullWorkflow_Create_S3_YAML_as_JSON_Complex_Flat - 2] >>>SecretsManagerMockClient.PutSecretValue.Input { + ClientRequestToken: "", SecretId: "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", SecretString: "{\n \"and now.some[0].basic\": false,\n \"and now.some[1].nested\": 12345,\n \"and now.some[2].type\": 1.2345,\n \"and now.some[3].tests\": \"Finish!\",\n \"some.deep.nested.arrays[0]\": \"with\",\n \"some.deep.nested.arrays[1]\": \"several\",\n \"some.deep.nested.arrays[2].values.and\": \"objects\",\n \"some.deep.nested.object\": \"structure\",\n \"some.notsodeep\": \"struct\"\n}" } diff --git a/lambda/__snapshots__/main_test.snap b/lambda/__snapshots__/main_test.snap index 2512641d..c13b4296 100755 --- a/lambda/__snapshots__/main_test.snap +++ b/lambda/__snapshots__/main_test.snap @@ -35,7 +35,8 @@ [Test_UpdateSecret - 1] >>>SecretsManagerMockClient.PutSecretValue.Input { - SecretId: "arn:${Partition}:secretsmanager:${Region}:${Account}:secret:${SecretId}", + ClientRequestToken: "4547532a137611d83958d17095c6c2d38ae0036a760c3b79c9dd5957d1c20cf2", + SecretId: "arn::secretsmanager:::secret:", SecretString: "some-secret-data" } --- diff --git a/lambda/main.go b/lambda/main.go index d77cdc02..856d7664 100644 --- a/lambda/main.go +++ b/lambda/main.go @@ -8,6 +8,7 @@ import ( "log" "regexp" "strconv" + "strings" runtime "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go/aws" @@ -66,23 +67,29 @@ func decryptSopsFileContent(content []byte, format string) (data []byte, err err return resp, nil } -func (a AWS) updateSecret(secretArn string, secretContent []byte) (data *secretsmanager.PutSecretValueOutput, err error) { +func (a AWS) updateSecret(sopsFileName string, secretArn string, secretContent []byte) (data *secretsmanager.PutSecretValueOutput, err error) { secretContentString := string(secretContent) + clientRequestToken := strings.Split(sopsFileName, ".")[0] input := &secretsmanager.PutSecretValueInput{ - SecretId: &secretArn, - SecretString: &secretContentString, + SecretId: &secretArn, + SecretString: &secretContentString, + ClientRequestToken: &clientRequestToken, } secretResp, secretErr := a.secretsmanager.PutSecretValue(input) if secretErr != nil { - return nil, errors.New(fmt.Sprintf("Failed to store secret value:\n%v\n", secretErr)) + return nil, errors.New(fmt.Sprintf("Failed to store secret value:\nsecretArn: %s\nClientRequestToken: %s\n%v\n", secretArn, clientRequestToken, err)) } - re := regexp.MustCompile(`(^arn:.*:secretsmanager:)(.*)`) - arn := re.ReplaceAllString(*secretResp.ARN, `arn:custom:sopssync:$2`) + arn := generatePhysicalResourceId(*secretResp.ARN) secretResp.ARN = &arn log.Printf("Succesfully stored secret:\n%v\n", secretResp) return secretResp, nil } +func generatePhysicalResourceId(input string) string { + re := regexp.MustCompile(`(^arn:.*:secretsmanager:)(.*)`) + return re.ReplaceAllString(input, `arn:custom:sopssync:$2`) +} + func (a AWS) syncSopsToSecretsmanager(ctx context.Context, event cfn.Event) (physicalResourceID string, data map[string]interface{}, err error) { // event // eventJson, _ := json.MarshalIndent(event, "", " ") @@ -94,7 +101,7 @@ func (a AWS) syncSopsToSecretsmanager(ctx context.Context, event cfn.Event) (phy jsonResourceProps, err := json.Marshal(event.ResourceProperties) if err != nil { - return "", nil, err + return "error", nil, err } resourceProperties := SopsSyncResourcePropertys{} @@ -102,16 +109,18 @@ func (a AWS) syncSopsToSecretsmanager(ctx context.Context, event cfn.Event) (phy return "", nil, err } + tempArn := generatePhysicalResourceId(resourceProperties.SecretARN) + sopsFile := resourceProperties.SopsS3File // This is where the magic happens ecnryptedContent, err := a.getS3FileContent(sopsFile) if err != nil { - return "", nil, err + return tempArn, nil, err } decryptedContent, err := decryptSopsFileContent(ecnryptedContent, resourceProperties.Format) if err != nil { - return "", nil, err + return tempArn, nil, err } //log.Println(string(decryptedContent)) var decryptedInterface interface{} @@ -120,14 +129,14 @@ func (a AWS) syncSopsToSecretsmanager(ctx context.Context, event cfn.Event) (phy { err := json.Unmarshal(decryptedContent, &decryptedInterface) if err != nil { - return "", nil, err + return tempArn, nil, err } } case "yaml": { err := yaml.Unmarshal(decryptedContent, &decryptedInterface) if err != nil { - return "", nil, err + return tempArn, nil, err } } default: @@ -139,7 +148,7 @@ func (a AWS) syncSopsToSecretsmanager(ctx context.Context, event cfn.Event) (phy } resourcePropertiesFlatten, err := strconv.ParseBool(resourceProperties.Flatten) if err != nil { - return "", nil, err + return tempArn, nil, err } var finalInterface interface{} @@ -148,7 +157,7 @@ func (a AWS) syncSopsToSecretsmanager(ctx context.Context, event cfn.Event) (phy flattenedInterface := make(map[string]interface{}) err := flatten("", decryptedInterface, flattenedInterface) if err != nil { - return "", nil, err + return tempArn, nil, err } finalInterface = flattenedInterface } else { @@ -160,12 +169,12 @@ func (a AWS) syncSopsToSecretsmanager(ctx context.Context, event cfn.Event) (phy } resourcePropertiesStringifyValues, err := strconv.ParseBool(resourceProperties.StringifyValues) if err != nil { - return "", nil, err + return tempArn, nil, err } if resourcePropertiesStringifyValues { finalInterface, _, err = stringifyValues(finalInterface) if err != nil { - return "", nil, err + return tempArn, nil, err } } @@ -174,23 +183,23 @@ func (a AWS) syncSopsToSecretsmanager(ctx context.Context, event cfn.Event) (phy } resourcePropertieConvertToJSON, err := strconv.ParseBool(resourceProperties.ConvertToJSON) if err != nil { - return "", nil, err + return tempArn, nil, err } if resourcePropertieConvertToJSON || resourceProperties.Format == "json" { decryptedContent, err = toJSON(finalInterface) if err != nil { - return "", nil, err + return tempArn, nil, err } } else if resourceProperties.Format == "yaml" { decryptedContent, err = toYAML(finalInterface) if err != nil { - return "", nil, err + return tempArn, nil, err } } // Write the secret - updateSecretResp, err := a.updateSecret(resourceProperties.SecretARN, decryptedContent) + updateSecretResp, err := a.updateSecret(sopsFile.Key, resourceProperties.SecretARN, decryptedContent) if err != nil { - return "", nil, err + return tempArn, nil, err } returnData := make(map[string]interface{}) @@ -204,6 +213,7 @@ func (a AWS) syncSopsToSecretsmanager(ctx context.Context, event cfn.Event) (phy } else if event.RequestType == cfn.RequestDelete { return "", nil, nil } else { + // Should never happen ... return "", nil, errors.New(fmt.Sprintf("RequestType '%s' not supported", event.RequestType)) } } diff --git a/lambda/main_test.go b/lambda/main_test.go index 4ab6bc15..d1f96b44 100644 --- a/lambda/main_test.go +++ b/lambda/main_test.go @@ -48,10 +48,11 @@ func Test_UpdateSecret(t *testing.T) { t: t, }, } + fileName := "4547532a137611d83958d17095c6c2d38ae0036a760c3b79c9dd5957d1c20cf2.yaml" inputArn := "arn:${Partition}:secretsmanager:${Region}:${Account}:secret:${SecretId}" secretValue := []byte("some-secret-data") - response, err := mocks.updateSecret(inputArn, secretValue) + response, err := mocks.updateSecret(fileName, inputArn, secretValue) check(err) snaps.MatchSnapshot(t, response) diff --git a/src/index.ts b/src/index.ts index b73b1b7a..85eead42 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,15 +1,31 @@ import * as fs from 'fs'; import * as path from 'path'; +import { + IGrantable, + Grant, + PolicyStatement, + AddToResourcePolicyResult, +} from '@aws-cdk/aws-iam'; import { IKey, Key } from '@aws-cdk/aws-kms'; import { Code, Runtime, SingletonFunction } from '@aws-cdk/aws-lambda'; import { Asset } from '@aws-cdk/aws-s3-assets'; -import { ISecret, Secret, SecretProps } from '@aws-cdk/aws-secretsmanager'; +import { + ISecret, + ISecretAttachmentTarget, + RotationSchedule, + RotationScheduleOptions, + Secret, + SecretProps, +} from '@aws-cdk/aws-secretsmanager'; import { Annotations, Construct, CustomResource, Lazy, + RemovalPolicy, + ResourceEnvironment, SecretValue, + Stack, } from '@aws-cdk/core'; /** @@ -230,20 +246,84 @@ export interface SopsSecretProps extends SecretProps, SopsSyncOptions {} * A drop in replacement for the normal Secret, that is populated with the encrypted * content of the given sops file. */ -export class SopsSecret extends Secret { +export class SopsSecret extends Construct implements ISecret { + private readonly secret: Secret; + readonly encryptionKey?: IKey | undefined; + readonly secretArn: string; + readonly secretFullArn?: string | undefined; + readonly secretName: string; + readonly stack: Stack; + readonly env: ResourceEnvironment; + readonly sync: SopsSync; public constructor(scope: Construct, id: string, props: SopsSecretProps) { - super(scope, id, props as SecretProps); + super(scope, id); + this.secret = new Secret(this, 'Resource', props as SecretProps); + + // Fullfill secret Interface + this.encryptionKey = this.secret.encryptionKey; + this.secretArn = this.secret.secretArn; + this.secretName = this.secret.secretName; + this.stack = Stack.of(scope); + this.env = { + account: this.stack.account, + region: this.stack.region, + }; + this.sync = new SopsSync(this, 'SopsSync', { - secret: this, + secret: this.secret, ...(props as SopsSyncOptions), }); } + /** + * Returns the current versionId that was created via the SopsSync + */ + public currentVersionId(): string { + return this.sync.versionId; + } + + public grantRead(grantee: IGrantable, versionStages?: string[]): Grant { + return this.secret.grantRead(grantee, versionStages); + } + public grantWrite(grantee: IGrantable): Grant { + return this.secret.grantWrite(grantee); + } + public addRotationSchedule( + id: string, + options: RotationScheduleOptions, + ): RotationSchedule { + throw new Error( + `Method not allowed as this secret is managed by SopsSync!\nid: ${id}\noptions: ${JSON.stringify( + options, + null, + 2, + )}`, + ); + } + public addToResourcePolicy( + statement: PolicyStatement, + ): AddToResourcePolicyResult { + return this.secret.addToResourcePolicy(statement); + } + public denyAccountRootDelete(): void { + return this.secret.denyAccountRootDelete(); + } + public attach(target: ISecretAttachmentTarget): ISecret { + return this.secret.attach(target); + } + public applyRemovalPolicy(policy: RemovalPolicy): void { + return this.secret.applyRemovalPolicy(policy); + } + public secretValueFromJson(jsonField: string) { return SecretValue.secretsManager(this.secretArn, { jsonField, versionId: this.sync.versionId, }); } + + public get secretValue(): SecretValue { + return this.secretValueFromJson(''); + } } diff --git a/test/secret.integ.snapshot/SecretIntegration.assets.json b/test/secret.integ.snapshot/SecretIntegration.assets.json index b6394111..8a6c67db 100644 --- a/test/secret.integ.snapshot/SecretIntegration.assets.json +++ b/test/secret.integ.snapshot/SecretIntegration.assets.json @@ -1,15 +1,15 @@ { "version": "16.0.0", "files": { - "934edcf6115b2213914d0b909d6a57615b77497bc80e31d20eff033244b10a83": { + "8bcfb9be629c18f572586234ab20394a37fd422dc1fb7657730fadbbdf69bc36": { "source": { - "path": "asset.934edcf6115b2213914d0b909d6a57615b77497bc80e31d20eff033244b10a83.zip", + "path": "asset.8bcfb9be629c18f572586234ab20394a37fd422dc1fb7657730fadbbdf69bc36.zip", "packaging": "file" }, "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "934edcf6115b2213914d0b909d6a57615b77497bc80e31d20eff033244b10a83.zip", + "objectKey": "8bcfb9be629c18f572586234ab20394a37fd422dc1fb7657730fadbbdf69bc36.zip", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } @@ -66,7 +66,7 @@ } } }, - "e2df3958ec05e41000a474e751c6beafa915cb92a2b6d699f4ceefdde98d611d": { + "06aa81c8f1b4bd04a11046c7287961ffe7a7a9372052cf5d65223b1e90e04266": { "source": { "path": "SecretIntegration.template.json", "packaging": "file" @@ -74,7 +74,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "e2df3958ec05e41000a474e751c6beafa915cb92a2b6d699f4ceefdde98d611d.json", + "objectKey": "06aa81c8f1b4bd04a11046c7287961ffe7a7a9372052cf5d65223b1e90e04266.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/test/secret.integ.snapshot/SecretIntegration.template.json b/test/secret.integ.snapshot/SecretIntegration.template.json index 8b5f20c2..47d9f683 100644 --- a/test/secret.integ.snapshot/SecretIntegration.template.json +++ b/test/secret.integ.snapshot/SecretIntegration.template.json @@ -1,6 +1,6 @@ { "Resources": { - "SopsSecretJSON9B18C923": { + "SopsSecretJSON72040543": { "Type": "AWS::SecretsManager::Secret", "Properties": { "GenerateSecretString": {} @@ -18,7 +18,7 @@ ] }, "SecretARN": { - "Ref": "SopsSecretJSON9B18C923" + "Ref": "SopsSecretJSON72040543" }, "SopsS3File": { "Bucket": { @@ -77,7 +77,7 @@ ], "Effect": "Allow", "Resource": { - "Ref": "SopsSecretJSON9B18C923" + "Ref": "SopsSecretJSON72040543" } }, { @@ -128,7 +128,7 @@ ], "Effect": "Allow", "Resource": { - "Ref": "SopsSecretYAML311046EA" + "Ref": "SopsSecretYAMLC392F558" } }, { @@ -138,7 +138,7 @@ ], "Effect": "Allow", "Resource": { - "Ref": "SopsSecretYAMLasJSONE90622C5" + "Ref": "SopsSecretYAMLasJSON64419C04" } }, { @@ -148,7 +148,7 @@ ], "Effect": "Allow", "Resource": { - "Ref": "SopsComplexSecretJSONFA37D522" + "Ref": "SopsComplexSecretJSONAD4C2662" } }, { @@ -158,7 +158,7 @@ ], "Effect": "Allow", "Resource": { - "Ref": "SopsComplexSecretJSONFlat34CFF1D0" + "Ref": "SopsComplexSecretJSONFlatF5FC1D69" } }, { @@ -168,7 +168,7 @@ ], "Effect": "Allow", "Resource": { - "Ref": "SopComplexSecretYAMLBAE4AFBC" + "Ref": "SopComplexSecretYAMLF52D88F2" } }, { @@ -178,7 +178,7 @@ ], "Effect": "Allow", "Resource": { - "Ref": "SopComplexSecretYAMLFlatC969B78F" + "Ref": "SopComplexSecretYAMLFlatD9CE8782" } }, { @@ -188,7 +188,7 @@ ], "Effect": "Allow", "Resource": { - "Ref": "SopsComplexSecretYAMLasJSON9DFE8B13" + "Ref": "SopsComplexSecretYAMLasJSONEAE81DB0" } }, { @@ -198,7 +198,7 @@ ], "Effect": "Allow", "Resource": { - "Ref": "SopsComplexSecretYAMLasJSONFlatAE095DDA" + "Ref": "SopsComplexSecretYAMLasJSONFlat9FD04B78" } } ], @@ -219,7 +219,7 @@ "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": "934edcf6115b2213914d0b909d6a57615b77497bc80e31d20eff033244b10a83.zip" + "S3Key": "8bcfb9be629c18f572586234ab20394a37fd422dc1fb7657730fadbbdf69bc36.zip" }, "Role": { "Fn::GetAtt": [ @@ -240,7 +240,7 @@ "SingletonLambdaSopsSyncProviderServiceRoleC45BBD25" ] }, - "SopsSecretYAML311046EA": { + "SopsSecretYAMLC392F558": { "Type": "AWS::SecretsManager::Secret", "Properties": { "GenerateSecretString": {} @@ -258,7 +258,7 @@ ] }, "SecretARN": { - "Ref": "SopsSecretYAML311046EA" + "Ref": "SopsSecretYAMLC392F558" }, "SopsS3File": { "Bucket": { @@ -274,7 +274,7 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, - "SopsSecretYAMLasJSONE90622C5": { + "SopsSecretYAMLasJSON64419C04": { "Type": "AWS::SecretsManager::Secret", "Properties": { "GenerateSecretString": {} @@ -292,7 +292,7 @@ ] }, "SecretARN": { - "Ref": "SopsSecretYAMLasJSONE90622C5" + "Ref": "SopsSecretYAMLasJSON64419C04" }, "SopsS3File": { "Bucket": { @@ -308,7 +308,7 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, - "SopsComplexSecretJSONFA37D522": { + "SopsComplexSecretJSONAD4C2662": { "Type": "AWS::SecretsManager::Secret", "Properties": { "GenerateSecretString": {} @@ -326,7 +326,7 @@ ] }, "SecretARN": { - "Ref": "SopsComplexSecretJSONFA37D522" + "Ref": "SopsComplexSecretJSONAD4C2662" }, "SopsS3File": { "Bucket": { @@ -342,7 +342,7 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, - "SopsComplexSecretJSONFlat34CFF1D0": { + "SopsComplexSecretJSONFlatF5FC1D69": { "Type": "AWS::SecretsManager::Secret", "Properties": { "GenerateSecretString": {} @@ -360,7 +360,7 @@ ] }, "SecretARN": { - "Ref": "SopsComplexSecretJSONFlat34CFF1D0" + "Ref": "SopsComplexSecretJSONFlatF5FC1D69" }, "SopsS3File": { "Bucket": { @@ -376,7 +376,7 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, - "SopComplexSecretYAMLBAE4AFBC": { + "SopComplexSecretYAMLF52D88F2": { "Type": "AWS::SecretsManager::Secret", "Properties": { "GenerateSecretString": {} @@ -394,7 +394,7 @@ ] }, "SecretARN": { - "Ref": "SopComplexSecretYAMLBAE4AFBC" + "Ref": "SopComplexSecretYAMLF52D88F2" }, "SopsS3File": { "Bucket": { @@ -410,7 +410,7 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, - "SopComplexSecretYAMLFlatC969B78F": { + "SopComplexSecretYAMLFlatD9CE8782": { "Type": "AWS::SecretsManager::Secret", "Properties": { "GenerateSecretString": {} @@ -428,7 +428,7 @@ ] }, "SecretARN": { - "Ref": "SopComplexSecretYAMLFlatC969B78F" + "Ref": "SopComplexSecretYAMLFlatD9CE8782" }, "SopsS3File": { "Bucket": { @@ -444,7 +444,7 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, - "SopsComplexSecretYAMLasJSON9DFE8B13": { + "SopsComplexSecretYAMLasJSONEAE81DB0": { "Type": "AWS::SecretsManager::Secret", "Properties": { "GenerateSecretString": {} @@ -462,7 +462,7 @@ ] }, "SecretARN": { - "Ref": "SopsComplexSecretYAMLasJSON9DFE8B13" + "Ref": "SopsComplexSecretYAMLasJSONEAE81DB0" }, "SopsS3File": { "Bucket": { @@ -478,7 +478,7 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, - "SopsComplexSecretYAMLasJSONFlatAE095DDA": { + "SopsComplexSecretYAMLasJSONFlat9FD04B78": { "Type": "AWS::SecretsManager::Secret", "Properties": { "GenerateSecretString": {} @@ -496,7 +496,7 @@ ] }, "SecretARN": { - "Ref": "SopsComplexSecretYAMLasJSONFlatAE095DDA" + "Ref": "SopsComplexSecretYAMLasJSONFlat9FD04B78" }, "SopsS3File": { "Bucket": { @@ -563,7 +563,7 @@ [ "{{resolve:secretsmanager:", { - "Ref": "SopsComplexSecretJSONFlat34CFF1D0" + "Ref": "SopsComplexSecretJSONFlatF5FC1D69" }, ":SecretString:and now.some[0].basic::", { @@ -582,7 +582,7 @@ [ "{{resolve:secretsmanager:", { - "Ref": "SopsComplexSecretJSONFlat34CFF1D0" + "Ref": "SopsComplexSecretJSONFlatF5FC1D69" }, ":SecretString:and now.some[1].nested::", { @@ -601,7 +601,7 @@ [ "{{resolve:secretsmanager:", { - "Ref": "SopsComplexSecretJSONFlat34CFF1D0" + "Ref": "SopsComplexSecretJSONFlatF5FC1D69" }, ":SecretString:and now.some[2].type::", { @@ -620,7 +620,7 @@ [ "{{resolve:secretsmanager:", { - "Ref": "SopsComplexSecretJSONFlat34CFF1D0" + "Ref": "SopsComplexSecretJSONFlatF5FC1D69" }, ":SecretString:and now.some[3].tests::", { @@ -639,7 +639,7 @@ [ "{{resolve:secretsmanager:", { - "Ref": "SopsComplexSecretJSONFlat34CFF1D0" + "Ref": "SopsComplexSecretJSONFlatF5FC1D69" }, ":SecretString:some.deep.nested.arrays[0]::", { @@ -658,7 +658,7 @@ [ "{{resolve:secretsmanager:", { - "Ref": "SopsComplexSecretJSONFlat34CFF1D0" + "Ref": "SopsComplexSecretJSONFlatF5FC1D69" }, ":SecretString:some.deep.nested.arrays[1]::", { @@ -677,7 +677,7 @@ [ "{{resolve:secretsmanager:", { - "Ref": "SopsComplexSecretJSONFlat34CFF1D0" + "Ref": "SopsComplexSecretJSONFlatF5FC1D69" }, ":SecretString:some.deep.nested.arrays[2].values.and::", { @@ -696,7 +696,7 @@ [ "{{resolve:secretsmanager:", { - "Ref": "SopsComplexSecretJSONFlat34CFF1D0" + "Ref": "SopsComplexSecretJSONFlatF5FC1D69" }, ":SecretString:some.deep.nested.object::", { @@ -715,7 +715,7 @@ [ "{{resolve:secretsmanager:", { - "Ref": "SopsComplexSecretJSONFlat34CFF1D0" + "Ref": "SopsComplexSecretJSONFlatF5FC1D69" }, ":SecretString:some.notsodeep::", { @@ -734,7 +734,7 @@ [ "{{resolve:secretsmanager:", { - "Ref": "SopsComplexSecretYAMLasJSONFlatAE095DDA" + "Ref": "SopsComplexSecretYAMLasJSONFlat9FD04B78" }, ":SecretString:and now.some[0].basic::", { @@ -753,7 +753,7 @@ [ "{{resolve:secretsmanager:", { - "Ref": "SopsComplexSecretYAMLasJSONFlatAE095DDA" + "Ref": "SopsComplexSecretYAMLasJSONFlat9FD04B78" }, ":SecretString:and now.some[1].nested::", { @@ -772,7 +772,7 @@ [ "{{resolve:secretsmanager:", { - "Ref": "SopsComplexSecretYAMLasJSONFlatAE095DDA" + "Ref": "SopsComplexSecretYAMLasJSONFlat9FD04B78" }, ":SecretString:and now.some[2].type::", { @@ -791,7 +791,7 @@ [ "{{resolve:secretsmanager:", { - "Ref": "SopsComplexSecretYAMLasJSONFlatAE095DDA" + "Ref": "SopsComplexSecretYAMLasJSONFlat9FD04B78" }, ":SecretString:and now.some[3].tests::", { @@ -810,7 +810,7 @@ [ "{{resolve:secretsmanager:", { - "Ref": "SopsComplexSecretYAMLasJSONFlatAE095DDA" + "Ref": "SopsComplexSecretYAMLasJSONFlat9FD04B78" }, ":SecretString:some.deep.nested.arrays[0]::", { @@ -829,7 +829,7 @@ [ "{{resolve:secretsmanager:", { - "Ref": "SopsComplexSecretYAMLasJSONFlatAE095DDA" + "Ref": "SopsComplexSecretYAMLasJSONFlat9FD04B78" }, ":SecretString:some.deep.nested.arrays[1]::", { @@ -848,7 +848,7 @@ [ "{{resolve:secretsmanager:", { - "Ref": "SopsComplexSecretYAMLasJSONFlatAE095DDA" + "Ref": "SopsComplexSecretYAMLasJSONFlat9FD04B78" }, ":SecretString:some.deep.nested.arrays[2].values.and::", { @@ -867,7 +867,7 @@ [ "{{resolve:secretsmanager:", { - "Ref": "SopsComplexSecretYAMLasJSONFlatAE095DDA" + "Ref": "SopsComplexSecretYAMLasJSONFlat9FD04B78" }, ":SecretString:some.deep.nested.object::", { @@ -886,7 +886,7 @@ [ "{{resolve:secretsmanager:", { - "Ref": "SopsComplexSecretYAMLasJSONFlatAE095DDA" + "Ref": "SopsComplexSecretYAMLasJSONFlat9FD04B78" }, ":SecretString:some.notsodeep::", { diff --git a/test/secret.test.ts b/test/secret.test.ts index 3eeff96d..8278ab73 100644 --- a/test/secret.test.ts +++ b/test/secret.test.ts @@ -190,7 +190,7 @@ test('secretValueFromJson(...)', () => { [ '{{resolve:secretsmanager:', { - Ref: 'SopsSecretBBFD4AF3', + Ref: 'SopsSecretF929FB43', }, ':SecretString:test::', {