diff --git a/npmDepsHash b/npmDepsHash index d1ec0c02..34721a94 100644 --- a/npmDepsHash +++ b/npmDepsHash @@ -1 +1 @@ -sha256-iXj2QmiuiYudb/V9FRTAPU52ns1i2g4H82Rb2NrWLNo= +sha256-FDJmb93Xgols7SXWv/ETyDm0uggBmVuVWiD+vJOcKcc= diff --git a/package-lock.json b/package-lock.json index 65ebf1a0..f3dda093 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "nexpect": "^0.6.0", "node-gyp-build": "^4.4.0", "nodemon": "^3.0.1", - "polykey": "^1.10.0", + "polykey": "^1.11.1", "prettier": "^3.0.0", "shelljs": "^0.8.5", "shx": "^0.3.4", @@ -7602,9 +7602,9 @@ } }, "node_modules/polykey": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/polykey/-/polykey-1.10.0.tgz", - "integrity": "sha512-An0lrW2KK+zO0J1Ig7j5cZ7ZwaEc1XGdsCNx0bx8a7UxusBc+A76WtdLuEhH27yiVNfBVgruMny+4htTE8uDiw==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/polykey/-/polykey-1.11.1.tgz", + "integrity": "sha512-2OEvd7LG4JP/YLPh6d3JQWbIlFChBRTgUupwkKqVyJBQCQ+E38gmJYeHD3RclqI0riT0vLlE+3Kosm7K8htC7w==", "dev": true, "dependencies": { "@matrixai/async-cancellable": "^1.1.1", diff --git a/package.json b/package.json index 2b514248..61d3f6fb 100644 --- a/package.json +++ b/package.json @@ -150,7 +150,7 @@ "nexpect": "^0.6.0", "node-gyp-build": "^4.4.0", "nodemon": "^3.0.1", - "polykey": "^1.10.0", + "polykey": "^1.11.1", "prettier": "^3.0.0", "shelljs": "^0.8.5", "shx": "^0.3.4", diff --git a/src/secrets/CommandDelete.ts b/src/secrets/CommandRemove.ts similarity index 66% rename from src/secrets/CommandDelete.ts rename to src/secrets/CommandRemove.ts index b7f8732d..2768b9d5 100644 --- a/src/secrets/CommandDelete.ts +++ b/src/secrets/CommandRemove.ts @@ -8,18 +8,20 @@ import * as binProcessors from '../utils/processors'; class CommandDelete extends CommandPolykey { constructor(...args: ConstructorParameters) { super(...args); - this.name('delete'); - this.aliases(['del', 'rm']); - this.description('Delete a Secret from a Specified Vault'); + this.name('rm'); + this.description('Delete a Secret from a specified Vault'); this.argument( - '', - 'Path to the secret that to be deleted, specified as :', - binParsers.parseSecretPathValue, + '', + 'Path to one or more secret to be deleted, each specified as :', ); this.addOption(binOptions.nodeId); this.addOption(binOptions.clientHost); this.addOption(binOptions.clientPort); - this.action(async (secretPath, options) => { + this.addOption(binOptions.recursive); + this.action(async (secretPaths, options) => { + secretPaths = secretPaths.map((path: string) => + binParsers.parseSecretPathValue(path), + ); const { default: PolykeyClient } = await import( 'polykey/dist/PolykeyClient' ); @@ -31,11 +33,10 @@ class CommandDelete extends CommandPolykey { this.fs, this.logger.getChild(binProcessors.processClientOptions.name), ); - const auth = await binProcessors.processAuthentication( + const meta = await binProcessors.processAuthentication( options.passwordFile, this.fs, ); - let pkClient: PolykeyClient; this.exitHandlers.handlers.push(async () => { if (pkClient != null) await pkClient.stop(); @@ -45,20 +46,16 @@ class CommandDelete extends CommandPolykey { nodeId: clientOptions.nodeId, host: clientOptions.clientHost, port: clientOptions.clientPort, - options: { - nodePath: options.nodePath, - }, + options: { nodePath: options.nodePath }, logger: this.logger.getChild(PolykeyClient.name), }); - await binUtils.retryAuthentication( - (auth) => - pkClient.rpcClient.methods.vaultsSecretsDelete({ - metadata: auth, - nameOrId: secretPath[0], - secretName: secretPath[1], - }), - auth, - ); + await binUtils.retryAuthentication(async (auth) => { + await pkClient.rpcClient.methods.vaultsSecretsRemove({ + metadata: auth, + secretNames: secretPaths, + options: { recursive: options.recursive }, + }); + }, meta); } finally { if (pkClient! != null) await pkClient.stop(); } diff --git a/src/secrets/CommandSecrets.ts b/src/secrets/CommandSecrets.ts index 183fbf5f..7925ceb5 100644 --- a/src/secrets/CommandSecrets.ts +++ b/src/secrets/CommandSecrets.ts @@ -1,5 +1,4 @@ import CommandCreate from './CommandCreate'; -import CommandDelete from './CommandDelete'; import CommandDir from './CommandDir'; import CommandEdit from './CommandEdit'; import CommandEnv from './CommandEnv'; @@ -7,6 +6,7 @@ import CommandGet from './CommandGet'; import CommandList from './CommandList'; import CommandMkdir from './CommandMkdir'; import CommandRename from './CommandRename'; +import CommandRemove from './CommandRemove'; import CommandUpdate from './CommandUpdate'; import CommandStat from './CommandStat'; import CommandPolykey from '../CommandPolykey'; @@ -17,7 +17,6 @@ class CommandSecrets extends CommandPolykey { this.name('secrets'); this.description('Secrets Operations'); this.addCommand(new CommandCreate(...args)); - this.addCommand(new CommandDelete(...args)); this.addCommand(new CommandDir(...args)); this.addCommand(new CommandEdit(...args)); this.addCommand(new CommandEnv(...args)); @@ -25,6 +24,7 @@ class CommandSecrets extends CommandPolykey { this.addCommand(new CommandList(...args)); this.addCommand(new CommandMkdir(...args)); this.addCommand(new CommandRename(...args)); + this.addCommand(new CommandRemove(...args)); this.addCommand(new CommandUpdate(...args)); this.addCommand(new CommandStat(...args)); } diff --git a/src/utils/options.ts b/src/utils/options.ts index 71fc3e9e..7ed0c1b7 100644 --- a/src/utils/options.ts +++ b/src/utils/options.ts @@ -302,6 +302,11 @@ const order = new commander.Option( .choices(['asc', 'desc']) .default('asc'); +const recursive = new commander.Option( + '--recursive', + 'If enabled, specified directories will be removed along with their contents', +).default(false); + export { nodePath, format, @@ -343,4 +348,5 @@ export { events, limit, order, + recursive, }; diff --git a/tests/secrets/delete.test.ts b/tests/secrets/delete.test.ts deleted file mode 100644 index 1e6c942b..00000000 --- a/tests/secrets/delete.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { VaultName } from 'polykey/dist/vaults/types'; -import path from 'path'; -import fs from 'fs'; -import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; -import PolykeyAgent from 'polykey/dist/PolykeyAgent'; -import { vaultOps } from 'polykey/dist/vaults'; -import * as keysUtils from 'polykey/dist/keys/utils'; -import * as testUtils from '../utils'; - -describe('commandDeleteSecret', () => { - const password = 'password'; - const logger = new Logger('CLI Test', LogLevel.WARN, [new StreamHandler()]); - let dataDir: string; - let polykeyAgent: PolykeyAgent; - let command: Array; - - beforeEach(async () => { - dataDir = await fs.promises.mkdtemp( - path.join(globalThis.tmpDir, 'polykey-test-'), - ); - polykeyAgent = await PolykeyAgent.createPolykeyAgent({ - password, - options: { - nodePath: dataDir, - agentServiceHost: '127.0.0.1', - clientServiceHost: '127.0.0.1', - keys: { - passwordOpsLimit: keysUtils.passwordOpsLimits.min, - passwordMemLimit: keysUtils.passwordMemLimits.min, - strictMemoryLock: false, - }, - }, - logger: logger, - }); - }); - afterEach(async () => { - await polykeyAgent.stop(); - await fs.promises.rm(dataDir, { - force: true, - recursive: true, - }); - }); - - test('should delete secrets', async () => { - const vaultName = 'Vault2' as VaultName; - const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); - - await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { - await vaultOps.addSecret(vault, 'MySecret', 'this is the secret'); - const list = await vaultOps.listSecrets(vault); - expect(list.sort()).toStrictEqual(['MySecret']); - }); - - command = ['secrets', 'delete', '-np', dataDir, `${vaultName}:MySecret`]; - - const result = await testUtils.pkStdio([...command], { - env: { - PK_PASSWORD: password, - }, - cwd: dataDir, - }); - expect(result.exitCode).toBe(0); - - await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { - const list = await vaultOps.listSecrets(vault); - expect(list.sort()).toStrictEqual([]); - }); - }); -}); diff --git a/tests/secrets/remove.test.ts b/tests/secrets/remove.test.ts new file mode 100644 index 00000000..561c57d2 --- /dev/null +++ b/tests/secrets/remove.test.ts @@ -0,0 +1,243 @@ +import type { VaultName } from 'polykey/dist/vaults/types'; +import path from 'path'; +import fs from 'fs'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import PolykeyAgent from 'polykey/dist/PolykeyAgent'; +import { vaultOps } from 'polykey/dist/vaults'; +import * as keysUtils from 'polykey/dist/keys/utils'; +import * as testUtils from '../utils'; + +describe('commandRemoveSecret', () => { + const password = 'password'; + const logger = new Logger('CLI Test', LogLevel.WARN, [new StreamHandler()]); + let dataDir: string; + let polykeyAgent: PolykeyAgent; + + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(globalThis.tmpDir, 'polykey-test-'), + ); + polykeyAgent = await PolykeyAgent.createPolykeyAgent({ + password, + options: { + nodePath: dataDir, + agentServiceHost: '127.0.0.1', + clientServiceHost: '127.0.0.1', + keys: { + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + }, + }, + logger: logger, + }); + }); + afterEach(async () => { + await polykeyAgent.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + + test('should remove secret', async () => { + const vaultName = 'Vault2' as VaultName; + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + await vaultOps.addSecret(vault, 'MySecret', 'this is the secret'); + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual(['MySecret']); + }); + + const command = ['secrets', 'rm', '-np', dataDir, `${vaultName}:MySecret`]; + + const result = await testUtils.pkStdio(command, { + env: { PK_PASSWORD: password }, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual([]); + }); + }); + + test('should remove multiple secrets', async () => { + const vaultName = 'Vault2' as VaultName; + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + const secretNames = ['secret1', 'secret2', 'secret3']; + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + for (const secretName of secretNames) { + await vaultOps.addSecret(vault, secretName, secretName); + } + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual(secretNames); + }); + + const secretPaths = secretNames.map((v) => `${vaultName}:${v}`); + const command = ['secrets', 'rm', '-np', dataDir, ...secretPaths]; + + const result = await testUtils.pkStdio(command, { + env: { PK_PASSWORD: password }, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual([]); + }); + }); + + test('should make one log message for deleting multiple secrets', async () => { + const vaultName = 'Vault2' as VaultName; + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + const secretNames = ['secret1', 'secret2', 'secret3']; + let vaultLogLength: number; + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + for (const secretName of secretNames) { + await vaultOps.addSecret(vault, secretName, secretName); + } + vaultLogLength = (await vault.log()).length; + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual(secretNames); + }); + + const secretPaths = secretNames.map((v) => `${vaultName}:${v}`); + const command = ['secrets', 'rm', '-np', dataDir, ...secretPaths]; + + const result = await testUtils.pkStdio(command, { + env: { PK_PASSWORD: password }, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual([]); + expect((await vault.log()).length).toEqual(vaultLogLength + 1); + }); + }); + + test('should remove secrets recursively', async () => { + const vaultName = 'Vault2' as VaultName; + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + const secretDir = 'secretDir'; + const secretNames = ['secret1', 'secret2', 'secret3'].map((v) => + path.join(secretDir, v), + ); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + await vaultOps.mkdir(vault, secretDir); + for (const secretName of secretNames) { + await vaultOps.addSecret(vault, secretName, secretName); + } + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual(secretNames); + }); + + const command = [ + 'secrets', + 'rm', + '-np', + dataDir, + `${vaultName}:secretDir`, + '--recursive', + ]; + + const result = await testUtils.pkStdio(command, { + env: { PK_PASSWORD: password }, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual([]); + }); + }); + + test('should fail to remove directory without recursive flag', async () => { + const vaultName = 'Vault2' as VaultName; + const vaultId = await polykeyAgent.vaultManager.createVault(vaultName); + const secretDir = 'secretDir'; + const secretNames = ['secret1', 'secret2', 'secret3'].map((v) => + path.join(secretDir, v), + ); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + await vaultOps.mkdir(vault, secretDir); + for (const secretName of secretNames) { + await vaultOps.addSecret(vault, secretName, secretName); + } + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual(secretNames); + }); + + const command = ['secrets', 'rm', '-np', dataDir, `${vaultName}:secretDir`]; + + const result = await testUtils.pkStdio(command, { + env: { PK_PASSWORD: password }, + cwd: dataDir, + }); + expect(result.exitCode).not.toBe(0); + + await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => { + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual(secretNames); + }); + }); + + test('should remove files from multiple vaults in the same command', async () => { + const vaultName1 = 'Vault2-1' as VaultName; + const vaultName2 = 'Vault2-2' as VaultName; + const vaultId1 = await polykeyAgent.vaultManager.createVault(vaultName1); + const vaultId2 = await polykeyAgent.vaultManager.createVault(vaultName2); + const secretNames1 = ['secret1', 'secret2', 'secret3']; + const secretNames2 = ['secret4', 'secret5']; + + await polykeyAgent.vaultManager.withVaults([vaultId1], async (vault) => { + for (const secretName of secretNames1) { + await vaultOps.addSecret(vault, secretName, secretName); + } + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual(secretNames1); + }); + + await polykeyAgent.vaultManager.withVaults([vaultId2], async (vault) => { + for (const secretName of secretNames2) { + await vaultOps.addSecret(vault, secretName, secretName); + } + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual(secretNames2); + }); + + const command = [ + 'secrets', + 'rm', + '-np', + dataDir, + ...secretNames1.map((v) => `${vaultName1}:${v}`), + ...secretNames2.map((v) => `${vaultName2}:${v}`), + ]; + + const result = await testUtils.pkStdio(command, { + env: { PK_PASSWORD: password }, + cwd: dataDir, + }); + expect(result.exitCode).toBe(0); + + await polykeyAgent.vaultManager.withVaults([vaultId1], async (vault) => { + const list = await vaultOps.listSecrets(vault); + expect(list).toStrictEqual([]); + }); + await polykeyAgent.vaultManager.withVaults([vaultId2], async (vault) => { + const list = await vaultOps.listSecrets(vault); + expect(list).toStrictEqual([]); + }); + }); +});