diff --git a/CHANGELOG.md b/CHANGELOG.md index f0b9dea..dacd48b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +## 6.0.0 (2025/01/22) + +- Add support for **multi-session** on the AWS Management Console +- Add support for **multi-level source profile references** to enable role chaining +- Add experimental feature: **Automatic tab grouping for multi-session** for supporters + ## 5.0.2 (2024/10/27) - Fix to highlight the relevant part when validation fails in the configuration textarea (thanks to @brandonkgarner) diff --git a/README.md b/README.md index f8ea6b9..0871fef 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ This extension shows a menu of switchable roles that you can configure manually. - Supports the Sync feature on all sorts of browsers - Not support switching between AWS accounts you sign into with AWS SSO or SAML solution providers directly +- Experimental support for **multi-session** on the AWS Management Console ## Large Supporters @@ -153,6 +154,7 @@ The 'Show only matching roles' setting is for use with more sophisticated accoun - **Hide account id** hides the account_id for each profile. - **Show only matching roles** filters to only show profiles with roles that match your role in your master account. +- **Automatic tab grouping for multi-session (Experimental, Supporters only)** automatically organizes tabs from the same AWS Management Console multi-session into tab groups. The tab group name will be the corresponding profile name. When a tab group is removed, the corresponding session will be automatically signed out. - **Sign-in endpoint in current region (Experimental, Supporters only)** instead of *signin.aws.amazon.com* when you browse a non-global page in AWS Management Console. For those working geographically far from Virginia, the switch role may be a little faster. - ~~**Automatically assume last assumed role (Experimental)** automatically assumes last assumed role on the next sign-in if did not back to the base account and signed out.~~ **temporarily disabled** - **Configuration storage** specifies which storage to save to. 'Sync' can automatically share it between browsers with your account but cannot store many profiles. 'Local' is the exact opposite of 'Sync.' diff --git a/bin/build.sh b/bin/build.sh index 1bdda22..db29512 100755 --- a/bin/build.sh +++ b/bin/build.sh @@ -27,7 +27,7 @@ browsers=("chrome" "firefox") for brw in ${browsers[@]} do \cp src/js/content.js dist/$brw/js/ - \cp src/js/attach_target.js dist/$brw/js/ + \cp -r src/js/war dist/$brw/js/ \cp -r src/*.html dist/$brw/ \cp -r icons dist/$brw/ done diff --git a/manifest.json b/manifest.json index 71067c0..c6491c6 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,8 @@ { - "version": "5.0.2", + "version": "6.0.0", "name": "AWS Extend Switch Roles", "description": "Extend your AWS IAM switching roles. You can set the configuration like aws config format", "short_name": "Extend SwitchRole", - "permissions": ["activeTab", "storage"], "icons" : { "48": "icons/Icon_48x48.png", "128": "icons/Icon_128x128.png" @@ -43,7 +42,7 @@ "open_in_tab": true }, "web_accessible_resources": [{ - "resources": ["js/attach_target.js"], + "resources": ["js/war/attach_target.js", "js/war/prism_switch_dest.js"], "matches": [ "https://*.console.aws.amazon.com/*", "https://health.aws.amazon.com/*", diff --git a/manifest_chrome.json b/manifest_chrome.json index ec2e74e..3db07fa 100644 --- a/manifest_chrome.json +++ b/manifest_chrome.json @@ -1,5 +1,7 @@ { - "minimum_chrome_version": "88.0", + "permissions": ["activeTab", "storage"], + "minimum_chrome_version": "89.0", + "optional_permissions": ["tabGroups"], "optional_host_permissions": ["https://*.aesr.dev/*"], "background": { "service_worker": "js/background.js", diff --git a/manifest_firefox.json b/manifest_firefox.json index e163dbb..f61d632 100644 --- a/manifest_firefox.json +++ b/manifest_firefox.json @@ -1,4 +1,5 @@ { + "permissions": ["activeTab", "storage"], "browser_specific_settings": { "gecko": { "id": "aws-extend-switch-roles@toshi.tilfin.com", diff --git a/package-lock.json b/package-lock.json index 7174b82..82d46d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "aws-extend-switch-roles", - "version": "5.0.2", + "version": "6.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "aws-extend-switch-roles", - "version": "5.0.2", + "version": "6.0.0", "license": "MIT", "dependencies": { - "aesr-config": "^0.5.1" + "aesr-config": "^0.6.0" }, "devDependencies": { "@playwright/test": "^1.49.1", @@ -492,14 +492,15 @@ "dev": true }, "node_modules/aesr-config": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/aesr-config/-/aesr-config-0.5.1.tgz", - "integrity": "sha512-8OVC0aY+8X6waJISzxVqMrbZTJlLMG1b2cNZQGnVT6XliWRSWy6OUioRVM/xETjs/ma2pPrrFztwWISjX2vUgQ==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aesr-config/-/aesr-config-0.6.0.tgz", + "integrity": "sha512-sGDFqG3qX6AS5qaN8HGIVqJwR9tAMJng+fDMJDuZQeoJ9yR+zxXDFDnAWFehDvKjkbhS9yXyW7RY/D2VbcCZpw==", + "license": "MIT", "bin": { "parse-aesr-config": "bin/parse-aesr-config.js" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/agent-base": { @@ -2283,9 +2284,9 @@ "dev": true }, "aesr-config": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/aesr-config/-/aesr-config-0.5.1.tgz", - "integrity": "sha512-8OVC0aY+8X6waJISzxVqMrbZTJlLMG1b2cNZQGnVT6XliWRSWy6OUioRVM/xETjs/ma2pPrrFztwWISjX2vUgQ==" + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aesr-config/-/aesr-config-0.6.0.tgz", + "integrity": "sha512-sGDFqG3qX6AS5qaN8HGIVqJwR9tAMJng+fDMJDuZQeoJ9yR+zxXDFDnAWFehDvKjkbhS9yXyW7RY/D2VbcCZpw==" }, "agent-base": { "version": "7.1.3", diff --git a/package.json b/package.json index 73634a9..548505a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aws-extend-switch-roles", - "version": "5.0.2", + "version": "6.0.0", "description": "Extend your AWS IAM switching roles by Chrome extension", "main": "index.js", "directories": { @@ -27,7 +27,7 @@ }, "homepage": "https://github.com/tilfinltd/aws-extend-switch-roles#readme", "dependencies": { - "aesr-config": "^0.5.1" + "aesr-config": "^0.6.0" }, "devDependencies": { "@playwright/test": "^1.49.1", diff --git a/src/js/background.js b/src/js/background.js index d1e869c..2b8c3d7 100644 --- a/src/js/background.js +++ b/src/js/background.js @@ -6,18 +6,16 @@ import { updateProfilesTable } from './handlers/update_profiles.js' const syncStorageRepo = new SyncStorageRepository(chrome || browser) const sessionMemory = new SessionMemory(chrome || browser) -function initScript() { - sessionMemory.set({ switchCount: 0 }).then(() => {}); - - syncStorageRepo.get(['goldenKeyExpire']) - .then(data => { - const { goldenKeyExpire } = data; - if ((new Date().getTime() / 1000) < Number(goldenKeyExpire)) { - return sessionMemory.set({ hasGoldenKey: 't' }).then(() => { - return setIcon('/icons/Icon_48x48_g.png'); - }); - } - }) +async function initScript() { + await sessionMemory.set({ switchCount: 0 }); + + const { goldenKeyExpire } = await syncStorageRepo.get(['goldenKeyExpire']); + if ((new Date().getTime() / 1000) < Number(goldenKeyExpire)) { + await sessionMemory.set({ hasGoldenKey: 't' }); + return setIcon('/icons/Icon_48x48_g.png'); + } else { + await syncStorageRepo.set({ autoTabGrouping: false, signinEndpointInHere: false }); + } } chrome.runtime.onStartup.addListener(function () { @@ -59,3 +57,93 @@ chrome.runtime.onMessageExternal.addListener(function (message, sender, sendResp }, 1000); // delay to prevent to try scanning id }); }); + +function createTabGroupKey(title) { + return encodeURIComponent(`tabGroup/${title}`); +} + +let listeningTabGroupsRemove = false; + +chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => { + if (message.action === 'listenTabGroupsRemove' && !listeningTabGroupsRemove) { + chrome.tabGroups.onRemoved.addListener(async function (group) { + const key = createTabGroupKey(group.title); + const result = await sessionMemory.get([key]); + const { [key]: url } = result; + if (url) { + await sessionMemory.delete([key]); + const tab = await chrome.tabs.create({ url, active: false }); + setTimeout(() => { + chrome.tabs.remove(tab.id); + }, 1000); + console.info('Logout', group.title, url); + } + }); + listeningTabGroupsRemove = true; + } else if (message.action === 'openTab') { + const { url, signinHost, tabGroup } = message; + const tab = await chrome.tabs.create({ url }); + + if (tabGroup) { + const uRL = new URL(url); + const params = new URLSearchParams(uRL.search); + const sessionId = params.get('login_hint'); + + const { title, color } = tabGroup; + + const [group] = await chrome.tabGroups.query({ title }); + if (group) { + await chrome.tabs.group({ groupId: group.id, tabIds: tab.id }); + } else { + const newGroupId = await chrome.tabs.group({ tabIds: tab.id }); + await chrome.tabGroups.update(newGroupId, { title, color: getTabGroupColor(color) }); + } + + const key = createTabGroupKey(title); + await sessionMemory.set({ [key]: `https://${signinHost}/sessions/${sessionId}/v1/logout` }); + } + } + sendResponse({}); +}); + +function getTabGroupColor(hexColor) { + if (!hexColor) return 'grey'; + + const tabGroupColors = { + grey: [128, 128, 128], + blue: [0, 0, 255], + red: [255, 0, 0], + yellow: [255, 255, 0], + green: [0, 128, 0], + pink: [255, 192, 203], + purple: [128, 0, 128], + cyan: [0, 255, 255], + orange: [255, 165, 0], + }; + + const inputRgb = (() => { + const result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hexColor); + return result ? [ + parseInt(result[1], 16), + parseInt(result[2], 16), + parseInt(result[3], 16) + ] : null; + })(); + + const calcDistance = (rgb1, rgb2) => { + if (!rgb1) return Infinity; + return rgb1.reduce((dis, val, i) => dis + (val - rgb2[i]) ** 2, 0); + }; + + let closestColor = 'grey'; + let minDistance = Infinity; + for (const tgColor in tabGroupColors) { + const distance = calcDistance(inputRgb, tabGroupColors[tgColor]); + if (distance < minDistance) { + minDistance = distance; + closestColor = tgColor; + } + } + + return closestColor; +} diff --git a/src/js/content.js b/src/js/content.js index 6af3d99..509d5f6 100644 --- a/src/js/content.js +++ b/src/js/content.js @@ -26,6 +26,17 @@ function adjustDisplayNameColor() { } } +function adjustPrismDisplayNameColor() { + try { + const navUM = document.getElementById("nav-usernameMenu"); + const spanEl = Array.from(navUM.querySelectorAll("div > span")).at(-1); + const frColor = window.getComputedStyle(spanEl).color; + if (frColor && needsInvertForeColorByBack(frColor)) { + spanEl.style.backgroundColor = "#bbbbbb"; + } + } catch {} +} + function appendAESR() { const form = document.createElement('form'); form.id = 'AESR_form'; @@ -39,59 +50,136 @@ function appendAESR() { divInfo.style.display = 'none'; divInfo.style.visibility = 'hidden'; document.body.appendChild(divInfo); + + const inputResult = document.createElement('input'); + inputResult.type = 'hidden'; + inputResult.id = 'AESR_result'; + inputResult.style.display = 'none'; + inputResult.style.visibility = 'hidden'; + document.body.appendChild(inputResult); } +function getMetaData() { + const ase = document.getElementById('awsc-signin-endpoint'); + if (!ase) return null; + + const result = { prismModeEnabled: false }; + + const asd = document.querySelector('meta[name="awsc-session-data"]'); + if (asd) { + try { + const json = asd.getAttribute('content'); + Object.assign(result, JSON.parse(json)); + } catch (e) {} + } + + if (!result.signInEndpoint) { + result.signInEndpoint = ase.getAttribute('content'); + } + + return result; +} + +const brw = (chrome || browser); +let session = null; let accountInfo = null; + function loadInfo(cb) { - if (!accountInfo) { - const script = document.createElement('script'); - script.src = chrome.runtime.getURL('/js/attach_target.js'); - script.onload = function() { - const json = document.getElementById('AESR_info').dataset.content; - accountInfo = JSON.parse(json); - cb(accountInfo); - this.remove(); - }; - document.body.appendChild(script); + if (accountInfo) { + cb(accountInfo); + return false; + } + + const script = document.createElement('script'); + script.src = brw.runtime.getURL('/js/war/attach_target.js'); + script.onload = function() { + const json = document.getElementById('AESR_info').dataset.content; + accountInfo = JSON.parse(json); + accountInfo.prism = session.prismModeEnabled; + cb(accountInfo); + this.remove(); + }; + document.body.appendChild(script); + return true; +} + +function getPrismSwitchUrl(cb) { + const script = document.createElement('script'); + script.src = brw.runtime.getURL('/js/war/prism_switch_dest.js'); + + const aesrResult = document.getElementById('AESR_result'); + function aesrResultOnChange() { + aesrResult.removeEventListener('change', aesrResultOnChange); + script.remove(); + const url = this.value; + this.value = ''; + cb(url); + } + aesrResult.addEventListener('change', aesrResultOnChange); + + document.body.appendChild(script); + return true; +} + +function doSwitch(data, cb) { + const formActionUrl = (() => { + if (session.prismModeEnabled) { + return `https://${session.signInEndpoint}/sessions/${session.sessionDifferentiator}/v1/switchrole`; + } else { + let actionHost = session.signInEndpoint; + const { actionSubdomain } = data; + if (actionSubdomain) { + if (actionHost === 'signin.aws.amazon.com') { + actionHost = actionSubdomain + '.' + actionHost; + } else if (actionHost.endsWith('.signin.aws.amazon.com')) { + actionHost = actionHost.replace(/^[^\.]+/, actionSubdomain); + } + } + return `https://${actionHost}/switchrole`; + } + })(); + + const form = document.getElementById('AESR_form'); + form.setAttribute('action', formActionUrl); + form.account.value = data.account; + form.color.value = data.color; + form.roleName.value = data.rolename; + form.displayName.value = data.displayname; + + if (session.prismModeEnabled) { + form.redirect_uri.value = data.redirecturi.replace(`${session.sessionDifferentiator}.`, "") + getPrismSwitchUrl(url => { + cb({ prism: true, url, signinHost: session.signInEndpoint }); + }); return true; } else { - cb(accountInfo); + form.redirect_uri.value = data.redirecturi; + cb({ prism: false }); + form.submit(); return false; } } -function setupMessageListener(metaASE) { - (chrome || browser).runtime.onMessage.addListener(function(msg, sender, cb) { +function setupMessageListener() { + brw.runtime.onMessage.addListener(function(msg, sender, cb) { const { data, action } = msg; if (action === 'loadInfo') { return loadInfo(cb); } else if (action === 'switch') { - let actionHost = metaASE.getAttribute('content'); - const { actionSubdomain } = data; - if (actionSubdomain && actionHost === 'signin.aws.amazon.com') { - actionHost = actionSubdomain + '.' + actionHost; - } - const form = document.getElementById('AESR_form'); - form.setAttribute('action', `https://${actionHost}/switchrole`); - form.account.value = data.account; - form.color.value = data.color; - form.roleName.value = data.rolename; - form.displayName.value = data.displayname; - form.redirect_uri.value = data.redirecturi; - cb(); - form.submit(); - return false; + return doSwitch(data, cb); } }) } if (document.body) { - const metaASE = document.getElementById('awsc-signin-endpoint'); - if (metaASE) { + const data = getMetaData(); + if (data) { + session = data; appendAESR(); - setupMessageListener(metaASE); - setTimeout(() => { - adjustDisplayNameColor(); + setupMessageListener(); + + setTimeout(() => { + session.prismModeEnabled ? adjustPrismDisplayNameColor() : adjustDisplayNameColor(); }, 1000); } } diff --git a/src/js/lib/create_role_list_item.js b/src/js/lib/create_role_list_item.js index 23152af..adf36e5 100644 --- a/src/js/lib/create_role_list_item.js +++ b/src/js/lib/create_role_list_item.js @@ -39,7 +39,7 @@ export function createRoleListItem(document, item, url, region, { hidesAccountId anchor.onclick = function() { const data = { ...this.dataset }; // do not directly refer DOM data in Firefox - selectHandler(data) + selectHandler(this, data) return false; } diff --git a/src/js/lib/create_role_list_item.test.js b/src/js/lib/create_role_list_item.test.js index bd343c0..99a5ebf 100644 --- a/src/js/lib/create_role_list_item.test.js +++ b/src/js/lib/create_role_list_item.test.js @@ -26,7 +26,7 @@ describe('createRoleListItem', () => { const url = 'https://console.aws.amazonaws.com/'; const options = {}; let handlerData = null; - const li = createRoleListItem(window.document, item, url, '', options, (data) => { handlerData = data }); + const li = createRoleListItem(window.document, item, url, '', options, (sender, data) => { handlerData = data }); const a = li.querySelector('a') expect(a.href).to.eq('http://localhost/#'); @@ -63,7 +63,7 @@ describe('createRoleListItem', () => { color: 'ffaa99', } const url = 'https://console.aws.amazonaws.com/?region=us-east-1'; - const li = createRoleListItem(window.document, item, url, {}, () => {}); + const li = createRoleListItem(window.document, item, url, {}, (sender, data) => {}); const a = li.querySelector('a') expect(a.title).to.eq('role-b@000011115555'); @@ -88,7 +88,7 @@ describe('createRoleListItem', () => { image: '"https://www.exapmle.com/icon.png"', } const url = 'https://console.aws.amazonaws.com/?region=us-east-1'; - const li = createRoleListItem(window.document, item, url, 'us-east-1', {}, () => {}); + const li = createRoleListItem(window.document, item, url, 'us-east-1', {}, (sender, data) => {}); const a = li.querySelector('a') expect(a.innerHTML).to.eq(` prf {}); + const li = createRoleListItem(window.document, item, url, 'us-east-1', {}, (sender, data) => {}); const a = li.querySelector('a') expect(a.innerHTML).to.eq(` prf {}); + const li = createRoleListItem(window.document, item, url, 'ap-southeast-1', {}, (sender, data) => {}); const a = li.querySelector('a') expect(a.dataset.redirecturi).to.eq('https%3A%2F%2Fconsole.aws.amazonaws.com%2F%3Fregion%3Dus-west-2'); @@ -139,7 +139,7 @@ background-image: url(https://www.exapmle.com/icon.png);"> prf {}); + const li = createRoleListItem(window.document, item, url, 'us-east-1', options, (sender, data) => {}); const a = li.querySelector('a') expect(a.title).to.eq('role-C@000011117777'); diff --git a/src/js/lib/current_context.js b/src/js/lib/current_context.js index 12c7926..262f40f 100644 --- a/src/js/lib/current_context.js +++ b/src/js/lib/current_context.js @@ -3,11 +3,18 @@ export class CurrentContext { const { loginDisplayNameAccount, loginDisplayNameUser, roleDisplayNameAccount, roleDisplayNameUser, + prism, } = userInfo; const { showOnlyMatchingRoles } = settings; - this.baseAccount = brushAccountId(loginDisplayNameAccount); - this.loginRole = extractLoginRole(loginDisplayNameUser.split("/", 2)[0]); + this.baseAccount = (() => { + if (prism && roleDisplayNameAccount) return brushAccountId(roleDisplayNameAccount); + return brushAccountId(loginDisplayNameAccount); + })(); + this.loginRole = (() => { + if (prism && roleDisplayNameUser) return extractLoginRole(roleDisplayNameUser); + return extractLoginRole(loginDisplayNameUser.split("/", 2)[0]); + })(); this.filterByTargetRole = showOnlyMatchingRoles ? (roleDisplayNameUser || this.loginRole) : null; } } diff --git a/src/js/lib/current_context.test.js b/src/js/lib/current_context.test.js index b691a12..c86ac16 100644 --- a/src/js/lib/current_context.test.js +++ b/src/js/lib/current_context.test.js @@ -91,6 +91,23 @@ describe('CurrentContext', () => { }); }); }); + + describe('when prism', () => { + const userInfo = { + loginDisplayNameAccount: '1111-0000-4444', + loginDisplayNameUser: 'a1-user', + roleDisplayNameAccount: null, + roleDisplayNameUser: null, + prism: true, + }; + + it('returns baseAccount with hyphens removed, login user as loginRole', () => { + const ctx = new CurrentContext(userInfo, defaultSettings); + + expect(ctx.baseAccount).to.eq('111100004444'); + expect(ctx.loginRole).to.eq('a1-user'); + }); + }) }); describe('when userInfo is on switched', () => { @@ -171,5 +188,20 @@ describe('CurrentContext', () => { }); }); }); + + describe('when prism', () => { + const prismUserInfo = { + ...userInfo, + prism: true, + } + + it('returns roleDisplayNameAccount as baseAccount, roleDisplayNameUser as loginRole', () => { + const ctx = new CurrentContext(prismUserInfo, defaultSettings); + + expect(ctx.baseAccount).to.eq('tilfin'); + expect(ctx.loginRole).to.eq('role1'); + expect(ctx.filterByTargetRole).to.be.null; + }); + }) }); }); diff --git a/src/js/options.js b/src/js/options.js index 6b5f68d..71aaf06 100644 --- a/src/js/options.js +++ b/src/js/options.js @@ -11,7 +11,8 @@ function elById(id) { return document.getElementById(id); } -const sessionMemory = new SessionMemory(chrome || browser) +const brw = chrome || browser; +const sessionMemory = new SessionMemory(brw); window.onload = function() { const syncStorageRepo = StorageProvider.getSyncRepository(); @@ -97,10 +98,32 @@ window.onload = function() { syncStorageRepo.set({ [key]: this.checked }); } } + const autoTabGroupingCheckBox = elById('autoTabGroupingCheckBox'); + if (navigator.userAgent.includes('Firefox')) { + autoTabGroupingCheckBox.disabled = true; + autoTabGroupingCheckBox.parentElement.style.textDecoration = 'line-through'; + autoTabGroupingCheckBox.parentElement.title = 'This browser does not support tab groups.'; + } const signinEndpointInHereCheckBox = elById('signinEndpointInHereCheckBox'); sessionMemory.get(['hasGoldenKey']) .then(({ hasGoldenKey }) => { if (hasGoldenKey) { + autoTabGroupingCheckBox.onchange = function(evt) { + if (this.checked) { + brw.permissions.request({ + permissions: ['tabGroups'], + origins: ["https://*.console.aws.amazon.com/*"], + }, (granted) => { + if (granted) { + syncStorageRepo.set({ autoTabGrouping: 'AddTabGroup,LogoutOnRemove' }); + } else { + this.checked = false; + } + }); + } else { + syncStorageRepo.set({ autoTabGrouping: false }); + } + } signinEndpointInHereCheckBox.onchange = function() { syncStorageRepo.set({ signinEndpointInHere: this.checked }); } @@ -118,12 +141,14 @@ window.onload = function() { } }); } else { + autoTabGroupingCheckBox.disabled = true; signinEndpointInHereCheckBox.disabled = true; const schb = elById('switchConfigHubButton') schb.disabled = true; schb.title = 'Supporters only'; } }); + booleanSettings.push('autoTabGrouping'); booleanSettings.push('signinEndpointInHere'); elById('configSenderIdText').onchange = function() { @@ -177,7 +202,7 @@ window.onload = function() { .then(data => { elById('configSenderIdText').value = data.configSenderId || ''; for (let key of booleanSettings) { - elById(`${key}CheckBox`).checked = data[key] || false; + elById(`${key}CheckBox`).checked = Boolean(data[key]); } configStorageArea = data.configStorageArea || 'sync' @@ -263,7 +288,7 @@ function updateRemoteFieldsState(state) { } function focusConfigTextArea(ln) { - const ta = document.getElementById('awsConfigTextArea'); + const ta = elById('awsConfigTextArea'); ta.scrollTop = ln < 10 ? 0 : 16 * (ln - 10); const lines = ta.value.split('\n'); if (ln === 1) { diff --git a/src/js/popup.js b/src/js/popup.js index 177cfaf..4a81c37 100644 --- a/src/js/popup.js +++ b/src/js/popup.js @@ -5,39 +5,51 @@ import { SessionMemory, SyncStorageRepository } from './lib/storage_repository.j import { remoteCallback } from './handlers/remote_connect.js'; import { writeProfileSetToTable } from './lib/profile_db.js'; -const sessionMemory = new SessionMemory(chrome || browser); +const brw = chrome || browser; + +const sessionMemory = new SessionMemory(brw); function openOptions() { - (chrome || browser).runtime.openOptionsPage().catch(err => { + brw.runtime.openOptionsPage().catch(err => { console.error(`Error: ${err}`); }); } function openPage(pageUrl) { - const brw = chrome || browser; const url = brw.runtime.getURL(pageUrl); - brw.tabs.create({ url }).catch(err => { + return brw.tabs.create({ url }).catch(err => { console.error(`Error: ${err}`); }); } async function getCurrentTab() { - const brw = chrome || browser; const [tab] = await brw.tabs.query({ currentWindow:true, active:true }); return tab; } async function moveTabToOption(tabId) { - const brw = chrome || browser; const url = await brw.runtime.getURL('options.html'); await brw.tabs.update(tabId, { url }); } async function executeAction(tabId, action, data) { - return (chrome || browser).tabs.sendMessage(tabId, { action, data }); + return brw.tabs.sendMessage(tabId, { action, data }); +} + +let mainEl, noMainEl; + +function showMessage(msg, level = 'info') { + const p = noMainEl.querySelector('p'); + p.textContent = msg; + if (level === 'error') p.style.color = '#d11'; + noMainEl.style.display = 'block'; + mainEl.style.display = 'none'; } window.onload = function() { + mainEl = document.getElementById('main'); + noMainEl = document.getElementById('noMain'); + const MANY_SWITCH_COUNT = 4; document.getElementById('openOptionsLink').onclick = function(e) { @@ -60,12 +72,16 @@ window.onload = function() { return false; } - const storageRepo = new SyncStorageRepository(chrome || browser); - storageRepo.get(['visualMode']).then(({ visualMode }) => { + const storageRepo = new SyncStorageRepository(brw); + storageRepo.get(['visualMode', 'autoTabGrouping']).then(({ visualMode, autoTabGrouping }) => { const mode = visualMode || 'default'; if (mode === 'dark' || (mode === 'default' && window.matchMedia('(prefers-color-scheme: dark)').matches)) { document.body.classList.add('darkMode'); } + + if (autoTabGrouping) { + brw.runtime.sendMessage({ action: 'listenTabGroupsRemove' }); + } }); sessionMemory.get(['hasGoldenKey', 'switchCount']) @@ -90,53 +106,53 @@ function main() { || url.host.endsWith('.amazonaws.cn')) { executeAction(tab.id, 'loadInfo', {}).then(userInfo => { if (userInfo) { - document.getElementById('main').style.display = 'block'; + mainEl.style.display = 'block'; return loadFormList(url, userInfo, tab.id); } else { - const noMain = document.getElementById('noMain'); - const p = noMain.querySelector('p'); - p.textContent = 'Failed to fetch user info from the AWS Management Console page'; - p.style.color = '#d11'; - noMain.style.display = 'block'; + showMessage('Failed to fetch user info from the AWS Management Console page', 'error'); } }) } else if (url.host.endsWith('.aesr.dev') && url.pathname.startsWith('/callback')) { remoteCallback(url) .then(userCfg => { - const p = noMain.querySelector('p'); - p.textContent = "Successfully connected to AESR Config Hub!"; - noMain.style.display = 'block'; + showMessage("Successfully connected to AESR Config Hub!"); return writeProfileSetToTable(userCfg.profile); }) .then(() => moveTabToOption(tab.id)) .catch(err => { - const p = noMain.querySelector('p'); - p.textContent = `Failed to connect to AESR Config Hub because.\n${err.message}`; - noMain.style.display = 'block'; + showMessage(`Failed to connect to AESR Config Hub because.\n${err.message}`, 'error'); }); } else { - const p = noMain.querySelector('p'); - p.textContent = "You'll see the role list here when the current tab is AWS Management Console page."; - noMain.style.display = 'block'; + showMessage("You'll see the role list here when the current tab is AWS Management Console page."); } }) } async function loadFormList(curURL, userInfo, tabId) { - const storageRepo = new SyncStorageRepository(chrome || browser); - const data = await storageRepo.get(['hidesAccountId', 'showOnlyMatchingRoles', 'signinEndpointInHere']); - const { hidesAccountId = false, showOnlyMatchingRoles = false, signinEndpointInHere = false } = data; + const storageRepo = new SyncStorageRepository(brw); + const data = await storageRepo.get(['hidesAccountId', 'showOnlyMatchingRoles', 'autoTabGrouping', 'signinEndpointInHere']); + const { hidesAccountId = false, showOnlyMatchingRoles = false, autoTabGrouping = false, signinEndpointInHere = false } = data; const curCtx = new CurrentContext(userInfo, { showOnlyMatchingRoles }); const profiles = await findTargetProfiles(curCtx); - renderRoleList(profiles, tabId, curURL, { hidesAccountId, signinEndpointInHere }); + renderRoleList(profiles, tabId, curURL, userInfo.prism, { hidesAccountId, autoTabGrouping, signinEndpointInHere }); setupRoleFilter(); } -function renderRoleList(profiles, tabId, curURL, options) { +function renderRoleList(profiles, tabId, curURL, isPrism, options) { const { url, region, isLocal } = getCurrentUrlandRegion(curURL) - const listItemOnSelect = function(data) { + const listItemOnSelect = function(sender, data) { + // disable link for loading + sender.style.fontWeight = 'bold'; + sender.onclick = null; + if (options.signinEndpointInHere && isLocal) data.actionSubdomain = region; + if (isPrism) { + if (options.autoTabGrouping) { + data.tabGroup = { title: data.profile, color: data.color }; + } + data.displayname = data.displayname.replace(/\s\s\|\s\s\d{12}$/, ''); + } sendSwitchRole(tabId, data); } const list = document.getElementById('roleList'); @@ -180,15 +196,26 @@ function setupRoleFilter() { roleFilter.focus() } -function sendSwitchRole(tabId, data) { - executeAction(tabId, 'switch', data).then(() => { - sessionMemory.get(['switchCount']).then(({ switchCount }) => { - let swcnt = switchCount || 0; - return sessionMemory.set({ switchCount: ++swcnt }); - }).then(() => { - window.close() - }) - }); +async function sendSwitchRole(tabId, data) { + const { prism, url, signinHost } = await executeAction(tabId, 'switch', data); + if (prism && !url) { + showMessage("Switch failed: this session doesn't have permission to switch to target profile.", 'error'); + return; + } + + const { switchCount } = await sessionMemory.get(['switchCount']); + await sessionMemory.set({ switchCount: (switchCount || 0) + 1 }); + + if (prism) { + await brw.runtime.sendMessage({ + action: 'openTab', + url, + signinHost, + tabGroup: data.tabGroup, + }); + } + + window.close(); } function getCurrentUrlandRegion(aURL) { @@ -198,7 +225,7 @@ function getCurrentUrlandRegion(aURL) { if (md) region = md[1]; let isLocal = false; - const mdsd = aURL.host.match(/^(([a-z]{2}\-[a-z]+\-[1-9])\.)?console\.aws/); + const mdsd = aURL.host.match(/(([a-z]{2}\-[a-z-]+\-[1-9])\.)?console\.(aws|amazonaws)/); if (mdsd) { const [,, cr = 'us-east-1'] = mdsd; if (cr === region) isLocal = true; diff --git a/src/js/attach_target.js b/src/js/war/attach_target.js similarity index 100% rename from src/js/attach_target.js rename to src/js/war/attach_target.js diff --git a/src/js/war/prism_switch_dest.js b/src/js/war/prism_switch_dest.js new file mode 100644 index 0000000..20854a5 --- /dev/null +++ b/src/js/war/prism_switch_dest.js @@ -0,0 +1,31 @@ +(async function () { + const form = document.getElementById('AESR_form'); + + const reqBody = { + account: form.account.value, + color: form.color.value, + displayName: form.displayName.value, + redirectUri: decodeURIComponent(form.redirect_uri.value), + roleName: form.roleName.value, + }; + + const url = form.getAttribute('action'); + const res = await fetch(url, { + body: JSON.stringify(reqBody), + headers: { + "X-CSRF-PROTECTION": "1", + "content-type": "application/json" + }, + method: "POST", + credentials: "include" + }); + + const resultEl = document.getElementById('AESR_result'); + try { + const resBody = await res.json(); + resultEl.value = resBody.destination; + } catch (e) { + resultEl.value = ''; + } + resultEl.dispatchEvent(new Event('change')); +})(); diff --git a/src/options.html b/src/options.html index a020bb8..b1b0787 100644 --- a/src/options.html +++ b/src/options.html @@ -183,6 +183,7 @@

Settings