|
// |
|
// Intended to be used as a bookmarklet that is executed when on the AWS Single Sign-On landing page, |
|
// this script scans the named SSO application and its accounts (specified below) and generates the |
|
// ~/.aws/credentials content and copies it to the clipboard ready to be used. |
|
// |
|
|
|
// the SSO application to use |
|
const APPLICATION_TITLE = "AWS Account"; |
|
|
|
// specify the names of the accounts to include (case sensitive). Any that are not found will be ignored. |
|
const ACCOUNTS = ["account1", "account2"] |
|
|
|
const waitFor = async (predicate, attempts = 10, delayMs = 250) => { |
|
for (let i = 0; i < attempts; ++i) { |
|
const value = predicate(); |
|
if (Boolean(value)) { |
|
return; |
|
} |
|
|
|
await new Promise(r => setTimeout(r, delayMs)); |
|
} |
|
|
|
console.error(`Predicate check failed after ${attempts} attempts`); |
|
} |
|
|
|
const expandApplication = async (title) => { |
|
const application = [...document.querySelectorAll("portal-application")].find(el => el.getAttribute("title") === title); |
|
if (!application) { |
|
console.error(`Could not find application titled '${title}'`); |
|
} |
|
else if (!application.className.includes("selected")) { |
|
application.click(); |
|
waitFor(() => application.className.includes("selected")); |
|
} |
|
} |
|
|
|
const findAccountSections = (...accounts) => { |
|
const allSections = [...document.querySelectorAll(".portal-instance-section")]; |
|
const matchedSections = allSections.reduce((acc, section) => { |
|
const name = section.querySelector(".instance-block")?.querySelector(".name")?.textContent; |
|
if (accounts.includes(name)) { |
|
acc[name] = section; |
|
} |
|
return acc; |
|
}, {}); |
|
accounts.forEach(name => { |
|
if (!matchedSections[name]) { |
|
console.warn(`Couldn't find account named ${name}, ignoring...`); |
|
} |
|
}); |
|
return matchedSections; |
|
} |
|
|
|
const findCredsLink = (section) => section.querySelector("#temp-credentials-button"); |
|
|
|
const expandAccountSection = async (section) => { |
|
const credsLink = findCredsLink(section); |
|
if (!credsLink) { |
|
section.querySelector(".instance-section")?.click(); |
|
await waitFor(() => findCredsLink(section)); |
|
} |
|
} |
|
|
|
const collapseAccountSection = async (section) => { |
|
const credsLink = findCredsLink(section); |
|
if (credsLink) { |
|
section.querySelector(".instance-section")?.click(); |
|
await waitFor(() => !findCredsLink(section)); |
|
} |
|
} |
|
|
|
const getCredsDialog = () => document.querySelector("creds-modal"); |
|
|
|
const getCreds = (logErrors = true) => { |
|
const dialog = getCredsDialog(); |
|
if (!dialog) { |
|
if (logErrors) { |
|
console.error("Credential dialog not found") |
|
} |
|
return null; |
|
} |
|
const creds = { |
|
accessKeyId: getCredsDialog()?.querySelector("#accessKeyId")?.value, |
|
secretAccessKey: getCredsDialog()?.querySelector("#secretAccessKey")?.value, |
|
sessionToken: getCredsDialog()?.querySelector("#sessionToken")?.value, |
|
}; |
|
for (const k of Object.keys(creds)) { |
|
if (!creds[k]) { |
|
if (logErrors) { |
|
console.error(`Missing value for ${k}`); |
|
} |
|
return null; |
|
} |
|
} |
|
return creds; |
|
} |
|
|
|
const openCredsDialog = async (section) => { |
|
const credsLink = findCredsLink(section); |
|
if (!credsLink) { |
|
console.error('Credentials link not found in section'); |
|
return; |
|
} |
|
credsLink.click(); |
|
|
|
await waitFor(() => getCreds(false)); |
|
} |
|
|
|
const closeCredsDialog = async () => { |
|
getCredsDialog()?.querySelector(".close")?.click(); |
|
await waitFor(() => !getCredsDialog()); |
|
} |
|
|
|
const formatAccountCreds = (name, creds) => `[${name}]\naws_access_key_id=${creds?.accessKeyId}\naws_secret_access_key=${creds?.secretAccessKey}\naws_session_token=${creds?.sessionToken}\n`; |
|
|
|
const getCredsFileContent = async () => { |
|
expandApplication(APPLICATION_TITLE); |
|
const namedSections = findAccountSections(...ACCOUNTS); |
|
|
|
let content = ""; |
|
for (const name of Object.keys(namedSections)) { |
|
const section = namedSections[name]; |
|
await expandAccountSection(section); |
|
await openCredsDialog(section); |
|
|
|
const creds = getCreds(); |
|
content += formatAccountCreds(name, creds) + '\n'; |
|
|
|
await closeCredsDialog(section); |
|
await collapseAccountSection(section); |
|
}; |
|
return content; |
|
} |
|
|
|
const installCopyToClipboardButton = () => { |
|
// can't use navigator.clipboard directly from console, has to be from user interaction |
|
// so create a button that will do this |
|
const initialButtonText = "📋 Copy ~/.aws/credentials content"; |
|
const button = document.createElement("button"); |
|
button.textContent = initialButtonText; |
|
button.style.fontSize = "14px" |
|
button.onclick = async () => { |
|
try { |
|
button.disabled = true; |
|
button.textContent = "📋 Generating..."; |
|
const credsContent = await getCredsFileContent(); |
|
navigator.clipboard.writeText(credsContent); |
|
console.log("Copied ~/.aws/credentials content to clipboard"); |
|
} finally { |
|
button.textContent = initialButtonText; |
|
button.disabled = false; |
|
} |
|
} |
|
const portalApplicationList = document.querySelector("portal-application-list"); |
|
portalApplicationList.parentElement.insertBefore(button, portalApplicationList); |
|
} |
|
|
|
installCopyToClipboardButton(); |