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

View File

@@ -422,10 +422,11 @@ export const registerRoutes = async (
projectMembershipDAL,
smtpService,
projectDAL,
projectBotDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL,
secretVersionTagDAL
});
const secretBlindIndexService = secretBlindIndexServiceFactory({
permissionService,

View File

@@ -32,7 +32,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
.object({
secretPrefix: z.string().optional(),
secretSuffix: z.string().optional(),
syncBehavior: z.string().optional(),
initialSyncBehavior: z.string().optional(),
secretGCPLabel: z
.object({
labelName: z.string(),

View File

@@ -43,7 +43,6 @@ import {
import { getIntegrationOptions, Integrations, IntegrationUrls } from "./integration-list";
import { getTeams } from "./integration-team";
import { exchangeCode, exchangeRefresh } from "./integration-token";
import { access } from "node:fs";
type TIntegrationAuthServiceFactoryDep = {
integrationAuthDAL: TIntegrationAuthDALFactory;
@@ -603,7 +602,7 @@ export const integrationAuthServiceFactory = ({
}
}
);
return data.map(({ app: { id: appId }, stage, pipeline: { id: pipelineId, name } }) => ({
app: { appId },
stage,

View File

@@ -37,7 +37,7 @@ export enum IntegrationType {
OAUTH2 = "oauth2"
}
export enum IntegrationSyncBehavior {
export enum IntegrationInitialSyncBehavior {
OVERWRITE_TARGET = "overwrite-target",
PREFER_TARGET = "prefer-target",
PREFER_SOURCE = "prefer-source"

View File

@@ -20,23 +20,13 @@ import sodium from "libsodium-wrappers";
import isEqual from "lodash.isequal";
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 { BadRequestError } from "@app/lib/errors";
import { TProjectDALFactory } from "@app/services/project/project-dal";
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 { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/secret/secret-types";
import { TIntegrationDALFactory } from "../integration/integration-dal";
import {
createManySecretsRawHelper
// updateManySecretsRawHelper
} from "../secret/secret-fns";
import { Integrations, IntegrationSyncBehavior, IntegrationUrls } from "./integration-list";
import { IntegrationInitialSyncBehavior, Integrations, IntegrationUrls } from "./integration-list";
const getSecretKeyValuePair = (secrets: Record<string, { value: string | null; comment?: string } | null>) =>
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]
*/
const syncSecretsHeroku = async ({
projectDAL,
createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL,
secretDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL,
botKey, // TODO: consider getting botKey inside this fn
projectId,
environment,
secretPath,
integration,
secrets,
accessToken
}: {
projectDAL: TProjectDALFactory;
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
integrationDAL: Pick<TIntegrationDALFactory, "updateById">;
secretDAL: TSecretDALFactory;
secretVersionDAL: TSecretVersionDALFactory;
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
secretTagDAL: TSecretTagDALFactory;
secretVersionTagDAL: TSecretVersionTagDALFactory;
folderDAL: TSecretFolderDALFactory;
botKey: string;
projectId: string;
environment: string;
secretPath: string;
integration: TIntegrations;
integration: TIntegrations & {
projectId: string;
environment: {
id: string;
name: string;
slug: string;
};
secretPath: string;
};
secrets: Record<string, { value: string; comment?: string } | null>;
accessToken: string;
}) => {
@@ -644,13 +624,13 @@ const syncSecretsHeroku = async ({
Object.keys(herokuSecrets).forEach((key) => {
if (!integration.lastUsed) {
// first time using integration
// -> apply initial sync behavior rule
switch (metadata.syncBehavior) {
case IntegrationSyncBehavior.OVERWRITE_TARGET: {
// -> apply initial sync behavior
switch (metadata.initialSyncBehavior) {
case IntegrationInitialSyncBehavior.OVERWRITE_TARGET: {
if (!(key in secrets)) secrets[key] = null;
break;
}
case IntegrationSyncBehavior.PREFER_TARGET: {
case IntegrationInitialSyncBehavior.PREFER_TARGET: {
if (!(key in secrets)) {
secretsToAdd[key] = herokuSecrets[key];
} else if (secrets[key]?.value !== herokuSecrets[key]) {
@@ -661,7 +641,7 @@ const syncSecretsHeroku = async ({
};
break;
}
case IntegrationSyncBehavior.PREFER_SOURCE: {
case IntegrationInitialSyncBehavior.PREFER_SOURCE: {
if (!(key in secrets)) {
secrets[key] = herokuSecrets[key];
secretsToAdd[key] = herokuSecrets[key];
@@ -677,18 +657,10 @@ const syncSecretsHeroku = async ({
});
if (Object.keys(secretsToAdd).length) {
await createManySecretsRawHelper({
botKey,
projectDAL,
secretDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL,
projectId,
environment,
path: secretPath,
await createManySecretsRawFn({
projectId: integration.projectId,
environment: integration.environment.slug,
path: integration.secretPath,
secrets: Object.keys(secretsToAdd).map((key) => ({
secretName: key,
secretValue: secretsToAdd[key],
@@ -698,27 +670,19 @@ const syncSecretsHeroku = async ({
});
}
// if (Object.keys(secretsToUpdate).length) {
// await updateManySecretsRawHelper({
// projectId,
// environment,
// path: secretPath,
// secrets: Object.keys(secretsToUpdate).map((key) => ({
// secretName: key,
// secretValue: secretsToUpdate[key],
// type: SecretType.Shared,
// secretComment: ""
// })),
// botKey, // TODO: consider getting botKey inside this fn
// projectDAL,
// secretDAL,
// secretVersionDAL,
// secretBlindIndexDAL,
// secretTagDAL,
// secretVersionTagDAL,
// folderDAL
// });
// }
if (Object.keys(secretsToUpdate).length) {
await updateManySecretsRawFn({
projectId: integration.projectId,
environment: integration.environment.slug,
path: integration.secretPath,
secrets: Object.keys(secretsToUpdate).map((key) => ({
secretName: key,
secretValue: secretsToUpdate[key],
type: SecretType.Shared,
secretComment: ""
}))
});
}
await request.patch(
`${IntegrationUrls.HEROKU_API_URL}/apps/${integration.app}/config-vars`,
@@ -3053,18 +3017,9 @@ const syncSecretsHasuraCloud = async ({
*
*/
export const syncIntegrationSecrets = async ({
projectDAL,
createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL,
secretDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL,
botKey,
projectId,
environment,
secretPath,
integration,
integrationAuth,
secrets,
@@ -3072,19 +3027,18 @@ export const syncIntegrationSecrets = async ({
accessToken,
appendices
}: {
projectDAL: TProjectDALFactory;
createManySecretsRawFn: (params: TCreateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
updateManySecretsRawFn: (params: TUpdateManySecretsRawFn) => Promise<Array<TSecrets & { _id: string }>>;
integrationDAL: Pick<TIntegrationDALFactory, "updateById">;
secretDAL: TSecretDALFactory;
secretVersionDAL: TSecretVersionDALFactory;
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
secretTagDAL: TSecretTagDALFactory;
secretVersionTagDAL: TSecretVersionTagDALFactory;
folderDAL: TSecretFolderDALFactory;
botKey: string;
projectId: string;
environment: string;
secretPath: string;
integration: TIntegrations;
integration: TIntegrations & {
projectId: string;
environment: {
id: string;
name: string;
slug: string;
};
secretPath: string;
};
integrationAuth: TIntegrationAuths;
secrets: Record<string, { value: string; comment?: string }>;
accessId: string | null;
@@ -3124,18 +3078,9 @@ export const syncIntegrationSecrets = async ({
break;
case Integrations.HEROKU:
await syncSecretsHeroku({
projectDAL,
createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL,
secretDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL,
botKey,
projectId,
environment,
secretPath,
integration,
secrets,
accessToken

View File

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

View File

@@ -1,15 +1,16 @@
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 { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { decryptAsymmetric, generateAsymmetricKeyPair } from "@app/lib/crypto";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { TProjectDALFactory } from "../project/project-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 = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
@@ -24,29 +25,10 @@ export const projectBotServiceFactory = ({
projectDAL,
permissionService
}: TProjectBotServiceFactoryDep) => {
const getBotPrivateKey = ({ bot }: TGetPrivateKeyDTO) =>
infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL);
const getBotKey = 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(projectId);
};
const findBotByProjectId = async ({

View File

@@ -18,10 +18,12 @@ import {
import { BadRequestError } from "@app/lib/errors";
import { groupBy, unique } from "@app/lib/fn";
import { getBotKeyFnFactory } from "../project-bot/project-bot-fns";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretDALFactory } from "./secret-dal";
import {
TCreateManySecretsRawHelper,
TCreateManySecretsRawFn,
TCreateManySecretsRawFnFactory,
TFnSecretBlindIndexCheck,
TFnSecretBulkInsert,
TFnSecretBulkUpdate,
@@ -405,112 +407,114 @@ export const fnSecretBulkUpdate = async ({
return newSecrets.map((secret) => ({ ...secret, _id: secret.id }));
};
export const createManySecretsRawHelper = async ({
projectId,
environment,
path: secretPath,
secrets,
userId,
botKey, // TODO: consider getting botKey inside this fn
export const createManySecretsRawFnFactory = ({
projectDAL,
projectBotDAL,
secretDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL
}: TCreateManySecretsRawHelper) => {
await projectDAL.checkProjectUpgradeStatus(projectId);
}: TCreateManySecretsRawFnFactory) => {
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);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
const folderId = folder.id;
await projectDAL.checkProjectUpgradeStatus(projectId);
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Create secret" });
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
const folderId = folder.id;
// insert operation
const { keyName2BlindIndex } = await fnSecretBlindIndexCheck({
inputSecrets: secrets,
folderId,
isNew: true,
blindIndexCfg,
secretDAL
});
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Create secret" });
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
})),
// insert operation
const { keyName2BlindIndex } = await fnSecretBlindIndexCheck({
inputSecrets: secrets,
folderId,
secretDAL,
secretVersionDAL,
secretTagDAL,
secretVersionTagDAL,
tx
})
);
isNew: true,
blindIndexCfg,
secretDAL
});
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
// updateManySecretsRawFnFactory
// updateManySecretsRawHelper
export const updateManySecretsRawFnFactory = async ({
// TODO: refactor
botKey,
export const updateManySecretsRawFnFactory = ({
projectDAL,
projectBotDAL,
secretDAL,
secretVersionDAL,
secretBlindIndexDAL,
@@ -518,14 +522,16 @@ export const updateManySecretsRawFnFactory = async ({
secretVersionTagDAL,
folderDAL
}: TUpdateManySecretsRawFnFactory) => {
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL);
const updateManySecretsRawFn = async ({
projectId,
environment,
path: secretPath,
secrets, // accept instead ciphertext secrets
secrets, // consider accepting instead ciphertext secrets
userId
}: TUpdateManySecretsRawFn) => {
// TODO: fetch botKey from here
}: TUpdateManySecretsRawFn): Promise<Array<TSecrets & { _id: string }>> => {
const botKey = await getBotKeyFn(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
await projectDAL.checkProjectUpgradeStatus(projectId);

View File

@@ -6,6 +6,8 @@ import { BadRequestError } from "@app/lib/errors";
import { isSamePath } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
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 { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-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 { TCreateSecretReminderDTO, THandleReminderDTO, TRemoveSecretReminderDTO } from "./secret-types";
// import { updateManySecretsRawFnFactory } from "@app/services/secret/secret-fns";
export type TSecretQueueFactory = ReturnType<typeof secretQueueFactory>;
type TSecretQueueFactoryDep = {
@@ -44,6 +44,7 @@ type TSecretQueueFactoryDep = {
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: TProjectDALFactory;
projectBotDAL: TProjectBotDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
smtpService: TSmtpService;
orgDAL: Pick<TOrgDALFactory, "findOrgByProjectId">;
@@ -72,12 +73,35 @@ export const secretQueueFactory = ({
orgDAL,
smtpService,
projectDAL,
projectBotDAL,
projectMembershipDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL
}: 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) => {
await queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, {
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({
projectDAL,
createManySecretsRawFn,
updateManySecretsRawFn,
integrationDAL,
secretDAL,
secretVersionDAL,
secretBlindIndexDAL,
secretTagDAL,
secretVersionTagDAL,
folderDAL,
botKey,
projectId, // service
environment,
secretPath,
integration,
integrationAuth,
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets,

View File

@@ -3,6 +3,7 @@ import { Knex } from "knex";
import { SecretType, TSecretBlindIndexes, TSecrets, TSecretsInsert, TSecretsUpdate } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
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 { TSecretVersionDALFactory } from "@app/services/secret/secret-version-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;
environment: string;
path: string;
@@ -264,19 +276,11 @@ export type TCreateManySecretsRawHelper = {
};
}[];
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 = {
botKey: string;
projectDAL: TProjectDALFactory;
projectBotDAL: TProjectBotDALFactory;
secretDAL: TSecretDALFactory;
secretVersionDAL: TSecretVersionDALFactory;
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

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.
![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)
</Step>
</Steps>

View File

@@ -1,5 +1,5 @@
{
"name": "npm-proj-1709146141702-0.772936286416932EMIzNi",
"name": "frontend",
"lockfileVersion": 3,
"requires": true,
"packages": {

View File

@@ -61,7 +61,7 @@ export const useCreateIntegration = () => {
metadata?: {
secretPrefix?: string;
secretSuffix?: string;
syncBehavior?: string;
initialSyncBehavior?: string;
}
}) => {
const { data: { integration } } = await apiRequest.post("/api/v1/integration", {

View File

@@ -45,10 +45,4 @@ export enum IntegrationSyncBehavior {
OVERWRITE_TARGET = "overwrite-target",
PREFER_TARGET = "prefer-target",
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 }
];
}

View File

@@ -4,15 +4,22 @@ import Head from "next/head";
import Image from "next/image";
import Link from "next/link";
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 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 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 {
Button,
@@ -27,16 +34,24 @@ import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
} from "../../../hooks/api/integrationAuth";
import { useCreateWsEnvironment, useGetWorkspaceById } from "../../../hooks/api/workspace";
import { IntegrationSyncBehavior, syncBehaviors } from "@app/hooks/api/integrations/types";
import {
// 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({
selectedSourceEnvironment: yup.string().required("Source environment is required"),
secretPath: yup.string().required("Secret path is required"),
targetApp: yup.string().required("Heroku app is required"),
syncBehavior: yup
initialSyncBehavior: yup
.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")
});
@@ -49,7 +64,7 @@ export default function HerokuCreateIntegrationPage() {
resolver: yupResolver(schema),
defaultValues: {
secretPath: "/",
syncBehavior: IntegrationSyncBehavior.PREFER_SOURCE
initialSyncBehavior: IntegrationSyncBehavior.PREFER_SOURCE
}
});
@@ -57,7 +72,7 @@ export default function HerokuCreateIntegrationPage() {
const { mutateAsync } = useCreateIntegration();
const { mutateAsync: mutateAsyncEnv } = useCreateWsEnvironment();
// const { mutateAsync: mutateAsyncEnv } = useCreateWsEnvironment();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
@@ -67,14 +82,14 @@ export default function HerokuCreateIntegrationPage() {
integrationAuthId: (integrationAuthId as string) ?? ""
});
const { data: integrationAuthPipelineCouplings } = useGetIntegrationAuthHerokuPipelines({
integrationAuthId: (integrationAuthId as string) ?? ""
});
// const { data: integrationAuthPipelineCouplings } = useGetIntegrationAuthHerokuPipelines({
// integrationAuthId: (integrationAuthId as string) ?? ""
// });
const [uniquePipelines, setUniquePipelines] = useState<Pipeline[]>();
const [selectedPipeline, setSelectedPipeline] = useState("");
const [selectedPipelineApps, setSelectedPipelineApps] = useState<App[]>();
const [integrationType, setIntegrationType] = useState("App");
// const [uniquePipelines, setUniquePipelines] = useState<Pipeline[]>();
// const [selectedPipeline, setSelectedPipeline] = useState("");
// const [selectedPipelineApps, setSelectedPipelineApps] = useState<App[]>();
// const [integrationType, setIntegrationType] = useState("App");
const [isLoading, setIsLoading] = useState(false);
@@ -84,37 +99,37 @@ export default function HerokuCreateIntegrationPage() {
}
}, [workspace]);
useEffect(() => {
if (integrationAuthPipelineCouplings) {
const uniquePipelinesConst = Array.from(
new Set(
integrationAuthPipelineCouplings
.map(({ pipeline: { pipelineId, name } }) => ({
name,
pipelineId
}))
.map((obj) => JSON.stringify(obj))
)).map((str) => JSON.parse(str)) as { pipelineId: string; name: string }[]
// useEffect(() => {
// if (integrationAuthPipelineCouplings) {
// const uniquePipelinesConst = Array.from(
// new Set(
// integrationAuthPipelineCouplings
// .map(({ pipeline: { pipelineId, name } }) => ({
// name,
// pipelineId
// }))
// .map((obj) => JSON.stringify(obj))
// )).map((str) => JSON.parse(str)) as { pipelineId: string; name: string }[]
[... (new Set())]
setUniquePipelines(uniquePipelinesConst);
if (uniquePipelinesConst) {
if (uniquePipelinesConst!.length > 0) {
setSelectedPipeline(uniquePipelinesConst![0].name);
} else {
setSelectedPipeline("none");
}
}
}
}, [integrationAuthPipelineCouplings]);
// [... (new Set())]
// setUniquePipelines(uniquePipelinesConst);
// if (uniquePipelinesConst) {
// if (uniquePipelinesConst!.length > 0) {
// setSelectedPipeline(uniquePipelinesConst![0].name);
// } else {
// setSelectedPipeline("none");
// }
// }
// }
// }, [integrationAuthPipelineCouplings]);
useEffect(() => {
if (integrationAuthPipelineCouplings) {
setSelectedPipelineApps(integrationAuthApps?.filter(app => integrationAuthPipelineCouplings
.filter((pipelineCoupling) => pipelineCoupling.pipeline.name === selectedPipeline)
.map(coupling => coupling.app.appId).includes(String(app.appId))))
}
}, [selectedPipeline]);
// useEffect(() => {
// if (integrationAuthPipelineCouplings) {
// setSelectedPipelineApps(integrationAuthApps?.filter(app => integrationAuthPipelineCouplings
// .filter((pipelineCoupling) => pipelineCoupling.pipeline.name === selectedPipeline)
// .map(coupling => coupling.app.appId).includes(String(app.appId))))
// }
// }, [selectedPipeline]);
useEffect(() => {
if (integrationAuthApps) {
@@ -167,10 +182,9 @@ export default function HerokuCreateIntegrationPage() {
// };
const onFormSubmit = async ({
selectedSourceEnvironment: sce,
secretPath,
targetApp,
syncBehavior,
initialSyncBehavior,
}: FormData) => {
try {
if (!integrationAuth?.id) return;
@@ -184,7 +198,7 @@ export default function HerokuCreateIntegrationPage() {
sourceEnvironment: selectedSourceEnvironment,
secretPath,
metadata: {
syncBehavior
initialSyncBehavior
}
});
@@ -314,7 +328,7 @@ export default function HerokuCreateIntegrationPage() {
/>
<Controller
control={control}
name="syncBehavior"
name="initialSyncBehavior"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Initial Sync Behavior"
@@ -322,7 +336,7 @@ export default function HerokuCreateIntegrationPage() {
isError={Boolean(error)}
>
<Select {...field} onValueChange={(e) => onChange(e)} className="w-full">
{syncBehaviors.map((b) => {
{initialSyncBehaviors.map((b) => {
return (
<SelectItem value={b.value} key={`sync-behavior-${b.value}`}>
{b.label}