Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementing UNIX-like secrets ls command #255

Merged
merged 3 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion npmDepsHash
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sha256-NF/FIaXGr8EAxGRp+e9KP9+Vcb3KFzJBir/bZqU0J+w=
sha256-N6qZhhoqgktogCaRmQO1/9dFbSL9SISI5y/37kjD95U=
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "polykey-cli",
"version": "0.6.5",
"version": "0.6.3",
"homepage": "https://polykey.com",
"author": "Roger Qiu",
"contributors": [
Expand Down Expand Up @@ -150,7 +150,7 @@
"nexpect": "^0.6.0",
"node-gyp-build": "^4.4.0",
"nodemon": "^3.0.1",
"polykey": "^1.9.0",
"polykey": "^1.10.0",
"prettier": "^3.0.0",
"shelljs": "^0.8.5",
"shx": "^0.3.4",
Expand Down
42 changes: 23 additions & 19 deletions src/secrets/CommandList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@ import CommandPolykey from '../CommandPolykey';
import * as binUtils from '../utils';
import * as binOptions from '../utils/options';
import * as binProcessors from '../utils/processors';
import * as binParsers from '../utils/parsers';

class CommandList extends CommandPolykey {
constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
super(...args);
this.name('list');
this.aliases(['ls']);
this.description('List all Available Secrets for a Vault');
this.argument('<vaultName>', 'Name of the vault to list secrets from');
this.name('ls');
this.aliases(['list']);
this.description('List all secrets for a vault within a directory');
this.argument(
'<directoryPath>',
'Directory to list files from, specified as <vaultName>[:<path>]',
binParsers.parseSecretPathOptional,
);
this.addOption(binOptions.nodeId);
this.addOption(binOptions.clientHost);
this.addOption(binOptions.clientPort);
this.action(async (vaultName, options) => {
this.action(async (vaultPattern, options) => {
const { default: PolykeyClient } = await import(
'polykey/dist/PolykeyClient'
);
Expand All @@ -40,39 +45,38 @@ class CommandList extends CommandPolykey {
nodeId: clientOptions.nodeId,
host: clientOptions.clientHost,
port: clientOptions.clientPort,
options: {
nodePath: options.nodePath,
},
options: { nodePath: options.nodePath },
logger: this.logger.getChild(PolykeyClient.name),
});
const data = await binUtils.retryAuthentication(async (auth) => {
const data: Array<{ secretName: string }> = [];
const secretPaths = await binUtils.retryAuthentication(async (auth) => {
const secretPaths: Array<string> = [];
const stream = await pkClient.rpcClient.methods.vaultsSecretsList({
metadata: auth,
nameOrId: vaultName,
nameOrId: vaultPattern[0],
secretName: vaultPattern[1] ?? '/',
});
for await (const secret of stream) {
data.push({
secretName: secret.secretName,
});
// Remove leading slashes
if (secret.path.startsWith('/')) {
secret.path = secret.path.substring(1);
}
secretPaths.push(secret.path);
}
return data;
return secretPaths;
}, auth);

if (options.format === 'json') {
process.stdout.write(
binUtils.outputFormatter({
type: 'json',
data: data,
data: secretPaths,
}),
);
} else {
process.stdout.write(
binUtils.outputFormatter({
type: 'list',
data: data.map(
(secretsListMessage) => secretsListMessage.secretName,
),
data: secretPaths,
}),
);
}
Expand Down
4 changes: 2 additions & 2 deletions src/secrets/CommandSecrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import CommandList from './CommandList';
import CommandMkdir from './CommandMkdir';
import CommandRename from './CommandRename';
import CommandUpdate from './CommandUpdate';
import commandStat from './CommandStat';
import CommandStat from './CommandStat';
import CommandPolykey from '../CommandPolykey';

class CommandSecrets extends CommandPolykey {
Expand All @@ -26,7 +26,7 @@ class CommandSecrets extends CommandPolykey {
this.addCommand(new CommandMkdir(...args));
this.addCommand(new CommandRename(...args));
this.addCommand(new CommandUpdate(...args));
this.addCommand(new commandStat(...args));
this.addCommand(new CommandStat(...args));
}
}

Expand Down
28 changes: 23 additions & 5 deletions src/utils/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as gestaltsUtils from 'polykey/dist/gestalts/utils';
import * as networkUtils from 'polykey/dist/network/utils';
import * as nodesUtils from 'polykey/dist/nodes/utils';

const secretPathRegex = /^([\w-]+)(?::)([^\0\\=]+)$/;
const secretPathRegex = /^([\w-]+)(?::([^\0\\=]+))?$/;
const secretPathValueRegex = /^([a-zA-Z_][\w]+)?$/;
const environmentVariableRegex = /^([a-zA-Z_]+[a-zA-Z0-9_]*)?$/;

Expand Down Expand Up @@ -65,25 +65,42 @@ function parseCoreCount(v: string): number | undefined {
}
}

function parseSecretPath(secretPath: string): [string, string, string?] {
function parseSecretPathOptional(
secretPath: string,
): [string, string?, string?] {
// E.g. If 'vault1:a/b/c', ['vault1', 'a/b/c'] is returned
// If 'vault1:a/b/c=VARIABLE', ['vault1, 'a/b/c', 'VARIABLE'] is returned
// If 'vault1', ['vault1, undefined] is returned
// splits out everything after an `=` separator
const lastEqualIndex = secretPath.lastIndexOf('=');
const splitSecretPath =
lastEqualIndex === -1
? secretPath
: secretPath.substring(0, lastEqualIndex);
const value =
lastEqualIndex === -1 ? '' : secretPath.substring(lastEqualIndex + 1);
lastEqualIndex === -1
? undefined
: secretPath.substring(lastEqualIndex + 1);
if (!secretPathRegex.test(splitSecretPath)) {
throw new commander.InvalidArgumentError(
`${splitSecretPath} is not of the format <vaultName>:<directoryPath>`,
`${secretPath} is not of the format <vaultName>[:<directoryPath>][=<value>]`,
);
}
const [, vaultName, directoryPath] = splitSecretPath.match(secretPathRegex)!;
return [vaultName, directoryPath, value];
}

function parseSecretPath(secretPath: string): [string, string, string?] {
// E.g. If 'vault1:a/b/c', ['vault1', 'a/b/c'] is returned
// If 'vault1', an error is thrown
const [vaultName, secretName, value] = parseSecretPathOptional(secretPath);
if (secretName === undefined) {
throw new commander.InvalidArgumentError(
`${secretPath} is not of the format <vaultName>:<directoryPath>[=<value>]`,
);
}
return [vaultName, secretName, value];
}

function parseSecretPathValue(secretPath: string): [string, string, string?] {
const [vaultName, directoryPath, value] = parseSecretPath(secretPath);
if (value != null && !secretPathValueRegex.test(value)) {
Expand Down Expand Up @@ -204,6 +221,7 @@ export {
validateParserToArgParser,
validateParserToArgListParser,
parseCoreCount,
parseSecretPathOptional,
parseSecretPath,
parseSecretPathValue,
parseSecretPathEnv,
Expand Down
2 changes: 1 addition & 1 deletion src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ function outputFormatterList(items: Array<string>): string {
* If it is `Record<string, number>`, the `number` values will be used as the initial padding lengths.
* The object is also mutated if any cells exceed the initial padding lengths.
* This parameter can also be supplied to filter the columns that will be displayed.
* @param options.includeHeaders - Defaults to `True`
* @param options.includeHeaders - Defaults to `True`.
* @param options.includeRowCount - Defaults to `False`.
* @returns
*/
Expand Down
3 changes: 1 addition & 2 deletions tests/secrets/env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -764,7 +764,7 @@ describe('commandEnv', () => {
const jsonOut = JSON.parse(result.stdout);
expect(jsonOut['SECRET']).toBe('this is a secret\nit has multiple lines\n');
});
test.only.prop([
test.prop([
testUtils.secretPathEnvArrayArb,
fc.string().noShrink(),
testUtils.cmdArgsArrayArb,
Expand All @@ -773,7 +773,6 @@ describe('commandEnv', () => {
async (secretPathEnvArray, cmd, cmdArgsArray) => {
// If we don't use the optional `--` delimiter then we can't include `:` in vault names
fc.pre(!cmd.includes(':'));

let output:
| [Array<[string, string, string?]>, Array<string>]
| undefined = undefined;
Expand Down
106 changes: 101 additions & 5 deletions tests/secrets/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,27 +41,123 @@ describe('commandListSecrets', () => {
});
});

test(
'should fail when vault does not exist',
async () => {
command = ['secrets', 'ls', '-np', dataDir, 'DoesntExist'];
const result = await testUtils.pkStdio([...command], {
env: {
PK_PASSWORD: password,
},
cwd: dataDir,
});
expect(result.exitCode).toBe(64); // Sysexits.USAGE
},
globalThis.defaultTimeout * 2,
);

test(
'should list secrets',
async () => {
const vaultName = 'Vault4' as VaultName;
const vaultId = await polykeyAgent.vaultManager.createVault(vaultName);

await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
await vaultOps.addSecret(vault, 'MySecret1', 'this is the secret 1');
await vaultOps.addSecret(vault, 'MySecret2', 'this is the secret 2');
await vaultOps.addSecret(vault, 'MySecret3', 'this is the secret 3');
await vaultOps.addSecret(vault, 'MySecret1', '');
await vaultOps.addSecret(vault, 'MySecret2', '');
await vaultOps.addSecret(vault, 'MySecret3', '');
});

command = ['secrets', 'list', '-np', dataDir, vaultName];

command = ['secrets', 'ls', '-np', dataDir, vaultName];
const result = await testUtils.pkStdio([...command], {
env: {
PK_PASSWORD: password,
},
cwd: dataDir,
});
expect(result.exitCode).toBe(0);
expect(result.stdout).toBe('MySecret1\nMySecret2\nMySecret3\n');
},
globalThis.defaultTimeout * 2,
);

test(
'should fail when path is not a directory',
async () => {
const vaultName = 'Vault5' as VaultName;
const vaultId = await polykeyAgent.vaultManager.createVault(vaultName);

await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
await vaultOps.mkdir(vault, 'SecretDir');
await vaultOps.addSecret(vault, 'SecretDir/MySecret1', '');
await vaultOps.addSecret(vault, 'SecretDir/MySecret2', '');
await vaultOps.addSecret(vault, 'SecretDir/MySecret3', '');
});

command = ['secrets', 'ls', '-np', dataDir, `${vaultName}:WrongDirName`];
let result = await testUtils.pkStdio([...command], {
env: {
PK_PASSWORD: password,
},
cwd: dataDir,
});
expect(result.exitCode).toBe(64);

command = [
'secrets',
'ls',
'-np',
dataDir,
`${vaultName}:SecretDir/MySecret1`,
];
result = await testUtils.pkStdio([...command], {
env: {
PK_PASSWORD: password,
},
cwd: dataDir,
});
expect(result.exitCode).toBe(64);
},
globalThis.defaultTimeout * 2,
);

test(
'should list secrets within directories',
async () => {
const vaultName = 'Vault6' as VaultName;
const vaultId = await polykeyAgent.vaultManager.createVault(vaultName);

await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
await vaultOps.mkdir(vault, 'SecretDir/NestedDir', { recursive: true });
await vaultOps.addSecret(vault, 'SecretDir/MySecret1', '');
await vaultOps.addSecret(vault, 'SecretDir/NestedDir/MySecret2', '');
});

command = ['secrets', 'ls', '-np', dataDir, `${vaultName}:SecretDir`];
let result = await testUtils.pkStdio([...command], {
env: {
PK_PASSWORD: password,
},
cwd: dataDir,
});
expect(result.exitCode).toBe(0);
expect(result.stdout).toBe('SecretDir/MySecret1\nSecretDir/NestedDir\n');

command = [
'secrets',
'ls',
'-np',
dataDir,
`${vaultName}:SecretDir/NestedDir`,
];
result = await testUtils.pkStdio([...command], {
env: {
PK_PASSWORD: password,
},
cwd: dataDir,
});
expect(result.exitCode).toBe(0);
expect(result.stdout).toBe('SecretDir/NestedDir/MySecret2\n');
},
globalThis.defaultTimeout * 2,
);
Expand Down
6 changes: 3 additions & 3 deletions tests/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@ async function nodesConnect(localNode: PolykeyAgent, remoteNode: PolykeyAgent) {
);
}

const secretPathWithoutEnvArb = fc
.stringMatching(binParsers.secretPathRegex)
.noShrink();
// This regex defines a vault secret path that always includes the secret path
const secretPathRegex = /^([\w-]+)(?::)([^\0\\=]+)$/;
const secretPathWithoutEnvArb = fc.stringMatching(secretPathRegex).noShrink();
const environmentVariableAre = fc
.stringMatching(binParsers.environmentVariableRegex)
.filter((v) => v.length > 0)
Expand Down