feat: secret variables in environments (#3779)

Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
This commit is contained in:
Nivedin
2024-02-08 21:58:42 +05:30
committed by GitHub
parent 16803acb26
commit 00862eb192
55 changed files with 2141 additions and 439 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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({

View File

@@ -12,8 +12,6 @@ const getEnvironmentJson = (
? cloneDeep(environmentObj.environment)
: cloneDeep(environmentObj)
delete newEnvironment.id
const environmentId =
environmentIndex || environmentIndex === 0
? environmentIndex

View File

@@ -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)

View File

@@ -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 })
),
}

View File

@@ -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)

View File

@@ -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,

View File

@@ -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),
},

View File

@@ -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,
})),