Skip to content

Commit

Permalink
Merge pull request #366 from MatrixAI/feature-unix-touch
Browse files Browse the repository at this point in the history
Added `secrets touch` command
  • Loading branch information
aryanjassal authored Feb 4, 2025
2 parents 566468b + 945c59c commit f46aabc
Show file tree
Hide file tree
Showing 7 changed files with 293 additions and 6 deletions.
2 changes: 1 addition & 1 deletion npmDepsHash
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sha256-m1ZpBUrLygWELYlgShs1oWs+o3qf3e6G4rhd5ZzsOhE=
sha256-yH5mPEv7drHBiLeI+KzMWv4ZQqqqJyiEzDdl6ntMRvQ=
8 changes: 4 additions & 4 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@
"nexpect": "^0.6.0",
"node-gyp-build": "^4.4.0",
"nodemon": "^3.0.1",
"polykey": "^1.18.0",
"polykey": "^1.19.0",
"prettier": "^3.0.0",
"shelljs": "^0.8.5",
"shx": "^0.3.4",
Expand Down
6 changes: 6 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@ class ErrorPolykeyCLIEditSecret<T> extends ErrorPolykeyCLI<T> {
exitCode = 1;
}

class ErrorPolykeyCLITouchSecret<T> extends ErrorPolykeyCLI<T> {
static description = 'Failed to touch one or more secret';
exitCode = 1;
}

export {
ErrorPolykeyCLI,
ErrorPolykeyCLIUncaughtException,
Expand Down Expand Up @@ -218,4 +223,5 @@ export {
ErrorPolykeyCLIRemoveSecret,
ErrorPolykeyCLICatSecret,
ErrorPolykeyCLIEditSecret,
ErrorPolykeyCLITouchSecret,
};
2 changes: 2 additions & 0 deletions src/secrets/CommandSecrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import CommandMkdir from './CommandMkdir';
import CommandRename from './CommandRename';
import CommandRemove from './CommandRemove';
import CommandStat from './CommandStat';
import CommandTouch from './CommandTouch';
import CommandWrite from './CommandWrite';
import CommandPolykey from '../CommandPolykey';

Expand All @@ -26,6 +27,7 @@ class CommandSecrets extends CommandPolykey {
this.addCommand(new CommandRename(...args));
this.addCommand(new CommandRemove(...args));
this.addCommand(new CommandStat(...args));
this.addCommand(new CommandTouch(...args));
this.addCommand(new CommandWrite(...args));
}
}
Expand Down
111 changes: 111 additions & 0 deletions src/secrets/CommandTouch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type PolykeyClient from 'polykey/dist/PolykeyClient';
import CommandPolykey from '../CommandPolykey';
import * as binProcessors from '../utils/processors';
import * as binParsers from '../utils/parsers';
import * as binUtils from '../utils';
import * as binOptions from '../utils/options';
import * as errors from '../errors';

class CommandTouch extends CommandPolykey {
constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
super(...args);
this.name('touch');
this.description('Create a secret if it does not exist');
this.argument(
'<secretPaths...>',
'One or more paths, specified as <vaultName>:<secretPath>',
);
this.addOption(binOptions.nodeId);
this.addOption(binOptions.clientHost);
this.addOption(binOptions.clientPort);
this.action(async (secretPaths, options) => {
secretPaths = secretPaths.map((path: string) =>
binParsers.parseSecretPath(path),
);
const { default: PolykeyClient } = await import(
'polykey/dist/PolykeyClient'
);
const clientOptions = await binProcessors.processClientOptions(
options.nodePath,
options.nodeId,
options.clientHost,
options.clientPort,
this.fs,
this.logger.getChild(binProcessors.processClientOptions.name),
);
const meta = await binProcessors.processAuthentication(
options.passwordFile,
this.fs,
);
let pkClient: PolykeyClient;
this.exitHandlers.handlers.push(async () => {
if (pkClient != null) await pkClient.stop();
});
try {
pkClient = await PolykeyClient.createPolykeyClient({
nodeId: clientOptions.nodeId,
host: clientOptions.clientHost,
port: clientOptions.clientPort,
options: {
nodePath: options.nodePath,
},
logger: this.logger.getChild(PolykeyClient.name),
});
const hasErrored = await binUtils.retryAuthentication(async (auth) => {
const response =
await pkClient.rpcClient.methods.vaultsSecretsTouch();
// Extract all unique vault names
const uniqueVaultNames = new Set<string>();
for (const [vaultName] of secretPaths) {
uniqueVaultNames.add(vaultName);
}
const writer = response.writable.getWriter();
// Send the header message first
await writer.write({
type: 'VaultNamesHeaderMessage',
vaultNames: Array.from(uniqueVaultNames),
metadata: auth,
});
// Then send all the paths in subsequent messages
for (const [vaultName, secretPath] of secretPaths) {
await writer.write({
type: 'SecretIdentifierMessage',
nameOrId: vaultName,
secretName: secretPath,
});
}
await writer.close();
// Check if any errors were raised
let hasErrored = false;
for await (const result of response.readable) {
if (result.type === 'ErrorMessage') {
hasErrored = true;
switch (result.code) {
case 'ENOENT':
// Attempt to touch a path which doesn't exist
process.stderr.write(
`touch: cannot touch '${result.reason}': No such file or directory\n`,
);
break;
default:
// No other code should be thrown
throw result;
}
}
}
return hasErrored;
}, meta);

if (hasErrored) {
throw new errors.ErrorPolykeyCLITouchSecret(
'Failed to touch one or more secrets',
);
}
} finally {
if (pkClient! != null) await pkClient.stop();
}
});
}
}

export default CommandTouch;
168 changes: 168 additions & 0 deletions tests/secrets/touch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import path from 'path';
import fs from 'fs';
import Logger, { LogLevel, StreamHandler } from '@matrixai/logger';
import PolykeyAgent from 'polykey/dist/PolykeyAgent';
import { vaultOps } from 'polykey/dist/vaults';
import * as keysUtils from 'polykey/dist/keys/utils';
import * as testUtils from '../utils';

describe('commandTouch', () => {
const password = 'password';
const logger = new Logger('CLI Test', LogLevel.WARN, [new StreamHandler()]);
let dataDir: string;
let polykeyAgent: PolykeyAgent;

beforeEach(async () => {
dataDir = await fs.promises.mkdtemp(
path.join(globalThis.tmpDir, 'polykey-test-'),
);
polykeyAgent = await PolykeyAgent.createPolykeyAgent({
password: password,
options: {
nodePath: dataDir,
agentServiceHost: '127.0.0.1',
clientServiceHost: '127.0.0.1',
keys: {
passwordOpsLimit: keysUtils.passwordOpsLimits.min,
passwordMemLimit: keysUtils.passwordMemLimits.min,
strictMemoryLock: false,
},
},
logger: logger,
});
});
afterEach(async () => {
await polykeyAgent.stop();
await fs.promises.rm(dataDir, {
force: true,
recursive: true,
});
});

test('should create a secret if it does not exist', async () => {
const vaultName = 'vault';
const vaultId = await polykeyAgent.vaultManager.createVault(vaultName);
const secretName = 'secret';
const command = [
'secrets',
'touch',
'-np',
dataDir,
`${vaultName}:${secretName}`,
];
const result = await testUtils.pkStdio(command, {
env: { PK_PASSWORD: password },
cwd: dataDir,
});
expect(result.exitCode).toBe(0);
await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
await vault.readF(async (efs) => {
await expect(efs.exists(secretName)).resolves.toBeTruthy();
});
});
});
test('should update mtime if secret exists', async () => {
const vaultName = 'vault';
const vaultId = await polykeyAgent.vaultManager.createVault(vaultName);
const secretName = 'secret';
let oldMtime: Date | undefined = undefined;
await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
await vaultOps.writeSecret(vault, secretName, secretName);
oldMtime = await vault.readF(async (efs) => {
return (await efs.stat(secretName)).mtime;
});
});
if (oldMtime == null) fail('Mtime cannot be nullish');
const command = [
'secrets',
'touch',
'-np',
dataDir,
`${vaultName}:${secretName}`,
];
const startTime = new Date();
const result = await testUtils.pkStdio(command, {
env: { PK_PASSWORD: password },
cwd: dataDir,
});
const endTime = new Date();
expect(result.exitCode).toBe(0);
await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
await vault.readF(async (efs) => {
await expect(efs.exists(secretName)).resolves.toBeTruthy();
// File content isn't modified
const content = await efs.readFile(secretName);
expect(content.toString()).toEqual(secretName);
// Timestamp has changed
const stat = await efs.stat(secretName);
expect(
stat.mtime >= startTime &&
stat.mtime <= endTime &&
stat.mtime !== oldMtime,
).toBeTruthy();
});
});
});
test('should update mtime if directory exists', async () => {
const vaultName = 'vault';
const vaultId = await polykeyAgent.vaultManager.createVault(vaultName);
const dirName = 'dir';
let oldMtime: Date | undefined = undefined;
await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
await vaultOps.mkdir(vault, dirName);
oldMtime = await vault.readF(async (efs) => {
return (await efs.stat(dirName)).mtime;
});
});
if (oldMtime == null) fail('Mtime cannot be nullish');
const command = [
'secrets',
'touch',
'-np',
dataDir,
`${vaultName}:${dirName}`,
];
const startTime = new Date();
const result = await testUtils.pkStdio(command, {
env: { PK_PASSWORD: password },
cwd: dataDir,
});
const endTime = new Date();
expect(result.exitCode).toBe(0);
await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
await vault.readF(async (efs) => {
await expect(efs.exists(dirName)).resolves.toBeTruthy();
// Timestamp has changed
const stat = await efs.stat(dirName);
expect(
stat.mtime >= startTime &&
stat.mtime <= endTime &&
stat.mtime !== oldMtime,
).toBeTruthy();
});
});
});
test('should fail if parent directory does not exist', async () => {
const vaultName = 'vault';
const vaultId = await polykeyAgent.vaultManager.createVault(vaultName);
const secretName = path.join('dir', 'secret');
const command = [
'secrets',
'touch',
'-np',
dataDir,
`${vaultName}:${secretName}`,
];
const result = await testUtils.pkStdio(command, {
env: { PK_PASSWORD: password },
cwd: dataDir,
});
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toInclude('No such file or directory');
await polykeyAgent.vaultManager.withVaults([vaultId], async (vault) => {
await vault.readF(async (efs) => {
await expect(efs.exists(secretName)).resolves.toBeFalsy();
});
});
});
});

0 comments on commit f46aabc

Please sign in to comment.