From e2e3895a58e97cb30f51dc68dca532c6e5e5f084 Mon Sep 17 00:00:00 2001 From: Brian Rodgers Date: Fri, 6 Oct 2023 08:47:05 -0500 Subject: [PATCH 1/3] Added support for AWS Sig V4 Authentication --- .../RequestPane/Auth/AuthMode/index.js | 9 + .../Auth/AwsV4Auth/StyledWrapper.js | 16 ++ .../RequestPane/Auth/AwsV4Auth/index.js | 206 ++++++++++++++++++ .../src/components/RequestPane/Auth/index.js | 4 + .../ReduxStore/slices/collections/index.js | 4 + .../bruno-app/src/utils/collections/index.js | 4 + packages/bruno-electron/package.json | 2 + .../src/ipc/network/awsv4auth-helper.js | 49 +++++ .../bruno-electron/src/ipc/network/index.js | 6 + .../src/ipc/network/interpolate-vars.js | 10 + .../src/ipc/network/prepare-request.js | 29 ++- packages/bruno-lang/v2/src/bruToJson.js | 30 ++- packages/bruno-lang/v2/src/jsonToBru.js | 13 ++ .../bruno-lang/v2/tests/fixtures/request.bru | 9 + .../bruno-lang/v2/tests/fixtures/request.json | 8 + .../bruno-schema/src/collections/index.js | 14 +- 16 files changed, 402 insertions(+), 11 deletions(-) create mode 100644 packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/index.js create mode 100644 packages/bruno-electron/src/ipc/network/awsv4auth-helper.js diff --git a/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js b/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js index 43c90e2c..10130c3a 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/AuthMode/index.js @@ -35,6 +35,15 @@ const AuthMode = ({ item, collection }) => {
} placement="bottom-end"> +
{ + dropdownTippyRef.current.hide(); + onModeChange('awsv4'); + }} + > + AWS Sig v4 +
{ diff --git a/packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/StyledWrapper.js new file mode 100644 index 00000000..c2bb5d20 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/StyledWrapper.js @@ -0,0 +1,16 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + label { + font-size: 0.8125rem; + } + + .single-line-editor-wrapper { + padding: 0.15rem 0.4rem; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/index.js new file mode 100644 index 00000000..9ed29ac0 --- /dev/null +++ b/packages/bruno-app/src/components/RequestPane/Auth/AwsV4Auth/index.js @@ -0,0 +1,206 @@ +import React, { useState } from 'react'; +import get from 'lodash/get'; +import { useTheme } from 'providers/Theme'; +import { useDispatch } from 'react-redux'; +import SingleLineEditor from 'components/SingleLineEditor'; +import { updateAuth } from 'providers/ReduxStore/slices/collections'; +import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; +import StyledWrapper from './StyledWrapper'; +import { update } from 'lodash'; + +const AwsV4Auth = ({ onTokenChange, item, collection }) => { + const dispatch = useDispatch(); + const { storedTheme } = useTheme(); + + const awsv4Auth = item.draft ? get(item, 'draft.request.auth.awsv4', {}) : get(item, 'request.auth.awsv4', {}); + console.log('saved auth', awsv4Auth); + + const handleRun = () => dispatch(sendRequest(item, collection.uid)); + const handleSave = () => dispatch(saveRequest(item.uid, collection.uid)); + + const handleAccessKeyIdChange = (accessKeyId) => { + dispatch( + updateAuth({ + mode: 'awsv4', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + accessKeyId: accessKeyId, + secretAccessKey: awsv4Auth.secretAccessKey, + sessionToken: awsv4Auth.sessionToken, + service: awsv4Auth.service, + region: awsv4Auth.region, + profileName: awsv4Auth.profileName + } + }) + ); + }; + + const handleSecretAccessKeyChange = (secretAccessKey) => { + dispatch( + updateAuth({ + mode: 'awsv4', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + accessKeyId: awsv4Auth.accessKeyId, + secretAccessKey: secretAccessKey, + sessionToken: awsv4Auth.sessionToken, + service: awsv4Auth.service, + region: awsv4Auth.region, + profileName: awsv4Auth.profileName + } + }) + ); + }; + + const handleSessionTokenChange = (sessionToken) => { + dispatch( + updateAuth({ + mode: 'awsv4', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + accessKeyId: awsv4Auth.accessKeyId, + secretAccessKey: awsv4Auth.secretAccessKey, + sessionToken: sessionToken, + service: awsv4Auth.service, + region: awsv4Auth.region, + profileName: awsv4Auth.profileName + } + }) + ); + }; + + const handleServiceChange = (service) => { + dispatch( + updateAuth({ + mode: 'awsv4', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + accessKeyId: awsv4Auth.accessKeyId, + secretAccessKey: awsv4Auth.secretAccessKey, + sessionToken: awsv4Auth.sessionToken, + service: service, + region: awsv4Auth.region, + profileName: awsv4Auth.profileName + } + }) + ); + }; + + const handleRegionChange = (region) => { + dispatch( + updateAuth({ + mode: 'awsv4', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + accessKeyId: awsv4Auth.accessKeyId, + secretAccessKey: awsv4Auth.secretAccessKey, + sessionToken: awsv4Auth.sessionToken, + service: awsv4Auth.service, + region: region, + profileName: awsv4Auth.profileName + } + }) + ); + }; + + const handleProfileNameChange = (profileName) => { + dispatch( + updateAuth({ + mode: 'awsv4', + collectionUid: collection.uid, + itemUid: item.uid, + content: { + accessKeyId: awsv4Auth.accessKeyId, + secretAccessKey: awsv4Auth.secretAccessKey, + sessionToken: awsv4Auth.sessionToken, + service: awsv4Auth.service, + region: awsv4Auth.region, + profileName: profileName + } + }) + ); + }; + + return ( + + +
+ handleAccessKeyIdChange(val)} + onRun={handleRun} + collection={collection} + /> +
+ + +
+ handleSecretAccessKeyChange(val)} + onRun={handleRun} + collection={collection} + /> +
+ + +
+ handleSessionTokenChange(val)} + onRun={handleRun} + collection={collection} + /> +
+ + +
+ handleServiceChange(val)} + onRun={handleRun} + collection={collection} + /> +
+ + +
+ handleRegionChange(val)} + onRun={handleRun} + collection={collection} + /> +
+ + +
+ handleProfileNameChange(val)} + onRun={handleRun} + collection={collection} + /> +
+
+ ); +}; + +export default AwsV4Auth; diff --git a/packages/bruno-app/src/components/RequestPane/Auth/index.js b/packages/bruno-app/src/components/RequestPane/Auth/index.js index f07b80f9..ac194cd7 100644 --- a/packages/bruno-app/src/components/RequestPane/Auth/index.js +++ b/packages/bruno-app/src/components/RequestPane/Auth/index.js @@ -1,6 +1,7 @@ import React from 'react'; import get from 'lodash/get'; import AuthMode from './AuthMode'; +import AwsV4Auth from './AwsV4Auth'; import BearerAuth from './BearerAuth'; import BasicAuth from './BasicAuth'; import StyledWrapper from './StyledWrapper'; @@ -10,6 +11,9 @@ const Auth = ({ item, collection }) => { const getAuthView = () => { switch (authMode) { + case 'awsv4': { + return ; + } case 'basic': { return ; } diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 31cf6d6c..e91d2a0e 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -349,6 +349,10 @@ export const collectionsSlice = createSlice({ item.draft.request.auth = item.draft.request.auth || {}; switch (action.payload.mode) { + case 'awsv4': + item.draft.request.auth.mode = 'awsv4'; + item.draft.request.auth.awsv4 = action.payload.content; + break; case 'bearer': item.draft.request.auth.mode = 'bearer'; item.draft.request.auth.bearer = action.payload.content; diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index bf0fe687..9abe1a66 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -473,6 +473,10 @@ export const humanizeRequestBodyMode = (mode) => { export const humanizeRequestAuthMode = (mode) => { let label = 'No Auth'; switch (mode) { + case 'awsv4': { + label = 'AWS Sig V4'; + break; + } case 'basic': { label = 'Basic Auth'; break; diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json index 11f30a66..f25fc7f7 100644 --- a/packages/bruno-electron/package.json +++ b/packages/bruno-electron/package.json @@ -14,10 +14,12 @@ "test": "jest" }, "dependencies": { + "@aws-sdk/credential-providers": "^3.425.0", "@usebruno/js": "0.6.0", "@usebruno/lang": "0.5.0", "@usebruno/schema": "0.5.0", "about-window": "^1.15.2", + "aws4-axios": "^3.3.0", "axios": "^1.5.1", "chai": "^4.3.7", "chokidar": "^3.5.3", diff --git a/packages/bruno-electron/src/ipc/network/awsv4auth-helper.js b/packages/bruno-electron/src/ipc/network/awsv4auth-helper.js new file mode 100644 index 00000000..d5e4a896 --- /dev/null +++ b/packages/bruno-electron/src/ipc/network/awsv4auth-helper.js @@ -0,0 +1,49 @@ +const { fromIni } = require('@aws-sdk/credential-providers'); +const { aws4Interceptor } = require('aws4-axios'); + +function isStrPresent(str) { + return str && str !== '' && str !== 'undefined'; +} + +function addAwsV4Interceptor(axiosInstance, request) { + if (!request.awsv4config) { + console.warn('No Auth Config found!'); + return; + } + + const awsv4 = request.awsv4config; + if (!isStrPresent(awsv4.profileName) && (!isStrPresent(awsv4.accessKeyId) || !isStrPresent(awsv4.secretAccessKey))) { + console.warn('Required Auth Fields are not present'); + return; + } + + let credentials = { + accessKeyId: awsv4.accessKeyId, + secretAccessKey: awsv4.secretAccessKey, + sessionToken: awsv4.sessionToken + }; + + if (isStrPresent(awsv4.profileName)) { + try { + credentials = fromIni({ + profile: awsv4.profileName + }); + } catch { + console.error('Failed to fetch credentials from AWS profile.'); + } + } + + const interceptor = aws4Interceptor({ + options: { + region: awsv4.region, + service: awsv4.service + }, + credentials + }); + axiosInstance.interceptors.request.use(interceptor); + console.log('Added AWS V4 interceptor to axios.'); +} + +module.exports = { + addAwsV4Interceptor +}; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 2dad7286..36e7e6c2 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -17,6 +17,7 @@ const { getPreferences } = require('../../store/preferences'); const { getProcessEnvVars } = require('../../store/process-env'); const { getBrunoConfig } = require('../../store/bruno-config'); const { makeAxiosInstance } = require('./axios-instance'); +const { addAwsV4Interceptor } = require('./awsv4auth-helper'); // override the default escape function to prevent escaping Mustache.escape = function (value) { @@ -246,6 +247,11 @@ const registerNetworkIpc = (mainWindow) => { const axiosInstance = makeAxiosInstance(); + if (request.awsv4config) { + addAwsV4Interceptor(axiosInstance, request); + delete request.awsv4config; + } + /** @type {import('axios').AxiosResponse} */ const response = await axiosInstance(request); diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index b9261647..31d5cab5 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -121,6 +121,16 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces delete request.auth; } + // interpolate vars for aws sigv4 auth + if (request.awsv4config) { + request.awsv4config.accessKeyId = interpolate(request.awsv4config.accessKeyId) || ''; + request.awsv4config.secretAccessKey = interpolate(request.awsv4config.secretAccessKey) || ''; + request.awsv4config.sessionToken = interpolate(request.awsv4config.sessionToken) || ''; + request.awsv4config.service = interpolate(request.awsv4config.service) || ''; + request.awsv4config.region = interpolate(request.awsv4config.region) || ''; + request.awsv4config.profileName = interpolate(request.awsv4config.profileName) || ''; + } + return request; }; diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 922c9929..226cb86b 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -21,15 +21,26 @@ const prepareRequest = (request) => { // Authentication if (request.auth) { - if (request.auth.mode === 'basic') { - axiosRequest.auth = { - username: get(request, 'auth.basic.username'), - password: get(request, 'auth.basic.password') - }; - } - - if (request.auth.mode === 'bearer') { - axiosRequest.headers['authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`; + switch (request.auth.mode) { + case 'awsv4': + axiosRequest.awsv4config = { + accessKeyId: get(request, 'auth.awsv4.accessKeyId'), + secretAccessKey: get(request, 'auth.awsv4.secretAccessKey'), + sessionToken: get(request, 'auth.awsv4.sessionToken'), + service: get(request, 'auth.awsv4.service'), + region: get(request, 'auth.awsv4.region'), + profileName: get(request, 'auth.awsv4.profileName') + }; + break; + case 'basic': + axiosRequest.auth = { + username: get(request, 'auth.basic.username'), + password: get(request, 'auth.basic.password') + }; + break; + case 'bearer': + axiosRequest.headers['authorization'] = `Bearer ${get(request, 'auth.bearer.token')}`; + break; } } diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 576c58c2..8df77f1c 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -23,7 +23,7 @@ const { outdentString } = require('../../v1/src/utils'); */ const grammar = ohm.grammar(`Bru { BruFile = (meta | http | query | headers | auths | bodies | varsandassert | script | tests | docs)* - auths = authbasic | authbearer + auths = authawsv4 | authbasic | authbearer bodies = bodyjson | bodytext | bodyxml | bodygraphql | bodygraphqlvars | bodyforms | body bodyforms = bodyformurlencoded | bodymultipart @@ -76,6 +76,7 @@ const grammar = ohm.grammar(`Bru { varsres = "vars:post-response" dictionary assert = "assert" assertdictionary + authawsv4 = "auth:awsv4" dictionary authbasic = "auth:basic" dictionary authbearer = "auth:bearer" dictionary @@ -294,6 +295,33 @@ const sem = grammar.createSemantics().addAttribute('ast', { headers: mapPairListToKeyValPairs(dictionary.ast) }; }, + authawsv4(_1, dictionary) { + const auth = mapPairListToKeyValPairs(dictionary.ast, false); + const accessKeyIdKey = _.find(auth, { name: 'accessKeyId' }); + const secretAccessKeyKey = _.find(auth, { name: 'secretAccessKey' }); + const sessionTokenKey = _.find(auth, { name: 'sessionToken' }); + const serviceKey = _.find(auth, { name: 'service' }); + const regionKey = _.find(auth, { name: 'region' }); + const profileNameKey = _.find(auth, { name: 'profileName' }); + const accessKeyId = accessKeyIdKey ? accessKeyIdKey.value : ''; + const secretAccessKey = secretAccessKeyKey ? secretAccessKeyKey.value : ''; + const sessionToken = sessionTokenKey ? sessionTokenKey.value : ''; + const service = serviceKey ? serviceKey.value : ''; + const region = regionKey ? regionKey.value : ''; + const profileName = profileNameKey ? profileNameKey.value : ''; + return { + auth: { + awsv4: { + accessKeyId, + secretAccessKey, + sessionToken, + service, + region, + profileName + } + } + }; + }, authbasic(_1, dictionary) { const auth = mapPairListToKeyValPairs(dictionary.ast, false); const usernameKey = _.find(auth, { name: 'username' }); diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 8ef44d7a..0c4debb6 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -87,6 +87,19 @@ const jsonToBru = (json) => { bru += '\n}\n\n'; } + if (auth && auth.awsv4) { + bru += `auth:awsv4 { +${indentString(`accessKeyId: ${auth.awsv4.accessKeyId}`)} +${indentString(`secretAccessKey: ${auth.awsv4.secretAccessKey}`)} +${indentString(`sessionToken: ${auth.awsv4.sessionToken}`)} +${indentString(`service: ${auth.awsv4.service}`)} +${indentString(`region: ${auth.awsv4.region}`)} +${indentString(`profileName: ${auth.awsv4.profileName}`)} +} + +`; + } + if (auth && auth.basic) { bru += `auth:basic { ${indentString(`username: ${auth.basic.username}`)} diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru index c4ae4b05..56b35b81 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.bru +++ b/packages/bruno-lang/v2/tests/fixtures/request.bru @@ -22,6 +22,15 @@ headers { ~transaction-id: {{transactionId}} } +auth:awsv4 { + accessKeyId: A12345678 + secretAccessKey: thisisasecret + sessionToken: thisisafakesessiontoken + service: execute-api + region: us-east-1 + profileName: test_profile +} + auth:basic { username: john password: secret diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json index 7a00f5bb..151ba4fe 100644 --- a/packages/bruno-lang/v2/tests/fixtures/request.json +++ b/packages/bruno-lang/v2/tests/fixtures/request.json @@ -45,6 +45,14 @@ } ], "auth": { + "awsv4": { + "accessKeyId": "A12345678", + "secretAccessKey": "thisisasecret", + "sessionToken": "thisisafakesessiontoken", + "service": "execute-api", + "region": "us-east-1", + "profileName": "test_profile" + }, "basic": { "username": "john", "password": "secret" diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index 81cd2528..6c7c2223 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -69,6 +69,17 @@ const requestBodySchema = Yup.object({ .noUnknown(true) .strict(); +const authAwsV4Schema = Yup.object({ + accessKeyId: Yup.string().nullable(), + secretAccessKey: Yup.string().nullable(), + sessionToken: Yup.string().nullable(), + service: Yup.string().nullable(), + region: Yup.string().nullable(), + profileName: Yup.string().nullable() +}) + .noUnknown(true) + .strict(); + const authBasicSchema = Yup.object({ username: Yup.string().nullable(), password: Yup.string().nullable() @@ -83,7 +94,8 @@ const authBearerSchema = Yup.object({ .strict(); const authSchema = Yup.object({ - mode: Yup.string().oneOf(['none', 'basic', 'bearer']).required('mode is required'), + mode: Yup.string().oneOf(['none', 'awsv4', 'basic', 'bearer']).required('mode is required'), + awsv4: authAwsV4Schema.nullable(), basic: authBasicSchema.nullable(), bearer: authBearerSchema.nullable() }) From 295d82dca990fae728b9da437479beaaf7f7ca8c Mon Sep 17 00:00:00 2001 From: Brian Rodgers Date: Fri, 6 Oct 2023 08:47:05 -0500 Subject: [PATCH 2/3] Added support for AWS Sig V4 Authentication --- .../src/ipc/network/awsv4auth-helper.js | 47 +++++++++++-------- .../bruno-electron/src/ipc/network/index.js | 3 +- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/packages/bruno-electron/src/ipc/network/awsv4auth-helper.js b/packages/bruno-electron/src/ipc/network/awsv4auth-helper.js index d5e4a896..e3c99062 100644 --- a/packages/bruno-electron/src/ipc/network/awsv4auth-helper.js +++ b/packages/bruno-electron/src/ipc/network/awsv4auth-helper.js @@ -5,6 +5,24 @@ function isStrPresent(str) { return str && str !== '' && str !== 'undefined'; } +async function resolveCredentials(request) { + const awsv4 = request.awsv4config; + if (isStrPresent(awsv4.profileName)) { + try { + credentialsProvider = fromIni({ + profile: awsv4.profileName + }); + credentials = await credentialsProvider(); + awsv4.accessKeyId = credentials.accessKeyId; + awsv4.secretAccessKey = credentials.secretAccessKey; + awsv4.sessionToken = credentials.sessionToken; + } catch { + console.error('Failed to fetch credentials from AWS profile.'); + } + } + return awsv4; +} + function addAwsV4Interceptor(axiosInstance, request) { if (!request.awsv4config) { console.warn('No Auth Config found!'); @@ -12,38 +30,27 @@ function addAwsV4Interceptor(axiosInstance, request) { } const awsv4 = request.awsv4config; - if (!isStrPresent(awsv4.profileName) && (!isStrPresent(awsv4.accessKeyId) || !isStrPresent(awsv4.secretAccessKey))) { + if (!isStrPresent(awsv4.accessKeyId) || !isStrPresent(awsv4.secretAccessKey)) { console.warn('Required Auth Fields are not present'); return; } - let credentials = { - accessKeyId: awsv4.accessKeyId, - secretAccessKey: awsv4.secretAccessKey, - sessionToken: awsv4.sessionToken - }; - - if (isStrPresent(awsv4.profileName)) { - try { - credentials = fromIni({ - profile: awsv4.profileName - }); - } catch { - console.error('Failed to fetch credentials from AWS profile.'); - } - } - const interceptor = aws4Interceptor({ options: { region: awsv4.region, service: awsv4.service }, - credentials + credentials: { + accessKeyId: awsv4.accessKeyId, + secretAccessKey: awsv4.secretAccessKey, + sessionToken: awsv4.sessionToken + } }); + axiosInstance.interceptors.request.use(interceptor); - console.log('Added AWS V4 interceptor to axios.'); } module.exports = { - addAwsV4Interceptor + addAwsV4Interceptor, + resolveCredentials }; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 36e7e6c2..233f1550 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -17,7 +17,7 @@ const { getPreferences } = require('../../store/preferences'); const { getProcessEnvVars } = require('../../store/process-env'); const { getBrunoConfig } = require('../../store/bruno-config'); const { makeAxiosInstance } = require('./axios-instance'); -const { addAwsV4Interceptor } = require('./awsv4auth-helper'); +const { addAwsV4Interceptor, resolveCredentials } = require('./awsv4auth-helper'); // override the default escape function to prevent escaping Mustache.escape = function (value) { @@ -248,6 +248,7 @@ const registerNetworkIpc = (mainWindow) => { const axiosInstance = makeAxiosInstance(); if (request.awsv4config) { + request.awsv4config = await resolveCredentials(request); addAwsV4Interceptor(axiosInstance, request); delete request.awsv4config; } From ba8bfc6d17d044468e45e0ae73ab0214f5b064cb Mon Sep 17 00:00:00 2001 From: Brian Rodgers Date: Wed, 11 Oct 2023 17:00:47 -0700 Subject: [PATCH 3/3] Added AWS SigV4 to collection auth --- .../CollectionSettings/Auth/AuthMode/index.js | 9 + .../Auth/AwsV4Auth/StyledWrapper.js | 16 ++ .../Auth/AwsV4Auth/index.js | 192 ++++++++++++++++++ .../CollectionSettings/Auth/index.js | 4 + .../ReduxStore/slices/collections/index.js | 4 + .../src/ipc/network/prepare-request.js | 29 ++- .../bruno-lang/v2/src/collectionBruToJson.js | 30 ++- .../bruno-lang/v2/src/jsonToCollectionBru.js | 13 ++ 8 files changed, 287 insertions(+), 10 deletions(-) create mode 100644 packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/index.js diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js index 2e0abe57..5cf3a5f7 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/AuthMode/index.js @@ -34,6 +34,15 @@ const AuthMode = ({ collection }) => {
} placement="bottom-end"> +
{ + dropdownTippyRef.current.hide(); + onModeChange('awsv4'); + }} + > + AWS Sig v4 +
{ diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/StyledWrapper.js b/packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/StyledWrapper.js new file mode 100644 index 00000000..c2bb5d20 --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/StyledWrapper.js @@ -0,0 +1,16 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + label { + font-size: 0.8125rem; + } + + .single-line-editor-wrapper { + padding: 0.15rem 0.4rem; + border-radius: 3px; + border: solid 1px ${(props) => props.theme.input.border}; + background-color: ${(props) => props.theme.input.bg}; + } +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/index.js new file mode 100644 index 00000000..1fe35eea --- /dev/null +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/AwsV4Auth/index.js @@ -0,0 +1,192 @@ +import React from 'react'; +import get from 'lodash/get'; +import { useTheme } from 'providers/Theme'; +import { useDispatch } from 'react-redux'; +import SingleLineEditor from 'components/SingleLineEditor'; +import { updateCollectionAuth } from 'providers/ReduxStore/slices/collections'; +import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions'; +import StyledWrapper from './StyledWrapper'; + +const AwsV4Auth = ({ collection }) => { + const dispatch = useDispatch(); + const { storedTheme } = useTheme(); + + const awsv4Auth = get(collection, 'root.request.auth.awsv4', {}); + console.log('saved auth', awsv4Auth); + + const handleSave = () => dispatch(saveCollectionRoot(collection.uid)); + + const handleAccessKeyIdChange = (accessKeyId) => { + dispatch( + updateCollectionAuth({ + mode: 'awsv4', + collectionUid: collection.uid, + content: { + accessKeyId: accessKeyId, + secretAccessKey: awsv4Auth.secretAccessKey, + sessionToken: awsv4Auth.sessionToken, + service: awsv4Auth.service, + region: awsv4Auth.region, + profileName: awsv4Auth.profileName + } + }) + ); + }; + + const handleSecretAccessKeyChange = (secretAccessKey) => { + dispatch( + updateCollectionAuth({ + mode: 'awsv4', + collectionUid: collection.uid, + content: { + accessKeyId: awsv4Auth.accessKeyId, + secretAccessKey: secretAccessKey, + sessionToken: awsv4Auth.sessionToken, + service: awsv4Auth.service, + region: awsv4Auth.region, + profileName: awsv4Auth.profileName + } + }) + ); + }; + + const handleSessionTokenChange = (sessionToken) => { + dispatch( + updateCollectionAuth({ + mode: 'awsv4', + collectionUid: collection.uid, + content: { + accessKeyId: awsv4Auth.accessKeyId, + secretAccessKey: awsv4Auth.secretAccessKey, + sessionToken: sessionToken, + service: awsv4Auth.service, + region: awsv4Auth.region, + profileName: awsv4Auth.profileName + } + }) + ); + }; + + const handleServiceChange = (service) => { + dispatch( + updateCollectionAuth({ + mode: 'awsv4', + collectionUid: collection.uid, + content: { + accessKeyId: awsv4Auth.accessKeyId, + secretAccessKey: awsv4Auth.secretAccessKey, + sessionToken: awsv4Auth.sessionToken, + service: service, + region: awsv4Auth.region, + profileName: awsv4Auth.profileName + } + }) + ); + }; + + const handleRegionChange = (region) => { + dispatch( + updateCollectionAuth({ + mode: 'awsv4', + collectionUid: collection.uid, + content: { + accessKeyId: awsv4Auth.accessKeyId, + secretAccessKey: awsv4Auth.secretAccessKey, + sessionToken: awsv4Auth.sessionToken, + service: awsv4Auth.service, + region: region, + profileName: awsv4Auth.profileName + } + }) + ); + }; + + const handleProfileNameChange = (profileName) => { + dispatch( + updateCollectionAuth({ + mode: 'awsv4', + collectionUid: collection.uid, + content: { + accessKeyId: awsv4Auth.accessKeyId, + secretAccessKey: awsv4Auth.secretAccessKey, + sessionToken: awsv4Auth.sessionToken, + service: awsv4Auth.service, + region: awsv4Auth.region, + profileName: profileName + } + }) + ); + }; + + return ( + + +
+ handleAccessKeyIdChange(val)} + collection={collection} + /> +
+ + +
+ handleSecretAccessKeyChange(val)} + collection={collection} + /> +
+ + +
+ handleSessionTokenChange(val)} + collection={collection} + /> +
+ + +
+ handleServiceChange(val)} + collection={collection} + /> +
+ + +
+ handleRegionChange(val)} + collection={collection} + /> +
+ + +
+ handleProfileNameChange(val)} + collection={collection} + /> +
+
+ ); +}; + +export default AwsV4Auth; diff --git a/packages/bruno-app/src/components/CollectionSettings/Auth/index.js b/packages/bruno-app/src/components/CollectionSettings/Auth/index.js index fe2fd5b3..d9e80358 100644 --- a/packages/bruno-app/src/components/CollectionSettings/Auth/index.js +++ b/packages/bruno-app/src/components/CollectionSettings/Auth/index.js @@ -2,6 +2,7 @@ import React from 'react'; import get from 'lodash/get'; import { useDispatch } from 'react-redux'; import AuthMode from './AuthMode'; +import AwsV4Auth from './AwsV4Auth'; import BearerAuth from './BearerAuth'; import BasicAuth from './BasicAuth'; import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions'; @@ -15,6 +16,9 @@ const Auth = ({ collection }) => { const getAuthView = () => { switch (authMode) { + case 'awsv4': { + return ; + } case 'basic': { return ; } diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index f8595daa..8140dcf7 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -969,6 +969,10 @@ export const collectionsSlice = createSlice({ if (collection) { switch (action.payload.mode) { + case 'awsv4': + set(collection, 'root.request.auth.awsv4', action.payload.content); + console.log('set auth awsv4', action.payload.content); + break; case 'bearer': set(collection, 'root.request.auth.bearer', action.payload.content); break; diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index f0853620..3beab80f 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -8,15 +8,26 @@ const decomment = require('decomment'); const setAuthHeaders = (axiosRequest, request, collectionRoot) => { const collectionAuth = get(collectionRoot, 'request.auth'); if (collectionAuth) { - if (collectionAuth.mode === 'basic') { - axiosRequest.auth = { - username: get(collectionAuth, 'basic.username'), - password: get(collectionAuth, 'basic.password') - }; - } - - if (collectionAuth.mode === 'bearer') { - axiosRequest.headers['authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`; + switch (collectionAuth.mode) { + case 'awsv4': + axiosRequest.awsv4config = { + accessKeyId: get(collectionAuth, 'awsv4.accessKeyId'), + secretAccessKey: get(collectionAuth, 'awsv4.secretAccessKey'), + sessionToken: get(collectionAuth, 'awsv4.sessionToken'), + service: get(collectionAuth, 'awsv4.service'), + region: get(collectionAuth, 'awsv4.region'), + profileName: get(collectionAuth, 'awsv4.profileName') + }; + break; + case 'basic': + axiosRequest.auth = { + username: get(collectionAuth, 'basic.username'), + password: get(collectionAuth, 'basic.password') + }; + break; + case 'bearer': + axiosRequest.headers['authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`; + break; } } diff --git a/packages/bruno-lang/v2/src/collectionBruToJson.js b/packages/bruno-lang/v2/src/collectionBruToJson.js index d78f752c..4569736f 100644 --- a/packages/bruno-lang/v2/src/collectionBruToJson.js +++ b/packages/bruno-lang/v2/src/collectionBruToJson.js @@ -4,7 +4,7 @@ const { outdentString } = require('../../v1/src/utils'); const grammar = ohm.grammar(`Bru { BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)* - auths = authbasic | authbearer + auths = authawsv4 | authbasic | authbearer nl = "\\r"? "\\n" st = " " | "\\t" @@ -38,6 +38,7 @@ const grammar = ohm.grammar(`Bru { varsreq = "vars:pre-request" dictionary varsres = "vars:post-response" dictionary + authawsv4 = "auth:awsv4" dictionary authbasic = "auth:basic" dictionary authbearer = "auth:bearer" dictionary @@ -171,6 +172,33 @@ const sem = grammar.createSemantics().addAttribute('ast', { headers: mapPairListToKeyValPairs(dictionary.ast) }; }, + authawsv4(_1, dictionary) { + const auth = mapPairListToKeyValPairs(dictionary.ast, false); + const accessKeyIdKey = _.find(auth, { name: 'accessKeyId' }); + const secretAccessKeyKey = _.find(auth, { name: 'secretAccessKey' }); + const sessionTokenKey = _.find(auth, { name: 'sessionToken' }); + const serviceKey = _.find(auth, { name: 'service' }); + const regionKey = _.find(auth, { name: 'region' }); + const profileNameKey = _.find(auth, { name: 'profileName' }); + const accessKeyId = accessKeyIdKey ? accessKeyIdKey.value : ''; + const secretAccessKey = secretAccessKeyKey ? secretAccessKeyKey.value : ''; + const sessionToken = sessionTokenKey ? sessionTokenKey.value : ''; + const service = serviceKey ? serviceKey.value : ''; + const region = regionKey ? regionKey.value : ''; + const profileName = profileNameKey ? profileNameKey.value : ''; + return { + auth: { + awsv4: { + accessKeyId, + secretAccessKey, + sessionToken, + service, + region, + profileName + } + } + }; + }, authbasic(_1, dictionary) { const auth = mapPairListToKeyValPairs(dictionary.ast, false); const usernameKey = _.find(auth, { name: 'username' }); diff --git a/packages/bruno-lang/v2/src/jsonToCollectionBru.js b/packages/bruno-lang/v2/src/jsonToCollectionBru.js index 57a5ea7b..ea928a68 100644 --- a/packages/bruno-lang/v2/src/jsonToCollectionBru.js +++ b/packages/bruno-lang/v2/src/jsonToCollectionBru.js @@ -72,6 +72,19 @@ const jsonToCollectionBru = (json) => { ${indentString(`mode: ${auth.mode}`)} } +`; + } + + if (auth && auth.awsv4) { + bru += `auth:awsv4 { +${indentString(`accessKeyId: ${auth.awsv4.accessKeyId}`)} +${indentString(`secretAccessKey: ${auth.awsv4.secretAccessKey}`)} +${indentString(`sessionToken: ${auth.awsv4.sessionToken}`)} +${indentString(`service: ${auth.awsv4.service}`)} +${indentString(`region: ${auth.awsv4.region}`)} +${indentString(`profileName: ${auth.awsv4.profileName}`)} +} + `; }