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

Adding support for specifying secrets using a list of strings. #32

Merged
merged 4 commits into from
Sep 16, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,23 @@ provider:
MYSECRET: /path/to/ssm/secret
```

Alternatively, if you would like to reference the secrets by their name in the
parameter store you can specify the secrets as a list of strings.

```
provider:
environmentSecrets:
- MYSECRET
mccredie marked this conversation as resolved.
Show resolved Hide resolved
```

Which is equivalent to:

```
provider:
environmentSecrets:
MY_SECRET: MY_SECRET
```

The plugin will create a json file with all the secrets. In the above example the ciphertext and ARN of the secret located at `path/to/ssm/secret` will be stored in the file under the key `MYSECRET`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph references path/to/ssm/secret which doesn't match up with the last example.

What do you all think of having a basic getting started that just uses the 'simple' way, and then create a new Advanced Configuration section that details how to customize the key name. This we we lower the barrier to adoption w/ as little upfront customization as possible, but then provide opportunities for 'advanced' config for power users.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thought.

See example code in [examples](/examples) folder for reference.

Expand Down
32 changes: 29 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,38 @@ class ServerlessSecretBaker {
this.serverless = serverless;
}

getSecretsConfig() {
const environmentSecrets = this.serverless.service.provider.environmentSecrets || [];

if (Array.isArray(environmentSecrets)) {
return environmentSecrets.map((item) => {
if (typeof item === 'string') {
return {
name: item,
path: item
}
} else {
return item
}
})
} else if (typeof environmentSecrets === 'object') {
return Object.entries(environmentSecrets).map(([name, path]) => ({
name,
path
}));
}
throw new this.serverless.classes.Error(
"`environmentSecrets` contained an unexpected value."
);
}

async writeEnvironmentSecretToFile() {
const providerSecrets = this.serverless.service.provider.environmentSecrets || {};
const providerSecrets = this.getSecretsConfig()
const secrets = {};

for (const name of Object.keys(providerSecrets)) {
const param = await this.getParameterFromSsm(providerSecrets[name]);

for (const {name, path} of providerSecrets) {
const param = await this.getParameterFromSsm(path);

if (!param) {
throw Error(`Unable to load Secret ${name}`);
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "serverless-secret-baker",
"version": "1.0.3",
"version": "1.1.0",
"description": "Serverless Plugin to store encrypted secrets from SSM as a packaged file availble to your lambdas.",
"main": "index.js",
"scripts": {
Expand Down
128 changes: 127 additions & 1 deletion tests/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,22 @@ describe("ServerlessSecretBaker", () => {
});
});

describe("With Secrets", () => {
describe("With secrets in unexpected format", () => {
let serverless;
let bakedGoods;

beforeEach(() => {
serverless = makeServerless();
serverless.service.provider.environmentSecrets = 5;
bakedGoods = new ServerlessSecretBaker(serverless);
});

it('should write an empty json object to the output file.', async () => {
expect(bakedGoods.writeEnvironmentSecretToFile()).to.be.rejected;
});
})

describe("With Secrets Object", () => {
const expectedSecretName = "MY_SECRET";
const expectedParameterStoreKey = "PARAMETER STORE KEY";
const expectedCiphertext = "SECRET VALUE CIPHERTEXT";
Expand Down Expand Up @@ -139,6 +154,117 @@ describe("ServerlessSecretBaker", () => {
});
});

describe("With Secrets String Array", () => {
const expectedSecretName = "MY_SECRET";
const expectedCiphertext = "SECRET VALUE CIPHERTEXT";
const expectedArn = "SECRET VALUE CIPHERTEXT";

let serverless;
let bakedGoods;

beforeEach(() => {
serverless = makeServerless();
serverless.service.provider.environmentSecrets = [
expectedSecretName
];
bakedGoods = new ServerlessSecretBaker(serverless);
sinon.stub(bakedGoods, "getParameterFromSsm");
bakedGoods.getParameterFromSsm.resolves({
Value: expectedCiphertext,
ARN: expectedArn
});
});

it("should write ciphertext for secret to secrets file on package", async () => {
await bakedGoods.writeEnvironmentSecretToFile();
const secretsJson = fs.writeFileAsync.firstCall.args[1];
const secrets = JSON.parse(secretsJson);

expect(secrets[expectedSecretName].ciphertext).to.equal(
expectedCiphertext
);
});

it("should write ARN from secret to secrets file on package", async () => {
await bakedGoods.writeEnvironmentSecretToFile();
const secretsJson = fs.writeFileAsync.firstCall.args[1];
const secrets = JSON.parse(secretsJson);

expect(secrets[expectedSecretName].arn).to.equal(expectedArn);
});

it("should throw an error if the parameter cannot be retrieved", async () => {
bakedGoods.getParameterFromSsm.reset();
bakedGoods.getParameterFromSsm.resolves(undefined);
expect(bakedGoods.writeEnvironmentSecretToFile()).to.be.rejected;
});

it("should call getParameterFromSsm with the correct parameter key", async () => {
await bakedGoods.writeEnvironmentSecretToFile();
expect(bakedGoods.getParameterFromSsm).to.have.been.calledWith(
expectedSecretName
);
});
});

describe("With Secrets Object Array", () => {
const expectedSecretName = "MY_SECRET";
const expectedParameterStoreKey = "MY_PARAMETER_STORE_KEY"
const expectedCiphertext = "SECRET VALUE CIPHERTEXT";
const expectedArn = "SECRET VALUE CIPHERTEXT";

let serverless;
let bakedGoods;

beforeEach(() => {
serverless = makeServerless();
serverless.service.provider.environmentSecrets = [
{
name: expectedSecretName,
path: expectedParameterStoreKey
}

];
bakedGoods = new ServerlessSecretBaker(serverless);
sinon.stub(bakedGoods, "getParameterFromSsm");
bakedGoods.getParameterFromSsm.resolves({
Value: expectedCiphertext,
ARN: expectedArn
});
});

it("should write ciphertext for secret to secrets file on package", async () => {
await bakedGoods.writeEnvironmentSecretToFile();
const secretsJson = fs.writeFileAsync.firstCall.args[1];
const secrets = JSON.parse(secretsJson);

expect(secrets[expectedSecretName].ciphertext).to.equal(
expectedCiphertext
);
});

it("should write ARN from secret to secrets file on package", async () => {
await bakedGoods.writeEnvironmentSecretToFile();
const secretsJson = fs.writeFileAsync.firstCall.args[1];
const secrets = JSON.parse(secretsJson);

expect(secrets[expectedSecretName].arn).to.equal(expectedArn);
});

it("should throw an error if the parameter cannot be retrieved", async () => {
bakedGoods.getParameterFromSsm.reset();
bakedGoods.getParameterFromSsm.resolves(undefined);
expect(bakedGoods.writeEnvironmentSecretToFile()).to.be.rejected;
});

it("should call getParameterFromSsm with the correct parameter key", async () => {
await bakedGoods.writeEnvironmentSecretToFile();
expect(bakedGoods.getParameterFromSsm).to.have.been.calledWith(
expectedParameterStoreKey
);
});
});

describe("getParameterFromSsm", () => {
let bakedGoods;
let getProviderStub;
Expand Down