Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 142 additions & 87 deletions src/secrets/CommandEnv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,58 @@ import * as binErrors from '../errors';
import CommandPolykey from '../CommandPolykey';
import * as binOptions from '../utils/options';

const description = `
Run a command with the given secrets and env variables. If no command is specified then the variables are printed to stdout in the format specified by env-format.

When selecting secrets with --env secrets with invalid names can be selected. By default when these are encountered then the command will throw an error. This behaviour can be modified with '--env-invalid'. the invalid name can be silently dropped with 'ignore' or logged out with 'warn'

Duplicate secret names can be specified, by default with 'overwrite' the env variable will be overwritten with the latest found secret of that name. It can be specified to 'keep' the first found secret of that name, 'error' to throw if there is a duplicate and 'warn' to log a warning while overwriting.
`;

const helpText = `
This command has two main ways of functioning. Executing a provided command or outputting formatted env variables to] stdout.

Running the command with 'polykey secrets env --env vault:secret -- some command' will do process replacement to run 'some command' while providing environment variables selected by '-e' to that process. Note that process replacement is only supported on unix systems such as linux or macos. When running on windows a child process will be used.

Running the command with 'polykey secrets env --env vault:secret --env-format <format>' will output the environment variables to stdout with the given <format>. The following formats are supported, 'platform', 'json', 'unix', 'cmd' and 'powershell'.

'platform' will automatically detect the current platform and select the appropriate format. This is 'unix' for unix based systems and 'cmd' for windows.

'json' Will format the environment variables as a json object in the form {'key': 'value'}.

'unix' Will format the environment variables as a '.env' file for use on unix systems. It will include comments before each variable showing the secret path used for that variable.

'cmd' Will format the environment variables as a '.bat' file for use on windows cmd. It will include comments before each variable showing the secret path used for that variable.

'powershell' Will format the environment variables as a '.ps1' file for use on windows Powershell. It will include comments before each variable showing the secret path used for that variable.
`;

class CommandEnv extends CommandPolykey {
constructor(...args: ConstructorParameters<typeof CommandPolykey>) {
super(...args);

// Modify the --format choices to include `dotenv` and `prepend`
// @ts-ignore: missing type for `options`
const formatOption = this.options.filter((v) => v.short === '-f')[0];
if (formatOption == null) {
utils.never('The -f --format option should exist');
}
formatOption.argChoices.push('dotenv', 'prepend');

this.name('env');
this.description(
`Run a command with the given secrets and env variables. If no command is specified then the variables are printed to stdout in the format specified by env-format.\n\nWhen selecting secrets with --env secrets with invalid names can be selected. By default when these are encountered then the command will throw an error. This behaviour can be modified with '--env-invalid'. the invalid name can be silently dropped with 'ignore' or logged out with 'warn'\n\nDuplicate secret names can be specified, by default with 'overwrite' the env variable will be overwritten with the latest found secret of that name. It can be specified to 'keep' the first found secret of that name, 'error' to throw if there is a duplicate and 'warn' to log a warning while overwriting.`,
);
this.description(description);
this.addOption(binOptions.nodeId);
this.addOption(binOptions.clientHost);
this.addOption(binOptions.clientPort);
this.addOption(binOptions.envVariables);
this.addOption(binOptions.envFormat);
this.addOption(binOptions.envInvalid);
this.addOption(binOptions.envDuplicate);
this.argument('[cmd] [argv...]', 'command and arguments');
this.addHelpText('after', helpText);
this.action(async (args: Array<string>, options) => {
const [cmd, ...argv] = args;
const {
env: envVariables,
envInvalid,
envDuplicate,
format,
envFormat,
}: {
env: Array<[string, string, string?]>;
envInvalid: 'error' | 'warn' | 'ignore';
envDuplicate: 'keep' | 'overwrite' | 'warn' | 'error';
format: 'human' | 'dotenv' | 'json' | 'prepend';
envFormat: 'platform' | 'unix' | 'cmd' | 'powershell' | 'json';
} = options;

// There are a few stages here
Expand Down Expand Up @@ -84,88 +101,102 @@ class CommandEnv extends CommandPolykey {
});

// Getting envs
const envp = await binUtils.retryAuthentication(async (auth) => {
const responseStream =
await pkClient.rpcClient.methods.vaultsSecretsEnv();
// Writing desired secrets
const secretRenameMap = new Map<string, string | undefined>();
const writeP = (async () => {
const writer = responseStream.writable.getWriter();
let first = true;
for (const envVariable of envVariables) {
const [nameOrId, secretName, secretNameNew] = envVariable;
secretRenameMap.set(secretName, secretNameNew);
await writer.write({
nameOrId,
secretName,
metadata: first ? auth : undefined,
});
first = false;
}
await writer.close();
})();

const envp: Record<string, string> = {};
for await (const value of responseStream.readable) {
const { secretName, secretContent } = value;
let newName = secretRenameMap.get(secretName);
if (newName == null) {
const secretEnvName = path.basename(secretName);
// Validating name
if (!binUtils.validEnvRegex.test(secretEnvName)) {
switch (envInvalid) {
const [envp, envpPath] = await binUtils.retryAuthentication(
async (auth) => {
const responseStream =
await pkClient.rpcClient.methods.vaultsSecretsEnv();
// Writing desired secrets
const secretRenameMap = new Map<string, string | undefined>();
const writeP = (async () => {
const writer = responseStream.writable.getWriter();
let first = true;
for (const envVariable of envVariables) {
const [nameOrId, secretName, secretNameNew] = envVariable;
secretRenameMap.set(secretName, secretNameNew);
await writer.write({
nameOrId,
secretName,
metadata: first ? auth : undefined,
});
first = false;
}
await writer.close();
})();

const envp: Record<string, string> = {};
const envpPath: Record<
string,
{
nameOrId: string;
secretName: string;
}
> = {};
for await (const value of responseStream.readable) {
const { nameOrId, secretName, secretContent } = value;
let newName = secretRenameMap.get(secretName);
if (newName == null) {
const secretEnvName = path.basename(secretName);
// Validating name
if (!binUtils.validEnvRegex.test(secretEnvName)) {
switch (envInvalid) {
case 'error':
throw new binErrors.ErrorPolykeyCLIInvalidEnvName(
`The following env variable name (${secretEnvName}) is invalid`,
);
case 'warn':
this.logger.warn(
`The following env variable name (${secretEnvName}) is invalid and was dropped`,
);
// Fallthrough
case 'ignore':
continue;
default:
utils.never();
}
}
newName = secretEnvName;
}
// Handling duplicate names
if (envp[newName] != null) {
switch (envDuplicate) {
// Continue without modifying
case 'error':
throw new binErrors.ErrorPolykeyCLIInvalidEnvName(
`The following env variable name (${secretEnvName}) is invalid`,
throw new binErrors.ErrorPolykeyCLIDuplicateEnvName(
`The env variable (${newName}) is duplicate`,
);
// Fallthrough
case 'keep':
continue;
// Log a warning and overwrite
case 'warn':
this.logger.warn(
`The following env variable name (${secretEnvName}) is invalid and was dropped`,
`The env variable (${newName}) is duplicate, overwriting`,
);
// Fallthrough
case 'ignore':
continue;
case 'overwrite':
break;
default:
utils.never();
}
}
newName = secretEnvName;
}
// Handling duplicate names
if (envp[newName] != null) {
switch (envDuplicate) {
// Continue without modifying
case 'error':
throw new binErrors.ErrorPolykeyCLIDuplicateEnvName(
`The env variable (${newName}) is duplicate`,
);
// Fallthrough
case 'keep':
continue;
// Log a warning and overwrite
case 'warn':
this.logger.warn(
`The env variable (${newName}) is duplicate, overwriting`,
);
// Fallthrough
case 'overwrite':
break;
default:
utils.never();
}
envp[newName] = secretContent;
envpPath[newName] = {
nameOrId,
secretName,
};
}
envp[newName] = secretContent;
}
await writeP;
return envp;
}, meta);
await writeP;
return [envp, envpPath];
},
meta,
);
// End connection early to avoid errors on server
await pkClient.stop();

// Here we want to switch between the different usages
const platform = os.platform();
if (cmd != null) {
// If a cmd is| provided then we default to exec it
const platform = os.platform();
switch (platform) {
case 'linux':
// Fallthrough
Expand All @@ -191,14 +222,23 @@ class CommandEnv extends CommandPolykey {
}
} else {
// Otherwise we switch between output formats
// If set to platform then we need to infer the format
let format = envFormat;
if (envFormat === 'platform') {
format =
{
darwin: 'unix',
linux: 'unix',
win32: 'cmd',
}[platform] ?? 'unix';
}
switch (format) {
case 'human':
// Fallthrough
case 'dotenv':
case 'unix':
{
// Formatting as a .env file
let data = '';
for (const [key, value] of Object.entries(envp)) {
data += `# ${envpPath[key].nameOrId}:${envpPath[key].secretName}\n`;
data += `${key}="${value}"\n`;
}
process.stdout.write(
Expand All @@ -209,14 +249,29 @@ class CommandEnv extends CommandPolykey {
);
}
break;
case 'prepend':
case 'cmd':
{
// Formatting as a .bat file for windows cmd
let data = '';
for (const [key, value] of Object.entries(envp)) {
data += `REM ${envpPath[key].nameOrId}:${envpPath[key].secretName}\n`;
data += `set "${key}=${value}"\n`;
}
process.stdout.write(
binUtils.outputFormatter({
type: 'raw',
data,
}),
);
}
break;
case 'powershell':
{
// Formatting as a command input
let first = true;
// Formatting as a .bat file for windows cmd
let data = '';
for (const [key, value] of Object.entries(envp)) {
data += `${first ? '' : ' '}${key}="${value}"`;
first = false;
data += `# ${envpPath[key].nameOrId}:${envpPath[key].secretName}\n`;
data += `\$env:${key} = '${value}'\n`;
}
process.stdout.write(
binUtils.outputFormatter({
Expand Down
8 changes: 8 additions & 0 deletions src/utils/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,13 @@ const envVariables = new commander.Option('-e --env <envs...>', 'specify envs')
},
);

const envFormat = new commander.Option(
'-ef --env-format <envFormat>',
'Select how the env variables are formatted on stdout if no command is specified',
)
.choices(['platform', 'json', 'unix', 'cmd', 'powershell'])
.default('platform');

const envInvalid = new commander.Option(
'-ei --env-invalid <envInvalid>',
'How invalid env variable names are handled when retrieving secrets. `error` will throw, `warn` will log a warning and drop and `ignore` will silently drop.',
Expand Down Expand Up @@ -250,6 +257,7 @@ export {
depth,
commitId,
envVariables,
envFormat,
envInvalid,
envDuplicate,
};
Loading