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/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 582715ecb..a0fa0f94d 100644 --- a/src/client/handlers/VaultsSecretsGet.ts +++ b/src/client/handlers/VaultsSecretsGet.ts @@ -2,66 +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>, - ): AsyncGenerator> { + input: ClientRPCRequestParams, + ): 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. - let metadata: any = undefined; - for await (const secretIdentiferMessage of input) { - 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/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 e79e62c0b..ae415bb1e 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -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 d5f9e5401..a87d8defd 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -13,7 +13,7 @@ class ErrorPolykeyUnknown extends ErrorPolykey { class ErrorPolykeyUnexpected extends ErrorPolykey { static description = 'Unknown error occurred'; - exitCode = sysexits.PROTOCOL; + exitCode = sysexits.UNKNOWN; } class ErrorPolykeyAgentRunning extends ErrorPolykey { diff --git a/tests/client/handlers/vaults.test.ts b/tests/client/handlers/vaults.test.ts index 0dc1e0220..93fb1efc7 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, @@ -1602,8 +1605,330 @@ 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}`, + ), + ]); + 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<{ + vaultsSecretsCat: typeof vaultsSecretsCat; + }>; + 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: { + vaultsSecretsCat: new VaultsSecretsCat({ + 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: { + vaultsSecretsCat, + }, + 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'; + // Cat file + const response = await rpcClient.methods.vaultsSecretsCat(); + const writer = response.writable.getWriter(); + await writer.write({ + nameOrId: vaultName, + secretName: secretName, + }); + await writer.close(); + // Read response + const consumeP = async () => { + for await (const _ of response.readable); + }; + await testsUtils.expectRemoteError( + consumeP(), + vaultsErrors.ErrorVaultsVaultUndefined, + ); + }); + 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); + }); + }); + // Cat file + const response = await rpcClient.methods.vaultsSecretsCat(); + const writer = response.writable.getWriter(); + await writer.write({ + nameOrId: vaultsUtils.encodeVaultId(vaultId), + secretName: secretName, + }); + 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 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); + } + }); + 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); + }); + }); + // Cat file + const response = await rpcClient.methods.vaultsSecretsCat(); + const writer = response.writable.getWriter(); + await writer.write({ + nameOrId: vaultsUtils.encodeVaultId(vaultId), + secretName: secretName, + }); + 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('reads multiple secrets in order', async () => { + const vaultName = 'test-vault'; + const vaultId = await vaultManager.createVault(vaultName); + const vaultIdEncoded = vaultsUtils.encodeVaultId(vaultId); + 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); + }); + }); + // 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('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); + 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}`, ), @@ -1617,54 +1942,401 @@ 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; }>; let vaultManager: VaultManager; - // Helper function to create secrets in a vault - const createVaultSecret = async ( - vaultId: VaultId, - secretName: string, - content: string, - ) => { + 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, content); - expect(await efs.exists(secretName)).toBeTruthy(); + await efs.writeFile(secretName, secretContent); }); }); - }; - // Helper function to ensure each file path was deleted - const checkSecretIsDeleted = async (vaultId: VaultId, secretName: string) => { + // 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) => { - expect(await efs.exists(secretName)).toBeFalsy(); + const fileContent = await efs.readFile(secretName); + expect(fileContent.toString()).toEqual(secretContent); }); }); - }; - // Helper function to ensure each file path exists in the vault - const checkSecretExists = async (vaultId: VaultId, secretName: string) => { + }); + 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) => { - expect(await efs.exists(secretName)).toBeTruthy(); + 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); }); }); - }; - // Helper function to create a directory - const createVaultDir = async ( - vaultId: VaultId, - dirName: string, - recursive: boolean = false, - ) => { + }); + 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.mkdir(dirName, { recursive: recursive }); + 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; + // // 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-'), @@ -1701,26 +2373,10 @@ 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({ - db, - vaultManager, - }), }, host: localhost, }); @@ -1734,11 +2390,7 @@ describe('vaultsSecretsNew and vaultsSecretsDelete, vaultsSecretsGet', () => { }); rpcClient = new RPCClient({ manifest: { - vaultsSecretsNew, vaultsSecretsRemove, - vaultsSecretsGet, - vaultsSecretsMkdir, - vaultsSecretsStat, }, streamFactory: () => webSocketClient.connection.newStream(), toError: networkUtils.toError, @@ -1756,247 +2408,72 @@ describe('vaultsSecretsNew and vaultsSecretsDelete, vaultsSecretsGet', () => { recursive: true, }); }); - test('creating secret should fail with invalid vault name', async () => { - const newP = rpcClient.methods.vaultsSecretsNew({ - nameOrId: 'invalid', - secretName: 'invalid', - secretContent: 'invalid', - }); - await testsUtils.expectRemoteError( - newP, - vaultsErrors.ErrorVaultsVaultUndefined, - ); - }); - test('getting secret should fail with invalid vault name', async () => { - const response = await rpcClient.methods.vaultsSecretsGet(); + test('fails with invalid vault name', async () => { // Write paths - const writer = response.writable.getWriter(); - await writer.write({ - nameOrId: 'invalid', - secretName: 'invalid', - }); - await writer.close(); - // Read response - const getP = async () => { - for await (const _ of response.readable); - }; - await testsUtils.expectRemoteError( - getP(), - vaultsErrors.ErrorVaultsVaultUndefined, - ); - }); - test('deleting secret should fail with invalid vault name', async () => { const response = await rpcClient.methods.vaultsSecretsRemove(); - // Write paths const writer = response.writable.getWriter(); - await writer.write({ - nameOrId: 'invalid', - secretName: 'invalid', - }); + await writer.write({ nameOrId: 'invalid', secretName: 'invalid' }); await writer.close(); // Read response - const deleteP = async () => { + const consumeP = async () => { for await (const _ of response.readable); }; await testsUtils.expectRemoteError( - deleteP(), + consumeP(), vaultsErrors.ErrorVaultsVaultUndefined, ); }); - 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(); - for await (const data of deleteStream.readable) { - expect(data.type).toStrictEqual('success'); - } - // Check secret was deleted - const deleteGetStream = await rpcClient.methods.vaultsSecretsGet(); - const deleteGetWriter = deleteGetStream.writable.getWriter(); - await deleteGetWriter.write({ - nameOrId: vaultIdEncoded, - secretName: secretName, - }); - await deleteGetWriter.close(); - await testsUtils.expectRemoteError( - (async () => { - for await (const _ of deleteGetStream.readable); - })(), - vaultsErrors.ErrorSecretsSecretUndefined, - ); - }); - test('gets multiple secrets in order', async () => { + test('deletes multiple secrets', 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, - }); - await getWriter.write({ - nameOrId: vaultIdEncoded, - secretName: secretName2, - }); - await getWriter.write({ - nameOrId: vaultIdEncoded, - secretName: secretName3, + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.writeFile(secretName1, secretName1); + await efs.writeFile(secretName2, secretName2); + }); }); - await getWriter.close(); - let secretContent: string = ''; - for await (const data of getStream.readable) { - secretContent += data.secretContent; - } - expect(secretContent).toStrictEqual( - `${secretName1}${secretName2}${secretName3}`, - ); - }); - test('fails to get a directory', async () => { - // Create directory - const dirName = 'dir'; - const vaultId = await vaultManager.createVault('test-vault'); - const vaultIdEncoded = vaultsUtils.encodeVaultId(vaultId); - await createVaultDir(vaultId, dirName); - // Write directory path - const response = await rpcClient.methods.vaultsSecretsGet(); + // Delete secrets + const response = await rpcClient.methods.vaultsSecretsRemove(); const writer = response.writable.getWriter(); - await writer.write({ - nameOrId: vaultIdEncoded, - secretName: dirName, - metadata: { options: { continueOnError: true } }, - }); + await writer.write({ nameOrId: vaultIdEncoded, secretName: secretName1 }); + await writer.write({ nameOrId: vaultIdEncoded, secretName: secretName2 }); await writer.close(); - // Read response for await (const data of response.readable) { - expect(data.error).toInclude(dirName); - } - }); - test('continues getting secrets if option 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 } }, - }); - await getWriter.write({ nameOrId: vaultIdEncoded, secretName: 'invalid' }); - await getWriter.write({ - nameOrId: vaultIdEncoded, - secretName: secretName2, - }); - 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); - }); - 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'); - 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, - }); - await deleteWriter.close(); - for await (const data of deleteStream.readable) { expect(data.type).toStrictEqual('success'); } // Check each secret was deleted - await checkSecretIsDeleted(vaultId, secretName1); - await checkSecretIsDeleted(vaultId, secretName2); + 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 deleting secrets if invalid secret is present', async () => { + test('continues on error', async () => { // Create secrets const secretName1 = 'test-secret1'; const secretName2 = 'test-secret2'; - const invalidName = 'invalid-name'; + const invalidName = 'invalid'; const vaultId = await vaultManager.createVault('test-vault'); 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: invalidName, - }); - await deleteWriter.write({ - nameOrId: vaultIdEncoded, - secretName: secretName2, + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.writeF(async (efs) => { + await efs.writeFile(secretName1, secretName1); + await efs.writeFile(secretName2, secretName2); + }); }); - await deleteWriter.close(); + // 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 deleteStream.readable) { + 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'. @@ -2008,48 +2485,48 @@ describe('vaultsSecretsNew and vaultsSecretsDelete, vaultsSecretsGet', () => { } expect(data.type).toStrictEqual('success'); } + // Only one error should have happened expect(errorCount).toEqual(1); // Check each secret was deleted - await checkSecretIsDeleted(vaultId, secretName1); - await checkSecretIsDeleted(vaultId, secretName2); + await vaultManager.withVaults([vaultId], async (vault) => { + await vault.readF(async (efs) => { + expect(await efs.exists(secretName1)).toBeFalsy(); + expect(await efs.exists(secretName2)).toBeFalsy(); + }); + }); }); - test('gets secrets from multiple vaults', async () => { + test('deletes multiple secrets in one log message', 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 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, + 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); + }); }); - await getWriter.write({ - nameOrId: vaultIdEncoded2, - secretName: secretName3, + // Get log size + let logLength = 0; + await vaultManager.withVaults([vaultId], async (vault) => { + logLength = (await vault.log()).length; }); - await getWriter.close(); - let secretContent: string = ''; - for await (const data of getStream.readable) { - secretContent += data.secretContent; + // 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'); } - expect(secretContent).toStrictEqual( - `${secretName1}${secretName2}${secretName3}`, - ); + // 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 in one log', async () => { + test('deletes secrets from multiple vaults', async () => { // Create secret const secretName1 = 'test-secret1'; const secretName2 = 'test-secret2'; @@ -2058,116 +2535,115 @@ describe('vaultsSecretsNew and vaultsSecretsDelete, vaultsSecretsGet', () => { 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(); - for await (const data of deleteStream.readable) { + 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 dirName = 'dir'; const secretName1 = 'test-secret1'; const secretName2 = 'test-secret2'; - const secretName3 = 'test-secret3'; - const secretPath1 = path.join(secretDir, secretName1); - const secretPath2 = path.join(secretDir, secretName2); - const secretPath3 = path.join(secretDir, secretName3); - await createVaultDir(vaultId, secretDir); - await createVaultSecret(vaultId, secretPath1, secretPath1); - await createVaultSecret(vaultId, secretPath2, secretPath2); - await createVaultSecret(vaultId, secretPath3, secretPath3); - // 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 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(); - })(); - for await (const data of deleteStream.readable) { + }); + // 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, secretPath1); - await checkSecretIsDeleted(vaultId, secretPath2); - await checkSecretIsDeleted(vaultId, secretPath3); - 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 dirName = 'dir'; const secretName1 = 'test-secret1'; const secretName2 = 'test-secret2'; - const secretName3 = 'test-secret3'; - const secretPath1 = path.join(secretDir, secretName1); - const secretPath2 = path.join(secretDir, secretName2); - const secretPath3 = path.join(secretDir, secretName3); - await createVaultDir(vaultId, secretDir); - await createVaultSecret(vaultId, secretPath1, secretPath1); - await createVaultSecret(vaultId, secretPath2, secretPath2); - await createVaultSecret(vaultId, secretPath3, secretPath3); - // Deleting directory with recursive unset should fail + 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: secretDir }); + 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 exist - await checkSecretExists(vaultId, secretPath1); - await checkSecretExists(vaultId, secretPath2); - await checkSecretExists(vaultId, secretPath3); - await checkSecretExists(vaultId, secretDir); + // 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', () => {