feat: changed backend-pg to backend

This commit is contained in:
Akhil Mohan
2024-01-27 19:14:33 +05:30
parent d13eafcef7
commit 66d258f02b
792 changed files with 40298 additions and 40607 deletions
+161
View File
@@ -0,0 +1,161 @@
import { Knex } from "knex";
import { validate as uuidValidate } from "uuid";
import { TDbClient } from "@app/db";
import { SecretsSchema, SecretType, TableName, TSecrets, TSecretsUpdate } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
export type TSecretDALFactory = ReturnType<typeof secretDALFactory>;
export const secretDALFactory = (db: TDbClient) => {
const secretOrm = ormify(db, TableName.Secret);
const update = async (
filter: Partial<TSecrets>,
data: Omit<TSecretsUpdate, "version">,
tx?: Knex
) => {
try {
const sec = await (tx || db)(TableName.Secret)
.where(filter)
.update(data)
.increment("version", 1)
.returning("*");
return sec;
} catch (error) {
throw new DatabaseError({ error, name: "update secret" });
}
};
// the idea is to use postgres specific function
// insert with id this will cause a conflict then merge the data
const bulkUpdate = async (
data: Array<{ filter: Partial<TSecrets>; data: TSecretsUpdate }>,
tx?: Knex
) => {
try {
const secs = await Promise.all(
data.map(async ({ filter, data: updateData }) => {
const [doc] = await (tx || db)(TableName.Secret)
.where(filter)
.update(updateData)
.increment("version", 1)
.returning("*");
if (!doc) throw new BadRequestError({ message: "Failed to update document" });
return doc;
})
);
return secs;
} catch (error) {
throw new DatabaseError({ error, name: "bulk update secret" });
}
};
const deleteMany = async (
data: Array<{ blindIndex: string; type: SecretType }>,
folderId: string,
userId: string,
tx?: Knex
) => {
try {
const deletedSecrets = await (tx || db)(TableName.Secret)
.where({ folderId })
.where((bd) => {
data.forEach((el) => {
bd.orWhere({
secretBlindIndex: el.blindIndex,
type: el.type,
...(el.type === SecretType.Personal ? { userId } : {})
});
});
})
.delete()
.returning("*");
return deletedSecrets;
} catch (error) {
throw new DatabaseError({ error, name: "delete many secret" });
}
};
const findByFolderId = async (folderId: string, userId?: string, tx?: Knex) => {
try {
// check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo)
if (userId && !uuidValidate(userId)) {
// eslint-disable-next-line
userId = undefined;
}
const secs = await (tx || db)(TableName.Secret)
.where({ folderId })
.where((bd) => {
bd.whereNull("userId").orWhere({ userId: userId || null });
})
.leftJoin(
TableName.JnSecretTag,
`${TableName.Secret}.id`,
`${TableName.JnSecretTag}.${TableName.Secret}Id`
)
.leftJoin(
TableName.SecretTag,
`${TableName.JnSecretTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.select(selectAllTableCols(TableName.Secret))
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
.select(db.ref("name").withSchema(TableName.SecretTag).as("tagName"));
const data = sqlNestRelationships({
data: secs,
key: "id",
parentMapper: (el) => SecretsSchema.parse(el),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({
id,
color,
slug,
name
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: "get all secret" });
}
};
const findByBlindIndexes = async (
folderId: string,
blindIndexes: Array<{ blindIndex: string; type: SecretType }>,
userId?: string,
tx?: Knex
) => {
if (!blindIndexes.length) return [];
try {
const secrets = await (tx || db)(TableName.Secret)
.where({ folderId })
.where((bd) => {
blindIndexes.forEach((el) => {
if (el.type === SecretType.Personal && !userId) {
throw new BadRequestError({ message: "Missing personal user id" });
}
bd.orWhere({
secretBlindIndex: el.blindIndex,
type: el.type,
userId: el.type === SecretType.Personal ? userId : null
});
});
});
return secrets;
} catch (error) {
throw new DatabaseError({ error, name: "find by blind indexes" });
}
};
return { ...secretOrm, update, bulkUpdate, deleteMany, findByFolderId, findByBlindIndexes };
};
+241
View File
@@ -0,0 +1,241 @@
/* eslint-disable no-await-in-loop */
import path from "path";
import { SecretKeyEncoding, TSecretBlindIndexes, TSecrets } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { buildSecretBlindIndexFromName, decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretDALFactory } from "./secret-dal";
export const generateSecretBlindIndexBySalt = async (
secretName: string,
secretBlindIndexDoc: TSecretBlindIndexes
) => {
const appCfg = getConfig();
const secretBlindIndex = await buildSecretBlindIndexFromName({
secretName,
keyEncoding: secretBlindIndexDoc.keyEncoding as SecretKeyEncoding,
rootEncryptionKey: appCfg.ROOT_ENCRYPTION_KEY,
encryptionKey: appCfg.ENCRYPTION_KEY,
tag: secretBlindIndexDoc.saltTag,
ciphertext: secretBlindIndexDoc.encryptedSaltCipherText,
iv: secretBlindIndexDoc.saltIV
});
return secretBlindIndex;
};
type TInterpolateSecretArg = {
projectId: string;
secretEncKey: string;
secretDAL: Pick<TSecretDALFactory, "findByFolderId">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
};
export const interpolateSecrets = ({
projectId,
secretEncKey,
secretDAL,
folderDAL
}: TInterpolateSecretArg) => {
const fetchSecretsCrossEnv = () => {
const fetchCache: Record<string, Record<string, string>> = {};
return async (secRefEnv: string, secRefPath: string[], secRefKey: string) => {
const secRefPathUrl = path.join("/", ...secRefPath);
const uniqKey = `${secRefEnv}-${secRefPathUrl}`;
if (fetchCache?.[uniqKey]) {
return fetchCache[uniqKey][secRefKey];
}
const folder = await folderDAL.findBySecretPath(projectId, secRefEnv, secRefPathUrl);
if (!folder) return "";
const secrets = await secretDAL.findByFolderId(folder.id);
const decryptedSec = secrets.reduce<Record<string, string>>((prev, secret) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key: secretEncKey
});
const secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key: secretEncKey
});
// eslint-disable-next-line
prev[secretKey] = secretValue;
return prev;
}, {});
fetchCache[uniqKey] = decryptedSec;
return fetchCache[uniqKey][secRefKey];
};
};
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g;
const recursivelyExpandSecret = async (
expandedSec: Record<string, string>,
interpolatedSec: Record<string, string>,
fetchCrossEnv: (env: string, secPath: string[], secKey: string) => Promise<string>,
recursionChainBreaker: Record<string, boolean>,
key: string
) => {
if (expandedSec?.[key] !== undefined) {
return expandedSec[key];
}
if (recursionChainBreaker?.[key]) {
return "";
}
// eslint-disable-next-line
recursionChainBreaker[key] = true;
let interpolatedValue = interpolatedSec[key];
if (!interpolatedValue) {
// eslint-disable-next-line no-console
console.error(`Couldn't find referenced value - ${key}`);
return "";
}
const refs = interpolatedValue.match(INTERPOLATION_SYNTAX_REG);
if (refs) {
for (const interpolationSyntax of refs) {
const interpolationKey = interpolationSyntax.slice(2, interpolationSyntax.length - 1);
const entities = interpolationKey.trim().split(".");
if (entities.length === 1) {
const val = await recursivelyExpandSecret(
expandedSec,
interpolatedSec,
fetchCrossEnv,
recursionChainBreaker,
interpolationKey
);
if (val) {
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
}
// eslint-disable-next-line
continue;
}
if (entities.length > 1) {
const secRefEnv = entities[0];
const secRefPath = entities.slice(1, entities.length - 1);
const secRefKey = entities[entities.length - 1];
const val = await fetchCrossEnv(secRefEnv, secRefPath, secRefKey);
if (val) {
interpolatedValue = interpolatedValue.replaceAll(interpolationSyntax, val);
}
}
}
}
// eslint-disable-next-line
expandedSec[key] = interpolatedValue;
return interpolatedValue;
};
// used to convert multi line ones to quotes ones with \n
const formatMultiValueEnv = (val?: string) => {
if (!val) return "";
if (!val.match("\n")) return val;
return `"${val.replace(/\n/g, "\\n")}"`;
};
const expandSecrets = async (
secrets: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }>
) => {
const expandedSec: Record<string, string> = {};
const interpolatedSec: Record<string, string> = {};
const crossSecEnvFetch = fetchSecretsCrossEnv();
Object.keys(secrets).forEach((key) => {
if (secrets[key].value.match(INTERPOLATION_SYNTAX_REG)) {
interpolatedSec[key] = secrets[key].value;
} else {
expandedSec[key] = secrets[key].value;
}
});
for (const key of Object.keys(secrets)) {
if (expandedSec?.[key]) {
// should not do multi line encoding if user has set it to skip
// eslint-disable-next-line
secrets[key].value = secrets[key].skipMultilineEncoding
? expandedSec[key]
: formatMultiValueEnv(expandedSec[key]);
// eslint-disable-next-line
continue;
}
// this is to avoid recursion loop. So the graph should be direct graph rather than cyclic
// so for any recursion building if there is an entity two times same key meaning it will be looped
const recursionChainBreaker: Record<string, boolean> = {};
const expandedVal = await recursivelyExpandSecret(
expandedSec,
interpolatedSec,
crossSecEnvFetch,
recursionChainBreaker,
key
);
// eslint-disable-next-line
secrets[key].value = secrets[key].skipMultilineEncoding
? expandedVal
: formatMultiValueEnv(expandedVal);
}
return secrets;
};
return expandSecrets;
};
export const decryptSecretRaw = (
secret: TSecrets & { workspace: string; environment: string },
key: string
) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key
});
const secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key
});
let secretComment = "";
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
secretComment = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretCommentCiphertext,
iv: secret.secretCommentIV,
tag: secret.secretCommentTag,
key
});
}
return {
secretKey,
workspace: secret.workspace,
environment: secret.environment,
secretValue,
secretComment,
version: secret.version,
type: secret.type,
_id: secret.id,
id: secret.id,
user: secret.userId
};
};
+405
View File
@@ -0,0 +1,405 @@
/* eslint-disable no-await-in-loop */
import { getConfig } from "@app/lib/config/env";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
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 { TIntegrationDALFactory } from "../integration/integration-dal";
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
import { syncIntegrationSecrets } from "../integration-auth/integration-sync-secret";
import { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { fnSecretsFromImports } from "../secret-import/secret-import-fns";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TWebhookDALFactory } from "../webhook/webhook-dal";
import { fnTriggerWebhook } from "../webhook/webhook-fns";
import { TSecretDALFactory } from "./secret-dal";
import { interpolateSecrets } from "./secret-fns";
import {
TCreateSecretReminderDTO,
THandleReminderDTO,
TRemoveSecretReminderDTO
} from "./secret-types";
export type TSecretQueueFactory = ReturnType<typeof secretQueueFactory>;
type TSecretQueueFactoryDep = {
queueService: TQueueServiceFactory;
integrationDAL: Pick<TIntegrationDALFactory, "findByProjectIdV2">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
integrationAuthService: Pick<TIntegrationAuthServiceFactory, "getIntegrationAccessToken">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "findByManySecretPath">;
secretDAL: Pick<TSecretDALFactory, "findByFolderId" | "find">;
secretImportDAL: Pick<TSecretImportDALFactory, "find">;
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
projectDAL: Pick<TProjectDALFactory, "findById">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
smtpService: TSmtpService;
orgDAL: Pick<TOrgDALFactory, "findOrgByProjectId">;
};
export type TGetSecrets = {
secretPath: string;
projectId: string;
environment: string;
};
export const secretQueueFactory = ({
queueService,
integrationDAL,
projectBotService,
integrationAuthService,
secretDAL,
secretImportDAL,
folderDAL,
webhookDAL,
projectEnvDAL,
orgDAL,
smtpService,
projectDAL,
projectMembershipDAL
}: TSecretQueueFactoryDep) => {
const syncIntegrations = async (dto: TGetSecrets) => {
await queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, {
attempts: 5,
delay: 1000,
backoff: {
type: "exponential",
delay: 3000
},
removeOnComplete: true,
removeOnFail: {
count: 5 // keep the most recent jobs
}
});
};
const syncSecrets = async (dto: TGetSecrets) => {
await queueService.queue(QueueName.SecretWebhook, QueueJobs.SecWebhook, dto, {
jobId: `secret-webhook-${dto.environment}-${dto.projectId}-${dto.secretPath}`,
removeOnFail: { count: 5 },
removeOnComplete: true,
delay: 1000,
attempts: 5,
backoff: {
type: "exponential",
delay: 3000
}
});
await syncIntegrations(dto);
};
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
const appCfg = getConfig();
await queueService.stopRepeatableJob(
QueueName.SecretReminder,
QueueJobs.SecretReminder,
{
// on prod it this will be in days, in development this will be second
every:
appCfg.NODE_ENV === "development"
? secondsToMillis(dto.repeatDays)
: daysToMillisecond(dto.repeatDays)
},
`reminder-${dto.secretId}`
);
};
const addSecretReminder = async ({
oldSecret,
newSecret,
projectId
}: TCreateSecretReminderDTO) => {
try {
const appCfg = getConfig();
if (oldSecret.id !== newSecret.id) {
throw new BadRequestError({
name: "SecretReminderIdMismatch",
message: "Existing secret didn't match the updated secret ID."
});
}
if (!newSecret.secretReminderRepeatDays) {
throw new BadRequestError({
name: "SecretReminderRepeatDaysMissing",
message: "Secret reminder repeat days is missing."
});
}
// If the secret already has a reminder, we should remove the existing one first.
if (oldSecret.secretReminderRepeatDays) {
await removeSecretReminder({
repeatDays: oldSecret.secretReminderRepeatDays,
secretId: oldSecret.id
});
}
await queueService.queue(
QueueName.SecretReminder,
QueueJobs.SecretReminder,
{
note: newSecret.secretReminderNote,
projectId,
repeatDays: newSecret.secretReminderRepeatDays,
secretId: newSecret.id
},
{
jobId: `reminder-${newSecret.id}`,
repeat: {
// on prod it this will be in days, in development this will be second
every:
appCfg.NODE_ENV === "development"
? secondsToMillis(newSecret.secretReminderRepeatDays)
: daysToMillisecond(newSecret.secretReminderRepeatDays),
immediately: true
}
}
);
} catch (err) {
logger.error(err, "Failed to create secret reminder.");
throw new BadRequestError({
name: "SecretReminderCreateFailed",
message: "Failed to create secret reminder."
});
}
};
const handleSecretReminder = async ({ newSecret, oldSecret, projectId }: THandleReminderDTO) => {
const { secretReminderRepeatDays, secretReminderNote } = newSecret;
if (newSecret.type !== "personal" && secretReminderRepeatDays !== undefined) {
if (
(secretReminderRepeatDays &&
oldSecret.secretReminderRepeatDays !== secretReminderRepeatDays) ||
(secretReminderNote && oldSecret.secretReminderNote !== secretReminderNote)
) {
await addSecretReminder({
oldSecret,
newSecret,
projectId
});
} else if (
secretReminderRepeatDays === null &&
secretReminderNote === null &&
oldSecret.secretReminderRepeatDays
) {
await removeSecretReminder({
secretId: oldSecret.id,
repeatDays: oldSecret.secretReminderRepeatDays
});
}
}
};
const getIntegrationSecrets = async (dto: TGetSecrets & { folderId: string }, key: string) => {
const secrets = await secretDAL.findByFolderId(dto.folderId);
if (!secrets.length) return {};
// get imported secrets
const secretImport = await secretImportDAL.find({ folderId: dto.folderId });
const importedSecrets = await fnSecretsFromImports({
allowedImports: secretImport,
secretDAL,
folderDAL
});
const content: Record<
string,
{ value: string; comment?: string; skipMultilineEncoding?: boolean }
> = {};
importedSecrets.forEach(({ secrets: secs }) => {
secs.forEach((secret) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key
});
const secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key
});
content[secretKey] = { value: secretValue };
content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
const commentValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretCommentCiphertext,
iv: secret.secretCommentIV,
tag: secret.secretCommentTag,
key
});
content[secretKey].comment = commentValue;
}
});
});
secrets.forEach((secret) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key
});
const secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key
});
content[secretKey] = { value: secretValue };
if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) {
const commentValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretCommentCiphertext,
iv: secret.secretCommentIV,
tag: secret.secretCommentTag,
key
});
content[secretKey].comment = commentValue;
}
content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding);
});
const expandSecrets = interpolateSecrets({
projectId: dto.projectId,
secretEncKey: key,
folderDAL,
secretDAL
});
await expandSecrets(content);
return content;
};
queueService.start(QueueName.IntegrationSync, async (job) => {
const { environment, projectId, secretPath } = job.data;
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) {
logger.error("Secret path not found");
return;
}
const integrations = await integrationDAL.findByProjectIdV2(projectId, environment);
const toBeSyncedIntegrations = integrations.filter(
({ secretPath: integrationSecPath, isActive }) =>
isActive && isSamePath(secretPath, integrationSecPath)
);
if (!integrations.length) return;
logger.info("Secret integration sync started", job.data, job.id);
for (const integration of toBeSyncedIntegrations) {
const integrationAuth = {
...integration.integrationAuth,
createdAt: new Date(),
updatedAt: new Date(),
projectId: integration.projectId
};
const botKey = await projectBotService.getBotKey(projectId);
const { accessToken, accessId } = await integrationAuthService.getIntegrationAccessToken(
integrationAuth,
botKey
);
const secrets = await getIntegrationSecrets(
{ environment, projectId, secretPath, folderId: folder.id },
botKey
);
const suffixedSecrets: typeof secrets = {};
const metadata = integration.metadata as Record<string, any>;
if (metadata) {
Object.keys(secrets).forEach((key) => {
const prefix = metadata?.secretPrefix || "";
const suffix = metadata?.secretSuffix || "";
const newKey = prefix + key + suffix;
suffixedSecrets[newKey] = secrets[key];
});
}
await syncIntegrationSecrets({
integration,
integrationAuth,
secrets: Object.keys(suffixedSecrets).length !== 0 ? suffixedSecrets : secrets,
accessId: accessId as string,
accessToken,
appendices: {
prefix: metadata?.secretPrefix || "",
suffix: metadata?.secretSuffix || ""
}
});
}
logger.info("Secret integration sync ended", job.id);
});
queueService.start(QueueName.SecretReminder, async ({ data }) => {
logger.info(`secretReminderQueue.process: [secretDocument=${data.secretId}]`);
const { projectId } = data;
const organization = await orgDAL.findOrgByProjectId(projectId);
const project = await projectDAL.findById(projectId);
if (!organization) {
logger.info(
`secretReminderQueue.process: [secretDocument=${data.secretId}] no organization found`
);
return;
}
if (!project) {
logger.info(
`secretReminderQueue.process: [secretDocument=${data.secretId}] no project found`
);
return;
}
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
if (!projectMembers || !projectMembers.length) {
logger.info(
`secretReminderQueue.process: [secretDocument=${data.secretId}] no project members found`
);
return;
}
await smtpService.sendMail({
template: SmtpTemplates.SecretReminder,
subjectLine: "Infisical secret reminder",
recipients: [...projectMembers.map((m) => m.user.email)],
substitutions: {
reminderNote: data.note, // May not be present.
projectName: project.name,
organizationName: organization.name
}
});
});
queueService.listen(QueueName.IntegrationSync, "failed", (job, err) => {
logger.error("Failed to sync integration", job?.data, err);
});
queueService.start(QueueName.SecretWebhook, async (job) => {
await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL });
});
return {
syncSecrets,
syncIntegrations,
addSecretReminder,
removeSecretReminder,
handleSecretReminder
};
};
File diff suppressed because it is too large Load Diff
+246
View File
@@ -0,0 +1,246 @@
import { Knex } from "knex";
import {
SecretType,
TSecretBlindIndexes,
TSecrets,
TSecretsInsert,
TSecretsUpdate
} from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
type TPartialSecret = Pick<TSecrets, "id" | "secretReminderRepeatDays" | "secretReminderNote">;
type TPartialInputSecret = Pick<
TSecrets,
"type" | "secretReminderNote" | "secretReminderRepeatDays" | "id"
>;
export type TCreateSecretDTO = {
secretName: string;
path: string;
environment: string;
type: "shared" | "personal";
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
skipMultilineEncoding?: boolean;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
tags?: string[];
metadata?: {
source?: string;
};
} & TProjectPermission;
export type TUpdateSecretDTO = {
secretName: string;
path: string;
newSecretName?: string;
environment: string;
type: "shared" | "personal";
secretKeyCiphertext?: string;
secretKeyIV?: string;
secretKeyTag?: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
tags?: string[];
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
skipMultilineEncoding?: boolean;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
metadata?: {
source?: string;
};
} & TProjectPermission;
export type TDeleteSecretDTO = {
secretName: string;
secretId?: string;
path: string;
environment: string;
type: "shared" | "personal";
} & TProjectPermission;
export type TGetSecretsDTO = {
path: string;
environment: string;
includeImports?: boolean;
} & TProjectPermission;
export type TGetASecretDTO = {
secretName: string;
path: string;
environment: string;
type: "shared" | "personal";
includeImports?: boolean;
version?: number;
} & TProjectPermission;
export type TCreateBulkSecretDTO = {
path: string;
environment: string;
secrets: Array<{
secretName: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
tags?: string[];
secretValueTag: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
skipMultilineEncoding?: boolean;
metadata?: {
source?: string;
};
}>;
} & TProjectPermission;
export type TUpdateBulkSecretDTO = {
path: string;
environment: string;
secrets: Array<{
type: SecretType;
secretName: string;
newSecretName?: string;
secretValueCiphertext?: string;
secretValueIV?: string;
secretValueTag?: string;
tags?: string[];
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
skipMultilineEncoding?: boolean;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
}>;
} & TProjectPermission;
export type TDeleteBulkSecretDTO = {
path: string;
environment: string;
secrets: Array<{
type: SecretType;
secretName: string;
}>;
} & TProjectPermission;
export type TListSecretVersionDTO = {
secretId: string;
offset?: number;
limit?: number;
} & Omit<TProjectPermission, "projectId">;
export type TGetSecretsRawDTO = {
path: string;
environment: string;
includeImports?: boolean;
} & TProjectPermission;
export type TGetASecretRawDTO = {
secretName: string;
path: string;
environment: string;
type: "shared" | "personal";
includeImports?: boolean;
version?: number;
} & TProjectPermission;
export type TCreateSecretRawDTO = TProjectPermission & {
secretPath: string;
environment: string;
secretName: string;
secretValue: string;
type: SecretType;
secretComment?: string;
skipMultilineEncoding?: boolean;
};
export type TUpdateSecretRawDTO = TProjectPermission & {
secretPath: string;
environment: string;
secretName: string;
secretValue?: string;
type: SecretType;
skipMultilineEncoding?: boolean;
secretReminderRepeatDays?: number | null;
secretReminderNote?: string | null;
};
export type TDeleteSecretRawDTO = TProjectPermission & {
secretPath: string;
environment: string;
secretName: string;
type: SecretType;
};
export type TGetSecretVersionsDTO = Omit<TProjectPermission, "projectId"> & {
limit?: number;
offset?: number;
secretId: string;
};
export type TFnSecretBulkInsert = {
folderId: string;
tx?: Knex;
inputSecrets: Array<Omit<TSecretsInsert, "folderId"> & { tags?: string[] }>;
};
export type TFnSecretBulkUpdate = {
folderId: string;
projectId: string;
inputSecrets: { filter: Partial<TSecrets>; data: TSecretsUpdate & { tags?: string[] } }[];
tx?: Knex;
};
export type TFnSecretBulkDelete = {
folderId: string;
projectId: string;
inputSecrets: Array<{ type: SecretType; secretBlindIndex: string }>;
actorId: string;
tx?: Knex;
};
export type TFnSecretBlindIndexCheck = {
folderId: string;
userId?: string;
blindIndexCfg: TSecretBlindIndexes;
inputSecrets: Array<{ secretName: string; type?: SecretType }>;
isNew: boolean;
};
// when blind index is already present
export type TFnSecretBlindIndexCheckV2 = {
folderId: string;
userId?: string;
inputSecrets: Array<{ secretBlindIndex: string; type?: SecretType }>;
};
export type THandleReminderDTO = {
newSecret: TPartialInputSecret;
oldSecret: TPartialSecret;
projectId: string;
};
export type TCreateSecretReminderDTO = {
oldSecret: TPartialSecret;
newSecret: TPartialSecret;
projectId: string;
};
export type TRemoveSecretReminderDTO = {
secretId: string;
repeatDays: number;
};
@@ -0,0 +1,69 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName, TSecretVersions } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex";
export type TSecretVersionDALFactory = ReturnType<typeof secretVersionDALFactory>;
export const secretVersionDALFactory = (db: TDbClient) => {
const secretVersionOrm = ormify(db, TableName.SecretVersion);
// This will fetch all latest secret versions from a folder
const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => {
try {
const docs = await (tx || db)(TableName.SecretVersion)
.where(`${TableName.SecretVersion}.folderId`, folderId)
.join(TableName.Secret, `${TableName.Secret}.id`, `${TableName.SecretVersion}.secretId`)
.join<TSecretVersions, TSecretVersions & { secretId: string; max: number }>(
(tx || db)(TableName.SecretVersion)
.groupBy("folderId", "secretId")
.max("version")
.select("secretId")
.as("latestVersion"),
(bd) => {
bd.on(`${TableName.SecretVersion}.secretId`, "latestVersion.secretId").andOn(
`${TableName.SecretVersion}.version`,
"latestVersion.max"
);
}
)
.select(selectAllTableCols(TableName.SecretVersion));
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "FindLatestVersionByFolderId" });
}
};
const findLatestVersionMany = async (folderId: string, secretIds: string[], tx?: Knex) => {
try {
const docs: Array<TSecretVersions & { max: number }> = await (tx || db)(
TableName.SecretVersion
)
.where("folderId", folderId)
.whereIn(`${TableName.SecretVersion}.secretId`, secretIds)
.join(
(tx || db)(TableName.SecretVersion)
.groupBy("secretId")
.max("version")
.select("secretId")
.as("latestVersion"),
(bd) => {
bd.on(`${TableName.SecretVersion}.secretId`, "latestVersion.secretId").andOn(
`${TableName.SecretVersion}.version`,
"latestVersion.max"
);
}
);
return docs.reduce<Record<string, TSecretVersions>>(
(prev, curr) => ({ ...prev, [curr.secretId || ""]: curr }),
{}
);
} catch (error) {
throw new DatabaseError({ error, name: "FindLatestVersinMany" });
}
};
return { ...secretVersionOrm, findLatestVersionMany, findLatestVersionByFolderId };
};
@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TSecretVersionTagDALFactory = ReturnType<typeof secretVersionTagDALFactory>;
export const secretVersionTagDALFactory = (db: TDbClient) => {
const secretVersionTagDAL = ormify(db, TableName.SecretVersionTag);
return secretVersionTagDAL;
};