Skip to content

Commit

Permalink
Add singleValueMapping option (#115)
Browse files Browse the repository at this point in the history
  • Loading branch information
plumdog authored Nov 14, 2023
1 parent 2d52167 commit 36a0a0d
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 9 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,11 @@ if(ssm.secret) {
- `kmsKey` - optional
- must be a `kms.IKey`
- the sops file contains a reference to the KMS key, so probably not actually needed
- `mappings` and `wholeFile` - must set `mappings` or set `wholeFile` to `true`
- `mappings`, `wholeFile` and `singleValueMapping` - must set `mappings` or `singleValueMapping` or set `wholeFile` to `true`
- if `mappings`, must be a `SopsSecretsManagerMappings`
- which determines how the values from the sops file are mapped to keys in the secret (see below)
- if `singleValueMapping`, must be a `SopsSecretsManagerMapping`
- which determines how a single value from the sops file is mapped to the text value of the secret
- if `wholeFile` is true
- then rather than treating the sops data as structured and mapping keys over, the whole file will be decrypted and stored as the body of the secret
- `fileType` - optional
Expand Down
18 changes: 14 additions & 4 deletions cdkv1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,19 @@ export class SopsSecretsManager extends cdk.Construct {
}
this.asset = this.getAsset(props.asset, props.path);

if (props.wholeFile && props.mappings) {
throw new Error('Cannot set mappings and set wholeFile to true');
} else if (!props.wholeFile && !props.mappings) {
throw new Error('Must set mappings or set wholeFile to true');
const mutuallyExclusiveProps: Record<string, boolean> = {
wholeFile: !!props.wholeFile,
mappings: !!props.mappings,
singleValueMapping: !!props.singleValueMapping,
}

const mutuallyExclusivePropsEnabled = Object.keys(mutuallyExclusiveProps).filter((key) => mutuallyExclusiveProps[key]);
if (mutuallyExclusivePropsEnabled.length > 1) {
throw new Error(`Cannot set more than one of ${mutuallyExclusivePropsEnabled.join(', ')}`);
}

if (mutuallyExclusivePropsEnabled.length === 0) {
throw new Error(`Must set one of ${Object.keys(mutuallyExclusiveProps).join(', ')}`);
}

new cfn.CustomResource(this, 'Resource', {
Expand All @@ -87,6 +96,7 @@ export class SopsSecretsManager extends cdk.Construct {
SourceHash: this.asset.sourceHash,
KMSKeyArn: props.kmsKey?.keyArn,
Mappings: JSON.stringify(props.mappings || {}),
SingleValueMapping: JSON.stringify(props.singleValueMapping || null),
WholeFile: props.wholeFile || false,
FileType: props.fileType,
},
Expand Down
18 changes: 14 additions & 4 deletions cdkv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,19 @@ export class SopsSecretsManager extends constructs.Construct {
}
this.asset = this.getAsset(props.asset, props.path);

if (props.wholeFile && props.mappings) {
throw new Error('Cannot set mappings and set wholeFile to true');
} else if (!props.wholeFile && !props.mappings) {
throw new Error('Must set mappings or set wholeFile to true');
const mutuallyExclusiveProps: Record<string, boolean> = {
wholeFile: !!props.wholeFile,
mappings: !!props.mappings,
singleValueMapping: !!props.singleValueMapping,
}

const mutuallyExclusivePropsEnabled = Object.keys(mutuallyExclusiveProps).filter((key) => mutuallyExclusiveProps[key]);
if (mutuallyExclusivePropsEnabled.length > 1) {
throw new Error(`Cannot set more than one of ${mutuallyExclusivePropsEnabled.join(', ')}`);
}

if (mutuallyExclusivePropsEnabled.length === 0) {
throw new Error(`Must set one of ${Object.keys(mutuallyExclusiveProps).join(', ')}`);
}

const provider = SopsSecretsManagerProvider.getOrCreate(this);
Expand All @@ -90,6 +99,7 @@ export class SopsSecretsManager extends constructs.Construct {
SourceHash: this.asset.assetHash,
KMSKeyArn: props.kmsKey?.keyArn,
Mappings: JSON.stringify(props.mappings || {}),
SingleValueMapping: JSON.stringify(props.singleValueMapping || null),
WholeFile: props.wholeFile || false,
FileType: props.fileType,
},
Expand Down
1 change: 1 addition & 0 deletions common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface SopsSecretsManagerBaseProps {
readonly kmsKey?: unknown;
readonly mappings?: SopsSecretsManagerMappings;
readonly wholeFile?: boolean;
readonly singleValueMapping?: SopsSecretsManagerMapping;
readonly fileType?: SopsSecretsManagerFileType;
}

Expand Down
19 changes: 19 additions & 0 deletions provider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ interface ResourceProperties {
S3Bucket: string;
S3Path: string;
Mappings: string; // json encoded Mappings;
SingleValueMapping: string; // json encoded Mapping;
WholeFile: boolean | string;
SecretArn: string;
SourceHash: string;
Expand Down Expand Up @@ -170,6 +171,18 @@ const toMappingsOrError = (obj: unknown, errorMessage: string): Mappings => {
throw new Error(errorMessage);
};

const toMappingOrNullOrError = (obj: unknown, errorMessage: string): Mapping | null => {
console.log('obj', obj);

if (obj === null) {
return null;
}
if (isMapping(obj)) {
return obj;
}
throw new Error(errorMessage);
};

const toMappingEncodingOrError = (mappingEncodingAsString: string | undefined): MappingEncoding | undefined => {
if (typeof mappingEncodingAsString === 'undefined') {
return undefined;
Expand Down Expand Up @@ -311,6 +324,7 @@ const handleCreate = async (event: CreateOrUpdateEvent): Promise<Response> => {
const s3BucketName = event.ResourceProperties.S3Bucket;
const s3Path = event.ResourceProperties.S3Path;
const mappings = toMappingsOrError(JSON.parse(event.ResourceProperties.Mappings), 'Unable to parse mappings to a valid shape');
const singleValueMapping = toMappingOrNullOrError(JSON.parse(event.ResourceProperties.SingleValueMapping), 'Unable to parse singleValueMapping to a valid shape');
const wholeFile = normaliseBoolean(event.ResourceProperties.WholeFile);
const secretArn = event.ResourceProperties.SecretArn;
// const sourceHash = event.ResourceProperties.SourceHash;
Expand Down Expand Up @@ -348,6 +362,10 @@ const handleCreate = async (event: CreateOrUpdateEvent): Promise<Response> => {
log('Writing decoded data to secretsmanager as whole file', { secretArn });
const wholeFileData = (data as SopsWholeFileData).data || '';
await setSecretString(wholeFileData, secretArn);
} else if (singleValueMapping) {
log('Mapping values from decoded data', { singleValueMapping });
const mappedValue = resolveMappings(data, { '': singleValueMapping })[''];
await setSecretString(mappedValue, secretArn);
} else {
log('Mapping values from decoded data', { mappings });
const mappedValues = resolveMappings(data, mappings);
Expand Down Expand Up @@ -449,6 +467,7 @@ const decodeResourceProperties = (resourceProperties: unknown): ResourceProperti
S3Bucket: getStringKeyOrError('S3Bucket', resourceProperties, 'Invalid resourceProperties'),
S3Path: getStringKeyOrError('S3Path', resourceProperties, 'Invalid resourceProperties'),
Mappings: getStringKeyOrError('Mappings', resourceProperties, 'Invalid resourceProperties'),
SingleValueMapping: getStringKeyOrError('SingleValueMapping', resourceProperties, 'Invalid resourceProperties'),
WholeFile: getStringOrBooleanKeyOrError('WholeFile', resourceProperties, 'Invalid resourceProperties'),
SecretArn: getStringKeyOrError('SecretArn', resourceProperties, 'Invalid resourceProperties'),
SourceHash: getStringKeyOrError('SourceHash', resourceProperties, 'Invalid resourceProperties'),
Expand Down
41 changes: 41 additions & 0 deletions provider/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ describe('onCreate', () => {
path: ['a'],
},
}),
SingleValueMapping: JSON.stringify(null),
WholeFile: false,
SecretArn: 'mysecretarn',
SourceHash: '123',
Expand Down Expand Up @@ -175,6 +176,7 @@ describe('onCreate', () => {
path: ['a'],
},
}),
SingleValueMapping: JSON.stringify(null),
WholeFile: false,
SecretArn: 'mysecretarn',
SourceHash: '123',
Expand Down Expand Up @@ -221,6 +223,7 @@ describe('onCreate', () => {
path: ['a'],
},
}),
SingleValueMapping: JSON.stringify(null),
WholeFile: false,
SecretArn: 'mysecretarn',
SourceHash: '123',
Expand Down Expand Up @@ -262,6 +265,7 @@ describe('onCreate', () => {
encoding: 'json',
},
}),
SingleValueMapping: JSON.stringify(null),
WholeFile: false,
SecretArn: 'mysecretarn',
SourceHash: '123',
Expand Down Expand Up @@ -303,6 +307,7 @@ describe('onCreate', () => {
encoding: 'json',
},
}),
SingleValueMapping: JSON.stringify(null),
WholeFile: 'false', // because a boolean set in the CDK becomes a string once it reaches the provider
SecretArn: 'mysecretarn',
SourceHash: '123',
Expand Down Expand Up @@ -337,6 +342,7 @@ describe('onCreate', () => {
S3Bucket: 'mys3bucket',
S3Path: 'mys3path.txt',
Mappings: JSON.stringify({}),
SingleValueMapping: JSON.stringify(null),
WholeFile: true,
SecretArn: 'mysecretarn',
SourceHash: '123',
Expand All @@ -359,6 +365,38 @@ describe('onCreate', () => {
});
});

test('singleValueMapping', async () => {
setMockSpawn({
stdoutData: JSON.stringify({
a: {
b: 'c',
},
}),
});

await onEvent({
RequestType: 'Create',
ResourceProperties: {
KMSKeyArn: undefined,
S3Bucket: 'mys3bucket',
S3Path: 'mys3path.txt',
Mappings: JSON.stringify({}),
SingleValueMapping: JSON.stringify({
path: ['a', 'b'],
}),
WholeFile: false,
SecretArn: 'mysecretarn',
SourceHash: '123',
FileType: undefined,
},
});

expect(mockSecretsManagerPutSecretValue).toBeCalledWith({
SecretId: 'mysecretarn',
SecretString: 'c',
});
});

test('pass kms key arn', async () => {
mockS3GetObject.mockImplementation(
(): Promise<MockS3GetObjectResponse> =>
Expand All @@ -383,6 +421,7 @@ describe('onCreate', () => {
path: ['a'],
},
}),
SingleValueMapping: JSON.stringify(null),
WholeFile: false,
SecretArn: 'mysecretarn',
SourceHash: '123',
Expand Down Expand Up @@ -436,6 +475,7 @@ describe('onUpdate', () => {
path: ['a'],
},
}),
SingleValueMapping: JSON.stringify(null),
WholeFile: false,
SecretArn: 'mysecretarn',
SourceHash: '123',
Expand Down Expand Up @@ -554,6 +594,7 @@ describe('invalid event attribute value shapes', () => {
path: ['a'],
},
}),
SingleValueMapping: JSON.stringify(null),
WholeFile: false,
SecretArn: 'mysecretarn',
SourceHash: '123',
Expand Down

0 comments on commit 36a0a0d

Please sign in to comment.