Skip to content

Commit

Permalink
Secret environment command to inject or source variables into subshells
Browse files Browse the repository at this point in the history
  • Loading branch information
Scott committed Dec 2, 2021
1 parent 9200b11 commit 5695be2
Show file tree
Hide file tree
Showing 18 changed files with 1,077 additions and 198 deletions.
3 changes: 1 addition & 2 deletions package-lock.json

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

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"jose": "^4.3.6",
"lexicographic-integer": "^1.1.0",
"multiformats": "^9.4.8",
"nexpect": "^0.6.0",
"node-forge": "^0.10.0",
"pako": "^1.0.11",
"prompts": "^2.4.1",
Expand All @@ -106,6 +107,7 @@
"@types/cross-spawn": "^6.0.2",
"@types/google-protobuf": "^3.7.4",
"@types/jest": "^26.0.20",
"@types/level": "^6.0.0",
"@types/nexpect": "^0.4.31",
"@types/node": "^14.14.35",
"@types/node-forge": "^0.9.7",
Expand Down
147 changes: 147 additions & 0 deletions src/bin/secrets/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import path from 'path';
import Logger, { LogLevel, StreamHandler } from '@matrixai/logger';
import { utils as clientUtils } from '../../client';
import * as vaultsPB from '../../proto/js/polykey/v1/vaults/vaults_pb';
import * as secretsPB from '../../proto/js/polykey/v1/secrets/secrets_pb';
import PolykeyClient from '../../PolykeyClient';
import * as utils from '../../utils';
import * as binUtils from '../utils';
import * as CLIErrors from '../errors';
import * as grpcErrors from '../../grpc/errors';

const env = binUtils.createCommand('env', {
description: 'Runs a modified environment with injected secrets',
nodePath: true,
verbose: true,
format: true,
});
// env.option(
// '--command <command>',
// 'In the environment of the derivation, run the shell command cmd in an interactive shell (Use --run to use a non-interactive shell instead)',
// );
// env.option(
// '--run <run>',
// 'In the environment of the derivation, run the shell command cmd in a non-interactive shell, meaning (among other things) that if you hit Ctrl-C while the command is running, the shell exits (Use --command to use an interactive shell instead)',
// );
env.option(
'-e, --export',
'Export all variables',
);
env.arguments(
"Secrets to inject into env, of the format '<vaultName>:<secretPath>[=<variableName>]', you can also control what the environment variable will be called using '[<variableName>]' (defaults to upper, snake case of the original secret name)",
);
env.action(async (options, command) => {
const clientConfig = {};
clientConfig['logger'] = new Logger('CLI Logger', LogLevel.WARN, [
new StreamHandler(),
]);
if (options.verbose) {
clientConfig['logger'].setLevel(LogLevel.DEBUG);
}
clientConfig['nodePath'] = options.nodePath
? options.nodePath
: utils.getDefaultNodePath();

let shellCommand;
const client = await PolykeyClient.createPolykeyClient(clientConfig);
const directoryMessage = new secretsPB.Directory();
const vaultMessage = new vaultsPB.Vault();
directoryMessage.setVault(vaultMessage);
const secretPathList: string[] = Array.from<string>(command.args.values());
if(!binUtils.pathRegex.test(secretPathList[secretPathList.length - 1])) {
shellCommand = secretPathList.pop();
}
const data: string[] = [];

try {
const secretPathList: string[] = Array.from<string>(command.args.values());

if(!binUtils.pathRegex.test(secretPathList[secretPathList.length - 1])) {
shellCommand = secretPathList.pop();
}

if (secretPathList.length < 1) {
throw new CLIErrors.ErrorSecretsUndefined();
}

const secretEnv = { ...process.env };

await client.start({});
const grpcClient = client.grpcClient;

let output = '';

for (const secretPath of secretPathList) {
if (secretPath.includes(':')) {
if (options.export) {
output = 'export ';
}

const [, vaultName, secretExpression, variableName] = secretPath.match(
binUtils.pathRegex,
)!;

vaultMessage.setNameOrId(vaultName);
directoryMessage.setSecretDirectory(secretExpression)

const secretGenerator = grpcClient.vaultsSecretsEnv(directoryMessage);
const { p, resolveP } = utils.promise();
secretGenerator.stream.on('metadata', async (meta) => {
await clientUtils.refreshSession(meta, client.session);
resolveP(null);
});


for await (const secret of secretGenerator) {
const secretName = secret.getSecretName();
const secretContent = Buffer.from(secret.getSecretContent());
const secretVariableName = variableName !== '' ? variableName: path.basename(secretName.toUpperCase().replace('-', '_'));
secretEnv[secretVariableName] = secretContent.toString();
data.push(output + `${secretVariableName}=${secretContent.toString()}`);
}
output = '';
} else if (secretPath === '-e' || secretPath === '--export') {
output = 'export ';
} else {
throw new CLIErrors.ErrorSecretPathFormat();
}
}

if(shellCommand) {
binUtils.spawnShell(shellCommand, secretEnv, options.format);
} else {
process.stdout.write(
binUtils.outputFormatter({
type: options.format === 'json' ? 'json' : 'list',
data: data,
}),
);
}

} catch (err) {
if (err instanceof grpcErrors.ErrorGRPCClientTimeout) {
process.stderr.write(`${err.message}\n`);
}
if (err instanceof grpcErrors.ErrorGRPCServerNotStarted) {
process.stderr.write(`${err.message}\n`);
} else {
process.stderr.write(
binUtils.outputFormatter({
type: 'error',
description: err.description,
message: err.message,
}),
);
throw err;
}
} finally {
await client.stop();
options.nodePath = undefined;
options.verbose = undefined;
options.format = undefined;
options.command = undefined;
options.run = undefined;
}
});

export default env;
94 changes: 94 additions & 0 deletions src/bin/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import os from 'os';
import process from 'process';
import { LogLevel } from '@matrixai/logger';
import prompts from 'prompts';
import { spawn } from 'cross-spawn';
import commander from 'commander';
import Logger, { LogLevel } from '@matrixai/logger';

import * as grpc from '@grpc/grpc-js';
import * as clientUtils from '../client/utils';
import * as clientErrors from '../client/errors';
Expand Down Expand Up @@ -153,11 +157,101 @@ async function retryAuth<T>(
}
}
}
// Async function requestPassword(keyManager: KeyManager, attempts: number = 3) {
// let i = 0;
// let correct = false;
// while (i < attempts) {
// const response = await prompts({
// type: 'text',
// name: 'password',
// message: 'Please enter your password',
// });
// try {
// clientUtils.checkPassword(response.password, keyManager);
// correct = true;
// } catch (err) {
// if (err instanceof clientErrors.ErrorPassword) {
// if (attempts == 2) {
// throw new clientErrors.ErrorPassword();
// }
// i++;
// }
// }
// if (correct) {
// break;
// }
// }
// return;
// }
function spawnShell(command: string, environmentVariables: POJO, format: string): void {
// This code is what this function should look like after the kexec package is added
// try {
// kexec(command, {
// stdio: 'inherit',
// env: environmentVariables,
// shell: true,
// });
// } catch (err) {
// if (
// err.code !== "MODULE_NOT_FOUND" &&
// err.code !== "UNDECLARED_DEPENDENCY"
// ) {
// throw err;
// }

// const shell = spawn(command, {
// stdio: 'inherit',
// env: environmentVariables,
// shell: true,
// });
// shell.on("exit", (code: number, signal: NodeJS.Signals) => {
// process.on("exit", () => {
// if (signal) {
// process.kill(process.pid, signal);
// } else {
// process.exitCode = code;
// }
// });
// });
// process.on("SIGINT", () => {
// shell.kill("SIGINT")
// });
// shell.on('close', (code) => {
// if (code != 0) {
// process.stdout.write(
// outputFormatter({
// type: format === 'json' ? 'json' : 'list',
// data: [`Terminated with ${code}`],
// }),
// );
// }
// });
// }
const shell = spawn(command, {
stdio: 'inherit',
env: environmentVariables,
shell: true,
});

shell.on('close', (code) => {
if (code != 0) {
process.stdout.write(
outputFormatter({
type: format === 'json' ? 'json' : 'list',
data: [`Terminated with ${code}`],
}),
);
}
});
}

export {
getDefaultNodePath,
verboseToLogLevel,
createCommand,
promisifyGrpc,
outputFormatter,
spawnShell,
OutputObject,
outputFormatter,
requestPassword,
Expand Down
10 changes: 9 additions & 1 deletion src/client/GRPCClientClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,15 @@ class GRPCClientClient extends GRPCClient<ClientServiceClient> {
)(...args);
}

@ready(new clientErrors.ErrorClientClientDestroyed())
@ready(new grpcErrors.ErrorGRPCClientNotStarted())
public vaultsSecretsEnv(...args) {
return grpcUtils.promisifyReadableStreamCall<secretsPB.Secret>(
this.client,
this.client.vaultsSecretsEnv,
)(...args);
}

@ready(new grpcErrors.ErrorGRPCClientNotStarted())
public keysKeyPairRoot(...args) {
return grpcUtils.promisifyUnaryCall<keysPB.KeyPair>(
this.client,
Expand Down
67 changes: 67 additions & 0 deletions src/client/rpcVaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,73 @@ const createVaultRPC = ({
await genWritable.throw(err);
}
},
secretsEnv: async (
call: grpc.ServerWritableStream<
secretsPB.Directory,
secretsPB.Directory
>,
): Promise<void> => {
const genWritable = grpcUtils.generatorWritable(call);
try {
await sessionManager.verifyToken(utils.getToken(call.metadata));
const responseMeta = utils.createMetaTokenResponse(
await sessionManager.generateToken(),
);
call.sendMetadata(responseMeta);
//Getting the vault.
const directoryMessage = call.request;
const vaultMessage = directoryMessage.getVault();
if (vaultMessage == null) {
await genWritable.throw({ code: grpc.status.NOT_FOUND });
return;
}
const vaultId = await utils.parseVaultInput(vaultMessage, vaultManager);
const pattern = directoryMessage.getSecretDirectory();
const res = await vaultManager.glob(vaultId, pattern);
const dirMessage = new secretsPB.Directory();
for (const file in res) {
dirMessage.setSecretDirectory(file);
await genWritable.next(dirMessage);
}
await genWritable.next(null);
} catch (err) {
await genWritable.throw(err);
}
},
vaultsSecretsEnv: async (
call: grpc.ServerWritableStream<secretsPB.Directory, secretsPB.Secret>,
): Promise<void> => {
const genWritable = grpcUtils.generatorWritable(call);

try {
await sessionManager.verifyToken(utils.getToken(call.metadata));
const responseMeta = utils.createMetaTokenResponse(
await sessionManager.generateToken(),
);
call.sendMetadata(responseMeta);
const directoryMessage = call.request;
const vaultMessage = directoryMessage.getVault();
if (vaultMessage == null) {
await genWritable.throw({ code: grpc.status.NOT_FOUND });
return;
}
const pattern = directoryMessage.getSecretDirectory();
const id = await utils.parseVaultInput(vaultMessage, vaultManager);
const vault = await vaultManager.openVault(id);
const secretList = await vaultManager.glob(id, pattern);
let secretMessage: secretsPB.Secret;
for (const secretName of secretList) {
const secretContent = await vaultOps.getSecret(vault, secretName);
secretMessage = new secretsPB.Secret();
secretMessage.setSecretName(secretName);
secretMessage.setSecretContent(secretContent);
await genWritable.next(secretMessage);
}
await genWritable.next(null);
} catch (err) {
await genWritable.throw(err);
}
},
};
};

Expand Down
Loading

0 comments on commit 5695be2

Please sign in to comment.