From 1b4bf2f6b89fa981e2a008f9b7e508b578c55a95 Mon Sep 17 00:00:00 2001 From: Anudeep <94232161+anudeeps352@users.noreply.github.com> Date: Thu, 24 Oct 2024 17:35:25 +0530 Subject: [PATCH] feat(cli): Add functionality to operate on Secrets (#504) Co-authored-by: rajdip-b --- apps/cli/.eslintrc.cjs | 3 +- apps/cli/src/commands/secret.command.ts | 30 ++++ apps/cli/src/commands/secret/create.secret.ts | 129 ++++++++++++++++++ apps/cli/src/commands/secret/delete.secret.ts | 44 ++++++ apps/cli/src/commands/secret/get.secret.ts | 75 ++++++++++ apps/cli/src/commands/secret/list.secret.ts | 56 ++++++++ .../src/commands/secret/revisions.secret.ts | 62 +++++++++ .../src/commands/secret/rollback.secret.ts | 79 +++++++++++ apps/cli/src/commands/secret/update.secret.ts | 77 +++++++++++ apps/cli/src/index.ts | 2 + 10 files changed, 556 insertions(+), 1 deletion(-) create mode 100644 apps/cli/src/commands/secret.command.ts create mode 100644 apps/cli/src/commands/secret/create.secret.ts create mode 100644 apps/cli/src/commands/secret/delete.secret.ts create mode 100644 apps/cli/src/commands/secret/get.secret.ts create mode 100644 apps/cli/src/commands/secret/list.secret.ts create mode 100644 apps/cli/src/commands/secret/revisions.secret.ts create mode 100644 apps/cli/src/commands/secret/rollback.secret.ts create mode 100644 apps/cli/src/commands/secret/update.secret.ts diff --git a/apps/cli/.eslintrc.cjs b/apps/cli/.eslintrc.cjs index 0ac907bd..95ddcf78 100644 --- a/apps/cli/.eslintrc.cjs +++ b/apps/cli/.eslintrc.cjs @@ -26,6 +26,7 @@ module.exports = { '@typescript-eslint/space-before-function-paren': 'off', '@typescript-eslint/strict-boolean-expressions': 'off', '@typescript-eslint/prefer-nullish-coalescing': 'off', - 'space-before-function-paren': 'off' + 'space-before-function-paren': 'off', + '@typescript-eslint/member-delimiter-style': 'off' } } diff --git a/apps/cli/src/commands/secret.command.ts b/apps/cli/src/commands/secret.command.ts new file mode 100644 index 00000000..fa8e7b6d --- /dev/null +++ b/apps/cli/src/commands/secret.command.ts @@ -0,0 +1,30 @@ +import BaseCommand from '@/commands/base.command' +import CreateSecret from '@/commands/secret/create.secret' +import DeleteSecret from '@/commands/secret/delete.secret' +import ListSecret from '@/commands/secret/list.secret' +import FetchSecretRevisions from '@/commands/secret/revisions.secret' +import UpdateSecret from '@/commands/secret/update.secret' +import RollbackSecret from '@/commands/secret/rollback.secret' +import GetSecret from '@/commands/secret/get.secret' + +export default class SecretCommand extends BaseCommand { + getName(): string { + return 'secret' + } + + getDescription(): string { + return 'Manages the secrets on keyshade' + } + + getSubCommands(): BaseCommand[] { + return [ + new CreateSecret(), + new DeleteSecret(), + new GetSecret(), + new ListSecret(), + new FetchSecretRevisions(), + new UpdateSecret(), + new RollbackSecret() + ] + } +} diff --git a/apps/cli/src/commands/secret/create.secret.ts b/apps/cli/src/commands/secret/create.secret.ts new file mode 100644 index 00000000..b6745fb0 --- /dev/null +++ b/apps/cli/src/commands/secret/create.secret.ts @@ -0,0 +1,129 @@ +import BaseCommand from '@/commands/base.command' +import { text } from '@clack/prompts' +import ControllerInstance from '@/util/controller-instance' +import { Logger } from '@/util/logger' +import { + type CommandActionData, + type CommandArgument, + type CommandOption +} from '@/types/command/command.types' + +export default class CreateSecret extends BaseCommand { + getName(): string { + return 'create' + } + + getDescription(): string { + return 'Creates a secret' + } + + getArguments(): CommandArgument[] { + return [ + { + name: '', + description: 'Slug of the project under which you want to create' + } + ] + } + + getOptions(): CommandOption[] { + return [ + { + short: '-n', + long: '--name ', + description: 'Name of the secret. Must be unique across the project' + }, + { + short: '-d', + long: '--note ', + description: 'A note describing the usage of the secret.' + }, + { + short: '-r', + long: '--rotate-after ', + description: + ' The duration in days after which the value of the secret should be rotated. Accepted values are `24`, `168`, `720`, `8769` and `never`. Defaults to `never`.' + }, + { + short: '-e', + long: '--entries [entries...]', + description: + 'An array of key-value pair (value and environmentSlug) for the secret.' + } + ] + } + + async action({ args, options }: CommandActionData): Promise { + const { name, note, rotateAfter, entries } = await this.parseInput(options) + const [projectSlug] = args + + if (!projectSlug) { + Logger.error('Project slug is required') + return + } + + const { data, error, success } = + await ControllerInstance.getInstance().secretController.createSecret( + { + name, + note, + rotateAfter, + entries, + projectSlug + }, + this.headers + ) + + if (success) { + Logger.info(`Secret ${data.name} (${data.slug}) created successfully!`) + Logger.info(`Created at ${data.createdAt}`) + Logger.info(`Updated at ${data.updatedAt}`) + } else { + Logger.error(`Failed to create secret: ${error.message}`) + } + } + + private async parseInput(options: CommandActionData['options']): Promise<{ + name: string + note?: string + rotateAfter?: '24' | '168' | '720' | '8760' | 'never' + entries: Array<{ value: string; environmentSlug: string }> + }> { + let { name, note, rotateAfter } = options + const { entries } = options + + if (!name) { + name = await text({ + message: 'Enter the name of secret', + placeholder: 'My Secret' + }) + } + + if (!entries) { + throw new Error('Entries is required') + } + + if (!note) { + note = name + } + + const parsedEntries = entries.map((entry) => { + const entryObj: { value: string; environmentSlug: string } = { + value: '', + environmentSlug: '' + } + entry.split(' ').forEach((pair) => { + const [key, value] = pair.split('=') + entryObj[key] = value + }) + return entryObj + }) + + return { + name, + note, + rotateAfter, + entries: parsedEntries + } + } +} diff --git a/apps/cli/src/commands/secret/delete.secret.ts b/apps/cli/src/commands/secret/delete.secret.ts new file mode 100644 index 00000000..1e709e00 --- /dev/null +++ b/apps/cli/src/commands/secret/delete.secret.ts @@ -0,0 +1,44 @@ +import type { + CommandActionData, + CommandArgument +} from '@/types/command/command.types' +import BaseCommand from '@/commands/base.command' +import { Logger } from '@/util/logger' +import ControllerInstance from '@/util/controller-instance' + +export default class DeleteSecret extends BaseCommand { + getName(): string { + return 'delete' + } + + getDescription(): string { + return 'Deletes a secret' + } + + getArguments(): CommandArgument[] { + return [ + { + name: '', + description: 'Slug of the secret that you want to delete.' + } + ] + } + + async action({ args }: CommandActionData): Promise { + const [secretSlug] = args + + const { error, success } = + await ControllerInstance.getInstance().secretController.deleteSecret( + { + secretSlug + }, + this.headers + ) + + if (success) { + Logger.info(`Secret ${secretSlug} deleted successfully!`) + } else { + Logger.error(`Failed to delete secret: ${error.message}`) + } + } +} diff --git a/apps/cli/src/commands/secret/get.secret.ts b/apps/cli/src/commands/secret/get.secret.ts new file mode 100644 index 00000000..b015ed23 --- /dev/null +++ b/apps/cli/src/commands/secret/get.secret.ts @@ -0,0 +1,75 @@ +import type { + CommandActionData, + CommandArgument, + CommandOption +} from '@/types/command/command.types' +import BaseCommand from '@/commands/base.command' +import ControllerInstance from '@/util/controller-instance' +import { Logger } from '@/util/logger' + +export default class GetSecret extends BaseCommand { + getName(): string { + return 'get' + } + + getDescription(): string { + return 'Get all secrets under a project' + } + + getArguments(): CommandArgument[] { + return [ + { + name: '', + description: 'Slug of the project whose secrets you want.' + } + ] + } + + getOptions(): CommandOption[] { + return [ + { + short: '-d', + long: '--decrypt-value', + description: + 'Set this to true if the project contains the private key. If set to true, the values of the secret will be in plaintext format' + } + ] + } + + async action({ args, options }: CommandActionData): Promise { + const [projectSlug] = args + const { decryptValue } = await this.parseInput(options) + const { data, error, success } = + await ControllerInstance.getInstance().secretController.getAllSecretsOfProject( + { + projectSlug, + decryptValue + }, + this.headers + ) + + if (success) { + const secrets = data.items + + if (secrets.length > 0) { + data.items.forEach((item: any) => { + const secret = item.secret + Logger.info(`- ${secret.name} (${secret.slug})`) + }) + } else { + Logger.info('No secrets found') + } + } else { + Logger.error(`Failed fetching secrets: ${error.message}`) + } + } + + private async parseInput(options: CommandActionData['options']): Promise<{ + decryptValue: boolean + }> { + const { decryptValue = false } = options // defaults to false + return { + decryptValue + } + } +} diff --git a/apps/cli/src/commands/secret/list.secret.ts b/apps/cli/src/commands/secret/list.secret.ts new file mode 100644 index 00000000..cfb78992 --- /dev/null +++ b/apps/cli/src/commands/secret/list.secret.ts @@ -0,0 +1,56 @@ +import type { + CommandActionData, + CommandArgument +} from '@/types/command/command.types' +import BaseCommand from '@/commands/base.command' +import ControllerInstance from '@/util/controller-instance' +import { Logger } from '@/util/logger' + +export default class ListSecret extends BaseCommand { + getName(): string { + return 'list' + } + + getDescription(): string { + return 'List all secrets under a project and environment' + } + + getArguments(): CommandArgument[] { + return [ + { + name: '', + description: 'Slug of the project whose secrets you want.' + }, + { + name: '', + description: 'Slug of the environment whose secrets you want.' + } + ] + } + + async action({ args }: CommandActionData): Promise { + const [projectSlug, environmentSlug] = args + const { data, error, success } = + await ControllerInstance.getInstance().secretController.getAllSecretsOfEnvironment( + { + projectSlug, + environmentSlug + }, + this.headers + ) + + if (success) { + console.log(data) + const secrets = data + if (secrets.length > 0) { + data.forEach((secret: any) => { + Logger.info(`- ${secret.name} (${secret.value})`) + }) + } else { + Logger.info('No secrets found') + } + } else { + Logger.error(`Failed fetching secrets: ${error.message}`) + } + } +} diff --git a/apps/cli/src/commands/secret/revisions.secret.ts b/apps/cli/src/commands/secret/revisions.secret.ts new file mode 100644 index 00000000..9a031b63 --- /dev/null +++ b/apps/cli/src/commands/secret/revisions.secret.ts @@ -0,0 +1,62 @@ +import type { + CommandActionData, + CommandArgument +} from '@/types/command/command.types' +import BaseCommand from '@/commands/base.command' +import ControllerInstance from '@/util/controller-instance' +import { Logger } from '@/util/logger' + +export default class FetchSecretRevisions extends BaseCommand { + getName(): string { + return 'revisions' + } + + getDescription(): string { + return 'Fetch all revisions of a secret' + } + + getArguments(): CommandArgument[] { + return [ + { + name: '', + description: 'Slug of the secret whose revisions you want.' + }, + { + name: '', + description: 'Environment slug of the secret whose revisions you want.' + } + ] + } + + async action({ args }: CommandActionData): Promise { + const [secretSlug, environmentSlug] = args + + const { data, error, success } = + await ControllerInstance.getInstance().secretController.getRevisionsOfSecret( + { + secretSlug, + environmentSlug + }, + this.headers + ) + + if (success) { + const revisions = data.items + if (revisions.length > 0) { + data.items.forEach((revision: any) => { + Logger.info(`Id ${revision.id}`) + Logger.info(`value ${revision.value}`) + Logger.info(`version ${revision.version}`) + Logger.info(`secretID ${revision.secretId}`) + Logger.info(`Created On ${revision.createdOn}`) + Logger.info(`Created By Id ${revision.createdById}`) + Logger.info(`environmentId ${revision.environmentId}`) + }) + } else { + Logger.info('No revisions found') + } + } else { + Logger.error(`Failed fetching revisions: ${error.message}`) + } + } +} diff --git a/apps/cli/src/commands/secret/rollback.secret.ts b/apps/cli/src/commands/secret/rollback.secret.ts new file mode 100644 index 00000000..6892c6f1 --- /dev/null +++ b/apps/cli/src/commands/secret/rollback.secret.ts @@ -0,0 +1,79 @@ +import type { + CommandActionData, + CommandArgument, + CommandOption +} from '@/types/command/command.types' +import BaseCommand from '@/commands/base.command' +import { Logger } from '@/util/logger' +import ControllerInstance from '@/util/controller-instance' + +export default class RollbackSecret extends BaseCommand { + getName(): string { + return 'rollback' + } + + getDescription(): string { + return 'Rollbacks a secret' + } + + getArguments(): CommandArgument[] { + return [ + { + name: '', + description: 'Slug of the secret that you want to rollback' + } + ] + } + + getOptions(): CommandOption[] { + return [ + { + short: '-v', + long: '--version ', + description: 'Version of the secret to which you want to rollback' + }, + { + short: '-e', + long: '--environmentSlug ', + description: + 'Slug of the environment of the secret to which you want to rollback' + } + ] + } + + async action({ args, options }: CommandActionData): Promise { + const [secretSlug] = args + const { environmentSlug, version } = await this.parseInput(options) + const { data, error, success } = + await ControllerInstance.getInstance().secretController.rollbackSecret( + { + environmentSlug, + version, + secretSlug + }, + this.headers + ) + + if (success) { + Logger.info(`Secret ${data.name} (${data.slug}) updated successfully!`) + Logger.info(`Created at ${data.createdAt}`) + Logger.info(`Updated at ${data.updatedAt}`) + Logger.info(`Note: ${data.note}`) + Logger.info(`rotateAfter: ${data.rotateAfter}`) + } else { + Logger.error(`Failed to update secret: ${error.message}`) + } + } + + private async parseInput(options: CommandActionData['options']): Promise<{ + environmentSlug: string + version: string + }> { + const { environmentSlug, version } = options + + return { + environmentSlug, + version + } + } +} diff --git a/apps/cli/src/commands/secret/update.secret.ts b/apps/cli/src/commands/secret/update.secret.ts new file mode 100644 index 00000000..d31b78e7 --- /dev/null +++ b/apps/cli/src/commands/secret/update.secret.ts @@ -0,0 +1,77 @@ +import type { + CommandActionData, + CommandArgument, + CommandOption +} from '@/types/command/command.types' +import BaseCommand from '@/commands/base.command' +import { Logger } from '@/util/logger' +import ControllerInstance from '@/util/controller-instance' + +export default class UpdateSecret extends BaseCommand { + getName(): string { + return 'update' + } + + getDescription(): string { + return 'Updates a secret' + } + + getArguments(): CommandArgument[] { + return [ + { + name: '', + description: 'Slug of the secret that you want to update' + } + ] + } + + getOptions(): CommandOption[] { + return [ + { + short: '-n', + long: '--name ', + description: 'Name of the secret' + }, + { + short: '-d', + long: '--note ', + description: ' An optional note describing the usage of the secret.' + }, + { + short: '-r', + long: '--rotate-after', + description: + 'The duration in days after which the value of the secret should be rotated. Accepted values are `24`, `168`, `720`, `8769` and `never`. Defaults to `never`.', + defaultValue: 'never' + }, + { + short: '-e', + long: '--entries [entries...]', + description: 'An array of values for the secret.' + } + ] + } + + async action({ args, options }: CommandActionData): Promise { + const [secretSlug] = args + + const { data, error, success } = + await ControllerInstance.getInstance().secretController.updateSecret( + { + secretSlug, + ...options + }, + this.headers + ) + + if (success) { + Logger.info(`Secret ${data.name} (${data.slug}) updated successfully!`) + Logger.info(`Created at ${data.createdAt}`) + Logger.info(`Updated at ${data.updatedAt}`) + Logger.info(`Note: ${data.note}`) + Logger.info(`rotateAfter: ${data.rotateAfter}`) + } else { + Logger.error(`Failed to update secret: ${error.message}`) + } + } +} diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 793765a3..3f908dd0 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -7,6 +7,7 @@ import EnvironmentCommand from './commands/environment.command' import WorkspaceCommand from '@/commands/workspace.command' import ScanCommand from '@/commands/scan.command' import ProjectCommand from './commands/project.command' +import SecretCommand from './commands/secret.command' const program = new Command() @@ -21,6 +22,7 @@ const COMMANDS: BaseCommand[] = [ new WorkspaceCommand(), new ProjectCommand(), new EnvironmentCommand(), + new SecretCommand(), new ScanCommand() ]