From 9e2025ac4d8bd0e645a00dc686ae6b00c79dbaf6 Mon Sep 17 00:00:00 2001 From: Joel Gallant Date: Sat, 19 Jun 2021 17:34:42 -0600 Subject: [PATCH 1/6] feat(#105): starts on creating environmentOptions for encryption teamMembers and encryptionKeys --- app-config-cli/src/index.ts | 106 ++++++++++-- app-config-encryption/src/encryption.ts | 201 ++++++++++++++++++---- app-config-encryption/src/secret-agent.ts | 14 +- app-config-meta/src/index.ts | 6 +- 4 files changed, 276 insertions(+), 51 deletions(-) diff --git a/app-config-cli/src/index.ts b/app-config-cli/src/index.ts index 340eba86..7988ec58 100644 --- a/app-config-cli/src/index.ts +++ b/app-config-cli/src/index.ts @@ -16,7 +16,7 @@ import { FailedToSelectSubObject, EmptyStdinOrPromptResponse, } from '@app-config/core'; -import { promptUser, consumeStdin } from '@app-config/node'; +import { promptUser, consumeStdin, asEnvOptions } from '@app-config/node'; import { checkTTY, LogLevel, logger } from '@app-config/logging'; import { LoadedConfiguration, @@ -577,13 +577,23 @@ export const cli = yargs 'Creates properties in meta file, making you the first trusted user', ], ], + options: { + environmentOverride: environmentOverrideOption, + environmentVariableName: environmentVariableNameOption, + }, }, - async () => { + async (opts) => { + const environmentOptions = asEnvOptions( + opts.environmentOverride, + undefined, + opts.environmentVariableName, + ); + const myKey = await loadPublicKeyLazy(); const privateKey = await loadPrivateKeyLazy(); // we trust ourselves, essentially - await trustTeamMember(myKey, privateKey); + await trustTeamMember(myKey, privateKey, environmentOptions); logger.info('Initialized team members and a symmetric key'); }, ), @@ -599,10 +609,20 @@ export const cli = yargs 'Sets up a new symmetric key with the latest revision number', ], ], + options: { + environmentOverride: environmentOverrideOption, + environmentVariableName: environmentVariableNameOption, + }, }, - async () => { - const keys = await loadSymmetricKeys(); - const teamMembers = await loadTeamMembersLazy(); + async (opts) => { + const environmentOptions = asEnvOptions( + opts.environmentOverride, + undefined, + opts.environmentVariableName, + ); + + const keys = await loadSymmetricKeys(undefined, environmentOptions); + const teamMembers = await loadTeamMembersLazy(environmentOptions); let revision: number; @@ -612,7 +632,12 @@ export const cli = yargs revision = 1; } - await saveNewSymmetricKey(await generateSymmetricKey(revision), teamMembers); + await saveNewSymmetricKey( + await generateSymmetricKey(revision), + teamMembers, + environmentOptions, + ); + logger.info(`Saved a new symmetric key, revision ${revision}`); }, ), @@ -670,12 +695,27 @@ export const cli = yargs name: 'ci', description: 'Creates an encryption key that can be used without a passphrase (useful for CI)', + options: { + environmentOverride: environmentOverrideOption, + environmentVariableName: environmentVariableNameOption, + }, }, - async () => { + async (opts) => { + const environmentOptions = asEnvOptions( + opts.environmentOverride, + undefined, + opts.environmentVariableName, + ); + logger.info('Creating a new trusted CI encryption key'); const { privateKeyArmored, publicKeyArmored } = await initializeKeys(false); - await trustTeamMember(await loadKey(publicKeyArmored), await loadPrivateKeyLazy()); + + await trustTeamMember( + await loadKey(publicKeyArmored), + await loadPrivateKeyLazy(), + environmentOptions, + ); process.stdout.write(`\n${publicKeyArmored}\n\n${privateKeyArmored}\n\n`); @@ -708,11 +748,21 @@ export const cli = yargs description: 'Filepath of public key', }, }, + options: { + environmentOverride: environmentOverrideOption, + environmentVariableName: environmentVariableNameOption, + }, }, async (opts) => { + const environmentOptions = asEnvOptions( + opts.environmentOverride, + undefined, + opts.environmentVariableName, + ); + const key = await loadKey(await readFile(opts.keyPath)); const privateKey = await loadPrivateKeyLazy(); - await trustTeamMember(key, privateKey); + await trustTeamMember(key, privateKey, environmentOptions); logger.info(`Trusted ${key.getUserIds().join(', ')}`); }, @@ -736,10 +786,22 @@ export const cli = yargs description: 'User ID email address', }, }, + options: { + environmentOverride: environmentOverrideOption, + environmentVariableName: environmentVariableNameOption, + }, }, async (opts) => { + const environmentOptions = asEnvOptions( + opts.environmentOverride, + undefined, + opts.environmentVariableName, + ); + const privateKey = await loadPrivateKeyLazy(); - await untrustTeamMember(opts.email, privateKey); + + // TODO: by default, untrust for all envs? + await untrustTeamMember(opts.email, privateKey, environmentOptions); }, ), ) @@ -761,9 +823,17 @@ export const cli = yargs options: { clipboard: clipboardOption, agent: secretAgentOption, + environmentOverride: environmentOverrideOption, + environmentVariableName: environmentVariableNameOption, }, }, async (opts) => { + const environmentOptions = asEnvOptions( + opts.environmentOverride, + undefined, + opts.environmentVariableName, + ); + shouldUseSecretAgent(opts.agent); // load these right away, so user unlocks asap @@ -797,7 +867,7 @@ export const cli = yargs } } - const encrypted = await encryptValue(secretValue); + const encrypted = await encryptValue(secretValue, undefined, environmentOptions); if (opts.clipboard) { await clipboardy.write(encrypted); @@ -825,9 +895,17 @@ export const cli = yargs options: { clipboard: clipboardOption, agent: secretAgentOption, + environmentOverride: environmentOverrideOption, + environmentVariableName: environmentVariableNameOption, }, }, async (opts) => { + const environmentOptions = asEnvOptions( + opts.environmentOverride, + undefined, + opts.environmentVariableName, + ); + shouldUseSecretAgent(opts.agent); // load these right away, so user unlocks asap @@ -855,7 +933,9 @@ export const cli = yargs throw new EmptyStdinOrPromptResponse('Failed to read from stdin or prompt'); } - process.stdout.write(JSON.stringify(await decryptValue(encryptedText))); + const decrypted = await decryptValue(encryptedText, undefined, environmentOptions); + + process.stdout.write(JSON.stringify(decrypted)); process.stdout.write('\n'); }, ), diff --git a/app-config-encryption/src/encryption.ts b/app-config-encryption/src/encryption.ts index 0b68f2b3..0e1b8662 100644 --- a/app-config-encryption/src/encryption.ts +++ b/app-config-encryption/src/encryption.ts @@ -15,7 +15,12 @@ import { } from '@app-config/core'; import { Json } from '@app-config/utils'; import { checkTTY, logger } from '@app-config/logging'; -import { promptUser, promptUserWithRetry } from '@app-config/node'; +import { + currentEnvironment, + EnvironmentOptions, + promptUser, + promptUserWithRetry, +} from '@app-config/node'; import { loadMetaConfig, loadMetaConfigLazy, @@ -278,32 +283,42 @@ export async function decryptSymmetricKey( return { revision: encrypted.revision, key: data }; } -export async function saveNewSymmetricKey(symmetricKey: DecryptedSymmetricKey, teamMembers: Key[]) { +export async function saveNewSymmetricKey( + symmetricKey: DecryptedSymmetricKey, + teamMembers: Key[], + environmentOptions?: EnvironmentOptions, +) { const encrypted = await encryptSymmetricKey(symmetricKey, teamMembers); + const environment = currentEnvironment(environmentOptions); await saveNewMetaFile(({ encryptionKeys = [], ...meta }) => ({ ...meta, - encryptionKeys: [...encryptionKeys, encrypted], + encryptionKeys: addForEnvironment(encrypted, encryptionKeys, environment), })); } -export async function loadSymmetricKeys(lazy = true): Promise { +export async function loadSymmetricKeys( + lazy = true, + environmentOptions?: EnvironmentOptions, +): Promise { // flag is here mostly for testing const loadMeta = lazy ? loadMetaConfigLazy : loadMetaConfig; + const environment = currentEnvironment(environmentOptions); const { value: { encryptionKeys = [] }, } = await loadMeta(); - return encryptionKeys; + return selectForEnvironment(encryptionKeys, environment); } export async function loadSymmetricKey( revision: number, privateKey: Key, lazyMeta = true, + environmentOptions?: EnvironmentOptions, ): Promise { - const symmetricKeys = await loadSymmetricKeys(lazyMeta); + const symmetricKeys = await loadSymmetricKeys(lazyMeta, environmentOptions); const symmetricKey = symmetricKeys.find((k) => k.revision === revision); if (!symmetricKey) throw new InvalidEncryptionKey(`Could not find symmetric key ${revision}`); @@ -318,35 +333,48 @@ const symmetricKeys = new Map>(); export async function loadSymmetricKeyLazy( revision: number, privateKey: Key, + environmentOptions?: EnvironmentOptions, ): Promise { if (!symmetricKeys.has(revision)) { - symmetricKeys.set(revision, loadSymmetricKey(revision, privateKey, true)); + symmetricKeys.set(revision, loadSymmetricKey(revision, privateKey, true, environmentOptions)); } return symmetricKeys.get(revision)!; } -export async function loadLatestSymmetricKey(privateKey: Key): Promise { - const allKeys = await loadSymmetricKeys(false); +export async function loadLatestSymmetricKey( + privateKey: Key, + environmentOptions?: EnvironmentOptions, +): Promise { + const allKeys = await loadSymmetricKeys(false, environmentOptions); - return loadSymmetricKey(latestSymmetricKeyRevision(allKeys), privateKey, false); + return loadSymmetricKey( + latestSymmetricKeyRevision(allKeys), + privateKey, + false, + environmentOptions, + ); } -export async function loadLatestSymmetricKeyLazy(privateKey: Key): Promise { - const allKeys = await loadSymmetricKeys(); +export async function loadLatestSymmetricKeyLazy( + privateKey: Key, + environmentOptions?: EnvironmentOptions, +): Promise { + const allKeys = await loadSymmetricKeys(true, environmentOptions); - return loadSymmetricKeyLazy(latestSymmetricKeyRevision(allKeys), privateKey); + return loadSymmetricKeyLazy(latestSymmetricKeyRevision(allKeys), privateKey, environmentOptions); } export async function encryptValue( value: Json, symmetricKeyOverride?: DecryptedSymmetricKey, + environmentOptions?: EnvironmentOptions, ): Promise { if (!symmetricKeyOverride && shouldUseSecretAgent()) { - const client = await retrieveSecretAgent(); + const client = await retrieveSecretAgent(environmentOptions); if (client) { - const allKeys = await loadSymmetricKeys(); + const allKeys = await loadSymmetricKeys(true, environmentOptions); const latestRevision = latestSymmetricKeyRevision(allKeys); const symmetricKey = allKeys.find((k) => k.revision === latestRevision)!; @@ -359,7 +387,7 @@ export async function encryptValue( if (symmetricKeyOverride) { symmetricKey = symmetricKeyOverride; } else { - symmetricKey = await loadLatestSymmetricKeyLazy(await loadPrivateKeyLazy()); + symmetricKey = await loadLatestSymmetricKeyLazy(await loadPrivateKeyLazy(), environmentOptions); } // all encrypted data is JSON encoded @@ -386,9 +414,10 @@ export async function encryptValue( export async function decryptValue( text: string, symmetricKeyOverride?: DecryptedSymmetricKey, + environmentOptions?: EnvironmentOptions, ): Promise { if (!symmetricKeyOverride && shouldUseSecretAgent()) { - const client = await retrieveSecretAgent(); + const client = await retrieveSecretAgent(environmentOptions); if (client) { return client.decryptValue(text); @@ -410,7 +439,11 @@ export async function decryptValue( ); } - symmetricKey = await loadSymmetricKeyLazy(revisionNumber, await loadPrivateKeyLazy()); + symmetricKey = await loadSymmetricKeyLazy( + revisionNumber, + await loadPrivateKeyLazy(), + environmentOptions, + ); } const armored = `-----BEGIN PGP MESSAGE-----\nVersion: OpenPGP.js VERSION\n\n${base64}\n-----END PGP PUBLIC KEY BLOCK-----`; @@ -431,13 +464,14 @@ export async function decryptValue( return JSON.parse(data) as Json; } -export async function loadTeamMembers(): Promise { +export async function loadTeamMembers(environmentOptions?: EnvironmentOptions): Promise { + const environment = currentEnvironment(environmentOptions); const { value: { teamMembers = [] }, } = await loadMetaConfig(); return Promise.all( - teamMembers.map(({ keyName, publicKey }) => + selectForEnvironment(teamMembers, environment).map(({ keyName, publicKey }) => loadKey(publicKey).then((key) => Object.assign(key, { keyName })), ), ); @@ -445,16 +479,21 @@ export async function loadTeamMembers(): Promise { let loadedTeamMembers: Promise | undefined; -export async function loadTeamMembersLazy(): Promise { +export async function loadTeamMembersLazy(environmentOptions?: EnvironmentOptions): Promise { if (!loadedTeamMembers) { - loadedTeamMembers = loadTeamMembers(); + loadedTeamMembers = loadTeamMembers(environmentOptions); } return loadedTeamMembers; } -export async function trustTeamMember(newTeamMember: Key, privateKey: Key) { - const teamMembers = await loadTeamMembers(); +export async function trustTeamMember( + newTeamMember: Key, + privateKey: Key, + environmentOptions?: EnvironmentOptions, +) { + const environment = currentEnvironment(environmentOptions); + const teamMembers = await loadTeamMembers(environmentOptions); if (newTeamMember.isPrivate()) { throw new InvalidEncryptionKey( @@ -474,7 +513,7 @@ export async function trustTeamMember(newTeamMember: Key, privateKey: Key) { const newTeamMembers = teamMembers.concat(newTeamMember); const newEncryptionKeys = await reencryptSymmetricKeys( - await loadSymmetricKeys(), + await loadSymmetricKeys(true, environmentOptions), newTeamMembers, privateKey, ); @@ -486,12 +525,22 @@ export async function trustTeamMember(newTeamMember: Key, privateKey: Key) { keyName: key.keyName ?? null, publicKey: key.armor(), })), - encryptionKeys: newEncryptionKeys, + encryptionKeys: addForEnvironment( + newEncryptionKeys, + meta.encryptionKeys ?? [], + environment, + true, + ), })); } -export async function untrustTeamMember(email: string, privateKey: Key) { - const teamMembers = await loadTeamMembers(); +export async function untrustTeamMember( + email: string, + privateKey: Key, + environmentOptions?: EnvironmentOptions, +) { + const environment = currentEnvironment(environmentOptions); + const teamMembers = await loadTeamMembers(environmentOptions); const removalCandidates = new Set(); @@ -542,7 +591,7 @@ export async function untrustTeamMember(email: string, privateKey: Key) { // of course, nothing stops users from having previously copy-pasted secrets, so they should always be rotated when untrusting old users // reason being, they had previous access to the actual private symmetric key const newEncryptionKeys = await reencryptSymmetricKeys( - await loadSymmetricKeys(), + await loadSymmetricKeys(true, environmentOptions), newTeamMembers, privateKey, ); @@ -561,7 +610,12 @@ export async function untrustTeamMember(email: string, privateKey: Key) { keyName: key.keyName ?? null, publicKey: key.armor(), })), - encryptionKeys: newEncryptionKeys, + encryptionKeys: addForEnvironment( + newEncryptionKeys, + meta.encryptionKeys ?? [], + environment, + true, + ), })); } @@ -600,11 +654,11 @@ async function reencryptSymmetricKeys( return newEncryptionKeys; } -async function retrieveSecretAgent() { +async function retrieveSecretAgent(environmentOptions?: EnvironmentOptions) { let client; try { - client = await connectAgentLazy(); + client = await connectAgentLazy(undefined, undefined, environmentOptions); } catch (err: unknown) { if (err && typeof err === 'object' && 'error' in err) { const { error } = err as { error: { errno: string } }; @@ -633,6 +687,89 @@ async function saveNewMetaFile(mutate: (props: MetaProperties) => MetaProperties await fs.writeFile(writeFilePath, stringify(writeMeta, writeFileType)); } +function selectForEnvironment( + values: T[] | Record, + environment: string | undefined, +): T[] { + if (Array.isArray(values)) { + return values; + } + + if (environment === undefined) { + if ('none' in values) { + return values.none; + } + + if ('default' in values) { + return values.default; + } + + const environments = Array.from(Object.keys(values).values()).join(', '); + + throw new AppConfigError(`No current environment selected, found [${environments}}`); + } + + if (environment in values) { + return values[environment]; + } + + const environments = Array.from(Object.keys(values).values()).join(', '); + + throw new AppConfigError(`Current environment was ${environment}, only found [${environments}]`); +} + +function addForEnvironment( + add: T | T[], + values: T[] | Record, + environment: string | undefined, + overwrite = false, +): T[] | Record { + const addArray = Array.isArray(add) ? add : [add]; + const addOrReplace = (orig: T[]) => { + if (overwrite) { + return addArray; + } + + return orig.concat(addArray); + }; + + if (Array.isArray(values)) { + return values.concat(add); + } + + if (environment === undefined) { + if ('none' in values) { + return { + ...values, + none: addOrReplace(values.none), + }; + } + + if ('default' in values) { + return { + ...values, + default: addOrReplace(values.default), + }; + } + + const environments = Array.from(Object.keys(values).values()).join(', '); + + throw new AppConfigError(`No current environment selected, found [${environments}}`); + } + + if (environment in values) { + return { + ...values, + [environment]: addOrReplace(values[environment]), + }; + } + + return { + ...values, + [environment]: addArray, + }; +} + function decodeTypedArray(buf: ArrayBuffer): string { return String.fromCharCode.apply(null, (new Uint16Array(buf) as any) as number[]); } diff --git a/app-config-encryption/src/secret-agent.ts b/app-config-encryption/src/secret-agent.ts index 9c9a3da7..ef55fcd8 100644 --- a/app-config-encryption/src/secret-agent.ts +++ b/app-config-encryption/src/secret-agent.ts @@ -6,6 +6,7 @@ import { AppConfigError } from '@app-config/core'; import { Json } from '@app-config/utils'; import { logger } from '@app-config/logging'; import { loadSettingsLazy, saveSettings } from '@app-config/settings'; +import type { EnvironmentOptions } from '@app-config/node'; import { Key, @@ -81,6 +82,7 @@ export async function connectAgent( closeTimeoutMs = Infinity, socketOrPortOverride?: number | string, loadEncryptedKey: typeof loadSymmetricKey = loadSymmetricKey, + environmentOptions?: EnvironmentOptions, ) { let client: Client; @@ -145,7 +147,7 @@ export async function connectAgent( ); } - const symmetricKey = await loadEncryptedKey(revisionNumber); + const symmetricKey = await loadEncryptedKey(revisionNumber, environmentOptions); const decrypted = await client.Decrypt({ text, symmetricKey }); keepAlive(); @@ -169,11 +171,12 @@ const clients = new Map>(); export async function connectAgentLazy( closeTimeoutMs = 500, socketOrPortOverride?: number | string, + environmentOptions?: EnvironmentOptions, ): ReturnType { const socketOrPort = await getAgentPortOrSocket(socketOrPortOverride); if (!clients.has(socketOrPort)) { - const connection = connectAgent(closeTimeoutMs, socketOrPort); + const connection = connectAgent(closeTimeoutMs, socketOrPort, undefined, environmentOptions); clients.set(socketOrPort, connection); @@ -244,8 +247,11 @@ export async function getAgentPortOrSocket( return defaultPort; } -async function loadSymmetricKey(revision: number): Promise { - const symmetricKeys = await loadSymmetricKeys(true); +async function loadSymmetricKey( + revision: number, + environmentOptions?: EnvironmentOptions, +): Promise { + const symmetricKeys = await loadSymmetricKeys(true, environmentOptions); const symmetricKey = symmetricKeys.find((k) => k.revision === revision); if (!symmetricKey) throw new AppConfigError(`Could not find symmetric key ${revision}`); diff --git a/app-config-meta/src/index.ts b/app-config-meta/src/index.ts index 523391be..ab7b1959 100644 --- a/app-config-meta/src/index.ts +++ b/app-config-meta/src/index.ts @@ -48,8 +48,10 @@ export interface GenerateFile { } export interface MetaProperties { - teamMembers?: TeamMember[]; - encryptionKeys?: EncryptedSymmetricKey[]; + teamMembers?: TeamMember[] | Record; + encryptionKeys?: + | EncryptedSymmetricKey[] + | Record; generate?: GenerateFile[]; parsingExtensions?: (ParsingExtensionWithOptions | string)[]; environmentAliases?: Record; From f748f6171ca1683db19e56a969ccf2834da5c1cc Mon Sep 17 00:00:00 2001 From: Joel Gallant Date: Sat, 19 Jun 2021 17:53:17 -0600 Subject: [PATCH 2/6] feat: environment asliases for env-specific encryption --- app-config-encryption/src/encryption.ts | 61 ++++++++++++++++++++----- app-config-node/src/index.ts | 1 + 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/app-config-encryption/src/encryption.ts b/app-config-encryption/src/encryption.ts index 0e1b8662..6d793b2f 100644 --- a/app-config-encryption/src/encryption.ts +++ b/app-config-encryption/src/encryption.ts @@ -16,6 +16,7 @@ import { import { Json } from '@app-config/utils'; import { checkTTY, logger } from '@app-config/logging'; import { + aliasesFor, currentEnvironment, EnvironmentOptions, promptUser, @@ -289,11 +290,10 @@ export async function saveNewSymmetricKey( environmentOptions?: EnvironmentOptions, ) { const encrypted = await encryptSymmetricKey(symmetricKey, teamMembers); - const environment = currentEnvironment(environmentOptions); await saveNewMetaFile(({ encryptionKeys = [], ...meta }) => ({ ...meta, - encryptionKeys: addForEnvironment(encrypted, encryptionKeys, environment), + encryptionKeys: addForEnvironment(encrypted, encryptionKeys, environmentOptions), })); } @@ -309,7 +309,13 @@ export async function loadSymmetricKeys( value: { encryptionKeys = [] }, } = await loadMeta(); - return selectForEnvironment(encryptionKeys, environment); + const selected = selectForEnvironment(encryptionKeys, environmentOptions); + + logger.verbose( + `Found ${selected.length} symmetric keys for environment: ${environment ?? 'none'}`, + ); + + return selected; } export async function loadSymmetricKey( @@ -470,8 +476,14 @@ export async function loadTeamMembers(environmentOptions?: EnvironmentOptions): value: { teamMembers = [] }, } = await loadMetaConfig(); + const currentTeamMembers = selectForEnvironment(teamMembers, environmentOptions); + + logger.verbose( + `Found ${currentTeamMembers.length} team members for environment: ${environment ?? 'none'}`, + ); + return Promise.all( - selectForEnvironment(teamMembers, environment).map(({ keyName, publicKey }) => + currentTeamMembers.map(({ keyName, publicKey }) => loadKey(publicKey).then((key) => Object.assign(key, { keyName })), ), ); @@ -492,7 +504,6 @@ export async function trustTeamMember( privateKey: Key, environmentOptions?: EnvironmentOptions, ) { - const environment = currentEnvironment(environmentOptions); const teamMembers = await loadTeamMembers(environmentOptions); if (newTeamMember.isPrivate()) { @@ -528,7 +539,7 @@ export async function trustTeamMember( encryptionKeys: addForEnvironment( newEncryptionKeys, meta.encryptionKeys ?? [], - environment, + environmentOptions, true, ), })); @@ -539,7 +550,6 @@ export async function untrustTeamMember( privateKey: Key, environmentOptions?: EnvironmentOptions, ) { - const environment = currentEnvironment(environmentOptions); const teamMembers = await loadTeamMembers(environmentOptions); const removalCandidates = new Set(); @@ -613,7 +623,7 @@ export async function untrustTeamMember( encryptionKeys: addForEnvironment( newEncryptionKeys, meta.encryptionKeys ?? [], - environment, + environmentOptions, true, ), })); @@ -689,12 +699,14 @@ async function saveNewMetaFile(mutate: (props: MetaProperties) => MetaProperties function selectForEnvironment( values: T[] | Record, - environment: string | undefined, + environmentOptions: EnvironmentOptions | undefined, ): T[] { if (Array.isArray(values)) { return values; } + const environment = currentEnvironment(environmentOptions); + if (environment === undefined) { if ('none' in values) { return values.none; @@ -713,15 +725,25 @@ function selectForEnvironment( return values[environment]; } + if (environmentOptions?.aliases) { + for (const alias of aliasesFor(environment, environmentOptions.aliases)) { + if (alias in values) { + return values[alias]; + } + } + } + const environments = Array.from(Object.keys(values).values()).join(', '); - throw new AppConfigError(`Current environment was ${environment}, only found [${environments}]`); + throw new AppConfigError( + `Current environment was ${environment}, only found [${environments}] when selecting environment-specific encryption options from meta file`, + ); } function addForEnvironment( add: T | T[], values: T[] | Record, - environment: string | undefined, + environmentOptions: EnvironmentOptions | undefined, overwrite = false, ): T[] | Record { const addArray = Array.isArray(add) ? add : [add]; @@ -737,6 +759,8 @@ function addForEnvironment( return values.concat(add); } + const environment = currentEnvironment(environmentOptions); + if (environment === undefined) { if ('none' in values) { return { @@ -754,7 +778,9 @@ function addForEnvironment( const environments = Array.from(Object.keys(values).values()).join(', '); - throw new AppConfigError(`No current environment selected, found [${environments}}`); + throw new AppConfigError( + `No current environment selected, found [${environments}] when adding environment-specific encryption options to meta file`, + ); } if (environment in values) { @@ -764,6 +790,17 @@ function addForEnvironment( }; } + if (environmentOptions?.aliases) { + for (const alias of aliasesFor(environment, environmentOptions.aliases)) { + if (alias in values) { + return { + ...values, + [alias]: addOrReplace(values[alias]), + }; + } + } + } + return { ...values, [environment]: addArray, diff --git a/app-config-node/src/index.ts b/app-config-node/src/index.ts index 193c5d54..eef579ee 100644 --- a/app-config-node/src/index.ts +++ b/app-config-node/src/index.ts @@ -1,5 +1,6 @@ export { FileSource, FlexibleFileSource, resolveFilepath } from './file-source'; export { + aliasesFor, asEnvOptions, environmentOptionsFromContext, currentEnvironment, From 2ebb3bc31c367472ef48927b9390af246cb09345 Mon Sep 17 00:00:00 2001 From: Joel Gallant Date: Sat, 19 Jun 2021 17:57:14 -0600 Subject: [PATCH 3/6] fix: addForEnvironment should respect replace when using array input --- app-config-encryption/src/encryption.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-config-encryption/src/encryption.ts b/app-config-encryption/src/encryption.ts index 6d793b2f..84748382 100644 --- a/app-config-encryption/src/encryption.ts +++ b/app-config-encryption/src/encryption.ts @@ -756,7 +756,7 @@ function addForEnvironment( }; if (Array.isArray(values)) { - return values.concat(add); + return addOrReplace(values); } const environment = currentEnvironment(environmentOptions); From 315e6e34deec46c88fbf89437a0b35485d61515b Mon Sep 17 00:00:00 2001 From: Joel Gallant Date: Sun, 27 Jun 2021 13:39:52 -0600 Subject: [PATCH 4/6] feat: use meta file environmentOptions when looking up environment for encryption subcommands --- app-config-cli/src/index.ts | 59 +++++++++++++++---------------------- 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/app-config-cli/src/index.ts b/app-config-cli/src/index.ts index 45502753..144a4059 100644 --- a/app-config-cli/src/index.ts +++ b/app-config-cli/src/index.ts @@ -48,6 +48,7 @@ import { import { loadSchema, JSONSchema } from '@app-config/schema'; import { generateTypeFiles } from '@app-config/generate'; import { validateAllConfigVariants } from './validation'; +import { loadMetaConfigLazy } from '@app-config/meta'; enum OptionGroups { Options = 'Options:', @@ -318,6 +319,21 @@ function fileTypeForFormatOption(option: string): FileType { } } +async function loadEnvironmentOptions(opts: { + environmentOverride?: string; + environmentVariableName?: string; +}) { + const { + value: { environmentAliases, environmentSourceNames }, + } = await loadMetaConfigLazy(); + + return asEnvOptions( + opts.environmentOverride, + environmentAliases, + opts.environmentVariableName ?? environmentSourceNames, + ); +} + export const cli = yargs .scriptName('app-config') .wrap(Math.max(yargs.terminalWidth() - 5, 80)) @@ -583,11 +599,7 @@ export const cli = yargs }, }, async (opts) => { - const environmentOptions = asEnvOptions( - opts.environmentOverride, - undefined, - opts.environmentVariableName, - ); + const environmentOptions = await loadEnvironmentOptions(opts); const myKey = await loadPublicKeyLazy(); const privateKey = await loadPrivateKeyLazy(); @@ -615,11 +627,7 @@ export const cli = yargs }, }, async (opts) => { - const environmentOptions = asEnvOptions( - opts.environmentOverride, - undefined, - opts.environmentVariableName, - ); + const environmentOptions = await loadEnvironmentOptions(opts); const keys = await loadSymmetricKeys(undefined, environmentOptions); const teamMembers = await loadTeamMembersLazy(environmentOptions); @@ -701,11 +709,7 @@ export const cli = yargs }, }, async (opts) => { - const environmentOptions = asEnvOptions( - opts.environmentOverride, - undefined, - opts.environmentVariableName, - ); + const environmentOptions = await loadEnvironmentOptions(opts); logger.info('Creating a new trusted CI encryption key'); @@ -754,11 +758,7 @@ export const cli = yargs }, }, async (opts) => { - const environmentOptions = asEnvOptions( - opts.environmentOverride, - undefined, - opts.environmentVariableName, - ); + const environmentOptions = await loadEnvironmentOptions(opts); const key = await loadKey(await readFile(opts.keyPath)); const privateKey = await loadPrivateKeyLazy(); @@ -792,12 +792,7 @@ export const cli = yargs }, }, async (opts) => { - const environmentOptions = asEnvOptions( - opts.environmentOverride, - undefined, - opts.environmentVariableName, - ); - + const environmentOptions = await loadEnvironmentOptions(opts); const privateKey = await loadPrivateKeyLazy(); // TODO: by default, untrust for all envs? @@ -828,11 +823,7 @@ export const cli = yargs }, }, async (opts) => { - const environmentOptions = asEnvOptions( - opts.environmentOverride, - undefined, - opts.environmentVariableName, - ); + const environmentOptions = await loadEnvironmentOptions(opts); shouldUseSecretAgent(opts.agent); @@ -900,11 +891,7 @@ export const cli = yargs }, }, async (opts) => { - const environmentOptions = asEnvOptions( - opts.environmentOverride, - undefined, - opts.environmentVariableName, - ); + const environmentOptions = await loadEnvironmentOptions(opts); shouldUseSecretAgent(opts.agent); From 1c21d9506bf4f375a53a754449e8611743bfa9e9 Mon Sep 17 00:00:00 2001 From: Joel Gallant Date: Sun, 27 Jun 2021 13:49:04 -0600 Subject: [PATCH 5/6] fix: lint --- app-config-cli/package.json | 1 + app-config-cli/src/index.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app-config-cli/package.json b/app-config-cli/package.json index b1d8dddf..692e91fe 100644 --- a/app-config-cli/package.json +++ b/app-config-cli/package.json @@ -42,6 +42,7 @@ "@app-config/generate": "^2.6.0", "@app-config/logging": "^2.6.0", "@app-config/node": "^2.6.0", + "@app-config/meta": "^2.6.0", "@app-config/schema": "^2.6.0", "@app-config/utils": "^2.6.0", "ajv": "7", diff --git a/app-config-cli/src/index.ts b/app-config-cli/src/index.ts index 144a4059..ae7c1faa 100644 --- a/app-config-cli/src/index.ts +++ b/app-config-cli/src/index.ts @@ -47,8 +47,8 @@ import { } from '@app-config/encryption'; import { loadSchema, JSONSchema } from '@app-config/schema'; import { generateTypeFiles } from '@app-config/generate'; -import { validateAllConfigVariants } from './validation'; import { loadMetaConfigLazy } from '@app-config/meta'; +import { validateAllConfigVariants } from './validation'; enum OptionGroups { Options = 'Options:', From 6b7575a81710d06375b53985aa0d77d8125125ff Mon Sep 17 00:00:00 2001 From: Joel Gallant Date: Tue, 29 Jun 2021 18:41:05 -0600 Subject: [PATCH 6/6] feat: uses environmentOptionsFromContext for decryptValue encryption extension --- app-config-encryption/src/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app-config-encryption/src/index.ts b/app-config-encryption/src/index.ts index 7ad0fc17..1d36af47 100644 --- a/app-config-encryption/src/index.ts +++ b/app-config-encryption/src/index.ts @@ -1,6 +1,7 @@ import type { ParsingExtension } from '@app-config/core'; import { named } from '@app-config/extension-utils'; import { logger } from '@app-config/logging'; +import { environmentOptionsFromContext } from '@app-config/node'; import { DecryptedSymmetricKey, decryptValue } from './encryption'; export * from './encryption'; @@ -12,7 +13,7 @@ export default function encryptedDirective( symmetricKey?: DecryptedSymmetricKey, shouldShowDeprecationNotice?: true, ): ParsingExtension { - return named('encryption', (value) => { + return named('encryption', (value, _, __, ctx) => { if (typeof value === 'string' && value.startsWith('enc:')) { return async (parse) => { if (shouldShowDeprecationNotice) { @@ -21,7 +22,8 @@ export default function encryptedDirective( ); } - const decrypted = await decryptValue(value, symmetricKey); + const environmentOptions = environmentOptionsFromContext(ctx); + const decrypted = await decryptValue(value, symmetricKey, environmentOptions); return parse(decrypted, { fromSecrets: true, parsedFromEncryptedValue: true }); };