Skip to content

Commit fc7bb0b

Browse files
committed
feat: changed parsers to allow using vault name without secret path
chore: vault commands are now using vaultNameParser chore: writing fastcheck tests for parsers chore: jestified outputFormatter tests [ci skip]
1 parent 3377c18 commit fc7bb0b

15 files changed

+399
-306
lines changed

src/secrets/CommandCat.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class CommandGet extends CommandPolykey {
8585
for (const [vaultName, secretPath] of secretPaths) {
8686
await writer.write({
8787
nameOrId: vaultName,
88-
secretName: secretPath,
88+
secretName: secretPath ?? '/',
8989
metadata: first
9090
? { ...auth, options: { continueOnError: true } }
9191
: undefined,
@@ -98,6 +98,7 @@ class CommandGet extends CommandPolykey {
9898
if (chunk.error) process.stderr.write(chunk.error);
9999
else process.stdout.write(chunk.secretContent);
100100
}
101+
process.stderr.write("\n");
101102
}, meta);
102103
} finally {
103104
if (pkClient! != null) await pkClient.stop();

src/secrets/CommandEdit.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class CommandEdit extends CommandPolykey {
1717
this.argument(
1818
'<secretPath>',
1919
'Path to the secret to be edited, specified as <vaultName>:<directoryPath>',
20-
binParsers.parseSecretPathValue,
20+
binParsers.parseSecretPath,
2121
);
2222
this.addOption(binOptions.nodeId);
2323
this.addOption(binOptions.clientHost);
@@ -68,7 +68,7 @@ class CommandEdit extends CommandPolykey {
6868
const writer = res.writable.getWriter();
6969
await writer.write({
7070
nameOrId: secretPath[0],
71-
secretName: secretPath[1],
71+
secretName: secretPath[1] ?? '/',
7272
metadata: auth,
7373
});
7474
await writer.close();

src/secrets/CommandList.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class CommandList extends CommandPolykey {
1414
this.argument(
1515
'<directoryPath>',
1616
'Directory to list files from, specified as <vaultName>[:<path>]',
17-
binParsers.parseSecretPathOptional,
17+
binParsers.parseSecretPath,
1818
);
1919
this.addOption(binOptions.nodeId);
2020
this.addOption(binOptions.clientHost);

src/secrets/CommandMkdir.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class CommandMkdir extends CommandPolykey {
2424
this.addOption(binOptions.nodeId);
2525
this.addOption(binOptions.clientHost);
2626
this.addOption(binOptions.clientPort);
27-
this.addOption(binOptions.recursive);
27+
this.addOption(binOptions.parents);
2828
this.action(async (secretPaths, options) => {
2929
secretPaths = secretPaths.map((path: string) =>
3030
binParsers.parseSecretPath(path),
@@ -67,9 +67,9 @@ class CommandMkdir extends CommandPolykey {
6767
for (const [vault, path] of secretPaths) {
6868
await writer.write({
6969
nameOrId: vault,
70-
dirName: path,
70+
dirName: path ?? '/',
7171
metadata: first
72-
? { ...auth, options: { recursive: options.recursive } }
72+
? { ...auth, options: { recursive: options.parents } }
7373
: undefined,
7474
});
7575
first = false;

src/secrets/CommandRemove.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class CommandRemove extends CommandPolykey {
2121
this.addOption(binOptions.recursive);
2222
this.action(async (secretPaths, options) => {
2323
secretPaths = secretPaths.map((path: string) =>
24-
binParsers.parseSecretPathValue(path),
24+
binParsers.parseSecretPath(path),
2525
);
2626
const { default: PolykeyClient } = await import(
2727
'polykey/dist/PolykeyClient'

src/secrets/CommandStat.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class CommandStat extends CommandPolykey {
1313
this.argument(
1414
'<secretPath>',
1515
'Path to where the secret, specified as <vaultName>:<directoryPath>',
16-
binParsers.parseSecretPathValue,
16+
binParsers.parseSecretPath,
1717
);
1818
this.addOption(binOptions.nodeId);
1919
this.addOption(binOptions.clientHost);

src/secrets/CommandWrite.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class CommandWrite extends CommandPolykey {
1313
this.argument(
1414
'<secretPath>',
1515
'Path to the secret, specified as <vaultName>:<directoryPath>',
16-
binParsers.parseSecretPathValue,
16+
binParsers.parseSecretPath,
1717
);
1818
this.addOption(binOptions.nodeId);
1919
this.addOption(binOptions.clientHost);
@@ -77,7 +77,7 @@ class CommandWrite extends CommandPolykey {
7777
await pkClient.rpcClient.methods.vaultsSecretsWriteFile({
7878
metadata: auth,
7979
nameOrId: secretPath[0],
80-
secretName: secretPath[1],
80+
secretName: secretPath[1] ?? '/',
8181
secretContent: stdin,
8282
}),
8383
meta,

src/utils/options.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,12 @@ const order = new commander.Option(
310310

311311
const recursive = new commander.Option(
312312
'--recursive',
313-
'If enabled, specified directories will be removed along with their contents',
313+
'If enabled, specified operation will be applied recursively to the directory and its contents',
314+
).default(false);
315+
316+
const parents = new commander.Option(
317+
'--parents',
318+
'If enabled, create all parent directories as well. If the directories exist, do nothing.',
314319
).default(false);
315320

316321
export {
@@ -355,4 +360,5 @@ export {
355360
limit,
356361
order,
357362
recursive,
363+
parents,
358364
};

src/utils/parsers.ts

Lines changed: 28 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -67,32 +67,6 @@ function parseCoreCount(v: string): number | undefined {
6767
}
6868
}
6969

70-
function parseSecretPathOptional(
71-
secretPath: string,
72-
): [string, string?, string?] {
73-
// E.g. If 'vault1:a/b/c', ['vault1', 'a/b/c'] is returned
74-
// If 'vault1', ['vault1, undefined] is returned
75-
// splits out everything after an `=` separator
76-
const lastEqualIndex = secretPath.lastIndexOf('=');
77-
const splitSecretPath =
78-
lastEqualIndex === -1
79-
? secretPath
80-
: secretPath.substring(0, lastEqualIndex);
81-
const value =
82-
lastEqualIndex === -1
83-
? undefined
84-
: secretPath.substring(lastEqualIndex + 1);
85-
if (!vaultNameSecretPathRegex.test(splitSecretPath)) {
86-
throw new commander.InvalidArgumentError(
87-
`${secretPath} is not of the format <vaultName>[:<directoryPath>][=<value>]`,
88-
);
89-
}
90-
const [, vaultName, directoryPath] = splitSecretPath.match(
91-
vaultNameSecretPathRegex,
92-
)!;
93-
return [vaultName, directoryPath, value];
94-
}
95-
9670
function parseVaultName(vaultName: string): string {
9771
if (!vaultNameRegex.test(vaultName)) {
9872
throw new commander.InvalidArgumentError(
@@ -102,29 +76,12 @@ function parseVaultName(vaultName: string): string {
10276
return vaultName;
10377
}
10478

105-
function parseSecretPath(secretPath: string): [string, string, string?] {
106-
// E.g. If 'vault1:a/b/c', ['vault1', 'a/b/c'] is returned
107-
// If 'vault1', an error is thrown
108-
const [vaultName, secretName, value] = parseSecretPathOptional(secretPath);
109-
if (secretName === undefined) {
110-
throw new commander.InvalidArgumentError(
111-
`${secretPath} is not of the format <vaultName>:<directoryPath>[=<value>]`,
112-
);
113-
}
114-
return [vaultName, secretName, value];
115-
}
116-
117-
function parseSecretPathValue(secretPath: string): [string, string, string?] {
118-
const [vaultName, directoryPath, value] = parseSecretPath(secretPath);
119-
if (value != null && !secretPathValueRegex.test(value)) {
120-
throw new commander.InvalidArgumentError(
121-
`${value} is not a valid value name`,
122-
);
123-
}
124-
return [vaultName, directoryPath, value];
125-
}
126-
127-
function parseSecretPathEnv(secretPath: string): [string, string?, string?] {
79+
// E.g. If 'vault1:a/b/c', ['vault1', 'a/b/c'] is returned
80+
// If 'vault1', ['vault1, undefined] is returned
81+
// If 'vault1:', an error is thrown
82+
// If 'a/b/c', an error is thrown
83+
// Splits out everything after an `=` separator
84+
function parseSecretPath(secretPath: string): [string, string?, string?] {
12885
// The colon character `:` is prohibited in vaultName, so it's first occurence
12986
// means that this is the delimiter between vaultName and secretPath.
13087
const colonIndex = secretPath.indexOf(':');
@@ -141,20 +98,35 @@ function parseSecretPathEnv(secretPath: string): [string, string?, string?] {
14198
equalIndex === -1
14299
? secretPathPart
143100
: secretPathPart.substring(0, equalIndex);
144-
const valueData =
101+
const value =
145102
equalIndex === -1 ? undefined : secretPathPart.substring(equalIndex + 1);
146103
if (splitSecretPath != null && !secretPathRegex.test(splitSecretPath)) {
147104
throw new commander.InvalidArgumentError(
148105
`${secretPath} is not of the format <vaultName>[:<secretPath>][=<value>]`,
149106
);
150107
}
151108
const parsedVaultName = parseVaultName(vaultNamePart);
152-
const parsedSecretPath = splitSecretPath.match(secretPathRegex)?.[0] ?? '/';
153-
const [vaultName, directoryPath, value] = [
154-
parsedVaultName,
155-
parsedSecretPath,
156-
valueData,
157-
];
109+
const parsedSecretPath = splitSecretPath.match(secretPathRegex)?.[0];
110+
return [parsedVaultName, parsedSecretPath, value];
111+
}
112+
113+
function parseSecretPathValue(secretPath: string): [string, string, string?] {
114+
const [vaultName, directoryPath, value] = parseSecretPath(secretPath);
115+
if (value != null && !secretPathValueRegex.test(value)) {
116+
throw new commander.InvalidArgumentError(
117+
`${value} is not a valid value name`,
118+
);
119+
}
120+
if (directoryPath == null) {
121+
throw new commander.InvalidArgumentError(
122+
`${secretPath} is not of the format <vaultName>:<directoryPath>[=<value>]`,
123+
);
124+
}
125+
return [vaultName, directoryPath, value];
126+
}
127+
128+
function parseSecretPathEnv(secretPath: string): [string, string?, string?] {
129+
const [vaultName, directoryPath, value] = parseSecretPath(secretPath);
158130
if (value != null && !environmentVariableRegex.test(value)) {
159131
throw new commander.InvalidArgumentError(
160132
`${value} is not a valid environment variable name`,
@@ -263,7 +235,6 @@ export {
263235
validateParserToArgParser,
264236
validateParserToArgListParser,
265237
parseCoreCount,
266-
parseSecretPathOptional,
267238
parseVaultName,
268239
parseSecretPath,
269240
parseSecretPathValue,

tests/secrets/cat.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,22 @@ describe('commandCatSecret', () => {
6161
expect(result.exitCode).toBe(0);
6262
expect(result.stdout).toBe(secretContent);
6363
});
64+
test('should fail when reading root without secret path', async () => {
65+
const vaultName = 'Vault3' as VaultName;
66+
const vaultId = await polykeyAgent.vaultManager.createVault(vaultName);
67+
const secretName = 'secret-name';
68+
const secretContent = 'this is the contents of the secret';
69+
await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
70+
await vaultOps.addSecret(vault, secretName, secretContent);
71+
});
72+
const command = ['secrets', 'cat', '-np', dataDir, vaultName];
73+
const result = await testUtils.pkStdio(command, {
74+
env: { PK_PASSWORD: password },
75+
cwd: dataDir,
76+
});
77+
expect(result.exitCode).not.toBe(0);
78+
expect(result.stderr).toBeDefined();
79+
});
6480
test('should concatenate multiple secrets', async () => {
6581
const vaultName = 'Vault3' as VaultName;
6682
const vaultId = await polykeyAgent.vaultManager.createVault(vaultName);
@@ -169,7 +185,7 @@ describe('commandCatSecret', () => {
169185
resolve(exitCode);
170186
});
171187
});
172-
expect(exitCode).toStrictEqual(0);
188+
expect(exitCode).toBe(0);
173189
expect(stdout).toBe(stdinData);
174190
});
175191
});

0 commit comments

Comments
 (0)