From 8ae95508f3801f384e32744686d8c35e26b51b1d Mon Sep 17 00:00:00 2001 From: Mark Biesheuvel Date: Fri, 24 Nov 2017 19:06:39 +0100 Subject: [PATCH 1/8] Convert to Unix line endings and remove trailing whitespaces on end of line --- CHANGELOG.md | 40 +++++++-------- README.md | 58 +++++++++++----------- background/script.js | 18 +++---- manifest.json | 2 +- options/changelog.html | 10 ++-- options/options.css | 108 ++++++++++++++++++++--------------------- options/options.html | 9 ++-- options/options.js | 8 +-- popup/popup.html | 1 - popup/popup.js | 2 +- 10 files changed, 127 insertions(+), 129 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bbe49c..74b435e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +1,20 @@ -# Changelog - -## 2017-nov-01 (v2.1) -* Bug fix: Chrome 62 broke the extension. Special thanks for [Brice](https://github.com/bdruth) for contributing. Thanks to [Gijs](https://gitlab.com/gbvanrenswoude) for helping out with testing. - -## 2016-nov-21 (v2.0) -* Added functionality to specify Role ARN's in the options panel. This is meant for cross-account assume-role API calls. For each specified role temporary credentials will be fetched and added to the credentials file. -* Updated 'AWS SDK for Javascript' library to latest version -* Plugin now shows changelog to the user after the installation of new version -* Options panel has a new look to improve readability - -## 2016-jul-24 (v1.2) -* Bug fix: when just 1 role in the SAML Assertion available now also works well -* Now uses a regex to extract Role and Principal from SAML Assertion. This way it does not matter in what order the IDP adds the Role and Principle to the SAML Assertion. - -## 2016-apr-11 (v1.1) -* Improved usability. No longer needed to manually specify PrincipalArn and RoleArn in options panel. Removed these options from the options panel. PrincipalArn and RoleArn is now parsed from the SAML Assertion itself. - -## 2016-apr-04 (v1.0) -* Initial release \ No newline at end of file +# Changelog + +## 2017-nov-01 (v2.1) +* Bug fix: Chrome 62 broke the extension. Special thanks for [Brice](https://github.com/bdruth) for contributing. Thanks to [Gijs](https://gitlab.com/gbvanrenswoude) for helping out with testing. + +## 2016-nov-21 (v2.0) +* Added functionality to specify Role ARN's in the options panel. This is meant for cross-account assume-role API calls. For each specified role temporary credentials will be fetched and added to the credentials file. +* Updated 'AWS SDK for Javascript' library to latest version +* Plugin now shows changelog to the user after the installation of new version +* Options panel has a new look to improve readability + +## 2016-jul-24 (v1.2) +* Bug fix: when just 1 role in the SAML Assertion available now also works well +* Now uses a regex to extract Role and Principal from SAML Assertion. This way it does not matter in what order the IDP adds the Role and Principle to the SAML Assertion. + +## 2016-apr-11 (v1.1) +* Improved usability. No longer needed to manually specify PrincipalArn and RoleArn in options panel. Removed these options from the options panel. PrincipalArn and RoleArn is now parsed from the SAML Assertion itself. + +## 2016-apr-04 (v1.0) +* Initial release diff --git a/README.md b/README.md index 3da86b9..027372e 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,29 @@ -# SAML to AWS STS Keys Conversion -Google Chrome Extension which converts a SAML 2.0 assertion to AWS STS Keys (temporary credentials). Just log in to the AWS Web Management Console using your SAML IDP and the Chrome Extension will fetch the SAML Assertion from the HTTP request. The SAML Assertion is then used to call the assumeRoleWithSAML API to create the temporary credentials. (AccessKeyId, SecretAccessKey and SessionToken). - -The Chrome Extension can be downloaded here: -[Google Chrome Web Store](https://chrome.google.com/webstore/detail/ekniobabpcnfjgfbphhcolcinmnbehde/) - -# Table of Contents -* [Why this Chrome Extension?](#why) -* [Getting Started](#gettingstarted) -* [Create a symlink to your .aws directory (for Windows users)](#symlink) -* [Frequently Asked Question](#faq) - -## Why this Chrome Extension? -If you don't have any user administration setup within AWS Identity & Access Management (IAM) but instead rely on your corporate user directory, i.e. Microsoft Active Directory. Your company uses a SAML 2.0 Identity Provider (IDP) to log in to the AWS Web Management Console (Single Sign On). Then this Chrome Estension if for you! - -You run into trouble as soon as you would like to execute some fancy scripts from your computer which calls the AWS API's. When sending a request to the AWS API's you need credentials, meaning an AccessKey and SecretKey. You can easily generate these keys for each user in AWS IAM. However, since you don't have any users in AWS IAM and don't want to create users just for the sake of having an AccessKey and SecretKey you are screwed. But there is a way to get temporary credentials specifically for your corporate identity. - -The Security Token Service (STS) from AWS provides an API action assumeRoleWithSAML. Using the SAML Assertion given by your IDP the Chrome Extension will call this API action to fetch temporary credentials. (AccessKeyId, SecretAccessKey and SessionToken). This way there is no need to create some sort of anonymous user in AWS IAM used for executing scripts. This would be a real security nightmare, since it won't be possible to audit who did what. This Chrome Extension however will make it super easy for you to just use your corporate identity for executing scripts calling AWS API's. - -## Getting Started -TODO - -## Create a symlink to your .aws directory (for Windows users) -TODO - -## FAQ: Frequently Asked Question -1. Why can I not save file somewhere else? -TODO -2. How long are the credentials valid? +# SAML to AWS STS Keys Conversion +Google Chrome Extension which converts a SAML 2.0 assertion to AWS STS Keys (temporary credentials). Just log in to the AWS Web Management Console using your SAML IDP and the Chrome Extension will fetch the SAML Assertion from the HTTP request. The SAML Assertion is then used to call the assumeRoleWithSAML API to create the temporary credentials. (AccessKeyId, SecretAccessKey and SessionToken). + +The Chrome Extension can be downloaded here: +[Google Chrome Web Store](https://chrome.google.com/webstore/detail/ekniobabpcnfjgfbphhcolcinmnbehde/) + +# Table of Contents +* [Why this Chrome Extension?](#why) +* [Getting Started](#gettingstarted) +* [Create a symlink to your .aws directory (for Windows users)](#symlink) +* [Frequently Asked Question](#faq) + +## Why this Chrome Extension? +If you don't have any user administration setup within AWS Identity & Access Management (IAM) but instead rely on your corporate user directory, i.e. Microsoft Active Directory. Your company uses a SAML 2.0 Identity Provider (IDP) to log in to the AWS Web Management Console (Single Sign On). Then this Chrome Estension if for you! + +You run into trouble as soon as you would like to execute some fancy scripts from your computer which calls the AWS API's. When sending a request to the AWS API's you need credentials, meaning an AccessKey and SecretKey. You can easily generate these keys for each user in AWS IAM. However, since you don't have any users in AWS IAM and don't want to create users just for the sake of having an AccessKey and SecretKey you are screwed. But there is a way to get temporary credentials specifically for your corporate identity. + +The Security Token Service (STS) from AWS provides an API action assumeRoleWithSAML. Using the SAML Assertion given by your IDP the Chrome Extension will call this API action to fetch temporary credentials. (AccessKeyId, SecretAccessKey and SessionToken). This way there is no need to create some sort of anonymous user in AWS IAM used for executing scripts. This would be a real security nightmare, since it won't be possible to audit who did what. This Chrome Extension however will make it super easy for you to just use your corporate identity for executing scripts calling AWS API's. + +## Getting Started +TODO + +## Create a symlink to your .aws directory (for Windows users) +TODO + +## FAQ: Frequently Asked Question +1. Why can I not save file somewhere else? +TODO +2. How long are the credentials valid? diff --git a/background/script.js b/background/script.js index 0678371..8cd1adb 100644 --- a/background/script.js +++ b/background/script.js @@ -2,7 +2,7 @@ var FileName = 'credentials'; var RoleArns = {}; -// When this background process starts, load variables from chrome storage +// When this background process starts, load variables from chrome storage // from saved Extension Options loadItemsFromStorage(); // Additionaly on start of the background process it is checked if this extension can be activated @@ -58,10 +58,10 @@ function onBeforeRequestEvent(details) { samlXmlDoc = decodeURIComponent(unescape(window.atob(details.requestBody.formData.SAMLResponse[0]))); } else if (details.requestBody.raw) { var combined = new ArrayBuffer(0); - details.requestBody.raw.forEach(function(element) { - var tmp = new Uint8Array(combined.byteLength + element.bytes.byteLength); - tmp.set( new Uint8Array(combined), 0 ); - tmp.set( new Uint8Array(element.bytes),combined.byteLength ); + details.requestBody.raw.forEach(function(element) { + var tmp = new Uint8Array(combined.byteLength + element.bytes.byteLength); + tmp.set( new Uint8Array(combined), 0 ); + tmp.set( new Uint8Array(element.bytes),combined.byteLength ); combined = tmp.buffer; }); var combinedView = new DataView(combined); @@ -91,7 +91,7 @@ function onBeforeRequestEvent(details) { } // If there is more than 1 role in the claim, look at the 'roleIndex' HTTP Form data parameter to determine the role to assume if (roleDomNodes.length > 1 && hasRoleIndex) { - for (i = 0; i < roleDomNodes.length; i++) { + for (i = 0; i < roleDomNodes.length; i++) { var nodeValue = roleDomNodes[i].innerHTML; if (nodeValue.indexOf(roleIndex) > -1) { // This DomNode holdes the data for the role to assume. Use these details for the assumeRoleWithSAML API call @@ -122,7 +122,7 @@ function extractPrincipalPlusRoleAndAssumeRole(samlattribute, SAMLAssertion) { // Extraxt both regex patterns from SAMLAssertion attribute RoleArn = samlattribute.match(reRole)[0]; PrincipalArn = samlattribute.match(rePrincipal)[0]; - + // Set parameters needed for assumeRoleWithSAML method var params = { PrincipalArn: PrincipalArn, @@ -150,7 +150,7 @@ function extractPrincipalPlusRoleAndAssumeRole(samlattribute, SAMLAssertion) { console.log('INFO: Do additional assume-role for role -> ' + RoleArns[profileList[0]]); assumeAdditionalRole(profileList, 0, data.Credentials.AccessKeyId, data.Credentials.SecretAccessKey, data.Credentials.SessionToken, docContent); } - } + } }); } @@ -190,7 +190,7 @@ function assumeAdditionalRole(profileList, index, AccessKeyId, SecretAccessKey, // Called from either extractPrincipalPlusRoleAndAssumeRole (if RoleArns dict is empty) -// Otherwise called from assumeAdditionalRole as soon as all roles from RoleArns have been assumed +// Otherwise called from assumeAdditionalRole as soon as all roles from RoleArns have been assumed function outputDocAsDownload(docContent) { var doc = URL.createObjectURL( new Blob([docContent], {type: 'application/octet-binary'}) ); // Triggers download of the generated file diff --git a/manifest.json b/manifest.json index 52ef270..a027dd0 100644 --- a/manifest.json +++ b/manifest.json @@ -24,4 +24,4 @@ "downloads", "tabs" ] -} \ No newline at end of file +} diff --git a/options/changelog.html b/options/changelog.html index a74c855..e94ad27 100644 --- a/options/changelog.html +++ b/options/changelog.html @@ -11,14 +11,14 @@

Changelog for 'SAML to AWS STS Keys Converter'


- +

Developed by prolane.org (Gerard Laan)

For help, please see README at the project page on Github.com

Click here to open the options panel for this plugin.


- +

2017-nov-01
v2.1

+
- \ No newline at end of file + diff --git a/options/options.css b/options/options.css index 6d26022..4c290d9 100644 --- a/options/options.css +++ b/options/options.css @@ -1,54 +1,54 @@ -div { - padding: 1em; -} - -.inline-div { - padding: 0em; -} - -#divTitle { - display: flex; -} - -#divHeader { - margin-left: 10px; -} - -#divSettings { - width: 900px; - border-style: solid; - border-radius: 10px; - background-color: #fef6e0; -} - -.setting-part { - width: 876px; - border-radius: 10px; - background-color: #f1c47b; - margin-bottom: 14px; -} - -label { - font-weight: bold; - font-family: "Segoe UI", Tahoma, sans-serif; - font-size: 100%; - display:inline-block; - margin-bottom: 5px; -} - -#role_arns { - margin-top: 14px; -} - -#status { - height: 15px; - margin-bottom: 5px; -} - -#divSave { - padding-top: 0em; -} - -#divChangelog { - width: 600px; -} \ No newline at end of file +div { + padding: 1em; +} + +.inline-div { + padding: 0em; +} + +#divTitle { + display: flex; +} + +#divHeader { + margin-left: 10px; +} + +#divSettings { + width: 900px; + border-style: solid; + border-radius: 10px; + background-color: #fef6e0; +} + +.setting-part { + width: 876px; + border-radius: 10px; + background-color: #f1c47b; + margin-bottom: 14px; +} + +label { + font-weight: bold; + font-family: "Segoe UI", Tahoma, sans-serif; + font-size: 100%; + display:inline-block; + margin-bottom: 5px; +} + +#role_arns { + margin-top: 14px; +} + +#status { + height: 15px; + margin-bottom: 5px; +} + +#divSave { + padding-top: 0em; +} + +#divChangelog { + width: 600px; +} diff --git a/options/options.html b/options/options.html index 116d561..8fab72b 100644 --- a/options/options.html +++ b/options/options.html @@ -11,26 +11,26 @@

Options page for 'SAML to AWS STS Keys Converter'


- +

Developed by prolane.org (Gerard Laan)

For help, please see README at the project page on Github.com

- +

- +

- +
@@ -39,4 +39,3 @@ - diff --git a/options/options.js b/options/options.js index 8c94c30..9975e48 100644 --- a/options/options.js +++ b/options/options.js @@ -2,11 +2,11 @@ function save_options() { // Get the filename to be saved var FileName = document.getElementById('FileName').value; - + // Get the Role_ARN's (Profile/ARNs pairs) entered by the user in the table var RoleArns = {}; // Iterate over all added profiles in the list - $("input[id^='profile_']").each(function( index ) { + $("input[id^='profile_']").each(function( index ) { // Replace profile_ for arn_ to be able to get value of corresponding arn input field var input_id_arn = $(this).attr('id').replace("profile", "arn"); // Create key-value pair to add to RoleArns dictionary. @@ -15,7 +15,7 @@ function save_options() { RoleArns[$(this).val()] = $('#' + input_id_arn).val(); } }); - + // Do the actual saving into Chrome storage chrome.storage.sync.set({ FileName: FileName, @@ -101,4 +101,4 @@ function randomId() { } document.addEventListener('DOMContentLoaded', restore_options); -document.getElementById('save').addEventListener('click', save_options); \ No newline at end of file +document.getElementById('save').addEventListener('click', save_options); diff --git a/popup/popup.html b/popup/popup.html index 9441dea..cfd0a47 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -31,4 +31,3 @@

SAML to AWS STS Keys

- diff --git a/popup/popup.js b/popup/popup.js index 36e5852..bc62fb8 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -33,4 +33,4 @@ function chkboxChangeHandler(event) { chrome.runtime.sendMessage({action: action}, function(response) { console.log(response.message); }); -} \ No newline at end of file +} From 4a9d1fad8507293a44a375ce517057e6dcd4d744 Mon Sep 17 00:00:00 2001 From: Mark Biesheuvel Date: Fri, 24 Nov 2017 19:13:54 +0100 Subject: [PATCH 2/8] Change mixture of tab and spaces to consistent indentation with spaces --- background/background.html | 12 +-- background/script.js | 152 ++++++++++++++++++------------------- manifest.json | 10 ++- options/changelog.html | 94 +++++++++++------------ options/options.css | 42 +++++----- options/options.html | 58 +++++++------- options/options.js | 98 ++++++++++++------------ popup/popup.html | 36 ++++----- 8 files changed, 252 insertions(+), 250 deletions(-) diff --git a/background/background.html b/background/background.html index 2d66b6e..be8a08a 100644 --- a/background/background.html +++ b/background/background.html @@ -1,9 +1,9 @@ - - - - - - + + + + + + diff --git a/background/script.js b/background/script.js index 8cd1adb..4ffdf58 100644 --- a/background/script.js +++ b/background/script.js @@ -16,9 +16,9 @@ chrome.storage.sync.get({ // If so, show the user the changelog // var thisVersion = chrome.runtime.getManifest().version; chrome.runtime.onInstalled.addListener(function(details){ - if(details.reason == "install" || details.reason == "update"){ - // Open a new tab to show changelog html page - chrome.tabs.create({url: "../options/changelog.html"}); + if(details.reason == "install" || details.reason == "update"){ + // Open a new tab to show changelog html page + chrome.tabs.create({url: "../options/changelog.html"}); } }); @@ -95,16 +95,16 @@ function onBeforeRequestEvent(details) { var nodeValue = roleDomNodes[i].innerHTML; if (nodeValue.indexOf(roleIndex) > -1) { // This DomNode holdes the data for the role to assume. Use these details for the assumeRoleWithSAML API call - // The Role Attribute from the SAMLAssertion (DomNode) plus the SAMLAssertion itself is given as function arguments. - extractPrincipalPlusRoleAndAssumeRole(nodeValue, SAMLAssertion) + // The Role Attribute from the SAMLAssertion (DomNode) plus the SAMLAssertion itself is given as function arguments. + extractPrincipalPlusRoleAndAssumeRole(nodeValue, SAMLAssertion) } } } // If there is just 1 role in the claim there will be no 'roleIndex' in the form data. else if (roleDomNodes.length == 1) { // When there is just 1 role in the claim, use these details for the assumeRoleWithSAML API call - // The Role Attribute from the SAMLAssertion (DomNode) plus the SAMLAssertion itself is given as function arguments. - extractPrincipalPlusRoleAndAssumeRole(roleDomNodes[0].innerHTML, SAMLAssertion) + // The Role Attribute from the SAMLAssertion (DomNode) plus the SAMLAssertion itself is given as function arguments. + extractPrincipalPlusRoleAndAssumeRole(roleDomNodes[0].innerHTML, SAMLAssertion) } } @@ -115,76 +115,76 @@ function onBeforeRequestEvent(details) { // This function extracts the RoleArn and PrincipalArn (SAML-provider) // from this argument and uses it to call the AWS STS assumeRoleWithSAML API. function extractPrincipalPlusRoleAndAssumeRole(samlattribute, SAMLAssertion) { - // Pattern for Role - var reRole = /arn:aws:iam:[^:]*:[0-9]+:role\/[^,]+/i; - // Patern for Principal (SAML Provider) - var rePrincipal = /arn:aws:iam:[^:]*:[0-9]+:saml-provider\/[^,]+/i; - // Extraxt both regex patterns from SAMLAssertion attribute - RoleArn = samlattribute.match(reRole)[0]; - PrincipalArn = samlattribute.match(rePrincipal)[0]; - - // Set parameters needed for assumeRoleWithSAML method - var params = { - PrincipalArn: PrincipalArn, - RoleArn: RoleArn, - SAMLAssertion: SAMLAssertion, - }; - // Call STS API from AWS - var sts = new AWS.STS(); - sts.assumeRoleWithSAML(params, function(err, data) { - if (err) console.log(err, err.stack); // an error occurred - else { - // On succesful API response create file with the STS keys - var docContent = "[default] \n" + - "aws_access_key_id = " + data.Credentials.AccessKeyId + " \n" + - "aws_secret_access_key = " + data.Credentials.SecretAccessKey + " \n" + - "aws_session_token = " + data.Credentials.SessionToken; - - // If there are no Role ARNs configured in the options panel, continue to create credentials file - // Otherwise, extend docContent with a profile for each specified ARN in the options panel - if (Object.keys(RoleArns).length == 0) { - console.log('Output maken'); - outputDocAsDownload(docContent); - } else { - var profileList = Object.keys(RoleArns); - console.log('INFO: Do additional assume-role for role -> ' + RoleArns[profileList[0]]); - assumeAdditionalRole(profileList, 0, data.Credentials.AccessKeyId, data.Credentials.SecretAccessKey, data.Credentials.SessionToken, docContent); - } - } - }); + // Pattern for Role + var reRole = /arn:aws:iam:[^:]*:[0-9]+:role\/[^,]+/i; + // Patern for Principal (SAML Provider) + var rePrincipal = /arn:aws:iam:[^:]*:[0-9]+:saml-provider\/[^,]+/i; + // Extraxt both regex patterns from SAMLAssertion attribute + RoleArn = samlattribute.match(reRole)[0]; + PrincipalArn = samlattribute.match(rePrincipal)[0]; + + // Set parameters needed for assumeRoleWithSAML method + var params = { + PrincipalArn: PrincipalArn, + RoleArn: RoleArn, + SAMLAssertion: SAMLAssertion, + }; + // Call STS API from AWS + var sts = new AWS.STS(); + sts.assumeRoleWithSAML(params, function(err, data) { + if (err) console.log(err, err.stack); // an error occurred + else { + // On succesful API response create file with the STS keys + var docContent = "[default] \n" + + "aws_access_key_id = " + data.Credentials.AccessKeyId + " \n" + + "aws_secret_access_key = " + data.Credentials.SecretAccessKey + " \n" + + "aws_session_token = " + data.Credentials.SessionToken; + + // If there are no Role ARNs configured in the options panel, continue to create credentials file + // Otherwise, extend docContent with a profile for each specified ARN in the options panel + if (Object.keys(RoleArns).length == 0) { + console.log('Output maken'); + outputDocAsDownload(docContent); + } else { + var profileList = Object.keys(RoleArns); + console.log('INFO: Do additional assume-role for role -> ' + RoleArns[profileList[0]]); + assumeAdditionalRole(profileList, 0, data.Credentials.AccessKeyId, data.Credentials.SecretAccessKey, data.Credentials.SessionToken, docContent); + } + } + }); } // Will fetch additional STS keys for 1 role from the RoleArns dict // The assume-role API is called using the credentials (STS keys) fetched using the SAML claim. Basically the default profile. function assumeAdditionalRole(profileList, index, AccessKeyId, SecretAccessKey, SessionToken, docContent) { - // Set the fetched STS keys from the SAML reponse as credentials for doing the API call - var options = {'accessKeyId': AccessKeyId, 'secretAccessKey': SecretAccessKey, 'sessionToken': SessionToken}; - var sts = new AWS.STS(options); - // Set the parameters for the AssumeRole API call. Meaning: What role to assume - var params = { - RoleArn: RoleArns[profileList[index]], - RoleSessionName: profileList[index] - }; - // Call the API - sts.assumeRole(params, function(err, data) { - if (err) console.log(err, err.stack); // an error occurred - else { - docContent += " \n\n" + - "[" + profileList[index] + "] \n" + - "aws_access_key_id = " + data.Credentials.AccessKeyId + " \n" + - "aws_secret_access_key = " + data.Credentials.SecretAccessKey + " \n" + - "aws_session_token = " + data.Credentials.SessionToken; - } - // If there are more profiles/roles in the RoleArns dict, do another call of assumeAdditionalRole to extend the docContent with another profile - // Otherwise, this is the last profile/role in the RoleArns dict. Proceed to creating the credentials file - if (index < profileList.length - 1) { - console.log('INFO: Do additional assume-role for role -> ' + RoleArns[profileList[index + 1]]); - assumeAdditionalRole(profileList, index + 1, AccessKeyId, SecretAccessKey, SessionToken, docContent); - } else { - outputDocAsDownload(docContent); - } - }); + // Set the fetched STS keys from the SAML reponse as credentials for doing the API call + var options = {'accessKeyId': AccessKeyId, 'secretAccessKey': SecretAccessKey, 'sessionToken': SessionToken}; + var sts = new AWS.STS(options); + // Set the parameters for the AssumeRole API call. Meaning: What role to assume + var params = { + RoleArn: RoleArns[profileList[index]], + RoleSessionName: profileList[index] + }; + // Call the API + sts.assumeRole(params, function(err, data) { + if (err) console.log(err, err.stack); // an error occurred + else { + docContent += " \n\n" + + "[" + profileList[index] + "] \n" + + "aws_access_key_id = " + data.Credentials.AccessKeyId + " \n" + + "aws_secret_access_key = " + data.Credentials.SecretAccessKey + " \n" + + "aws_session_token = " + data.Credentials.SessionToken; + } + // If there are more profiles/roles in the RoleArns dict, do another call of assumeAdditionalRole to extend the docContent with another profile + // Otherwise, this is the last profile/role in the RoleArns dict. Proceed to creating the credentials file + if (index < profileList.length - 1) { + console.log('INFO: Do additional assume-role for role -> ' + RoleArns[profileList[index + 1]]); + assumeAdditionalRole(profileList, index + 1, AccessKeyId, SecretAccessKey, SessionToken, docContent); + } else { + outputDocAsDownload(docContent); + } + }); } @@ -192,9 +192,9 @@ function assumeAdditionalRole(profileList, index, AccessKeyId, SecretAccessKey, // Called from either extractPrincipalPlusRoleAndAssumeRole (if RoleArns dict is empty) // Otherwise called from assumeAdditionalRole as soon as all roles from RoleArns have been assumed function outputDocAsDownload(docContent) { - var doc = URL.createObjectURL( new Blob([docContent], {type: 'application/octet-binary'}) ); - // Triggers download of the generated file - chrome.downloads.download({ url: doc, filename: FileName, conflictAction: 'overwrite', saveAs: false }); + var doc = URL.createObjectURL( new Blob([docContent], {type: 'application/octet-binary'}) ); + // Triggers download of the generated file + chrome.downloads.download({ url: doc, filename: FileName, conflictAction: 'overwrite', saveAs: false }); } @@ -226,9 +226,9 @@ chrome.runtime.onMessage.addListener( function loadItemsFromStorage() { chrome.storage.sync.get({ FileName: 'credentials', - RoleArns: {} + RoleArns: {} }, function(items) { FileName = items.FileName; - RoleArns = items.RoleArns; + RoleArns = items.RoleArns; }); } diff --git a/manifest.json b/manifest.json index a027dd0..f15369c 100644 --- a/manifest.json +++ b/manifest.json @@ -5,10 +5,12 @@ "name": "SAML to AWS STS Keys Conversion", "description": "Generates file with AWS STS Keys after logging in to AWS webconsole using SSO (SAML 2.0). It leverages 'assumeRoleWithSAML' API.", "version": "2.1", - "icons": { "16": "icons/icon_16.png", - "32": "icons/icon_32.png", - "48": "icons/icon_48.png", - "128": "icons/icon_128.png" }, + "icons": { + "16": "icons/icon_16.png", + "32": "icons/icon_32.png", + "48": "icons/icon_48.png", + "128": "icons/icon_128.png" + }, "browser_action": { "default_icon": "icons/icon_32.png", "default_popup": "popup/popup.html" diff --git a/options/changelog.html b/options/changelog.html index e94ad27..e75d6bd 100644 --- a/options/changelog.html +++ b/options/changelog.html @@ -1,62 +1,62 @@ - - + + Changelog for 'SAML to AWS STS Keys Converter' -
-
Icon
-

Changelog for 'SAML to AWS STS Keys Converter'

-
-
+
+
Icon
+

Changelog for 'SAML to AWS STS Keys Converter'

+
+
-
-

Developed by prolane.org (Gerard Laan)

-

For help, please see README at the project page on Github.com

-

Click here to open the options panel for this plugin.

-
-
+
+

Developed by prolane.org (Gerard Laan)

+

For help, please see README at the project page on Github.com

+

Click here to open the options panel for this plugin.

+
+
-
-

2017-nov-01
v2.1

-
    -
  • Bug fix: Chrome 62 broke the extension. Special thanks for Brice for contributing. Thanks to Gijs for helping out with testing.
  • -
-
-
+
+

2017-nov-01
v2.1

+
    +
  • Bug fix: Chrome 62 broke the extension. Special thanks for Brice for contributing. Thanks to Gijs for helping out with testing.
  • +
+
+
-

2016-nov-21
v2.0

-
    -
  • Added functionality to specify Role ARN's in the options panel. This is meant for cross-account assume-role API calls. For each specified role temporary credentials will be fetched and added to the credentials file.
  • -
  • Updated 'AWS SDK for Javascript' library to latest version
  • -
  • Plugin now shows changelog to the user after the installation of new version
  • -
  • Options panel has a new look to improve readability
  • -
-
-
+

2016-nov-21
v2.0

+
    +
  • Added functionality to specify Role ARN's in the options panel. This is meant for cross-account assume-role API calls. For each specified role temporary credentials will be fetched and added to the credentials file.
  • +
  • Updated 'AWS SDK for Javascript' library to latest version
  • +
  • Plugin now shows changelog to the user after the installation of new version
  • +
  • Options panel has a new look to improve readability
  • +
+
+
-

2016-jul-24
v1.2

-
    -
  • Bug fix: when just 1 role in the SAML Assertion available now also works well
  • -
  • Now uses a regex to extract Role and Principal from SAML Assertion. This way it does not matter in what order the IDP adds the Role and Principle to the SAML Assertion.
  • -
-
-
+

2016-jul-24
v1.2

+
    +
  • Bug fix: when just 1 role in the SAML Assertion available now also works well
  • +
  • Now uses a regex to extract Role and Principal from SAML Assertion. This way it does not matter in what order the IDP adds the Role and Principle to the SAML Assertion.
  • +
+
+
-

2016-apr-11
v1.1

-
    -
  • Improved usability. No longer needed to manually specify PrincipalArn and RoleArn in options panel. Removed these options from the options panel. PrincipalArn and RoleArn is now parsed from the SAML Assertion itself.
  • -
-
-
+

2016-apr-11
v1.1

+
    +
  • Improved usability. No longer needed to manually specify PrincipalArn and RoleArn in options panel. Removed these options from the options panel. PrincipalArn and RoleArn is now parsed from the SAML Assertion itself.
  • +
+
+
-

2016-apr-04
v1.0

-
    -
  • Initial release
  • -
+

2016-apr-04
v1.0

+
    +
  • Initial release
  • +
-
+
diff --git a/options/options.css b/options/options.css index 4c290d9..0c78292 100644 --- a/options/options.css +++ b/options/options.css @@ -1,9 +1,9 @@ div { - padding: 1em; + padding: 1em; } .inline-div { - padding: 0em; + padding: 0em; } #divTitle { @@ -11,44 +11,44 @@ div { } #divHeader { - margin-left: 10px; + margin-left: 10px; } #divSettings { - width: 900px; - border-style: solid; - border-radius: 10px; - background-color: #fef6e0; + width: 900px; + border-style: solid; + border-radius: 10px; + background-color: #fef6e0; } .setting-part { - width: 876px; - border-radius: 10px; - background-color: #f1c47b; - margin-bottom: 14px; + width: 876px; + border-radius: 10px; + background-color: #f1c47b; + margin-bottom: 14px; } label { - font-weight: bold; - font-family: "Segoe UI", Tahoma, sans-serif; - font-size: 100%; - display:inline-block; - margin-bottom: 5px; + font-weight: bold; + font-family: "Segoe UI", Tahoma, sans-serif; + font-size: 100%; + display:inline-block; + margin-bottom: 5px; } #role_arns { - margin-top: 14px; + margin-top: 14px; } #status { - height: 15px; - margin-bottom: 5px; + height: 15px; + margin-bottom: 5px; } #divSave { - padding-top: 0em; + padding-top: 0em; } #divChangelog { - width: 600px; + width: 600px; } diff --git a/options/options.html b/options/options.html index 8fab72b..db228ef 100644 --- a/options/options.html +++ b/options/options.html @@ -1,41 +1,41 @@ - - + + Options page for 'SAML to AWS STS Keys Converter' -
-
Icon
-

Options page for 'SAML to AWS STS Keys Converter'

-
-
+
+
Icon
+

Options page for 'SAML to AWS STS Keys Converter'

+
+
-
-

Developed by prolane.org (Gerard Laan)

-

For help, please see README at the project page on Github.com

-
+
+

Developed by prolane.org (Gerard Laan)

+

For help, please see README at the project page on Github.com

+
-
-
- -
- -
+
+
+ +
+ +
-
-
- -
-
-
+
+
+ +
+
+
-
-
- -
-
- +
+
+ +
+
+ diff --git a/options/options.js b/options/options.js index 9975e48..d3b3677 100644 --- a/options/options.js +++ b/options/options.js @@ -7,19 +7,19 @@ function save_options() { var RoleArns = {}; // Iterate over all added profiles in the list $("input[id^='profile_']").each(function( index ) { - // Replace profile_ for arn_ to be able to get value of corresponding arn input field - var input_id_arn = $(this).attr('id').replace("profile", "arn"); - // Create key-value pair to add to RoleArns dictionary. - // Only add it to the dict if both profile and arn are not an empty string - if ($(this).val() != '' && $('#' + input_id_arn).val() != '') { - RoleArns[$(this).val()] = $('#' + input_id_arn).val(); - } + // Replace profile_ for arn_ to be able to get value of corresponding arn input field + var input_id_arn = $(this).attr('id').replace("profile", "arn"); + // Create key-value pair to add to RoleArns dictionary. + // Only add it to the dict if both profile and arn are not an empty string + if ($(this).val() != '' && $('#' + input_id_arn).val() != '') { + RoleArns[$(this).val()] = $('#' + input_id_arn).val(); + } }); // Do the actual saving into Chrome storage chrome.storage.sync.set({ FileName: FileName, - RoleArns: RoleArns + RoleArns: RoleArns }, function() { // Show 'Options saved' message to let user know options were saved. var status = document.getElementById('status'); @@ -38,66 +38,66 @@ function save_options() { // Restores state using the preferences stored in chrome.storage. function restore_options() { chrome.storage.sync.get({ - // Default values + // Default values FileName: 'credentials', - RoleArns: {} + RoleArns: {} }, function(items) { - // Set filename + // Set filename document.getElementById('FileName').value = items.FileName; - // Set the html for the Role ARN's Table - $("#role_arns").html('
ProfileARN of the role
'); - // For each profile/ARN pair add table row (showing the profile-name and ARN) - for (var profile in items.RoleArns){ - if (items.RoleArns.hasOwnProperty(profile)) { - addTableRow('#tr_header', profile, items.RoleArns[profile]); - } - } - // Add a blank table row if there are now current entries (So the user can easily add a new profile/ARN pair) - if (Object.keys(items.RoleArns).length == 0) { - addTableRow('#role_arns table tr:last', null, null); - } + // Set the html for the Role ARN's Table + $("#role_arns").html('
ProfileARN of the role
'); + // For each profile/ARN pair add table row (showing the profile-name and ARN) + for (var profile in items.RoleArns){ + if (items.RoleArns.hasOwnProperty(profile)) { + addTableRow('#tr_header', profile, items.RoleArns[profile]); + } + } + // Add a blank table row if there are now current entries (So the user can easily add a new profile/ARN pair) + if (Object.keys(items.RoleArns).length == 0) { + addTableRow('#role_arns table tr:last', null, null); + } }); } // Add a blank table row for the user to add a new profile/ARN pair function addTableRow(previousRowJquerySelector, profile, arn) { - // Generate random identifier for the to be added row - var newRowId = randomId(); - $(previousRowJquerySelector).after(getTableRowHtml(newRowId, profile, arn)); - // Add eventHandlers for the newly added buttons - $('#btn_add_' + newRowId).on("click", function() { - addTableRow('#tr_' + newRowId, null, null); - }); - $('#btn_del_' + newRowId).on("click", function() { - delTableRow('#tr_' + newRowId); - }); + // Generate random identifier for the to be added row + var newRowId = randomId(); + $(previousRowJquerySelector).after(getTableRowHtml(newRowId, profile, arn)); + // Add eventHandlers for the newly added buttons + $('#btn_add_' + newRowId).on("click", function() { + addTableRow('#tr_' + newRowId, null, null); + }); + $('#btn_del_' + newRowId).on("click", function() { + delTableRow('#tr_' + newRowId); + }); } // Remove table row function delTableRow(tableRowJquerySelector) { - // Remove table row from the DOM including bound events - $(tableRowJquerySelector).remove(); + // Remove table row from the DOM including bound events + $(tableRowJquerySelector).remove(); } // Generate HTML for a table row of the role_arns table function getTableRowHtml(tableRowId, profile, arn) { - var profileValue = ''; - var arnValue = ''; - // If profile and arn are not NULL, generate HTML value attribute - if (profile) {profileValue = 'value="' + profile + '"'}; - if (arn) {arnValue = 'value="' + arn + '"'}; - // Generate HTML for the row - var html = '\ - \ - \ - \ - \ - '; - return html; + var profileValue = ''; + var arnValue = ''; + // If profile and arn are not NULL, generate HTML value attribute + if (profile) {profileValue = 'value="' + profile + '"'}; + if (arn) {arnValue = 'value="' + arn + '"'}; + // Generate HTML for the row + var html = '\ + \ + \ + \ + \ + '; + return html; } function randomId() { - return Math.random().toString(36).substr(2, 8); + return Math.random().toString(36).substr(2, 8); } document.addEventListener('DOMContentLoaded', restore_options); diff --git a/popup/popup.html b/popup/popup.html index cfd0a47..09eeed2 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -3,29 +3,29 @@ SAML to AWS STS Keys Converter + div.status { + width:200px; + } + a.button { + -webkit-appearance: button; + -webkit-padding-after: 3px; + -webkit-padding-before: 3px; + -webkit-padding-end: 3px; + -webkit-padding-start: 3px; + text-decoration: none; + color: initial; + margin-top: 15px; + } +
-

SAML to AWS STS Keys

- -
- Options +

SAML to AWS STS Keys

+ +
+ Options
From b0fd9e8d17a4aefee11bb0ecb22aa461dfea5a0e Mon Sep 17 00:00:00 2001 From: Mark Biesheuvel Date: Fri, 24 Nov 2017 19:35:00 +0100 Subject: [PATCH 3/8] Define a code style with eslint and update code to comply with said code style --- .eslintrc.json | 33 ++++++++++++++++ background/script.js | 92 ++++++++++++++++++++++---------------------- options/options.js | 44 ++++++++++----------- popup/popup.js | 4 +- 4 files changed, 102 insertions(+), 71 deletions(-) create mode 100644 .eslintrc.json diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..54fab16 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "env": { + "browser": true, + "jquery": true, + "webextensions": true + }, + "globals": { + "ArrayBuffer": true, + "Uint8Array": true, + "DataView": true, + "AWS": true + }, + "extends": "eslint:recommended", + "rules": { + "no-console": "off", + "indent": [ + "error", + 2 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ] + } +} diff --git a/background/script.js b/background/script.js index 4ffdf58..e3d2abb 100644 --- a/background/script.js +++ b/background/script.js @@ -7,19 +7,19 @@ var RoleArns = {}; loadItemsFromStorage(); // Additionaly on start of the background process it is checked if this extension can be activated chrome.storage.sync.get({ - // The default is activated - Activated: true - }, function(item) { - if (item.Activated) addOnBeforeRequestEventListener(); + // The default is activated + Activated: true +}, function(item) { + if (item.Activated) addOnBeforeRequestEventListener(); }); // Additionaly on start of the background process it is checked if a new version of the plugin is installed. // If so, show the user the changelog // var thisVersion = chrome.runtime.getManifest().version; -chrome.runtime.onInstalled.addListener(function(details){ - if(details.reason == "install" || details.reason == "update"){ +chrome.runtime.onInstalled.addListener(function(details) { + if (details.reason == 'install' || details.reason == 'update') { // Open a new tab to show changelog html page - chrome.tabs.create({url: "../options/changelog.html"}); - } + chrome.tabs.create({url: '../options/changelog.html'}); + } }); @@ -28,12 +28,12 @@ chrome.runtime.onInstalled.addListener(function(details){ // This adds an EventListener for each request to signin.aws.amazon.com function addOnBeforeRequestEventListener() { if (chrome.webRequest.onBeforeRequest.hasListener(onBeforeRequestEvent)) { - console.log("ERROR: onBeforeRequest EventListener could not be added, because onBeforeRequest already has an EventListener."); + console.log('ERROR: onBeforeRequest EventListener could not be added, because onBeforeRequest already has an EventListener.'); } else { chrome.webRequest.onBeforeRequest.addListener( onBeforeRequestEvent, - {urls: ["https://signin.aws.amazon.com/saml"]}, - ["requestBody"] + {urls: ['https://signin.aws.amazon.com/saml']}, + ['requestBody'] ); } } @@ -52,7 +52,7 @@ function removeOnBeforeRequestEventListener() { // This function runs on each request to https://signin.aws.amazon.com/saml function onBeforeRequestEvent(details) { // Decode base64 SAML assertion in the request - var samlXmlDoc = ""; + var samlXmlDoc = ''; var formDataPayload = undefined; if (details.requestBody.formData) { samlXmlDoc = decodeURIComponent(unescape(window.atob(details.requestBody.formData.SAMLResponse[0]))); @@ -67,44 +67,42 @@ function onBeforeRequestEvent(details) { var combinedView = new DataView(combined); var decoder = new TextDecoder('utf-8'); formDataPayload = new URLSearchParams(decoder.decode(combinedView)); - samlXmlDoc = decodeURIComponent(unescape(window.atob(formDataPayload.get('SAMLResponse')))) + samlXmlDoc = decodeURIComponent(unescape(window.atob(formDataPayload.get('SAMLResponse')))); } // Convert XML String to DOM - parser = new DOMParser() - domDoc = parser.parseFromString(samlXmlDoc, "text/xml"); + var parser = new DOMParser(); + var domDoc = parser.parseFromString(samlXmlDoc, 'text/xml'); // Get a list of claims (= AWS roles) from the SAML assertion - var roleDomNodes = domDoc.querySelectorAll('[Name="https://aws.amazon.com/SAML/Attributes/Role"]')[0].childNodes + var roleDomNodes = domDoc.querySelectorAll('[Name="https://aws.amazon.com/SAML/Attributes/Role"]')[0].childNodes; // Parse the PrincipalArn and the RoleArn from the SAML Assertion. - var PrincipalArn = ''; - var RoleArn = ''; var SAMLAssertion = undefined; var hasRoleIndex = false; - var roleIndex = ""; + var roleIndex = ''; if (details.requestBody.formData) { SAMLAssertion = details.requestBody.formData.SAMLResponse[0]; - hasRoleIndex = "roleIndex" in details.requestBody.formData; + hasRoleIndex = 'roleIndex' in details.requestBody.formData; roleIndex = details.requestBody.formData.roleIndex[0]; } else if (formDataPayload) { SAMLAssertion = formDataPayload.get('SAMLResponse'); roleIndex = formDataPayload.get('roleIndex'); hasRoleIndex = roleIndex != undefined; } - // If there is more than 1 role in the claim, look at the 'roleIndex' HTTP Form data parameter to determine the role to assume + // If there is more than 1 role in the claim, look at the 'roleIndex' HTTP Form data parameter to determine the role to assume if (roleDomNodes.length > 1 && hasRoleIndex) { - for (i = 0; i < roleDomNodes.length; i++) { + for (var i = 0; i < roleDomNodes.length; i++) { var nodeValue = roleDomNodes[i].innerHTML; if (nodeValue.indexOf(roleIndex) > -1) { // This DomNode holdes the data for the role to assume. Use these details for the assumeRoleWithSAML API call - // The Role Attribute from the SAMLAssertion (DomNode) plus the SAMLAssertion itself is given as function arguments. - extractPrincipalPlusRoleAndAssumeRole(nodeValue, SAMLAssertion) + // The Role Attribute from the SAMLAssertion (DomNode) plus the SAMLAssertion itself is given as function arguments. + extractPrincipalPlusRoleAndAssumeRole(nodeValue, SAMLAssertion); } } } // If there is just 1 role in the claim there will be no 'roleIndex' in the form data. else if (roleDomNodes.length == 1) { // When there is just 1 role in the claim, use these details for the assumeRoleWithSAML API call - // The Role Attribute from the SAMLAssertion (DomNode) plus the SAMLAssertion itself is given as function arguments. - extractPrincipalPlusRoleAndAssumeRole(roleDomNodes[0].innerHTML, SAMLAssertion) + // The Role Attribute from the SAMLAssertion (DomNode) plus the SAMLAssertion itself is given as function arguments. + extractPrincipalPlusRoleAndAssumeRole(roleDomNodes[0].innerHTML, SAMLAssertion); } } @@ -120,8 +118,8 @@ function extractPrincipalPlusRoleAndAssumeRole(samlattribute, SAMLAssertion) { // Patern for Principal (SAML Provider) var rePrincipal = /arn:aws:iam:[^:]*:[0-9]+:saml-provider\/[^,]+/i; // Extraxt both regex patterns from SAMLAssertion attribute - RoleArn = samlattribute.match(reRole)[0]; - PrincipalArn = samlattribute.match(rePrincipal)[0]; + var RoleArn = samlattribute.match(reRole)[0]; + var PrincipalArn = samlattribute.match(rePrincipal)[0]; // Set parameters needed for assumeRoleWithSAML method var params = { @@ -129,16 +127,16 @@ function extractPrincipalPlusRoleAndAssumeRole(samlattribute, SAMLAssertion) { RoleArn: RoleArn, SAMLAssertion: SAMLAssertion, }; - // Call STS API from AWS + // Call STS API from AWS var sts = new AWS.STS(); sts.assumeRoleWithSAML(params, function(err, data) { if (err) console.log(err, err.stack); // an error occurred else { // On succesful API response create file with the STS keys - var docContent = "[default] \n" + - "aws_access_key_id = " + data.Credentials.AccessKeyId + " \n" + - "aws_secret_access_key = " + data.Credentials.SecretAccessKey + " \n" + - "aws_session_token = " + data.Credentials.SessionToken; + var docContent = '[default] \n' + + 'aws_access_key_id = ' + data.Credentials.AccessKeyId + ' \n' + + 'aws_secret_access_key = ' + data.Credentials.SecretAccessKey + ' \n' + + 'aws_session_token = ' + data.Credentials.SessionToken; // If there are no Role ARNs configured in the options panel, continue to create credentials file // Otherwise, extend docContent with a profile for each specified ARN in the options panel @@ -166,15 +164,15 @@ function assumeAdditionalRole(profileList, index, AccessKeyId, SecretAccessKey, RoleArn: RoleArns[profileList[index]], RoleSessionName: profileList[index] }; - // Call the API + // Call the API sts.assumeRole(params, function(err, data) { if (err) console.log(err, err.stack); // an error occurred else { - docContent += " \n\n" + - "[" + profileList[index] + "] \n" + - "aws_access_key_id = " + data.Credentials.AccessKeyId + " \n" + - "aws_secret_access_key = " + data.Credentials.SecretAccessKey + " \n" + - "aws_session_token = " + data.Credentials.SessionToken; + docContent += ' \n\n' + + '[' + profileList[index] + '] \n' + + 'aws_access_key_id = ' + data.Credentials.AccessKeyId + ' \n' + + 'aws_secret_access_key = ' + data.Credentials.SecretAccessKey + ' \n' + + 'aws_session_token = ' + data.Credentials.SessionToken; } // If there are more profiles/roles in the RoleArns dict, do another call of assumeAdditionalRole to extend the docContent with another profile // Otherwise, this is the last profile/role in the RoleArns dict. Proceed to creating the credentials file @@ -205,19 +203,19 @@ chrome.runtime.onMessage.addListener( function(request, sender, sendResponse) { // When the options are changed in the Options panel // these items need to be reloaded in this background process. - if (request.action == "reloadStorageItems") { + if (request.action == 'reloadStorageItems') { loadItemsFromStorage(); - sendResponse({message: "Storage items reloaded in background process."}); + sendResponse({message: 'Storage items reloaded in background process.'}); } // When the activation checkbox on the popup screen is checked/unchecked // the webRequest event listener needs to be added or removed. - if (request.action == "addWebRequestEventListener") { + if (request.action == 'addWebRequestEventListener') { addOnBeforeRequestEventListener(); - sendResponse({message: "webRequest EventListener added in background process."}); + sendResponse({message: 'webRequest EventListener added in background process.'}); } - if (request.action == "removeWebRequestEventListener") { + if (request.action == 'removeWebRequestEventListener') { removeOnBeforeRequestEventListener(); - sendResponse({message: "webRequest EventListener removed in background process."}); + sendResponse({message: 'webRequest EventListener removed in background process.'}); } }); @@ -226,9 +224,9 @@ chrome.runtime.onMessage.addListener( function loadItemsFromStorage() { chrome.storage.sync.get({ FileName: 'credentials', - RoleArns: {} + RoleArns: {} }, function(items) { FileName = items.FileName; - RoleArns = items.RoleArns; + RoleArns = items.RoleArns; }); } diff --git a/options/options.js b/options/options.js index d3b3677..7227055 100644 --- a/options/options.js +++ b/options/options.js @@ -6,9 +6,9 @@ function save_options() { // Get the Role_ARN's (Profile/ARNs pairs) entered by the user in the table var RoleArns = {}; // Iterate over all added profiles in the list - $("input[id^='profile_']").each(function( index ) { + $('input[id^=\'profile_\']').each(function() { // Replace profile_ for arn_ to be able to get value of corresponding arn input field - var input_id_arn = $(this).attr('id').replace("profile", "arn"); + var input_id_arn = $(this).attr('id').replace('profile', 'arn'); // Create key-value pair to add to RoleArns dictionary. // Only add it to the dict if both profile and arn are not an empty string if ($(this).val() != '' && $('#' + input_id_arn).val() != '') { @@ -19,7 +19,7 @@ function save_options() { // Do the actual saving into Chrome storage chrome.storage.sync.set({ FileName: FileName, - RoleArns: RoleArns + RoleArns: RoleArns }, function() { // Show 'Options saved' message to let user know options were saved. var status = document.getElementById('status'); @@ -30,7 +30,7 @@ function save_options() { }); // Notify background process of changed storage items. - chrome.runtime.sendMessage({action: "reloadStorageItems"}, function(response) { + chrome.runtime.sendMessage({action: 'reloadStorageItems'}, function(response) { console.log(response.message); }); } @@ -38,24 +38,24 @@ function save_options() { // Restores state using the preferences stored in chrome.storage. function restore_options() { chrome.storage.sync.get({ - // Default values + // Default values FileName: 'credentials', - RoleArns: {} + RoleArns: {} }, function(items) { - // Set filename + // Set filename document.getElementById('FileName').value = items.FileName; - // Set the html for the Role ARN's Table - $("#role_arns").html('
ProfileARN of the role
'); - // For each profile/ARN pair add table row (showing the profile-name and ARN) - for (var profile in items.RoleArns){ - if (items.RoleArns.hasOwnProperty(profile)) { - addTableRow('#tr_header', profile, items.RoleArns[profile]); + // Set the html for the Role ARN's Table + $('#role_arns').html('
ProfileARN of the role
'); + // For each profile/ARN pair add table row (showing the profile-name and ARN) + for (var profile in items.RoleArns) { + if (items.RoleArns.hasOwnProperty(profile)) { + addTableRow('#tr_header', profile, items.RoleArns[profile]); + } + } + // Add a blank table row if there are now current entries (So the user can easily add a new profile/ARN pair) + if (Object.keys(items.RoleArns).length == 0) { + addTableRow('#role_arns table tr:last', null, null); } - } - // Add a blank table row if there are now current entries (So the user can easily add a new profile/ARN pair) - if (Object.keys(items.RoleArns).length == 0) { - addTableRow('#role_arns table tr:last', null, null); - } }); } @@ -65,10 +65,10 @@ function addTableRow(previousRowJquerySelector, profile, arn) { var newRowId = randomId(); $(previousRowJquerySelector).after(getTableRowHtml(newRowId, profile, arn)); // Add eventHandlers for the newly added buttons - $('#btn_add_' + newRowId).on("click", function() { + $('#btn_add_' + newRowId).on('click', function() { addTableRow('#tr_' + newRowId, null, null); }); - $('#btn_del_' + newRowId).on("click", function() { + $('#btn_del_' + newRowId).on('click', function() { delTableRow('#tr_' + newRowId); }); } @@ -84,8 +84,8 @@ function getTableRowHtml(tableRowId, profile, arn) { var profileValue = ''; var arnValue = ''; // If profile and arn are not NULL, generate HTML value attribute - if (profile) {profileValue = 'value="' + profile + '"'}; - if (arn) {arnValue = 'value="' + arn + '"'}; + if (profile) {profileValue = 'value="' + profile + '"';} + if (arn) {arnValue = 'value="' + arn + '"';} // Generate HTML for the row var html = '\ \ diff --git a/popup/popup.js b/popup/popup.js index bc62fb8..66a56ec 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -24,11 +24,11 @@ function chkboxChangeHandler(event) { // Save checkbox state to chrome.storage chrome.storage.sync.set({ Activated: checkbox.checked }); // Default action for background process - var action = "removeWebRequestEventListener"; + var action = 'removeWebRequestEventListener'; // If the checkbox is checked, an EventListener needs to be started for // webRequests to signin.aws.amazon.com in the background process if (checkbox.checked) { - action = "addWebRequestEventListener"; + action = 'addWebRequestEventListener'; } chrome.runtime.sendMessage({action: action}, function(response) { console.log(response.message); From 51feca49d5feb4314e45107c6e83c0b429056e5d Mon Sep 17 00:00:00 2001 From: Mark Biesheuvel Date: Fri, 24 Nov 2017 20:24:50 +0100 Subject: [PATCH 4/8] Rewrite options.js to make better use of jQuery --- options/changelog.html | 10 ++-- options/options.css | 14 ++--- options/options.html | 28 ++++----- options/options.js | 125 +++++++++++++++++++---------------------- 4 files changed, 86 insertions(+), 91 deletions(-) diff --git a/options/changelog.html b/options/changelog.html index e75d6bd..abc4564 100644 --- a/options/changelog.html +++ b/options/changelog.html @@ -6,20 +6,20 @@ Changelog for 'SAML to AWS STS Keys Converter' -
-
Icon
-

Changelog for 'SAML to AWS STS Keys Converter'

+
+
Icon
+

Changelog for 'SAML to AWS STS Keys Converter'


-
+

Developed by prolane.org (Gerard Laan)

For help, please see README at the project page on Github.com

Click here to open the options panel for this plugin.


-
+

2017-nov-01
v2.1

  • Bug fix: Chrome 62 broke the extension. Special thanks for Brice for contributing. Thanks to Gijs for helping out with testing.
  • diff --git a/options/options.css b/options/options.css index 0c78292..702a921 100644 --- a/options/options.css +++ b/options/options.css @@ -6,15 +6,15 @@ div { padding: 0em; } -#divTitle { +.header { display: flex; } -#divHeader { +.header-title { margin-left: 10px; } -#divSettings { +.settings { width: 900px; border-style: solid; border-radius: 10px; @@ -36,19 +36,19 @@ label { margin-bottom: 5px; } -#role_arns { +.roles { margin-top: 14px; } -#status { +.status { height: 15px; margin-bottom: 5px; } -#divSave { +.save { padding-top: 0em; } -#divChangelog { +.changelog { width: 600px; } diff --git a/options/options.html b/options/options.html index db228ef..91189da 100644 --- a/options/options.html +++ b/options/options.html @@ -6,34 +6,36 @@ Options page for 'SAML to AWS STS Keys Converter' -
    -
    Icon
    -

    Options page for 'SAML to AWS STS Keys Converter'

    +
    +
    Icon
    +

    Options page for 'SAML to AWS STS Keys Converter'


    -
    +

    Developed by prolane.org (Gerard Laan)

    For help, please see README at the project page on Github.com

    -
    -
    +
    +
    -
    - +
    +
    -
    +

    -
    +
    + +
    -
    -
    - +
    + +
    diff --git a/options/options.js b/options/options.js index 7227055..08fa340 100644 --- a/options/options.js +++ b/options/options.js @@ -1,32 +1,27 @@ // Saves options to chrome.storage function save_options() { // Get the filename to be saved - var FileName = document.getElementById('FileName').value; + var filename = $('#filename').val(); // Get the Role_ARN's (Profile/ARNs pairs) entered by the user in the table - var RoleArns = {}; + var roles = {}; + // Iterate over all added profiles in the list - $('input[id^=\'profile_\']').each(function() { - // Replace profile_ for arn_ to be able to get value of corresponding arn input field - var input_id_arn = $(this).attr('id').replace('profile', 'arn'); - // Create key-value pair to add to RoleArns dictionary. - // Only add it to the dict if both profile and arn are not an empty string - if ($(this).val() != '' && $('#' + input_id_arn).val() != '') { - RoleArns[$(this).val()] = $('#' + input_id_arn).val(); - } + $('#roles').find('.role').each(function() { + + var profile = $(this).find('[name=profile]').val(); + var arn = $(this).find('[name=arn]').val(); + + roles[profile] = arn; }); // Do the actual saving into Chrome storage chrome.storage.sync.set({ - FileName: FileName, - RoleArns: RoleArns + FileName: filename, + RoleArns: roles }, function() { // Show 'Options saved' message to let user know options were saved. - var status = document.getElementById('status'); - status.textContent = 'Options saved.'; - setTimeout(function() { - status.textContent = ''; - }, 1500); + $('#status').text('Options saved.').show().delay(1500).fadeOut(); }); // Notify background process of changed storage items. @@ -42,63 +37,61 @@ function restore_options() { FileName: 'credentials', RoleArns: {} }, function(items) { + // Set filename - document.getElementById('FileName').value = items.FileName; - // Set the html for the Role ARN's Table - $('#role_arns').html('
    ProfileARN of the role
    '); - // For each profile/ARN pair add table row (showing the profile-name and ARN) - for (var profile in items.RoleArns) { - if (items.RoleArns.hasOwnProperty(profile)) { - addTableRow('#tr_header', profile, items.RoleArns[profile]); - } - } - // Add a blank table row if there are now current entries (So the user can easily add a new profile/ARN pair) - if (Object.keys(items.RoleArns).length == 0) { - addTableRow('#role_arns table tr:last', null, null); - } + $('#filename').val(items.FileName); + + // Create table header + var tr = $('') + .append($('').text('Profile')) + .append($('').text('ARN of the role')) + .append($('')) // Delete button + .append($('')); // Add button + + $('#roles').append(tr); + + // Add a table row for each profile/ARN pair + $.each(items.RoleArns, function(profile, arn) { + add_table_row(profile, arn); + }); + + // Add a blank table row to easily add a new profile + add_table_row(null, null); }); } -// Add a blank table row for the user to add a new profile/ARN pair -function addTableRow(previousRowJquerySelector, profile, arn) { - // Generate random identifier for the to be added row - var newRowId = randomId(); - $(previousRowJquerySelector).after(getTableRowHtml(newRowId, profile, arn)); +// Generate HTML for a table row of the roles table +function add_table_row(profile, arn) { + // Create input fields for profile name and arn + profile = $('').attr('name', 'profile').attr('size', 18).val(profile); + arn = $('').attr('name', 'arn').attr('size', 55).val(arn); + + // Create add/delete buttons + var del = $(' \ - \ - '; - return html; -} - -function randomId() { - return Math.random().toString(36).substr(2, 8); -} +$(function(){ + restore_options(); -document.addEventListener('DOMContentLoaded', restore_options); -document.getElementById('save').addEventListener('click', save_options); + $('#save-button').click(function(){ + save_options(); + }); +}); From 13b3765d3b0214c66e39833ca26dd007426d136d Mon Sep 17 00:00:00 2001 From: Mark Biesheuvel Date: Fri, 24 Nov 2017 20:36:48 +0100 Subject: [PATCH 5/8] Rewrite popup.js to make better use of jQuery --- background/background.html | 4 ++-- options/options.html | 3 ++- popup/popup.html | 3 ++- popup/popup.js | 36 ++++++++++++++++++------------------ 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/background/background.html b/background/background.html index be8a08a..1d553af 100644 --- a/background/background.html +++ b/background/background.html @@ -1,9 +1,9 @@ - - + + diff --git a/options/options.html b/options/options.html index 91189da..d994efb 100644 --- a/options/options.html +++ b/options/options.html @@ -1,7 +1,6 @@ - Options page for 'SAML to AWS STS Keys Converter' @@ -38,6 +37,8 @@
    + + diff --git a/popup/popup.html b/popup/popup.html index 09eeed2..a5c283a 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -23,11 +23,12 @@

    SAML to AWS STS Keys

    - +
    Options
    + diff --git a/popup/popup.js b/popup/popup.js index 66a56ec..932b8d4 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -3,34 +3,34 @@ // found in the LICENSE file. // On load of popup -document.addEventListener('DOMContentLoaded', function() { +$(function() { // On load of the popup screen check in Chrome's storage if the // 'SAML to AWS STS Keys' function is in a activated state or not. // Default value is 'activated' chrome.storage.sync.get({ Activated: true }, function(items) { - document.getElementById('chkboxactivated').checked = items.Activated; + $('#activated').prop('checked', items.Activated); }); // Add event handler to checkbox - document.getElementById('chkboxactivated').addEventListener('change', chkboxChangeHandler); -}); + $('#activated').on('change', function() { + var activated = $(this).prop('checked'); + // Save checkbox state to chrome.storage + chrome.storage.sync.set({Activated: activated}); + var message = {}; + // If the checkbox is checked, an EventListener needs to be started for + // webRequests to signin.aws.amazon.com in the background process + if (activated) { + message.action = 'addWebRequestEventListener'; + } else { + message.action = 'removeWebRequestEventListener'; + } -function chkboxChangeHandler(event) { - var checkbox = event.target; - // Save checkbox state to chrome.storage - chrome.storage.sync.set({ Activated: checkbox.checked }); - // Default action for background process - var action = 'removeWebRequestEventListener'; - // If the checkbox is checked, an EventListener needs to be started for - // webRequests to signin.aws.amazon.com in the background process - if (checkbox.checked) { - action = 'addWebRequestEventListener'; - } - chrome.runtime.sendMessage({action: action}, function(response) { - console.log(response.message); + chrome.runtime.sendMessage(message, function(response) { + console.log(response.message); + }); }); -} +}); From c4918bee344bee20a1b873bad8eb49676e045098 Mon Sep 17 00:00:00 2001 From: Mark Biesheuvel Date: Mon, 27 Nov 2017 11:54:07 +0100 Subject: [PATCH 6/8] Improve background.js by seperating logic/presentation and doing all additional assumeRoles async --- .eslintrc.json | 3 +- background/background.html | 1 + background/script.js | 291 +++++++++++++++++++------------------ options/options.js | 4 +- 4 files changed, 152 insertions(+), 147 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 54fab16..07d4147 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -8,7 +8,8 @@ "ArrayBuffer": true, "Uint8Array": true, "DataView": true, - "AWS": true + "AWS": true, + "Promise": true }, "extends": "eslint:recommended", "rules": { diff --git a/background/background.html b/background/background.html index 1d553af..7e564ac 100644 --- a/background/background.html +++ b/background/background.html @@ -3,6 +3,7 @@ + diff --git a/background/script.js b/background/script.js index e3d2abb..e4c87fc 100644 --- a/background/script.js +++ b/background/script.js @@ -1,232 +1,233 @@ // Global variables -var FileName = 'credentials'; -var RoleArns = {}; +var filename = 'credentials'; +var roles = {}; // When this background process starts, load variables from chrome storage // from saved Extension Options -loadItemsFromStorage(); +load_items_from_storage(); + // Additionaly on start of the background process it is checked if this extension can be activated chrome.storage.sync.get({ // The default is activated Activated: true }, function(item) { - if (item.Activated) addOnBeforeRequestEventListener(); + if (item.Activated) { + add_request_listener(); + } }); + // Additionaly on start of the background process it is checked if a new version of the plugin is installed. // If so, show the user the changelog // var thisVersion = chrome.runtime.getManifest().version; chrome.runtime.onInstalled.addListener(function(details) { - if (details.reason == 'install' || details.reason == 'update') { + if (details.reason === 'install' || details.reason === 'update') { // Open a new tab to show changelog html page chrome.tabs.create({url: '../options/changelog.html'}); } }); - - // Function to be called when this extension is activated. // This adds an EventListener for each request to signin.aws.amazon.com -function addOnBeforeRequestEventListener() { - if (chrome.webRequest.onBeforeRequest.hasListener(onBeforeRequestEvent)) { +function add_request_listener() { + if (chrome.webRequest.onBeforeRequest.hasListener(on_request_event)) { console.log('ERROR: onBeforeRequest EventListener could not be added, because onBeforeRequest already has an EventListener.'); } else { chrome.webRequest.onBeforeRequest.addListener( - onBeforeRequestEvent, + on_request_event, {urls: ['https://signin.aws.amazon.com/saml']}, ['requestBody'] ); } } - - // Function to be called when this extension is de-actived // by unchecking the activation checkbox on the popup page -function removeOnBeforeRequestEventListener() { - chrome.webRequest.onBeforeRequest.removeListener(onBeforeRequestEvent); +function remove_request_listener() { + chrome.webRequest.onBeforeRequest.removeListener(on_request_event); } - - // Callback function for the webRequest OnBeforeRequest EventListener // This function runs on each request to https://signin.aws.amazon.com/saml -function onBeforeRequestEvent(details) { +function on_request_event(details) { + var saml = extract_saml(details.requestBody); + + // Start out with a non-authenticated STS client + var sts_client = new AWS.STS(); + + // Assume role with SAML + assume_base_role(sts_client, saml.attribute, saml.assertion).then(function(base_profile) { + + // Update STS client to use base credentials + sts_client = new AWS.STS({ + accessKeyId: base_profile.credentials.AccessKeyId, + secretAccessKey: base_profile.credentials.SecretAccessKey, + sessionToken: base_profile.credentials.SessionToken + }); + + // Get credentials from all accounts + var promises = $.map(roles, function(role_arn, profile_name) { + return assume_additional_role(sts_client, role_arn, profile_name); + }); + + Promise.all(promises).then(function(profiles) { + profiles.unshift(base_profile); + download_file(profiles); + }); + }); +} + +function extract_saml(request) { // Decode base64 SAML assertion in the request - var samlXmlDoc = ''; - var formDataPayload = undefined; - if (details.requestBody.formData) { - samlXmlDoc = decodeURIComponent(unescape(window.atob(details.requestBody.formData.SAMLResponse[0]))); - } else if (details.requestBody.raw) { + var saml_assertion; + var has_role_index; + var role_index; + + if (request.formData) { + saml_assertion = request.formData.SAMLResponse[0]; + has_role_index = 'roleIndex' in request.formData; + if (has_role_index) { + role_index = request.formData.roleIndex[0]; + } + } else if (request.raw) { var combined = new ArrayBuffer(0); - details.requestBody.raw.forEach(function(element) { + request.raw.forEach(function(element) { var tmp = new Uint8Array(combined.byteLength + element.bytes.byteLength); tmp.set( new Uint8Array(combined), 0 ); tmp.set( new Uint8Array(element.bytes),combined.byteLength ); combined = tmp.buffer; }); - var combinedView = new DataView(combined); + var view = new DataView(combined); var decoder = new TextDecoder('utf-8'); - formDataPayload = new URLSearchParams(decoder.decode(combinedView)); - samlXmlDoc = decodeURIComponent(unescape(window.atob(formDataPayload.get('SAMLResponse')))); + var form_data = new URLSearchParams(decoder.decode(view)); + + saml_assertion = form_data.get('SAMLResponse'); + role_index = form_data.get('roleIndex'); + has_role_index = role_index != undefined; } + + var saml_xml = decodeURIComponent(unescape(atob(saml_assertion))); + // Convert XML String to DOM var parser = new DOMParser(); - var domDoc = parser.parseFromString(samlXmlDoc, 'text/xml'); // Get a list of claims (= AWS roles) from the SAML assertion - var roleDomNodes = domDoc.querySelectorAll('[Name="https://aws.amazon.com/SAML/Attributes/Role"]')[0].childNodes; - // Parse the PrincipalArn and the RoleArn from the SAML Assertion. - var SAMLAssertion = undefined; - var hasRoleIndex = false; - var roleIndex = ''; - if (details.requestBody.formData) { - SAMLAssertion = details.requestBody.formData.SAMLResponse[0]; - hasRoleIndex = 'roleIndex' in details.requestBody.formData; - roleIndex = details.requestBody.formData.roleIndex[0]; - } else if (formDataPayload) { - SAMLAssertion = formDataPayload.get('SAMLResponse'); - roleIndex = formDataPayload.get('roleIndex'); - hasRoleIndex = roleIndex != undefined; - } + var role_nodes = parser.parseFromString(saml_xml, 'text/xml') + .querySelectorAll('[Name="https://aws.amazon.com/SAML/Attributes/Role"]')[0].childNodes; + // If there is more than 1 role in the claim, look at the 'roleIndex' HTTP Form data parameter to determine the role to assume - if (roleDomNodes.length > 1 && hasRoleIndex) { - for (var i = 0; i < roleDomNodes.length; i++) { - var nodeValue = roleDomNodes[i].innerHTML; - if (nodeValue.indexOf(roleIndex) > -1) { + var saml_attribute; + if (role_nodes.length > 1 && has_role_index) { + for (var i = 0; i < role_nodes.length; i++) { + var node_value = role_nodes[i].innerHTML; + if (node_value.indexOf(role_index) > -1) { // This DomNode holdes the data for the role to assume. Use these details for the assumeRoleWithSAML API call // The Role Attribute from the SAMLAssertion (DomNode) plus the SAMLAssertion itself is given as function arguments. - extractPrincipalPlusRoleAndAssumeRole(nodeValue, SAMLAssertion); + saml_attribute = node_value; } } } // If there is just 1 role in the claim there will be no 'roleIndex' in the form data. - else if (roleDomNodes.length == 1) { + else if (role_nodes.length === 1) { // When there is just 1 role in the claim, use these details for the assumeRoleWithSAML API call // The Role Attribute from the SAMLAssertion (DomNode) plus the SAMLAssertion itself is given as function arguments. - extractPrincipalPlusRoleAndAssumeRole(roleDomNodes[0].innerHTML, SAMLAssertion); + saml_attribute = role_nodes[0].innerHTML; } -} - + return { + assertion: saml_assertion, + attribute: saml_attribute + }; +} -// Called from 'onBeforeRequestEvent' function. +// Called from 'on_request_event' function. // Gets a Role Attribute from a SAMLAssertion as function argument. Gets the SAMLAssertion as a second argument. // This function extracts the RoleArn and PrincipalArn (SAML-provider) // from this argument and uses it to call the AWS STS assumeRoleWithSAML API. -function extractPrincipalPlusRoleAndAssumeRole(samlattribute, SAMLAssertion) { - // Pattern for Role - var reRole = /arn:aws:iam:[^:]*:[0-9]+:role\/[^,]+/i; - // Patern for Principal (SAML Provider) - var rePrincipal = /arn:aws:iam:[^:]*:[0-9]+:saml-provider\/[^,]+/i; +function assume_base_role(sts_client, saml_attribute, saml_assertion) { // Extraxt both regex patterns from SAMLAssertion attribute - var RoleArn = samlattribute.match(reRole)[0]; - var PrincipalArn = samlattribute.match(rePrincipal)[0]; - - // Set parameters needed for assumeRoleWithSAML method - var params = { - PrincipalArn: PrincipalArn, - RoleArn: RoleArn, - SAMLAssertion: SAMLAssertion, - }; - // Call STS API from AWS - var sts = new AWS.STS(); - sts.assumeRoleWithSAML(params, function(err, data) { - if (err) console.log(err, err.stack); // an error occurred - else { - // On succesful API response create file with the STS keys - var docContent = '[default] \n' + - 'aws_access_key_id = ' + data.Credentials.AccessKeyId + ' \n' + - 'aws_secret_access_key = ' + data.Credentials.SecretAccessKey + ' \n' + - 'aws_session_token = ' + data.Credentials.SessionToken; - - // If there are no Role ARNs configured in the options panel, continue to create credentials file - // Otherwise, extend docContent with a profile for each specified ARN in the options panel - if (Object.keys(RoleArns).length == 0) { - console.log('Output maken'); - outputDocAsDownload(docContent); + var role_arn = saml_attribute.match(/arn:aws:iam:[^:]*:[0-9]+:role\/[^,]+/i)[0]; + var principal_arn = saml_attribute.match(/arn:aws:iam:[^:]*:[0-9]+:saml-provider\/[^,]+/i)[0]; + + return new Promise(function(resolve, reject) { + sts_client.assumeRoleWithSAML({ + PrincipalArn: principal_arn, + RoleArn: role_arn, + SAMLAssertion: saml_assertion, + }, function(err, data) { + if (err) { + reject(err); } else { - var profileList = Object.keys(RoleArns); - console.log('INFO: Do additional assume-role for role -> ' + RoleArns[profileList[0]]); - assumeAdditionalRole(profileList, 0, data.Credentials.AccessKeyId, data.Credentials.SecretAccessKey, data.Credentials.SessionToken, docContent); + resolve({ + name: 'default', + credentials: data.Credentials + }); } - } + }); }); } - // Will fetch additional STS keys for 1 role from the RoleArns dict // The assume-role API is called using the credentials (STS keys) fetched using the SAML claim. Basically the default profile. -function assumeAdditionalRole(profileList, index, AccessKeyId, SecretAccessKey, SessionToken, docContent) { - // Set the fetched STS keys from the SAML reponse as credentials for doing the API call - var options = {'accessKeyId': AccessKeyId, 'secretAccessKey': SecretAccessKey, 'sessionToken': SessionToken}; - var sts = new AWS.STS(options); +function assume_additional_role(sts_client, role_arn, name) { // Set the parameters for the AssumeRole API call. Meaning: What role to assume - var params = { - RoleArn: RoleArns[profileList[index]], - RoleSessionName: profileList[index] - }; - // Call the API - sts.assumeRole(params, function(err, data) { - if (err) console.log(err, err.stack); // an error occurred - else { - docContent += ' \n\n' + - '[' + profileList[index] + '] \n' + - 'aws_access_key_id = ' + data.Credentials.AccessKeyId + ' \n' + - 'aws_secret_access_key = ' + data.Credentials.SecretAccessKey + ' \n' + - 'aws_session_token = ' + data.Credentials.SessionToken; - } - // If there are more profiles/roles in the RoleArns dict, do another call of assumeAdditionalRole to extend the docContent with another profile - // Otherwise, this is the last profile/role in the RoleArns dict. Proceed to creating the credentials file - if (index < profileList.length - 1) { - console.log('INFO: Do additional assume-role for role -> ' + RoleArns[profileList[index + 1]]); - assumeAdditionalRole(profileList, index + 1, AccessKeyId, SecretAccessKey, SessionToken, docContent); - } else { - outputDocAsDownload(docContent); - } + return new Promise(function(resolve, reject) { + sts_client.assumeRole({ + RoleArn: role_arn, + RoleSessionName: name + }, function(err, data) { + if (err) { + reject(err); + } else { + resolve({ + name: name, + credentials: data.Credentials + }); + } + }); }); } - - -// Called from either extractPrincipalPlusRoleAndAssumeRole (if RoleArns dict is empty) -// Otherwise called from assumeAdditionalRole as soon as all roles from RoleArns have been assumed -function outputDocAsDownload(docContent) { - var doc = URL.createObjectURL( new Blob([docContent], {type: 'application/octet-binary'}) ); +function download_file(profiles) { + // Convert list of profiles to a credentials file + var content = $.map(profiles, function(profile) { + return '[' + profile.name + '] \n' + + 'aws_access_key_id = ' + profile.credentials.AccessKeyId + ' \n' + + 'aws_secret_access_key = ' + profile.credentials.SecretAccessKey + ' \n' + + 'aws_session_token = ' + profile.credentials.SessionToken; + }).join('\n\n'); + + var blob = new Blob([content], {type: 'application/octet-binary'}); + var url = URL.createObjectURL(blob); // Triggers download of the generated file - chrome.downloads.download({ url: doc, filename: FileName, conflictAction: 'overwrite', saveAs: false }); + chrome.downloads.download({ url: url, filename: filename, conflictAction: 'overwrite', saveAs: false }); } - - // This Listener receives messages from options.js and popup.js // Received messages are meant to affect the background process. -chrome.runtime.onMessage.addListener( - function(request, sender, sendResponse) { - // When the options are changed in the Options panel - // these items need to be reloaded in this background process. - if (request.action == 'reloadStorageItems') { - loadItemsFromStorage(); - sendResponse({message: 'Storage items reloaded in background process.'}); - } - // When the activation checkbox on the popup screen is checked/unchecked - // the webRequest event listener needs to be added or removed. - if (request.action == 'addWebRequestEventListener') { - addOnBeforeRequestEventListener(); - sendResponse({message: 'webRequest EventListener added in background process.'}); - } - if (request.action == 'removeWebRequestEventListener') { - removeOnBeforeRequestEventListener(); - sendResponse({message: 'webRequest EventListener removed in background process.'}); - } - }); - - +chrome.runtime.onMessage.addListener(function(request, sender, send_response) { + // When the options are changed in the Options panel + // these items need to be reloaded in this background process. + if (request.action === 'reloadStorageItems') { + load_items_from_storage(); + send_response({message: 'Storage items reloaded in background process.'}); + } + // When the activation checkbox on the popup screen is checked/unchecked + // the webRequest event listener needs to be added or removed. + if (request.action === 'addWebRequestEventListener') { + add_request_listener(); + send_response({message: 'webRequest EventListener added in background process.'}); + } + if (request.action === 'removeWebRequestEventListener') { + remove_request_listener(); + send_response({message: 'webRequest EventListener removed in background process.'}); + } +}); -function loadItemsFromStorage() { +function load_items_from_storage() { chrome.storage.sync.get({ FileName: 'credentials', RoleArns: {} }, function(items) { - FileName = items.FileName; - RoleArns = items.RoleArns; + filename = items.FileName; + roles = items.RoleArns; }); } diff --git a/options/options.js b/options/options.js index 08fa340..c54750b 100644 --- a/options/options.js +++ b/options/options.js @@ -12,7 +12,9 @@ function save_options() { var profile = $(this).find('[name=profile]').val(); var arn = $(this).find('[name=arn]').val(); - roles[profile] = arn; + if (profile && arn) { + roles[profile] = arn; + } }); // Do the actual saving into Chrome storage From d5b27db9627f08ab73c727845f1489ceae09d894 Mon Sep 17 00:00:00 2001 From: Mark Biesheuvel Date: Fri, 27 Jul 2018 13:39:15 +0200 Subject: [PATCH 7/8] Improve error handling --- background/script.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/background/script.js b/background/script.js index e4c87fc..e116a7e 100644 --- a/background/script.js +++ b/background/script.js @@ -57,6 +57,11 @@ function on_request_event(details) { // Assume role with SAML assume_base_role(sts_client, saml.attribute, saml.assertion).then(function(base_profile) { + if (base_profile.credentials === null) { + console.log('ERROR: unable to assume role from SAML') + return + } + // Update STS client to use base credentials sts_client = new AWS.STS({ accessKeyId: base_profile.credentials.AccessKeyId, @@ -154,7 +159,10 @@ function assume_base_role(sts_client, saml_attribute, saml_assertion) { SAMLAssertion: saml_assertion, }, function(err, data) { if (err) { - reject(err); + resolve({ + name: name, + credentials: null + }); } else { resolve({ name: 'default', @@ -172,10 +180,13 @@ function assume_additional_role(sts_client, role_arn, name) { return new Promise(function(resolve, reject) { sts_client.assumeRole({ RoleArn: role_arn, - RoleSessionName: name + RoleSessionName: 'samltoawsstskeys-' + name }, function(err, data) { if (err) { - reject(err); + resolve({ + name: name, + credentials: null + }); } else { resolve({ name: name, @@ -189,6 +200,9 @@ function assume_additional_role(sts_client, role_arn, name) { function download_file(profiles) { // Convert list of profiles to a credentials file var content = $.map(profiles, function(profile) { + if (profile.credentials === null) { + return '# Invalid credentials for ' + profile.name; + } return '[' + profile.name + '] \n' + 'aws_access_key_id = ' + profile.credentials.AccessKeyId + ' \n' + 'aws_secret_access_key = ' + profile.credentials.SecretAccessKey + ' \n' + From 30774946c12d190b78ffda0a2a69759de701c97d Mon Sep 17 00:00:00 2001 From: Mark Biesheuvel Date: Thu, 25 Oct 2018 11:56:58 +0200 Subject: [PATCH 8/8] Add session duration as setting --- background/script.js | 9 ++++++--- options/options.html | 12 ++++++++++++ options/options.js | 7 +++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/background/script.js b/background/script.js index e116a7e..cc9ac62 100644 --- a/background/script.js +++ b/background/script.js @@ -1,6 +1,5 @@ // Global variables -var filename = 'credentials'; -var roles = {}; +var filename, duration, roles; // When this background process starts, load variables from chrome storage // from saved Extension Options @@ -157,6 +156,7 @@ function assume_base_role(sts_client, saml_attribute, saml_assertion) { PrincipalArn: principal_arn, RoleArn: role_arn, SAMLAssertion: saml_assertion, + DurationSeconds: duration }, function(err, data) { if (err) { resolve({ @@ -180,7 +180,8 @@ function assume_additional_role(sts_client, role_arn, name) { return new Promise(function(resolve, reject) { sts_client.assumeRole({ RoleArn: role_arn, - RoleSessionName: 'samltoawsstskeys-' + name + RoleSessionName: 'samltoawsstskeys-' + name, + DurationSeconds: duration }, function(err, data) { if (err) { resolve({ @@ -239,9 +240,11 @@ chrome.runtime.onMessage.addListener(function(request, sender, send_response) { function load_items_from_storage() { chrome.storage.sync.get({ FileName: 'credentials', + SessionDuration: 3600, RoleArns: {} }, function(items) { filename = items.FileName; + duration = items.SessionDuration; roles = items.RoleArns; }); } diff --git a/options/options.html b/options/options.html index d994efb..73b07b7 100644 --- a/options/options.html +++ b/options/options.html @@ -23,6 +23,18 @@
    +
    + +
    + +
    +

    diff --git a/options/options.js b/options/options.js index c54750b..7895c1e 100644 --- a/options/options.js +++ b/options/options.js @@ -3,6 +3,8 @@ function save_options() { // Get the filename to be saved var filename = $('#filename').val(); + var duration = $('#duration').val(); + // Get the Role_ARN's (Profile/ARNs pairs) entered by the user in the table var roles = {}; @@ -20,6 +22,7 @@ function save_options() { // Do the actual saving into Chrome storage chrome.storage.sync.set({ FileName: filename, + SessionDuration: duration, RoleArns: roles }, function() { // Show 'Options saved' message to let user know options were saved. @@ -37,12 +40,16 @@ function restore_options() { chrome.storage.sync.get({ // Default values FileName: 'credentials', + SessionDuration: 3600, RoleArns: {} }, function(items) { // Set filename $('#filename').val(items.FileName); + // Set session duration + $('#duration').val(items.SessionDuration); + // Create table header var tr = $('') .append($('').text('Profile'))