diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index ea495ad87..098cfed6d 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -67,8 +67,17 @@ export const createSecrets = async (req: Request, res: Response) => { })) ); + setTimeout(async () => { + // trigger event - push secrets + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId + }) + }); + }, 5000); + // (EE) add secret versions for new secrets - EESecretService.addSecretVersions({ + await EESecretService.addSecretVersions({ secretVersions: newSecrets.map(({ _id, version, @@ -104,13 +113,6 @@ export const createSecrets = async (req: Request, res: Response) => { })) }); - // trigger event - push secrets - await EventService.handleEvent({ - event: eventPushSecrets({ - workspaceId - }) - }); - const addAction = await EELogService.createActionSecret({ name: ACTION_ADD_SECRETS, userId: req.user._id.toString(), @@ -342,11 +344,13 @@ export const updateSecrets = async (req: Request, res: Response) => { Object.keys(workspaceSecretObj).forEach(async (key) => { // trigger event - push secrets - await EventService.handleEvent({ - event: eventPushSecrets({ - workspaceId: key - }) - }); + setTimeout(async () => { + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: key + }) + }); + }, 10000); const updateAction = await EELogService.createActionSecret({ name: ACTION_UPDATE_SECRETS, diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index 59d72f469..74ba062bd 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -39,7 +39,7 @@ const validateSecrets = async ({ try { secrets = await Secret.find({ _id: { - $in: secretIds + $in: secretIds.map((secretId: string) => new Types.ObjectId(secretId)) } }); diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index c4a4f16ee..0ae57dc59 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import axios, { AxiosError } from 'axios'; import * as Sentry from '@sentry/node'; import { Octokit } from '@octokit/rest'; // import * as sodium from 'libsodium-wrappers'; @@ -145,7 +145,6 @@ const syncSecretsVercel = async ({ secrets: any; accessToken: string; }) => { - interface VercelSecret { id?: string; type: string; @@ -155,131 +154,131 @@ const syncSecretsVercel = async ({ } try { - // Get all (decrypted) secrets back from Vercel in - // decrypted format - const params: { [key: string]: string } = { - decrypt: 'true', - ...( integrationAuth?.teamId ? { - teamId: integrationAuth.teamId - } : {}) - } - - const res = (await Promise.all((await axios.get( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, - { + // Get all (decrypted) secrets back from Vercel in + // decrypted format + const params: { [key: string]: string } = { + decrypt: 'true', + ...( integrationAuth?.teamId ? { + teamId: integrationAuth.teamId + } : {}) + } + + const res = (await Promise.all((await axios.get( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + )) + .data + .envs + .filter((secret: VercelSecret) => secret.target.includes(integration.target)) + .map(async (secret: VercelSecret) => (await axios.get( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + { params, headers: { Authorization: `Bearer ${accessToken}` } - } - )) - .data - .envs - .filter((secret: VercelSecret) => secret.target.includes(integration.target)) - .map(async (secret: VercelSecret) => (await axios.get( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, - { - params, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - )).data) - )).reduce((obj: any, secret: any) => ({ - ...obj, - [secret.key]: secret - }), {}); - - const updateSecrets: VercelSecret[] = []; - const deleteSecrets: VercelSecret[] = []; - const newSecrets: VercelSecret[] = []; + } + )).data) + )).reduce((obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret + }), {}); + + const updateSecrets: VercelSecret[] = []; + const deleteSecrets: VercelSecret[] = []; + const newSecrets: VercelSecret[] = []; - // Identify secrets to create - Object.keys(secrets).map((key) => { - if (!(key in res)) { - // case: secret has been created - newSecrets.push({ - key: key, - value: secrets[key], - type: 'encrypted', - target: [integration.target] - }); - } - }); - - // Identify secrets to update and delete - Object.keys(res).map((key) => { - if (key in secrets) { - if (res[key].value !== secrets[key]) { - // case: secret value has changed - updateSecrets.push({ - id: res[key].id, - key: key, - value: secrets[key], - type: 'encrypted', - target: [integration.target] - }); - } - } else { - // case: secret has been deleted - deleteSecrets.push({ - id: res[key].id, - key: key, - value: res[key].value, - type: 'encrypted', - target: [integration.target], - }); - } - }); + // Identify secrets to create + Object.keys(secrets).map((key) => { + if (!(key in res)) { + // case: secret has been created + newSecrets.push({ + key: key, + value: secrets[key], + type: 'encrypted', + target: [integration.target] + }); + } + }); + + // Identify secrets to update and delete + Object.keys(res).map((key) => { + if (key in secrets) { + if (res[key].value !== secrets[key]) { + // case: secret value has changed + updateSecrets.push({ + id: res[key].id, + key: key, + value: secrets[key], + type: 'encrypted', + target: [integration.target] + }); + } + } else { + // case: secret has been deleted + deleteSecrets.push({ + id: res[key].id, + key: key, + value: res[key].value, + type: 'encrypted', + target: [integration.target], + }); + } + }); - // Sync/push new secrets - if (newSecrets.length > 0) { - await axios.post( - `${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`, - newSecrets, - { - params, - headers: { - Authorization: `Bearer ${accessToken}` - } + // Sync/push new secrets + if (newSecrets.length > 0) { + await axios.post( + `${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`, + newSecrets, + { + params, + headers: { + Authorization: `Bearer ${accessToken}` } - ); - } + } + ); + } - // Sync/push updated secrets - if (updateSecrets.length > 0) { - updateSecrets.forEach(async (secret: VercelSecret) => { - const { - id, - ...updatedSecret - } = secret; - await axios.patch( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, - updatedSecret, - { - params, - headers: { - Authorization: `Bearer ${accessToken}` - } + // Sync/push updated secrets + if (updateSecrets.length > 0) { + updateSecrets.forEach(async (secret: VercelSecret) => { + const { + id, + ...updatedSecret + } = secret; + await axios.patch( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + updatedSecret, + { + params, + headers: { + Authorization: `Bearer ${accessToken}` } - ); - }); - } + } + ); + }); + } - // Delete secrets - if (deleteSecrets.length > 0) { - deleteSecrets.forEach(async (secret: VercelSecret) => { - await axios.delete( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, - { - params, - headers: { - Authorization: `Bearer ${accessToken}` - } + // Delete secrets + if (deleteSecrets.length > 0) { + deleteSecrets.forEach(async (secret: VercelSecret) => { + await axios.delete( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}` } - ); - }); - } + } + ); + }); + } } catch (err) { Sentry.setUser(null); Sentry.captureException(err); @@ -307,188 +306,188 @@ const syncSecretsNetlify = async ({ }) => { try { - interface NetlifyValue { - id?: string; - context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production', - value: string; - } - - interface NetlifySecret { - key: string; - values: NetlifyValue[]; - } - - interface NetlifySecretsRes { - [index: string]: NetlifySecret; - } - - const getParams = new URLSearchParams({ - context_name: 'all', // integration.context or all - site_id: integration.siteId - }); - - const res = (await axios.get( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, - { - params: getParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - )) - .data - .reduce((obj: any, secret: any) => ({ - ...obj, - [secret.key]: secret - }), {}); - - const newSecrets: NetlifySecret[] = []; // createEnvVars - const deleteSecrets: string[] = []; // deleteEnvVar - const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue - const updateSecrets: NetlifySecret[] = []; // setEnvVarValue + interface NetlifyValue { + id?: string; + context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production', + value: string; + } + + interface NetlifySecret { + key: string; + values: NetlifyValue[]; + } + + interface NetlifySecretsRes { + [index: string]: NetlifySecret; + } + + const getParams = new URLSearchParams({ + context_name: 'all', // integration.context or all + site_id: integration.siteId + }); + + const res = (await axios.get( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, + { + params: getParams, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + )) + .data + .reduce((obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret + }), {}); + + const newSecrets: NetlifySecret[] = []; // createEnvVars + const deleteSecrets: string[] = []; // deleteEnvVar + const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue + const updateSecrets: NetlifySecret[] = []; // setEnvVarValue + + // identify secrets to create and update + Object.keys(secrets).map((key) => { + if (!(key in res)) { + // case: Infisical secret does not exist in Netlify -> create secret + newSecrets.push({ + key, + values: [{ + value: secrets[key], + context: integration.context + }] + }); + } else { + // case: Infisical secret exists in Netlify + const contexts = res[key].values + .reduce((obj: any, value: NetlifyValue) => ({ + ...obj, + [value.context]: value + }), {}); + + if (integration.context in contexts) { + // case: Netlify secret value exists in integration context + if (secrets[key] !== contexts[integration.context].value) { + // case: Infisical and Netlify secret values are different + // -> update Netlify secret context and value + updateSecrets.push({ + key, + values: [{ + context: integration.context, + value: secrets[key] + }] + }); + } + } else { + // case: Netlify secret value does not exist in integration context + // -> add the new Netlify secret context and value + updateSecrets.push({ + key, + values: [{ + context: integration.context, + value: secrets[key] + }] + }); + } + } + }) + + // identify secrets to delete + // TODO: revise (patch case where 1 context was deleted but others still there + Object.keys(res).map((key) => { + // loop through each key's context + if (!(key in secrets)) { + // case: Netlify secret does not exist in Infisical + + const numberOfValues = res[key].values.length; + + res[key].values.forEach((value: NetlifyValue) => { + if (value.context === integration.context) { + if (numberOfValues <= 1) { + // case: Netlify secret value has less than 1 context -> delete secret + deleteSecrets.push(key); + } else { + // case: Netlify secret value has more than 1 context -> delete secret value context + deleteSecretValues.push({ + key, + values: [{ + id: value.id, + context: integration.context, + value: value.value + }] + }); + } + } + }); + } + }); - // identify secrets to create and update - Object.keys(secrets).map((key) => { - if (!(key in res)) { - // case: Infisical secret does not exist in Netlify -> create secret - newSecrets.push({ - key, - values: [{ - value: secrets[key], - context: integration.context - }] - }); - } else { - // case: Infisical secret exists in Netlify - const contexts = res[key].values - .reduce((obj: any, value: NetlifyValue) => ({ - ...obj, - [value.context]: value - }), {}); - - if (integration.context in contexts) { - // case: Netlify secret value exists in integration context - if (secrets[key] !== contexts[integration.context].value) { - // case: Infisical and Netlify secret values are different - // -> update Netlify secret context and value - updateSecrets.push({ - key, - values: [{ - context: integration.context, - value: secrets[key] - }] - }); - } - } else { - // case: Netlify secret value does not exist in integration context - // -> add the new Netlify secret context and value - updateSecrets.push({ - key, - values: [{ - context: integration.context, - value: secrets[key] - }] - }); - } - } - }) - - // identify secrets to delete - // TODO: revise (patch case where 1 context was deleted but others still there - Object.keys(res).map((key) => { - // loop through each key's context - if (!(key in secrets)) { - // case: Netlify secret does not exist in Infisical - - const numberOfValues = res[key].values.length; - - res[key].values.forEach((value: NetlifyValue) => { - if (value.context === integration.context) { - if (numberOfValues <= 1) { - // case: Netlify secret value has less than 1 context -> delete secret - deleteSecrets.push(key); - } else { - // case: Netlify secret value has more than 1 context -> delete secret value context - deleteSecretValues.push({ - key, - values: [{ - id: value.id, - context: integration.context, - value: value.value - }] - }); - } - } - }); - } - }); + const syncParams = new URLSearchParams({ + site_id: integration.siteId + }); - const syncParams = new URLSearchParams({ - site_id: integration.siteId - }); + if (newSecrets.length > 0) { + await axios.post( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, + newSecrets, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + } - if (newSecrets.length > 0) { - await axios.post( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, - newSecrets, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - } + if (updateSecrets.length > 0) { + updateSecrets.forEach(async (secret: NetlifySecret) => { + await axios.patch( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, + { + context: secret.values[0].context, + value: secret.values[0].value + }, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + }); + } - if (updateSecrets.length > 0) { - updateSecrets.forEach(async (secret: NetlifySecret) => { - await axios.patch( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, - { - context: secret.values[0].context, - value: secret.values[0].value - }, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); - } + if (deleteSecrets.length > 0) { + deleteSecrets.forEach(async (key: string) => { + await axios.delete( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + }); + } - if (deleteSecrets.length > 0) { - deleteSecrets.forEach(async (key: string) => { - await axios.delete( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); - } - - if (deleteSecretValues.length > 0) { - deleteSecretValues.forEach(async (secret: NetlifySecret) => { - await axios.delete( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - }); - } + if (deleteSecretValues.length > 0) { + deleteSecretValues.forEach(async (secret: NetlifySecret) => { + await axios.delete( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + }); + } } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to sync secrets to Heroku'); + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to sync secrets to Heroku'); } }