-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #366 from MatrixAI/feature-unix-touch
Added `secrets touch` command
- Loading branch information
Showing
7 changed files
with
293 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
sha256-m1ZpBUrLygWELYlgShs1oWs+o3qf3e6G4rhd5ZzsOhE= | ||
sha256-yH5mPEv7drHBiLeI+KzMWv4ZQqqqJyiEzDdl6ntMRvQ= |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); | ||
}); | ||
}); |