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 43c90e2ce..10130c3a9 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 000000000..c2bb5d207 --- /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 000000000..9ed29ac07 --- /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 f07b80f95..ac194cd78 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 31cf6d6ca..e91d2a0e0 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 bf0fe6879..9abe1a66f 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 11f30a66b..f25fc7f7d 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 000000000..d5e4a896f --- /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 2dad72860..36e7e6c2c 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 b92616478..31d5cab55 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 922c9929e..226cb86bd 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 576c58c24..8df77f1cd 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 8ef44d7ad..0c4debb63 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 c4ae4b058..56b35b81b 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 7a00f5bb3..151ba4fee 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 81cd2528b..6c7c22230 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() })