Iron out naming / text, update docs for Heroku integration

This commit is contained in:
Tuan Dang
2024-03-08 18:12:52 -08:00
parent 8ac7a29893
commit 89e109e404
17 changed files with 325 additions and 329 deletions
+2 -1
View File
@@ -422,10 +422,11 @@ export const registerRoutes = async (
projectMembershipDAL, projectMembershipDAL,
smtpService, smtpService,
projectDAL, projectDAL,
projectBotDAL,
secretVersionDAL, secretVersionDAL,
secretBlindIndexDAL, secretBlindIndexDAL,
secretTagDAL, secretTagDAL,
secretVersionTagDAL, secretVersionTagDAL
}); });
const secretBlindIndexService = secretBlindIndexServiceFactory({ const secretBlindIndexService = secretBlindIndexServiceFactory({
permissionService, permissionService,
@@ -32,7 +32,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
.object({ .object({
secretPrefix: z.string().optional(), secretPrefix: z.string().optional(),
secretSuffix: z.string().optional(), secretSuffix: z.string().optional(),
syncBehavior: z.string().optional(), initialSyncBehavior: z.string().optional(),
secretGCPLabel: z secretGCPLabel: z
.object({ .object({
labelName: z.string(), labelName: z.string(),
@@ -43,7 +43,6 @@ import {
import { getIntegrationOptions, Integrations, IntegrationUrls } from "./integration-list"; import { getIntegrationOptions, Integrations, IntegrationUrls } from "./integration-list";
import { getTeams } from "./integration-team"; import { getTeams } from "./integration-team";
import { exchangeCode, exchangeRefresh } from "./integration-token"; import { exchangeCode, exchangeRefresh } from "./integration-token";
import { access } from "node:fs";
type TIntegrationAuthServiceFactoryDep = { type TIntegrationAuthServiceFactoryDep = {
integrationAuthDAL: TIntegrationAuthDALFactory; integrationAuthDAL: TIntegrationAuthDALFactory;
@@ -603,7 +602,7 @@ export const integrationAuthServiceFactory = ({
} }
} }
); );
return data.map(({ app: { id: appId }, stage, pipeline: { id: pipelineId, name } }) => ({ return data.map(({ app: { id: appId }, stage, pipeline: { id: pipelineId, name } }) => ({
app: { appId }, app: { appId },
stage, stage,
@@ -37,7 +37,7 @@ export enum IntegrationType {
OAUTH2 = "oauth2" OAUTH2 = "oauth2"
} }
export enum IntegrationSyncBehavior { export enum IntegrationInitialSyncBehavior {
OVERWRITE_TARGET = "overwrite-target", OVERWRITE_TARGET = "overwrite-target",
PREFER_TARGET = "prefer-target", PREFER_TARGET = "prefer-target",
PREFER_SOURCE = "prefer-source" PREFER_SOURCE = "prefer-source"
@@ -20,23 +20,13 @@ import sodium from "libsodium-wrappers";
import isEqual from "lodash.isequal"; import isEqual from "lodash.isequal";
import { z } from "zod"; import { z } from "zod";
import { SecretType, TIntegrationAuths, TIntegrations } from "@app/db/schemas"; import { SecretType, TIntegrationAuths, TIntegrations, TSecrets } from "@app/db/schemas";
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types";
import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { TIntegrationDALFactory } from "../integration/integration-dal"; import { TIntegrationDALFactory } from "../integration/integration-dal";
import { import { IntegrationInitialSyncBehavior, Integrations, IntegrationUrls } from "./integration-list";
createManySecretsRawHelper
// updateManySecretsRawHelper
} from "../secret/secret-fns";
import { Integrations, IntegrationSyncBehavior, IntegrationUrls } from "./integration-list";
const getSecretKeyValuePair = (secrets: Record<string, { value: string | null; comment?: string } | null>) => const getSecretKeyValuePair = (secrets: Record<string, { value: string | null; comment?: string } | null>) =>
Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => { Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => {
@@ -594,35 +584,25 @@ const syncSecretsAWSSecretManager = async ({
* Sync/push [secrets] to Heroku app named [integration.app] * Sync/push [secrets] to Heroku app named [integration.app]
*/ */
const syncSecretsHeroku = async ({ const syncSecretsHeroku = async ({
projectDAL, createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL, integrationDAL,
secretDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL,
botKey, // TODO: consider getting botKey inside this fn
projectId,
environment,
secretPath,
integration, integration,
secrets, secrets,
accessToken accessToken
}: { }: {
projectDAL: TProjectDALFactory; createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
integrationDAL: Pick<TIntegrationDALFactory, "updateById">; integrationDAL: Pick<TIntegrationDALFactory, "updateById">;
secretDAL: TSecretDALFactory; integration: TIntegrations & {
secretVersionDAL: TSecretVersionDALFactory; projectId: string;
secretBlindIndexDAL: TSecretBlindIndexDALFactory; environment: {
secretTagDAL: TSecretTagDALFactory; id: string;
secretVersionTagDAL: TSecretVersionTagDALFactory; name: string;
folderDAL: TSecretFolderDALFactory; slug: string;
botKey: string; };
projectId: string; secretPath: string;
environment: string; };
secretPath: string;
integration: TIntegrations;
secrets: Record<string, { value: string; comment?: string } | null>; secrets: Record<string, { value: string; comment?: string } | null>;
accessToken: string; accessToken: string;
}) => { }) => {
@@ -644,13 +624,13 @@ const syncSecretsHeroku = async ({
Object.keys(herokuSecrets).forEach((key) => { Object.keys(herokuSecrets).forEach((key) => {
if (!integration.lastUsed) { if (!integration.lastUsed) {
// first time using integration // first time using integration
// -> apply initial sync behavior rule // -> apply initial sync behavior
switch (metadata.syncBehavior) { switch (metadata.initialSyncBehavior) {
case IntegrationSyncBehavior.OVERWRITE_TARGET: { case IntegrationInitialSyncBehavior.OVERWRITE_TARGET: {
if (!(key in secrets)) secrets[key] = null; if (!(key in secrets)) secrets[key] = null;
break; break;
} }
case IntegrationSyncBehavior.PREFER_TARGET: { case IntegrationInitialSyncBehavior.PREFER_TARGET: {
if (!(key in secrets)) { if (!(key in secrets)) {
secretsToAdd[key] = herokuSecrets[key]; secretsToAdd[key] = herokuSecrets[key];
} else if (secrets[key]?.value !== herokuSecrets[key]) { } else if (secrets[key]?.value !== herokuSecrets[key]) {
@@ -661,7 +641,7 @@ const syncSecretsHeroku = async ({
}; };
break; break;
} }
case IntegrationSyncBehavior.PREFER_SOURCE: { case IntegrationInitialSyncBehavior.PREFER_SOURCE: {
if (!(key in secrets)) { if (!(key in secrets)) {
secrets[key] = herokuSecrets[key]; secrets[key] = herokuSecrets[key];
secretsToAdd[key] = herokuSecrets[key]; secretsToAdd[key] = herokuSecrets[key];
@@ -677,18 +657,10 @@ const syncSecretsHeroku = async ({
}); });
if (Object.keys(secretsToAdd).length) { if (Object.keys(secretsToAdd).length) {
await createManySecretsRawHelper({ await createManySecretsRawFn({
botKey, projectId: integration.projectId,
projectDAL, environment: integration.environment.slug,
secretDAL, path: integration.secretPath,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL,
projectId,
environment,
path: secretPath,
secrets: Object.keys(secretsToAdd).map((key) => ({ secrets: Object.keys(secretsToAdd).map((key) => ({
secretName: key, secretName: key,
secretValue: secretsToAdd[key], secretValue: secretsToAdd[key],
@@ -698,27 +670,19 @@ const syncSecretsHeroku = async ({
}); });
} }
// if (Object.keys(secretsToUpdate).length) { if (Object.keys(secretsToUpdate).length) {
// await updateManySecretsRawHelper({ await updateManySecretsRawFn({
// projectId, projectId: integration.projectId,
// environment, environment: integration.environment.slug,
// path: secretPath, path: integration.secretPath,
// secrets: Object.keys(secretsToUpdate).map((key) => ({ secrets: Object.keys(secretsToUpdate).map((key) => ({
// secretName: key, secretName: key,
// secretValue: secretsToUpdate[key], secretValue: secretsToUpdate[key],
// type: SecretType.Shared, type: SecretType.Shared,
// secretComment: "" secretComment: ""
// })), }))
// botKey, // TODO: consider getting botKey inside this fn });
// projectDAL, }
// secretDAL,
// secretVersionDAL,
// secretBlindIndexDAL,
// secretTagDAL,
// secretVersionTagDAL,
// folderDAL
// });
// }
await request.patch( await request.patch(
`${IntegrationUrls.HEROKU_API_URL}/apps/${integration.app}/config-vars`, `${IntegrationUrls.HEROKU_API_URL}/apps/${integration.app}/config-vars`,
@@ -3053,18 +3017,9 @@ const syncSecretsHasuraCloud = async ({
* *
*/ */
export const syncIntegrationSecrets = async ({ export const syncIntegrationSecrets = async ({
projectDAL, createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL, integrationDAL,
secretDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL,
botKey,
projectId,
environment,
secretPath,
integration, integration,
integrationAuth, integrationAuth,
secrets, secrets,
@@ -3072,19 +3027,18 @@ export const syncIntegrationSecrets = async ({
accessToken, accessToken,
appendices appendices
}: { }: {
projectDAL: TProjectDALFactory; createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
integrationDAL: Pick<TIntegrationDALFactory, "updateById">; integrationDAL: Pick<TIntegrationDALFactory, "updateById">;
secretDAL: TSecretDALFactory; integration: TIntegrations & {
secretVersionDAL: TSecretVersionDALFactory; projectId: string;
secretBlindIndexDAL: TSecretBlindIndexDALFactory; environment: {
secretTagDAL: TSecretTagDALFactory; id: string;
secretVersionTagDAL: TSecretVersionTagDALFactory; name: string;
folderDAL: TSecretFolderDALFactory; slug: string;
botKey: string; };
projectId: string; secretPath: string;
environment: string; };
secretPath: string;
integration: TIntegrations;
integrationAuth: TIntegrationAuths; integrationAuth: TIntegrationAuths;
secrets: Record<string, { value: string; comment?: string }>; secrets: Record<string, { value: string; comment?: string }>;
accessId: string | null; accessId: string | null;
@@ -3124,18 +3078,9 @@ export const syncIntegrationSecrets = async ({
break; break;
case Integrations.HEROKU: case Integrations.HEROKU:
await syncSecretsHeroku({ await syncSecretsHeroku({
projectDAL, createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL, integrationDAL,
secretDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL,
botKey,
projectId,
environment,
secretPath,
integration, integration,
secrets, secrets,
accessToken accessToken
@@ -0,0 +1,36 @@
import { SecretKeyEncoding } from "@app/db/schemas";
import { decryptAsymmetric, infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TGetPrivateKeyDTO } from "./project-bot-types";
export const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) =>
infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
export const getBotKeyFnFactory = (projectBotDAL: TProjectBotDALFactory) => {
const getBotKeyFn = async (projectId: string) => {
const bot = await projectBotDAL.findOne({ projectId });
if (!bot) throw new BadRequestError({ message: "failed to find bot key" });
if (!bot.isActive) throw new BadRequestError({ message: "Bot is not active" });
if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey)
throw new BadRequestError({ message: "Encryption key missing" });
const botPrivateKey = getBotPrivateKey({ bot });
return decryptAsymmetric({
ciphertext: bot.encryptedProjectKey,
privateKey: botPrivateKey,
nonce: bot.encryptedProjectKeyNonce,
publicKey: bot.sender.publicKey
});
};
return getBotKeyFn;
};
@@ -1,15 +1,16 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { ProjectVersion, SecretKeyEncoding } from "@app/db/schemas"; import { ProjectVersion } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { decryptAsymmetric, generateAsymmetricKeyPair } from "@app/lib/crypto"; import { generateAsymmetricKeyPair } from "@app/lib/crypto";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotDALFactory } from "./project-bot-dal"; import { TProjectBotDALFactory } from "./project-bot-dal";
import { TFindBotByProjectIdDTO, TGetPrivateKeyDTO, TSetActiveStateDTO } from "./project-bot-types"; import { getBotKeyFnFactory, getBotPrivateKey } from "./project-bot-fns";
import { TFindBotByProjectIdDTO, TSetActiveStateDTO } from "./project-bot-types";
type TProjectBotServiceFactoryDep = { type TProjectBotServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
@@ -24,29 +25,10 @@ export const projectBotServiceFactory = ({
projectDAL, projectDAL,
permissionService permissionService
}: TProjectBotServiceFactoryDep) => { }: TProjectBotServiceFactoryDep) => {
const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) => const getBotKeyFn = getBotKeyFnFactory(projectBotDAL);
infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const getBotKey = async (projectId: string) => { const getBotKey = async (projectId: string) => {
const bot = await projectBotDAL.findOne({ projectId }); return getBotKeyFn(projectId);
if (!bot) throw new BadRequestError({ message: "failed to find bot key" });
if (!bot.isActive) throw new BadRequestError({ message: "Bot is not active" });
if (!bot.encryptedProjectKeyNonce || !bot.encryptedProjectKey)
throw new BadRequestError({ message: "Encryption key missing" });
const botPrivateKey = getBotPrivateKey({ bot });
return decryptAsymmetric({
ciphertext: bot.encryptedProjectKey,
privateKey: botPrivateKey,
nonce: bot.encryptedProjectKeyNonce,
publicKey: bot.sender.publicKey
});
}; };
const findBotByProjectId = async ({ const findBotByProjectId = async ({
+100 -94
View File
@@ -18,10 +18,12 @@ import {
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { groupBy, unique } from "@app/lib/fn"; import { groupBy, unique } from "@app/lib/fn";
import { getBotKeyFnFactory } from "../project-bot/project-bot-fns";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretDALFactory } from "./secret-dal"; import { TSecretDALFactory } from "./secret-dal";
import { import {
TCreateManySecretsRawHelper, TCreateManySecretsRawFn,
TCreateManySecretsRawFnFactory,
TFnSecretBlindIndexCheck, TFnSecretBlindIndexCheck,
TFnSecretBulkInsert, TFnSecretBulkInsert,
TFnSecretBulkUpdate, TFnSecretBulkUpdate,
@@ -405,112 +407,114 @@ export const fnSecretBulkUpdate = async ({
return newSecrets.map((secret) => ({ ...secret, _id: secret.id })); return newSecrets.map((secret) => ({ ...secret, _id: secret.id }));
}; };
export const createManySecretsRawHelper = async ({ export const createManySecretsRawFnFactory = ({
projectId,
environment,
path: secretPath,
secrets,
userId,
botKey, // TODO: consider getting botKey inside this fn
projectDAL, projectDAL,
projectBotDAL,
secretDAL, secretDAL,
secretVersionDAL, secretVersionDAL,
secretBlindIndexDAL, secretBlindIndexDAL,
secretTagDAL, secretTagDAL,
secretVersionTagDAL, secretVersionTagDAL,
folderDAL folderDAL
}: TCreateManySecretsRawHelper) => { }: TCreateManySecretsRawFnFactory) => {
await projectDAL.checkProjectUpgradeStatus(projectId); const getBotKeyFn = getBotKeyFnFactory(projectBotDAL);
const createManySecretsRawFn = async ({
projectId,
environment,
path: secretPath,
secrets,
userId
}: TCreateManySecretsRawFn) => {
const botKey = await getBotKeyFn(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); await projectDAL.checkProjectUpgradeStatus(projectId);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
const folderId = folder.id;
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId }); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Create secret" }); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
const folderId = folder.id;
// insert operation const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
const { keyName2BlindIndex } = await fnSecretBlindIndexCheck({ if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Create secret" });
inputSecrets: secrets,
folderId,
isNew: true,
blindIndexCfg,
secretDAL
});
const inputSecrets = await Promise.all( // insert operation
secrets.map(async (secret) => { const { keyName2BlindIndex } = await fnSecretBlindIndexCheck({
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey); inputSecrets: secrets,
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey);
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretComment || "", botKey);
if (secret.type === SecretType.Personal) {
if (!userId) throw new BadRequestError({ message: "Missing user id for personal secret" });
const sharedExist = await secretDAL.findOne({
secretBlindIndex: keyName2BlindIndex[secret.secretName],
folderId,
type: SecretType.Shared
});
if (!sharedExist)
throw new BadRequestError({
message: "Failed to create personal secret override for no corresponding shared secret"
});
}
const tags = secret.tags ? await secretTagDAL.findManyTagsById(projectId, secret.tags) : [];
if ((secret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
return {
type: secret.type,
userId: secret.type === SecretType.Personal ? userId : null,
secretName: secret.secretName,
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
secretKeyIV: secretKeyEncrypted.iv,
secretKeyTag: secretKeyEncrypted.tag,
secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag,
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag,
skipMultilineEncoding: secret.skipMultilineEncoding,
tags: secret.tags
};
})
);
const newSecrets = await secretDAL.transaction(async (tx) =>
fnSecretBulkInsert({
inputSecrets: inputSecrets.map(({ secretName, ...el }) => ({
...el,
version: 0,
secretBlindIndex: keyName2BlindIndex[secretName],
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8
})),
folderId, folderId,
secretDAL, isNew: true,
secretVersionDAL, blindIndexCfg,
secretTagDAL, secretDAL
secretVersionTagDAL, });
tx
})
);
return newSecrets; const inputSecrets = await Promise.all(
secrets.map(async (secret) => {
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey);
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretComment || "", botKey);
if (secret.type === SecretType.Personal) {
if (!userId) throw new BadRequestError({ message: "Missing user id for personal secret" });
const sharedExist = await secretDAL.findOne({
secretBlindIndex: keyName2BlindIndex[secret.secretName],
folderId,
type: SecretType.Shared
});
if (!sharedExist)
throw new BadRequestError({
message: "Failed to create personal secret override for no corresponding shared secret"
});
}
const tags = secret.tags ? await secretTagDAL.findManyTagsById(projectId, secret.tags) : [];
if ((secret.tags || []).length !== tags.length) throw new BadRequestError({ message: "Tag not found" });
return {
type: secret.type,
userId: secret.type === SecretType.Personal ? userId : null,
secretName: secret.secretName,
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
secretKeyIV: secretKeyEncrypted.iv,
secretKeyTag: secretKeyEncrypted.tag,
secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag,
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag,
skipMultilineEncoding: secret.skipMultilineEncoding,
tags: secret.tags
};
})
);
const newSecrets = await secretDAL.transaction(async (tx) =>
fnSecretBulkInsert({
inputSecrets: inputSecrets.map(({ secretName, ...el }) => ({
...el,
version: 0,
secretBlindIndex: keyName2BlindIndex[secretName],
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8
})),
folderId,
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
tx
})
);
return newSecrets;
};
return createManySecretsRawFn;
}; };
// TOOD: potentially convert raw stuff export const updateManySecretsRawFnFactory = ({
// updateManySecretsRawFnFactory
// updateManySecretsRawHelper
export const updateManySecretsRawFnFactory = async ({
// TODO: refactor
botKey,
projectDAL, projectDAL,
projectBotDAL,
secretDAL, secretDAL,
secretVersionDAL, secretVersionDAL,
secretBlindIndexDAL, secretBlindIndexDAL,
@@ -518,14 +522,16 @@ export const updateManySecretsRawFnFactory = async ({
secretVersionTagDAL, secretVersionTagDAL,
folderDAL folderDAL
}: TUpdateManySecretsRawFnFactory) => { }: TUpdateManySecretsRawFnFactory) => {
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL);
const updateManySecretsRawFn = async ({ const updateManySecretsRawFn = async ({
projectId, projectId,
environment, environment,
path: secretPath, path: secretPath,
secrets, // accept instead ciphertext secrets secrets, // consider accepting instead ciphertext secrets
userId userId
}: TUpdateManySecretsRawFn) => { }: TUpdateManySecretsRawFn): Promise<Array<TSecrets & { _id: string }>> => {
// TODO: fetch botKey from here const botKey = await getBotKeyFn(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
await projectDAL.checkProjectUpgradeStatus(projectId); await projectDAL.checkProjectUpgradeStatus(projectId);
+28 -24
View File
@@ -6,6 +6,8 @@ import { BadRequestError } from "@app/lib/errors";
import { isSamePath } from "@app/lib/fn"; import { isSamePath } from "@app/lib/fn";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { createManySecretsRawFnFactory, updateManySecretsRawFnFactory } from "@app/services/secret/secret-fns";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal"; import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal"; import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal"; import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
@@ -29,8 +31,6 @@ import { TSecretDALFactory } from "./secret-dal";
import { interpolateSecrets } from "./secret-fns"; import { interpolateSecrets } from "./secret-fns";
import { TCreateSecretReminderDTO, THandleReminderDTO, TRemoveSecretReminderDTO } from "./secret-types"; import { TCreateSecretReminderDTO, THandleReminderDTO, TRemoveSecretReminderDTO } from "./secret-types";
// import { updateManySecretsRawFnFactory } from "@app/services/secret/secret-fns";
export type TSecretQueueFactory = ReturnType<typeof secretQueueFactory>; export type TSecretQueueFactory = ReturnType<typeof secretQueueFactory>;
type TSecretQueueFactoryDep = { type TSecretQueueFactoryDep = {
@@ -44,6 +44,7 @@ type TSecretQueueFactoryDep = {
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">; webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: TProjectDALFactory; projectDAL: TProjectDALFactory;
projectBotDAL: TProjectBotDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
smtpService: TSmtpService; smtpService: TSmtpService;
orgDAL: Pick<TOrgDALFactory, "findOrgByProjectId">; orgDAL: Pick<TOrgDALFactory, "findOrgByProjectId">;
@@ -72,12 +73,35 @@ export const secretQueueFactory = ({
orgDAL, orgDAL,
smtpService, smtpService,
projectDAL, projectDAL,
projectBotDAL,
projectMembershipDAL, projectMembershipDAL,
secretVersionDAL, secretVersionDAL,
secretBlindIndexDAL, secretBlindIndexDAL,
secretTagDAL, secretTagDAL,
secretVersionTagDAL secretVersionTagDAL
}: TSecretQueueFactoryDep) => { }: TSecretQueueFactoryDep) => {
const createManySecretsRawFn = createManySecretsRawFnFactory({
projectDAL,
projectBotDAL,
secretDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL
});
const updateManySecretsRawFn = updateManySecretsRawFnFactory({
projectDAL,
projectBotDAL,
secretDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL
});
const syncIntegrations = async (dto: TGetSecrets) => { const syncIntegrations = async (dto: TGetSecrets) => {
await queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, { await queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, {
attempts: 5, attempts: 5,
@@ -320,30 +344,10 @@ export const secretQueueFactory = ({
}); });
} }
// const updateManySecretsRawFn = updateManySecretsRawFnFactory({
// botKey, // can move this out
// projectDAL,
// secretDAL,
// secretVersionDAL,
// secretBlindIndexDAL,
// secretTagDAL,
// secretVersionTagDAL,
// folderDAL
// });
await syncIntegrationSecrets({ await syncIntegrationSecrets({
projectDAL, createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL, integrationDAL,
secretDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL,
botKey,
projectId, // service
environment,
secretPath,
integration, integration,
integrationAuth, integrationAuth,
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets, secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets,
+14 -10
View File
@@ -3,6 +3,7 @@ import { Knex } from "knex";
import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas"; import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
import { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal"; import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal"; import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
@@ -248,7 +249,18 @@ export type TRemoveSecretReminderDTO = {
// --- // ---
export type TCreateManySecretsRawHelper = { export type TCreateManySecretsRawFnFactory = {
projectDAL: TProjectDALFactory;
projectBotDAL: TProjectBotDALFactory;
secretDAL: TSecretDALFactory;
secretVersionDAL: TSecretVersionDALFactory;
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
secretTagDAL: TSecretTagDALFactory;
secretVersionTagDAL: TSecretVersionTagDALFactory;
folderDAL: TSecretFolderDALFactory;
};
export type TCreateManySecretsRawFn = {
projectId: string; projectId: string;
environment: string; environment: string;
path: string; path: string;
@@ -264,19 +276,11 @@ export type TCreateManySecretsRawHelper = {
}; };
}[]; }[];
userId?: string; // only relevant for personal secret(s) userId?: string; // only relevant for personal secret(s)
botKey: string;
projectDAL: TProjectDALFactory;
secretDAL: TSecretDALFactory;
secretVersionDAL: TSecretVersionDALFactory;
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
secretTagDAL: TSecretTagDALFactory;
secretVersionTagDAL: TSecretVersionTagDALFactory;
folderDAL: TSecretFolderDALFactory;
}; };
export type TUpdateManySecretsRawFnFactory = { export type TUpdateManySecretsRawFnFactory = {
botKey: string;
projectDAL: TProjectDALFactory; projectDAL: TProjectDALFactory;
projectBotDAL: TProjectBotDALFactory;
secretDAL: TSecretDALFactory; secretDAL: TSecretDALFactory;
secretVersionDAL: TSecretVersionDALFactory; secretVersionDAL: TSecretVersionDALFactory;
secretBlindIndexDAL: TSecretBlindIndexDALFactory; secretBlindIndexDAL: TSecretBlindIndexDALFactory;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

After

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 371 KiB

After

Width:  |  Height:  |  Size: 700 KiB

+11
View File
@@ -30,6 +30,17 @@ description: "How to sync secrets from Infisical to Heroku"
Select which Infisical environment secrets you want to sync to which Heroku app and press create integration to start syncing secrets to Heroku. Select which Infisical environment secrets you want to sync to which Heroku app and press create integration to start syncing secrets to Heroku.
![integrations heroku](../../images/integrations/heroku/integrations-heroku-create.png) ![integrations heroku](../../images/integrations/heroku/integrations-heroku-create.png)
Here's some guidance on each field:
- Project Environment: The environment in the current Infisical project from which you want to sync secrets from.
- Secrets Path: The path in the current Infisical project from which you want to sync secrets from such as `/` (for secrets that do not reside in a folder) or `/foo/bar` (for secrets nested in a folder, in this case a folder called `bar` in another folder called `foo`).
- Heroku App: The application in Heroku that you want to sync secrets to.
- Initial Sync Behavior (default is **Import - Prefer values from Infisical**): The behavior of the first sync operation triggered after creating the integration.
- **No Import - Overwrite all values in Heroku**: Sync secrets and overwrite any existing secrets in Heroku.
- **Import - Prefer values from Infisical**: Import secrets from Heroku to Infisical; if a secret with the same name already exists in Infisical, do nothing. Afterwards, sync secrets to Heroku.
- **Import - Prefer values from Heroku**: Import secrets from Heroku to Infisical; if a secret with the same name already exists in Infisical, replace its value with the one from Heroku. Afterwards, sync secrets to Heroku.
![integrations heroku](../../images/integrations/heroku/integrations-heroku.png) ![integrations heroku](../../images/integrations/heroku/integrations-heroku.png)
</Step> </Step>
</Steps> </Steps>
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "npm-proj-1709146141702-0.772936286416932EMIzNi", "name": "frontend",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
@@ -61,7 +61,7 @@ export const useCreateIntegration = () => {
metadata?: { metadata?: {
secretPrefix?: string; secretPrefix?: string;
secretSuffix?: string; secretSuffix?: string;
syncBehavior?: string; initialSyncBehavior?: string;
} }
}) => { }) => {
const { data: { integration } } = await apiRequest.post("/api/v1/integration", { const { data: { integration } } = await apiRequest.post("/api/v1/integration", {
+1 -7
View File
@@ -45,10 +45,4 @@ export enum IntegrationSyncBehavior {
OVERWRITE_TARGET = "overwrite-target", OVERWRITE_TARGET = "overwrite-target",
PREFER_TARGET = "prefer-target", PREFER_TARGET = "prefer-target",
PREFER_SOURCE = "prefer-source" PREFER_SOURCE = "prefer-source"
} }
export const syncBehaviors = [
{ label: "Overwrite target", value: IntegrationSyncBehavior.OVERWRITE_TARGET },
{ label: "Prefer target", value: IntegrationSyncBehavior.PREFER_TARGET },
{ label: "Prefer source", value: IntegrationSyncBehavior.PREFER_SOURCE }
];
@@ -4,15 +4,22 @@ import Head from "next/head";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { faArrowUpRightFromSquare, faBookOpen, faBugs, faCircleInfo } from "@fortawesome/free-solid-svg-icons"; import {
faArrowUpRightFromSquare,
faBookOpen,
faBugs,
// faCircleInfo
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import queryString from "query-string";
import { RadioGroup } from "@app/components/v2/RadioGroup";
import { useCreateIntegration } from "@app/hooks/api";
import { useGetIntegrationAuthHerokuPipelines } from "@app/hooks/api/integrationAuth/queries";
import { App, Pipeline } from "@app/hooks/api/integrationAuth/types";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import queryString from "query-string";
// import { useGetIntegrationAuthHerokuPipelines } from "@app/hooks/api/integrationAuth/queries";
// import { App, Pipeline } from "@app/hooks/api/integrationAuth/types";
import * as yup from "yup";
// import { RadioGroup } from "@app/components/v2/RadioGroup";
import { useCreateIntegration } from "@app/hooks/api";
import { IntegrationSyncBehavior } from "@app/hooks/api/integrations/types";
import { import {
Button, Button,
@@ -27,16 +34,24 @@ import {
useGetIntegrationAuthApps, useGetIntegrationAuthApps,
useGetIntegrationAuthById useGetIntegrationAuthById
} from "../../../hooks/api/integrationAuth"; } from "../../../hooks/api/integrationAuth";
import { useCreateWsEnvironment, useGetWorkspaceById } from "../../../hooks/api/workspace"; import {
import { IntegrationSyncBehavior, syncBehaviors } from "@app/hooks/api/integrations/types"; // useCreateWsEnvironment,
useGetWorkspaceById
} from "../../../hooks/api/workspace";
const initialSyncBehaviors = [
{ label: "No Import - Overwrite all values in Heroku", value: IntegrationSyncBehavior.OVERWRITE_TARGET },
{ label: "Import - Prefer values from Heroku", value: IntegrationSyncBehavior.PREFER_TARGET },
{ label: "Import - Prefer values from Infisical", value: IntegrationSyncBehavior.PREFER_SOURCE }
];
const schema = yup.object({ const schema = yup.object({
selectedSourceEnvironment: yup.string().required("Source environment is required"), selectedSourceEnvironment: yup.string().required("Source environment is required"),
secretPath: yup.string().required("Secret path is required"), secretPath: yup.string().required("Secret path is required"),
targetApp: yup.string().required("Heroku app is required"), targetApp: yup.string().required("Heroku app is required"),
syncBehavior: yup initialSyncBehavior: yup
.string() .string()
.oneOf(syncBehaviors.map((b) => b.value), "Invalid sync behavior") .oneOf(initialSyncBehaviors.map((b) => b.value), "Invalid initial sync behavior")
.required("Initial sync behavior is required") .required("Initial sync behavior is required")
}); });
@@ -49,7 +64,7 @@ export default function HerokuCreateIntegrationPage() {
resolver: yupResolver(schema), resolver: yupResolver(schema),
defaultValues: { defaultValues: {
secretPath: "/", secretPath: "/",
syncBehavior: IntegrationSyncBehavior.PREFER_SOURCE initialSyncBehavior: IntegrationSyncBehavior.PREFER_SOURCE
} }
}); });
@@ -57,7 +72,7 @@ export default function HerokuCreateIntegrationPage() {
const { mutateAsync } = useCreateIntegration(); const { mutateAsync } = useCreateIntegration();
const { mutateAsync: mutateAsyncEnv } = useCreateWsEnvironment(); // const { mutateAsync: mutateAsyncEnv } = useCreateWsEnvironment();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]); const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
@@ -67,14 +82,14 @@ export default function HerokuCreateIntegrationPage() {
integrationAuthId: (integrationAuthId as string) ?? "" integrationAuthId: (integrationAuthId as string) ?? ""
}); });
const { data: integrationAuthPipelineCouplings } = useGetIntegrationAuthHerokuPipelines({ // const { data: integrationAuthPipelineCouplings } = useGetIntegrationAuthHerokuPipelines({
integrationAuthId: (integrationAuthId as string) ?? "" // integrationAuthId: (integrationAuthId as string) ?? ""
}); // });
const [uniquePipelines, setUniquePipelines] = useState<Pipeline[]>(); // const [uniquePipelines, setUniquePipelines] = useState<Pipeline[]>();
const [selectedPipeline, setSelectedPipeline] = useState(""); // const [selectedPipeline, setSelectedPipeline] = useState("");
const [selectedPipelineApps, setSelectedPipelineApps] = useState<App[]>(); // const [selectedPipelineApps, setSelectedPipelineApps] = useState<App[]>();
const [integrationType, setIntegrationType] = useState("App"); // const [integrationType, setIntegrationType] = useState("App");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -84,37 +99,37 @@ export default function HerokuCreateIntegrationPage() {
} }
}, [workspace]); }, [workspace]);
useEffect(() => { // useEffect(() => {
if (integrationAuthPipelineCouplings) { // if (integrationAuthPipelineCouplings) {
const uniquePipelinesConst = Array.from( // const uniquePipelinesConst = Array.from(
new Set( // new Set(
integrationAuthPipelineCouplings // integrationAuthPipelineCouplings
.map(({ pipeline: { pipelineId, name } }) => ({ // .map(({ pipeline: { pipelineId, name } }) => ({
name, // name,
pipelineId // pipelineId
})) // }))
.map((obj) => JSON.stringify(obj)) // .map((obj) => JSON.stringify(obj))
)).map((str) => JSON.parse(str)) as { pipelineId: string; name: string }[] // )).map((str) => JSON.parse(str)) as { pipelineId: string; name: string }[]
[... (new Set())] // [... (new Set())]
setUniquePipelines(uniquePipelinesConst); // setUniquePipelines(uniquePipelinesConst);
if (uniquePipelinesConst) { // if (uniquePipelinesConst) {
if (uniquePipelinesConst!.length > 0) { // if (uniquePipelinesConst!.length > 0) {
setSelectedPipeline(uniquePipelinesConst![0].name); // setSelectedPipeline(uniquePipelinesConst![0].name);
} else { // } else {
setSelectedPipeline("none"); // setSelectedPipeline("none");
} // }
} // }
} // }
}, [integrationAuthPipelineCouplings]); // }, [integrationAuthPipelineCouplings]);
useEffect(() => { // useEffect(() => {
if (integrationAuthPipelineCouplings) { // if (integrationAuthPipelineCouplings) {
setSelectedPipelineApps(integrationAuthApps?.filter(app => integrationAuthPipelineCouplings // setSelectedPipelineApps(integrationAuthApps?.filter(app => integrationAuthPipelineCouplings
.filter((pipelineCoupling) => pipelineCoupling.pipeline.name === selectedPipeline) // .filter((pipelineCoupling) => pipelineCoupling.pipeline.name === selectedPipeline)
.map(coupling => coupling.app.appId).includes(String(app.appId)))) // .map(coupling => coupling.app.appId).includes(String(app.appId))))
} // }
}, [selectedPipeline]); // }, [selectedPipeline]);
useEffect(() => { useEffect(() => {
if (integrationAuthApps) { if (integrationAuthApps) {
@@ -167,10 +182,9 @@ export default function HerokuCreateIntegrationPage() {
// }; // };
const onFormSubmit = async ({ const onFormSubmit = async ({
selectedSourceEnvironment: sce,
secretPath, secretPath,
targetApp, targetApp,
syncBehavior, initialSyncBehavior,
}: FormData) => { }: FormData) => {
try { try {
if (!integrationAuth?.id) return; if (!integrationAuth?.id) return;
@@ -184,7 +198,7 @@ export default function HerokuCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment, sourceEnvironment: selectedSourceEnvironment,
secretPath, secretPath,
metadata: { metadata: {
syncBehavior initialSyncBehavior
} }
}); });
@@ -314,7 +328,7 @@ export default function HerokuCreateIntegrationPage() {
/> />
<Controller <Controller
control={control} control={control}
name="syncBehavior" name="initialSyncBehavior"
render={({ field: { onChange, ...field }, fieldState: { error } }) => ( render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl <FormControl
label="Initial Sync Behavior" label="Initial Sync Behavior"
@@ -322,7 +336,7 @@ export default function HerokuCreateIntegrationPage() {
isError={Boolean(error)} isError={Boolean(error)}
> >
<Select {...field} onValueChange={(e) => onChange(e)} className="w-full"> <Select {...field} onValueChange={(e) => onChange(e)} className="w-full">
{syncBehaviors.map((b) => { {initialSyncBehaviors.map((b) => {
return ( return (
<SelectItem value={b.value} key={`sync-behavior-${b.value}`}> <SelectItem value={b.value} key={`sync-behavior-${b.value}`}>
{b.label} {b.label}