|
#!/usr/bin/env zx |
|
$.verbose = false; |
|
|
|
import { writeFileSync } from 'fs'; |
|
import { extname } from 'path'; |
|
|
|
const profile = process.argv.length === 5 |
|
? process.argv[process.argv.length - 2] |
|
: process.argv[process.argv.length - 1] |
|
|
|
const mfaTokenCodeCandidate = process.argv[process.argv.length - 1] || '' |
|
const mfaTokenCode = mfaTokenCodeCandidate.match(/[0-9]{6}/) |
|
? mfaTokenCodeCandidate |
|
: null |
|
|
|
const homePath = process.env.HOME; |
|
const awsFolderPath = `${homePath}/.aws`; |
|
const credentialsFilePath = `${awsFolderPath}/credentials`; |
|
|
|
if (extname(profile) === '.mjs') { |
|
const availableConfigs = |
|
(await $`ls ${awsFolderPath} | grep .json`) |
|
.stdout |
|
.trim() |
|
.split('\n') |
|
.map(f => f.replace('.json', '')) |
|
.join(', '); |
|
|
|
console.log('> Configuration missing.'); |
|
console.log('> Available configurations:', availableConfigs); |
|
|
|
process.exit(1) |
|
} |
|
|
|
const rawCredentialRefs = await $`cat ${awsFolderPath}/${profile}.json`; |
|
const credentialRefs = JSON.parse(rawCredentialRefs); |
|
|
|
if (!mfaTokenCode && !credentialRefs.mfa_token_code_ref) { |
|
console.log('Error: No mfa token code provided and no ref configured, failing') |
|
process.exit(1) |
|
} |
|
|
|
await spinner(`> Obtaining temporary AWS credentials for "${profile}"`, async () => { |
|
const sessionDurationSeconds = credentialRefs.session_duration_seconds; |
|
const awsDefaultRegion = credentialRefs.default_region; |
|
const awsDefaultOutputFormat = credentialRefs.default_output_format; |
|
|
|
// To prevent multiple 1pass auth dialogs |
|
const accessKeyId = credentialRefs.access_key_id || await $`op read ${credentialRefs.access_key_id_ref}`; |
|
const [secretAccessKeyId, mfaDeviceId, tokenCode] = await Promise.all([ |
|
credentialRefs.secret_access_key || $`op read ${credentialRefs.secret_access_key_ref}`, |
|
credentialRefs.mfa_device_identifier || $`op read ${credentialRefs.mfa_device_identifier_ref}`, |
|
credentialRefs.mfa_token_code_ref ? |
|
$`op read ${credentialRefs.mfa_token_code_ref}?attribute=otp` : |
|
mfaTokenCode, |
|
]); |
|
|
|
const rawSessionTokenOutput = await $` |
|
AWS_ACCESS_KEY_ID=${accessKeyId} \ |
|
AWS_SECRET_ACCESS_KEY=${secretAccessKeyId} \ |
|
AWS_DEFAULT_REGION=${awsDefaultRegion} \ |
|
aws sts get-session-token --output=json \ |
|
--serial-number ${mfaDeviceId} \ |
|
--token-code ${tokenCode} \ |
|
--duration-seconds ${sessionDurationSeconds} |
|
`; |
|
|
|
const { Credentials: credentials } = JSON.parse(rawSessionTokenOutput); |
|
|
|
let credentialsFile = `# |
|
# TEMPORARY CREDENTIALS FOR "${profile}" AWS ACCOUNT |
|
# EXPIRES AT: ${new Date(credentials.Expiration).toLocaleString()} |
|
# RUN \`${awsFolderPath}/resolve-credentials.mjs\ ${profile}\` TO REFRESH |
|
# EDIT \`${awsFolderPath}/${profile}.json\` TO UPDATE CONFIGURATION |
|
# |
|
|
|
[default] |
|
aws_access_key_id=${credentials.AccessKeyId} |
|
aws_secret_access_key=${credentials.SecretAccessKey} |
|
aws_session_token=${credentials.SessionToken} |
|
region=${awsDefaultRegion} |
|
output=${awsDefaultOutputFormat} |
|
`; |
|
|
|
Object.entries(credentialRefs.profiles || {}) |
|
.forEach(([profile, profileConfig]) => { |
|
credentialsFile += ` |
|
[${profile}] |
|
role_arn=${profileConfig.roleArn} |
|
source_profile=${profileConfig.sourceProfile || 'default'} |
|
` |
|
}) |
|
|
|
writeFileSync(credentialsFilePath, credentialsFile.trim()); |
|
}) |
|
|
|
const formatObject = (data) => { |
|
if (Array.isArray(data)) { |
|
return data. |
|
map(value => `\n#\t - ${formatObject(value)}`) |
|
.join('\n'); |
|
} |
|
|
|
if (typeof data === 'object') { |
|
return Object.entries(data) |
|
.map(([key, value]) => `# ${key}: ${formatObject(value)}`) |
|
.join('\n'); |
|
} |
|
|
|
if (!data) { |
|
return ''; |
|
} |
|
|
|
return data.toString() |
|
} |
|
|
|
const COMMENT_SEPARATOR = '\n#\n' |
|
const encloseWith = (text, string) => |
|
`${string}${text}${string}` |
|
|
|
await spinner('> Obtaining caller identity and account aliases', async () => { |
|
const [ |
|
callerIdentityResponse, |
|
accountAliasesResponse |
|
] = await Promise.allSettled([ |
|
$`aws sts get-caller-identity --output=json`, |
|
$`aws iam list-account-aliases --output=json`, |
|
// aws organizations describe-account --account-id 537397127806 |
|
]) |
|
|
|
let extraInfo = [] |
|
if (callerIdentityResponse.status === 'fulfilled') { |
|
const { stdout: callerIdentity } = callerIdentityResponse.value |
|
extraInfo.push(formatObject(JSON.parse(callerIdentity))); |
|
} |
|
|
|
if (accountAliasesResponse.status === 'fulfilled') { |
|
const { stdout: accountAliases } = accountAliasesResponse.value |
|
extraInfo.push(formatObject(JSON.parse(accountAliases))); |
|
} |
|
|
|
if (extraInfo) { |
|
extraInfo = extraInfo.join(COMMENT_SEPARATOR); |
|
extraInfo = '\n\n' + encloseWith(extraInfo, COMMENT_SEPARATOR).trim(); |
|
} |
|
|
|
await $`echo ${extraInfo} >> ${credentialsFilePath}` |
|
}) |