Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add singleValueMapping option #115

Merged
merged 1 commit into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading