diff --git a/package-lock.json b/package-lock.json index 1ac7ccd3b..b4feb69d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "@matrixai/mdns": "^1.2.6", "@matrixai/quic": "^1.2.10", "@matrixai/resources": "^1.1.5", - "@matrixai/rpc": "^0.5.1", + "@matrixai/rpc": "^0.6.0", "@matrixai/timer": "^1.1.3", "@matrixai/workers": "^1.3.7", "@matrixai/ws": "^1.1.7", @@ -1696,9 +1696,9 @@ "integrity": "sha512-m/DEZEe3wHqWEPTyoBtzFF6U9vWYhEnQtGgwvqiAlTxTM0rk96UBpWjDZCTF/vYG11ZlmlQFtg5H+zGgbjaB3Q==" }, "node_modules/@matrixai/rpc": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@matrixai/rpc/-/rpc-0.5.1.tgz", - "integrity": "sha512-crqC2J7jGSQhPwHOoO0dWmUXx5JVb1Cfl4bXB9dcc9JobetU3k0rf5iC4BF7JGCtIxQPRJnT63y9+6EGzozZgA==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@matrixai/rpc/-/rpc-0.6.0.tgz", + "integrity": "sha512-ENjJO2h7CmPLaHhObHs2nvpv98YZPxa79/jf+TqEPEfbhE1BkNCys9pXDE/CYDP9vxb4seS39WkR9cNivQU50A==", "dependencies": { "@matrixai/async-init": "^1.10.0", "@matrixai/contexts": "^1.2.0", diff --git a/package.json b/package.json index 3fc2979a9..68b20df14 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "@matrixai/mdns": "^1.2.6", "@matrixai/quic": "^1.2.10", "@matrixai/resources": "^1.1.5", - "@matrixai/rpc": "^0.5.1", + "@matrixai/rpc": "^0.6.0", "@matrixai/timer": "^1.1.3", "@matrixai/workers": "^1.3.7", "@matrixai/ws": "^1.1.7", diff --git a/src/client/callers/index.ts b/src/client/callers/index.ts index 4f4f805ba..48189df7c 100644 --- a/src/client/callers/index.ts +++ b/src/client/callers/index.ts @@ -63,6 +63,7 @@ import vaultsPermissionUnset from './vaultsPermissionUnset'; import vaultsPull from './vaultsPull'; import vaultsRename from './vaultsRename'; import vaultsScan from './vaultsScan'; +import vaultsSecretsCat from './vaultsSecretsCat'; import vaultsSecretsEnv from './vaultsSecretsEnv'; import vaultsSecretsGet from './vaultsSecretsGet'; import vaultsSecretsList from './vaultsSecretsList'; @@ -144,6 +145,7 @@ const clientManifest = { vaultsPull, vaultsRename, vaultsScan, + vaultsSecretsCat, vaultsSecretsEnv, vaultsSecretsGet, vaultsSecretsList, @@ -224,6 +226,7 @@ export { vaultsPull, vaultsRename, vaultsScan, + vaultsSecretsCat, vaultsSecretsEnv, vaultsSecretsGet, vaultsSecretsList, diff --git a/src/client/callers/vaultsSecretsCat.ts b/src/client/callers/vaultsSecretsCat.ts new file mode 100644 index 000000000..1671a7966 --- /dev/null +++ b/src/client/callers/vaultsSecretsCat.ts @@ -0,0 +1,12 @@ +import type { HandlerTypes } from '@matrixai/rpc'; +import type VaultsSecretsCat from '../handlers/VaultsSecretsCat'; +import { DuplexCaller } from '@matrixai/rpc'; + +type CallerTypes = HandlerTypes; + +const vaultsSecretsCat = new DuplexCaller< + CallerTypes['input'], + CallerTypes['output'] +>(); + +export default vaultsSecretsCat; diff --git a/src/client/callers/vaultsSecretsGet.ts b/src/client/callers/vaultsSecretsGet.ts index 4c00b0c4b..7e7172255 100644 --- a/src/client/callers/vaultsSecretsGet.ts +++ b/src/client/callers/vaultsSecretsGet.ts @@ -1,10 +1,10 @@ import type { HandlerTypes } from '@matrixai/rpc'; import type VaultsSecretsGet from '../handlers/VaultsSecretsGet'; -import { DuplexCaller } from '@matrixai/rpc'; +import { ServerCaller } from '@matrixai/rpc'; type CallerTypes = HandlerTypes; -const vaultsSecretsGet = new DuplexCaller< +const vaultsSecretsGet = new ServerCaller< CallerTypes['input'], CallerTypes['output'] >(); diff --git a/src/client/callers/vaultsSecretsRemove.ts b/src/client/callers/vaultsSecretsRemove.ts index e2bda1e28..8362ce46a 100644 --- a/src/client/callers/vaultsSecretsRemove.ts +++ b/src/client/callers/vaultsSecretsRemove.ts @@ -1,10 +1,10 @@ import type { HandlerTypes } from '@matrixai/rpc'; import type VaultsSecretsRemove from '../handlers/VaultsSecretsRemove'; -import { ClientCaller } from '@matrixai/rpc'; +import { DuplexCaller } from '@matrixai/rpc'; type CallerTypes = HandlerTypes; -const vaultsSecretsRemove = new ClientCaller< +const vaultsSecretsRemove = new DuplexCaller< CallerTypes['input'], CallerTypes['output'] >(); diff --git a/src/client/handlers/VaultsSecretsCat.ts b/src/client/handlers/VaultsSecretsCat.ts new file mode 100644 index 000000000..2770b3b0b --- /dev/null +++ b/src/client/handlers/VaultsSecretsCat.ts @@ -0,0 +1,71 @@ +import type { DB } from '@matrixai/db'; +import type { + ClientRPCRequestParams, + ClientRPCResponseResult, + ContentOrErrorMessage, + SecretIdentifierMessage, +} from '../types'; +import type VaultManager from '../../vaults/VaultManager'; +import { DuplexHandler } from '@matrixai/rpc'; +import * as vaultsUtils from '../../vaults/utils'; +import * as vaultsErrors from '../../vaults/errors'; +import * as vaultOps from '../../vaults/VaultOps'; + +// This method takes in multiple secret paths, and either returns the file +// contents, or an `ErrorMessage` signifying the error. To read a single secret +// instead, refer to `VaultsSecretsGet`. +class VaultsSecretsCat extends DuplexHandler< + { + db: DB; + vaultManager: VaultManager; + }, + ClientRPCRequestParams, + ClientRPCResponseResult +> { + public handle = async function* ( + input: AsyncIterable>, + ): AsyncGenerator> { + const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = + this.container; + yield* db.withTransactionG(async function* (tran): AsyncGenerator< + ClientRPCResponseResult + > { + // As we need to preserve the order of parameters, we need to loop over + // them individually, as grouping them would make them go out of order. + for await (const secretIdentiferMessage of input) { + const { nameOrId, secretName } = secretIdentiferMessage; + const vaultIdFromName = await vaultManager.getVaultId(nameOrId, tran); + const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(nameOrId); + if (vaultId == null) throw new vaultsErrors.ErrorVaultsVaultUndefined(); + yield await vaultManager.withVaults( + [vaultId], + async (vault) => { + try { + const content = await vaultOps.getSecret(vault, secretName); + return { + type: 'success', + success: true, + secretContent: content.toString('binary'), + }; + } catch (e) { + if ( + e instanceof vaultsErrors.ErrorSecretsSecretUndefined || + e instanceof vaultsErrors.ErrorSecretsIsDirectory + ) { + return { + type: 'error', + code: e.cause.code, + reason: secretName, + }; + } + throw e; + } + }, + tran, + ); + } + }); + }; +} + +export default VaultsSecretsCat; diff --git a/src/client/handlers/VaultsSecretsGet.ts b/src/client/handlers/VaultsSecretsGet.ts index 361016490..a0fa0f94d 100644 --- a/src/client/handlers/VaultsSecretsGet.ts +++ b/src/client/handlers/VaultsSecretsGet.ts @@ -2,72 +2,44 @@ import type { DB } from '@matrixai/db'; import type { ClientRPCRequestParams, ClientRPCResponseResult, - ContentWithErrorMessage, + ContentMessage, SecretIdentifierMessage, } from '../types'; import type VaultManager from '../../vaults/VaultManager'; -import { DuplexHandler } from '@matrixai/rpc'; +import { ServerHandler } from '@matrixai/rpc'; import * as vaultsUtils from '../../vaults/utils'; import * as vaultsErrors from '../../vaults/errors'; import * as vaultOps from '../../vaults/VaultOps'; -class VaultsSecretsGet extends DuplexHandler< +// This method only returns the contents of a single secret, and throws an error +// if the secret couldn't be read. To read multiple secrets, refer to +// `VaultsSecretsCat`. +class VaultsSecretsGet extends ServerHandler< { db: DB; vaultManager: VaultManager; }, ClientRPCRequestParams, - ClientRPCResponseResult + ClientRPCResponseResult > { public handle = async function* ( - input: AsyncIterable>, - _cancel, - _meta, - ctx, - ): AsyncGenerator> { - if (ctx.signal.aborted) throw ctx.signal.reason; + input: ClientRPCRequestParams, + ): AsyncGenerator> { const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = this.container; - yield* db.withTransactionG(async function* (tran): AsyncGenerator< - ClientRPCResponseResult - > { - if (ctx.signal.aborted) throw ctx.signal.reason; - // As we need to preserve the order of parameters, we need to loop over - // them individually, as grouping them would make them go out of order. - let metadata: any = undefined; - for await (const secretIdentiferMessage of input) { - if (ctx.signal.aborted) throw ctx.signal.reason; - if (metadata == null) metadata = secretIdentiferMessage.metadata ?? {}; - const { nameOrId, secretName } = secretIdentiferMessage; - const vaultIdFromName = await vaultManager.getVaultId(nameOrId, tran); - const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(nameOrId); - if (vaultId == null) throw new vaultsErrors.ErrorVaultsVaultUndefined(); - yield await vaultManager.withVaults( - [vaultId], - async (vault) => { - try { - const content = await vaultOps.getSecret(vault, secretName); - return { secretContent: content.toString('binary') }; - } catch (e) { - if (metadata?.options?.continueOnError === true) { - if (e instanceof vaultsErrors.ErrorSecretsSecretUndefined) { - return { - secretContent: '', - error: `${e.name}: ${secretName}: No such secret or directory\n`, - }; - } else if (e instanceof vaultsErrors.ErrorSecretsIsDirectory) { - return { - secretContent: '', - error: `${e.name}: ${secretName}: Is a directory\n`, - }; - } - } - throw e; - } - }, - tran, - ); - } + yield await db.withTransactionF(async (tran) => { + const vaultIdFromName = await vaultManager.getVaultId( + input.nameOrId, + tran, + ); + const vaultId = + vaultIdFromName ?? vaultsUtils.decodeVaultId(input.nameOrId); + if (vaultId == null) throw new vaultsErrors.ErrorVaultsVaultUndefined(); + // Get the contents of the file + return await vaultManager.withVaults([vaultId], async (vault) => { + const content = await vaultOps.getSecret(vault, input.secretName); + return { secretContent: content.toString('binary') }; + }); }); }; } diff --git a/src/client/handlers/VaultsSecretsMkdir.ts b/src/client/handlers/VaultsSecretsMkdir.ts index 0249db33f..9284c74e0 100644 --- a/src/client/handlers/VaultsSecretsMkdir.ts +++ b/src/client/handlers/VaultsSecretsMkdir.ts @@ -48,9 +48,25 @@ class VaultsSecretsMkdir extends DuplexHandler< yield await vaultManager.withVaults( [vaultId], async (vault) => { - return await vaultOps.mkdir(vault, dirName, { - recursive: metadata?.options?.recursive, - }); + try { + await vaultOps.mkdir(vault, dirName, { + recursive: metadata?.options?.recursive, + }); + return { type: 'success', success: true }; + } catch (e) { + if ( + e instanceof vaultsErrors.ErrorVaultsRecursive || + e instanceof vaultsErrors.ErrorSecretsSecretDefined + ) { + return { + type: 'error', + code: e.cause.code, + reason: dirName, + }; + } else { + throw e; + } + } }, tran, ); diff --git a/src/client/handlers/VaultsSecretsRemove.ts b/src/client/handlers/VaultsSecretsRemove.ts index c06807639..2f8a5cbba 100644 --- a/src/client/handlers/VaultsSecretsRemove.ts +++ b/src/client/handlers/VaultsSecretsRemove.ts @@ -2,26 +2,25 @@ import type { DB } from '@matrixai/db'; import type { ClientRPCRequestParams, ClientRPCResponseResult, - SuccessMessage, SecretIdentifierMessage, + SuccessOrErrorMessage, } from '../types'; import type VaultManager from '../../vaults/VaultManager'; -import { ClientHandler } from '@matrixai/rpc'; +import { DuplexHandler } from '@matrixai/rpc'; import * as vaultsUtils from '../../vaults/utils'; import * as vaultsErrors from '../../vaults/errors'; -import * as vaultOps from '../../vaults/VaultOps'; -class VaultsSecretsRemove extends ClientHandler< +class VaultsSecretsRemove extends DuplexHandler< { db: DB; vaultManager: VaultManager; }, ClientRPCRequestParams, - ClientRPCResponseResult + ClientRPCResponseResult > { - public handle = async ( + public handle = async function* ( input: AsyncIterable>, - ): Promise> => { + ): AsyncGenerator> { const { db, vaultManager }: { db: DB; vaultManager: VaultManager } = this.container; // Create a record of secrets to be removed, grouped by vault names @@ -41,25 +40,64 @@ class VaultsSecretsRemove extends ClientHandler< } vaultGroups[vaultName].push(secretName); }); - - await db.withTransactionF(async (tran) => { - for (const [vaultName, secretNames] of Object.entries(vaultGroups)) { - const vaultIdFromName = await vaultManager.getVaultId(vaultName, tran); - const vaultId = vaultIdFromName ?? vaultsUtils.decodeVaultId(vaultName); - if (vaultId == null) throw new vaultsErrors.ErrorVaultsVaultUndefined(); - await vaultManager.withVaults( - [vaultId], - async (vault) => { - await vaultOps.deleteSecret(vault, secretNames, { - recursive: metadata?.options?.recursive, - }); - }, - tran, - ); - } - }); - - return { type: 'success', success: true }; + // Now, all the paths will be removed for a vault within a single commit + yield* db.withTransactionG( + async function* (tran): AsyncGenerator { + for (const [vaultName, secretNames] of Object.entries(vaultGroups)) { + const vaultIdFromName = await vaultManager.getVaultId( + vaultName, + tran, + ); + const vaultId = + vaultIdFromName ?? vaultsUtils.decodeVaultId(vaultName); + if (vaultId == null) { + throw new vaultsErrors.ErrorVaultsVaultUndefined(); + } + yield* vaultManager.withVaultsG( + [vaultId], + async function* (vault): AsyncGenerator { + yield* vault.writeG( + async function* (efs): AsyncGenerator { + for (const secretName of secretNames) { + try { + const stat = await efs.stat(secretName); + if (stat.isDirectory()) { + await efs.rmdir(secretName, { + recursive: metadata?.options?.recursive, + }); + } else { + await efs.unlink(secretName); + } + yield { + type: 'success', + success: true, + }; + } catch (e) { + if ( + e.code === 'ENOENT' || + e.code === 'ENOTEMPTY' || + e.code === 'EINVAL' + ) { + // INVAL can be triggered if removing the root of the + // vault is attempted. + yield { + type: 'error', + code: e.code, + reason: secretName, + }; + } else { + throw e; + } + } + } + }, + ); + }, + tran, + ); + } + }, + ); }; } diff --git a/src/client/handlers/index.ts b/src/client/handlers/index.ts index 76b1d4450..4d2d74727 100644 --- a/src/client/handlers/index.ts +++ b/src/client/handlers/index.ts @@ -80,6 +80,7 @@ import VaultsPermissionUnset from './VaultsPermissionUnset'; import VaultsPull from './VaultsPull'; import VaultsRename from './VaultsRename'; import VaultsScan from './VaultsScan'; +import VaultsSecretsCat from './VaultsSecretsCat'; import VaultsSecretsEnv from './VaultsSecretsEnv'; import VaultsSecretsGet from './VaultsSecretsGet'; import VaultsSecretsList from './VaultsSecretsList'; @@ -184,6 +185,7 @@ const serverManifest = (container: { vaultsPull: new VaultsPull(container), vaultsRename: new VaultsRename(container), vaultsScan: new VaultsScan(container), + vaultsSecretsCat: new VaultsSecretsCat(container), vaultsSecretsEnv: new VaultsSecretsEnv(container), vaultsSecretsGet: new VaultsSecretsGet(container), vaultsSecretsList: new VaultsSecretsList(container), @@ -266,6 +268,7 @@ export { VaultsPull, VaultsRename, VaultsScan, + VaultsSecretsCat, VaultsSecretsEnv, VaultsSecretsGet, VaultsSecretsList, diff --git a/src/client/types.ts b/src/client/types.ts index 32acc7046..ae415bb1e 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -207,7 +207,7 @@ type SuccessMessage = { type ErrorMessage = { type: 'error'; - code: string; + code?: string | number; reason?: string; data?: JSONObject; }; @@ -321,9 +321,9 @@ type ContentMessage = { secretContent: string; }; -type ContentWithErrorMessage = ContentMessage & { - error?: string; -}; +type ContentSuccessMessage = ContentMessage & SuccessMessage; + +type ContentOrErrorMessage = ContentSuccessMessage | ErrorMessage; type SecretContentMessage = SecretIdentifierMessage & ContentMessage; @@ -428,7 +428,8 @@ export type { SecretPathMessage, SecretIdentifierMessage, ContentMessage, - ContentWithErrorMessage, + ContentSuccessMessage, + ContentOrErrorMessage, SecretContentMessage, SecretDirMessage, SecretRenameMessage, diff --git a/src/errors.ts b/src/errors.ts index 843a93d4a..0b8f663ef 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -11,6 +11,11 @@ class ErrorPolykeyUnknown extends ErrorPolykey { exitCode = sysexits.PROTOCOL; } +class ErrorPolykeyUnexpected extends ErrorPolykey { + static description = 'An error originating outside Polykey was thrown'; + exitCode = sysexits.UNKNOWN; +} + class ErrorPolykeyAgentRunning extends ErrorPolykey { static description = 'PolykeyAgent is running'; exitCode = sysexits.USAGE; @@ -56,6 +61,7 @@ export { ErrorPolykey, ErrorPolykeyUnimplemented, ErrorPolykeyUnknown, + ErrorPolykeyUnexpected, ErrorPolykeyAgentRunning, ErrorPolykeyAgentNotRunning, ErrorPolykeyAgentDestroyed, diff --git a/src/network/utils.ts b/src/network/utils.ts index 2d3ed1439..6f03f6929 100644 --- a/src/network/utils.ts +++ b/src/network/utils.ts @@ -450,7 +450,14 @@ function fromError(error: any) { if (error instanceof Error) { const cause = fromError(error.cause); const timestamp: string = ((error as any).timestamp ?? new Date()).toJSON(); - if (error instanceof AbstractError) { + // ErrorParse is an exception to the Polykey error tree, where the error is + // not actually derived from ErrorPolykey, but rather an AbstractError. We + // cannot simply check for AbstractError because other libraries can also + // throw that, and those errors should be wrapped in ErrorPolykeyUnexpected. + if ( + error instanceof ErrorPolykey || + error instanceof validationErrors.ErrorParse + ) { return error.toJSON(); } else if (error instanceof AggregateError) { // AggregateError has an `errors` property @@ -466,18 +473,55 @@ function fromError(error: any) { }; } - // If it's some other type of error then only serialise the message and - // stack (and the type of the error) - return { - type: error.name, - message: error.message, - data: { - stack: error.stack, - timestamp, - cause, - }, - }; + // If it's some other type of error then wrap it in ErrorPolykeyUnexpected, + // serialising only the error type, message and its stack. + const wrappedError = new errors.ErrorPolykeyUnexpected( + `Unexpected error occurred: ${error.name}`, + { cause: error }, + ); + return wrappedError.toJSON(); } + + // Use the properties of the error to create an appropriate error message. + let message = ''; + switch (typeof error) { + case 'boolean': + case 'number': + case 'string': + case 'bigint': + case 'symbol': + message = `Non-error literal ${String(error)} was thrown`; + break; + case 'object': + // Let the fallback handler catch null values. + if (error == null) break; + // If we have an error message defined, then return that. + if ('message' in error && typeof error.message === 'string') { + message = error.message; + } + // If present, mention the constructor name in the message. + if (error.constructor?.name != null) { + message = `Non-error object ${error.constructor.name} was thrown`; + } + // Any other values should be handled by the fallback handler. + break; + } + + // Handle cases where the error is not serialisable, like objects created + // using Object.create(null). Trying to serialise this throws a TypeError. + try { + message = `Non-error value ${String(error)} was thrown`; + } catch (e) { + if (e instanceof TypeError) message = 'Non-error value was thrown'; + else throw e; + } + + // If the error was not an Error, then wrap it inside ErrorPolykeyUnexpected + // and return that. + const wrappedError = new errors.ErrorPolykeyUnexpected(message, { + cause: error, + }); + return wrappedError.toJSON(); } const standardErrors: { diff --git a/src/vaults/VaultOps.ts b/src/vaults/VaultOps.ts index ea559ab1d..6d2ea6079 100644 --- a/src/vaults/VaultOps.ts +++ b/src/vaults/VaultOps.ts @@ -4,7 +4,6 @@ import type Logger from '@matrixai/logger'; import type { Vault } from './Vault'; import type { Stat } from 'encryptedfs'; -import type { SuccessOrErrorMessage } from '../client/types'; import path from 'path'; import * as vaultsErrors from './errors'; import * as vaultsUtils from './utils'; @@ -130,37 +129,35 @@ async function statSecret(vault: Vault, secretName: string): Promise { */ async function deleteSecret( vault: Vault, - secretNames: Array, + secretName: string, fileOptions?: FileOptions, logger?: Logger, ): Promise { await vault.writeF(async (efs) => { - for (const secretName of secretNames) { - try { - const stat = await efs.stat(secretName); - if (stat.isDirectory()) { - await efs.rmdir(secretName, fileOptions); - logger?.info(`Deleted directory at '${secretName}'`); - } else { - // Remove the specified file - await efs.unlink(secretName); - logger?.info(`Deleted secret at '${secretName}'`); - } - } catch (e) { - if (e.code === 'ENOENT') { - throw new vaultsErrors.ErrorSecretsSecretUndefined( - `Secret with name: ${secretName} does not exist`, - { cause: e }, - ); - } - if (e.code === 'ENOTEMPTY') { - throw new vaultsErrors.ErrorVaultsRecursive( - `Could not delete directory '${secretName}' without recursive option`, - { cause: e }, - ); - } - throw e; + try { + const stat = await efs.stat(secretName); + if (stat.isDirectory()) { + await efs.rmdir(secretName, fileOptions); + logger?.info(`Deleted directory at '${secretName}'`); + } else { + // Remove the specified file + await efs.unlink(secretName); + logger?.info(`Deleted secret at '${secretName}'`); } + } catch (e) { + if (e.code === 'ENOENT') { + throw new vaultsErrors.ErrorSecretsSecretUndefined( + `Secret with name: ${secretName} does not exist`, + { cause: e }, + ); + } + if (e.code === 'ENOTEMPTY') { + throw new vaultsErrors.ErrorVaultsRecursive( + `Could not delete directory '${secretName}' without recursive option`, + { cause: e }, + ); + } + throw e; } }); } @@ -174,7 +171,7 @@ async function mkdir( dirPath: string, fileOptions?: FileOptions, logger?: Logger, -): Promise { +): Promise { const recursive = fileOptions?.recursive ?? false; // Technically, writing an empty directory won't make a commit, and doesn't // need a write resource as git doesn't track empty directories. It is @@ -184,22 +181,19 @@ async function mkdir( await efs.mkdir(dirPath, fileOptions); logger?.info(`Created secret directory at '${dirPath}'`); }); - return { type: 'success', success: true }; } catch (e) { logger?.error(`Failed to create directory '${dirPath}'. Reason: ${e.code}`); if (e.code === 'ENOENT' && !recursive) { - return { - type: 'error', - code: e.code, - reason: dirPath, - }; + throw new vaultsErrors.ErrorVaultsRecursive( + `Could not create direcotry '${dirPath}' without recursive option`, + { cause: e }, + ); } if (e.code === 'EEXIST') { - return { - type: 'error', - code: e.code, - reason: dirPath, - }; + throw new vaultsErrors.ErrorSecretsSecretDefined( + `${dirPath} already exists`, + { cause: e }, + ); } throw e; } @@ -280,9 +274,25 @@ async function writeSecret( logger?: Logger, ): Promise { await vault.writeF(async (efs) => { - await efs.writeFile(secretName, content); + try { + await efs.writeFile(secretName, content); + logger?.info(`Wrote secret ${secretName} in vault ${vault.vaultId}`); + } catch (e) { + if (e.code === 'ENOENT') { + throw new vaultsErrors.ErrorSecretsSecretUndefined( + `One or more parent directories for '${secretName}' do not exist`, + { cause: e }, + ); + } + if (e.code === 'EISDIR') { + throw new vaultsErrors.ErrorSecretsIsDirectory( + `Secret path '${secretName}' is a directory`, + { cause: e }, + ); + } + throw e; + } }); - logger?.info(`Wrote secret ${secretName} in vault ${vault.vaultId}`); } export { diff --git a/tests/client/handlers/vaults.test.ts b/tests/client/handlers/vaults.test.ts index 26f5efba8..1f1b85713 100644 --- a/tests/client/handlers/vaults.test.ts +++ b/tests/client/handlers/vaults.test.ts @@ -3,6 +3,7 @@ import type { FileSystem } from '@/types'; import type { VaultId } from '@/ids'; import type NodeManager from '@/nodes/NodeManager'; import type { + ContentSuccessMessage, ErrorMessage, LogEntryMessage, SecretContentMessage, @@ -36,6 +37,7 @@ import { VaultsSecretsWriteFile, VaultsSecretsEnv, VaultsSecretsGet, + VaultsSecretsCat, VaultsSecretsList, VaultsSecretsMkdir, VaultsSecretsNewDir, @@ -57,6 +59,7 @@ import { vaultsSecretsWriteFile, vaultsSecretsEnv, vaultsSecretsGet, + vaultsSecretsCat, vaultsSecretsList, vaultsSecretsMkdir, vaultsSecretsNew, @@ -944,10 +947,27 @@ describe('vaultsSecretsWriteFile', () => { recursive: true, }); }); + test('should fail with an invalid vault name', async () => { + const writeP = async () => { + try { + await rpcClient.methods.vaultsSecretsWriteFile({ + nameOrId: 'doesnt-exist', + secretName: 'doesnt-matter', + secretContent: 'doesnt-matter', + }); + } catch (e) { + throw e.cause; + } + }; + await expect(writeP).rejects.toThrow( + vaultsErrors.ErrorVaultsVaultUndefined, + ); + }); test('should edit a secret', async () => { const vaultName = 'test-vault'; - const secretName = 'test-secret'; const vaultId = await vaultManager.createVault(vaultName); + const secretName = 'test-secret'; + const newContent = 'content-changed'; await vaultManager.withVaults([vaultId], async (vault) => { await vault.writeF(async (efs) => { await efs.writeFile(secretName, secretName); @@ -956,32 +976,103 @@ describe('vaultsSecretsWriteFile', () => { const response = await rpcClient.methods.vaultsSecretsWriteFile({ nameOrId: vaultsUtils.encodeVaultId(vaultId), secretName: secretName, - secretContent: Buffer.from('content-change').toString('binary'), + secretContent: Buffer.from(newContent).toString('binary'), }); expect(response.success).toBeTruthy(); await vaultManager.withVaults([vaultId], async (vault) => { await vault.readF(async (efs) => { expect((await efs.readFile(secretName)).toString()).toStrictEqual( - 'content-change', + newContent, ); }); }); }); - test('should create target file with contents if it does not exist', async () => { + test('should create file if it does not exist', async () => { const vaultName = 'test-vault'; - const secretName = 'test-secret'; const vaultId = await vaultManager.createVault(vaultName); + const secretName = 'test-secret'; const response = await rpcClient.methods.vaultsSecretsWriteFile({ nameOrId: vaultsUtils.encodeVaultId(vaultId), secretName: secretName, - secretContent: Buffer.from('content-change').toString('binary'), + secretContent: Buffer.from(secretName).toString('binary'), }); expect(response.success).toBeTruthy(); await vaultManager.withVaults([vaultId], async (vault) => { await vault.readF(async (efs) => { - expect((await efs.readFile(secretName)).toString()).toStrictEqual( - 'content-change', - ); + const content = await efs.readFile(secretName); + expect(content.toString()).toStrictEqual(secretName); + }); + }); + }); + test('should fail when writing to a directory', async () => { + const vaultName = 'test-vault'; + const vaultId = await vaultManager.createVault(vaultName); + const dirName = 'test-secret'; + // Make the directory + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.mkdir(dirName); + }); + }); + // Try writing to directory + const writeP = async () => { + try { + await rpcClient.methods.vaultsSecretsWriteFile({ + nameOrId: vaultsUtils.encodeVaultId(vaultId), + secretName: dirName, + secretContent: Buffer.from(dirName).toString('binary'), + }); + } catch (e) { + throw e.cause; + } + }; + await expect(writeP).rejects.toThrow(vaultsErrors.ErrorSecretsIsDirectory); + }); + test('should fail when parent does not exist', async () => { + const vaultName = 'test-vault'; + const vaultId = await vaultManager.createVault(vaultName); + const dirName = 'dir'; + const secretName = 'secret-name'; + const secretPath = path.join(dirName, secretName); + // Try writing + const writeP = async () => { + try { + await rpcClient.methods.vaultsSecretsWriteFile({ + nameOrId: vaultsUtils.encodeVaultId(vaultId), + secretName: secretPath, + secretContent: Buffer.from(secretName).toString('binary'), + }); + } catch (e) { + throw e.cause; + } + }; + await expect(writeP).rejects.toThrow( + vaultsErrors.ErrorSecretsSecretUndefined, + ); + }); + test('should write to nested path', async () => { + const vaultName = 'test-vault'; + const vaultId = await vaultManager.createVault(vaultName); + const dirName = 'dir'; + const secretName = 'secret-name'; + const secretPath = path.join(dirName, secretName); + // Make the directory + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.mkdir(dirName); + }); + }); + // Try writing + const response = await rpcClient.methods.vaultsSecretsWriteFile({ + nameOrId: vaultsUtils.encodeVaultId(vaultId), + secretName: secretPath, + secretContent: Buffer.from(secretName).toString('binary'), + }); + expect(response.success).toBeTruthy(); + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.readF(async (efs) => { + const content = await efs.readFile(secretPath); + expect(content.toString()).toStrictEqual(secretName); }); }); }); @@ -1338,6 +1429,27 @@ describe('vaultsSecretsMkdir', () => { recursive: true, }); }); + test('fails with invalid vault name', async () => { + const vaultName = 'test-vault'; + const dirName = 'dir'; + const response = await rpcClient.methods.vaultsSecretsMkdir(); + const writer = response.writable.getWriter(); + await writer.write({ + nameOrId: vaultName, + dirName: dirName, + }); + await writer.close(); + const consumeP = async () => { + try { + for await (const _ of response.readable); + } catch (e) { + throw e.cause; + } + }; + await expect(consumeP()).rejects.toThrow( + vaultsErrors.ErrorVaultsVaultUndefined, + ); + }); test('makes a directory', async () => { const vaultName = 'test-vault'; const vaultId = await vaultManager.createVault(vaultName); @@ -1493,8 +1605,8 @@ describe('vaultsSecretsMkdir', () => { }); }); }); -describe('vaultsSecretsNew and vaultsSecretsDelete, vaultsSecretsGet', () => { - const logger = new Logger('vaultsSecretsNewDeleteGet test', LogLevel.WARN, [ +describe('vaultsSecretsCat', () => { + const logger = new Logger('vaultsSecretsCat test', LogLevel.WARN, [ new StreamHandler( formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`, ), @@ -1508,54 +1620,9 @@ describe('vaultsSecretsNew and vaultsSecretsDelete, vaultsSecretsGet', () => { let clientService: ClientService; let webSocketClient: WebSocketClient; let rpcClient: RPCClient<{ - vaultsSecretsNew: typeof vaultsSecretsNew; - vaultsSecretsRemove: typeof vaultsSecretsRemove; - vaultsSecretsGet: typeof vaultsSecretsGet; - vaultsSecretsMkdir: typeof vaultsSecretsMkdir; - vaultsSecretsStat: typeof vaultsSecretsStat; + vaultsSecretsCat: typeof vaultsSecretsCat; }>; let vaultManager: VaultManager; - // Helper function to create secrets in a vault - const createVaultSecret = async ( - vaultId: VaultId, - secretName: string, - content: string, - ) => { - await vaultManager.withVaults([vaultId], async (vault) => { - await vault.writeF(async (efs) => { - await efs.writeFile(secretName, content); - expect(await efs.exists(secretName)).toBeTruthy(); - }); - }); - }; - // Helper function to ensure each file path was deleted - const checkSecretIsDeleted = async (vaultId: VaultId, secretName: string) => { - await vaultManager.withVaults([vaultId], async (vault) => { - await vault.readF(async (efs) => { - expect(await efs.exists(secretName)).toBeFalsy(); - }); - }); - }; - // Helper function to ensure each file path exists in the vault - const checkSecretExists = async (vaultId: VaultId, secretName: string) => { - await vaultManager.withVaults([vaultId], async (vault) => { - await vault.readF(async (efs) => { - expect(await efs.exists(secretName)).toBeTruthy(); - }); - }); - }; - // Helper function to create a directory - const createVaultDir = async ( - vaultId: VaultId, - dirName: string, - recursive: boolean = false, - ) => { - await vaultManager.withVaults([vaultId], async (vault) => { - await vault.writeF(async (efs) => { - await efs.mkdir(dirName, { recursive: recursive }); - }); - }); - }; beforeEach(async () => { dataDir = await fs.promises.mkdtemp( path.join(os.tmpdir(), 'polykey-test-'), @@ -1592,23 +1659,7 @@ describe('vaultsSecretsNew and vaultsSecretsDelete, vaultsSecretsGet', () => { }); await clientService.start({ manifest: { - vaultsSecretsNew: new VaultsSecretsNew({ - db, - vaultManager, - }), - vaultsSecretsRemove: new VaultsSecretsRemove({ - db, - vaultManager, - }), - vaultsSecretsGet: new VaultsSecretsGet({ - db, - vaultManager, - }), - vaultsSecretsMkdir: new VaultsSecretsMkdir({ - db, - vaultManager, - }), - vaultsSecretsStat: new VaultsSecretsStat({ + vaultsSecretsCat: new VaultsSecretsCat({ db, vaultManager, }), @@ -1625,11 +1676,7 @@ describe('vaultsSecretsNew and vaultsSecretsDelete, vaultsSecretsGet', () => { }); rpcClient = new RPCClient({ manifest: { - vaultsSecretsNew, - vaultsSecretsRemove, - vaultsSecretsGet, - vaultsSecretsMkdir, - vaultsSecretsStat, + vaultsSecretsCat, }, streamFactory: () => webSocketClient.connection.newStream(), toError: networkUtils.toError, @@ -1647,297 +1694,945 @@ describe('vaultsSecretsNew and vaultsSecretsDelete, vaultsSecretsGet', () => { recursive: true, }); }); - test('creates, gets, and deletes secrets', async () => { - // Create secret - const secretName = 'test-secret'; - const vaultId = await vaultManager.createVault('test-vault'); - const vaultIdEncoded = vaultsUtils.encodeVaultId(vaultId); - const createResponse = await rpcClient.methods.vaultsSecretsNew({ - nameOrId: vaultIdEncoded, - secretName: secretName, - secretContent: Buffer.from(secretName).toString('binary'), - }); - expect(createResponse.success).toBeTruthy(); - // Get secret - const getStream = await rpcClient.methods.vaultsSecretsGet(); - const getWriter = getStream.writable.getWriter(); - await getWriter.write({ - nameOrId: vaultIdEncoded, - secretName: secretName, - }); - await getWriter.close(); - const secretContent: Array = []; - for await (const data of getStream.readable) { - secretContent.push(data.secretContent); - } - const concatenatedContent = secretContent.join(''); - expect(concatenatedContent).toStrictEqual(secretName); - // Delete secret - const deleteStream = await rpcClient.methods.vaultsSecretsRemove(); - const deleteWriter = deleteStream.writable.getWriter(); - await deleteWriter.write({ - nameOrId: vaultIdEncoded, - secretName: secretName, - }); - await deleteWriter.close(); - expect((await deleteStream.output).success).toBeTruthy(); - // Check secret was deleted - const deleteGetStream = await rpcClient.methods.vaultsSecretsGet(); - const deleteGetWriter = deleteGetStream.writable.getWriter(); - await deleteGetWriter.write({ - nameOrId: vaultIdEncoded, + test('fails with invalid vault name', async () => { + const vaultName = 'test-vault'; + const secretName = 'secret'; + // Cat file + const response = await rpcClient.methods.vaultsSecretsCat(); + const writer = response.writable.getWriter(); + await writer.write({ + nameOrId: vaultName, secretName: secretName, }); - await deleteGetWriter.close(); + await writer.close(); + // Read response + const consumeP = async () => { + for await (const _ of response.readable); + }; await testsUtils.expectRemoteError( - (async () => { - for await (const _ of deleteGetStream.readable); - })(), - vaultsErrors.ErrorSecretsSecretUndefined, + consumeP(), + vaultsErrors.ErrorVaultsVaultUndefined, ); }); - test('gets multiple secrets in order', async () => { - // Create secrets - const secretName1 = 'test-secret1'; - const secretName2 = 'test-secret2'; - const secretName3 = 'test-secret3'; - const vaultId = await vaultManager.createVault('test-vault'); - const vaultIdEncoded = vaultsUtils.encodeVaultId(vaultId); - await createVaultSecret(vaultId, secretName1, secretName1); - await createVaultSecret(vaultId, secretName2, secretName2); - await createVaultSecret(vaultId, secretName3, secretName3); - // Get secrets - const getStream = await rpcClient.methods.vaultsSecretsGet(); - const getWriter = getStream.writable.getWriter(); - await getWriter.write({ - nameOrId: vaultIdEncoded, - secretName: secretName1, + test('reads a secret', async () => { + const vaultName = 'test-vault'; + const vaultId = await vaultManager.createVault(vaultName); + const secretName = 'secret'; + const secretContent = 'secret-content'; + // Write file + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.writeFile(secretName, secretContent); + }); }); - await getWriter.write({ - nameOrId: vaultIdEncoded, - secretName: secretName2, + // Cat file + const response = await rpcClient.methods.vaultsSecretsCat(); + const writer = response.writable.getWriter(); + await writer.write({ + nameOrId: vaultsUtils.encodeVaultId(vaultId), + secretName: secretName, }); - await getWriter.write({ - nameOrId: vaultIdEncoded, - secretName: secretName3, + await writer.close(); + // Read response + for await (const data of response.readable) { + expect(data.type).toEqual('success'); + // TS cannot properly evaluate a type as nested as this, so we use the + // as keyword to help it. Inside this block, the type of data is 'success'. + const message = data as ContentSuccessMessage; + expect(message.secretContent).toEqual(secretContent); + } + }); + test('fails to read invalid secret', async () => { + const vaultName = 'test-vault'; + const vaultId = await vaultManager.createVault(vaultName); + const secretName = 'secret'; + // Cat file + const response = await rpcClient.methods.vaultsSecretsCat(); + const writer = response.writable.getWriter(); + await writer.write({ + nameOrId: vaultsUtils.encodeVaultId(vaultId), + secretName: secretName, }); - await getWriter.close(); - let secretContent: string = ''; - for await (const data of getStream.readable) { - secretContent += data.secretContent; + await writer.close(); + // Read response + for await (const data of response.readable) { + expect(data.type).toEqual('error'); + // TS cannot properly evaluate a type as nested as this, so we use the + // as keyword to help it. Inside this block, the type of data is 'success'. + const error = data as ErrorMessage; + expect(error.code).toEqual('ENOENT'); + expect(error.reason).toEqual(secretName); } - expect(secretContent).toStrictEqual( - `${secretName1}${secretName2}${secretName3}`, - ); }); - test('should not fail to get secrets on error when continueOnError is set', async () => { - // Create secrets - const secretName1 = 'test-secret1'; - const secretName2 = 'test-secret2'; - const vaultId = await vaultManager.createVault('test-vault'); - const vaultIdEncoded = vaultsUtils.encodeVaultId(vaultId); - await createVaultSecret(vaultId, secretName1, secretName1); - await createVaultSecret(vaultId, secretName2, secretName2); - // Get secrets - const getStream = await rpcClient.methods.vaultsSecretsGet(); - const getWriter = getStream.writable.getWriter(); - await getWriter.write({ - nameOrId: vaultIdEncoded, - secretName: secretName1, - metadata: { options: { continueOnError: true } }, + test('fails to read a directory', async () => { + const vaultName = 'test-vault'; + const vaultId = await vaultManager.createVault(vaultName); + const secretName = 'secret'; + // Write files + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.mkdir(secretName); + }); }); - await getWriter.write({ nameOrId: vaultIdEncoded, secretName: 'invalid' }); - await getWriter.write({ - nameOrId: vaultIdEncoded, - secretName: secretName2, + // Cat file + const response = await rpcClient.methods.vaultsSecretsCat(); + const writer = response.writable.getWriter(); + await writer.write({ + nameOrId: vaultsUtils.encodeVaultId(vaultId), + secretName: secretName, }); - await getWriter.close(); - let secretContent: string = ''; - let errorContent: string = ''; - await expect( - (async () => { - for await (const data of getStream.readable) { - if (data.error) errorContent += data.error; - else secretContent += data.secretContent; - } - })(), - ).toResolve(); - expect(secretContent).toStrictEqual(`${secretName1}${secretName2}`); - expect(errorContent.length).not.toBe(0); + await writer.close(); + // Read response + for await (const data of response.readable) { + expect(data.type).toEqual('error'); + // TS cannot properly evaluate a type as nested as this, so we use the + // as keyword to help it. Inside this block, the type of data is 'success'. + const error = data as ErrorMessage; + expect(error.code).toEqual('EISDIR'); + expect(error.reason).toEqual(secretName); + } }); - test('deletes multiple secrets from the same vault', async () => { - // Create secrets - const secretName1 = 'test-secret1'; - const secretName2 = 'test-secret2'; - const vaultId = await vaultManager.createVault('test-vault'); + test('reads multiple secrets in order', async () => { + const vaultName = 'test-vault'; + const vaultId = await vaultManager.createVault(vaultName); const vaultIdEncoded = vaultsUtils.encodeVaultId(vaultId); - await createVaultSecret(vaultId, secretName1, secretName1); - await createVaultSecret(vaultId, secretName2, secretName2); - // Delete secrets - const deleteStream = await rpcClient.methods.vaultsSecretsRemove(); - const deleteWriter = deleteStream.writable.getWriter(); - await deleteWriter.write({ - nameOrId: vaultIdEncoded, - secretName: secretName1, - }); - await deleteWriter.write({ - nameOrId: vaultIdEncoded, - secretName: secretName2, + const secretName1 = 'secret1'; + const secretName2 = 'secret2'; + const secretContent1 = 'contents-of-secret1'; + const secretContent2 = 'contents-of-secret2'; + // Write secrets + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.writeFile(secretName1, secretContent1); + await efs.writeFile(secretName2, secretContent2); + }); }); - await deleteWriter.close(); - expect((await deleteStream.output).success).toBeTruthy(); - // Check each secret was deleted - await checkSecretIsDeleted(vaultId, secretName1); - await checkSecretIsDeleted(vaultId, secretName2); + // Cat files + const response = await rpcClient.methods.vaultsSecretsCat(); + const writer = response.writable.getWriter(); + await writer.write({ nameOrId: vaultIdEncoded, secretName: secretName1 }); + await writer.write({ nameOrId: vaultIdEncoded, secretName: secretName2 }); + await writer.close(); + // Read response + let totalContent = ''; + for await (const data of response.readable) { + expect(data.type).toEqual('success'); + // TS cannot properly evaluate a type as nested as this, so we use the + // as keyword to help it. Inside this block, the type of data is 'success'. + const message = data as ContentSuccessMessage; + totalContent += message.secretContent; + } + expect(totalContent).toEqual(`${secretContent1}${secretContent2}`); }); - test('gets secrets from multiple vaults', async () => { - // Create secret - const secretName1 = 'test-secret1'; - const secretName2 = 'test-secret2'; - const secretName3 = 'test-secret3'; - const vaultId1 = await vaultManager.createVault('test-vault1'); - const vaultId2 = await vaultManager.createVault('test-vault2'); + test('reads secrets across multiple vaults', async () => { + const vaultName1 = 'test-vault1'; + const vaultName2 = 'test-vault2'; + const vaultId1 = await vaultManager.createVault(vaultName1); + const vaultId2 = await vaultManager.createVault(vaultName2); const vaultIdEncoded1 = vaultsUtils.encodeVaultId(vaultId1); const vaultIdEncoded2 = vaultsUtils.encodeVaultId(vaultId2); - await createVaultSecret(vaultId1, secretName1, secretName1); - await createVaultSecret(vaultId1, secretName2, secretName2); - await createVaultSecret(vaultId2, secretName3, secretName3); - // Get secret - const getStream = await rpcClient.methods.vaultsSecretsGet(); - const getWriter = getStream.writable.getWriter(); - await getWriter.write({ - nameOrId: vaultIdEncoded1, - secretName: secretName1, - }); - await getWriter.write({ - nameOrId: vaultIdEncoded1, - secretName: secretName2, - }); - await getWriter.write({ - nameOrId: vaultIdEncoded2, - secretName: secretName3, - }); - await getWriter.close(); - let secretContent: string = ''; - for await (const data of getStream.readable) { - secretContent += data.secretContent; - } - expect(secretContent).toStrictEqual( - `${secretName1}${secretName2}${secretName3}`, - ); - }); - test('deletes secrets from multiple vaults in one log', async () => { - // Create secret - const secretName1 = 'test-secret1'; - const secretName2 = 'test-secret2'; - const secretName3 = 'test-secret3'; - const vaultId1 = await vaultManager.createVault('test-vault1'); + const secretName1 = 'secret1'; + const secretName2 = 'secret2'; + const secretName3 = 'secret3'; + const secretContent1 = 'content1'; + const secretContent2 = 'content2'; + const secretContent3 = 'content3'; + // Write secrets + await vaultManager.withVaults( + [vaultId1, vaultId2], + async (vault1, vault2) => { + await vault1.writeF(async (efs) => { + await efs.writeFile(secretName1, secretContent1); + await efs.writeFile(secretName3, secretContent3); + }); + await vault2.writeF(async (efs) => { + await efs.writeFile(secretName2, secretContent2); + }); + }, + ); + // Cat files + const response = await rpcClient.methods.vaultsSecretsCat(); + const writer = response.writable.getWriter(); + await writer.write({ nameOrId: vaultIdEncoded1, secretName: secretName1 }); + await writer.write({ nameOrId: vaultIdEncoded2, secretName: secretName2 }); + await writer.write({ nameOrId: vaultIdEncoded1, secretName: secretName3 }); + await writer.close(); + // Read response + let totalContent = ''; + for await (const data of response.readable) { + expect(data.type).toEqual('success'); + // TS cannot properly evaluate a type as nested as this, so we use the + // as keyword to help it. Inside this block, the type of data is 'success'. + const message = data as ContentSuccessMessage; + totalContent += message.secretContent; + } + expect(totalContent).toEqual( + `${secretContent1}${secretContent2}${secretContent3}`, + ); + }); + test('continues on error across multiple vaults', async () => { + const vaultName1 = 'test-vault1'; + const vaultName2 = 'test-vault2'; + const vaultId1 = await vaultManager.createVault(vaultName1); + const vaultId2 = await vaultManager.createVault(vaultName2); + const vaultIdEncoded1 = vaultsUtils.encodeVaultId(vaultId1); + const vaultIdEncoded2 = vaultsUtils.encodeVaultId(vaultId2); + const secretName1 = 'secret1'; + const secretName2 = 'secret2'; + const secretName3 = 'secret3'; + const invalidName = 'nosecret'; + const secretContent1 = 'content1'; + const secretContent2 = 'content2'; + const secretContent3 = 'content3'; + // Write secrets + await vaultManager.withVaults( + [vaultId1, vaultId2], + async (vault1, vault2) => { + await vault1.writeF(async (efs) => { + await efs.writeFile(secretName1, secretContent1); + await efs.writeFile(secretName3, secretContent3); + }); + await vault2.writeF(async (efs) => { + await efs.writeFile(secretName2, secretContent2); + }); + }, + ); + // Cat files + const response = await rpcClient.methods.vaultsSecretsCat(); + const writer = response.writable.getWriter(); + await writer.write({ nameOrId: vaultIdEncoded1, secretName: secretName1 }); + await writer.write({ nameOrId: vaultIdEncoded2, secretName: secretName2 }); + await writer.write({ nameOrId: vaultIdEncoded1, secretName: invalidName }); + await writer.write({ nameOrId: vaultIdEncoded2, secretName: invalidName }); + await writer.write({ nameOrId: vaultIdEncoded1, secretName: secretName3 }); + await writer.close(); + // Read response + let totalContent = ''; + for await (const data of response.readable) { + if (data.type === 'success') { + // TS cannot properly evaluate a type as nested as this, so we use the + // as keyword to help it. Inside this block, the type of data is 'success'. + const message = data as ContentSuccessMessage; + totalContent += message.secretContent; + } else { + // TS cannot properly evaluate a type as nested as this, so we use the + // as keyword to help it. Inside this block, the type of data is 'success'. + const error = data as ErrorMessage; + expect(error.code).toEqual('ENOENT'); + expect(error.reason).toEqual(invalidName); + } + } + expect(totalContent).toEqual( + `${secretContent1}${secretContent2}${secretContent3}`, + ); + }); +}); +describe('vaultsSecretsGet', () => { + const logger = new Logger('vaultsSecretsGet test', LogLevel.WARN, [ + new StreamHandler( + formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`, + ), + ]); + const password = 'helloWorld'; + const localhost = '127.0.0.1'; + let dataDir: string; + let db: DB; + let keyRing: KeyRing; + let tlsConfig: TLSConfig; + let clientService: ClientService; + let webSocketClient: WebSocketClient; + let rpcClient: RPCClient<{ + vaultsSecretsGet: typeof vaultsSecretsGet; + }>; + let vaultManager: VaultManager; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + const keysPath = path.join(dataDir, 'keys'); + keyRing = await KeyRing.createKeyRing({ + password, + keysPath, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + logger, + }); + tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); + const dbPath = path.join(dataDir, 'db'); + db = await DB.createDB({ + dbPath, + logger, + }); + const vaultsPath = path.join(dataDir, 'vaults'); + vaultManager = await VaultManager.createVaultManager({ + vaultsPath, + db, + acl: {} as ACL, + keyRing, + nodeManager: {} as NodeManager, + gestaltGraph: {} as GestaltGraph, + notificationsManager: {} as NotificationsManager, + logger, + }); + clientService = new ClientService({ + tlsConfig, + logger: logger.getChild(ClientService.name), + }); + await clientService.start({ + manifest: { + vaultsSecretsGet: new VaultsSecretsGet({ + db, + vaultManager, + }), + }, + host: localhost, + }); + webSocketClient = await WebSocketClient.createWebSocketClient({ + config: { + verifyPeer: false, + }, + host: localhost, + logger: logger.getChild(WebSocketClient.name), + port: clientService.port, + }); + rpcClient = new RPCClient({ + manifest: { + vaultsSecretsGet, + }, + streamFactory: () => webSocketClient.connection.newStream(), + toError: networkUtils.toError, + logger: logger.getChild(RPCClient.name), + }); + }); + afterEach(async () => { + await clientService?.stop({ force: true }); + await webSocketClient.destroy({ force: true }); + await vaultManager.stop(); + await db.stop(); + await keyRing.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + test('fails with invalid vault name', async () => { + const vaultName = 'test-vault'; + const secretName = 'secret'; + // Get file + const response = await rpcClient.methods.vaultsSecretsGet({ + nameOrId: vaultName, + secretName: secretName, + }); + // Read response + const consumeP = async () => { + for await (const _ of response); + }; + await testsUtils.expectRemoteError( + consumeP(), + vaultsErrors.ErrorVaultsVaultUndefined, + ); + }); + test('gets a secret', async () => { + const vaultName = 'test-vault'; + const vaultId = await vaultManager.createVault(vaultName); + const secretName = 'secret'; + const secretContent = 'secret-content'; + // Write file + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.writeFile(secretName, secretContent); + }); + }); + // Cat file + const response = await rpcClient.methods.vaultsSecretsGet({ + nameOrId: vaultsUtils.encodeVaultId(vaultId), + secretName: secretName, + }); + // Read response + let totalContent = ''; + for await (const data of response) { + totalContent += data.secretContent; + } + expect(totalContent).toEqual(secretContent); + }); + test('fails to read invalid secret', async () => { + const vaultName = 'test-vault'; + const vaultId = await vaultManager.createVault(vaultName); + const secretName = 'secret'; + // Cat file + const response = await rpcClient.methods.vaultsSecretsGet({ + nameOrId: vaultsUtils.encodeVaultId(vaultId), + secretName: secretName, + }); + // Read response + const consumeP = async () => { + for await (const _ of response); + }; + await testsUtils.expectRemoteError( + consumeP(), + vaultsErrors.ErrorSecretsSecretUndefined, + ); + }); + test('fails to read a directory', async () => { + const vaultName = 'test-vault'; + const vaultId = await vaultManager.createVault(vaultName); + const secretName = 'secret'; + // Create a directory + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.mkdir(secretName); + }); + }); + // Cat file + const response = await rpcClient.methods.vaultsSecretsGet({ + nameOrId: vaultsUtils.encodeVaultId(vaultId), + secretName: secretName, + }); + // Read response + const consumeP = async () => { + for await (const _ of response); + }; + await testsUtils.expectRemoteError( + consumeP(), + vaultsErrors.ErrorSecretsIsDirectory, + ); + }); +}); +describe('vaultsSecretsNew', () => { + const logger = new Logger('vaultsSecretsNew test', LogLevel.WARN, [ + new StreamHandler( + formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`, + ), + ]); + const password = 'helloWorld'; + const localhost = '127.0.0.1'; + let dataDir: string; + let db: DB; + let keyRing: KeyRing; + let tlsConfig: TLSConfig; + let clientService: ClientService; + let webSocketClient: WebSocketClient; + let rpcClient: RPCClient<{ + vaultsSecretsNew: typeof vaultsSecretsNew; + }>; + let vaultManager: VaultManager; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + const keysPath = path.join(dataDir, 'keys'); + keyRing = await KeyRing.createKeyRing({ + password, + keysPath, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + logger, + }); + tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); + const dbPath = path.join(dataDir, 'db'); + db = await DB.createDB({ + dbPath, + logger, + }); + const vaultsPath = path.join(dataDir, 'vaults'); + vaultManager = await VaultManager.createVaultManager({ + vaultsPath, + db, + acl: {} as ACL, + keyRing, + nodeManager: {} as NodeManager, + gestaltGraph: {} as GestaltGraph, + notificationsManager: {} as NotificationsManager, + logger, + }); + clientService = new ClientService({ + tlsConfig, + logger: logger.getChild(ClientService.name), + }); + await clientService.start({ + manifest: { + vaultsSecretsNew: new VaultsSecretsNew({ + db, + vaultManager, + }), + }, + host: localhost, + }); + webSocketClient = await WebSocketClient.createWebSocketClient({ + config: { + verifyPeer: false, + }, + host: localhost, + logger: logger.getChild(WebSocketClient.name), + port: clientService.port, + }); + rpcClient = new RPCClient({ + manifest: { + vaultsSecretsNew, + }, + streamFactory: () => webSocketClient.connection.newStream(), + toError: networkUtils.toError, + logger: logger.getChild(RPCClient.name), + }); + }); + afterEach(async () => { + await clientService?.stop({ force: true }); + await webSocketClient.destroy({ force: true }); + await vaultManager.stop(); + await db.stop(); + await keyRing.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + test('fails with invalid vault name', async () => { + const vaultName = 'test-vault'; + const secretName = 'secret'; + // New file + const responseP = rpcClient.methods.vaultsSecretsNew({ + nameOrId: vaultName, + secretName: secretName, + secretContent: secretName, + }); + await testsUtils.expectRemoteError( + responseP, + vaultsErrors.ErrorVaultsVaultUndefined, + ); + }); + test('creates a secret', async () => { + const vaultName = 'test-vault'; + const vaultId = await vaultManager.createVault(vaultName); + const secretName = 'secret'; + const secretContent = 'secret-content'; + // Create file + await rpcClient.methods.vaultsSecretsNew({ + nameOrId: vaultsUtils.encodeVaultId(vaultId), + secretName: secretName, + secretContent: secretContent, + }); + // Check for file + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.readF(async (efs) => { + const fileContent = await efs.readFile(secretName); + expect(fileContent.toString()).toEqual(secretContent); + }); + }); + }); + test('creates nested secret', async () => { + const vaultName = 'test-vault'; + const vaultId = await vaultManager.createVault(vaultName); + const dirName = 'dir'; + const secretName = 'secret'; + const secretPath = path.join(dirName, secretName); + const secretContent = 'secret-content'; + // Create file + await rpcClient.methods.vaultsSecretsNew({ + nameOrId: vaultsUtils.encodeVaultId(vaultId), + secretName: secretPath, + secretContent: secretContent, + }); + // Check for file + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.readF(async (efs) => { + const dirStat = await efs.stat(dirName); + expect(dirStat.isDirectory()).toBeTruthy(); + const fileStat = await efs.stat(secretPath); + expect(fileStat.isFile()).toBeTruthy(); + const fileContent = await efs.readFile(secretPath); + expect(fileContent.toString()).toEqual(secretContent); + }); + }); + }); + test('fails to create an existing secret', async () => { + const vaultName = 'test-vault'; + const vaultId = await vaultManager.createVault(vaultName); + const secretName = 'secret'; + const oldSecretContent = 'secret-content'; + const newSecretContent = 'new-secret-content'; + // Write file + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.writeFile(secretName, oldSecretContent); + }); + }); + // Cat file + const responseP = rpcClient.methods.vaultsSecretsNew({ + nameOrId: vaultsUtils.encodeVaultId(vaultId), + secretName: secretName, + secretContent: newSecretContent, + }); + // Read response + await testsUtils.expectRemoteError( + responseP, + vaultsErrors.ErrorSecretsSecretDefined, + ); + // Confirm the file is unchanged + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.readF(async (efs) => { + const content = await efs.readFile(secretName); + expect(content.toString()).toEqual(oldSecretContent); + }); + }); + }); +}); +describe('vaultsSecretsRemove', () => { + const logger = new Logger('vaultsSecretsRemove test', LogLevel.WARN, [ + new StreamHandler( + formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`, + ), + ]); + const password = 'helloWorld'; + const localhost = '127.0.0.1'; + let dataDir: string; + let db: DB; + let keyRing: KeyRing; + let tlsConfig: TLSConfig; + let clientService: ClientService; + let webSocketClient: WebSocketClient; + let rpcClient: RPCClient<{ + vaultsSecretsRemove: typeof vaultsSecretsRemove; + }>; + let vaultManager: VaultManager; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + const keysPath = path.join(dataDir, 'keys'); + keyRing = await KeyRing.createKeyRing({ + password, + keysPath, + passwordOpsLimit: keysUtils.passwordOpsLimits.min, + passwordMemLimit: keysUtils.passwordMemLimits.min, + strictMemoryLock: false, + logger, + }); + tlsConfig = await testsUtils.createTLSConfig(keyRing.keyPair); + const dbPath = path.join(dataDir, 'db'); + db = await DB.createDB({ + dbPath, + logger, + }); + const vaultsPath = path.join(dataDir, 'vaults'); + vaultManager = await VaultManager.createVaultManager({ + vaultsPath, + db, + acl: {} as ACL, + keyRing, + nodeManager: {} as NodeManager, + gestaltGraph: {} as GestaltGraph, + notificationsManager: {} as NotificationsManager, + logger, + }); + clientService = new ClientService({ + tlsConfig, + logger: logger.getChild(ClientService.name), + }); + await clientService.start({ + manifest: { + vaultsSecretsRemove: new VaultsSecretsRemove({ + db, + vaultManager, + }), + }, + host: localhost, + }); + webSocketClient = await WebSocketClient.createWebSocketClient({ + config: { + verifyPeer: false, + }, + host: localhost, + logger: logger.getChild(WebSocketClient.name), + port: clientService.port, + }); + rpcClient = new RPCClient({ + manifest: { + vaultsSecretsRemove, + }, + streamFactory: () => webSocketClient.connection.newStream(), + toError: networkUtils.toError, + logger: logger.getChild(RPCClient.name), + }); + }); + afterEach(async () => { + await clientService?.stop({ force: true }); + await webSocketClient.destroy({ force: true }); + await vaultManager.stop(); + await db.stop(); + await keyRing.stop(); + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + test('fails with invalid vault name', async () => { + // Write paths + const response = await rpcClient.methods.vaultsSecretsRemove(); + const writer = response.writable.getWriter(); + await writer.write({ nameOrId: 'invalid', secretName: 'invalid' }); + await writer.close(); + // Read response + const consumeP = async () => { + for await (const _ of response.readable); + }; + await testsUtils.expectRemoteError( + consumeP(), + vaultsErrors.ErrorVaultsVaultUndefined, + ); + }); + test('fails deleting vault root', async () => { + // Create secrets + const secretName = 'test-secret1'; + const vaultId = await vaultManager.createVault('test-vault'); + const vaultIdEncoded = vaultsUtils.encodeVaultId(vaultId); + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.writeFile(secretName, secretName); + }); + }); + // Delete secrets + const response = await rpcClient.methods.vaultsSecretsRemove(); + const writer = response.writable.getWriter(); + await writer.write({ nameOrId: vaultIdEncoded, secretName: '/' }); + await writer.close(); + for await (const data of response.readable) { + expect(data.type).toStrictEqual('error'); + // TS cannot properly evaluate a type as nested as this, so we use the + // as keyword to help it. Inside this block, the type of data is 'error'. + const error = data as ErrorMessage; + // The error code should be an invalid operation + expect(error.code).toStrictEqual('EINVAL'); + } + // Check + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.readF(async (efs) => { + expect(await efs.exists(secretName)).toBeTruthy(); + }); + }); + }); + test('deletes multiple secrets', async () => { + // Create secrets + const secretName1 = 'test-secret1'; + const secretName2 = 'test-secret2'; + const vaultId = await vaultManager.createVault('test-vault'); + const vaultIdEncoded = vaultsUtils.encodeVaultId(vaultId); + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.writeFile(secretName1, secretName1); + await efs.writeFile(secretName2, secretName2); + }); + }); + // Delete secrets + const response = await rpcClient.methods.vaultsSecretsRemove(); + const writer = response.writable.getWriter(); + await writer.write({ nameOrId: vaultIdEncoded, secretName: secretName1 }); + await writer.write({ nameOrId: vaultIdEncoded, secretName: secretName2 }); + await writer.close(); + for await (const data of response.readable) { + expect(data.type).toStrictEqual('success'); + } + // Check each secret was deleted + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.readF(async (efs) => { + expect(await efs.exists(secretName1)).toBeFalsy(); + expect(await efs.exists(secretName2)).toBeFalsy(); + }); + }); + }); + test('continues on error', async () => { + // Create secrets + const secretName1 = 'test-secret1'; + const secretName2 = 'test-secret2'; + const invalidName = 'invalid'; + const vaultId = await vaultManager.createVault('test-vault'); + const vaultIdEncoded = vaultsUtils.encodeVaultId(vaultId); + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.writeFile(secretName1, secretName1); + await efs.writeFile(secretName2, secretName2); + }); + }); + // Delete secrets + const response = await rpcClient.methods.vaultsSecretsRemove(); + const writer = response.writable.getWriter(); + await writer.write({ nameOrId: vaultIdEncoded, secretName: secretName1 }); + await writer.write({ nameOrId: vaultIdEncoded, secretName: invalidName }); + await writer.write({ nameOrId: vaultIdEncoded, secretName: secretName2 }); + await writer.close(); + let errorCount = 0; + for await (const data of response.readable) { + if (data.type === 'error') { + // TS cannot properly evaluate a type as nested as this, so we use the + // as keyword to help it. Inside this block, the type of data is 'error'. + const error = data as ErrorMessage; + // No other file name should raise this error + expect(error.reason).toStrictEqual(invalidName); + errorCount++; + continue; + } + expect(data.type).toStrictEqual('success'); + } + // Only one error should have happened + expect(errorCount).toEqual(1); + // Check each secret was deleted + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.readF(async (efs) => { + expect(await efs.exists(secretName1)).toBeFalsy(); + expect(await efs.exists(secretName2)).toBeFalsy(); + }); + }); + }); + test('deletes multiple secrets in one log message', async () => { + // Create secret + const secretName1 = 'test-secret1'; + const secretName2 = 'test-secret2'; + const vaultId = await vaultManager.createVault('test-vault'); + const vaultIdEncoded = vaultsUtils.encodeVaultId(vaultId); + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.writeFile(secretName1, secretName1); + await efs.writeFile(secretName2, secretName2); + }); + }); + // Get log size + let logLength = 0; + await vaultManager.withVaults([vaultId], async (vault) => { + logLength = (await vault.log()).length; + }); + // Delete secret + const response = await rpcClient.methods.vaultsSecretsRemove(); + const writer = response.writable.getWriter(); + await writer.write({ nameOrId: vaultIdEncoded, secretName: secretName1 }); + await writer.write({ nameOrId: vaultIdEncoded, secretName: secretName2 }); + await writer.close(); + for await (const data of response.readable) { + expect(data.type).toStrictEqual('success'); + } + // Ensure single log message for deleting the secrets + await vaultManager.withVaults([vaultId], async (vault) => { + expect((await vault.log()).length).toEqual(logLength + 1); + }); + }); + test('deletes secrets from multiple vaults', async () => { + // Create secret + const secretName1 = 'test-secret1'; + const secretName2 = 'test-secret2'; + const secretName3 = 'test-secret3'; + const vaultId1 = await vaultManager.createVault('test-vault1'); const vaultId2 = await vaultManager.createVault('test-vault2'); const vaultIdEncoded1 = vaultsUtils.encodeVaultId(vaultId1); const vaultIdEncoded2 = vaultsUtils.encodeVaultId(vaultId2); - await createVaultSecret(vaultId1, secretName1, secretName1); - await createVaultSecret(vaultId1, secretName2, secretName2); - await createVaultSecret(vaultId2, secretName3, secretName3); - // Get log size - let logLength1 = 0; - let logLength2 = 0; + // Write files await vaultManager.withVaults( [vaultId1, vaultId2], async (vault1, vault2) => { - logLength1 = (await vault1.log()).length; - logLength2 = (await vault2.log()).length; + await vault1.writeF(async (efs) => { + await efs.writeFile(secretName1, secretName1); + await efs.writeFile(secretName3, secretName3); + }); + await vault2.writeF(async (efs) => { + await efs.writeFile(secretName2, secretName2); + }); }, ); // Delete secret - const deleteStream = await rpcClient.methods.vaultsSecretsRemove(); - const deleteWriter = deleteStream.writable.getWriter(); - await deleteWriter.write({ - nameOrId: vaultIdEncoded1, - secretName: secretName1, - }); - await deleteWriter.write({ - nameOrId: vaultIdEncoded1, - secretName: secretName2, - }); - await deleteWriter.write({ - nameOrId: vaultIdEncoded2, - secretName: secretName3, - }); - await deleteWriter.close(); - expect((await deleteStream.output).success).toBeTruthy(); + const response = await rpcClient.methods.vaultsSecretsRemove(); + const writer = response.writable.getWriter(); + await writer.write({ nameOrId: vaultIdEncoded1, secretName: secretName1 }); + await writer.write({ nameOrId: vaultIdEncoded2, secretName: secretName2 }); + await writer.write({ nameOrId: vaultIdEncoded1, secretName: secretName3 }); + await writer.close(); + for await (const data of response.readable) { + expect(data.type).toStrictEqual('success'); + } // Ensure single log message for deleting the secrets await vaultManager.withVaults( [vaultId1, vaultId2], async (vault1, vault2) => { - expect((await vault1.log()).length).toEqual(logLength1 + 1); - expect((await vault2.log()).length).toEqual(logLength2 + 1); + await vault1.readF(async (efs) => { + expect(await efs.exists(secretName1)).toBeFalsy(); + expect(await efs.exists(secretName3)).toBeFalsy(); + }); + await vault2.readF(async (efs) => { + expect(await efs.exists(secretName2)).toBeFalsy(); + }); }, ); }); test('should recursively delete directories', async () => { - // Create secrets const vaultId = await vaultManager.createVault('test-vault'); const vaultIdEncoded = vaultsUtils.encodeVaultId(vaultId); - const secretDir = 'secret-dir'; - const secretName1 = `${secretDir}/test-secret1`; - const secretName2 = `${secretDir}/test-secret2`; - const secretName3 = `${secretDir}/test-secret3`; - await createVaultDir(vaultId, secretDir); - await createVaultSecret(vaultId, secretName1, secretName1); - await createVaultSecret(vaultId, secretName2, secretName2); - await createVaultSecret(vaultId, secretName3, secretName3); - // Deleting directory with recursive set should not fail - const deleteStream = await rpcClient.methods.vaultsSecretsRemove(); - await (async () => { - const writer = deleteStream.writable.getWriter(); - await writer.write({ - nameOrId: vaultIdEncoded, - secretName: secretDir, - metadata: { options: { recursive: true } }, + const dirName = 'dir'; + const secretName1 = 'test-secret1'; + const secretName2 = 'test-secret2'; + const secretPath1 = path.join(dirName, secretName1); + const secretPath2 = path.join(dirName, secretName2); + // Create secrets + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.mkdir(dirName); + await efs.writeFile(secretPath1); + await efs.writeFile(secretPath2); }); - await writer.close(); - })(); - expect((await deleteStream.output).success).toBeTruthy(); + }); + // Deleting directory with recursive set should not fail + const response = await rpcClient.methods.vaultsSecretsRemove(); + const writer = response.writable.getWriter(); + await writer.write({ + nameOrId: vaultIdEncoded, + secretName: dirName, + metadata: { options: { recursive: true } }, + }); + await writer.close(); + for await (const data of response.readable) { + expect(data.type).toStrictEqual('success'); + } // Check each secret and the secret directory were deleted - await checkSecretIsDeleted(vaultId, secretName1); - await checkSecretIsDeleted(vaultId, secretName2); - await checkSecretIsDeleted(vaultId, secretName3); - await testsUtils.expectRemoteError( - rpcClient.methods.vaultsSecretsStat({ - nameOrId: vaultIdEncoded, - secretName: secretDir, - }), - vaultsErrors.ErrorSecretsSecretUndefined, - ); + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.readF(async (efs) => { + expect(await efs.exists(dirName)).toBeFalsy(); + expect(await efs.exists(secretPath1)).toBeFalsy(); + expect(await efs.exists(secretPath2)).toBeFalsy(); + }); + }); }); - test('should fail to delete directory without recursive option', async () => { - // Create secrets + test('fails to delete directory without recursive', async () => { const vaultId = await vaultManager.createVault('test-vault'); const vaultIdEncoded = vaultsUtils.encodeVaultId(vaultId); - const secretDir = 'secret-dir'; - const secretName1 = `${secretDir}/test-secret1`; - const secretName2 = `${secretDir}/test-secret2`; - const secretName3 = `${secretDir}/test-secret3`; - await createVaultDir(vaultId, secretDir); - await createVaultSecret(vaultId, secretName1, secretName1); - await createVaultSecret(vaultId, secretName2, secretName2); - await createVaultSecret(vaultId, secretName3, secretName3); - // Deleting directory with recursive unset should fail - const failDeleteStream = await rpcClient.methods.vaultsSecretsRemove(); - await (async () => { - const writer = failDeleteStream.writable.getWriter(); - await writer.write({ nameOrId: vaultIdEncoded, secretName: secretDir }); - await writer.close(); - })(); - await testsUtils.expectRemoteError( - failDeleteStream.output, - vaultsErrors.ErrorVaultsRecursive, - ); - // Check each secret and the secret directory exist - await checkSecretExists(vaultId, secretName1); - await checkSecretExists(vaultId, secretName2); - await checkSecretExists(vaultId, secretName3); - await checkSecretExists(vaultId, secretDir); + const dirName = 'dir'; + const secretName1 = 'test-secret1'; + const secretName2 = 'test-secret2'; + const secretPath1 = path.join(dirName, secretName1); + const secretPath2 = path.join(dirName, secretName2); + // Create secrets + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.mkdir(dirName); + await efs.writeFile(secretPath1); + await efs.writeFile(secretPath2); + }); + }); + // Deleting directory with recursive set should not fail + const response = await rpcClient.methods.vaultsSecretsRemove(); + const writer = response.writable.getWriter(); + await writer.write({ + nameOrId: vaultIdEncoded, + secretName: dirName, + }); + await writer.close(); + for await (const data of response.readable) { + expect(data.type).toStrictEqual('error'); + } + // Check each secret and the secret directory were deleted + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.readF(async (efs) => { + expect(await efs.exists(dirName)).toBeTruthy(); + expect(await efs.exists(secretPath1)).toBeTruthy(); + expect(await efs.exists(secretPath2)).toBeTruthy(); + }); + }); }); }); describe('vaultsSecretsNewDir and vaultsSecretsList', () => { diff --git a/tests/vaults/VaultOps/deleteSecret.test.ts b/tests/vaults/VaultOps/deleteSecret.test.ts index 4ffbe1cdd..f2061ca72 100644 --- a/tests/vaults/VaultOps/deleteSecret.test.ts +++ b/tests/vaults/VaultOps/deleteSecret.test.ts @@ -91,76 +91,54 @@ describe('deleteSecret', () => { test('deleting a secret', async () => { await testVaultsUtils.writeSecret(vault, secretName, secretContent); - await vaultOps.deleteSecret(vault, [secretName]); + await vaultOps.deleteSecret(vault, secretName); await testVaultsUtils.expectSecretNot(vault, secretName); }); test('deleting a secret in a directory', async () => { const secretPath = path.join(dirName, secretName); await testVaultsUtils.writeSecret(vault, secretPath, secretContent); - await vaultOps.deleteSecret(vault, [secretPath]); + await vaultOps.deleteSecret(vault, secretPath); await testVaultsUtils.expectSecretNot(vault, secretPath); await testVaultsUtils.expectDirExists(vault, dirName); }); test('deleting a directory', async () => { await testVaultsUtils.mkdir(vault, dirName); - await vaultOps.deleteSecret(vault, [dirName]); + await vaultOps.deleteSecret(vault, dirName); await testVaultsUtils.expectDirExistsNot(vault, dirName); }); test('deleting a directory with a file should fail', async () => { const secretPath = path.join(dirName, secretName); await testVaultsUtils.writeSecret(vault, secretPath, secretContent); - await expect(vaultOps.deleteSecret(vault, [dirName])).rejects.toThrow( + await expect(vaultOps.deleteSecret(vault, dirName)).rejects.toThrow( vaultsErrors.ErrorVaultsRecursive, ); }); test('deleting a directory with force', async () => { const secretPath = path.join(dirName, secretName); await testVaultsUtils.writeSecret(vault, secretPath, secretContent); - await vaultOps.deleteSecret(vault, [dirName], { recursive: true }); + await vaultOps.deleteSecret(vault, dirName, { recursive: true }); await testVaultsUtils.expectDirExistsNot(vault, dirName); }); test('deleting a secret that does not exist should fail', async () => { - await expect(vaultOps.deleteSecret(vault, [secretName])).rejects.toThrow( + await expect(vaultOps.deleteSecret(vault, secretName)).rejects.toThrow( vaultsErrors.ErrorSecretsSecretUndefined, ); }); - test('deleting multiple secrets', async () => { - const secretNames = ['secret1', 'secret2', 'secret3']; - for (const secretName of secretNames) { - await testVaultsUtils.writeSecret(vault, secretName, secretName); - } - await vaultOps.deleteSecret(vault, secretNames); - for (const secretName of secretNames) { - await testVaultsUtils.expectSecretNot(vault, secretName); - } - }); - test('deleting multiple secrets should add only one new log message', async () => { - const secretNames = ['secret1', 'secret2', 'secret3']; - for (const secretName of secretNames) { - await testVaultsUtils.writeSecret(vault, secretName, secretName); - } - const logLength = (await vault.log()).length; - await vaultOps.deleteSecret(vault, secretNames); - for (const secretName of secretNames) { - await testVaultsUtils.expectSecretNot(vault, secretName); - } - expect((await vault.log()).length).toBe(logLength + 1); - }); test('deleting a hidden secret', async () => { await testVaultsUtils.writeSecret(vault, secretNameHidden, secretContent); - await vaultOps.deleteSecret(vault, [secretNameHidden]); + await vaultOps.deleteSecret(vault, secretNameHidden); await testVaultsUtils.expectSecretNot(vault, secretNameHidden); }); test('deleting a hidden secret in a hidden directory', async () => { const secretPathHidden = path.join(dirNameHidden, secretNameHidden); await testVaultsUtils.writeSecret(vault, secretPathHidden, secretContent); - await vaultOps.deleteSecret(vault, [secretPathHidden]); + await vaultOps.deleteSecret(vault, secretPathHidden); await testVaultsUtils.expectSecretNot(vault, secretPathHidden); await testVaultsUtils.expectDirExists(vault, dirNameHidden); }); test('deleting a hidden directory', async () => { await testVaultsUtils.mkdir(vault, dirNameHidden); - await vaultOps.deleteSecret(vault, [dirNameHidden]); + await vaultOps.deleteSecret(vault, dirNameHidden); await testVaultsUtils.expectDirExistsNot(vault, dirNameHidden); }); }); diff --git a/tests/vaults/VaultOps/mkdir.test.ts b/tests/vaults/VaultOps/mkdir.test.ts index 9e0e0c0a1..ea97be492 100644 --- a/tests/vaults/VaultOps/mkdir.test.ts +++ b/tests/vaults/VaultOps/mkdir.test.ts @@ -2,7 +2,6 @@ import type { VaultId } from '@/vaults/types'; import type { Vault } from '@/vaults/Vault'; import type KeyRing from '@/keys/KeyRing'; import type { LevelPath } from '@matrixai/db'; -import type { ErrorMessage } from '@/client/types'; import fs from 'fs'; import path from 'path'; import os from 'os'; @@ -12,6 +11,7 @@ import { DB } from '@matrixai/db'; import VaultInternal from '@/vaults/VaultInternal'; import * as vaultOps from '@/vaults/VaultOps'; import * as vaultsUtils from '@/vaults/utils'; +import * as vaultsErrors from '@/vaults/errors'; import * as keysUtils from '@/keys/utils'; import * as testNodesUtils from '../../nodes/utils'; import * as testVaultsUtils from '../utils'; @@ -89,44 +89,39 @@ describe('mkdir', () => { }); test('can create directory', async () => { - const response = await vaultOps.mkdir(vault, dirName); - expect(response.type).toEqual('success'); + await vaultOps.mkdir(vault, dirName); await testVaultsUtils.expectDirExists(vault, dirName); }); test('can create recursive directory', async () => { const dirPath = path.join(dirName, dirName); - const response = await vaultOps.mkdir(vault, dirPath, { + await vaultOps.mkdir(vault, dirPath, { recursive: true, }); - expect(response.type).toEqual('success'); await testVaultsUtils.expectDirExists(vault, dirPath); }); test('creating directories fails without recursive', async () => { const dirPath = path.join(dirName, dirName); - const response = await vaultOps.mkdir(vault, dirPath); - expect(response.type).toEqual('error'); - const error = response as ErrorMessage; - expect(error.code).toEqual('ENOENT'); + await expect(vaultOps.mkdir(vault, dirPath)).rejects.toThrow( + vaultsErrors.ErrorVaultsRecursive, + ); await testVaultsUtils.expectDirExistsNot(vault, dirPath); }); test('creating existing directory should fail', async () => { await testVaultsUtils.mkdir(vault, dirName); - const response = await vaultOps.mkdir(vault, dirName); - expect(response.type).toEqual('error'); - const error = response as ErrorMessage; - expect(error.code).toEqual('EEXIST'); + await expect(vaultOps.mkdir(vault, dirName)).rejects.toThrow( + vaultsErrors.ErrorSecretsSecretDefined, + ); + await testVaultsUtils.expectDirExists(vault, dirName); }); test('creating existing secret should fail', async () => { await testVaultsUtils.writeSecret(vault, secretName, secretContent); - const response = await vaultOps.mkdir(vault, secretName); - expect(response.type).toEqual('error'); - const error = response as ErrorMessage; - expect(error.code).toEqual('EEXIST'); + await expect(vaultOps.mkdir(vault, secretName)).rejects.toThrow( + vaultsErrors.ErrorSecretsSecretDefined, + ); await testVaultsUtils.expectSecret(vault, secretName, secretContent); }); test('can create a hidden directory', async () => { - const response = await vaultOps.mkdir(vault, dirNameHidden); - expect(response.type).toEqual('success'); + await vaultOps.mkdir(vault, dirNameHidden); await testVaultsUtils.expectDirExists(vault, dirNameHidden); }); });