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

feat: add delete secret action #12

Merged
merged 1 commit into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
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
Loading