Skip to content

Commit

Permalink
feat: support json secrets (#24)
Browse files Browse the repository at this point in the history
Fixes #22
  • Loading branch information
WtfJoke committed Jul 9, 2022
2 parents 88e509b + 0e86828 commit b62961b
Show file tree
Hide file tree
Showing 11 changed files with 155 additions and 18 deletions.
32 changes: 32 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ const actionEnvironmentSecretProps: ActionEnvironmentSecretProps = { ... }
| <code><a href="#cdk-github.ActionEnvironmentSecretProps.property.repositorySecretName">repositorySecretName</a></code> | <code>string</code> | The GitHub secret name to be stored. |
| <code><a href="#cdk-github.ActionEnvironmentSecretProps.property.sourceSecret">sourceSecret</a></code> | <code>aws-cdk-lib.aws_secretsmanager.ISecret</code> | This AWS secret value will be stored in GitHub as a secret (under the name of repositorySecretName). |
| <code><a href="#cdk-github.ActionEnvironmentSecretProps.property.repositoryOwner">repositoryOwner</a></code> | <code>string</code> | The GitHub repository owner. |
| <code><a href="#cdk-github.ActionEnvironmentSecretProps.property.sourceSecretJsonField">sourceSecretJsonField</a></code> | <code>string</code> | The key of a JSON field to retrieve in sourceSecret. |

---

Expand Down Expand Up @@ -397,6 +398,21 @@ The GitHub repository owner.

---

##### `sourceSecretJsonField`<sup>Optional</sup> <a name="sourceSecretJsonField" id="cdk-github.ActionEnvironmentSecretProps.property.sourceSecretJsonField"></a>

```typescript
public readonly sourceSecretJsonField: string;
```

- *Type:* string
- *Default:* returns all the content stored in the Secrets Manager secret.

The key of a JSON field to retrieve in sourceSecret.

This can only be used if the secret stores a JSON object.

---

### ActionSecretProps <a name="ActionSecretProps" id="cdk-github.ActionSecretProps"></a>

#### Initializer <a name="Initializer" id="cdk-github.ActionSecretProps.Initializer"></a>
Expand All @@ -416,6 +432,7 @@ const actionSecretProps: ActionSecretProps = { ... }
| <code><a href="#cdk-github.ActionSecretProps.property.repositorySecretName">repositorySecretName</a></code> | <code>string</code> | The GitHub secret name to be stored. |
| <code><a href="#cdk-github.ActionSecretProps.property.sourceSecret">sourceSecret</a></code> | <code>aws-cdk-lib.aws_secretsmanager.ISecret</code> | This AWS secret value will be stored in GitHub as a secret (under the name of repositorySecretName). |
| <code><a href="#cdk-github.ActionSecretProps.property.repositoryOwner">repositoryOwner</a></code> | <code>string</code> | The GitHub repository owner. |
| <code><a href="#cdk-github.ActionSecretProps.property.sourceSecretJsonField">sourceSecretJsonField</a></code> | <code>string</code> | The key of a JSON field to retrieve in sourceSecret. |

---

Expand Down Expand Up @@ -480,6 +497,21 @@ The GitHub repository owner.

---

##### `sourceSecretJsonField`<sup>Optional</sup> <a name="sourceSecretJsonField" id="cdk-github.ActionSecretProps.property.sourceSecretJsonField"></a>

```typescript
public readonly sourceSecretJsonField: string;
```

- *Type:* string
- *Default:* returns all the content stored in the Secrets Manager secret.

The key of a JSON field to retrieve in sourceSecret.

This can only be used if the secret stores a JSON object.

---

### GitHubResourceProps <a name="GitHubResourceProps" id="cdk-github.GitHubResourceProps"></a>

#### Initializer <a name="Initializer" id="cdk-github.GitHubResourceProps.Initializer"></a>
Expand Down
11 changes: 10 additions & 1 deletion src/constructs/action-environment-secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,20 @@ export interface ActionEnvironmentSecretProps {
* This AWS secret value will be stored in GitHub as a secret (under the name of repositorySecretName)
*/
readonly sourceSecret: ISecret;

/**
* The key of a JSON field to retrieve in sourceSecret.
* This can only be used if the secret stores a JSON object.
*
* @default - returns all the content stored in the Secrets Manager secret.
*/
readonly sourceSecretJsonField?: string;
}

export class ActionEnvironmentSecret extends Construct {
constructor(scope: Construct, id: string, props: ActionEnvironmentSecretProps) {
super(scope, id);
const { githubTokenSecret, repositorySecretName, repositoryName, repositoryOwner, sourceSecret, environment } = props;
const { githubTokenSecret, repositorySecretName, repositoryName, repositoryOwner, sourceSecret, sourceSecretJsonField, environment } = props;
const awsRegion = Stack.of(this).region;
const shortId = Names.uniqueId(this).slice(-8);

Expand All @@ -69,6 +77,7 @@ export class ActionEnvironmentSecret extends Construct {
repositoryOwner,
repositoryName,
sourceSecretArn: sourceSecret.secretArn,
sourceSecretJsonField,
repositorySecretName,
awsRegion,
};
Expand Down
11 changes: 10 additions & 1 deletion src/constructs/action-secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,20 @@ export interface ActionSecretProps {
* This AWS secret value will be stored in GitHub as a secret (under the name of repositorySecretName)
*/
readonly sourceSecret: ISecret;

/**
* The key of a JSON field to retrieve in sourceSecret.
* This can only be used if the secret stores a JSON object.
*
* @default - returns all the content stored in the Secrets Manager secret.
*/
readonly sourceSecretJsonField?: string;
}

export class ActionSecret extends Construct {
constructor(scope: Construct, id: string, props: ActionSecretProps) {
super(scope, id);
const { githubTokenSecret, repositorySecretName, repositoryName, repositoryOwner, sourceSecret } = props;
const { githubTokenSecret, repositorySecretName, repositoryName, repositoryOwner, sourceSecret, sourceSecretJsonField } = props;
const awsRegion = Stack.of(this).region;
const shortId = Names.uniqueId(this).slice(-8);

Expand All @@ -62,6 +70,7 @@ export class ActionSecret extends Construct {
repositoryOwner,
repositoryName,
sourceSecretArn: sourceSecret.secretArn,
sourceSecretJsonField,
repositorySecretName,
awsRegion,
};
Expand Down
3 changes: 2 additions & 1 deletion src/examples/action-secret/action-secret-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class ActionSecretStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);

const sourceSecret = Secret.fromSecretNameV2(this, 'secretToStoreInGitHub', 'testcdkgithub');
const sourceSecret = Secret.fromSecretNameV2(this, 'secretToStoreInGitHub', 'cdk-github/test/structured');
const githubTokenSecret = Secret.fromSecretNameV2(this, 'ghSecret', 'GITHUB_TOKEN');

new ActionSecret(this, 'GitHubActionSecret', {
Expand All @@ -16,6 +16,7 @@ export class ActionSecretStack extends Stack {
repositoryOwner: 'wtfjoke',
repositorySecretName: 'A_RANDOM_GITHUB_SECRET',
sourceSecret,
sourceSecretJsonField: 'key',
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SecretsManager } from '@aws-sdk/client-secrets-manager';
import { Octokit } from '@octokit/core';
import { CdkCustomResourceResponse } from 'aws-lambda';
import type { OnEventRequest, ActionEnvironmentSecretEventProps } from '../../../types';
import { getSecretString } from '../aws-secret-helper';
import { getOwner } from '../github-helper';

import { encryptValue } from '../github-secret-encryptor';
Expand Down Expand Up @@ -75,16 +76,11 @@ const createOrUpdateEnvironmentSecret = async (
repositorySecretName: secret_name,
environment: environment_name,
sourceSecretArn: secretId,
sourceSecretJsonField,
} = event.ResourceProperties;

const secretToEncrypt = await smClient.getSecretValue({ SecretId: secretId });
console.log(`Encrypt value of secret with id: ${secretId}`);

const secretString = secretToEncrypt.SecretString;
if (!secretString) {
throw new Error('SecretString is empty from secret with id: ' + secretId);
}

const secretString = await getSecretString(secretId, smClient, sourceSecretJsonField);
const owner = await getOwner(octokit, repositoryOwner);
const { data } = await octokit.request('GET /repos/{owner}/{repo}/actions/secrets/public-key', { owner, repo });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SecretsManager } from '@aws-sdk/client-secrets-manager';
import { Octokit } from '@octokit/core';
import { CdkCustomResourceResponse } from 'aws-lambda';
import type { OnEventRequest, ActionSecretEventProps } from '../../../types';
import { getSecretString } from '../aws-secret-helper';
import { getOwner } from '../github-helper';

import { encryptValue } from '../github-secret-encryptor';
Expand Down Expand Up @@ -71,16 +72,14 @@ const createOrUpdateRepoSecret = async (
octokit: Octokit,
smClient: SecretsManager,
) => {
const { repositoryOwner, repositoryName: repo, repositorySecretName: secret_name } = event.ResourceProperties;
const secretId = event.ResourceProperties.sourceSecretArn;
const secretToEncrypt = await smClient.getSecretValue({ SecretId: secretId });
const {
repositoryOwner, repositoryName: repo,
repositorySecretName: secret_name, sourceSecretArn: secretId,
sourceSecretJsonField,
} = event.ResourceProperties;
console.log(`Encrypt value of secret with id: ${secretId}`);

const secretString = secretToEncrypt.SecretString;
if (!secretString) {
throw new Error('SecretString is empty from secret with id: ' + secretId);
}

const secretString = await getSecretString(secretId, smClient, sourceSecretJsonField);
const owner = await getOwner(octokit, repositoryOwner);
const { data } = await octokit.request('GET /repos/{owner}/{repo}/actions/secrets/public-key', { owner, repo });

Expand Down
24 changes: 24 additions & 0 deletions src/handler/secrets/aws-secret-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { SecretsManager } from '@aws-sdk/client-secrets-manager';

export const getSecretString = async (secretId: string, smClient: SecretsManager, jsonField?: string): Promise<string> => {
const secretToEncrypt = await smClient.getSecretValue({ SecretId: secretId });

let secretString = secretToEncrypt.SecretString;
if (!secretString) {
throw new Error('SecretString is empty from secret with id: ' + secretId);
}
if (jsonField) {
try {
secretString = JSON.parse(secretString)[jsonField];
} catch (error) {
if (error instanceof Error) {
throw new Error('Error while parsing SecretString with id: ' + secretId + ' and jsonField: ' + jsonField + ': ' + error.message);
}
throw error;
}
}
if (!secretString) {
throw new Error('SecretString is empty from secret with id: ' + secretId);
}
return secretString;
};
1 change: 1 addition & 0 deletions src/handler/secrets/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './aws-secret-helper';
export * from './github-helper';
export * from './github-secret-encryptor';
export * from './github-secret-name-validator';
1 change: 1 addition & 0 deletions src/types/action-environment-secret-event-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export type ActionEnvironmentSecretEventProps = {
repositorySecretName: string;
repositoryOwner?: string;
sourceSecretArn: string;
sourceSecretJsonField?: string;
awsRegion: string;
};
1 change: 1 addition & 0 deletions src/types/action-secret-event-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export type ActionSecretEventProps = {
repositorySecretName: string;
repositoryOwner?: string;
sourceSecretArn: string;
sourceSecretJsonField?: string;
awsRegion: string;
};
64 changes: 64 additions & 0 deletions test/handler/secrets/aws-secret-helper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { GetSecretValueCommand, SecretsManager } from '@aws-sdk/client-secrets-manager';
import { mockClient } from 'aws-sdk-client-mock';
import { getSecretString } from '../../../src/handler/secrets/aws-secret-helper';

describe('aws-secret-helper', () => {

const smMock = mockClient(SecretsManager);
const SecretId = 'arn:aws:secretsmanager:eu-central-1:123456789012:secret:secret-id';
const sm = new SecretsManager({});

it('getSecretValue', async () => {
smMock.on(GetSecretValueCommand, {
SecretId,
}).resolves({
SecretString: 'mySecretValue',
});
expect(await getSecretString(SecretId, sm)).toBe('mySecretValue');
});

it('getSecretValue with jsonField', async () => {
smMock.on(GetSecretValueCommand, {
SecretId,
}).resolves({
SecretString: JSON.stringify({ mySecretKey: 'mySecretValue' }),
});
expect(await getSecretString(SecretId, sm, 'mySecretKey')).toBe('mySecretValue');
});

it('getSecretValue with jsonField without json structure', async () => {
smMock.on(GetSecretValueCommand, {
SecretId,
}).resolves({
SecretString: 'mySecretValue',
});

await expect(getSecretString(SecretId, sm, 'mySecretKey'))
.rejects
.toThrowError('Error while parsing SecretString with id: arn:aws:secretsmanager:eu-central-1:123456789012:secret:secret-id and jsonField: mySecretKey: Unexpected token m in JSON at position 0');
});

it('getSecretValue with empty jsonField', async () => {
smMock.on(GetSecretValueCommand, {
SecretId,
}).resolves({
SecretString: JSON.stringify({ mySecretKey: '' }),
});

await expect(getSecretString(SecretId, sm, 'mySecretKey'))
.rejects
.toThrowError('SecretString is empty from secret with id: arn:aws:secretsmanager:eu-central-1:123456789012:secret:secret-id');
});

it('getSecretValue with empty secret value', async () => {
smMock.on(GetSecretValueCommand, {
SecretId,
}).resolves({
SecretString: '',
});
await expect(getSecretString(SecretId, sm, 'mySecretKey'))
.rejects
.toThrowError('SecretString is empty from secret with id: arn:aws:secretsmanager:eu-central-1:123456789012:secret:secret-id');
});

});

0 comments on commit b62961b

Please sign in to comment.