Skip to content

Commit bc8fcf1

Browse files
aryanjassaltegefaulkes
authored andcommitted
feat: adding unix-like ls for secrets
wip: working on adding unix-like ls support feat: added ls for listing vaults with long and short formatting [ci skip] feat: added option to show hidden files and removed redundant metadata [ci skip] feat: added directory traversal, showing hidden files by default [ci skip] chore: simplified pattern formatting code [ci skip] chore: updated long list formatting and added header option [ci skip] chore: updated long list formatting and header formatting [ci skip] chore: updated date formatter to handle old files chore: removed redundant directory rpc call and removed leading slashes from file paths [ci skip] chore: added error handling for invalid patterns [ci skip] chore: updated long list formatting using table formatter [ci skip] chore: added new parser for parsing secret filepaths [ci skip] chore: simplifying functionality for merging [ci skip] chore: cleaning up code [ci skip] chore: fixed tests chore: consolidating redundant maps fix: lint chore: updated tests and added edge cases fix: lint [ci skip] chore: updated parsers [ci skip]
1 parent 6ceb9f5 commit bc8fcf1

6 files changed

Lines changed: 170 additions & 43 deletions

File tree

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.parseSecretName,
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: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ 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 vaultNameRegex = /^[\w.-]+$/;
12+
const secretPathNameRegex = /^([\w-]+)(?::([^\0\\=]+))?$/;
1213
const secretPathValueRegex = /^([a-zA-Z_][\w]+)?$/;
1314
const environmentVariableRegex = /^([a-zA-Z_]+[a-zA-Z0-9_]*)?$/;
1415

@@ -65,27 +66,47 @@ function parseCoreCount(v: string): number | undefined {
6566
}
6667
}
6768

69+
function parseVaultName(vaultName: string): string {
70+
// E.g. If 'vault1, 'vault1' is returned
71+
// If 'vault1:a/b/c', an error is thrown
72+
if (!vaultNameRegex.test(vaultName)) {
73+
throw new commander.InvalidArgumentError(
74+
`${vaultName} is not of the format <vaultName>`,
75+
);
76+
}
77+
// Returns match[1], or the parsed vaultName
78+
return vaultName.match(secretPathNameRegex)![1];
79+
}
80+
81+
function parseSecretName(secretPath: string): [string, string?] {
82+
// E.g. If 'vault1:a/b/c', ['vault1', 'a/b/c'] is returned
83+
// If 'vault1', ['vault1, undefined] is returned
84+
if (!secretPathNameRegex.test(secretPath)) {
85+
throw new commander.InvalidArgumentError(
86+
`${secretPath} is not of the format <vaultName>[:<directoryPath>]`,
87+
);
88+
}
89+
// Returns [vaultName, secretName?]
90+
const match = secretPath.match(secretPathNameRegex)!;
91+
return [match[1], match[2] || undefined];
92+
}
93+
6894
function parseSecretPath(secretPath: string): [string, string, string?] {
6995
// 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
71-
const lastEqualIndex = secretPath.lastIndexOf('=');
72-
const splitSecretPath =
73-
lastEqualIndex === -1
74-
? secretPath
75-
: secretPath.substring(0, lastEqualIndex);
76-
const value =
77-
lastEqualIndex === -1 ? '' : secretPath.substring(lastEqualIndex + 1);
78-
if (!secretPathRegex.test(splitSecretPath)) {
96+
// If 'vault1', an error is thrown
97+
const [vaultName, secretName] = parseSecretName(secretPath);
98+
if (secretName === undefined) {
7999
throw new commander.InvalidArgumentError(
80-
`${splitSecretPath} is not of the format <vaultName>:<directoryPath>`,
100+
`${secretPath} is not of the format <vaultName>:<directoryPath>`,
81101
);
82102
}
83-
const [, vaultName, directoryPath] = splitSecretPath.match(secretPathRegex)!;
84-
return [vaultName, directoryPath, value];
103+
return [vaultName, secretName];
85104
}
86105

87106
function parseSecretPathValue(secretPath: string): [string, string, string?] {
88-
const [vaultName, directoryPath, value] = parseSecretPath(secretPath);
107+
const [vaultName, directoryPath] = parseSecretPath(secretPath);
108+
const lastEqualIndex = secretPath.lastIndexOf('=');
109+
const value = lastEqualIndex === -1 ? '' : secretPath.substring(lastEqualIndex + 1);
89110
if (value != null && !secretPathValueRegex.test(value)) {
90111
throw new commander.InvalidArgumentError(
91112
`${value} is not a valid value name`,
@@ -198,12 +219,13 @@ function parseEnvArgs(
198219
}
199220

200221
export {
201-
secretPathRegex,
202222
secretPathValueRegex,
203223
environmentVariableRegex,
204224
validateParserToArgParser,
205225
validateParserToArgListParser,
206226
parseCoreCount,
227+
parseVaultName,
228+
parseSecretName,
207229
parseSecretPath,
208230
parseSecretPathValue,
209231
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
*/

src/vaults/CommandCreate.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@ import CommandPolykey from '../CommandPolykey';
44
import * as binUtils from '../utils';
55
import * as binOptions from '../utils/options';
66
import * as binProcessors from '../utils/processors';
7+
import * as binParsers from '../utils/parsers';
78

89
class CommandCreate extends CommandPolykey {
910
constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
1011
super(...args);
1112
this.name('create');
1213
this.aliases(['touch']);
1314
this.description('Create a new Vault');
14-
this.argument('<vaultName>', 'Name of the new vault to be created');
15+
this.argument(
16+
'<vaultName>',
17+
'Name of the new vault to be created',
18+
binParsers.parseVaultName,
19+
);
1520
this.addOption(binOptions.nodeId);
1621
this.addOption(binOptions.clientHost);
1722
this.addOption(binOptions.clientPort);

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
);

0 commit comments

Comments
 (0)