Skip to content

Commit

Permalink
feat: add delete secret action (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
danteay authored Sep 23, 2024
1 parent 5b67966 commit 0539c25
Show file tree
Hide file tree
Showing 10 changed files with 362 additions and 196 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ dist

# Stores VSCode versions used for testing VSCode extensions
.vscode-test
.idea/

# yarn v2
.yarn/cache
Expand Down
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,54 @@ jobs:
dry_run: true # Default false
show_values: false # If true secret values will be displayed on action logs (default false)
exclude: "^_" # Regular expression that excludes the matching keys to be synced (default '^_')
delete_secret: false # If true it will delete the secret instead of creating or updating its values.
```
### Syncing secrets
This should be the configuration to sync the secret values from a json file to the AWS Secrets Manager.
```yml
jobs:
sync-secrets:
name: Sync secrets
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Sync
uses: Drafteame/sync-secrets-manager@main
with:
aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws_region: us-east-1
secret_name: my-secret
json_file_path: path/to/json/secrets.json

# Optional if secret does not exist
create_secret: true
```
### Deleting secrets
This should be the configuration to delete the secret from the AWS Secrets Manager.
```yml
jobs:
sync-secrets:
name: Sync secrets
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Sync
uses: Drafteame/sync-secrets-manager@main
with:
aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws_region: us-east-1
secret_name: my-secret
delete_secret: true
```
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ inputs:
exclude:
description: "List of regular expressions that determines if a secret key should be excluded from sync"
required: false
delete_secret:
description: "Flag that marks the specified secret to be deleted"
required: false

runs:
using: "docker"
Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const getAction = () => {
getInput("exclude"),
getBooleanInput("show_values", false),
getBooleanInput("create_secret", false),
getBooleanInput("delete_secret", false),
);
};

Expand Down
61 changes: 47 additions & 14 deletions src/action/Action.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ export default class Action {
*/
#createSecretFlag;

/**
* Flag that marks the specified secret to be deleted
*/
#deleteSecretFlag;

/**
* Creates a new Action instance.
*
Expand All @@ -49,11 +54,12 @@ export default class Action {
* @param {string} region The AWS region.
* @param {string} secretName The name of the secret in AWS Secrets Manager.
* @param {string} jsonFile The path to the JSON file containing the new secret values.
* @param {string} skipPattern A regular expression that eval keys of the json file and if match,
* @param {string} skipPattern A regular expression that eval keys of the json file and if matched,
* that key should be omitted
* @param {boolean} showValues If this flag is set to true all secret values will be displayed on logs,
* if false, a place holder will be displayed.
* @param {boolean} createSecretFlag Flag to create the secret before sync if not exists
* if false, a placeholder will be displayed.
* @param {boolean} createSecret Flag to create the secret before sync if not exists
* @param {boolean} deleteSecret Flag that marks the specified secret to be deleted.
*
* @throws {Error} Throws an error if any required parameter is missing or if the JSON file doesn't exist.
*/
Expand All @@ -66,13 +72,22 @@ export default class Action {
skipPattern,
showValues = false,
createSecret = false,
deleteSecret = false,
) {
this.#validateData(keyId, secretKey, region, secretName, jsonFile);
this.#validateData(
keyId,
secretKey,
region,
secretName,
jsonFile,
deleteSecret,
);

this.#jsonFile = jsonFile;
this.#skipPattern = skipPattern || defaultSkipPattern;
this.#showValues = showValues;
this.#createSecretFlag = createSecret;
this.#deleteSecretFlag = deleteSecret;

this.#smClient = new SecretsManager(keyId, secretKey, region, secretName);
}
Expand All @@ -95,6 +110,15 @@ export default class Action {
this.#createSecretFlag = flag;
}

/**
* Set the deleteSecretFlag after constructor.
*
* @param {boolean} flag - Value of the flag
*/
setDeleteSecretFlag(flag) {
this.#deleteSecretFlag = flag;
}

/**
* Runs the action to synchronize secrets by fetching existing secrets and creating a change set.
*
Expand All @@ -103,23 +127,29 @@ export default class Action {
async run() {
await this.#createSecret();

const existingSecretData = await this.#smClient.getValues();
const newSecretData = JSON.parse(fs.readFileSync(this.#jsonFile, "utf8"));
let existingSecretData = {};
let newSecretData = {};

if (!this.#deleteSecretFlag) {
existingSecretData = await this.#smClient.getValues();
newSecretData = JSON.parse(fs.readFileSync(this.#jsonFile, "utf8"));
}

return new ChangeSet(
this.#smClient,
newSecretData,
existingSecretData,
this.#skipPattern,
this.#showValues,
this.#deleteSecretFlag,
);
}

/**
* Execute secret creation if needed
*/
async #createSecret() {
if (!this.#createSecretFlag) {
if (this.#deleteSecretFlag || !this.#createSecretFlag) {
core.info("secret creation skip...");
return;
}
Expand All @@ -139,10 +169,11 @@ export default class Action {
* @param {string} region - The AWS region.
* @param {string} secretName - The name of the secret in AWS Secrets Manager.
* @param {string} jsonFile - The path to the JSON file containing the new secret values.
* @param {boolean} deleteSecret - Flag to validate if the secret should be deleted.
*
* @throws {Error} Throws an error if any required parameter is missing or if the JSON file doesn't exist.
*/
#validateData(keyId, secretKey, region, secretName, jsonFile) {
#validateData(keyId, secretKey, region, secretName, jsonFile, deleteSecret) {
if (!keyId) {
throw new Error("Missing aws_access_key_id");
}
Expand All @@ -159,13 +190,15 @@ export default class Action {
throw new Error("Missing secret_name");
}

if (!jsonFile) {
throw new Error("Missing json_file_path");
}
if (!deleteSecret) {
if (!jsonFile) {
throw new Error("Missing json_file_path");
}

// Check if the JSON file exists
if (!fs.existsSync(jsonFile)) {
throw new Error(`JSON file does not exist at path: ${jsonFile}`);
// Check if the JSON file exists
if (!fs.existsSync(jsonFile)) {
throw new Error(`JSON file does not exist at path: ${jsonFile}`);
}
}
}
}
29 changes: 22 additions & 7 deletions src/action/ChangeSet.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,32 +40,41 @@ export default class ChangeSet {
#skipPattern;

/**
* A flag to unhide secret values on log messages
* A flag to un-hide secret values on log messages
* @type {boolean}
*/
#showValues;

/**
* A flag to delete the secret
* @type {boolean}
*/
#deleteAction;

/**
* Creates a new ChangeSet instance.
*
* @param {SecretsManager} smClient An instance of the SecretsManager class for interacting with AWS Secrets Manager.
* @param {Object} newValues The new set of values to be applied to the secrets manager.
* @param {Object} existingValues The existing set of values to be replaced by the new ones.
* @param {string} skipPattern A regular expression to skip keys.
* @param {boolean} showValues A flag to unhide secret values on log messages
* @param {boolean} showValues A flag to un-hide secret values on log messages
* @param {boolean} deleteAction A flag to delete the secret
*/
constructor(
smClient,
newValues,
existingValues,
skipPattern,
showValues = false,
deleteAction = false,
) {
this.#changeDesc = [];
this.#updatedValues = { ...existingValues };
this.#smClient = smClient;
this.#skipPattern = skipPattern || "";
this.#showValues = showValues;
this.#deleteAction = deleteAction;

this.#eval(newValues, existingValues);
}
Expand All @@ -85,6 +94,11 @@ export default class ChangeSet {
* @returns {Promise} A promise that resolves when the update is completed.
*/
async apply() {
if (this.#deleteAction) {
await this.#smClient.delete();
return;
}

await this.#smClient.update(this.#updatedValues);
}

Expand All @@ -95,6 +109,11 @@ export default class ChangeSet {
* @param {Object} existingValues - The existing set of values to be replaced by the new ones.
*/
#eval(newValues, existingValues) {
if (this.#deleteAction) {
this.#removedDesc("ALL_KEYS");
return;
}

// Check for changes and update the secret (or preview changes)
for (const key in newValues) {
if (this.#shouldSkip(key)) {
Expand Down Expand Up @@ -141,11 +160,7 @@ export default class ChangeSet {

let exp = new RegExp(this.#skipPattern);

if (exp.test(key)) {
return true;
}

return false;
return exp.test(key);
}

/**
Expand Down
17 changes: 15 additions & 2 deletions src/secrets-manager/SecretsManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
UpdateSecretCommand,
CreateSecretCommand,
ListSecretsCommand,
DeleteSecretCommand,
} from "@aws-sdk/client-secrets-manager";

import lodash from "lodash";
Expand Down Expand Up @@ -39,7 +40,7 @@ export default class SecretsManager {
}

/**
* Take a new set of values an replace the current values for the configured secrets manager.
* Take a new set of values as replacement the current values for the configured secrets manager.
*
* @param {Object} newValues Object with new values to replace existing ones on secrets manager
*/
Expand Down Expand Up @@ -74,7 +75,7 @@ export default class SecretsManager {

const res = await this.client.send(listCommand);

if (res.SecretList.length == 0) {
if (res.SecretList.length === 0) {
return false;
}

Expand All @@ -100,4 +101,16 @@ export default class SecretsManager {

await this.client.send(createCommand);
}

/**
* Delete the given secret name
*/
async delete() {
const deleteCommand = new DeleteSecretCommand({
SecretId: this.secretName,
RecoveryWindowInDays: 7,
});

await this.client.send(deleteCommand);
}
}
36 changes: 3 additions & 33 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,5 @@
import core from "@actions/core";

/**
* Checks if a given value is empty.
*
* @param {*} value - The value to check for emptiness.
* @returns {boolean} - Returns true if the value is empty, false otherwise.
*/
export function isEmpty(value) {
if (value == null || value == undefined) {
// Handles null and undefined
return true;
}

if (typeof value === "boolean") {
// Boolean values are never empty
return false;
}

if (typeof value === "number") {
// Number values are never empty
return false;
}

if (typeof value === "string") {
// Check if the string is empty
return value.trim().length === 0;
}

// For any other types, assume it's not empty
return false;
}
import lodash from "lodash";

/**
* Get action input with default value
Expand All @@ -40,7 +10,7 @@ export function isEmpty(value) {
export function getInput(input, value = "") {
const inputValue = core.getInput(input);

if (isEmpty(inputValue)) {
if (lodash.isEmpty(inputValue)) {
return value;
}

Expand All @@ -56,7 +26,7 @@ export function getInput(input, value = "") {
export function getBooleanInput(input, value = false) {
const inputValue = core.getInput(input);

if (isEmpty(inputValue)) {
if (lodash.isEmpty(inputValue)) {
return value;
}

Expand Down
Loading

0 comments on commit 0539c25

Please sign in to comment.