Skip to content

Commit

Permalink
feat: context support (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
erezrokah committed Feb 28, 2022
1 parent 99fe2f7 commit 0d32761
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 23 deletions.
25 changes: 16 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,29 @@ Inject secrets from AWS Secrets Manager into the Netlify build process.
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:<region>:<account-id>:secret:<secret-path>"
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:us-east-1:534156574994:secret:netlify/plugin/*"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": "secretsmanager:ListSecrets",
"Resource": "*"
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": "secretsmanager:DescribeSecret",
"Resource": "arn:aws:secretsmanager:us-east-1:534156574994:secret:netlify/plugin/*"
},
{
"Sid": "VisualEditor2",
"Effect": "Allow",
"Action": "secretsmanager:ListSecrets",
"Resource": "*"
}
]
}
```

> You can scope the `GetSecretValue` permission to a path, but the `ListSecrets` must be a wildcard `*`
> You can scope the `GetSecretValue` permission to a path, but the `ListSecrets` must be a wildcard `*`.
> `DescribeSecret` is required for context based secrets (we use secret tags to get the context)
## Usage

Expand Down
64 changes: 50 additions & 14 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
const process = require('process')

const { SecretsManagerClient, ListSecretsCommand, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager')
const {
SecretsManagerClient,
ListSecretsCommand,
GetSecretValueCommand,
DescribeSecretCommand,
} = require('@aws-sdk/client-secrets-manager')
const chalk = require('chalk')

const listSecrets = async ({ client, nextToken, secrets = [] }) => {
Expand All @@ -14,23 +19,39 @@ const listSecrets = async ({ client, nextToken, secrets = [] }) => {
return newSecrets
}

const normalizeSecretValue = async ({ client, secret }) => {
const getSecretContext = async ({ client, secret }) => {
try {
const { Tags: tags = [] } = await client.send(new DescribeSecretCommand({ SecretId: secret.ARN }))
const context = tags.find(({ Key: key }) => key === 'NETLIFY_CONTEXT')
return context && context.Value
} catch (error) {
if (error.name === 'AccessDeniedException') {
return console.log(
chalk.dim(`Does not have permissions to retrieve context for secret ${chalk.yellow(secret.Name)}`),
)
}
}
}

const getSecretsValue = async ({ client, secret }) => {
try {
// SecretString is a JSON string representation of the secret, e.g. '{"SECRET_NAME":"SECRET_VALUE"}'
const { SecretString: secretString } = await client.send(new GetSecretValueCommand({ SecretId: secret.ARN }))
const context = await getSecretContext({ client, secret })
const parsedValue = JSON.parse(secretString)
return parsedValue
return Object.entries(parsedValue).map(([key, value]) => ({ key, value, context }))
} catch (error) {
if (error.name === 'AccessDeniedException') {
return console.log(chalk.dim(`Skipping restricted AWS secret ${chalk.yellow(secret.Name)}`))
console.log(chalk.dim(`Skipping restricted secret ${chalk.yellow(secret.Name)}`))
return []
}
throw error
}
}

const normalizeSecrets = async ({ client, secrets }) => {
const values = await Promise.all(secrets.map((secret) => normalizeSecretValue({ client, secret })))
return Object.assign({}, ...values)
const getSecretsValues = async ({ client, secrets }) => {
const secretsWithValues = await Promise.all(secrets.map((secret) => getSecretsValue({ client, secret })))
return secretsWithValues.flat()
}

const SECRET_PREFIX = process.env.NETLIFY_AWS_SECRET_PREFIX || 'NETLIFY_AWS_SECRET_'
Expand All @@ -43,6 +64,7 @@ module.exports = {
NETLIFY_AWS_ACCESS_KEY_ID: accessKeyId,
NETLIFY_AWS_SECRET_ACCESS_KEY: secretAccessKey,
NETLIFY_AWS_DEFAULT_REGION: region = 'us-east-1',
CONTEXT,
} = process.env
if (!accessKeyId) {
return utils.build.failBuild(`Missing environment variable NETLIFY_AWS_ACCESS_KEY_ID`)
Expand All @@ -57,14 +79,28 @@ module.exports = {
credentials: { accessKeyId, secretAccessKey },
})
const secrets = await listSecrets({ client })
const normalizedSecrets = await normalizeSecrets({ client, secrets })

const entries = Object.entries(normalizedSecrets)
entries.forEach(([key, value]) => {
const secretsWithValues = await getSecretsValues({ client, secrets })
secretsWithValues.forEach(({ key, value, context }) => {
const prefixedKey = getPrefixedKey(key)
console.log(`${chalk.bold('Injecting AWS secret')} ${chalk.magenta(`${key}`)} as ${chalk.green(prefixedKey)}`)
// eslint-disable-next-line no-param-reassign
netlifyConfig.build.environment[prefixedKey] = value

// no context, inject to all
if (!context) {
console.log(`${chalk.bold('Injecting AWS secret')} ${chalk.magenta(`${key}`)} as ${chalk.green(prefixedKey)}`)
// eslint-disable-next-line no-param-reassign
netlifyConfig.build.environment[prefixedKey] = value
return
}

// inject only to matching context
if (CONTEXT === context) {
console.log(
`${chalk.bold('Injecting AWS secret')} ${chalk.magenta(`${key}`)} as ${chalk.green(
prefixedKey,
)} to context ${chalk.yellow(context)}`,
)
/* eslint-disable-next-line no-param-reassign */
netlifyConfig.build.environment[prefixedKey] = value
}
})
},
}

0 comments on commit 0d32761

Please sign in to comment.