Skip to content

Commit 45edcf3

Browse files
authored
Merge pull request #255 from MatrixAI/feature-unix-ls
Implementing UNIX-like `secrets ls` command
2 parents 6ceb9f5 + c2d70c1 commit 45edcf3

10 files changed

Lines changed: 163 additions & 46 deletions

File tree

npmDepsHash

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
sha256-NF/FIaXGr8EAxGRp+e9KP9+Vcb3KFzJBir/bZqU0J+w=
1+
sha256-N6qZhhoqgktogCaRmQO1/9dFbSL9SISI5y/37kjD95U=

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "polykey-cli",
3-
"version": "0.6.5",
3+
"version": "0.6.3",
44
"homepage": "https://polykey.com",
55
"author": "Roger Qiu",
66
"contributors": [
@@ -150,7 +150,7 @@
150150
"nexpect": "^0.6.0",
151151
"node-gyp-build": "^4.4.0",
152152
"nodemon": "^3.0.1",
153-
"polykey": "^1.9.0",
153+
"polykey": "^1.10.0",
154154
"prettier": "^3.0.0",
155155
"shelljs": "^0.8.5",
156156
"shx": "^0.3.4",

src/secrets/CommandList.ts

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,23 @@ import CommandPolykey from '../CommandPolykey';
33
import * as binUtils from '../utils';
44
import * as binOptions from '../utils/options';
55
import * as binProcessors from '../utils/processors';
6+
import * as binParsers from '../utils/parsers';
67

78
class CommandList extends CommandPolykey {
89
constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
910
super(...args);
10-
this.name('list');
11-
this.aliases(['ls']);
12-
this.description('List all Available Secrets for a Vault');
13-
this.argument('<vaultName>', 'Name of the vault to list secrets from');
11+
this.name('ls');
12+
this.aliases(['list']);
13+
this.description('List all secrets for a vault within a directory');
14+
this.argument(
15+
'<directoryPath>',
16+
'Directory to list files from, specified as <vaultName>[:<path>]',
17+
binParsers.parseSecretPathOptional,
18+
);
1419
this.addOption(binOptions.nodeId);
1520
this.addOption(binOptions.clientHost);
1621
this.addOption(binOptions.clientPort);
17-
this.action(async (vaultName, options) => {
22+
this.action(async (vaultPattern, options) => {
1823
const { default: PolykeyClient } = await import(
1924
'polykey/dist/PolykeyClient'
2025
);
@@ -40,39 +45,38 @@ class CommandList extends CommandPolykey {
4045
nodeId: clientOptions.nodeId,
4146
host: clientOptions.clientHost,
4247
port: clientOptions.clientPort,
43-
options: {
44-
nodePath: options.nodePath,
45-
},
48+
options: { nodePath: options.nodePath },
4649
logger: this.logger.getChild(PolykeyClient.name),
4750
});
48-
const data = await binUtils.retryAuthentication(async (auth) => {
49-
const data: Array<{ secretName: string }> = [];
51+
const secretPaths = await binUtils.retryAuthentication(async (auth) => {
52+
const secretPaths: Array<string> = [];
5053
const stream = await pkClient.rpcClient.methods.vaultsSecretsList({
5154
metadata: auth,
52-
nameOrId: vaultName,
55+
nameOrId: vaultPattern[0],
56+
secretName: vaultPattern[1] ?? '/',
5357
});
5458
for await (const secret of stream) {
55-
data.push({
56-
secretName: secret.secretName,
57-
});
59+
// Remove leading slashes
60+
if (secret.path.startsWith('/')) {
61+
secret.path = secret.path.substring(1);
62+
}
63+
secretPaths.push(secret.path);
5864
}
59-
return data;
65+
return secretPaths;
6066
}, auth);
6167

6268
if (options.format === 'json') {
6369
process.stdout.write(
6470
binUtils.outputFormatter({
6571
type: 'json',
66-
data: data,
72+
data: secretPaths,
6773
}),
6874
);
6975
} else {
7076
process.stdout.write(
7177
binUtils.outputFormatter({
7278
type: 'list',
73-
data: data.map(
74-
(secretsListMessage) => secretsListMessage.secretName,
75-
),
79+
data: secretPaths,
7680
}),
7781
);
7882
}

src/secrets/CommandSecrets.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import CommandList from './CommandList';
88
import CommandMkdir from './CommandMkdir';
99
import CommandRename from './CommandRename';
1010
import CommandUpdate from './CommandUpdate';
11-
import commandStat from './CommandStat';
11+
import CommandStat from './CommandStat';
1212
import CommandPolykey from '../CommandPolykey';
1313

1414
class CommandSecrets extends CommandPolykey {
@@ -26,7 +26,7 @@ class CommandSecrets extends CommandPolykey {
2626
this.addCommand(new CommandMkdir(...args));
2727
this.addCommand(new CommandRename(...args));
2828
this.addCommand(new CommandUpdate(...args));
29-
this.addCommand(new commandStat(...args));
29+
this.addCommand(new CommandStat(...args));
3030
}
3131
}
3232

src/utils/parsers.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as gestaltsUtils from 'polykey/dist/gestalts/utils';
88
import * as networkUtils from 'polykey/dist/network/utils';
99
import * as nodesUtils from 'polykey/dist/nodes/utils';
1010

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

@@ -65,25 +65,42 @@ function parseCoreCount(v: string): number | undefined {
6565
}
6666
}
6767

68-
function parseSecretPath(secretPath: string): [string, string, string?] {
68+
function parseSecretPathOptional(
69+
secretPath: string,
70+
): [string, string?, string?] {
6971
// E.g. If 'vault1:a/b/c', ['vault1', 'a/b/c'] is returned
70-
// If 'vault1:a/b/c=VARIABLE', ['vault1, 'a/b/c', 'VARIABLE'] is returned
72+
// If 'vault1', ['vault1, undefined] is returned
73+
// splits out everything after an `=` separator
7174
const lastEqualIndex = secretPath.lastIndexOf('=');
7275
const splitSecretPath =
7376
lastEqualIndex === -1
7477
? secretPath
7578
: secretPath.substring(0, lastEqualIndex);
7679
const value =
77-
lastEqualIndex === -1 ? '' : secretPath.substring(lastEqualIndex + 1);
80+
lastEqualIndex === -1
81+
? undefined
82+
: secretPath.substring(lastEqualIndex + 1);
7883
if (!secretPathRegex.test(splitSecretPath)) {
7984
throw new commander.InvalidArgumentError(
80-
`${splitSecretPath} is not of the format <vaultName>:<directoryPath>`,
85+
`${secretPath} is not of the format <vaultName>[:<directoryPath>][=<value>]`,
8186
);
8287
}
8388
const [, vaultName, directoryPath] = splitSecretPath.match(secretPathRegex)!;
8489
return [vaultName, directoryPath, value];
8590
}
8691

92+
function parseSecretPath(secretPath: string): [string, string, string?] {
93+
// E.g. If 'vault1:a/b/c', ['vault1', 'a/b/c'] is returned
94+
// If 'vault1', an error is thrown
95+
const [vaultName, secretName, value] = parseSecretPathOptional(secretPath);
96+
if (secretName === undefined) {
97+
throw new commander.InvalidArgumentError(
98+
`${secretPath} is not of the format <vaultName>:<directoryPath>[=<value>]`,
99+
);
100+
}
101+
return [vaultName, secretName, value];
102+
}
103+
87104
function parseSecretPathValue(secretPath: string): [string, string, string?] {
88105
const [vaultName, directoryPath, value] = parseSecretPath(secretPath);
89106
if (value != null && !secretPathValueRegex.test(value)) {
@@ -204,6 +221,7 @@ export {
204221
validateParserToArgParser,
205222
validateParserToArgListParser,
206223
parseCoreCount,
224+
parseSecretPathOptional,
207225
parseSecretPath,
208226
parseSecretPathValue,
209227
parseSecretPathEnv,

src/utils/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ function outputFormatterList(items: Array<string>): string {
254254
* If it is `Record<string, number>`, the `number` values will be used as the initial padding lengths.
255255
* The object is also mutated if any cells exceed the initial padding lengths.
256256
* This parameter can also be supplied to filter the columns that will be displayed.
257-
* @param options.includeHeaders - Defaults to `True`
257+
* @param options.includeHeaders - Defaults to `True`.
258258
* @param options.includeRowCount - Defaults to `False`.
259259
* @returns
260260
*/

tests/secrets/env.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -764,7 +764,7 @@ describe('commandEnv', () => {
764764
const jsonOut = JSON.parse(result.stdout);
765765
expect(jsonOut['SECRET']).toBe('this is a secret\nit has multiple lines\n');
766766
});
767-
test.only.prop([
767+
test.prop([
768768
testUtils.secretPathEnvArrayArb,
769769
fc.string().noShrink(),
770770
testUtils.cmdArgsArrayArb,
@@ -773,7 +773,6 @@ describe('commandEnv', () => {
773773
async (secretPathEnvArray, cmd, cmdArgsArray) => {
774774
// If we don't use the optional `--` delimiter then we can't include `:` in vault names
775775
fc.pre(!cmd.includes(':'));
776-
777776
let output:
778777
| [Array<[string, string, string?]>, Array<string>]
779778
| undefined = undefined;

tests/secrets/list.test.ts

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,27 +41,123 @@ describe('commandListSecrets', () => {
4141
});
4242
});
4343

44+
test(
45+
'should fail when vault does not exist',
46+
async () => {
47+
command = ['secrets', 'ls', '-np', dataDir, 'DoesntExist'];
48+
const result = await testUtils.pkStdio([...command], {
49+
env: {
50+
PK_PASSWORD: password,
51+
},
52+
cwd: dataDir,
53+
});
54+
expect(result.exitCode).toBe(64); // Sysexits.USAGE
55+
},
56+
globalThis.defaultTimeout * 2,
57+
);
58+
4459
test(
4560
'should list secrets',
4661
async () => {
4762
const vaultName = 'Vault4' as VaultName;
4863
const vaultId = await polykeyAgent.vaultManager.createVault(vaultName);
4964

5065
await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
51-
await vaultOps.addSecret(vault, 'MySecret1', 'this is the secret 1');
52-
await vaultOps.addSecret(vault, 'MySecret2', 'this is the secret 2');
53-
await vaultOps.addSecret(vault, 'MySecret3', 'this is the secret 3');
66+
await vaultOps.addSecret(vault, 'MySecret1', '');
67+
await vaultOps.addSecret(vault, 'MySecret2', '');
68+
await vaultOps.addSecret(vault, 'MySecret3', '');
5469
});
5570

56-
command = ['secrets', 'list', '-np', dataDir, vaultName];
57-
71+
command = ['secrets', 'ls', '-np', dataDir, vaultName];
5872
const result = await testUtils.pkStdio([...command], {
5973
env: {
6074
PK_PASSWORD: password,
6175
},
6276
cwd: dataDir,
6377
});
6478
expect(result.exitCode).toBe(0);
79+
expect(result.stdout).toBe('MySecret1\nMySecret2\nMySecret3\n');
80+
},
81+
globalThis.defaultTimeout * 2,
82+
);
83+
84+
test(
85+
'should fail when path is not a directory',
86+
async () => {
87+
const vaultName = 'Vault5' as VaultName;
88+
const vaultId = await polykeyAgent.vaultManager.createVault(vaultName);
89+
90+
await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
91+
await vaultOps.mkdir(vault, 'SecretDir');
92+
await vaultOps.addSecret(vault, 'SecretDir/MySecret1', '');
93+
await vaultOps.addSecret(vault, 'SecretDir/MySecret2', '');
94+
await vaultOps.addSecret(vault, 'SecretDir/MySecret3', '');
95+
});
96+
97+
command = ['secrets', 'ls', '-np', dataDir, `${vaultName}:WrongDirName`];
98+
let result = await testUtils.pkStdio([...command], {
99+
env: {
100+
PK_PASSWORD: password,
101+
},
102+
cwd: dataDir,
103+
});
104+
expect(result.exitCode).toBe(64);
105+
106+
command = [
107+
'secrets',
108+
'ls',
109+
'-np',
110+
dataDir,
111+
`${vaultName}:SecretDir/MySecret1`,
112+
];
113+
result = await testUtils.pkStdio([...command], {
114+
env: {
115+
PK_PASSWORD: password,
116+
},
117+
cwd: dataDir,
118+
});
119+
expect(result.exitCode).toBe(64);
120+
},
121+
globalThis.defaultTimeout * 2,
122+
);
123+
124+
test(
125+
'should list secrets within directories',
126+
async () => {
127+
const vaultName = 'Vault6' as VaultName;
128+
const vaultId = await polykeyAgent.vaultManager.createVault(vaultName);
129+
130+
await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
131+
await vaultOps.mkdir(vault, 'SecretDir/NestedDir', { recursive: true });
132+
await vaultOps.addSecret(vault, 'SecretDir/MySecret1', '');
133+
await vaultOps.addSecret(vault, 'SecretDir/NestedDir/MySecret2', '');
134+
});
135+
136+
command = ['secrets', 'ls', '-np', dataDir, `${vaultName}:SecretDir`];
137+
let result = await testUtils.pkStdio([...command], {
138+
env: {
139+
PK_PASSWORD: password,
140+
},
141+
cwd: dataDir,
142+
});
143+
expect(result.exitCode).toBe(0);
144+
expect(result.stdout).toBe('SecretDir/MySecret1\nSecretDir/NestedDir\n');
145+
146+
command = [
147+
'secrets',
148+
'ls',
149+
'-np',
150+
dataDir,
151+
`${vaultName}:SecretDir/NestedDir`,
152+
];
153+
result = await testUtils.pkStdio([...command], {
154+
env: {
155+
PK_PASSWORD: password,
156+
},
157+
cwd: dataDir,
158+
});
159+
expect(result.exitCode).toBe(0);
160+
expect(result.stdout).toBe('SecretDir/NestedDir/MySecret2\n');
65161
},
66162
globalThis.defaultTimeout * 2,
67163
);

tests/utils/utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ async function nodesConnect(localNode: PolykeyAgent, remoteNode: PolykeyAgent) {
8888
);
8989
}
9090

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

0 commit comments

Comments
 (0)