Skip to content

Commit

Permalink
feat: adding unix-like ls for secrets
Browse files Browse the repository at this point in the history
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]
  • Loading branch information
aryanjassal committed Aug 27, 2024
1 parent 41d6dd0 commit a4ed695
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 43 deletions.
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.parseSecretName,
);
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
52 changes: 37 additions & 15 deletions src/utils/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ 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 vaultNameRegex = /^[\w.-]+$/;
const secretPathNameRegex = /^([\w-]+)(?::([^\0\\=]+))?$/;
const secretPathValueRegex = /^([a-zA-Z_][\w]+)?$/;
const environmentVariableRegex = /^([a-zA-Z_]+[a-zA-Z0-9_]*)?$/;

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

function parseVaultName(vaultName: string): string {
// E.g. If 'vault1, 'vault1' is returned
// If 'vault1:a/b/c', an error is thrown
if (!vaultNameRegex.test(vaultName)) {
throw new commander.InvalidArgumentError(
`${vaultName} is not of the format <vaultName>`,
);
}
// Returns match[1], or the parsed vaultName
return vaultName.match(secretPathNameRegex)![1];
}

function parseSecretName(secretPath: string): [string, string?] {
// E.g. If 'vault1:a/b/c', ['vault1', 'a/b/c'] is returned
// If 'vault1', ['vault1, undefined] is returned
if (!secretPathNameRegex.test(secretPath)) {
throw new commander.InvalidArgumentError(
`${secretPath} is not of the format <vaultName>[:<directoryPath>]`,
);
}
// Returns [vaultName, secretName?]
const match = secretPath.match(secretPathNameRegex)!;
return [match[1], match[2] || undefined];
}

function parseSecretPath(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
const lastEqualIndex = secretPath.lastIndexOf('=');
const splitSecretPath =
lastEqualIndex === -1
? secretPath
: secretPath.substring(0, lastEqualIndex);
const value =
lastEqualIndex === -1 ? '' : secretPath.substring(lastEqualIndex + 1);
if (!secretPathRegex.test(splitSecretPath)) {
// If 'vault1', an error is thrown
const [vaultName, secretName] = parseSecretName(secretPath);
if (secretName === undefined) {
throw new commander.InvalidArgumentError(
`${splitSecretPath} is not of the format <vaultName>:<directoryPath>`,
`${secretPath} is not of the format <vaultName>:<directoryPath>`,
);
}
const [, vaultName, directoryPath] = splitSecretPath.match(secretPathRegex)!;
return [vaultName, directoryPath, value];
return [vaultName, secretName];
}

function parseSecretPathValue(secretPath: string): [string, string, string?] {
const [vaultName, directoryPath, value] = parseSecretPath(secretPath);
const [vaultName, directoryPath] = parseSecretPath(secretPath);
const lastEqualIndex = secretPath.lastIndexOf('=');
const value = lastEqualIndex === -1 ? '' : secretPath.substring(lastEqualIndex + 1);
if (value != null && !secretPathValueRegex.test(value)) {
throw new commander.InvalidArgumentError(
`${value} is not a valid value name`,
Expand Down Expand Up @@ -198,12 +219,13 @@ function parseEnvArgs(
}

export {
secretPathRegex,
secretPathValueRegex,
environmentVariableRegex,
validateParserToArgParser,
validateParserToArgListParser,
parseCoreCount,
parseVaultName,
parseSecretName,
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
7 changes: 6 additions & 1 deletion src/vaults/CommandCreate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ 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 CommandCreate extends CommandPolykey {
constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
super(...args);
this.name('create');
this.aliases(['touch']);
this.description('Create a new Vault');
this.argument('<vaultName>', 'Name of the new vault to be created');
this.argument(
'<vaultName>',
'Name of the new vault to be created',
binParsers.parseVaultName,
);
this.addOption(binOptions.nodeId);
this.addOption(binOptions.clientHost);
this.addOption(binOptions.clientPort);
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

0 comments on commit a4ed695

Please sign in to comment.