Skip to content

Commit

Permalink
feat: added secrets write command
Browse files Browse the repository at this point in the history
  • Loading branch information
aryanjassal committed Sep 27, 2024
1 parent a901228 commit adab612
Show file tree
Hide file tree
Showing 4 changed files with 249 additions and 32 deletions.
51 changes: 19 additions & 32 deletions src/secrets/CommandEdit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,18 @@ class CommandEdit extends CommandPolykey {
const tmpFile = path.join(tmpDir, path.basename(secretPath[1]));
const secretExists = await binUtils.retryAuthentication(
async (auth) => {
let exists: boolean = true;
const response =
await pkClient.rpcClient.methods.vaultsSecretsGet();
await (async () => {
const writer = response.writable.getWriter();
await writer.write({
nameOrId: secretPath[0],
secretName: secretPath[1],
metadata: auth,
});
await writer.close();
})();
let exists = true;
const res = await pkClient.rpcClient.methods.vaultsSecretsGet();
const writer = res.writable.getWriter();
await writer.write({
nameOrId: secretPath[0],
secretName: secretPath[1],
metadata: auth,
});
await writer.close();
try {
let rawSecretContent: string = '';
for await (const chunk of response.readable) {
for await (const chunk of res.readable) {
rawSecretContent += chunk.secretContent;
}
const secretContent = Buffer.from(rawSecretContent, 'binary');
Expand All @@ -99,9 +96,8 @@ class CommandEdit extends CommandPolykey {
execSync(`${process.env.EDITOR} \"${tmpFile}\"`, { stdio: 'inherit' });
let content: string;
try {
content = (await this.fs.promises.readFile(tmpFile)).toString(
'binary',
);
const buffer = await this.fs.promises.readFile(tmpFile);
content = buffer.toString('binary');
} catch (e) {
if (e.code === 'ENOENT') {
// If the secret exists but the file doesn't, then something went
Expand All @@ -125,26 +121,17 @@ class CommandEdit extends CommandPolykey {
}
throw e;
}
await binUtils.retryAuthentication(async (auth) => {
// This point will never be reached if the temp file doesn't exist.
// As such, if the secret didn't exist before, then we want to make it.
// Otherwise, if the secret existed before, then we want to edit it.
if (secretExists) {
await pkClient.rpcClient.methods.vaultsSecretsEdit({
metadata: auth,
nameOrId: secretPath[0],
secretName: secretPath[1],
secretContent: content,
});
} else {
await pkClient.rpcClient.methods.vaultsSecretsNew({
// We will reach here only when the user wants to write a new secret.
await binUtils.retryAuthentication(
async (auth) =>
await pkClient.rpcClient.methods.vaultsSecretsWriteFile({
metadata: auth,
nameOrId: secretPath[0],
secretName: secretPath[1],
secretContent: content,
});
}
}, meta);
}),
meta,
);
// Windows
// TODO: complete windows impl
} finally {
Expand Down
2 changes: 2 additions & 0 deletions src/secrets/CommandSecrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import CommandRename from './CommandRename';
import CommandRemove from './CommandRemove';
import CommandUpdate from './CommandUpdate';
import CommandStat from './CommandStat';
import CommandWrite from './CommandWrite';
import CommandPolykey from '../CommandPolykey';

class CommandSecrets extends CommandPolykey {
Expand All @@ -27,6 +28,7 @@ class CommandSecrets extends CommandPolykey {
this.addCommand(new CommandRemove(...args));
this.addCommand(new CommandUpdate(...args));
this.addCommand(new CommandStat(...args));
this.addCommand(new CommandWrite(...args));
}
}

Expand Down
92 changes: 92 additions & 0 deletions src/secrets/CommandWrite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type PolykeyClient from 'polykey/dist/PolykeyClient';
import CommandPolykey from '../CommandPolykey';
import * as binProcessors from '../utils/processors';
import * as binParsers from '../utils/parsers';
import * as binUtils from '../utils';
import * as binOptions from '../utils/options';

class CommandStat extends CommandPolykey {
constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
super(...args);
this.name('write');
this.description('Write data into a secret from standard in');
this.argument(
'<secretPath>',
'Path to the secret, specified as <vaultName>:<directoryPath>',
binParsers.parseSecretPathValue,
);
this.addOption(binOptions.nodeId);
this.addOption(binOptions.clientHost);
this.addOption(binOptions.clientPort);
this.action(async (secretPath, options) => {
const { default: PolykeyClient } = await import(
'polykey/dist/PolykeyClient'
);
const clientOptions = await binProcessors.processClientOptions(
options.nodePath,
options.nodeId,
options.clientHost,
options.clientPort,
this.fs,
this.logger.getChild(binProcessors.processClientOptions.name),
);
const meta = await binProcessors.processAuthentication(
options.passwordFile,
this.fs,
);

let pkClient: PolykeyClient;
this.exitHandlers.handlers.push(async () => {
if (pkClient != null) await pkClient.stop();
});
try {
pkClient = await PolykeyClient.createPolykeyClient({
nodeId: clientOptions.nodeId,
host: clientOptions.clientHost,
port: clientOptions.clientPort,
options: {
nodePath: options.nodePath,
},
logger: this.logger.getChild(PolykeyClient.name),
});

let stdin: string = '';
await new Promise<void>((resolve, reject) => {
const cleanup = () => {
process.stdin.removeListener('data', dataHandler);
process.stdin.removeListener('error', errorHandler);
process.stdin.removeListener('end', endHandler);
};
const dataHandler = (data: Buffer) => {
stdin += data.toString();
};
const errorHandler = (err: Error) => {
cleanup();
reject(err);
};
const endHandler = () => {
cleanup();
resolve();
};
process.stdin.on('data', dataHandler);
process.stdin.once('error', errorHandler);
process.stdin.once('end', endHandler);
});
await binUtils.retryAuthentication(
async (auth) =>
await pkClient.rpcClient.methods.vaultsSecretsWriteFile({
metadata: auth,
nameOrId: secretPath[0],
secretName: secretPath[1],
secretContent: stdin,
}),
meta,
);
} finally {
if (pkClient! != null) await pkClient.stop();
}
});
}
}

export default CommandStat;
136 changes: 136 additions & 0 deletions tests/secrets/writeFile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import type { VaultName } from 'polykey/dist/vaults/types';
import path from 'path';
import fs from 'fs';
import fc from 'fast-check';
import { test } from '@fast-check/jest';
import Logger, { LogLevel, StreamHandler } from '@matrixai/logger';
import PolykeyAgent from 'polykey/dist/PolykeyAgent';
import { vaultOps } from 'polykey/dist/vaults';
import * as keysUtils from 'polykey/dist/keys/utils';
import * as testUtils from '../utils';

describe('commandWriteFile', () => {
const password = 'password';
const logger = new Logger('CLI Test', LogLevel.WARN, [new StreamHandler()]);
const stdinArb = fc.string({ minLength: 0, maxLength: 100 });
const contentArb = fc.constantFrom('content', '');
let dataDir: string;
let polykeyAgent: PolykeyAgent;
let command: Array<string>;
let vaultNumber: number = 0;

// Helper function to generate unique vault names
function genVaultName() {
vaultNumber++;
return `vault-${vaultNumber}` as VaultName;
}

beforeEach(async () => {
dataDir = await fs.promises.mkdtemp(
path.join(globalThis.tmpDir, 'polykey-test-'),
);
polykeyAgent = await PolykeyAgent.createPolykeyAgent({
password,
options: {
nodePath: dataDir,
agentServiceHost: '127.0.0.1',
clientServiceHost: '127.0.0.1',
keys: {
passwordOpsLimit: keysUtils.passwordOpsLimits.min,
passwordMemLimit: keysUtils.passwordMemLimits.min,
strictMemoryLock: false,
},
},
logger: logger,
});
});
afterEach(async () => {
await polykeyAgent.stop();
await fs.promises.rm(dataDir, {
force: true,
recursive: true,
});
});

test.prop([stdinArb, contentArb], { numRuns: 5 })(
'should write secret',
async (stdinData, secretContent) => {
const vaultName = genVaultName();
const vaultId = await polykeyAgent.vaultManager.createVault(vaultName);
const secretName = 'secret';
await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
await vaultOps.addSecret(vault, secretName, secretContent);
});
command = [
'secrets',
'write',
'-np',
dataDir,
`${vaultName}:${secretName}`,
];

const childProcess = await testUtils.pkSpawn(
command,
{
env: { PK_PASSWORD: password },
cwd: dataDir,
},
logger,
);
// The conditions of stdin being null will not be met in the test, so we
// don't have to worry about the fields being null.
childProcess.stdin!.write(stdinData);
childProcess.stdin!.end();
const exitCode = await new Promise((resolve) => {
childProcess.once('exit', (code) => {
const exitCode = code ?? -255;
childProcess.removeAllListeners('data');
resolve(exitCode);
});
});
expect(exitCode).toStrictEqual(0);
await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
const contents = await vaultOps.getSecret(vault, secretName);
expect(contents.toString()).toStrictEqual(stdinData);
});
},
);
test('should overwrite secret', async () => {
const vaultName = 'vault' as VaultName;
const vaultId = await polykeyAgent.vaultManager.createVault(vaultName);
const secretName = 'secret';
const newContent = 'new contents';
command = [
'secrets',
'write',
'-np',
dataDir,
`${vaultName}:${secretName}`,
];

const childProcess = await testUtils.pkSpawn(
command,
{
env: { PK_PASSWORD: password },
cwd: dataDir,
},
logger,
);
// The conditions of stdin being null will not be met in the test, so we
// don't have to worry about the fields being null.
childProcess.stdin!.write(newContent);
childProcess.stdin!.end();
const exitCode = await new Promise((resolve) => {
childProcess.once('exit', (code) => {
const exitCode = code ?? -255;
childProcess.removeAllListeners('data');
resolve(exitCode);
});
});
expect(exitCode).toStrictEqual(0);
await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
const contents = await vaultOps.getSecret(vault, secretName);
expect(contents.toString()).toStrictEqual(newContent);
});
});
});

0 comments on commit adab612

Please # to comment.