Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fetching user configuration from AESR Config Hub #341

Merged
merged 7 commits into from
Apr 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "4.0.3",
"version": "5.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",
Expand Down
1 change: 1 addition & 0 deletions manifest_chrome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"minimum_chrome_version": "88.0",
"optional_host_permissions": ["https://*.aesr.dev/*"],
"background": {
"service_worker": "js/background.js",
"type": "module"
Expand Down
1 change: 1 addition & 0 deletions manifest_firefox.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"strict_min_version": "109.0"
}
},
"optional_permissions": ["https://*.aesr.dev/*"],
"background": {
"scripts": ["js/background.js"]
}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "aws-extend-switch-roles",
"version": "4.0.3",
"version": "5.0.0",
"description": "Extend your AWS IAM switching roles by Chrome extension",
"main": "index.js",
"directories": {
Expand Down
63 changes: 63 additions & 0 deletions src/js/handlers/remote_connect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { SessionMemory, StorageProvider } from '../lib/storage_repository.js';
import { nowEpochSeconds } from '../lib/util.js';
import { OAuthClient } from '../remote/oauth-client.js';

const sessionMemory = new SessionMemory(chrome || browser);

export async function remoteConnect(subdomain, clientId) {
const permparams = { origins: [`https://*.${subdomain}.aesr.dev/*`] };
const granted = await chrome.permissions.request(permparams);
if (!granted) return;

const oauthClient = new OAuthClient(subdomain, clientId);
const { authorizeUrl, codeVerifier } = await oauthClient.startAuthFlow();
const remoteConnectParams = { subdomain, clientId, codeVerifier };
await sessionMemory.set({ remoteConnectParams });

window.location.href = authorizeUrl;
}

export async function remoteCallback(uRL) {
const { remoteConnectParams } = await sessionMemory.get(['remoteConnectParams']);
const { subdomain, clientId, codeVerifier } = remoteConnectParams;

const oauthClient = new OAuthClient(subdomain, clientId);
const authCode = oauthClient.validateCallbackUrl(uRL);

const now = nowEpochSeconds();
const resultToken = await oauthClient.verify(codeVerifier, authCode);
await sessionMemory.set({
remoteConnectParams: {
idToken: resultToken.id_token,
expiresAt: now + resultToken.expires_in - 15,
apiEndpoint: `https://api.${subdomain}.aesr.dev`,
}
});

const localRepo = StorageProvider.getLocalRepository();
const remoteConnectInfo = {
subdomain,
clientId,
refreshToken: resultToken.refresh_token,
};
await localRepo.set({ remoteConnectInfo });
return await oauthClient.getUserConfig(resultToken.id_token);
}

export async function getRemoteConnectInfo() {
const localRepo = StorageProvider.getLocalRepository();
const { remoteConnectInfo } = await localRepo.get(['remoteConnectInfo']);
return remoteConnectInfo;
}

export function deleteRemoteConnectInfo() {
const localRepo = StorageProvider.getLocalRepository();
localRepo.delete(['remoteConnectInfo']);
}

export async function deleteRefreshTokenFromRemoteConnectInfo() {
const localRepo = StorageProvider.getLocalRepository();
const { remoteConnectInfo } = await localRepo.get(['remoteConnectInfo']);
delete remoteConnectInfo.refreshToken;
await localRepo.set({ remoteConnectInfo });
}
12 changes: 11 additions & 1 deletion src/js/handlers/update_profiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,23 @@ import { DataProfilesSplitter } from "../lib/data_profiles_splitter.js";
import { writeProfileItemsToTable, refreshDB } from "../lib/profile_db.js";
import { StorageProvider } from "../lib/storage_repository.js";
import { saveConfigIni } from "../lib/config_ini.js";
import { reloadConfig } from "../lib/reload-config.js";

export async function updateProfilesTable() {
const syncRepo = StorageProvider.getSyncRepository();
const localRepo = StorageProvider.getLocalRepository();

const { configStorageArea = 'sync', profilesLastUpdated = 0 } = await syncRepo.get(['configStorageArea', 'profilesLastUpdated']);
const { profilesTableUpdated = 0 } = await localRepo.get(['profilesTableUpdated']);
const { profilesTableUpdated = 0, remoteConnectInfo = null } = await localRepo.get(['profilesTableUpdated', 'remoteConnectInfo']);

if (remoteConnectInfo) {
try {
await reloadConfig(remoteConnectInfo);
} catch (err) {
console.warn('Failed to get profile from Config Hub');
}
return;
}

const now = nowEpochSeconds();
if (profilesTableUpdated === 0) {
Expand Down
6 changes: 4 additions & 2 deletions src/js/lib/profile_db.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@ export async function writeProfileSetToTable(profileSet) {
});

await dbManager.transaction('profiles', async dbTable => {
const { singles = [], complexes = [] } = profileSet;
let i = 0;
for (const profile of profileSet.singles) {

for (const profile of singles) {
await dbTable.insert({
profilePath: `[SINGLE];${formatNum(++i)}`,
...profile,
});
}

for (const baseProfile of profileSet.complexes) {
for (const baseProfile of complexes) {
const { targets, ...props } = baseProfile;
await dbTable.insert({
profilePath: `[COMPLEX];${formatNum(++i)}`,
Expand Down
21 changes: 21 additions & 0 deletions src/js/lib/reload-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { OAuthClient, RefreshTokenError } from "../remote/oauth-client.js";
import { writeProfileSetToTable } from "./profile_db.js";
import { deleteRefreshTokenFromRemoteConnectInfo } from "../handlers/remote_connect.js";

export async function reloadConfig(remoteConnectInfo) {
const oaClient = new OAuthClient(remoteConnectInfo.subdomain, remoteConnectInfo.clientId);
try {
const idToken = await oaClient.getIdTokenByRefresh(remoteConnectInfo.refreshToken);
const { profile } = await oaClient.getUserConfig(idToken);
await writeProfileSetToTable(profile);
console.log('Updated profile from Config Hub');
return true;
} catch (err) {
if (err instanceof RefreshTokenError) {
await deleteRefreshTokenFromRemoteConnectInfo();
console.log('Refresh token is expired');
return false;
}
throw err;
}
}
101 changes: 91 additions & 10 deletions src/js/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { loadConfigIni, saveConfigIni } from './lib/config_ini.js';
import { ColorPicker } from './lib/color_picker.js';
import { SessionMemory, StorageProvider } from './lib/storage_repository.js';
import { writeProfileSetToTable } from "./lib/profile_db.js";
import { remoteConnect, getRemoteConnectInfo, deleteRemoteConnectInfo } from './handlers/remote_connect.js';
import { reloadConfig } from './lib/reload-config.js';

function elById(id) {
return document.getElementById(id);
Expand All @@ -16,6 +18,42 @@ window.onload = function() {
let configStorageArea = 'sync';
let colorPicker = new ColorPicker(document);

elById('switchConfigHubButton').onclick = function() {
updateRemoteFieldsState('disconnected');
}
elById('cancelConfigHubButton').onclick = function() {
updateRemoteFieldsState('not_shown');
}
elById('connectConfigHubButton').onclick = function() {
const subdomain = elById('configHubDomain').value;
const clientId = elById('configHubClientId').value;
remoteConnect(subdomain, clientId).catch(err => {
updateMessage('remoteMsgSpan', err.message, 'warn');
});
}
elById('disconnectConfigHubButton').onclick = function() {
updateRemoteFieldsState('disconnected');
deleteRemoteConnectInfo();
}
elById('reloadConfigHubButton').onclick = function() {
getRemoteConnectInfo().then(rci => {
if (rci && rci.subdomain && rci.clientId) {
reloadConfig(rci).then(result => {
if (result) {
updateMessage('remoteMsgSpan', "Successfully reloaded config from Hub!");
} else {
updateMessage('remoteMsgSpan', `Failed to reload because the connection expired.`, 'warn');
updateRemoteFieldsState('disconnected');
}
}).catch(e => {
updateMessage('remoteMsgSpan', `Failed to reload because ${e.message}`, 'warn');
});
} else {
updateMessage('remoteMsgSpan', `Failed to reload because the connection is broken.`, 'warn');
}
});
}

let selection = [];
let textArea = elById('awsConfigTextArea');
textArea.onselect = function() {
Expand All @@ -34,26 +72,21 @@ window.onload = function() {
}
}

let msgSpan = elById('msgSpan');
let saveButton = elById('saveButton');
saveButton.onclick = function() {
elById('saveButton').onclick = function() {
try {
const area = elById('configStorageSyncRadioButton').checked ? 'sync' : 'local';
saveConfiguration(textArea.value, area).then(() => {
updateMessage(msgSpan, 'Configuration has been updated!', 'success');
setTimeout(() => {
msgSpan.firstChild.remove();
}, 2500);
updateMessage('msgSpan', 'Configuration has been updated!');
})
.catch(lastError => {
let msg = lastError.message
if (lastError.message === "A mutation operation was attempted on a database that did not allow mutations.") {
msg = "Configuration cannot be saved while using Private Browsing."
}
updateMessage(msgSpan, msg, 'warn');
updateMessage('msgSpan', msg, 'warn');
});
} catch (e) {
updateMessage(msgSpan, `Failed to save because ${e.message}`, 'warn');
updateMessage('msgSpan', `Failed to save because ${e.message}`, 'warn');
}
}

Expand All @@ -70,8 +103,24 @@ window.onload = function() {
signinEndpointInHereCheckBox.onchange = function() {
syncStorageRepo.set({ signinEndpointInHere: this.checked });
}

getRemoteConnectInfo().then(rci => {
if (rci && rci.subdomain && rci.clientId) {
elById('configHubDomain').value = rci.subdomain;
elById('configHubClientId').value = rci.clientId;
if (rci.refreshToken) {
updateRemoteFieldsState('connected');
} else {
updateRemoteFieldsState('disconnected');
updateMessage('remoteMsgSpan', "Please reconnect because your credentials have expired.", 'warn');
}
}
});
} else {
signinEndpointInHereCheckBox.disabled = true;
const schb = elById('switchConfigHubButton')
schb.disabled = true;
schb.title = 'Supporters only';
}
});
booleanSettings.push('signinEndpointInHere');
Expand Down Expand Up @@ -168,7 +217,8 @@ async function saveConfiguration(text, storageArea) {
await localRepo.set({ profilesTableUpdated: now });
}

function updateMessage(el, msg, cls) {
function updateMessage(elId, msg, cls = 'success') {
const el = elById(elId);
const span = document.createElement('span');
span.className = cls;
span.textContent = msg;
Expand All @@ -178,4 +228,35 @@ function updateMessage(el, msg, cls) {
} else {
el.appendChild(span);
}

if (cls === 'success') {
setTimeout(() => {
span.remove();
}, 2500);
}
}

function updateRemoteFieldsState(state) {
if (state === 'connected') {
elById('configHubPanel').style.display = 'block';
elById('standalonePanel').style.display = 'none';
elById('configHubDomain').disabled = true;
elById('configHubClientId').disabled = true;
elById('cancelConfigHubButton').style.display = 'none';
elById('connectConfigHubButton').style.display = 'none';
elById('disconnectConfigHubButton').style.display = 'inline-block';
elById('reloadConfigHubButton').style.display = 'inline-block';
} else if (state === 'disconnected') {
elById('configHubPanel').style.display = 'block';
elById('standalonePanel').style.display = 'none';
elById('configHubDomain').disabled = false;
elById('configHubClientId').disabled = false;
elById('cancelConfigHubButton').style.display = 'inline-block';
elById('connectConfigHubButton').style.display = 'inline-block';
elById('disconnectConfigHubButton').style.display = 'none';
elById('reloadConfigHubButton').style.display = 'none';
} else { // not shown
elById('standalonePanel').style.display = 'block';
elById('configHubPanel').style.display = 'none';
}
}
22 changes: 22 additions & 0 deletions src/js/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { createRoleListItem } from './lib/create_role_list_item.js';
import { CurrentContext } from './lib/current_context.js';
import { findTargetProfiles } from './lib/target_profiles.js';
import { SessionMemory, SyncStorageRepository } from './lib/storage_repository.js';
import { remoteCallback } from './handlers/remote_connect.js';
import { writeProfileSetToTable } from './lib/profile_db.js';

const sessionMemory = new SessionMemory(chrome || browser);

Expand All @@ -25,6 +27,12 @@ async function getCurrentTab() {
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 });
}
Expand Down Expand Up @@ -92,6 +100,20 @@ function main() {
noMain.style.display = 'block';
}
})
} 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';
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';
});
} else {
const p = noMain.querySelector('p');
p.textContent = "You'll see the role list here when the current tab is AWS Management Console page.";
Expand Down
Loading
Loading