feat: secret variables in environments (#3779)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
This commit is contained in:
@@ -30,6 +30,13 @@ import { HoppRESTResponse } from "./types/HoppRESTResponse"
|
||||
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
|
||||
import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
|
||||
import { isJSONContentType } from "./utils/contenttypes"
|
||||
import {
|
||||
SecretEnvironmentService,
|
||||
SecretVariable,
|
||||
} from "~/services/secret-environment.service"
|
||||
import { getService } from "~/modules/dioc"
|
||||
|
||||
const secretEnvironmentService = getService(SecretEnvironmentService)
|
||||
|
||||
const getTestableBody = (
|
||||
res: HoppRESTResponse & { type: "success" | "fail" }
|
||||
@@ -58,15 +65,63 @@ const getTestableBody = (
|
||||
return x
|
||||
}
|
||||
|
||||
const combineEnvVariables = (env: {
|
||||
const combineEnvVariables = (envs: {
|
||||
global: Environment["variables"]
|
||||
selected: Environment["variables"]
|
||||
}) => [...env.selected, ...env.global]
|
||||
}) => [...envs.selected, ...envs.global]
|
||||
|
||||
export const executedResponses$ = new Subject<
|
||||
HoppRESTResponse & { type: "success" | "fail " }
|
||||
>()
|
||||
|
||||
/**
|
||||
* Used to update the environment schema with the secret variables
|
||||
* and store the secret variable values in the secret environment service
|
||||
* @param envs The environment variables to update
|
||||
* @param type Whether the environment variables are global or selected
|
||||
* @returns the updated environment variables
|
||||
*/
|
||||
const updateEnvironmentsWithSecret = (
|
||||
envs: Environment["variables"] &
|
||||
{
|
||||
secret: true
|
||||
value: string | undefined
|
||||
key: string
|
||||
}[],
|
||||
type: "global" | "selected"
|
||||
) => {
|
||||
const currentEnvID =
|
||||
type === "selected" ? getCurrentEnvironment().id : "Global"
|
||||
|
||||
const updatedSecretEnvironments: SecretVariable[] = []
|
||||
|
||||
const updatedEnv = pipe(
|
||||
envs,
|
||||
A.mapWithIndex((index, e) => {
|
||||
if (e.secret) {
|
||||
updatedSecretEnvironments.push({
|
||||
key: e.key,
|
||||
value: e.value ?? "",
|
||||
varIndex: index,
|
||||
})
|
||||
|
||||
// delete the value from the environment
|
||||
// so that it doesn't get saved in the environment
|
||||
delete e.value
|
||||
return e
|
||||
}
|
||||
return e
|
||||
})
|
||||
)
|
||||
if (currentEnvID) {
|
||||
secretEnvironmentService.addSecretEnvironment(
|
||||
currentEnvID,
|
||||
updatedSecretEnvironments
|
||||
)
|
||||
}
|
||||
return updatedEnv
|
||||
}
|
||||
|
||||
export function runRESTRequest$(
|
||||
tab: Ref<HoppTab<HoppRESTDocument>>
|
||||
): [
|
||||
@@ -154,15 +209,36 @@ export function runRESTRequest$(
|
||||
)
|
||||
|
||||
if (E.isRight(runResult)) {
|
||||
const updatedGlobalEnvVariables = updateEnvironmentsWithSecret(
|
||||
cloneDeep(runResult.right.envs.global),
|
||||
"global"
|
||||
)
|
||||
|
||||
const updatedSelectedEnvVariables = updateEnvironmentsWithSecret(
|
||||
cloneDeep(runResult.right.envs.selected),
|
||||
"selected"
|
||||
)
|
||||
|
||||
// set the response in the tab so that multiple tabs can run request simultaneously
|
||||
tab.value.document.response = res
|
||||
|
||||
tab.value.document.testResults = translateToSandboxTestResults(
|
||||
runResult.right
|
||||
const updatedRunResult = {
|
||||
...runResult.right,
|
||||
envs: {
|
||||
global: updatedGlobalEnvVariables,
|
||||
selected: updatedSelectedEnvVariables,
|
||||
},
|
||||
}
|
||||
|
||||
tab.value.document.testResults =
|
||||
translateToSandboxTestResults(updatedRunResult)
|
||||
|
||||
setGlobalEnvVariables(
|
||||
updateEnvironmentsWithSecret(
|
||||
runResult.right.envs.global,
|
||||
"global"
|
||||
)
|
||||
)
|
||||
|
||||
setGlobalEnvVariables(runResult.right.envs.global)
|
||||
|
||||
if (
|
||||
environmentsStore.value.selectedEnvironmentIndex.type === "MY_ENV"
|
||||
) {
|
||||
@@ -173,8 +249,10 @@ export function runRESTRequest$(
|
||||
updateEnvironment(
|
||||
environmentsStore.value.selectedEnvironmentIndex.index,
|
||||
{
|
||||
...env,
|
||||
variables: runResult.right.envs.selected,
|
||||
name: env.name,
|
||||
v: 1,
|
||||
id: env.id ?? "",
|
||||
variables: updatedRunResult.envs.selected,
|
||||
}
|
||||
)
|
||||
} else if (
|
||||
@@ -186,7 +264,7 @@ export function runRESTRequest$(
|
||||
})
|
||||
pipe(
|
||||
updateTeamEnvironment(
|
||||
JSON.stringify(runResult.right.envs.selected),
|
||||
JSON.stringify(updatedRunResult.envs.selected),
|
||||
environmentsStore.value.selectedEnvironmentIndex.teamEnvID,
|
||||
env.name
|
||||
)
|
||||
@@ -275,7 +353,6 @@ function translateToSandboxTestResults(
|
||||
|
||||
const globals = cloneDeep(getGlobalVariables())
|
||||
const env = getCurrentEnvironment()
|
||||
|
||||
return {
|
||||
description: "",
|
||||
expectResults: testDesc.tests.expectResults,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { Ref, onBeforeUnmount, onMounted, reactive, watch } from "vue"
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
import { HoppRESTDocument } from "./rest/document"
|
||||
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { Environment, HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { RESTOptionTabs } from "~/components/http/RequestOptions.vue"
|
||||
import { HoppGQLSaveContext } from "./graphql/document"
|
||||
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
|
||||
@@ -43,6 +43,7 @@ export type HoppAction =
|
||||
| "modals.environment.new" // Add new environment
|
||||
| "modals.environment.delete-selected" // Delete Selected Environment
|
||||
| "modals.my.environment.edit" // Edit current personal environment
|
||||
| "modals.global.environment.update" // Update global environment
|
||||
| "modals.team.environment.edit" // Edit current team environment
|
||||
| "modals.team.new" // Add new team
|
||||
| "modals.team.edit" // Edit selected team
|
||||
@@ -93,13 +94,19 @@ type HoppActionArgsMap = {
|
||||
}
|
||||
text: string | null
|
||||
}
|
||||
"modals.global.environment.update": {
|
||||
variables?: Environment["variables"]
|
||||
isSecret?: boolean
|
||||
}
|
||||
"modals.my.environment.edit": {
|
||||
envName: string
|
||||
variableName?: string
|
||||
isSecret?: boolean
|
||||
}
|
||||
"modals.team.environment.edit": {
|
||||
envName: string
|
||||
variableName?: string
|
||||
isSecret?: boolean
|
||||
}
|
||||
"modals.team.delete": {
|
||||
teamId: string
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
mutation CreateTeamEnvironment($variables: String!,$teamID: ID!,$name: String!){
|
||||
createTeamEnvironment( variables: $variables ,teamID: $teamID ,name: $name){
|
||||
mutation CreateTeamEnvironment(
|
||||
$variables: String!
|
||||
$teamID: ID!
|
||||
$name: String!
|
||||
) {
|
||||
createTeamEnvironment(variables: $variables, teamID: $teamID, name: $name) {
|
||||
variables
|
||||
name
|
||||
teamID
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import { parseTemplateStringE } from "@hoppscotch/data"
|
||||
import { StreamSubscriberFunc } from "@composables/stream"
|
||||
import {
|
||||
AggregateEnvironment,
|
||||
aggregateEnvs$,
|
||||
getAggregateEnvs,
|
||||
aggregateEnvsWithSecrets$,
|
||||
getAggregateEnvsWithSecrets,
|
||||
getSelectedEnvironmentType,
|
||||
} from "~/newstore/environments"
|
||||
import { invokeAction } from "~/helpers/actions"
|
||||
@@ -66,7 +66,16 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
||||
|
||||
const envName = tooltipEnv?.sourceEnv ?? "Choose an Environment"
|
||||
|
||||
const envValue = tooltipEnv?.value ?? "Not found"
|
||||
let envValue = "Not Found"
|
||||
|
||||
if (!tooltipEnv?.secret && tooltipEnv?.value) envValue = tooltipEnv.value
|
||||
else if (tooltipEnv?.secret && tooltipEnv.value) {
|
||||
envValue = "******"
|
||||
} else if (!tooltipEnv?.sourceEnv) {
|
||||
envValue = "Not Found"
|
||||
} else if (!tooltipEnv?.value) {
|
||||
envValue = "Empty"
|
||||
}
|
||||
|
||||
const result = parseTemplateStringE(envValue, aggregateEnvs)
|
||||
|
||||
@@ -89,6 +98,7 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
||||
invokeAction(`modals.${action}.environment.edit`, {
|
||||
envName,
|
||||
variableName: parsedEnvKey,
|
||||
isSecret: tooltipEnv?.secret,
|
||||
})
|
||||
})
|
||||
editIcon.innerHTML = `<span class="inline-flex items-center justify-center my-1">${IconEdit}</span>`
|
||||
@@ -171,9 +181,9 @@ export class HoppEnvironmentPlugin {
|
||||
subscribeToStream: StreamSubscriberFunc,
|
||||
private editorView: Ref<EditorView | undefined>
|
||||
) {
|
||||
this.envs = getAggregateEnvs()
|
||||
this.envs = getAggregateEnvsWithSecrets()
|
||||
|
||||
subscribeToStream(aggregateEnvs$, (envs) => {
|
||||
subscribeToStream(aggregateEnvsWithSecrets$, (envs) => {
|
||||
this.envs = envs
|
||||
|
||||
this.editorView.value?.dispatch({
|
||||
|
||||
@@ -12,8 +12,6 @@ const getEnvironmentJson = (
|
||||
? cloneDeep(environmentObj.environment)
|
||||
: cloneDeep(environmentObj)
|
||||
|
||||
delete newEnvironment.id
|
||||
|
||||
const environmentId =
|
||||
environmentIndex || environmentIndex === 0
|
||||
? environmentIndex
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { entityReference } from "verzod"
|
||||
|
||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
import { safeParseJSON } from "~/helpers/functional/json"
|
||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { z } from "zod"
|
||||
|
||||
const hoppEnvSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string(),
|
||||
variables: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
export const hoppEnvImporter = (content: string) => {
|
||||
const parsedContent = safeParseJSON(content, true)
|
||||
|
||||
@@ -25,7 +16,9 @@ export const hoppEnvImporter = (content: string) => {
|
||||
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
||||
}
|
||||
|
||||
const validationResult = z.array(hoppEnvSchema).safeParse(parsedContent.value)
|
||||
const validationResult = z
|
||||
.array(entityReference(Environment))
|
||||
.safeParse(parsedContent.value)
|
||||
|
||||
if (!validationResult.success) {
|
||||
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
|
||||
|
||||
@@ -4,8 +4,9 @@ import * as O from "fp-ts/Option"
|
||||
import { IMPORTER_INVALID_FILE_FORMAT } from "."
|
||||
|
||||
import { z } from "zod"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { NonSecretEnvironment } from "@hoppscotch/data"
|
||||
import { safeParseJSONOrYAML } from "~/helpers/functional/yaml"
|
||||
import { uniqueId } from "lodash-es"
|
||||
|
||||
const insomniaResourcesSchema = z.object({
|
||||
resources: z.array(
|
||||
@@ -56,16 +57,18 @@ export const insomniaEnvImporter = (content: string) => {
|
||||
return { ...envResource, data: stringifiedData }
|
||||
})
|
||||
|
||||
const environments: Environment[] = []
|
||||
const environments: NonSecretEnvironment[] = []
|
||||
|
||||
insomniaEnvs.forEach((insomniaEnv) => {
|
||||
const parsedInsomniaEnv = insomniaEnvSchema.safeParse(insomniaEnv)
|
||||
|
||||
if (parsedInsomniaEnv.success) {
|
||||
const environment: Environment = {
|
||||
const environment: NonSecretEnvironment = {
|
||||
id: uniqueId(),
|
||||
v: 1,
|
||||
name: parsedInsomniaEnv.data.name,
|
||||
variables: Object.entries(parsedInsomniaEnv.data.data).map(
|
||||
([key, value]) => ({ key, value })
|
||||
([key, value]) => ({ key, value, secret: false })
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { safeParseJSON } from "~/helpers/functional/json"
|
||||
|
||||
import { z } from "zod"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { uniqueId } from "lodash-es"
|
||||
|
||||
const postmanEnvSchema = z.object({
|
||||
name: z.string(),
|
||||
@@ -34,12 +35,14 @@ export const postmanEnvImporter = (content: string) => {
|
||||
const postmanEnv = validationResult.data
|
||||
|
||||
const environment: Environment = {
|
||||
id: uniqueId(),
|
||||
v: 1,
|
||||
name: postmanEnv.name,
|
||||
variables: [],
|
||||
}
|
||||
|
||||
postmanEnv.values.forEach(({ key, value }) =>
|
||||
environment.variables.push({ key, value })
|
||||
environment.variables.push({ key, value, secret: false })
|
||||
)
|
||||
|
||||
return TE.right(environment)
|
||||
|
||||
@@ -8,11 +8,71 @@ import {
|
||||
getGlobalVariables,
|
||||
} from "~/newstore/environments"
|
||||
import { TestResult } from "@hoppscotch/js-sandbox"
|
||||
import { getService } from "~/modules/dioc"
|
||||
import { SecretEnvironmentService } from "~/services/secret-environment.service"
|
||||
|
||||
export const getCombinedEnvVariables = () => ({
|
||||
global: cloneDeep(getGlobalVariables()),
|
||||
selected: cloneDeep(getCurrentEnvironment().variables),
|
||||
})
|
||||
const secretEnvironmentService = getService(SecretEnvironmentService)
|
||||
|
||||
const unsecretEnvironments = (
|
||||
global: Environment["variables"],
|
||||
selected: Environment
|
||||
) => {
|
||||
const resolvedGlobalWithSecrets = global.map((globalVar, index) => {
|
||||
const secretVar = secretEnvironmentService.getSecretEnvironmentVariable(
|
||||
"Global",
|
||||
index
|
||||
)
|
||||
if (secretVar) {
|
||||
return {
|
||||
...globalVar,
|
||||
value: secretVar.value,
|
||||
}
|
||||
} else if (!("value" in globalVar) || !globalVar.value) {
|
||||
return {
|
||||
...globalVar,
|
||||
value: "",
|
||||
}
|
||||
}
|
||||
return globalVar
|
||||
})
|
||||
|
||||
const resolvedSelectedWithSecrets = selected.variables.map(
|
||||
(selectedVar, index) => {
|
||||
const secretVar = secretEnvironmentService.getSecretEnvironmentVariable(
|
||||
selected.id,
|
||||
index
|
||||
)
|
||||
if (secretVar) {
|
||||
return {
|
||||
...selectedVar,
|
||||
value: secretVar.value,
|
||||
}
|
||||
} else if (!("value" in selectedVar) || !selectedVar.value) {
|
||||
return {
|
||||
...selectedVar,
|
||||
value: "",
|
||||
}
|
||||
}
|
||||
return selectedVar
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
global: resolvedGlobalWithSecrets,
|
||||
selected: resolvedSelectedWithSecrets,
|
||||
}
|
||||
}
|
||||
|
||||
export const getCombinedEnvVariables = () => {
|
||||
const reformedVars = unsecretEnvironments(
|
||||
getGlobalVariables(),
|
||||
getCurrentEnvironment()
|
||||
)
|
||||
return {
|
||||
global: cloneDeep(reformedVars.global),
|
||||
selected: cloneDeep(reformedVars.selected),
|
||||
}
|
||||
}
|
||||
|
||||
export const getFinalEnvsFromPreRequest = (
|
||||
script: string,
|
||||
|
||||
@@ -118,6 +118,8 @@ export default class TeamEnvironmentAdapter {
|
||||
id: x.id,
|
||||
teamID: x.teamID,
|
||||
environment: {
|
||||
v: 1,
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
variables: JSON.parse(x.variables),
|
||||
},
|
||||
@@ -196,6 +198,8 @@ export default class TeamEnvironmentAdapter {
|
||||
id: x.id,
|
||||
teamID: x.teamID,
|
||||
environment: {
|
||||
v: 1,
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
variables: JSON.parse(x.variables),
|
||||
},
|
||||
@@ -249,6 +253,8 @@ export default class TeamEnvironmentAdapter {
|
||||
id: x.id,
|
||||
teamID: x.teamID,
|
||||
environment: {
|
||||
v: 1,
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
variables: JSON.parse(x.variables),
|
||||
},
|
||||
|
||||
@@ -45,7 +45,8 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
|
||||
export const getComputedAuthHeaders = (
|
||||
envVars: Environment["variables"],
|
||||
req?: HoppRESTRequest,
|
||||
auth?: HoppRESTRequest["auth"]
|
||||
auth?: HoppRESTRequest["auth"],
|
||||
parse = true
|
||||
) => {
|
||||
const request = auth ? { auth: auth ?? { authActive: false } } : req
|
||||
// If Authorization header is also being user-defined, that takes priority
|
||||
@@ -60,8 +61,12 @@ export const getComputedAuthHeaders = (
|
||||
|
||||
// TODO: Support a better b64 implementation than btoa ?
|
||||
if (request.auth.authType === "basic") {
|
||||
const username = parseTemplateString(request.auth.username, envVars)
|
||||
const password = parseTemplateString(request.auth.password, envVars)
|
||||
const username = parse
|
||||
? parseTemplateString(request.auth.username, envVars)
|
||||
: request.auth.username
|
||||
const password = parse
|
||||
? parseTemplateString(request.auth.password, envVars)
|
||||
: request.auth.password
|
||||
|
||||
headers.push({
|
||||
active: true,
|
||||
@@ -75,7 +80,11 @@ export const getComputedAuthHeaders = (
|
||||
headers.push({
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
value: `Bearer ${parseTemplateString(request.auth.token, envVars)}`,
|
||||
value: `Bearer ${
|
||||
parse
|
||||
? parseTemplateString(request.auth.token, envVars)
|
||||
: request.auth.token
|
||||
}`,
|
||||
})
|
||||
} else if (request.auth.authType === "api-key") {
|
||||
const { key, addTo } = request.auth
|
||||
@@ -83,7 +92,9 @@ export const getComputedAuthHeaders = (
|
||||
headers.push({
|
||||
active: true,
|
||||
key: parseTemplateString(key, envVars),
|
||||
value: parseTemplateString(request.auth.value ?? "", envVars),
|
||||
value: parse
|
||||
? parseTemplateString(request.auth.value ?? "", envVars)
|
||||
: request.auth.value ?? "",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -133,10 +144,11 @@ export type ComputedHeader = {
|
||||
*/
|
||||
export const getComputedHeaders = (
|
||||
req: HoppRESTRequest,
|
||||
envVars: Environment["variables"]
|
||||
envVars: Environment["variables"],
|
||||
parse = true
|
||||
): ComputedHeader[] => {
|
||||
return [
|
||||
...getComputedAuthHeaders(envVars, req).map((header) => ({
|
||||
...getComputedAuthHeaders(envVars, req, undefined, parse).map((header) => ({
|
||||
source: "auth" as const,
|
||||
header,
|
||||
})),
|
||||
|
||||
Reference in New Issue
Block a user