cypress-aws-secrets-manager
Version:
Cypress Plugin | Integrate the power of AWS Secrets Manager seamlessly into your Cypress tests with the cypress-aws-secrets-manager plugin. This lightweight yet powerful plugin facilitates the secure loading of secrets stored in AWS Secrets Manager direct
263 lines (227 loc) • 9.64 kB
JavaScript
const { SecretsManagerClient, GetSecretValueCommand, UpdateSecretCommand } = require('@aws-sdk/client-secrets-manager')
const { fromSSO } = require('@aws-sdk/credential-providers')
const converter = require('number-to-words')
const chalk = require('chalk')
const path = require('path')
const fs = require('fs')
const { info, log } = require('./logger')
const strategyTypes = ['profile', 'default', 'unset', 'credentials', 'multi']
const separator = chalk.grey('\n====================================================================================================\n')
let errorAlreadyThrew = false
const createFilePath = (directory, secretName) => {
const arnRegex = /arn:aws:secretsmanager:[^:]+:[^:]+:secret:([^:-]+)/
if (secretName.match(arnRegex)) {
secretName = secretName.match(arnRegex)[1]
}
const filePath = path.join(directory, secretName + '.json')
return filePath
}
const writeSecretsToFile = (jsonFilePath, secrets) => {
const directory = path.dirname(jsonFilePath)
// Check if the directory exists, if not, create it
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, { recursive: true })
}
// Write the secrets to the file
fs.writeFileSync(jsonFilePath, JSON.stringify(secrets, null, 1))
}
const getLocalSecrets = (jsonFilePath) => {
const fs = require('fs')
try {
const jsonData = fs.readFileSync(jsonFilePath, 'utf8')
return JSON.parse(jsonData)
} catch (err) {
throw new Error(`⚠️ \x1B[37mError reading JSON file: ${err}`)
}
}
const mergeSecrets = (env, secrets) => {
return {
...env,
...secrets
}
}
function logSecrets(secrets, source) {
const masked = Object.fromEntries(Object.keys(secrets).map((k) => [k, '*'.repeat(5)]))
info(chalk.yellow('secrets: ') + JSON.stringify(masked, null, 1) + '\n')
log(chalk.green('√ ') + chalk.white('Secret loaded correctly from: ') + chalk.cyan(`< ${source} >`))
}
const updateEnvWithSecrets = (env, secrets, source) => {
env = mergeSecrets(env, secrets)
return env
}
function checkOnMandatoryKeys(objectToControl, mandatoryKeys) {
const missingProperties = mandatoryKeys.filter((property) => !objectToControl[property])
if (missingProperties.length > 0) {
console.log(
chalk.red('ConfigurationError!\n') + chalk.yellow('The object MUST contain these mandatory properties: ') + chalk.white(mandatoryKeys)
)
console.log(chalk.green('\nPassed: ') + JSON.stringify(objectToControl, null, 1))
console.log(chalk.red('\nMissing: ') + JSON.stringify(missingProperties, null, 1))
throwException('The object MUST contain these mandatory properties: ' + mandatoryKeys)
}
}
async function getAwsSecrets(strategy, awsSecretsManagerConfig, directory) {
info('AWS SSO strategy: ' + JSON.stringify(strategy))
errorAlreadyThrew = false
if (strategy === 'multi') {
return await tryMultiStrategy(awsSecretsManagerConfig, directory)
} else {
return await getSecretsFromAws(awsSecretsManagerConfig, strategy, directory)
}
}
async function tryMultiStrategy(awsSecretsManagerConfig, directory) {
let counter = 0
let success = false
let response
while (counter < strategyTypes.length - 1 && !success) {
try {
counter++
const client = await createClient(awsSecretsManagerConfig, strategyTypes[counter - 1], counter, directory, false)
response = await fetchSecret(client, awsSecretsManagerConfig.secretName)
if (response) success = true
} catch (error) {
console.log(chalk.red(converter.toOrdinal(counter) + ' attempt FAILED whit ERROR: ') + chalk.white(error.message))
if (counter === strategyTypes.length - 1) {
throwException('All login attempts have been exhausted. Unable to log in using any supported credential strategy.')
}
}
}
return parseSecret(response)
}
async function getSecretsFromAws(awsSecretsManagerConfig, strategy, directory) {
const secretName = awsSecretsManagerConfig.secretName
try {
const client = await createClient(awsSecretsManagerConfig, strategy, 1, directory, true)
const response = await fetchSecret(client, secretName)
return parseSecret(response)
} catch (error) {
throwException(error, true, true)
throwException('Error while setting credentials. Please check the console logs for more information.', false, false)
}
}
async function fetchSecret(client, secretName) {
const response = await client.send(
new GetSecretValueCommand({
SecretId: secretName,
VersionStage: 'AWSCURRENT'
})
)
console.log(chalk.green('\n√ ') + 'AWS SDK credentials are set up correctly!\n')
console.log('Extracting secret from: ' + chalk.cyan('"AWS Secrets Manager"\n'))
return response
}
function parseSecret(response) {
if (!response?.SecretString) {
throw new Error('Invalid response from AWS Secrets Manager')
}
return JSON.parse(response.SecretString)
}
async function createClient(awsSecretsManagerConfig, strategy, counter, directory, throwError) {
switch (strategy) {
case 'profile':
return setClientWithSSO(awsSecretsManagerConfig, counter, throwError)
case 'default':
awsSecretsManagerConfig.profile = 'default'
return setClientWithSSO(awsSecretsManagerConfig, counter, throwError)
case 'unset':
return setClientWithoutCredentials(awsSecretsManagerConfig, counter)
case 'credentials':
return setClientWithCredentials(awsSecretsManagerConfig, directory, counter, throwError)
default:
throw new Error('Strategy type: ' + chalk.cyan(strategy) + ' not supported')
}
}
function setClientWithSSO(awsSecretsManagerConfig, counter = 1, throwError = true) {
console.log('HERE')
console.log(
'\n' +
converter.toOrdinal(counter) +
' attempt: Trying to retrieve secrets using profile: ' +
chalk.cyan(JSON.stringify(awsSecretsManagerConfig.profile))
)
if (awsSecretsManagerConfig.profile) {
return new SecretsManagerClient({
region: awsSecretsManagerConfig.region,
credentials: fromSSO({ profile: awsSecretsManagerConfig.profile })
})
} else if (throwError) {
throwException("Missing 'profile' key in awsSecretsManagerConfig", false, throwError)
errorAlreadyThrew = true
}
}
function setClientWithoutCredentials(awsSecretsManagerConfig, counter = 1) {
console.log('\n' + converter.toOrdinal(counter) + ' attempt: Trying to retrieve secrets using AWS credentials from environment variables')
return new SecretsManagerClient({
region: awsSecretsManagerConfig.region
})
}
function setClientWithCredentials(awsSecretsManagerConfig, directory, counter = 1, throwError = true) {
console.log('\n' + converter.toOrdinal(counter) + ' attempt: Trying to retrieve secrets using AWS credentials passed by user')
if (awsSecretsManagerConfig.pathToCredentials) {
const credentialsFilename = path.join(directory, awsSecretsManagerConfig.pathToCredentials)
const credentials = require(credentialsFilename)
const hiddenCredentials = {}
const mandatoryCredentials = ['accessKeyId', 'secretAccessKey', 'sessionToken']
checkOnMandatoryKeys(credentials, mandatoryCredentials)
Object.keys(credentials).forEach((key) => {
hiddenCredentials[key] = ''.padStart(5, '*')
})
console.log('\n' + chalk.cyan('\nCredentials imported correctly: ') + chalk.white(JSON.stringify(hiddenCredentials, null, 1)))
return new SecretsManagerClient({
region: awsSecretsManagerConfig.region,
credentials: credentials
})
} else {
throwException("Missing 'pathToCredentials' key in awsSecretsManagerConfig", false, throwError)
errorAlreadyThrew = true
}
}
const throwException = (errorMessage, logInTerminal = true, throwError = true) => {
errorAlreadyThrew = true
if (errorMessage == 'ExpiredTokenException: The security token included in the request is expired') {
errorMessage += " or maybe the environment not configured correctly to use the 'unset' strategy"
}
if (logInTerminal) {
console.log(chalk.red('\n⚠️ Incorrect plugin configuration!'))
console.log(chalk.red('ERROR: ') + errorMessage)
console.log(separator)
}
if (throwError) throw Error(errorMessage)
}
async function updateSecret(env, secretValue) {
const awsSecretsManagerConfig = env.awsSecretsManagerConfig ?? env.AWS_SECRET_MANAGER_CONFIG
const secretName = awsSecretsManagerConfig.secretName
const kmsKeyId = awsSecretsManagerConfig.kmsKeyId
console.log('Updating secret value for secretName: ' + chalk.cyan(secretName) + ' for kmsKeyId: ' + chalk.cyan(kmsKeyId))
try {
if (typeof secretValue !== 'object') {
throw new Error('secretValue deve essere un oggetto')
}
const strategy = env.AWS_SSO_STRATEGY ?? 'multi'
const existingSecrets = await getAwsSecrets(strategy, awsSecretsManagerConfig)
const updatedSecrets = { ...existingSecrets, ...secretValue }
const body = {
SecretId: secretName,
SecretString: JSON.stringify(updatedSecrets),
...(kmsKeyId && { KmsKeyId: kmsKeyId })
}
const putCommand = new UpdateSecretCommand(body)
const client = await createClient(awsSecretsManagerConfig, strategy)
const putResponse = await client.send(putCommand)
console.log(chalk.green('\n√ ') + 'Secret updated successfully: ' + chalk.cyan(secretName))
return putResponse
} catch (error) {
console.log(chalk.red('⚠️ Error updating secret: '), error)
throw error
}
}
module.exports = {
createFilePath,
getLocalSecrets,
updateEnvWithSecrets,
checkOnMandatoryKeys,
getAwsSecrets,
writeSecretsToFile,
updateSecret,
logSecrets
}