feat(infisical-pg): fixed secret rotation and secret approval limit offset

This commit is contained in:
Akhil Mohan
2024-01-27 12:38:23 +05:30
parent 28fdf4ed4b
commit bf059e0fe5
14 changed files with 265 additions and 142 deletions
@@ -103,7 +103,10 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => {
const findProjectRequestCount = async (projectId: string, membershipId: string, tx?: Knex) => { const findProjectRequestCount = async (projectId: string, membershipId: string, tx?: Knex) => {
try { try {
const doc = await (tx || db)(TableName.SecretApprovalRequest) const docs = await (tx || db)
.with(
"temp",
(tx || db)(TableName.SecretApprovalRequest)
.join( .join(
TableName.SecretFolder, TableName.SecretFolder,
`${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretApprovalRequest}.folderId`,
@@ -120,18 +123,27 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => {
`${TableName.SapApprover}.policyId` `${TableName.SapApprover}.policyId`
) )
.where({ projectId }) .where({ projectId })
.andWhere((bd) =>
bd
.where(`${TableName.SapApprover}.approverId`, membershipId) .where(`${TableName.SapApprover}.approverId`, membershipId)
.orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId) .orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId)
.groupBy("status") )
.select("status", `${TableName.SecretApprovalRequest}.id`)
.groupBy(`${TableName.SecretApprovalRequest}.id`, "status")
.count("status") .count("status")
.select("status"); )
.select("status")
.from("temp")
.groupBy("status")
.count("status");
return { return {
open: parseInt( open: parseInt(
(doc.find(({ status }) => status === RequestState.Open)?.count as string) || "0", (docs.find(({ status }) => status === RequestState.Open)?.count as string) || "0",
10 10
), ),
closed: parseInt( closed: parseInt(
(doc.find(({ status }) => status === RequestState.Closed)?.count as string) || "0", (docs.find(({ status }) => status === RequestState.Closed)?.count as string) || "0",
10 10
) )
}; };
@@ -141,11 +153,21 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => {
}; };
const findByProjectId = async ( const findByProjectId = async (
{ status, limit, offset, projectId, committer, environment, membershipId }: TFindQueryFilter, {
status,
limit = 20,
offset = 0,
projectId,
committer,
environment,
membershipId
}: TFindQueryFilter,
tx?: Knex tx?: Knex
) => { ) => {
try { try {
const docs = await (tx || db)(TableName.SecretApprovalRequest) // akhilmhdh: If ever u wanted a 1 to so many relationship connected with pagination
// this is the place u wanna look at.
const query = (tx || db)(TableName.SecretApprovalRequest)
.join( .join(
TableName.SecretFolder, TableName.SecretFolder,
`${TableName.SecretApprovalRequest}.folderId`, `${TableName.SecretApprovalRequest}.folderId`,
@@ -174,7 +196,7 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => {
.where( .where(
stripUndefinedInWhere({ stripUndefinedInWhere({
projectId, projectId,
slug: environment, [`${TableName.Environment}.slug` as "slug"]: environment,
[`${TableName.SecretApprovalRequest}.status`]: status, [`${TableName.SecretApprovalRequest}.status`]: status,
committerId: committer committerId: committer
}) })
@@ -190,13 +212,27 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => {
.select(db.ref("status").withSchema(TableName.SarReviewer).as("reviewerStatus")) .select(db.ref("status").withSchema(TableName.SarReviewer).as("reviewerStatus"))
.select(db.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId")) .select(db.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"))
.select(db.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName")) .select(db.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"))
.select(
db.raw(
`DENSE_RANK() OVER (partition by ${TableName.Environment}."projectId" ORDER BY ${TableName.SecretApprovalRequest}."id" DESC) as rank`
)
)
.select( .select(
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath") db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath")
) )
.select( .select(
db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals") db.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals")
) )
.select(db.ref("approverId").withSchema(TableName.SapApprover)); .select(db.ref("approverId").withSchema(TableName.SapApprover))
.orderBy("createdAt", "desc");
const docs = await (tx || db)
.with("w", query)
.select("*")
.from<Awaited<typeof query>[number]>("w")
.where("w.rank", ">=", offset)
.andWhere("w.rank", "<", offset + limit);
const formatedDoc = sqlNestRelationships({ const formatedDoc = sqlNestRelationships({
data: docs, data: docs,
key: "id", key: "id",
@@ -81,6 +81,7 @@ export const secretApprovalRequestServiceFactory = ({
actorId, actorId,
projectId projectId
); );
const count = await secretApprovalRequestDal.findProjectRequestCount(projectId, membership.id); const count = await secretApprovalRequestDal.findProjectRequestCount(projectId, membership.id);
return count; return count;
}; };
@@ -91,7 +92,9 @@ export const secretApprovalRequestServiceFactory = ({
actor, actor,
status, status,
environment, environment,
committer committer,
limit,
offset
}: TListApprovalsDTO) => { }: TListApprovalsDTO) => {
if (actor === ActorType.SERVICE) if (actor === ActorType.SERVICE)
throw new BadRequestError({ message: "Cannot use service token" }); throw new BadRequestError({ message: "Cannot use service token" });
@@ -102,7 +105,9 @@ export const secretApprovalRequestServiceFactory = ({
committer, committer,
environment, environment,
status, status,
membershipId: membership.id membershipId: membership.id,
limit,
offset
}); });
return approvals; return approvals;
}; };
@@ -332,7 +337,11 @@ export const secretApprovalRequestServiceFactory = ({
projectId, projectId,
tx, tx,
inputSecrets: secretUpdationCommits.map((el) => ({ inputSecrets: secretUpdationCommits.map((el) => ({
...pick(el, [ filter: {
id: el.secretId,
type: SecretType.Shared
},
data: pick(el, [
"secretCommentCiphertext", "secretCommentCiphertext",
"secretCommentTag", "secretCommentTag",
"secretCommentIV", "secretCommentIV",
@@ -346,14 +355,8 @@ export const secretApprovalRequestServiceFactory = ({
"skipMultilineEncoding", "skipMultilineEncoding",
"secretReminderNote", "secretReminderNote",
"secretReminderRepeatDays", "secretReminderRepeatDays",
"version",
"algorithm",
"keyEncoding",
"secretBlindIndex" "secretBlindIndex"
]), ])
version: (el.secret?.version || 0) + 1,
id: el.secretId,
type: SecretType.Shared
})) }))
}) })
: []; : [];
@@ -98,7 +98,7 @@ export const secretRotationDbFn = async ({
const db = knex({ const db = knex({
client, client,
connection: { connection: {
db: database, database,
port, port,
host, host,
user: username, user: username,
@@ -142,17 +142,17 @@ export const secretRotationHttpSetFn = async (
export const getDbSetQuery = ( export const getDbSetQuery = (
db: TDbProviderClients, db: TDbProviderClients,
variable: { username: string; password: string } variables: { username: string; password: string }
) => { ) => {
if (db === TDbProviderClients.Pg) { if (db === TDbProviderClients.Pg) {
return { return {
query: "ALTER USER :username WITH PASSWORD :password", query: `ALTER USER ?? WITH PASSWORD '${variables.password}'`,
variable variables: [variables.username]
}; };
} }
// add more based on client // add more based on client
return { return {
query: "ALTER USER :username IDENTIFIED BY :password", query: `ALTER USER ?? IDENTIFIED BY '${variables.password}'`,
variable variables: [variables.username]
}; };
}; };
@@ -22,6 +22,6 @@ export type TSecretRotationDbFn = {
database: string; database: string;
port: number; port: number;
query: string; query: string;
variables: Record<string, unknown>; variables: unknown[];
ca?: string; ca?: string;
}; };
@@ -1,10 +1,11 @@
import { SecretKeyEncoding } from "@app/db/schemas"; import { SecretKeyEncoding, SecretType } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { import {
encryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8,
infisicalSymmetricDecrypt, infisicalSymmetricDecrypt,
infisicalSymmetricEncypt infisicalSymmetricEncypt
} from "@app/lib/crypto/encryption"; } from "@app/lib/crypto/encryption";
import { daysToMillisecond } from "@app/lib/dates"; import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
@@ -34,7 +35,7 @@ type TSecretRotationQueueFactoryDep = {
queue: TQueueServiceFactory; queue: TQueueServiceFactory;
secretRotationDal: TSecretRotationDalFactory; secretRotationDal: TSecretRotationDalFactory;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">; projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
secretDal: Pick<TSecretDalFactory, "bulkUpdate">; secretDal: Pick<TSecretDalFactory, "bulkUpdate" | "find">;
secretVersionDal: Pick<TSecretVersionDalFactory, "insertMany" | "findLatestVersionMany">; secretVersionDal: Pick<TSecretVersionDalFactory, "insertMany" | "findLatestVersionMany">;
}; };
@@ -58,13 +59,25 @@ export const secretRotationQueueFactory = ({
secretDal, secretDal,
secretVersionDal secretVersionDal
}: TSecretRotationQueueFactoryDep) => { }: TSecretRotationQueueFactoryDep) => {
const addToQueue = async (rotationId: string, interval: number) => const addToQueue = async (rotationId: string, interval: number) => {
const appCfg = getConfig();
queue.queue( queue.queue(
QueueName.SecretRotation, QueueName.SecretRotation,
QueueJobs.SecretRotation, QueueJobs.SecretRotation,
{ rotationId }, { rotationId },
{ jobId: rotationId, repeat: { every: daysToMillisecond(interval), immediately: true } } {
jobId: rotationId,
repeat: {
// on prod it this will be in days, in development this will be second
every:
appCfg.NODE_ENV === "development"
? secondsToMillis(interval)
: daysToMillisecond(interval),
immediately: true
}
}
); );
};
const removeFromQueue = async (rotationId: string) => const removeFromQueue = async (rotationId: string) =>
queue.stopRepeatableJob(QueueName.SecretRotation, rotationId); queue.stopRepeatableJob(QueueName.SecretRotation, rotationId);
@@ -127,7 +140,14 @@ export const secretRotationQueueFactory = ({
} }
// set a random value for new password // set a random value for new password
newCredential.internal.rotated_password = alphaNumericNanoId(32); newCredential.internal.rotated_password = alphaNumericNanoId(32);
const { username, password, host, database, port, ca } = newCredential.inputs; const {
admin_username: username,
admin_password: password,
host,
database,
port,
ca
} = newCredential.inputs;
const dbFunctionArg = { const dbFunctionArg = {
username, username,
password, password,
@@ -149,8 +169,10 @@ export const secretRotationQueueFactory = ({
await secretRotationDbFn({ await secretRotationDbFn({
...dbFunctionArg, ...dbFunctionArg,
query: "SELECT NOW()", query: "SELECT NOW()",
variables: {} variables: []
}); });
newCredential.outputs.db_username = newCredential.internal.username;
newCredential.outputs.db_password = newCredential.internal.rotated_password;
// clean up // clean up
if (variables.creds.length === 2) variables.creds.pop(); if (variables.creds.length === 2) variables.creds.pop();
} }
@@ -172,7 +194,6 @@ export const secretRotationQueueFactory = ({
} }
} }
} }
variables.creds.unshift({ variables.creds.unshift({
outputs: newCredential.outputs, outputs: newCredential.outputs,
internal: newCredential.internal internal: newCredential.internal
@@ -205,17 +226,16 @@ export const secretRotationQueueFactory = ({
); );
const updatedSecrets = await secretDal.bulkUpdate( const updatedSecrets = await secretDal.bulkUpdate(
encryptedSecrets.map(({ secretId, value }) => ({ encryptedSecrets.map(({ secretId, value }) => ({
id: secretId, // this secret id is validated when user is inserted
filter: { id: secretId, type: SecretType.Shared },
data: {
secretValueCiphertext: value.ciphertext, secretValueCiphertext: value.ciphertext,
secretValueIV: value.iv, secretValueIV: value.iv,
secretValueTag: value.tag secretValueTag: value.tag
}
})), })),
tx tx
); );
await secretDal.bulkUpdate(
updatedSecrets.map(({ id, version }) => ({ id, version: (version || 0) + 1 })),
tx
);
await secretVersionDal.insertMany( await secretVersionDal.insertMany(
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({ updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
...el, ...el,
@@ -224,7 +244,9 @@ export const secretRotationQueueFactory = ({
tx tx
); );
}); });
logger.info("Finished logging: rotation id: ", rotationId);
} catch (error) { } catch (error) {
logger.error(error);
if (error instanceof DisableRotationErrors) { if (error instanceof DisableRotationErrors) {
if (job.id) { if (job.id) {
queue.stopRepeatableJob(QueueName.SecretRotation, job.id); queue.stopRepeatableJob(QueueName.SecretRotation, job.id);
@@ -1,11 +1,12 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import Ajv from "ajv"; import Ajv from "ajv";
import { 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 { 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 { TProjectEnvDalFactory } from "@app/services/project-env/project-env-dal"; import { TSecretDalFactory } from "@app/services/secret/secret-dal";
import { TSecretFolderDalFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
@@ -25,8 +26,9 @@ import { rotationTemplates } from "./templates";
type TSecretRotationServiceFactoryDep = { type TSecretRotationServiceFactoryDep = {
secretRotationDal: TSecretRotationDalFactory; secretRotationDal: TSecretRotationDalFactory;
projectDal: Pick<TProjectDalFactory, "findById">; projectDal: Pick<TProjectDalFactory, "findById">;
folderDal: Pick<TSecretFolderDalFactory, "findBySecretPath">;
secretDal: Pick<TSecretDalFactory, "find">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
projectEnvDal: Pick<TProjectEnvDalFactory, "findOne">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
secretRotationQueue: TSecretRotationQueueFactory; secretRotationQueue: TSecretRotationQueueFactory;
}; };
@@ -37,10 +39,11 @@ const ajv = new Ajv({ strict: false });
export const secretRotationServiceFactory = ({ export const secretRotationServiceFactory = ({
secretRotationDal, secretRotationDal,
permissionService, permissionService,
projectEnvDal,
secretRotationQueue, secretRotationQueue,
licenseService, licenseService,
projectDal projectDal,
folderDal,
secretDal
}: TSecretRotationServiceFactoryDep) => { }: TSecretRotationServiceFactoryDep) => {
const getProviderTemplates = async ({ actor, actorId, projectId }: TProjectPermission) => { const getProviderTemplates = async ({ actor, actorId, projectId }: TProjectPermission) => {
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId); const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
@@ -71,8 +74,20 @@ export const secretRotationServiceFactory = ({
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRotation ProjectPermissionSub.SecretRotation
); );
const env = await projectEnvDal.findOne({ slug: environment });
if (!env) throw new BadRequestError({ message: "Environment not found" }); const folder = await folderDal.findBySecretPath(projectId, environment, secretPath);
if (!folder) throw new BadRequestError({ message: "Secret path not found" });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
const selectedSecrets = await secretDal.find({
folderId: folder.id,
$in: { id: Object.values(outputs) }
});
if (selectedSecrets.length !== Object.values(outputs).length)
throw new BadRequestError({ message: "Secrets not found" });
const project = await projectDal.findById(projectId); const project = await projectDal.findById(projectId);
const plan = await licenseService.getPlan(project.orgId); const plan = await licenseService.getPlan(project.orgId);
@@ -114,7 +129,7 @@ export const secretRotationServiceFactory = ({
provider, provider,
secretPath, secretPath,
interval, interval,
envId: env.id, envId: folder.envId,
encryptedDataTag: encData.tag, encryptedDataTag: encData.tag,
encryptedDataIV: encData.iv, encryptedDataIV: encData.iv,
encryptedData: encData.ciphertext, encryptedData: encData.ciphertext,
@@ -128,7 +143,7 @@ export const secretRotationServiceFactory = ({
Object.entries(outputs).map(([key, secretId]) => ({ key, secretId, rotationId: doc.id })), Object.entries(outputs).map(([key, secretId]) => ({ key, secretId, rotationId: doc.id })),
tx tx
); );
return { ...doc, outputs: outputSecretMapping, environment: env }; return { ...doc, outputs: outputSecretMapping, environment: folder.environment };
}); });
return secretRotation; return secretRotation;
}; };
+1 -1
View File
@@ -16,7 +16,7 @@ const envSchema = z
REDIS_URL: zpStr(z.string()), REDIS_URL: zpStr(z.string()),
HOST: zpStr(z.string().default("localhost")), HOST: zpStr(z.string().default("localhost")),
DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database conntection string")), DB_CONNECTION_URI: zpStr(z.string().describe("Postgres database conntection string")),
NODE_ENV: z.enum(["development", "test", "production"]).default("development"), NODE_ENV: z.enum(["development", "test", "production"]).default("production"),
SALT_ROUNDS: z.coerce.number().default(10), SALT_ROUNDS: z.coerce.number().default(10),
// TODO(akhilmhdh): will be changed to one // TODO(akhilmhdh): will be changed to one
ENCRYPTION_KEY: zpStr(z.string().optional()), ENCRYPTION_KEY: zpStr(z.string().optional()),
+2
View File
@@ -1 +1,3 @@
export const daysToMillisecond = (days: number) => days * 24 * 60 * 60 * 1000; export const daysToMillisecond = (days: number) => days * 24 * 60 * 60 * 1000;
export const secondsToMillis = (seconds: number) => seconds * 1000;
+3 -2
View File
@@ -381,11 +381,12 @@ export const registerRoutes = async (
}); });
const secretRotationService = secretRotationServiceFactory({ const secretRotationService = secretRotationServiceFactory({
permissionService, permissionService,
projectEnvDal,
secretRotationDal, secretRotationDal,
secretRotationQueue, secretRotationQueue,
projectDal, projectDal,
licenseService licenseService,
secretDal,
folderDal
}); });
const integrationService = integrationServiceFactory({ const integrationService = integrationServiceFactory({
@@ -1,7 +1,12 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TSecretFolders, TSecretFoldersUpdate } from "@app/db/schemas"; import {
TableName,
TProjectEnvironments,
TSecretFolders,
TSecretFoldersUpdate
} from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
@@ -65,8 +70,7 @@ const sqlFindMultipleFolderByEnvPathQuery = (
.select(selectAllTableCols(TableName.SecretFolder)) .select(selectAllTableCols(TableName.SecretFolder))
.where((wb) => .where((wb) =>
formatedQuery.map(({ secretPath }) => formatedQuery.map(({ secretPath }) =>
wb wb.orWhereRaw(
.orWhereRaw(
`depth = array_position(ARRAY[${secretPath `depth = array_position(ARRAY[${secretPath
.map(() => "?") .map(() => "?")
.join(",")}]::varchar[], ${TableName.SecretFolder}.name,depth)`, .join(",")}]::varchar[], ${TableName.SecretFolder}.name,depth)`,
@@ -75,8 +79,10 @@ const sqlFindMultipleFolderByEnvPathQuery = (
) )
) )
.from(TableName.SecretFolder) .from(TableName.SecretFolder)
.join("parent", (bd)=> .join("parent", (bd) =>
bd.on("parent.id", `${TableName.SecretFolder}.parentId`).andOn("parent.envId",`${TableName.SecretFolder}.envId`) bd
.on("parent.id", `${TableName.SecretFolder}.parentId`)
.andOn("parent.envId", `${TableName.SecretFolder}.envId`)
) )
); );
}) })
@@ -146,8 +152,28 @@ const sqlFindFolderByPathQuery = (
.join("parent", "parent.id", `${TableName.SecretFolder}.parentId`) .join("parent", "parent.id", `${TableName.SecretFolder}.parentId`)
); );
}) })
.select("*") .from<TSecretFolders & { depth: number; path: string }>("parent")
.from<TSecretFolders & { depth: number; path: string }>("parent"); .leftJoin<TProjectEnvironments>(
TableName.Environment,
`${TableName.Environment}.id`,
"parent.envId"
)
.select<
TSecretFolders & {
depth: number;
path: string;
envId: string;
envSlug: string;
envName: string;
projectId: string;
}
>(
selectAllTableCols("parent" as TableName.SecretFolder),
db.ref("id").withSchema(TableName.Environment).as("envId"),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("name").withSchema(TableName.Environment).as("envName"),
db.ref("projectId").withSchema(TableName.Environment)
);
}; };
export type TSecretFolderDalFactory = ReturnType<typeof secretFolderDalFactory>; export type TSecretFolderDalFactory = ReturnType<typeof secretFolderDalFactory>;
@@ -169,7 +195,9 @@ export const secretFolderDalFactory = (db: TDbClient) => {
if (folder && folder.path !== path) { if (folder && folder.path !== path) {
return; return;
} }
return folder; if (!folder) return;
const { envId: id, envName: name, envSlug: slug, ...el } = folder;
return { ...el, envId: id, environment: { id, name, slug } };
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find by secret path" }); throw new DatabaseError({ error, name: "Find by secret path" });
} }
+15 -13
View File
@@ -1,14 +1,7 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { import { SecretsSchema, SecretType, TableName, TSecrets, TSecretsUpdate } from "@app/db/schemas";
SecretsSchema,
SecretType,
TableName,
TSecrets,
TSecretsInsert,
TSecretsUpdate
} from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
@@ -36,13 +29,22 @@ export const secretDalFactory = (db: TDbClient) => {
// the idea is to use postgres specific function // the idea is to use postgres specific function
// insert with id this will cause a conflict then merge the data // insert with id this will cause a conflict then merge the data
const bulkUpdate = async (data: Array<TSecretsUpdate & { id: string }>, tx?: Knex) => { const bulkUpdate = async (
data: Array<{ filter: Partial<TSecrets>; data: TSecretsUpdate }>,
tx?: Knex
) => {
try { try {
const secs = await (tx || db)(TableName.Secret) const secs = await Promise.all(
.insert(data as TSecretsInsert[]) data.map(async ({ filter, data: updateData }) => {
.onConflict("id") const [doc] = await (tx || db)(TableName.Secret)
.merge() .where(filter)
.update(updateData)
.increment("version", 1)
.returning("*"); .returning("*");
if (!doc) throw new BadRequestError({ message: "Failed to update document" });
return doc;
})
);
return secs; return secs;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "bulk update secret" }); throw new DatabaseError({ error, name: "bulk update secret" });
@@ -132,20 +132,25 @@ export const secretServiceFactory = ({
projectId projectId
}: TFnSecretBulkUpdate) => { }: TFnSecretBulkUpdate) => {
const newSecrets = await secretDal.bulkUpdate( const newSecrets = await secretDal.bulkUpdate(
inputSecrets.map(({ tags, ...el }) => ({ ...el, folderId })), inputSecrets.map(({ filter, data: { tags, ...data } }) => ({
filter: { ...filter, folderId },
data
})),
tx tx
); );
const secsUpdatedTag = inputSecrets.filter(({ tags }) => Boolean(tags)); const secsUpdatedTag = inputSecrets.flatMap(({ data: { tags } }, i) =>
tags?.length ? { tags, secretId: newSecrets[i].id } : []
);
if (secsUpdatedTag.length) { if (secsUpdatedTag.length) {
await secretTagDal.deleteTagsManySecret( await secretTagDal.deleteTagsManySecret(
projectId, projectId,
secsUpdatedTag.map(({ id }) => id), secsUpdatedTag.flatMap(({ tags }) => tags),
tx tx
); );
const newSecretTags = secsUpdatedTag.flatMap(({ tags: secretTags = [], id }) => const newSecretTags = secsUpdatedTag.flatMap(({ tags: secretTags = [], secretId }) =>
secretTags.map((tag) => ({ secretTags.map((tag) => ({
[`${TableName.SecretTag}Id` as const]: tag, [`${TableName.SecretTag}Id` as const]: tag,
[`${TableName.Secret}Id` as const]: id [`${TableName.Secret}Id` as const]: secretId
})) }))
); );
await secretTagDal.saveTagsToSecret(newSecretTags, tx); await secretTagDal.saveTagsToSecret(newSecretTags, tx);
@@ -391,8 +396,8 @@ export const secretServiceFactory = ({
projectId, projectId,
inputSecrets: [ inputSecrets: [
{ {
id: secrets[0].id, filter: { id: secrets[0].id },
version: (secrets[0].version || 0) + 1, data: {
...pick(el, [ ...pick(el, [
"type", "type",
"secretCommentCiphertext", "secretCommentCiphertext",
@@ -412,6 +417,7 @@ export const secretServiceFactory = ({
]), ]),
secretBlindIndex: newSecretNameBlindIndex || keyName2BlindIndex[secretName] secretBlindIndex: newSecretNameBlindIndex || keyName2BlindIndex[secretName]
} }
}
], ],
tx tx
}) })
@@ -667,7 +673,7 @@ export const secretServiceFactory = ({
if (!blindIndexCfg) if (!blindIndexCfg)
throw new BadRequestError({ message: "Blind index not found", name: "Update secret" }); throw new BadRequestError({ message: "Blind index not found", name: "Update secret" });
const { keyName2BlindIndex, secrets: secretsToBeUpdated } = await fnSecretBlindIndexCheck({ const { keyName2BlindIndex } = await fnSecretBlindIndexCheck({
inputSecrets, inputSecrets,
folderId, folderId,
isNew: false, isNew: false,
@@ -684,7 +690,6 @@ export const secretServiceFactory = ({
blindIndexCfg blindIndexCfg
}); });
const secsGroupedByBlindIndex = groupBy(secretsToBeUpdated, (el) => el.secretBlindIndex);
// get all tags // get all tags
const tagIds = inputSecrets.flatMap(({ tags = [] }) => tags); const tagIds = inputSecrets.flatMap(({ tags = [] }) => tags);
const tags = tagIds.length ? await secretTagDal.findManyTagsById(projectId, tagIds) : []; const tags = tagIds.length ? await secretTagDal.findManyTagsById(projectId, tagIds) : [];
@@ -694,13 +699,10 @@ export const secretServiceFactory = ({
folderId, folderId,
projectId, projectId,
tx, tx,
inputSecrets: inputSecrets.map(({ secretName, newSecretName, ...el }) => { inputSecrets: inputSecrets.map(({ secretName, newSecretName, ...el }) => ({
const { version, updatedAt, ...info } = filter: { secretBlindIndex: keyName2BlindIndex[secretName], type: SecretType.Shared },
secsGroupedByBlindIndex[keyName2BlindIndex[secretName]][0]; data: {
return {
...el, ...el,
version: (version || 0) + 1,
...info,
folderId, folderId,
type: SecretType.Shared, type: SecretType.Shared,
secretBlindIndex: secretBlindIndex:
@@ -709,8 +711,8 @@ export const secretServiceFactory = ({
: keyName2BlindIndex[secretName], : keyName2BlindIndex[secretName],
algorithm: SecretEncryptionAlgo.AES_256_GCM, algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8 keyEncoding: SecretKeyEncoding.UTF8
}; }
}) }))
}) })
); );
@@ -806,7 +808,8 @@ export const secretServiceFactory = ({
includeImports includeImports
}: TGetSecretsRawDTO) => { }: TGetSecretsRawDTO) => {
const botKey = await projectBotService.getBotKey(projectId); const botKey = await projectBotService.getBotKey(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found" }); if (!botKey)
throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
const { secrets, imports } = await getSecrets({ const { secrets, imports } = await getSecrets({
actorId, actorId,
@@ -842,7 +845,8 @@ export const secretServiceFactory = ({
includeImports includeImports
}: TGetASecretRawDTO) => { }: TGetASecretRawDTO) => {
const botKey = await projectBotService.getBotKey(projectId); const botKey = await projectBotService.getBotKey(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found" }); if (!botKey)
throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
const secret = await getASecret({ const secret = await getASecret({
actorId, actorId,
@@ -870,7 +874,8 @@ export const secretServiceFactory = ({
skipMultilineEncoding skipMultilineEncoding
}: TCreateSecretRawDTO) => { }: TCreateSecretRawDTO) => {
const botKey = await projectBotService.getBotKey(projectId); const botKey = await projectBotService.getBotKey(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found" }); if (!botKey)
throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretName, botKey); const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretName, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey); const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
@@ -914,7 +919,8 @@ export const secretServiceFactory = ({
skipMultilineEncoding skipMultilineEncoding
}: TUpdateSecretRawDTO) => { }: TUpdateSecretRawDTO) => {
const botKey = await projectBotService.getBotKey(projectId); const botKey = await projectBotService.getBotKey(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found" }); if (!botKey)
throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey); const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
@@ -948,7 +954,8 @@ export const secretServiceFactory = ({
secretPath secretPath
}: TDeleteSecretRawDTO) => { }: TDeleteSecretRawDTO) => {
const botKey = await projectBotService.getBotKey(projectId); const botKey = await projectBotService.getBotKey(projectId);
if (!botKey) throw new BadRequestError({ message: "Project bot not found" }); if (!botKey)
throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
const secret = await deleteSecret({ const secret = await deleteSecret({
secretName, secretName,
@@ -1,6 +1,12 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { SecretType, TSecretBlindIndexes, 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";
export type TCreateSecretDTO = { export type TCreateSecretDTO = {
@@ -182,7 +188,7 @@ export type TFnSecretBulkInsert = {
export type TFnSecretBulkUpdate = { export type TFnSecretBulkUpdate = {
folderId: string; folderId: string;
projectId: string; projectId: string;
inputSecrets: Array<TSecretsUpdate & { tags?: string[]; id: string }>; inputSecrets: { filter: Partial<TSecrets>; data: TSecretsUpdate & { tags?: string[] } }[];
tx?: Knex; tx?: Knex;
}; };
+2 -1
View File
@@ -38,7 +38,7 @@ services:
profiles: ["test"] profiles: ["test"]
image: postgres:14-alpine image: postgres:14-alpine
ports: ports:
- "5432:5432" - "5430:5432"
environment: environment:
POSTGRES_PASSWORD: infisical POSTGRES_PASSWORD: infisical
POSTGRES_USER: infisical POSTGRES_USER: infisical
@@ -54,6 +54,7 @@ services:
env_file: env_file:
- .env - .env
environment: environment:
- NODE_ENV=development
- DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable - DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable
volumes: volumes:
- ./backend-pg/src:/app/src - ./backend-pg/src:/app/src