You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
docker-infisical/backend/src/integrations/sync.ts

2004 lines
53 KiB

import _ from "lodash";
import AWS from "aws-sdk";
import {
CreateSecretCommand,
GetSecretValueCommand,
ResourceNotFoundException,
SecretsManagerClient,
UpdateSecretCommand,
} from "@aws-sdk/client-secrets-manager";
import { Octokit } from "@octokit/rest";
import sodium from "libsodium-wrappers";
import { IIntegration, IIntegrationAuth } from "../models";
import {
INTEGRATION_AWS_PARAMETER_STORE,
INTEGRATION_AWS_SECRET_MANAGER,
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_CHECKLY,
INTEGRATION_CHECKLY_API_URL,
INTEGRATION_CIRCLECI,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_CLOUDFLARE_PAGES,
INTEGRATION_CLOUDFLARE_PAGES_API_URL,
INTEGRATION_FLYIO,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_GITLAB_API_URL,
INTEGRATION_HASHICORP_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_NETLIFY,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_NORTHFLANK,
INTEGRATION_NORTHFLANK_API_URL,
INTEGRATION_RAILWAY,
INTEGRATION_RAILWAY_API_URL,
INTEGRATION_RENDER,
INTEGRATION_RENDER_API_URL,
INTEGRATION_SUPABASE,
INTEGRATION_SUPABASE_API_URL,
INTEGRATION_TRAVISCI,
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_VERCEL,
INTEGRATION_VERCEL_API_URL,
} from "../variables";
import { standardRequest} from "../config/request";
/**
* Sync/push [secrets] to [app] in integration named [integration]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessId - access id for integration
* @param {String} obj.accessToken - access token for integration
*/
const syncSecrets = async ({
integration,
integrationAuth,
secrets,
accessId,
accessToken,
}: {
integration: IIntegration;
integrationAuth: IIntegrationAuth;
secrets: any;
accessId: string | null;
accessToken: string;
}) => {
switch (integration.integration) {
case INTEGRATION_AZURE_KEY_VAULT:
await syncSecretsAzureKeyVault({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_AWS_PARAMETER_STORE:
await syncSecretsAWSParameterStore({
integration,
secrets,
accessId,
accessToken,
});
break;
case INTEGRATION_AWS_SECRET_MANAGER:
await syncSecretsAWSSecretManager({
integration,
secrets,
accessId,
accessToken,
});
break;
case INTEGRATION_HEROKU:
await syncSecretsHeroku({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_VERCEL:
await syncSecretsVercel({
integration,
integrationAuth,
secrets,
accessToken,
});
break;
case INTEGRATION_NETLIFY:
await syncSecretsNetlify({
integration,
integrationAuth,
secrets,
accessToken,
});
break;
case INTEGRATION_GITHUB:
await syncSecretsGitHub({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_GITLAB:
await syncSecretsGitLab({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_RENDER:
await syncSecretsRender({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_RAILWAY:
await syncSecretsRailway({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_FLYIO:
await syncSecretsFlyio({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_CIRCLECI:
await syncSecretsCircleCI({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_TRAVISCI:
await syncSecretsTravisCI({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_SUPABASE:
await syncSecretsSupabase({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_CHECKLY:
await syncSecretsCheckly({
integration,
secrets,
accessToken,
});
break;
case INTEGRATION_HASHICORP_VAULT:
await syncSecretsHashiCorpVault({
integration,
integrationAuth,
secrets,
accessId,
accessToken,
});
break;
case INTEGRATION_CLOUDFLARE_PAGES:
await syncSecretsCloudflarePages({
integration,
secrets,
accessId,
accessToken
});
break;
case INTEGRATION_NORTHFLANK:
await syncSecretsNorthflank({
integration,
secrets,
accessToken
});
break;
}
};
/**
* Sync/push [secrets] to Azure Key Vault with vault URI [integration.app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Azure Key Vault integration
*/
const syncSecretsAzureKeyVault = async ({
integration,
secrets,
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
interface GetAzureKeyVaultSecret {
id: string; // secret URI
attributes: {
enabled: true,
created: number;
updated: number;
recoveryLevel: string;
recoverableDays: number;
}
}
interface AzureKeyVaultSecret extends GetAzureKeyVaultSecret {
key: string;
}
/**
* Return all secrets from Azure Key Vault by paginating through URL [url]
* @param {String} url - pagination URL to get next set of secrets from Azure Key Vault
* @returns
*/
const paginateAzureKeyVaultSecrets = async (url: string) => {
let result: GetAzureKeyVaultSecret[] = [];
while (url) {
const res = await standardRequest.get(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
result = result.concat(res.data.value);
url = res.data.nextLink;
}
return result;
}
const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets(`${integration.app}/secrets?api-version=7.3`);
let lastSlashIndex: number;
const res = (await Promise.all(getAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => {
if (!lastSlashIndex) {
lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf("/");
}
const azureKeyVaultSecret = await standardRequest.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, {
headers: {
"Authorization": `Bearer ${accessToken}`,
},
});
return ({
...azureKeyVaultSecret.data,
key: getAzureKeyVaultSecret.id.substring(lastSlashIndex + 1),
});
})))
.reduce((obj: any, secret: any) => ({
...obj,
[secret.key]: secret,
}), {});
const setSecrets: {
key: string;
value: string;
}[] = [];
Object.keys(secrets).forEach((key) => {
const hyphenatedKey = key.replace(/_/g, "-");
if (!(hyphenatedKey in res)) {
// case: secret has been created
setSecrets.push({
key: hyphenatedKey,
value: secrets[key],
});
} else {
if (secrets[key] !== res[hyphenatedKey].value) {
// case: secret has been updated
setSecrets.push({
key: hyphenatedKey,
value: secrets[key],
});
}
}
});
const deleteSecrets: AzureKeyVaultSecret[] = [];
Object.keys(res).forEach((key) => {
const underscoredKey = key.replace(/-/g, "_");
if (!(underscoredKey in secrets)) {
deleteSecrets.push(res[key]);
}
});
const setSecretAzureKeyVault = async ({
key,
value,
integration,
accessToken,
}: {
key: string;
value: string;
integration: IIntegration;
accessToken: string;
}) => {
let isSecretSet = false;
let maxTries = 6;
while (!isSecretSet && maxTries > 0) {
// try to set secret
try {
await standardRequest.put(
`${integration.app}/secrets/${key}?api-version=7.3`,
{
value,
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
);
isSecretSet = true;
} catch (err) {
const error: any = err;
if (error?.response?.data?.error?.innererror?.code === "ObjectIsDeletedButRecoverable") {
await standardRequest.post(
`${integration.app}/deletedsecrets/${key}/recover?api-version=7.3`, {},
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
}
);
await new Promise(resolve => setTimeout(resolve, 10000));
} else {
await new Promise(resolve => setTimeout(resolve, 10000));
maxTries--;
}
}
}
}
// Sync/push set secrets
for await (const setSecret of setSecrets) {
const { key, value } = setSecret;
setSecretAzureKeyVault({
key,
value,
integration,
accessToken,
});
}
for await (const deleteSecret of deleteSecrets) {
const { key } = deleteSecret;
await standardRequest.delete(`${integration.app}/secrets/${key}?api-version=7.3`, {
headers: {
"Authorization": `Bearer ${accessToken}`,
},
});
}
};
/**
* Sync/push [secrets] to AWS parameter store
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessId - access id for AWS parameter store integration
* @param {String} obj.accessToken - access token for AWS parameter store integration
*/
const syncSecretsAWSParameterStore = async ({
integration,
secrets,
accessId,
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessId: string | null;
accessToken: string;
}) => {
if (!accessId) return;
AWS.config.update({
region: integration.region,
accessKeyId: accessId,
secretAccessKey: accessToken,
});
const ssm = new AWS.SSM({
apiVersion: "2014-11-06",
region: integration.region,
});
const params = {
Path: integration.path,
Recursive: true,
WithDecryption: true,
};
const parameterList = (await ssm.getParametersByPath(params).promise()).Parameters
let awsParameterStoreSecretsObj: {
[key: string]: any // TODO: fix type
} = {};
if (parameterList) {
awsParameterStoreSecretsObj = parameterList.reduce((obj: any, secret: any) => ({
...obj,
[secret.Name.split("/").pop()]: secret,
}), {});
}
// Identify secrets to create
Object.keys(secrets).map(async (key) => {
if (!(key in awsParameterStoreSecretsObj)) {
// case: secret does not exist in AWS parameter store
// -> create secret
await ssm.putParameter({
Name: `${integration.path}${key}`,
Type: "SecureString",
Value: secrets[key],
Overwrite: true,
}).promise();
} else {
// case: secret exists in AWS parameter store
if (awsParameterStoreSecretsObj[key].Value !== secrets[key]) {
// case: secret value doesn't match one in AWS parameter store
// -> update secret
await ssm.putParameter({
Name: `${integration.path}${key}`,
Type: "SecureString",
Value: secrets[key],
Overwrite: true,
}).promise();
}
}
});
// Identify secrets to delete
Object.keys(awsParameterStoreSecretsObj).map(async (key) => {
if (!(key in secrets)) {
// case:
// -> delete secret
await ssm.deleteParameter({
Name: awsParameterStoreSecretsObj[key].Name,
}).promise();
}
});
AWS.config.update({
region: undefined,
accessKeyId: undefined,
secretAccessKey: undefined,
});
}
/**
* Sync/push [secrets] to AWS secret manager
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessId - access id for AWS secret manager integration
* @param {String} obj.accessToken - access token for AWS secret manager integration
*/
const syncSecretsAWSSecretManager = async ({
integration,
secrets,
accessId,
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessId: string | null;
accessToken: string;
}) => {
let secretsManager;
try {
if (!accessId) return;
AWS.config.update({
region: integration.region,
accessKeyId: accessId,
secretAccessKey: accessToken,
});
secretsManager = new SecretsManagerClient({
region: integration.region,
credentials: {
accessKeyId: accessId,
secretAccessKey: accessToken,
},
});
const awsSecretManagerSecret = await secretsManager.send(
new GetSecretValueCommand({
SecretId: integration.app,
})
);
let awsSecretManagerSecretObj: { [key: string]: any } = {};
if (awsSecretManagerSecret?.SecretString) {
awsSecretManagerSecretObj = JSON.parse(awsSecretManagerSecret.SecretString);
}
if (!_.isEqual(awsSecretManagerSecretObj, secrets)) {
await secretsManager.send(new UpdateSecretCommand({
SecretId: integration.app,
SecretString: JSON.stringify(secrets),
}));
}
AWS.config.update({
region: undefined,
accessKeyId: undefined,
secretAccessKey: undefined,
});
} catch (err) {
if (err instanceof ResourceNotFoundException && secretsManager) {
await secretsManager.send(new CreateSecretCommand({
Name: integration.app,
SecretString: JSON.stringify(secrets),
}));
}
AWS.config.update({
region: undefined,
accessKeyId: undefined,
secretAccessKey: undefined,
});
}
}
/**
* Sync/push [secrets] to Heroku app named [integration.app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Heroku integration
*/
const syncSecretsHeroku = async ({
integration,
secrets,
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
const herokuSecrets = (
await standardRequest.get(
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
{
headers: {
Accept: "application/vnd.heroku+json; version=3",
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
)
).data;
Object.keys(herokuSecrets).forEach((key) => {
if (!(key in secrets)) {
secrets[key] = null;
}
});
await standardRequest.patch(
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
secrets,
{
headers: {
Accept: "application/vnd.heroku+json; version=3",
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
};
/**
* Sync/push [secrets] to Vercel project named [integration.app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
*/
const syncSecretsVercel = async ({
integration,
integrationAuth,
secrets,
accessToken,
}: {
integration: IIntegration;
integrationAuth: IIntegrationAuth;
secrets: any;
accessToken: string;
}) => {
interface VercelSecret {
id?: string;
type: string;
key: string;
value: string;
target: string[];
gitBranch?: string;
}
// Get all (decrypted) secrets back from Vercel in
// decrypted format
const params: { [key: string]: string } = {
decrypt: "true",
...(integrationAuth?.teamId
? {
teamId: integrationAuth.teamId,
}
: {}),
};
const vercelSecrets: VercelSecret[] = (await standardRequest.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
))
.data
.envs
.filter((secret: VercelSecret) => {
if (!secret.target.includes(integration.targetEnvironment)) {
// case: secret does not have the same target environment
return false;
}
if (integration.targetEnvironment === "preview" && integration.path && integration.path !== secret.gitBranch) {
// case: secret on preview environment does not have same target git branch
return false;
}
return true;
});
// return secret.target.includes(integration.targetEnvironment);
const res: { [key: string]: VercelSecret } = {};
for await (const vercelSecret of vercelSecrets) {
if (vercelSecret.type === "encrypted") {
// case: secret is encrypted -> need to decrypt
const decryptedSecret = (await standardRequest.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${vercelSecret.id}`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
)).data;
res[vercelSecret.key] = decryptedSecret;
} else {
res[vercelSecret.key] = vercelSecret;
}
}
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.targetEnvironment],
...(integration.path ? {
gitBranch: integration.path,
} : {}),
});
}
});
// 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: res[key].type,
target: res[key].target.includes(integration.targetEnvironment)
? [...res[key].target]
: [...res[key].target, integration.targetEnvironment],
...(integration.path ? {
gitBranch: integration.path,
} : {}),
});
}
} else {
// case: secret has been deleted
deleteSecrets.push({
id: res[key].id,
key: key,
value: res[key].value,
type: "encrypted", // value doesn't matter
target: [integration.targetEnvironment],
...(integration.path ? {
gitBranch: integration.path,
} : {}),
});
}
});
// Sync/push new secrets
if (newSecrets.length > 0) {
await standardRequest.post(
`${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`,
newSecrets,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
}
for await (const secret of updateSecrets) {
if (secret.type !== "sensitive") {
const { id, ...updatedSecret } = secret;
await standardRequest.patch(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
updatedSecret,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
}
}
for await (const secret of deleteSecrets) {
await standardRequest.delete(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
}
};
/**
* Sync/push [secrets] to Netlify site with id [integration.appId]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {Object} obj.accessToken - access token for Netlify integration
*/
const syncSecretsNetlify = async ({
integration,
integrationAuth,
secrets,
accessToken,
}: {
integration: IIntegration;
integrationAuth: IIntegrationAuth;
secrets: any;
accessToken: string;
}) => {
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.appId,
});
const res = (
await standardRequest.get(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
{
params: getParams,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
)
).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.targetEnvironment,
},
],
});
} else {
// case: Infisical secret exists in Netlify
const contexts = res[key].values.reduce(
(obj: any, value: NetlifyValue) => ({
...obj,
[value.context]: value,
}),
{}
);
if (integration.targetEnvironment in contexts) {
// case: Netlify secret value exists in integration context
if (secrets[key] !== contexts[integration.targetEnvironment].value) {
// case: Infisical and Netlify secret values are different
// -> update Netlify secret context and value
updateSecrets.push({
key,
values: [
{
context: integration.targetEnvironment,
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.targetEnvironment,
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.targetEnvironment) {
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.targetEnvironment,
value: value.value,
},
],
});
}
}
});
}
});
const syncParams = new URLSearchParams({
site_id: integration.appId,
});
if (newSecrets.length > 0) {
await standardRequest.post(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
newSecrets,
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
}
if (updateSecrets.length > 0) {
updateSecrets.forEach(async (secret: NetlifySecret) => {
await standardRequest.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}`,
"Accept-Encoding": "application/json",
},
}
);
});
}
if (deleteSecrets.length > 0) {
deleteSecrets.forEach(async (key: string) => {
await standardRequest.delete(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`,
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
});
}
if (deleteSecretValues.length > 0) {
deleteSecretValues.forEach(async (secret: NetlifySecret) => {
await standardRequest.delete(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`,
{
params: syncParams,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
});
}
};
/**
* Sync/push [secrets] to GitHub repo with name [integration.app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for GitHub integration
*/
const syncSecretsGitHub = async ({
integration,
secrets,
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
interface GitHubRepoKey {
key_id: string;
key: string;
}
interface GitHubSecret {
name: string;
created_at: string;
updated_at: string;
}
interface GitHubSecretRes {
[index: string]: GitHubSecret;
}
const deleteSecrets: GitHubSecret[] = [];
const octokit = new Octokit({
auth: accessToken,
});
// const user = (await octokit.request('GET /user', {})).data;
const repoPublicKey: GitHubRepoKey = (
await octokit.request(
"GET /repos/{owner}/{repo}/actions/secrets/public-key",
{
owner: integration.owner,
repo: integration.app,
}
)
).data;
// Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key
const encryptedSecrets: GitHubSecretRes = (
await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", {
owner: integration.owner,
repo: integration.app,
})
).data.secrets.reduce(
(obj: any, secret: any) => ({
...obj,
[secret.name]: secret,
}),
{}
);
Object.keys(encryptedSecrets).map(async (key) => {
if (!(key in secrets)) {
await octokit.request(
"DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}",
{
owner: integration.owner,
repo: integration.app,
secret_name: key,
}
);
}
});
Object.keys(secrets).map((key) => {
// let encryptedSecret;
sodium.ready.then(async () => {
// convert secret & base64 key to Uint8Array.
const binkey = sodium.from_base64(
repoPublicKey.key,
sodium.base64_variants.ORIGINAL
);
const binsec = sodium.from_string(secrets[key]);
// encrypt secret using libsodium
const encBytes = sodium.crypto_box_seal(binsec, binkey);
// convert encrypted Uint8Array to base64
const encryptedSecret = sodium.to_base64(
encBytes,
sodium.base64_variants.ORIGINAL
);
await octokit.request(
"PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}",
{
owner: integration.owner,
repo: integration.app,
secret_name: key,
encrypted_value: encryptedSecret,
key_id: repoPublicKey.key_id,
}
);
});
});
};
/**
* Sync/push [secrets] to Render service with id [integration.appId]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Render integration
*/
const syncSecretsRender = async ({
integration,
secrets,
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
await standardRequest.put(
`${INTEGRATION_RENDER_API_URL}/v1/services/${integration.appId}/env-vars`,
Object.keys(secrets).map((key) => ({
key,
value: secrets[key],
})),
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
};
/**
* Sync/push [secrets] to Railway project with id [integration.appId]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Railway integration
*/
const syncSecretsRailway = async ({
integration,
secrets,
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
const query = `
mutation UpsertVariables($input: VariableCollectionUpsertInput!) {
variableCollectionUpsert(input: $input)
}
`;
const input = {
projectId: integration.appId,
environmentId: integration.targetEnvironmentId,
...(integration.targetServiceId ? { serviceId: integration.targetServiceId } : {}),
replace: true,
variables: secrets,
};
await standardRequest.post(INTEGRATION_RAILWAY_API_URL, {
query,
variables: {
input,
},
}, {
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
"Accept-Encoding": "application/json",
},
});
}
/**
* Sync/push [secrets] to Fly.io app
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Render integration
*/
const syncSecretsFlyio = async ({
integration,
secrets,
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
// set secrets
const SetSecrets = `
mutation($input: SetSecretsInput!) {
setSecrets(input: $input) {
release {
id
version
reason
description
user {
id
email
name
}
evaluationId
createdAt
}
}
}
`;
await standardRequest.post(INTEGRATION_FLYIO_API_URL, {
query: SetSecrets,
variables: {
input: {
appId: integration.app,
secrets: Object.entries(secrets).map(([key, value]) => ({
key,
value,
})),
},
},
}, {
headers: {
Authorization: "Bearer " + accessToken,
"Accept-Encoding": "application/json",
},
});
// get secrets
interface FlyioSecret {
name: string;
digest: string;
createdAt: string;
}
const GetSecrets = `query ($appName: String!) {
app(name: $appName) {
secrets {
name
digest
createdAt
}
}
}`;
const getSecretsRes = (await standardRequest.post(INTEGRATION_FLYIO_API_URL, {
query: GetSecrets,
variables: {
appName: integration.app,
},
}, {
headers: {
Authorization: "Bearer " + accessToken,
"Content-Type": "application/json",
"Accept-Encoding": "application/json",
},
})).data.data.app.secrets;
const deleteSecretsKeys = getSecretsRes
.filter((secret: FlyioSecret) => !(secret.name in secrets))
.map((secret: FlyioSecret) => secret.name);
// unset (delete) secrets
const DeleteSecrets = `mutation($input: UnsetSecretsInput!) {
unsetSecrets(input: $input) {
release {
id
version
reason
description
user {
id
email
name
}
evaluationId
createdAt
}
}
}`;
await standardRequest.post(INTEGRATION_FLYIO_API_URL, {
query: DeleteSecrets,
variables: {
input: {
appId: integration.app,
keys: deleteSecretsKeys,
},
},
}, {
headers: {
Authorization: "Bearer " + accessToken,
"Content-Type": "application/json",
"Accept-Encoding": "application/json",
},
});
};
/**
* Sync/push [secrets] to CircleCI project
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for CircleCI integration
*/
const syncSecretsCircleCI = async ({
integration,
secrets,
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
const circleciOrganizationDetail = (
await standardRequest.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, {
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json",
},
})
).data[0];
const { slug } = circleciOrganizationDetail;
// sync secrets to CircleCI
Object.keys(secrets).forEach(
async (key) =>
await standardRequest.post(
`${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`,
{
name: key,
value: secrets[key],
},
{
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json",
},
}
)
);
// get secrets from CircleCI
const getSecretsRes = (
await standardRequest.get(
`${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`,
{
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json",
},
}
)
).data?.items;
// delete secrets from CircleCI
getSecretsRes.forEach(async (sec: any) => {
if (!(sec.name in secrets)) {
await standardRequest.delete(
`${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar/${sec.name}`,
{
headers: {
"Circle-Token": accessToken,
"Content-Type": "application/json",
},
}
);
}
});
};
/**
* Sync/push [secrets] to TravisCI project
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for TravisCI integration
*/
const syncSecretsTravisCI = async ({
integration,
secrets,
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
// get secrets from travis-ci
const getSecretsRes = (
await standardRequest.get(
`${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars?repository_id=${integration.appId}`,
{
headers: {
"Authorization": `token ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
)
)
.data
?.env_vars
.reduce((obj: any, secret: any) => ({
...obj,
[secret.name]: secret,
}), {});
// add secrets
for await (const key of Object.keys(secrets)) {
if (!(key in getSecretsRes)) {
// case: secret does not exist in travis ci
// -> add secret
await standardRequest.post(
`${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars?repository_id=${integration.appId}`,
{
env_var: {
name: key,
value: secrets[key],
},
},
{
headers: {
"Authorization": `token ${accessToken}`,
"Content-Type": "application/json",
"Accept-Encoding": "application/json",
},
}
);
} else {
// case: secret exists in travis ci
// -> update/set secret
await standardRequest.patch(
`${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${getSecretsRes[key].id}?repository_id=${getSecretsRes[key].repository_id}`,
{
env_var: {
name: key,
value: secrets[key],
},
},
{
headers: {
"Authorization": `token ${accessToken}`,
"Content-Type": "application/json",
"Accept-Encoding": "application/json",
},
}
);
}
}
for await (const key of Object.keys(getSecretsRes)) {
if (!(key in secrets)){
// delete secret
await standardRequest.delete(
`${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${getSecretsRes[key].id}?repository_id=${getSecretsRes[key].repository_id}`,
{
headers: {
"Authorization": `token ${accessToken}`,
"Content-Type": "application/json",
"Accept-Encoding": "application/json",
},
}
);
}
}
}
/**
* Sync/push [secrets] to GitLab repo with name [integration.app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for GitLab integration
*/
const syncSecretsGitLab = async ({
integration,
secrets,
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
interface GitLabSecret {
key: string;
value: string;
environment_scope: string;
}
const getAllEnvVariables = async (integrationAppId: string, accessToken: string) => {
const gitLabApiUrl = `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integrationAppId}/variables`;
const headers = {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
};
let allEnvVariables: GitLabSecret[] = [];
let url: string | null = `${gitLabApiUrl}?per_page=100`;
while (url) {
const response: any = await standardRequest.get(url, { headers });
allEnvVariables = [...allEnvVariables, ...response.data];
const linkHeader = response.headers.link;
const nextLink = linkHeader?.split(",").find((part: string) => part.includes('rel="next"'));
if (nextLink) {
url = nextLink.trim().split(";")[0].slice(1, -1);
} else {
url = null;
}
}
return allEnvVariables;
};
const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken);
const getSecretsRes: GitLabSecret[] = allEnvVariables.filter((secret: GitLabSecret) =>
secret.environment_scope === integration.targetEnvironment
);
for await (const key of Object.keys(secrets)) {
const existingSecret = getSecretsRes.find((s: any) => s.key == key);
if (!existingSecret) {
await standardRequest.post(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`,
{
key: key,
value: secrets[key],
protected: false,
masked: false,
raw: false,
environment_scope: integration.targetEnvironment,
},
{
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
"Accept-Encoding": "application/json",
},
}
)
} else {
// update secret
if (secrets[key] !== existingSecret.value) {
await standardRequest.put(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`,
{
...existingSecret,
value: secrets[existingSecret.key],
},
{
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
"Accept-Encoding": "application/json",
},
}
);
}
}
}
// delete secrets
for await (const sec of getSecretsRes) {
if (!(sec.key in secrets)) {
await standardRequest.delete(
`${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${sec.key}?filter[environment_scope]=${integration.targetEnvironment}`,
{
headers: {
"Authorization": `Bearer ${accessToken}`,
},
}
);
}
}
}
/**
* Sync/push [secrets] to Supabase with name [integration.app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Supabase integration
*/
const syncSecretsSupabase = async ({
integration,
secrets,
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
const { data: getSecretsRes } = await standardRequest.get(
`${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
// convert the secrets to [{}] format
const modifiedFormatForSecretInjection = Object.keys(secrets).map(
(key) => {
return {
name: key,
value: secrets[key],
};
}
);
await standardRequest.post(
`${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`,
modifiedFormatForSecretInjection,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
const secretsToDelete: any = [];
getSecretsRes?.forEach((secretObj: any) => {
if (!(secretObj.name in secrets)) {
secretsToDelete.push(secretObj.name);
}
});
await standardRequest.delete(
`${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
"Accept-Encoding": "application/json",
},
data: secretsToDelete,
}
);
};
/**
* Sync/push [secrets] to Checkly app
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Checkly integration
*/
const syncSecretsCheckly = async ({
integration,
secrets,
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
// get secrets from travis-ci
const getSecretsRes = (
await standardRequest.get(
`${INTEGRATION_CHECKLY_API_URL}/v1/variables`,
{
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
"X-Checkly-Account": integration.appId,
},
}
)
)
.data
.reduce((obj: any, secret: any) => ({
...obj,
[secret.key]: secret.value,
}), {});
// add secrets
for await (const key of Object.keys(secrets)) {
if (!(key in getSecretsRes)) {
// case: secret does not exist in checkly
// -> add secret
await standardRequest.post(
`${INTEGRATION_CHECKLY_API_URL}/v1/variables`,
{
key,
value: secrets[key],
},
{
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept": "application/json",
"Content-Type": "application/json",
"X-Checkly-Account": integration.appId,
},
}
);
} else {
// case: secret exists in checkly
// -> update/set secret
if (secrets[key] !== getSecretsRes[key]) {
await standardRequest.put(
`${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`,
{
value: secrets[key],
},
{
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
"Accept": "application/json",
"X-Checkly-Account": integration.appId,
},
}
);
}
}
}
for await (const key of Object.keys(getSecretsRes)) {
if (!(key in secrets)){
// delete secret
await standardRequest.delete(
`${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`,
{
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept": "application/json",
"X-Checkly-Account": integration.appId,
},
}
);
}
}
};
/**
* Sync/push [secrets] to HashiCorp Vault path
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for HashiCorp Vault integration
*/
const syncSecretsHashiCorpVault = async ({
integration,
integrationAuth,
secrets,
accessId,
accessToken,
}: {
integration: IIntegration;
integrationAuth: IIntegrationAuth;
secrets: any;
accessId: string | null;
accessToken: string;
}) => {
if (!accessId) return;
interface LoginAppRoleRes {
auth: {
client_token: string;
}
}
// get Vault client token (could be optimized)
const { data }: { data: LoginAppRoleRes } = await standardRequest.post(
`${integrationAuth.url}/v1/auth/approle/login`,
{
"role_id": accessId,
"secret_id": accessToken,
},
{
headers: {
"X-Vault-Namespace": integrationAuth.namespace,
},
}
);
const clientToken = data.auth.client_token;
await standardRequest.post(
`${integrationAuth.url}/v1/${integration.app}/data/${integration.path}`,
{
data: secrets,
},
{
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept": "application/json",
"Content-Type": "application/json",
"X-Vault-Token": clientToken,
"X-Vault-Namespace": integrationAuth.namespace,
},
}
);
};
/**
* Sync/push [secrets] to Cloudflare Pages project with name [integration.app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - API token for Cloudflare
*/
const syncSecretsCloudflarePages = async ({
integration,
secrets,
accessId,
accessToken,
}: {
integration: IIntegration;
secrets: any;
accessId: string | null;
accessToken: string;
}) => {
// get secrets from cloudflare pages
const getSecretsRes = (
await standardRequest.get(
`${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
)
)
.data.result["deployment_configs"][integration.targetEnvironment]["env_vars"];
// copy the secrets object, so we can set deleted keys to null
const secretsObj: any = {...secrets};
for (const [key, val] of Object.entries(secretsObj)) {
secretsObj[key] = { type: "secret_text", value: val };
}
if (getSecretsRes) {
for await (const key of Object.keys(getSecretsRes)) {
if (!(key in secrets)) {
// case: secret does not exist in infisical
// -> delete secret from cloudflare pages
secretsObj[key] = null;
}
}
}
const data = {
"deployment_configs": {
[integration.targetEnvironment]: {
"env_vars": secretsObj
}
}
};
await standardRequest.patch(
`${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}`,
data,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept": "application/json",
},
}
);
}
/* Sync/push [secrets] to Northflank
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Northflank integration
*/
const syncSecretsNorthflank = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
// secrets: {
// secretGroupID: 'some_id',
// secretGroupName: 'some_name',
// data: {}
// }
const {
data: {
secrets: getSecretsRes
}
} = await standardRequest.get(
`${INTEGRATION_NORTHFLANK_API_URL}/v1/projects/${integration.appId}/secrets`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
const secretGroups = getSecretsRes.map((group: any) => {
return {
id: group.id,
name: group.name
};
})
for await (const group of secretGroups) {
if (group.id === secrets.secretGroupID) {
// add secret to existing group
let {
data: {
secrets: {
variables
}
}
} = await standardRequest.get(
`${INTEGRATION_NORTHFLANK_API_URL}/v1/projects/${integration.appId}/secrets/${secrets.secretGroupID}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
variables = { ...secrets.data }
const modifiedFormatForSecretInjection = {
secrets: {
variables
}
}
await standardRequest.post(
`${INTEGRATION_NORTHFLANK_API_URL}/v1/projects/${integration.appId}/secrets/${secrets.secretGroupID}`,
modifiedFormatForSecretInjection,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
} else {
// create new secret group
const modifiedFormatForSecretInjection = {
name: secrets.secretGroupName,
secretType: "environment",
priority: 10,
secrets: {
variables: secrets.data
}
};
await standardRequest.post(
`${INTEGRATION_NORTHFLANK_API_URL}/v1/projects/${integration.appId}/secrets`,
modifiedFormatForSecretInjection,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
);
}
}
// TODO:: figure out delete business logic for secret groups
};
export { syncSecrets };