diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 00000000..48362bae --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,40 @@ +[extend] +useDefault = true + +[[rules]] +id = "generic-api-key" +# all the other attributes from the default rule are inherited + + [[rules.allowlists]] + regexTarget = "line" + regexes = [ + '''objectKey''', + '''S3Key''', + '''SopsAgeKey''', + '''s3Key''', + ] + +[[rules]] +id = "private-key" + + [[rules.allowlists]] + regexTarget = "line" + regexes = [ + '''(.*)OAdqlMznWINBDoyR\+PESgQJlUptwnh(.*)''', + ] + +[allowlist] +description = "global allow list" +paths = [ + '''\.gitleaks\.toml''', + '''lambda/events/(.*?)json''', + '''lambda/__snapshots__/(.*?)snap''', + '''test-secrets/(.*?)(json|yaml|yml|env|binary)''', + '''test/(.*)\.integ\.snapshot/(.*?)json''' +] + +regexTarget = "match" +regexes = [ + '''AGE-SECRET-KEY-1EFUWJ0G2XJTJFWTAM2DGMA4VCK3R05W58FSMHZP3MZQ0ZTAQEAFQC6T7T3''', +] + diff --git a/API.md b/API.md index 4658531e..a986522a 100644 --- a/API.md +++ b/API.md @@ -1717,12 +1717,11 @@ const multiStringParameterProps: MultiStringParameterProps = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | +| autoGenerateIamPermissions | boolean | Should this construct automatically create IAM permissions? | | convertToJSON | boolean | Should the encrypted sops value should be converted to JSON? | -| creationType | CreationType | *No description.* | | flatten | boolean | Should the structure be flattened? | | flattenSeparator | string | If the structure should be flattened use the provided separator between keys. | | parameterKeyPrefix | string | Add this prefix to parameter names. | -| resourceType | ResourceType | *No description.* | | sopsAgeKey | aws-cdk-lib.SecretValue | The age key that should be used for encryption. | | sopsFileFormat | string | The format of the sops file. | | sopsFilePath | string | The filepath to the sops file. | @@ -1746,28 +1745,31 @@ const multiStringParameterProps: MultiStringParameterProps = { ... } --- -##### `convertToJSON`Optional +##### `autoGenerateIamPermissions`Optional ```typescript -public readonly convertToJSON: boolean; +public readonly autoGenerateIamPermissions: boolean; ``` - *Type:* boolean - *Default:* true -Should the encrypted sops value should be converted to JSON? - -Only JSON can be handled by cloud formations dynamic references. +Should this construct automatically create IAM permissions? --- -##### `creationType`Optional +##### `convertToJSON`Optional ```typescript -public readonly creationType: CreationType; +public readonly convertToJSON: boolean; ``` -- *Type:* CreationType +- *Type:* boolean +- *Default:* true + +Should the encrypted sops value should be converted to JSON? + +Only JSON can be handled by cloud formations dynamic references. --- @@ -1813,16 +1815,6 @@ Add this prefix to parameter names. --- -##### `resourceType`Optional - -```typescript -public readonly resourceType: ResourceType; -``` - -- *Type:* ResourceType - ---- - ##### `sopsAgeKey`Optional ```typescript @@ -2117,12 +2109,11 @@ const sopsSecretProps: SopsSecretProps = { ... } | secretObjectValue | {[ key: string ]: aws-cdk-lib.SecretValue} | Initial value for a JSON secret. | | secretStringBeta1 | aws-cdk-lib.aws_secretsmanager.SecretStringValueBeta1 | Initial value for the secret. | | secretStringValue | aws-cdk-lib.SecretValue | Initial value for the secret. | +| autoGenerateIamPermissions | boolean | Should this construct automatically create IAM permissions? | | convertToJSON | boolean | Should the encrypted sops value should be converted to JSON? | -| creationType | CreationType | *No description.* | | flatten | boolean | Should the structure be flattened? | | flattenSeparator | string | If the structure should be flattened use the provided separator between keys. | | parameterKeyPrefix | string | Add this prefix to parameter names. | -| resourceType | ResourceType | *No description.* | | sopsAgeKey | aws-cdk-lib.SecretValue | The age key that should be used for encryption. | | sopsFileFormat | string | The format of the sops file. | | sopsFilePath | string | The filepath to the sops file. | @@ -2308,28 +2299,31 @@ Only one of `secretStringBeta1`, `secretStringValue`, 'secretObjectValue', and ` --- -##### `convertToJSON`Optional +##### `autoGenerateIamPermissions`Optional ```typescript -public readonly convertToJSON: boolean; +public readonly autoGenerateIamPermissions: boolean; ``` - *Type:* boolean - *Default:* true -Should the encrypted sops value should be converted to JSON? - -Only JSON can be handled by cloud formations dynamic references. +Should this construct automatically create IAM permissions? --- -##### `creationType`Optional +##### `convertToJSON`Optional ```typescript -public readonly creationType: CreationType; +public readonly convertToJSON: boolean; ``` -- *Type:* CreationType +- *Type:* boolean +- *Default:* true + +Should the encrypted sops value should be converted to JSON? + +Only JSON can be handled by cloud formations dynamic references. --- @@ -2375,16 +2369,6 @@ Add this prefix to parameter names. --- -##### `resourceType`Optional - -```typescript -public readonly resourceType: ResourceType; -``` - -- *Type:* ResourceType - ---- - ##### `sopsAgeKey`Optional ```typescript @@ -2522,12 +2506,11 @@ const sopsStringParameterProps: SopsStringParameterProps = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | +| autoGenerateIamPermissions | boolean | Should this construct automatically create IAM permissions? | | convertToJSON | boolean | Should the encrypted sops value should be converted to JSON? | -| creationType | CreationType | *No description.* | | flatten | boolean | Should the structure be flattened? | | flattenSeparator | string | If the structure should be flattened use the provided separator between keys. | | parameterKeyPrefix | string | Add this prefix to parameter names. | -| resourceType | ResourceType | *No description.* | | sopsAgeKey | aws-cdk-lib.SecretValue | The age key that should be used for encryption. | | sopsFileFormat | string | The format of the sops file. | | sopsFilePath | string | The filepath to the sops file. | @@ -2549,28 +2532,31 @@ const sopsStringParameterProps: SopsStringParameterProps = { ... } --- -##### `convertToJSON`Optional +##### `autoGenerateIamPermissions`Optional ```typescript -public readonly convertToJSON: boolean; +public readonly autoGenerateIamPermissions: boolean; ``` - *Type:* boolean - *Default:* true -Should the encrypted sops value should be converted to JSON? - -Only JSON can be handled by cloud formations dynamic references. +Should this construct automatically create IAM permissions? --- -##### `creationType`Optional +##### `convertToJSON`Optional ```typescript -public readonly creationType: CreationType; +public readonly convertToJSON: boolean; ``` -- *Type:* CreationType +- *Type:* boolean +- *Default:* true + +Should the encrypted sops value should be converted to JSON? + +Only JSON can be handled by cloud formations dynamic references. --- @@ -2616,16 +2602,6 @@ Add this prefix to parameter names. --- -##### `resourceType`Optional - -```typescript -public readonly resourceType: ResourceType; -``` - -- *Type:* ResourceType - ---- - ##### `sopsAgeKey`Optional ```typescript @@ -2891,12 +2867,11 @@ const sopsSyncOptions: SopsSyncOptions = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | +| autoGenerateIamPermissions | boolean | Should this construct automatically create IAM permissions? | | convertToJSON | boolean | Should the encrypted sops value should be converted to JSON? | -| creationType | CreationType | *No description.* | | flatten | boolean | Should the structure be flattened? | | flattenSeparator | string | If the structure should be flattened use the provided separator between keys. | | parameterKeyPrefix | string | Add this prefix to parameter names. | -| resourceType | ResourceType | *No description.* | | sopsAgeKey | aws-cdk-lib.SecretValue | The age key that should be used for encryption. | | sopsFileFormat | string | The format of the sops file. | | sopsFilePath | string | The filepath to the sops file. | @@ -2909,28 +2884,31 @@ const sopsSyncOptions: SopsSyncOptions = { ... } --- -##### `convertToJSON`Optional +##### `autoGenerateIamPermissions`Optional ```typescript -public readonly convertToJSON: boolean; +public readonly autoGenerateIamPermissions: boolean; ``` - *Type:* boolean - *Default:* true -Should the encrypted sops value should be converted to JSON? - -Only JSON can be handled by cloud formations dynamic references. +Should this construct automatically create IAM permissions? --- -##### `creationType`Optional +##### `convertToJSON`Optional ```typescript -public readonly creationType: CreationType; +public readonly convertToJSON: boolean; ``` -- *Type:* CreationType +- *Type:* boolean +- *Default:* true + +Should the encrypted sops value should be converted to JSON? + +Only JSON can be handled by cloud formations dynamic references. --- @@ -2976,16 +2954,6 @@ Add this prefix to parameter names. --- -##### `resourceType`Optional - -```typescript -public readonly resourceType: ResourceType; -``` - -- *Type:* ResourceType - ---- - ##### `sopsAgeKey`Optional ```typescript @@ -3109,7 +3077,7 @@ How should the secret be passed to the CustomResource? ### SopsSyncProps -The configuration options extended by the target Secret. +The configuration options extended by the target Secret / Parameter. #### Initializer @@ -3123,12 +3091,11 @@ const sopsSyncProps: SopsSyncProps = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | +| autoGenerateIamPermissions | boolean | Should this construct automatically create IAM permissions? | | convertToJSON | boolean | Should the encrypted sops value should be converted to JSON? | -| creationType | CreationType | *No description.* | | flatten | boolean | Should the structure be flattened? | | flattenSeparator | string | If the structure should be flattened use the provided separator between keys. | | parameterKeyPrefix | string | Add this prefix to parameter names. | -| resourceType | ResourceType | *No description.* | | sopsAgeKey | aws-cdk-lib.SecretValue | The age key that should be used for encryption. | | sopsFileFormat | string | The format of the sops file. | | sopsFilePath | string | The filepath to the sops file. | @@ -3139,34 +3106,37 @@ const sopsSyncProps: SopsSyncProps = { ... } | stringifyValues | boolean | Shall all values be flattened? | | uploadType | UploadType | How should the secret be passed to the CustomResource? | | encryptionKey | aws-cdk-lib.aws_kms.IKey | The encryption key used for encrypting the ssm parameter if `parameterName` is set. | -| parameterName | string | The parameter name. | -| parameterNames | string[] | The parameter name. | +| parameterNames | string[] | The parameter names. | +| resourceType | ResourceType | Will this Sync deploy a Secret or Parameter(s). | | secret | aws-cdk-lib.aws_secretsmanager.ISecret | The secret that will be populated with the encrypted sops file content. | --- -##### `convertToJSON`Optional +##### `autoGenerateIamPermissions`Optional ```typescript -public readonly convertToJSON: boolean; +public readonly autoGenerateIamPermissions: boolean; ``` - *Type:* boolean - *Default:* true -Should the encrypted sops value should be converted to JSON? - -Only JSON can be handled by cloud formations dynamic references. +Should this construct automatically create IAM permissions? --- -##### `creationType`Optional +##### `convertToJSON`Optional ```typescript -public readonly creationType: CreationType; +public readonly convertToJSON: boolean; ``` -- *Type:* CreationType +- *Type:* boolean +- *Default:* true + +Should the encrypted sops value should be converted to JSON? + +Only JSON can be handled by cloud formations dynamic references. --- @@ -3212,16 +3182,6 @@ Add this prefix to parameter names. --- -##### `resourceType`Optional - -```typescript -public readonly resourceType: ResourceType; -``` - -- *Type:* ResourceType - ---- - ##### `sopsAgeKey`Optional ```typescript @@ -3355,31 +3315,29 @@ The encryption key used for encrypting the ssm parameter if `parameterName` is s --- -##### `parameterName`Optional +##### `parameterNames`Optional ```typescript -public readonly parameterName: string; +public readonly parameterNames: string[]; ``` -- *Type:* string +- *Type:* string[] -The parameter name. +The parameter names. -If set this creates an encrypted SSM Parameter instead of a secret. +If set this creates encrypted SSM Parameters instead of a secret. --- -##### `parameterNames`Optional +##### `resourceType`Optional ```typescript -public readonly parameterNames: string[]; +public readonly resourceType: ResourceType; ``` -- *Type:* string[] - -The parameter name. +- *Type:* ResourceType -If set this creates an encrypted SSM Parameter instead of a secret. +Will this Sync deploy a Secret or Parameter(s). --- @@ -3411,12 +3369,28 @@ const sopsSyncProviderProps: SopsSyncProviderProps = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | +| role | aws-cdk-lib.aws_iam.IRole | The role that should be used for the custom resource provider. | | securityGroups | aws-cdk-lib.aws_ec2.ISecurityGroup[] | Only if `vpc` is supplied: The list of security groups to associate with the Lambda's network interfaces. | | vpc | aws-cdk-lib.aws_ec2.IVpc | VPC network to place Lambda network interfaces. | | vpcSubnets | aws-cdk-lib.aws_ec2.SubnetSelection | Where to place the network interfaces within the VPC. | --- +##### `role`Optional + +```typescript +public readonly role: IRole; +``` + +- *Type:* aws-cdk-lib.aws_iam.IRole +- *Default:* a new role will be created + +The role that should be used for the custom resource provider. + +If you don't specify any, a new role will be created with all required permissions + +--- + ##### `securityGroups`Optional ```typescript @@ -3460,31 +3434,6 @@ Where to place the network interfaces within the VPC. ## Enums -### CreationType - -#### Members - -| **Name** | **Description** | -| --- | --- | -| SINGLE | Create or update a single secret/parameter. | -| MULTI | Create or update a multiple secrets/parameters by flattening the SOPS file. | - ---- - -##### `SINGLE` - -Create or update a single secret/parameter. - ---- - - -##### `MULTI` - -Create or update a multiple secrets/parameters by flattening the SOPS file. - ---- - - ### ResourceType #### Members @@ -3493,6 +3442,7 @@ Create or update a multiple secrets/parameters by flattening the SOPS file. | --- | --- | | SECRET | *No description.* | | PARAMETER | *No description.* | +| PARAMETER_MULTI | *No description.* | --- @@ -3506,6 +3456,11 @@ Create or update a multiple secrets/parameters by flattening the SOPS file. --- +##### `PARAMETER_MULTI` + +--- + + ### UploadType #### Members diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8ace1023..44316f21 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,4 +2,26 @@ Thanks for your interest in our project. Contributions are welcome. Feel free to [open an issue](issues) with questions or reporting ideas and bugs, or [open pull requests](pulls) to contribute code. -We are committed to fostering a welcoming, respectful, and harassment-free environment. Be kind! \ No newline at end of file +We are committed to fostering a welcoming, respectful, and harassment-free environment. Be kind! + +## How to buidl/deploy local + +Install all necessary tools with `yarn install` and others manually like `go` + +Build the go Lambda code: +``` +./scripts/build.sh +``` +Build the package (for CDK development only the first `js` build has to complete): +``` +yarn projen build +``` +Link the package: +``` +yarn link +``` +Switch to the path/project where you would like to use cdk-sops-secrets. \ +Link the package to your local build source: +``` +yarn link "cdk-sops-secrets" +``` \ No newline at end of file diff --git a/lambda/__snapshots__/handler_parameter_raw_test.snap b/lambda/__snapshots__/handler_parameter_raw_test.snap index cf7705da..46007613 100755 --- a/lambda/__snapshots__/handler_parameter_raw_test.snap +++ b/lambda/__snapshots__/handler_parameter_raw_test.snap @@ -3,7 +3,8 @@ >>>S3ApiMockClient.GetObjectAttributes.Input { Bucket: "..", - Key: "../test-secrets/binary/sopsfile.enc-age.binary" + Key: "../test-secrets/binary/sopsfile.enc-age.binary", + ObjectAttributes: ["ETag"] } --- diff --git a/lambda/__snapshots__/handler_parameter_yaml_multi_test.snap b/lambda/__snapshots__/handler_parameter_yaml_multi_test.snap index b33091f7..8826756e 100755 --- a/lambda/__snapshots__/handler_parameter_yaml_multi_test.snap +++ b/lambda/__snapshots__/handler_parameter_yaml_multi_test.snap @@ -3,7 +3,8 @@ >>>S3ApiMockClient.GetObjectAttributes.Input { Bucket: "..", - Key: "../test-secrets/yaml/sopsfile-complex-parameters.enc-age.yaml" + Key: "../test-secrets/yaml/sopsfile-complex-parameters.enc-age.yaml", + ObjectAttributes: ["ETag"] } --- @@ -65,7 +66,8 @@ nil >>>S3ApiMockClient.GetObjectAttributes.Input { Bucket: "..", - Key: "../test-secrets/yaml/sopsfile-complex-parameters.enc-age.yaml" + Key: "../test-secrets/yaml/sopsfile-complex-parameters.enc-age.yaml", + ObjectAttributes: ["ETag"] } --- diff --git a/lambda/__snapshots__/handler_secret_env_test.snap b/lambda/__snapshots__/handler_secret_env_test.snap index 20b065d1..0488553e 100755 --- a/lambda/__snapshots__/handler_secret_env_test.snap +++ b/lambda/__snapshots__/handler_secret_env_test.snap @@ -3,7 +3,8 @@ >>>S3ApiMockClient.GetObjectAttributes.Input { Bucket: "..", - Key: "../test-secrets/dotenv/encrypted-best-secret.env" + Key: "../test-secrets/dotenv/encrypted-best-secret.env", + ObjectAttributes: ["ETag"] } --- @@ -46,7 +47,8 @@ nil >>>S3ApiMockClient.GetObjectAttributes.Input { Bucket: "..", - Key: "../test-secrets/dotenv/encrypted-best-secret.env" + Key: "../test-secrets/dotenv/encrypted-best-secret.env", + ObjectAttributes: ["ETag"] } --- diff --git a/lambda/__snapshots__/handler_secret_json_test.snap b/lambda/__snapshots__/handler_secret_json_test.snap index bbe043fc..83ca106c 100755 --- a/lambda/__snapshots__/handler_secret_json_test.snap +++ b/lambda/__snapshots__/handler_secret_json_test.snap @@ -3,7 +3,8 @@ >>>S3ApiMockClient.GetObjectAttributes.Input { Bucket: "..", - Key: "../test-secrets/json/sopsfile.enc-age.json" + Key: "../test-secrets/json/sopsfile.enc-age.json", + ObjectAttributes: ["ETag"] } --- @@ -46,7 +47,8 @@ nil >>>S3ApiMockClient.GetObjectAttributes.Input { Bucket: "..", - Key: "../test-secrets/json/sopsfile-complex.enc-age.json" + Key: "../test-secrets/json/sopsfile-complex.enc-age.json", + ObjectAttributes: ["ETag"] } --- @@ -89,7 +91,8 @@ nil >>>S3ApiMockClient.GetObjectAttributes.Input { Bucket: "..", - Key: "../test-secrets/json/sopsfile-complex.enc-age.json" + Key: "../test-secrets/json/sopsfile-complex.enc-age.json", + ObjectAttributes: ["ETag"] } --- @@ -132,7 +135,8 @@ nil >>>S3ApiMockClient.GetObjectAttributes.Input { Bucket: "..", - Key: "../test-secrets/json/sopsfile-complex.enc-age.json" + Key: "../test-secrets/json/sopsfile-complex.enc-age.json", + ObjectAttributes: ["ETag"] } --- diff --git a/lambda/__snapshots__/handler_secret_raw_test.snap b/lambda/__snapshots__/handler_secret_raw_test.snap index 78323e57..674c2da6 100755 --- a/lambda/__snapshots__/handler_secret_raw_test.snap +++ b/lambda/__snapshots__/handler_secret_raw_test.snap @@ -3,7 +3,8 @@ >>>S3ApiMockClient.GetObjectAttributes.Input { Bucket: "..", - Key: "../test-secrets/binary/sopsfile.enc-age.binary" + Key: "../test-secrets/binary/sopsfile.enc-age.binary", + ObjectAttributes: ["ETag"] } --- diff --git a/lambda/__snapshots__/handler_secret_yaml_test.snap b/lambda/__snapshots__/handler_secret_yaml_test.snap index 72c49a19..9e19453e 100755 --- a/lambda/__snapshots__/handler_secret_yaml_test.snap +++ b/lambda/__snapshots__/handler_secret_yaml_test.snap @@ -3,7 +3,8 @@ >>>S3ApiMockClient.GetObjectAttributes.Input { Bucket: "..", - Key: "../test-secrets/yaml/sopsfile.enc-age.yaml" + Key: "../test-secrets/yaml/sopsfile.enc-age.yaml", + ObjectAttributes: ["ETag"] } --- @@ -46,7 +47,8 @@ nil >>>S3ApiMockClient.GetObjectAttributes.Input { Bucket: "..", - Key: "../test-secrets/yaml/sopsfile.enc-age.yaml" + Key: "../test-secrets/yaml/sopsfile.enc-age.yaml", + ObjectAttributes: ["ETag"] } --- @@ -89,7 +91,8 @@ nil >>>S3ApiMockClient.GetObjectAttributes.Input { Bucket: "..", - Key: "../test-secrets/yaml/sopsfile-complex.enc-age.yaml" + Key: "../test-secrets/yaml/sopsfile-complex.enc-age.yaml", + ObjectAttributes: ["ETag"] } --- @@ -132,7 +135,8 @@ nil >>>S3ApiMockClient.GetObjectAttributes.Input { Bucket: "..", - Key: "../test-secrets/yaml/sopsfile-complex.enc-age.yaml" + Key: "../test-secrets/yaml/sopsfile-complex.enc-age.yaml", + ObjectAttributes: ["ETag"] } --- @@ -175,7 +179,8 @@ nil >>>S3ApiMockClient.GetObjectAttributes.Input { Bucket: "..", - Key: "../test-secrets/yaml/sopsfile-complex.enc-age.yaml" + Key: "../test-secrets/yaml/sopsfile-complex.enc-age.yaml", + ObjectAttributes: ["ETag"] } --- @@ -218,7 +223,8 @@ nil >>>S3ApiMockClient.GetObjectAttributes.Input { Bucket: "..", - Key: "../test-secrets/yaml/sopsfile-complex.enc-age.yaml" + Key: "../test-secrets/yaml/sopsfile-complex.enc-age.yaml", + ObjectAttributes: ["ETag"] } --- @@ -342,7 +348,8 @@ nil >>>S3ApiMockClient.GetObjectAttributes.Input { Bucket: "..", - Key: "../test-secrets/yaml/sopsfile-complex.enc-age.yaml" + Key: "../test-secrets/yaml/sopsfile-complex.enc-age.yaml", + ObjectAttributes: ["ETag"] } --- diff --git a/lambda/events/event_create_s3_parameter_raw_simple.json b/lambda/events/event_create_s3_parameter_raw_simple.json index e3a94b67..0309d8c2 100644 --- a/lambda/events/event_create_s3_parameter_raw_simple.json +++ b/lambda/events/event_create_s3_parameter_raw_simple.json @@ -3,7 +3,6 @@ "LogicalResourceId": "LogicalResourceId", "ResourceProperties": { "ResourceType": "PARAMETER", - "CreationType": "SINGLE", "ParameterName": "arn:aws:ssm:eu-central-1:123456789012:parameter/testsecret", "SopsS3File": { "Bucket": "..", diff --git a/lambda/events/event_create_s3_parameter_yaml_complex.json b/lambda/events/event_create_s3_parameter_yaml_complex.json index 620541ba..b84319a1 100644 --- a/lambda/events/event_create_s3_parameter_yaml_complex.json +++ b/lambda/events/event_create_s3_parameter_yaml_complex.json @@ -2,8 +2,7 @@ "RequestType": "Create", "LogicalResourceId": "LogicalResourceId", "ResourceProperties": { - "ResourceType": "PARAMETER", - "CreationType": "MULTI", + "ResourceType": "PARAMETER_MULTI", "ParameterName": "arn:aws:ssm:eu-central-1:123456789012:parameter/testsecret", "SopsS3File": { "Bucket": "..", diff --git a/lambda/events/event_create_s3_parameter_yaml_complex_custom_keys.json b/lambda/events/event_create_s3_parameter_yaml_complex_custom_keys.json index 1e8723e7..893a909e 100644 --- a/lambda/events/event_create_s3_parameter_yaml_complex_custom_keys.json +++ b/lambda/events/event_create_s3_parameter_yaml_complex_custom_keys.json @@ -2,8 +2,7 @@ "RequestType": "Create", "LogicalResourceId": "LogicalResourceId", "ResourceProperties": { - "ResourceType": "PARAMETER", - "CreationType": "MULTI", + "ResourceType": "PARAMETER_MULTI", "ParameterName": "arn:aws:ssm:eu-central-1:123456789012:parameter/testsecret", "SopsS3File": { "Bucket": "..", diff --git a/lambda/events/event_create_s3_secret_env_as_json_simple.json b/lambda/events/event_create_s3_secret_env_as_json_simple.json index ba1dc2a5..bc276b4c 100644 --- a/lambda/events/event_create_s3_secret_env_as_json_simple.json +++ b/lambda/events/event_create_s3_secret_env_as_json_simple.json @@ -3,7 +3,6 @@ "LogicalResourceId": "LogicalResourceId", "ResourceProperties": { "ResourceType": "SECRET", - "CreationType": "SINGLE", "FlattenSeparator": ".", "SecretARN": "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", "SopsS3File": { diff --git a/lambda/events/event_create_s3_secret_env_simple.json b/lambda/events/event_create_s3_secret_env_simple.json index f39d3d8c..9b1baf91 100644 --- a/lambda/events/event_create_s3_secret_env_simple.json +++ b/lambda/events/event_create_s3_secret_env_simple.json @@ -3,7 +3,6 @@ "LogicalResourceId": "LogicalResourceId", "ResourceProperties": { "ResourceType": "SECRET", - "CreationType": "SINGLE", "FlattenSeparator": ".", "SecretARN": "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", "SopsS3File": { diff --git a/lambda/events/event_create_s3_secret_json_complex.json b/lambda/events/event_create_s3_secret_json_complex.json index 74f08b32..a94df6b3 100644 --- a/lambda/events/event_create_s3_secret_json_complex.json +++ b/lambda/events/event_create_s3_secret_json_complex.json @@ -3,7 +3,6 @@ "LogicalResourceId": "LogicalResourceId", "ResourceProperties": { "ResourceType": "SECRET", - "CreationType": "SINGLE", "FlattenSeparator": ".", "SecretARN": "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", "SopsS3File": { diff --git a/lambda/events/event_create_s3_secret_json_complex_flat.json b/lambda/events/event_create_s3_secret_json_complex_flat.json index d579f7ae..5a7286bc 100644 --- a/lambda/events/event_create_s3_secret_json_complex_flat.json +++ b/lambda/events/event_create_s3_secret_json_complex_flat.json @@ -3,7 +3,6 @@ "LogicalResourceId": "LogicalResourceId", "ResourceProperties": { "ResourceType": "SECRET", - "CreationType": "SINGLE", "FlattenSeparator": ".", "SecretARN": "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", "SopsS3File": { diff --git a/lambda/events/event_create_s3_secret_json_complex_stringify.json b/lambda/events/event_create_s3_secret_json_complex_stringify.json index 533eb648..b08f818d 100644 --- a/lambda/events/event_create_s3_secret_json_complex_stringify.json +++ b/lambda/events/event_create_s3_secret_json_complex_stringify.json @@ -3,7 +3,6 @@ "LogicalResourceId": "LogicalResourceId", "ResourceProperties": { "ResourceType": "SECRET", - "CreationType": "SINGLE", "FlattenSeparator": ".", "SecretARN": "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", "SopsS3File": { diff --git a/lambda/events/event_create_s3_secret_json_simple.json b/lambda/events/event_create_s3_secret_json_simple.json index 4f423508..5d657957 100644 --- a/lambda/events/event_create_s3_secret_json_simple.json +++ b/lambda/events/event_create_s3_secret_json_simple.json @@ -3,7 +3,6 @@ "LogicalResourceId": "LogicalResourceId", "ResourceProperties": { "ResourceType": "SECRET", - "CreationType": "SINGLE", "FlattenSeparator": ".", "SecretARN": "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", "SopsS3File": { diff --git a/lambda/events/event_create_s3_secret_raw_simple.json b/lambda/events/event_create_s3_secret_raw_simple.json index 8c3a1b85..8f85ba91 100644 --- a/lambda/events/event_create_s3_secret_raw_simple.json +++ b/lambda/events/event_create_s3_secret_raw_simple.json @@ -3,7 +3,6 @@ "LogicalResourceId": "LogicalResourceId", "ResourceProperties": { "ResourceType": "SECRET", - "CreationType": "SINGLE", "FlattenSeparator": ".", "SecretARN": "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", "SopsS3File": { diff --git a/lambda/events/event_create_s3_secret_yaml_as_json_complex.json b/lambda/events/event_create_s3_secret_yaml_as_json_complex.json index 9e44b39c..48801dad 100644 --- a/lambda/events/event_create_s3_secret_yaml_as_json_complex.json +++ b/lambda/events/event_create_s3_secret_yaml_as_json_complex.json @@ -3,7 +3,6 @@ "LogicalResourceId": "LogicalResourceId", "ResourceProperties": { "ResourceType": "SECRET", - "CreationType": "SINGLE", "FlattenSeparator": ".", "SecretARN": "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", "SopsS3File": { diff --git a/lambda/events/event_create_s3_secret_yaml_as_json_complex_flat.json b/lambda/events/event_create_s3_secret_yaml_as_json_complex_flat.json index 9e88f100..995f00ed 100644 --- a/lambda/events/event_create_s3_secret_yaml_as_json_complex_flat.json +++ b/lambda/events/event_create_s3_secret_yaml_as_json_complex_flat.json @@ -3,7 +3,6 @@ "LogicalResourceId": "LogicalResourceId", "ResourceProperties": { "ResourceType": "SECRET", - "CreationType": "SINGLE", "FlattenSeparator": ".", "SecretARN": "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", "SopsS3File": { diff --git a/lambda/events/event_create_s3_secret_yaml_as_json_simple.json b/lambda/events/event_create_s3_secret_yaml_as_json_simple.json index d43d869f..02420ea7 100644 --- a/lambda/events/event_create_s3_secret_yaml_as_json_simple.json +++ b/lambda/events/event_create_s3_secret_yaml_as_json_simple.json @@ -3,7 +3,6 @@ "LogicalResourceId": "LogicalResourceId", "ResourceProperties": { "ResourceType": "SECRET", - "CreationType": "SINGLE", "FlattenSeparator": ".", "SecretARN": "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", "SopsS3File": { diff --git a/lambda/events/event_create_s3_secret_yaml_complex.json b/lambda/events/event_create_s3_secret_yaml_complex.json index 4ee77841..a2e0403d 100644 --- a/lambda/events/event_create_s3_secret_yaml_complex.json +++ b/lambda/events/event_create_s3_secret_yaml_complex.json @@ -3,7 +3,6 @@ "LogicalResourceId": "LogicalResourceId", "ResourceProperties": { "ResourceType": "SECRET", - "CreationType": "SINGLE", "FlattenSeparator": ".", "SecretARN": "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", "SopsS3File": { diff --git a/lambda/events/event_create_s3_secret_yaml_complex_flat.json b/lambda/events/event_create_s3_secret_yaml_complex_flat.json index 7e4ad675..d7d8cc78 100644 --- a/lambda/events/event_create_s3_secret_yaml_complex_flat.json +++ b/lambda/events/event_create_s3_secret_yaml_complex_flat.json @@ -3,7 +3,6 @@ "LogicalResourceId": "LogicalResourceId", "ResourceProperties": { "ResourceType": "SECRET", - "CreationType": "SINGLE", "FlattenSeparator": ".", "SecretARN": "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", "SopsS3File": { diff --git a/lambda/events/event_create_s3_secret_yaml_simple.json b/lambda/events/event_create_s3_secret_yaml_simple.json index 6d82161d..8fecbe62 100644 --- a/lambda/events/event_create_s3_secret_yaml_simple.json +++ b/lambda/events/event_create_s3_secret_yaml_simple.json @@ -3,7 +3,6 @@ "LogicalResourceId": "LogicalResourceId", "ResourceProperties": { "ResourceType": "SECRET", - "CreationType": "SINGLE", "FlattenSeparator": ".", "SecretARN": "arn:aws:secretsmanager:eu-central-1:123456789012:secret:testsecret", "SopsS3File": { diff --git a/lambda/main.go b/lambda/main.go index 2b6f3f8e..5ab7fd30 100644 --- a/lambda/main.go +++ b/lambda/main.go @@ -4,7 +4,6 @@ import ( "context" "encoding/base64" "encoding/json" - "errors" "fmt" "log" "reflect" @@ -51,7 +50,6 @@ type SopsSyncResourcePropertys struct { FlattenSeparator string `json:"FlattenSeparator,omitempty"` ParameterKeyPrefix string `json:"ParameterKeyPrefix,omitempty"` StringifyValues string `json:"StringifyValues,omitempty"` - CreationType string `json:"CreationType,omitempty"` ResourceType string `json:"ResourceType,omitempty"` } @@ -70,7 +68,7 @@ func (a AWS) getS3FileContent(file SopsS3File) (data []byte, err error) { Key: &file.Key, }) if err != nil { - return nil, errors.New(fmt.Sprintf("S3 download error:\n%v\n", err)) + return nil, fmt.Errorf("S3 download error:\n%v", err) } log.Printf("Downloaded %d bytes", resp) return buf.Bytes(), nil @@ -80,7 +78,7 @@ func decryptSopsFileContent(content []byte, format string) (data []byte, err err log.Printf("Decrypting content with format %s\n", format) resp, err := decrypt.Data(content, format) if err != nil { - return nil, errors.New(fmt.Sprintf("Decryption error:\n%v\n", err)) + return nil, fmt.Errorf("decryption error:\n%v", err) } log.Println("Decrypted") return resp, nil @@ -95,7 +93,7 @@ func (a AWS) updateSecret(sopsHash string, secretArn string, secretContent []byt } secretResp, secretErr := a.secretsmanager.PutSecretValue(input) if secretErr != nil { - return nil, errors.New(fmt.Sprintf("Failed to store secret value:\nsecretArn: %s\nClientRequestToken: %s\n%v\n", secretArn, sopsHash, secretErr)) + return nil, fmt.Errorf("failed to store secret value:\nsecretArn: %s\nClientRequestToken: %s\n%v", secretArn, sopsHash, secretErr) } arn := generatePhysicalResourceId(*secretResp.ARN) secretResp.ARN = &arn @@ -114,7 +112,7 @@ func (a AWS) updateSSMParameter(parameterName string, parameterContent []byte, k } paramResp, paramErr := a.ssm.PutParameter(input) if paramErr != nil { - return nil, errors.New(fmt.Sprintf("Failed to store parameter value:\nparameterName: %s\n%v\n", parameterName, paramErr)) + return nil, fmt.Errorf("failed to store parameter value:\nparameterName: %s\n%v", parameterName, paramErr) } log.Printf("Successfully stored parameter:\n%v\n", paramResp) return paramResp, nil @@ -156,13 +154,19 @@ func (a AWS) syncSopsToSecretsmanager(ctx context.Context, event cfn.Event) (phy attr, err := a.s3Api.GetObjectAttributes(&s3.GetObjectAttributesInput{ Bucket: &sopsFile.Bucket, Key: &sopsFile.Key, + ObjectAttributes: []*string{ + aws.String("ETag"), + }, }) + if err != nil { + return tempArn, nil, fmt.Errorf("error while getting S3 object attributes:\n%v", err) + } encryptedContent, err = a.getS3FileContent(sopsFile) if err != nil { - return tempArn, nil, err + return tempArn, nil, fmt.Errorf("error while getting S3 file content:\n%v", err) } if attr.ETag == nil { - return tempArn, nil, errors.New("No ETag checksum found in S3 object") + return tempArn, nil, fmt.Errorf("no ETag checksum found in S3 object:\n%v", err) } sopsHash = *attr.ETag } @@ -177,15 +181,15 @@ func (a AWS) syncSopsToSecretsmanager(ctx context.Context, event cfn.Event) (phy } if encryptedContent == nil { - return tempArn, nil, errors.New("No encrypted content found! Did you pass SopsS3File or SopsInline?") + return tempArn, nil, fmt.Errorf("no encrypted content found! Did you pass SopsS3File or SopsInline:\n%v", err) } if sopsHash == "" { - return tempArn, nil, errors.New("No sopsHash found! Did you pass SopsS3File or SopsInline?") + return tempArn, nil, fmt.Errorf("no sopsHash found! Did you pass SopsS3File or SopsInline:\n%v", err) } decryptedContent, err := decryptSopsFileContent(encryptedContent, resourceProperties.Format) if err != nil { - return tempArn, nil, err + return tempArn, nil, fmt.Errorf("error while decrypting file content\n%v", err) } var decryptedInterface interface{} switch resourceProperties.Format { @@ -193,14 +197,14 @@ func (a AWS) syncSopsToSecretsmanager(ctx context.Context, event cfn.Event) (phy { err := json.Unmarshal(decryptedContent, &decryptedInterface) if err != nil { - return tempArn, nil, fmt.Errorf("Failed to parse json content: %v", err) + return tempArn, nil, fmt.Errorf("failed to parse json content:\n%v", err) } } case "yaml": { err := yaml.Unmarshal(decryptedContent, &decryptedInterface) if err != nil { - return tempArn, nil, fmt.Errorf("Failed to parse yaml content: %v", err) + return tempArn, nil, fmt.Errorf("failed to parse yaml content:\n%v", err) } } case "dotenv": @@ -228,14 +232,14 @@ func (a AWS) syncSopsToSecretsmanager(ctx context.Context, event cfn.Event) (phy resourceProperties.ConvertToJSON = "false" } default: - return "", nil, errors.New(fmt.Sprintf("Format %s not supported", resourceProperties.Format)) + return "", nil, fmt.Errorf("format %s not supported", resourceProperties.Format) } if resourceProperties.Flatten == "" { resourceProperties.Flatten = "true" } resourcePropertiesFlatten, err := strconv.ParseBool(resourceProperties.Flatten) if err != nil { - return tempArn, nil, err + return tempArn, nil, fmt.Errorf("failed to parse bool:\n%v", err) } var finalInterface interface{} @@ -244,7 +248,7 @@ func (a AWS) syncSopsToSecretsmanager(ctx context.Context, event cfn.Event) (phy flattenedInterface := make(map[string]interface{}) err := flatten("", decryptedInterface, flattenedInterface, resourceProperties.FlattenSeparator) if err != nil { - return tempArn, nil, err + return tempArn, nil, fmt.Errorf("failed to flatten:\n%v", err) } finalInterface = flattenedInterface } else { @@ -256,12 +260,12 @@ func (a AWS) syncSopsToSecretsmanager(ctx context.Context, event cfn.Event) (phy } resourcePropertiesStringifyValues, err := strconv.ParseBool(resourceProperties.StringifyValues) if err != nil { - return tempArn, nil, err + return tempArn, nil, fmt.Errorf("failed to parse bool:\n%v", err) } if resourcePropertiesStringifyValues { finalInterface, _, err = stringifyValues(finalInterface) if err != nil { - return tempArn, nil, err + return tempArn, nil, fmt.Errorf("failed to stringify values:\n%v", err) } } @@ -270,24 +274,24 @@ func (a AWS) syncSopsToSecretsmanager(ctx context.Context, event cfn.Event) (phy } resourcePropertieConvertToJSON, err := strconv.ParseBool(resourceProperties.ConvertToJSON) if err != nil { - return tempArn, nil, err + return tempArn, nil, fmt.Errorf("failed to parse bool:\n%v", err) } if resourcePropertieConvertToJSON || resourceProperties.Format == "json" { decryptedContent, err = toJSON(finalInterface) if err != nil { - return tempArn, nil, err + return tempArn, nil, fmt.Errorf("failed to convert to JSON:\n%v", err) } } else if resourceProperties.Format == "yaml" { decryptedContent, err = toYAML(finalInterface) if err != nil { - return tempArn, nil, err + return tempArn, nil, fmt.Errorf("failed to convert to YAML:\n%v", err) } } if resourceProperties.ResourceType == "SECRET" { updateSecretResp, err := a.updateSecret(sopsHash, resourceProperties.SecretARN, decryptedContent) if err != nil { - return tempArn, nil, err + return tempArn, nil, fmt.Errorf("failed to update secret:\n%v", err) } returnData := make(map[string]interface{}) returnData["ARN"] = *updateSecretResp.ARN @@ -295,52 +299,49 @@ func (a AWS) syncSopsToSecretsmanager(ctx context.Context, event cfn.Event) (phy returnData["VersionStages"] = updateSecretResp.VersionStages returnData["VersionId"] = *updateSecretResp.VersionId return *updateSecretResp.ARN, returnData, nil - } else if resourceProperties.ResourceType == "PARAMETER" { - if resourceProperties.CreationType == "MULTI" && resourcePropertiesFlatten { - log.Printf("Patching multiple string parameters") - v := reflect.ValueOf(finalInterface) - returnData := make(map[string]interface{}) - keys := v.MapKeys() - keysOrder := func(i, j int) bool { return keys[i].Interface().(string) < keys[j].Interface().(string) } - sort.Slice(keys, keysOrder) - for _, key := range keys { - strKey := resourceProperties.ParameterKeyPrefix + key.String() - log.Printf("Parameter: " + strKey) - value := v.MapIndex(key).Interface() - strValue, ok := value.(string) - if !ok { - return tempArn, nil, nil - } - - _, err := a.updateSSMParameter(strKey, []byte(strValue), resourceProperties.EncryptionKey) - if err != nil { - return tempArn, nil, err - } - // A returnData map for each parameter is not created, because it would limit the number of possible parameters unnecessarily + } else if resourceProperties.ResourceType == "PARAMETER_MULTI" { + log.Printf("Patching multiple string parameters") + v := reflect.ValueOf(finalInterface) + returnData := make(map[string]interface{}) + keys := v.MapKeys() + keysOrder := func(i, j int) bool { return keys[i].Interface().(string) < keys[j].Interface().(string) } + sort.Slice(keys, keysOrder) + for _, key := range keys { + strKey := resourceProperties.ParameterKeyPrefix + key.String() + log.Printf("Parameter: " + strKey) + value := v.MapIndex(key).Interface() + strValue, ok := value.(string) + if !ok { + return tempArn, nil, nil } - returnData["Prefix"] = resourceProperties.ParameterKeyPrefix - returnData["Count"] = len(keys) - return tempArn, returnData, nil - } else { - log.Printf("Patching single string parameter") - response, err := a.updateSSMParameter(resourceProperties.ParameterName, decryptedContent, resourceProperties.EncryptionKey) + _, err := a.updateSSMParameter(strKey, []byte(strValue), resourceProperties.EncryptionKey) if err != nil { - return tempArn, nil, err + return tempArn, nil, fmt.Errorf("failed to update ssm parameter:\n%v", err) } - returnData := make(map[string]interface{}) - returnData["ParameterName"] = resourceProperties.ParameterName - returnData["Version"] = response.Version - returnData["Tier"] = response.Tier - return tempArn, returnData, nil + // A returnData map for each parameter is not created, because it would limit the number of possible parameters unnecessarily } + returnData["Prefix"] = resourceProperties.ParameterKeyPrefix + returnData["Count"] = len(keys) + return tempArn, returnData, nil + } else if resourceProperties.ResourceType == "PARAMETER" { + log.Printf("Patching single string parameter") + response, err := a.updateSSMParameter(resourceProperties.ParameterName, decryptedContent, resourceProperties.EncryptionKey) + if err != nil { + return tempArn, nil, fmt.Errorf("failed to update ssm parameter:\n%v", err) + } + returnData := make(map[string]interface{}) + returnData["ParameterName"] = resourceProperties.ParameterName + returnData["Version"] = response.Version + returnData["Tier"] = response.Tier + return tempArn, returnData, nil } else { // Should never happen ... - return tempArn, nil, errors.New("Neither SecretARN nor ParameterName is provided") + return tempArn, nil, fmt.Errorf("neither SecretARN nor ParameterName is provided:\n%v", err) } } else if event.RequestType == cfn.RequestDelete { return "", nil, nil } else { - return "", nil, errors.New(fmt.Sprintf("RequestType '%s' not supported", event.RequestType)) + return "", nil, fmt.Errorf("requestType '%s' not supported", event.RequestType) } } @@ -350,6 +351,7 @@ func handleRequest(ctx context.Context, event cfn.Event) (physicalResourceID str secretsmanager: secretsmanager.New(awsSession), ssm: ssm.New(awsSession), s3Downloader: s3manager.NewDownloader(awsSession), + s3Api: s3.New(awsSession), } return a.syncSopsToSecretsmanager(ctx, event) } diff --git a/scripts/lambda-build.sh b/scripts/lambda-build.sh index 7f46f771..06fda667 100755 --- a/scripts/lambda-build.sh +++ b/scripts/lambda-build.sh @@ -8,8 +8,8 @@ export GOPROXY=https://proxy.golang.org,direct export CGO_ENABLED=0 go build -trimpath -buildvcs=false -tags lambda.norpc -o bootstrap -ldflags="-s -w -buildid=" ls -la bootstrap -shasum bootstrap +sha1sum bootstrap touch -t 202002020000 bootstrap chmod 755 bootstrap ls -la bootstrap -shasum bootstrap +sha1sum bootstrap diff --git a/src/MultiStringParameter.ts b/src/MultiStringParameter.ts index cc7f886c..a2c98478 100644 --- a/src/MultiStringParameter.ts +++ b/src/MultiStringParameter.ts @@ -5,12 +5,7 @@ import { ResourceEnvironment, Stack } from 'aws-cdk-lib/core'; import { Construct } from 'constructs'; import * as YAML from 'yaml'; import { SopsStringParameterProps } from './SopsStringParameter'; -import { - CreationType, - ResourceType, - SopsSync, - SopsSyncOptions, -} from './SopsSync'; +import { ResourceType, SopsSync, SopsSyncOptions } from './SopsSync'; interface JSONObject { [key: string]: any; @@ -83,8 +78,7 @@ export class MultiStringParameter extends Construct { this.sync = new SopsSync(this, 'SopsSync', { encryptionKey: this.encryptionKey, - resourceType: ResourceType.PARAMETER, - creationType: CreationType.MULTI, + resourceType: ResourceType.PARAMETER_MULTI, flatten: true, flattenSeparator: this.keySeparator, parameterKeyPrefix: this.keyPrefix, diff --git a/src/SopsSecret.ts b/src/SopsSecret.ts index a3c8c121..ba574851 100644 --- a/src/SopsSecret.ts +++ b/src/SopsSecret.ts @@ -20,12 +20,7 @@ import { Stack, } from 'aws-cdk-lib/core'; import { Construct } from 'constructs'; -import { - CreationType, - ResourceType, - SopsSync, - SopsSyncOptions, -} from './SopsSync'; +import { ResourceType, SopsSync, SopsSyncOptions } from './SopsSync'; /** * The configuration options of the SopsSecret @@ -63,7 +58,6 @@ export class SopsSecret extends Construct implements ISecret { this.sync = new SopsSync(this, 'SopsSync', { secret: this.secret, resourceType: ResourceType.SECRET, - creationType: CreationType.SINGLE, flattenSeparator: '.', ...(props as SopsSyncOptions), }); diff --git a/src/SopsStringParameter.ts b/src/SopsStringParameter.ts index cbb3aad2..42321106 100644 --- a/src/SopsStringParameter.ts +++ b/src/SopsStringParameter.ts @@ -7,7 +7,7 @@ import { } from 'aws-cdk-lib/aws-ssm'; import { RemovalPolicy, ResourceEnvironment, Stack } from 'aws-cdk-lib/core'; import { Construct } from 'constructs'; -import { SopsSync, SopsSyncOptions } from './SopsSync'; +import { ResourceType, SopsSync, SopsSyncOptions } from './SopsSync'; /** * The configuration options of the StringParameter @@ -58,7 +58,8 @@ export class SopsStringParameter extends Construct implements IStringParameter { this.sync = new SopsSync(this, 'SopsSync', { encryptionKey: this.parameter.encryptionKey, - parameterName: this.parameter.parameterName, + parameterNames: [this.parameter.parameterName], + resourceType: ResourceType.PARAMETER, ...(props as SopsSyncOptions), }); } diff --git a/src/SopsSync.ts b/src/SopsSync.ts index 244d0976..fcffb5aa 100644 --- a/src/SopsSync.ts +++ b/src/SopsSync.ts @@ -33,20 +33,10 @@ export enum UploadType { ASSET = 'ASSET', } -export enum CreationType { - /** - * Create or update a single secret/parameter - */ - SINGLE = 'SINGLE', - /** - * Create or update a multiple secrets/parameters by flattening the SOPS file - */ - MULTI = 'MULTI', -} - export enum ResourceType { SECRET = 'SECRET', PARAMETER = 'PARAMETER', + PARAMETER_MULTI = 'PARAMETER_MULTI', } /** @@ -141,12 +131,16 @@ export interface SopsSyncOptions { */ readonly stringifyValues?: boolean; - readonly creationType?: CreationType; - readonly resourceType?: ResourceType; + /** + * Should this construct automatically create IAM permissions? + * + * @default true + */ + readonly autoGenerateIamPermissions?: boolean; } /** - * The configuration options extended by the target Secret + * The configuration options extended by the target Secret / Parameter */ export interface SopsSyncProps extends SopsSyncOptions { /** @@ -155,12 +149,7 @@ export interface SopsSyncProps extends SopsSyncOptions { readonly secret?: ISecret; /** - * The parameter name. If set this creates an encrypted SSM Parameter instead of a secret. - */ - readonly parameterName?: string; - - /** - * The parameter name. If set this creates an encrypted SSM Parameter instead of a secret. + * The parameter names. If set this creates encrypted SSM Parameters instead of a secret. */ readonly parameterNames?: string[]; @@ -168,6 +157,11 @@ export interface SopsSyncProps extends SopsSyncOptions { * The encryption key used for encrypting the ssm parameter if `parameterName` is set. */ readonly encryptionKey?: IKey; + + /** + * Will this Sync deploy a Secret or Parameter(s) + */ + readonly resourceType?: ResourceType; } /** @@ -192,6 +186,14 @@ export interface SopsSyncProviderProps { * @default - A dedicated security group will be created for the lambda function. */ readonly securityGroups?: ISecurityGroup[]; + + /** + * The role that should be used for the custom resource provider. + * If you don't specify any, a new role will be created with all required permissions + * + * @default - a new role will be created + */ + readonly role?: IRole; } export class SopsSyncProvider extends SingletonFunction implements IGrantable { @@ -206,6 +208,7 @@ export class SopsSyncProvider extends SingletonFunction implements IGrantable { runtime: Runtime.PROVIDED_AL2, handler: 'bootstrap', uuid: 'SopsSyncProvider', + role: props?.role, timeout: Duration.seconds(60), environment: { SOPS_AGE_KEY: Lazy.string({ @@ -268,8 +271,11 @@ export class SopsSync extends Construct { let sopsS3File: { Bucket: string; Key: string } | undefined = undefined; if ( - props.sopsFilePath !== undefined && - (props.sopsS3Bucket !== undefined || props.sopsS3Key !== undefined) + (props.sopsFilePath == undefined && + (props.sopsS3Bucket == undefined || props.sopsS3Key == undefined)) || + (props.sopsFilePath !== undefined && + props.sopsS3Bucket !== undefined && + props.sopsS3Key !== undefined) ) { throw new Error( 'You can either specify sopsFilePath or sopsS3Bucket and sopsS3Key!', @@ -312,11 +318,12 @@ export class SopsSync extends Construct { if (!fs.existsSync(props.sopsFilePath)) { throw new Error(`File ${props.sopsFilePath} does not exist!`); } + const sopsFileContent = fs.readFileSync(props.sopsFilePath); switch (uploadType) { case UploadType.INLINE: { sopsInline = { - Content: fs.readFileSync(props.sopsFilePath).toString('base64'), + Content: sopsFileContent.toString('base64'), // We calculate the hash the same way as it would be done by new Asset(..) - so we can ensure stable version names even if switching from INLINE to ASSET and viceversa. Hash: FileSystem.fingerprint(props.sopsFilePath), }; @@ -334,71 +341,29 @@ export class SopsSync extends Construct { } } - if (provider.role !== undefined) { - if (props.sopsKmsKey !== undefined) { - props.sopsKmsKey.forEach((key) => key.grantDecrypt(provider.role!)); - } - const fileContent = fs.readFileSync(props.sopsFilePath); - // Handle keys - const regexKey = /arn:aws:kms:[a-z0-9-]+:[\d]+:key\/[a-z0-9-]+/g; - const resultsKey = fileContent.toString().match(regexKey); - if (resultsKey !== undefined) { - resultsKey?.forEach((result, index) => - Key.fromKeyArn(this, `SopsKey${index}`, result).grantDecrypt( - provider.role!, - ), - ); - } - const regexAlias = /arn:aws:kms:[a-z0-9-]+:[\d]+:alias\/[a-z0-9-]+/g; - const resultsAlias = fileContent.toString().match(regexAlias); - if (resultsAlias !== undefined) { - resultsAlias?.forEach((result, index) => - Key.fromLookup(this, `SopsAlias${index}`, { - aliasName: `alias/${result.split('/').slice(1).join('/')}`, - }).grantDecrypt(provider.role!), - ); - } - if (props.secret) { - props.secret.grantWrite(provider); - props.secret.encryptionKey?.grantEncryptDecrypt(provider); - if (props.secret?.encryptionKey !== undefined) { - props.secret.encryptionKey.grantEncryptDecrypt(provider); - } - } - if (props.parameterName) { - provider.addToRolePolicy( - new PolicyStatement({ - actions: ['ssm:PutParameter'], - resources: [ - `arn:aws:ssm:${Stack.of(this).region}:${ - Stack.of(this).account - }:parameter${ - props.parameterName.startsWith('/') - ? props.parameterName - : `/${props.parameterName}` - }`, - ], - }), - ); - props.encryptionKey?.grantEncryptDecrypt(provider); - } - if (props.parameterNames) { - this.createReducedParameterPolicy( - props.parameterNames, - provider.role, - ); - props.encryptionKey?.grantEncryptDecrypt(provider); - } - if (sopsAsset !== undefined) { - sopsAsset.bucket.grantRead(provider); - } + if ( + // Is allways true, but to satisfy TS we check explicitly + provider.role !== undefined && + // Check if user has disabled automatic generation + props.autoGenerateIamPermissions !== false + ) { + Permissions.sopsKeys(this, { + userDefinedKeys: props.sopsKmsKey, + role: provider.role, + sopsFileContent: sopsFileContent.toString(), + }); + Permissions.assetBucket(sopsAsset, provider.role); + Permissions.encryptionKey(props.encryptionKey, provider.role); + Permissions.secret(props.secret, provider.role); + Permissions.parameters(this, props.parameterNames, provider.role); } else { Annotations.of(this).addWarning( - `Please ensure proper permissions for the passed lambda function:\n - write Access to the secret\n - encrypt with the sopsKmsKey${ - uploadType === UploadType.ASSET - ? '\n - download from asset bucket' - : '' - }`, + [ + 'Please ensure proper permissions for the passed lambda function:', + ' - write Access to the secret/parameters', + ' - encrypt with the sopsKmsKey', + ' - download from asset bucket', + ].join('\n'), ); } if (props.sopsAgeKey !== undefined) { @@ -416,10 +381,6 @@ export class SopsSync extends Construct { Annotations.of(this).addWarning( 'You have to manually add permissions to the sops provider to (permission to download file, to decrypt sops file)!', ); - } else { - throw new Error( - 'You have to specify both sopsS3Bucket and sopsS3Key or neither!', - ); } if (sopsFileFormat === undefined) { @@ -439,90 +400,169 @@ export class SopsSync extends Construct { ParameterKeyPrefix: props.parameterKeyPrefix, Format: sopsFileFormat, StringifiedValues: this.stringifiedValues, - ParameterName: props.parameterName, + ParameterName: + // Dirty Workaround, refactor ... + props.parameterNames && props.parameterNames.length == 1 + ? props.parameterNames[0] + : undefined, EncryptionKey: props.secret !== undefined ? undefined : props.encryptionKey?.keyId, ResourceType: props.resourceType ? props.resourceType.toString() : ResourceType.SECRET.toString(), - CreationType: props.creationType - ? props.creationType.toString() - : CreationType.SINGLE.toString(), }, }); this.versionId = cr.getAttString('VersionId'); } +} - private createReducedParameterPolicy(parameters: string[], role: IRole) { - // Avoid too large policies - // The maximum size of a managed policy is 6.144 bytes -> 1 character = 1 byte - const maxPolicyBytes = 6000; // Keep some bytes as a buffer - const arnPrefixBytes = 55; // Content for "arn:aws:ssm:ap-southeast-3::parameter/ - let startAtParameter = 0; - let currentPolicyBytes = 300; // Reserve some byte space for basic stuff inside the policy - for (let i = 0; i < parameters.length; i += 1) { - if ( - // Check if the current parameter would fit into the policy - arnPrefixBytes + parameters[i].length + currentPolicyBytes < - maxPolicyBytes - ) { - // If so increase the byte counter - currentPolicyBytes = - arnPrefixBytes + parameters[i].length + currentPolicyBytes; - } else { - const parameterNamesChunk = parameters.slice( - startAtParameter, - i, //end of slice is not included - ); - startAtParameter = i; - currentPolicyBytes = 300; - // Create the policy for the selected chunk - const putPolicy = new ManagedPolicy( - this, - `SopsSecretParameterProviderManagedPolicyParameterAccess${i}`, - { - description: - 'Policy to grant parameter provider permissions to put parameter', - }, - ); - putPolicy.addStatements( - new PolicyStatement({ - actions: ['ssm:PutParameter'], - resources: parameterNamesChunk.map( - (param) => - `arn:aws:ssm:${Stack.of(this).region}:${ - Stack.of(this).account - }:parameter${param.startsWith('/') ? param : `/${param}`}`, - ), - }), - ); - role.addManagedPolicy(putPolicy); +export namespace Permissions { + /** + * Grants the necessary permissions for encrypt/decrypt on the customer managed encryption key + * for the secrets / parameters. + */ + export function encryptionKey(key: IKey | undefined, target: IGrantable) { + if (key === undefined) { + return; + } + key.grantEncryptDecrypt(target); + } + + export function keysFromSopsContent(ctx: Construct, c: string): IKey[] { + const regexKey = /arn:aws:kms:[a-z0-9-]+:[\d]+:key\/[a-z0-9-]+/g; + const resultsKey = c.match(regexKey); + if (resultsKey !== null) { + return resultsKey.map((result, index) => + Key.fromKeyArn(ctx, `SopsKey${index}`, result), + ); + } + return []; + } + + export function keysFromSopsContentAlias(ctx: Construct, c: string): IKey[] { + const regexAlias = /arn:aws:kms:[a-z0-9-]+:[\d]+:alias\/[a-z0-9-]+/g; + const resultsAlias = c.match(regexAlias); + if (resultsAlias !== null) { + return resultsAlias.map((result, index) => + Key.fromLookup(ctx, `SopsAlias${index}`, { + aliasName: `alias/${result.split('/').slice(1).join('/')}`, + }), + ); + } + return []; + } + + /** + * Grants the necessary permissions to decrypt the given sops file content. + * Takes user defined keys, and searches the sops file for keys and aliases. + */ + export function sopsKeys( + ctx: Construct, + props: { + userDefinedKeys?: IKey[]; + sopsFileContent: string; + role: IRole; + }, + ) { + (props.userDefinedKeys ?? []) + .concat( + keysFromSopsContent(ctx, props.sopsFileContent), + keysFromSopsContentAlias(ctx, props.sopsFileContent), + ) + .forEach((key) => key.grantDecrypt(props.role)); + } + + /** + * Grants the necessary permissions to write the given secrets. + */ + export function secret( + targetSecret: ISecret | undefined, + target: IGrantable, + ) { + if (targetSecret === undefined) { + return; + } + targetSecret.grantWrite(target); + } + + function sliceParameters(params: string[]): string[][] { + const result: string[][] = []; + /** + * The maximum size of a managed policy is 6.144 bytes -> 1 character = 1 byte + * bout 300 characters are reserved for the policy apart from resource arns + * with some buffer, we end with an upper limit of 5750 bytes + */ + const limit = 5750; + + /** + * Content for "arn:aws:ssm:ap-southeast-3::parameter/ + */ + const prefix = 55; + + let currentSize = 0; + let currentChunk: string[] = []; + for (const param of params) { + const paramLength = param.length + prefix; + if (currentSize + paramLength > limit) { + result.push(currentChunk); + currentChunk = []; + currentSize = 0; } + currentChunk.push(param); + currentSize += paramLength; } - const parameterNamesChunk = parameters.slice( - startAtParameter, - parameters.length, - ); - // Create the policy for the remaning elements - const putPolicy = new ManagedPolicy( - this, - `SopsSecretParameterProviderManagedPolicyParameterAccess${parameters.length}`, - { - description: - 'Policy to grant parameter provider permissions to put parameter', - }, - ); - putPolicy.addStatements( - new PolicyStatement({ - actions: ['ssm:PutParameter'], - resources: parameterNamesChunk.map( - (param) => - `arn:aws:ssm:${Stack.of(this).region}:${ - Stack.of(this).account - }:parameter${param.startsWith('/') ? param : `/${param}`}`, - ), - }), - ); - role.addManagedPolicy(putPolicy); + + if (currentChunk.length > 0) { + result.push(currentChunk); + } + return result; + } + + /** + * Grants the necessary permissions to write the given parameters. + */ + export function parameters( + ctx: Construct, + targetParameters: string[] | undefined, + role: IRole, + ) { + if (targetParameters === undefined) { + return; + } + + const paramSlices = sliceParameters(targetParameters); + + for (let i = 0; i < paramSlices.length; i++) { + const putPolicy = new ManagedPolicy( + ctx, + `SopsSecretParameterProviderManagedPolicyParameterAccess${i}`, + { + description: + 'Policy to grant parameter provider permissions to put parameter', + }, + ); + putPolicy.addStatements( + new PolicyStatement({ + actions: ['ssm:PutParameter'], + resources: paramSlices[i].map( + (param) => + `arn:aws:ssm:${Stack.of(ctx).region}:${ + Stack.of(ctx).account + }:parameter${param.startsWith('/') ? param : `/${param}`}`, + ), + }), + ); + role.addManagedPolicy(putPolicy); + } + } + + /** + * Grants the necessary permissions to read the given asset from S3. + */ + export function assetBucket(asset: Asset | undefined, target: IGrantable) { + if (asset === undefined) { + return; + } + asset.bucket.grantRead(target); } } diff --git a/test/__snapshots__/permissions.test.ts.snap b/test/__snapshots__/permissions.test.ts.snap new file mode 100644 index 00000000..3c64327c --- /dev/null +++ b/test/__snapshots__/permissions.test.ts.snap @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`parameters 1 parameter - full snapshot 1`] = ` +Object { + "SopsSecretParameterProviderManagedPolicyParameterAccess03C7CE5D3": Object { + "Properties": Object { + "Description": "Policy to grant parameter provider permissions to put parameter", + "Path": "/", + "PolicyDocument": Object { + "Statement": Array [ + Object { + "Action": "ssm:PutParameter", + "Effect": "Allow", + "Resource": Object { + "Fn::Join": Array [ + "", + Array [ + "arn:aws:ssm:", + Object { + "Ref": "AWS::Region", + }, + ":", + Object { + "Ref": "AWS::AccountId", + }, + ":parameter/parameter0", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + }, + "Type": "AWS::IAM::ManagedPolicy", + }, +} +`; + +exports[`parameters 100 parameter - snapshot count 1`] = ` +Array [ + "SopsSecretParameterProviderManagedPolicyParameterAccess03C7CE5D3", + "SopsSecretParameterProviderManagedPolicyParameterAccess1E27103A2", +] +`; + +exports[`parameters 1000 parameter - snapshot count 1`] = ` +Array [ + "SopsSecretParameterProviderManagedPolicyParameterAccess03C7CE5D3", + "SopsSecretParameterProviderManagedPolicyParameterAccess1E27103A2", + "SopsSecretParameterProviderManagedPolicyParameterAccess204C56AEE", + "SopsSecretParameterProviderManagedPolicyParameterAccess39C2C7238", + "SopsSecretParameterProviderManagedPolicyParameterAccess4810168FC", + "SopsSecretParameterProviderManagedPolicyParameterAccess5A4020F4D", + "SopsSecretParameterProviderManagedPolicyParameterAccess6D18916A5", + "SopsSecretParameterProviderManagedPolicyParameterAccess7C1D2D5DD", + "SopsSecretParameterProviderManagedPolicyParameterAccess881B83B3B", + "SopsSecretParameterProviderManagedPolicyParameterAccess9F6C18C5F", + "SopsSecretParameterProviderManagedPolicyParameterAccess10B80D3BA8", + "SopsSecretParameterProviderManagedPolicyParameterAccess118D826D5D", +] +`; diff --git a/test/permissions.test.ts b/test/permissions.test.ts new file mode 100644 index 00000000..e10ab9f1 --- /dev/null +++ b/test/permissions.test.ts @@ -0,0 +1,151 @@ +import * as fs from 'fs'; +import path from 'path'; +import { Stack } from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import { IRole, Role } from 'aws-cdk-lib/aws-iam'; +import { Key } from 'aws-cdk-lib/aws-kms'; +import { Permissions } from '../src/SopsSync'; + +describe('keysFromSopsContent', () => { + let stack: Stack; + + beforeEach(() => { + stack = new Stack(); + }); + + test('returns an empty array when no keys are found', () => { + const content = 'no keys here'; + const result = Permissions.keysFromSopsContent(stack, content); + expect(result).toEqual([]); + }); + + test('returns an array of IKey objects when keys are found', () => { + const content = fs + .readFileSync( + path.join(__dirname, '../test-secrets/yaml/sopsfile.enc-kms.yaml'), + ) + .toString(); + const result = Permissions.keysFromSopsContent(stack, content); + expect(result).toHaveLength(1); + expect(result[0].keyArn).toBe( + 'arn:aws:kms:aws-region-1:123456789011:key/00000000-1234-4321-abcd-1234abcd12ab', + ); + expect(result[0].keyId).toBe('00000000-1234-4321-abcd-1234abcd12ab'); + }); + + test('returns multiple IKey objects when multiple keys are found', () => { + const content = fs + .readFileSync( + path.join(__dirname, '../test-secrets/yaml/sopsfile.enc-multikms.yaml'), + ) + .toString(); + const result = Permissions.keysFromSopsContent(stack, content); + expect(result).toHaveLength(4); + expect(result[0].keyArn).toBe( + 'arn:aws:kms:aws-region-1:123456789011:key/00000000-1234-4321-abcd-1234abcd12ab', + ); + expect(result[0].keyId).toBe('00000000-1234-4321-abcd-1234abcd12ab'); + expect(result[1].keyArn).toBe( + 'arn:aws:kms:aws-region-1:123456789011:key/00000001-1234-4321-abcd-1234abcd12ab', + ); + expect(result[1].keyId).toBe('00000001-1234-4321-abcd-1234abcd12ab'); + expect(result[2].keyArn).toBe( + 'arn:aws:kms:aws-region-1:123456789011:key/00000002-1234-4321-abcd-1234abcd12ab', + ); + expect(result[2].keyId).toBe('00000002-1234-4321-abcd-1234abcd12ab'); + expect(result[3].keyArn).toBe( + 'arn:aws:kms:aws-region-1:123456789011:key/00000003-1234-4321-abcd-1234abcd12ab', + ); + expect(result[3].keyId).toBe('00000003-1234-4321-abcd-1234abcd12ab'); + }); +}); + +describe('keysFromSopsContentAlias', () => { + let stack: Stack; + + beforeEach(() => { + stack = new Stack(); + }); + + test('returns an empty array when no keys are found', () => { + const content = 'no keys here'; + const result = Permissions.keysFromSopsContentAlias(stack, content); + expect(result).toEqual([]); + }); + + test('returns an array of IKey objects when keys are found', () => { + jest + .spyOn(Key, 'fromLookup') + .mockReturnValue( + Key.fromKeyArn( + stack, + 'Key', + 'arn:aws:kms:aws-region-1:123456789011:key/00000000-1234-4321-abcd-1234abcd12ab', + ), + ); + + const content = fs + .readFileSync( + path.join( + __dirname, + '../test-secrets/yaml/sopsfile.enc-kms-alias.yaml', + ), + ) + .toString(); + const result = Permissions.keysFromSopsContentAlias(stack, content); + expect(result[0].keyArn).toBe( + 'arn:aws:kms:aws-region-1:123456789011:key/00000000-1234-4321-abcd-1234abcd12ab', + ); + expect(result[0].keyId).toBe('00000000-1234-4321-abcd-1234abcd12ab'); + }); +}); + +describe('parameters', () => { + let stack: Stack; + let role: IRole; + + beforeEach(() => { + stack = new Stack(); + role = Role.fromRoleArn( + stack, + 'TestRole', + 'arn:aws:iam::123456789012:role/test-role', + ); + }); + + function genParameters(prefix: string, count: number) { + const params: string[] = []; + for (let i = 0; i < count; i++) { + params.push(`${prefix}${i}`); + } + return params; + } + + test('1 parameter - full snapshot', () => { + const params = genParameters('parameter', 1); + Permissions.parameters(stack, params, role); + expect( + Template.fromStack(stack).findResources('AWS::IAM::ManagedPolicy'), + ).toMatchSnapshot(); + }); + + test('100 parameter - snapshot count', () => { + const params = genParameters('parameter', 100); + Permissions.parameters(stack, params, role); + expect( + Object.keys( + Template.fromStack(stack).findResources('AWS::IAM::ManagedPolicy'), + ), + ).toMatchSnapshot(); + }); + + test('1000 parameter - snapshot count', () => { + const params = genParameters('parameter', 1000); + Permissions.parameters(stack, params, role); + expect( + Object.keys( + Template.fromStack(stack).findResources('AWS::IAM::ManagedPolicy'), + ), + ).toMatchSnapshot(); + }); +}); diff --git a/test/secret-asset.integ.snapshot/SecretIntegrationAsset.assets.json b/test/secret-asset.integ.snapshot/SecretIntegrationAsset.assets.json index a5efac8f..c412a32a 100644 --- a/test/secret-asset.integ.snapshot/SecretIntegrationAsset.assets.json +++ b/test/secret-asset.integ.snapshot/SecretIntegrationAsset.assets.json @@ -1,15 +1,15 @@ { "version": "36.0.0", "files": { - "b84f4ae044484433485a09ba2d15bdd1cd134698fecd0af024e8a36b2c001d92": { + "2c430c0ffff3fd5da4b853228c31f277ebb757a3e9a3f5e11321eb0cd8c91887": { "source": { - "path": "asset.b84f4ae044484433485a09ba2d15bdd1cd134698fecd0af024e8a36b2c001d92.zip", + "path": "asset.2c430c0ffff3fd5da4b853228c31f277ebb757a3e9a3f5e11321eb0cd8c91887.zip", "packaging": "file" }, "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "b84f4ae044484433485a09ba2d15bdd1cd134698fecd0af024e8a36b2c001d92.zip", + "objectKey": "2c430c0ffff3fd5da4b853228c31f277ebb757a3e9a3f5e11321eb0cd8c91887.zip", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } @@ -79,7 +79,7 @@ } } }, - "e363e1c42928a35c199fd17c68fdac2df3407feb9216f8cdf1511a609a2b0f72": { + "403db9f2066ea2fc6a72ec5211cda6589c0049fadd0dd5ea36461a810960d709": { "source": { "path": "SecretIntegrationAsset.template.json", "packaging": "file" @@ -87,7 +87,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "e363e1c42928a35c199fd17c68fdac2df3407feb9216f8cdf1511a609a2b0f72.json", + "objectKey": "403db9f2066ea2fc6a72ec5211cda6589c0049fadd0dd5ea36461a810960d709.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/test/secret-asset.integ.snapshot/SecretIntegrationAsset.template.json b/test/secret-asset.integ.snapshot/SecretIntegrationAsset.template.json index 40e8b4df..410ea70d 100644 --- a/test/secret-asset.integ.snapshot/SecretIntegrationAsset.template.json +++ b/test/secret-asset.integ.snapshot/SecretIntegrationAsset.template.json @@ -31,8 +31,7 @@ "FlattenSeparator": ".", "Format": "json", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -73,16 +72,6 @@ "Properties": { "PolicyDocument": { "Statement": [ - { - "Action": [ - "secretsmanager:PutSecretValue", - "secretsmanager:UpdateSecret" - ], - "Effect": "Allow", - "Resource": { - "Ref": "SopsSecretJSON72040543" - } - }, { "Action": [ "s3:GetObject*", @@ -124,6 +113,16 @@ } ] }, + { + "Action": [ + "secretsmanager:PutSecretValue", + "secretsmanager:UpdateSecret" + ], + "Effect": "Allow", + "Resource": { + "Ref": "SopsSecretJSON72040543" + } + }, { "Action": [ "secretsmanager:PutSecretValue", @@ -232,7 +231,7 @@ "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": "b84f4ae044484433485a09ba2d15bdd1cd134698fecd0af024e8a36b2c001d92.zip" + "S3Key": "2c430c0ffff3fd5da4b853228c31f277ebb757a3e9a3f5e11321eb0cd8c91887.zip" }, "Environment": { "Variables": { @@ -285,8 +284,7 @@ "FlattenSeparator": ".", "Format": "yaml", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -322,8 +320,7 @@ "FlattenSeparator": ".", "Format": "yaml", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -359,8 +356,7 @@ "FlattenSeparator": ".", "Format": "json", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -396,8 +392,7 @@ "FlattenSeparator": ".", "Format": "json", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -433,8 +428,7 @@ "FlattenSeparator": ".", "Format": "yaml", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -470,8 +464,7 @@ "FlattenSeparator": ".", "Format": "yaml", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -507,8 +500,7 @@ "FlattenSeparator": ".", "Format": "yaml", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -544,8 +536,7 @@ "FlattenSeparator": ".", "Format": "yaml", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -581,8 +572,7 @@ "FlattenSeparator": ".", "Format": "binary", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" diff --git a/test/secret-inline.integ.snapshot/SecretIntegrationInline.assets.json b/test/secret-inline.integ.snapshot/SecretIntegrationInline.assets.json index f17b8c6a..cdd252e3 100644 --- a/test/secret-inline.integ.snapshot/SecretIntegrationInline.assets.json +++ b/test/secret-inline.integ.snapshot/SecretIntegrationInline.assets.json @@ -1,20 +1,20 @@ { "version": "36.0.0", "files": { - "b84f4ae044484433485a09ba2d15bdd1cd134698fecd0af024e8a36b2c001d92": { + "2c430c0ffff3fd5da4b853228c31f277ebb757a3e9a3f5e11321eb0cd8c91887": { "source": { - "path": "asset.b84f4ae044484433485a09ba2d15bdd1cd134698fecd0af024e8a36b2c001d92.zip", + "path": "asset.2c430c0ffff3fd5da4b853228c31f277ebb757a3e9a3f5e11321eb0cd8c91887.zip", "packaging": "file" }, "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "b84f4ae044484433485a09ba2d15bdd1cd134698fecd0af024e8a36b2c001d92.zip", + "objectKey": "2c430c0ffff3fd5da4b853228c31f277ebb757a3e9a3f5e11321eb0cd8c91887.zip", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } }, - "3dcfa0a64e513705636a673bbca1b4c1ca9804d58579498b3479f581295035fa": { + "55aa34bad424010bf095d40fbfbbd4b7250f7c373c25a117c203aa788214e7a3": { "source": { "path": "SecretIntegrationInline.template.json", "packaging": "file" @@ -22,7 +22,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "3dcfa0a64e513705636a673bbca1b4c1ca9804d58579498b3479f581295035fa.json", + "objectKey": "55aa34bad424010bf095d40fbfbbd4b7250f7c373c25a117c203aa788214e7a3.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/test/secret-inline.integ.snapshot/SecretIntegrationInline.template.json b/test/secret-inline.integ.snapshot/SecretIntegrationInline.template.json index 5ff61693..827a6514 100644 --- a/test/secret-inline.integ.snapshot/SecretIntegrationInline.template.json +++ b/test/secret-inline.integ.snapshot/SecretIntegrationInline.template.json @@ -29,8 +29,7 @@ "FlattenSeparator": ".", "Format": "json", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -199,7 +198,7 @@ "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": "b84f4ae044484433485a09ba2d15bdd1cd134698fecd0af024e8a36b2c001d92.zip" + "S3Key": "2c430c0ffff3fd5da4b853228c31f277ebb757a3e9a3f5e11321eb0cd8c91887.zip" }, "Environment": { "Variables": { @@ -250,8 +249,7 @@ "FlattenSeparator": ".", "Format": "yaml", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -285,8 +283,7 @@ "FlattenSeparator": ".", "Format": "yaml", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -320,8 +317,7 @@ "FlattenSeparator": ".", "Format": "dotenv", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -355,8 +351,7 @@ "FlattenSeparator": ".", "Format": "dotenv", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -390,8 +385,7 @@ "FlattenSeparator": ".", "Format": "json", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -425,8 +419,7 @@ "FlattenSeparator": ".", "Format": "json", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -460,8 +453,7 @@ "FlattenSeparator": ".", "Format": "yaml", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -495,8 +487,7 @@ "FlattenSeparator": ".", "Format": "yaml", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -530,8 +521,7 @@ "FlattenSeparator": ".", "Format": "yaml", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -565,8 +555,7 @@ "FlattenSeparator": ".", "Format": "yaml", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" diff --git a/test/secret-manual.integ.snapshot/SecretIntegrationAsset.assets.json b/test/secret-manual.integ.snapshot/SecretIntegrationAsset.assets.json index 3aa36317..d52b2917 100644 --- a/test/secret-manual.integ.snapshot/SecretIntegrationAsset.assets.json +++ b/test/secret-manual.integ.snapshot/SecretIntegrationAsset.assets.json @@ -1,20 +1,20 @@ { "version": "36.0.0", "files": { - "b84f4ae044484433485a09ba2d15bdd1cd134698fecd0af024e8a36b2c001d92": { + "2c430c0ffff3fd5da4b853228c31f277ebb757a3e9a3f5e11321eb0cd8c91887": { "source": { - "path": "asset.b84f4ae044484433485a09ba2d15bdd1cd134698fecd0af024e8a36b2c001d92.zip", + "path": "asset.2c430c0ffff3fd5da4b853228c31f277ebb757a3e9a3f5e11321eb0cd8c91887.zip", "packaging": "file" }, "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "b84f4ae044484433485a09ba2d15bdd1cd134698fecd0af024e8a36b2c001d92.zip", + "objectKey": "2c430c0ffff3fd5da4b853228c31f277ebb757a3e9a3f5e11321eb0cd8c91887.zip", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } }, - "d4e0b1978d4b4a8bd4a3460f5250588a128ba18a716657ee533bbc19a93d6ce7": { + "d991538757fc85d0c7183933598d0517ea224a4d379c5cc40388ef064f255f29": { "source": { "path": "SecretIntegrationAsset.template.json", "packaging": "file" @@ -22,7 +22,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "d4e0b1978d4b4a8bd4a3460f5250588a128ba18a716657ee533bbc19a93d6ce7.json", + "objectKey": "d991538757fc85d0c7183933598d0517ea224a4d379c5cc40388ef064f255f29.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/test/secret-manual.integ.snapshot/SecretIntegrationAsset.template.json b/test/secret-manual.integ.snapshot/SecretIntegrationAsset.template.json index 059453d4..32e01b29 100644 --- a/test/secret-manual.integ.snapshot/SecretIntegrationAsset.template.json +++ b/test/secret-manual.integ.snapshot/SecretIntegrationAsset.template.json @@ -29,8 +29,7 @@ "FlattenSeparator": ".", "Format": "json", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -73,7 +72,7 @@ "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": "b84f4ae044484433485a09ba2d15bdd1cd134698fecd0af024e8a36b2c001d92.zip" + "S3Key": "2c430c0ffff3fd5da4b853228c31f277ebb757a3e9a3f5e11321eb0cd8c91887.zip" }, "Environment": { "Variables": { @@ -123,8 +122,7 @@ "FlattenSeparator": ".", "Format": "json", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -158,8 +156,7 @@ "FlattenSeparator": ".", "Format": "yaml", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -193,8 +190,7 @@ "FlattenSeparator": ".", "Format": "json", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -228,8 +224,7 @@ "FlattenSeparator": ".", "Format": "json", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -263,8 +258,7 @@ "FlattenSeparator": ".", "Format": "yaml", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -298,8 +292,7 @@ "FlattenSeparator": ".", "Format": "yaml", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -333,8 +326,7 @@ "FlattenSeparator": ".", "Format": "yaml", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -368,8 +360,7 @@ "FlattenSeparator": ".", "Format": "yaml", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" diff --git a/test/secret-multikms.integ.snapshot/SecretMultiKms.assets.json b/test/secret-multikms.integ.snapshot/SecretMultiKms.assets.json index 735fd0c3..86fe4c53 100644 --- a/test/secret-multikms.integ.snapshot/SecretMultiKms.assets.json +++ b/test/secret-multikms.integ.snapshot/SecretMultiKms.assets.json @@ -1,15 +1,15 @@ { "version": "36.0.0", "files": { - "b84f4ae044484433485a09ba2d15bdd1cd134698fecd0af024e8a36b2c001d92": { + "2c430c0ffff3fd5da4b853228c31f277ebb757a3e9a3f5e11321eb0cd8c91887": { "source": { - "path": "asset.b84f4ae044484433485a09ba2d15bdd1cd134698fecd0af024e8a36b2c001d92.zip", + "path": "asset.2c430c0ffff3fd5da4b853228c31f277ebb757a3e9a3f5e11321eb0cd8c91887.zip", "packaging": "file" }, "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "b84f4ae044484433485a09ba2d15bdd1cd134698fecd0af024e8a36b2c001d92.zip", + "objectKey": "2c430c0ffff3fd5da4b853228c31f277ebb757a3e9a3f5e11321eb0cd8c91887.zip", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } @@ -27,7 +27,7 @@ } } }, - "b2891d8d1afc3baddc99715abe47f05ff84ad390166c5277a91fc50103fc4457": { + "6bf23bfa8222ed50eef10cf00cfb221343b0f056159410f55f8a28dc7c92b9b6": { "source": { "path": "SecretMultiKms.template.json", "packaging": "file" @@ -35,7 +35,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "b2891d8d1afc3baddc99715abe47f05ff84ad390166c5277a91fc50103fc4457.json", + "objectKey": "6bf23bfa8222ed50eef10cf00cfb221343b0f056159410f55f8a28dc7c92b9b6.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/test/secret-multikms.integ.snapshot/SecretMultiKms.template.json b/test/secret-multikms.integ.snapshot/SecretMultiKms.template.json index 9a9960ee..1b9cd2fb 100644 --- a/test/secret-multikms.integ.snapshot/SecretMultiKms.template.json +++ b/test/secret-multikms.integ.snapshot/SecretMultiKms.template.json @@ -191,8 +191,7 @@ "FlattenSeparator": ".", "Format": "json", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" @@ -233,31 +232,6 @@ "Properties": { "PolicyDocument": { "Statement": [ - { - "Action": [ - "secretsmanager:PutSecretValue", - "secretsmanager:UpdateSecret" - ], - "Effect": "Allow", - "Resource": { - "Ref": "SopsSecretOwnKmsMey0B320436" - } - }, - { - "Action": [ - "kms:Decrypt", - "kms:Encrypt", - "kms:ReEncrypt*", - "kms:GenerateDataKey*" - ], - "Effect": "Allow", - "Resource": { - "Fn::GetAtt": [ - "CustomKey1E6D0D07", - "Arn" - ] - } - }, { "Action": [ "s3:GetObject*", @@ -299,6 +273,21 @@ } ] }, + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:ReEncrypt*", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "CustomKey1E6D0D07", + "Arn" + ] + } + }, { "Action": [ "secretsmanager:PutSecretValue", @@ -306,7 +295,7 @@ ], "Effect": "Allow", "Resource": { - "Ref": "SopsSecretForeignKmsMey8C3BA0B7" + "Ref": "SopsSecretOwnKmsMey0B320436" } }, { @@ -318,6 +307,16 @@ ], "Effect": "Allow", "Resource": "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012" + }, + { + "Action": [ + "secretsmanager:PutSecretValue", + "secretsmanager:UpdateSecret" + ], + "Effect": "Allow", + "Resource": { + "Ref": "SopsSecretForeignKmsMey8C3BA0B7" + } } ], "Version": "2012-10-17" @@ -337,7 +336,7 @@ "S3Bucket": { "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" }, - "S3Key": "b84f4ae044484433485a09ba2d15bdd1cd134698fecd0af024e8a36b2c001d92.zip" + "S3Key": "2c430c0ffff3fd5da4b853228c31f277ebb757a3e9a3f5e11321eb0cd8c91887.zip" }, "Environment": { "Variables": { @@ -391,8 +390,7 @@ "FlattenSeparator": ".", "Format": "json", "StringifiedValues": true, - "ResourceType": "SECRET", - "CreationType": "SINGLE" + "ResourceType": "SECRET" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" diff --git a/test/secret.test.ts b/test/secret.test.ts index 7797589e..92cfcdcd 100644 --- a/test/secret.test.ts +++ b/test/secret.test.ts @@ -506,7 +506,7 @@ test('Allowed options for SopsSync', () => { sopsS3Key: 'test', }), ).toThrowError( - 'You have to specify both sopsS3Bucket and sopsS3Key or neither!', + 'You can either specify sopsFilePath or sopsS3Bucket and sopsS3Key!', ); expect( () => @@ -514,7 +514,7 @@ test('Allowed options for SopsSync', () => { sopsS3Bucket: 'test', }), ).toThrowError( - 'You have to specify both sopsS3Bucket and sopsS3Key or neither!', + 'You can either specify sopsFilePath or sopsS3Bucket and sopsS3Key!', ); expect( () =>