server/aws-lsp-identity/src/sharedConfig/unmergeConfigFiles.ts (56 lines of code) (raw):
import { IniSection, ParsedIniData } from '@smithy/types'
/**
* Recommended:
* Store all non-secret settings in config, only store secret settings in creds
*
* Retain:
* If setting doesn't exist, use Recommended rules
* If setting defined exclusively in one file, overwrite in that file
* If setting defined in both files (tiebreaker):
* 1) Recommended rules (preferConfig)
* 2) overwrite creds, retain config (updateCredentials)
* 3) overwrite both (updateBoth)
*
* {
* mode: 'preferConfig' | 'retain',
* tiebreaker: 'preferConfig' | 'updateCredentials' | 'updateBoth'
* }
*
* aws_access_key_id, aws_secret_access_key, and aws_session_token are considered secret
*/
export function unmergeConfigFiles(
parsedKnownFiles: ParsedIniData,
configFile: ParsedIniData,
credentialsFile: ParsedIniData
): void {
// Assume sections not passed in parsedKnownFiles have been deleted
removeDeletedEntries({ removeFrom: configFile, deletedFrom: parsedKnownFiles })
removeDeletedEntries({ removeFrom: credentialsFile, deletedFrom: parsedKnownFiles })
// Unmerge parsedKnownFiles into configFile and credentialsFile
for (const [parsedSectionName, parsedSettings] of Object.entries(parsedKnownFiles)) {
unmergeSectionToConfigFiles(parsedSectionName, parsedSettings, configFile, credentialsFile)
}
}
// An options parameter can be added to allow variations in heuristics/rules for unmerging, but for now
// it is fixed with a focus on retaining as much of the customer's file as reasonable, applying best practices
// in cases where security is a concern.
function unmergeSectionToConfigFiles(
sectionName: string,
mergedSettings: IniSection,
configFile: ParsedIniData,
credentialsFile: ParsedIniData
): void {
const configSection = (configFile[sectionName] ||= {})
const credentialsSection = (credentialsFile[sectionName] ||= {})
// Remove existing settings not on mergedSettings (i.e. deleted)
removeDeletedEntries({ removeFrom: configSection, deletedFrom: mergedSettings })
removeDeletedEntries({ removeFrom: credentialsSection, deletedFrom: mergedSettings })
// Apply each setting to the correct file section, in some cases, both
for (const [settingName, settingValue] of Object.entries(mergedSettings)) {
const inConfig: boolean = configSection && Object.hasOwn(configSection, settingName)
const inCredentials: boolean = credentialsSection && Object.hasOwn(credentialsSection, settingName)
// BEST PRACTICE: Secrets should be stored in credentials only
// https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html#file-format-creds
if (isSecretSetting(settingName)) {
credentialsSection[settingName] = settingValue
if (inConfig) {
delete configSection[settingName]
}
continue
}
// Whether non-secret setting wasn't previously stored or just in config, prefer config
if (!inCredentials) {
configSection[settingName] = settingValue
continue
}
// Otherwise set in credentials and only update in config if it exists
credentialsSection[settingName] = settingValue
inConfig && (configSection[settingName] = settingValue)
}
// Remove empty sections
!Object.keys(configSection).length && delete configFile[sectionName]
!Object.keys(credentialsSection).length && delete credentialsFile[sectionName]
}
export function removeDeletedEntries(args: { removeFrom: Record<string, unknown>; deletedFrom: object }): void {
const { removeFrom, deletedFrom } = args
if (!(removeFrom && deletedFrom)) {
return
}
for (const key of Object.keys(removeFrom)) {
if (!Object.hasOwn(deletedFrom, key)) {
delete removeFrom[key]
}
}
}
function isSecretSetting(name: string): boolean {
// Settings containing "secrets" as defined in the public docs are to be stored in creds
// if not already defined in config (this is for "retain" support, future options may
// allow forcing secrets to always be written to credentials, existing or not).
// https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html#file-format-creds
return ['aws_access_key_id', 'aws_secret_access_key', 'aws_session_token'].includes(name)
}