mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(infisical-pg): fixed secret rotation and secret approval limit offset
This commit is contained in:
@@ -103,35 +103,47 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => {
|
||||
|
||||
const findProjectRequestCount = async (projectId: string, membershipId: string, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db)(TableName.SecretApprovalRequest)
|
||||
.join(
|
||||
TableName.SecretFolder,
|
||||
`${TableName.SecretApprovalRequest}.folderId`,
|
||||
`${TableName.SecretFolder}.id`
|
||||
const docs = await (tx || db)
|
||||
.with(
|
||||
"temp",
|
||||
(tx || db)(TableName.SecretApprovalRequest)
|
||||
.join(
|
||||
TableName.SecretFolder,
|
||||
`${TableName.SecretApprovalRequest}.folderId`,
|
||||
`${TableName.SecretFolder}.id`
|
||||
)
|
||||
.join(
|
||||
TableName.Environment,
|
||||
`${TableName.SecretFolder}.envId`,
|
||||
`${TableName.Environment}.id`
|
||||
)
|
||||
.join(
|
||||
TableName.SapApprover,
|
||||
`${TableName.SecretApprovalRequest}.policyId`,
|
||||
`${TableName.SapApprover}.policyId`
|
||||
)
|
||||
.where({ projectId })
|
||||
.andWhere((bd) =>
|
||||
bd
|
||||
.where(`${TableName.SapApprover}.approverId`, membershipId)
|
||||
.orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId)
|
||||
)
|
||||
.select("status", `${TableName.SecretApprovalRequest}.id`)
|
||||
.groupBy(`${TableName.SecretApprovalRequest}.id`, "status")
|
||||
.count("status")
|
||||
)
|
||||
.join(
|
||||
TableName.Environment,
|
||||
`${TableName.SecretFolder}.envId`,
|
||||
`${TableName.Environment}.id`
|
||||
)
|
||||
.join(
|
||||
TableName.SapApprover,
|
||||
`${TableName.SecretApprovalRequest}.policyId`,
|
||||
`${TableName.SapApprover}.policyId`
|
||||
)
|
||||
.where({ projectId })
|
||||
.where(`${TableName.SapApprover}.approverId`, membershipId)
|
||||
.orWhere(`${TableName.SecretApprovalRequest}.committerId`, membershipId)
|
||||
.select("status")
|
||||
.from("temp")
|
||||
.groupBy("status")
|
||||
.count("status")
|
||||
.select("status");
|
||||
.count("status");
|
||||
|
||||
return {
|
||||
open: parseInt(
|
||||
(doc.find(({ status }) => status === RequestState.Open)?.count as string) || "0",
|
||||
(docs.find(({ status }) => status === RequestState.Open)?.count as string) || "0",
|
||||
10
|
||||
),
|
||||
closed: parseInt(
|
||||
(doc.find(({ status }) => status === RequestState.Closed)?.count as string) || "0",
|
||||
(docs.find(({ status }) => status === RequestState.Closed)?.count as string) || "0",
|
||||
10
|
||||
)
|
||||
};
|
||||
@@ -141,11 +153,21 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => {
|
||||
};
|
||||
|
||||
const findByProjectId = async (
|
||||
{ status, limit, offset, projectId, committer, environment, membershipId }: TFindQueryFilter,
|
||||
{
|
||||
status,
|
||||
limit = 20,
|
||||
offset = 0,
|
||||
projectId,
|
||||
committer,
|
||||
environment,
|
||||
membershipId
|
||||
}: TFindQueryFilter,
|
||||
tx?: Knex
|
||||
) => {
|
||||
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(
|
||||
TableName.SecretFolder,
|
||||
`${TableName.SecretApprovalRequest}.folderId`,
|
||||
@@ -174,7 +196,7 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => {
|
||||
.where(
|
||||
stripUndefinedInWhere({
|
||||
projectId,
|
||||
slug: environment,
|
||||
[`${TableName.Environment}.slug` as "slug"]: environment,
|
||||
[`${TableName.SecretApprovalRequest}.status`]: status,
|
||||
committerId: committer
|
||||
})
|
||||
@@ -190,13 +212,27 @@ export const secretApprovalRequestDalFactory = (db: TDbClient) => {
|
||||
.select(db.ref("status").withSchema(TableName.SarReviewer).as("reviewerStatus"))
|
||||
.select(db.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"))
|
||||
.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(
|
||||
db.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath")
|
||||
)
|
||||
.select(
|
||||
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({
|
||||
data: docs,
|
||||
key: "id",
|
||||
|
||||
@@ -81,6 +81,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
actorId,
|
||||
projectId
|
||||
);
|
||||
|
||||
const count = await secretApprovalRequestDal.findProjectRequestCount(projectId, membership.id);
|
||||
return count;
|
||||
};
|
||||
@@ -91,7 +92,9 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
actor,
|
||||
status,
|
||||
environment,
|
||||
committer
|
||||
committer,
|
||||
limit,
|
||||
offset
|
||||
}: TListApprovalsDTO) => {
|
||||
if (actor === ActorType.SERVICE)
|
||||
throw new BadRequestError({ message: "Cannot use service token" });
|
||||
@@ -102,7 +105,9 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
committer,
|
||||
environment,
|
||||
status,
|
||||
membershipId: membership.id
|
||||
membershipId: membership.id,
|
||||
limit,
|
||||
offset
|
||||
});
|
||||
return approvals;
|
||||
};
|
||||
@@ -332,7 +337,11 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
projectId,
|
||||
tx,
|
||||
inputSecrets: secretUpdationCommits.map((el) => ({
|
||||
...pick(el, [
|
||||
filter: {
|
||||
id: el.secretId,
|
||||
type: SecretType.Shared
|
||||
},
|
||||
data: pick(el, [
|
||||
"secretCommentCiphertext",
|
||||
"secretCommentTag",
|
||||
"secretCommentIV",
|
||||
@@ -346,14 +355,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
"skipMultilineEncoding",
|
||||
"secretReminderNote",
|
||||
"secretReminderRepeatDays",
|
||||
"version",
|
||||
"algorithm",
|
||||
"keyEncoding",
|
||||
"secretBlindIndex"
|
||||
]),
|
||||
version: (el.secret?.version || 0) + 1,
|
||||
id: el.secretId,
|
||||
type: SecretType.Shared
|
||||
])
|
||||
}))
|
||||
})
|
||||
: [];
|
||||
|
||||
@@ -98,7 +98,7 @@ export const secretRotationDbFn = async ({
|
||||
const db = knex({
|
||||
client,
|
||||
connection: {
|
||||
db: database,
|
||||
database,
|
||||
port,
|
||||
host,
|
||||
user: username,
|
||||
@@ -142,17 +142,17 @@ export const secretRotationHttpSetFn = async (
|
||||
|
||||
export const getDbSetQuery = (
|
||||
db: TDbProviderClients,
|
||||
variable: { username: string; password: string }
|
||||
variables: { username: string; password: string }
|
||||
) => {
|
||||
if (db === TDbProviderClients.Pg) {
|
||||
return {
|
||||
query: "ALTER USER :username WITH PASSWORD :password",
|
||||
variable
|
||||
query: `ALTER USER ?? WITH PASSWORD '${variables.password}'`,
|
||||
variables: [variables.username]
|
||||
};
|
||||
}
|
||||
// add more based on client
|
||||
return {
|
||||
query: "ALTER USER :username IDENTIFIED BY :password",
|
||||
variable
|
||||
query: `ALTER USER ?? IDENTIFIED BY '${variables.password}'`,
|
||||
variables: [variables.username]
|
||||
};
|
||||
};
|
||||
|
||||
@@ -22,6 +22,6 @@ export type TSecretRotationDbFn = {
|
||||
database: string;
|
||||
port: number;
|
||||
query: string;
|
||||
variables: Record<string, unknown>;
|
||||
variables: unknown[];
|
||||
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 {
|
||||
encryptSymmetric128BitHexKeyUTF8,
|
||||
infisicalSymmetricDecrypt,
|
||||
infisicalSymmetricEncypt
|
||||
} 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 { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
@@ -34,7 +35,7 @@ type TSecretRotationQueueFactoryDep = {
|
||||
queue: TQueueServiceFactory;
|
||||
secretRotationDal: TSecretRotationDalFactory;
|
||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||
secretDal: Pick<TSecretDalFactory, "bulkUpdate">;
|
||||
secretDal: Pick<TSecretDalFactory, "bulkUpdate" | "find">;
|
||||
secretVersionDal: Pick<TSecretVersionDalFactory, "insertMany" | "findLatestVersionMany">;
|
||||
};
|
||||
|
||||
@@ -58,13 +59,25 @@ export const secretRotationQueueFactory = ({
|
||||
secretDal,
|
||||
secretVersionDal
|
||||
}: TSecretRotationQueueFactoryDep) => {
|
||||
const addToQueue = async (rotationId: string, interval: number) =>
|
||||
const addToQueue = async (rotationId: string, interval: number) => {
|
||||
const appCfg = getConfig();
|
||||
queue.queue(
|
||||
QueueName.SecretRotation,
|
||||
QueueJobs.SecretRotation,
|
||||
{ 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) =>
|
||||
queue.stopRepeatableJob(QueueName.SecretRotation, rotationId);
|
||||
@@ -127,7 +140,14 @@ export const secretRotationQueueFactory = ({
|
||||
}
|
||||
// set a random value for new password
|
||||
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 = {
|
||||
username,
|
||||
password,
|
||||
@@ -149,8 +169,10 @@ export const secretRotationQueueFactory = ({
|
||||
await secretRotationDbFn({
|
||||
...dbFunctionArg,
|
||||
query: "SELECT NOW()",
|
||||
variables: {}
|
||||
variables: []
|
||||
});
|
||||
newCredential.outputs.db_username = newCredential.internal.username;
|
||||
newCredential.outputs.db_password = newCredential.internal.rotated_password;
|
||||
// clean up
|
||||
if (variables.creds.length === 2) variables.creds.pop();
|
||||
}
|
||||
@@ -172,7 +194,6 @@ export const secretRotationQueueFactory = ({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variables.creds.unshift({
|
||||
outputs: newCredential.outputs,
|
||||
internal: newCredential.internal
|
||||
@@ -205,17 +226,16 @@ export const secretRotationQueueFactory = ({
|
||||
);
|
||||
const updatedSecrets = await secretDal.bulkUpdate(
|
||||
encryptedSecrets.map(({ secretId, value }) => ({
|
||||
id: secretId,
|
||||
secretValueCiphertext: value.ciphertext,
|
||||
secretValueIV: value.iv,
|
||||
secretValueTag: value.tag
|
||||
// this secret id is validated when user is inserted
|
||||
filter: { id: secretId, type: SecretType.Shared },
|
||||
data: {
|
||||
secretValueCiphertext: value.ciphertext,
|
||||
secretValueIV: value.iv,
|
||||
secretValueTag: value.tag
|
||||
}
|
||||
})),
|
||||
tx
|
||||
);
|
||||
await secretDal.bulkUpdate(
|
||||
updatedSecrets.map(({ id, version }) => ({ id, version: (version || 0) + 1 })),
|
||||
tx
|
||||
);
|
||||
await secretVersionDal.insertMany(
|
||||
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||
...el,
|
||||
@@ -224,7 +244,9 @@ export const secretRotationQueueFactory = ({
|
||||
tx
|
||||
);
|
||||
});
|
||||
logger.info("Finished logging: rotation id: ", rotationId);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
if (error instanceof DisableRotationErrors) {
|
||||
if (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 { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
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 { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
@@ -25,8 +26,9 @@ import { rotationTemplates } from "./templates";
|
||||
type TSecretRotationServiceFactoryDep = {
|
||||
secretRotationDal: TSecretRotationDalFactory;
|
||||
projectDal: Pick<TProjectDalFactory, "findById">;
|
||||
folderDal: Pick<TSecretFolderDalFactory, "findBySecretPath">;
|
||||
secretDal: Pick<TSecretDalFactory, "find">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
projectEnvDal: Pick<TProjectEnvDalFactory, "findOne">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
secretRotationQueue: TSecretRotationQueueFactory;
|
||||
};
|
||||
@@ -37,10 +39,11 @@ const ajv = new Ajv({ strict: false });
|
||||
export const secretRotationServiceFactory = ({
|
||||
secretRotationDal,
|
||||
permissionService,
|
||||
projectEnvDal,
|
||||
secretRotationQueue,
|
||||
licenseService,
|
||||
projectDal
|
||||
projectDal,
|
||||
folderDal,
|
||||
secretDal
|
||||
}: TSecretRotationServiceFactoryDep) => {
|
||||
const getProviderTemplates = async ({ actor, actorId, projectId }: TProjectPermission) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
@@ -71,8 +74,20 @@ export const secretRotationServiceFactory = ({
|
||||
ProjectPermissionActions.Create,
|
||||
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 plan = await licenseService.getPlan(project.orgId);
|
||||
@@ -114,7 +129,7 @@ export const secretRotationServiceFactory = ({
|
||||
provider,
|
||||
secretPath,
|
||||
interval,
|
||||
envId: env.id,
|
||||
envId: folder.envId,
|
||||
encryptedDataTag: encData.tag,
|
||||
encryptedDataIV: encData.iv,
|
||||
encryptedData: encData.ciphertext,
|
||||
@@ -128,7 +143,7 @@ export const secretRotationServiceFactory = ({
|
||||
Object.entries(outputs).map(([key, secretId]) => ({ key, secretId, rotationId: doc.id })),
|
||||
tx
|
||||
);
|
||||
return { ...doc, outputs: outputSecretMapping, environment: env };
|
||||
return { ...doc, outputs: outputSecretMapping, environment: folder.environment };
|
||||
});
|
||||
return secretRotation;
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ const envSchema = z
|
||||
REDIS_URL: zpStr(z.string()),
|
||||
HOST: zpStr(z.string().default("localhost")),
|
||||
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),
|
||||
// TODO(akhilmhdh): will be changed to one
|
||||
ENCRYPTION_KEY: zpStr(z.string().optional()),
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export const daysToMillisecond = (days: number) => days * 24 * 60 * 60 * 1000;
|
||||
|
||||
export const secondsToMillis = (seconds: number) => seconds * 1000;
|
||||
|
||||
@@ -381,11 +381,12 @@ export const registerRoutes = async (
|
||||
});
|
||||
const secretRotationService = secretRotationServiceFactory({
|
||||
permissionService,
|
||||
projectEnvDal,
|
||||
secretRotationDal,
|
||||
secretRotationQueue,
|
||||
projectDal,
|
||||
licenseService
|
||||
licenseService,
|
||||
secretDal,
|
||||
folderDal
|
||||
});
|
||||
|
||||
const integrationService = integrationServiceFactory({
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
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 { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
@@ -65,19 +70,20 @@ const sqlFindMultipleFolderByEnvPathQuery = (
|
||||
.select(selectAllTableCols(TableName.SecretFolder))
|
||||
.where((wb) =>
|
||||
formatedQuery.map(({ secretPath }) =>
|
||||
wb
|
||||
.orWhereRaw(
|
||||
`depth = array_position(ARRAY[${secretPath
|
||||
.map(() => "?")
|
||||
.join(",")}]::varchar[], ${TableName.SecretFolder}.name,depth)`,
|
||||
[...secretPath]
|
||||
)
|
||||
wb.orWhereRaw(
|
||||
`depth = array_position(ARRAY[${secretPath
|
||||
.map(() => "?")
|
||||
.join(",")}]::varchar[], ${TableName.SecretFolder}.name,depth)`,
|
||||
[...secretPath]
|
||||
)
|
||||
)
|
||||
)
|
||||
.from(TableName.SecretFolder)
|
||||
.join("parent", (bd)=>
|
||||
bd.on("parent.id", `${TableName.SecretFolder}.parentId`).andOn("parent.envId",`${TableName.SecretFolder}.envId`)
|
||||
)
|
||||
.join("parent", (bd) =>
|
||||
bd
|
||||
.on("parent.id", `${TableName.SecretFolder}.parentId`)
|
||||
.andOn("parent.envId", `${TableName.SecretFolder}.envId`)
|
||||
)
|
||||
);
|
||||
})
|
||||
.select("*")
|
||||
@@ -146,8 +152,28 @@ const sqlFindFolderByPathQuery = (
|
||||
.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>;
|
||||
@@ -169,7 +195,9 @@ export const secretFolderDalFactory = (db: TDbClient) => {
|
||||
if (folder && folder.path !== path) {
|
||||
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) {
|
||||
throw new DatabaseError({ error, name: "Find by secret path" });
|
||||
}
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import {
|
||||
SecretsSchema,
|
||||
SecretType,
|
||||
TableName,
|
||||
TSecrets,
|
||||
TSecretsInsert,
|
||||
TSecretsUpdate
|
||||
} from "@app/db/schemas";
|
||||
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";
|
||||
|
||||
@@ -36,13 +29,22 @@ export const secretDalFactory = (db: TDbClient) => {
|
||||
|
||||
// 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<TSecretsUpdate & { id: string }>, tx?: Knex) => {
|
||||
const bulkUpdate = async (
|
||||
data: Array<{ filter: Partial<TSecrets>; data: TSecretsUpdate }>,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const secs = await (tx || db)(TableName.Secret)
|
||||
.insert(data as TSecretsInsert[])
|
||||
.onConflict("id")
|
||||
.merge()
|
||||
.returning("*");
|
||||
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" });
|
||||
|
||||
@@ -132,20 +132,25 @@ export const secretServiceFactory = ({
|
||||
projectId
|
||||
}: TFnSecretBulkUpdate) => {
|
||||
const newSecrets = await secretDal.bulkUpdate(
|
||||
inputSecrets.map(({ tags, ...el }) => ({ ...el, folderId })),
|
||||
inputSecrets.map(({ filter, data: { tags, ...data } }) => ({
|
||||
filter: { ...filter, folderId },
|
||||
data
|
||||
})),
|
||||
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) {
|
||||
await secretTagDal.deleteTagsManySecret(
|
||||
projectId,
|
||||
secsUpdatedTag.map(({ id }) => id),
|
||||
secsUpdatedTag.flatMap(({ tags }) => tags),
|
||||
tx
|
||||
);
|
||||
const newSecretTags = secsUpdatedTag.flatMap(({ tags: secretTags = [], id }) =>
|
||||
const newSecretTags = secsUpdatedTag.flatMap(({ tags: secretTags = [], secretId }) =>
|
||||
secretTags.map((tag) => ({
|
||||
[`${TableName.SecretTag}Id` as const]: tag,
|
||||
[`${TableName.Secret}Id` as const]: id
|
||||
[`${TableName.Secret}Id` as const]: secretId
|
||||
}))
|
||||
);
|
||||
await secretTagDal.saveTagsToSecret(newSecretTags, tx);
|
||||
@@ -391,26 +396,27 @@ export const secretServiceFactory = ({
|
||||
projectId,
|
||||
inputSecrets: [
|
||||
{
|
||||
id: secrets[0].id,
|
||||
version: (secrets[0].version || 0) + 1,
|
||||
...pick(el, [
|
||||
"type",
|
||||
"secretCommentCiphertext",
|
||||
"secretCommentTag",
|
||||
"secretCommentIV",
|
||||
"secretValueIV",
|
||||
"secretValueTag",
|
||||
"secretValueCiphertext",
|
||||
"secretKeyCiphertext",
|
||||
"secretKeyTag",
|
||||
"secretKeyIV",
|
||||
"metadata",
|
||||
"skipMultilineEncoding",
|
||||
"secretReminderNote",
|
||||
"secretReminderRepeatDays",
|
||||
"tags"
|
||||
]),
|
||||
secretBlindIndex: newSecretNameBlindIndex || keyName2BlindIndex[secretName]
|
||||
filter: { id: secrets[0].id },
|
||||
data: {
|
||||
...pick(el, [
|
||||
"type",
|
||||
"secretCommentCiphertext",
|
||||
"secretCommentTag",
|
||||
"secretCommentIV",
|
||||
"secretValueIV",
|
||||
"secretValueTag",
|
||||
"secretValueCiphertext",
|
||||
"secretKeyCiphertext",
|
||||
"secretKeyTag",
|
||||
"secretKeyIV",
|
||||
"metadata",
|
||||
"skipMultilineEncoding",
|
||||
"secretReminderNote",
|
||||
"secretReminderRepeatDays",
|
||||
"tags"
|
||||
]),
|
||||
secretBlindIndex: newSecretNameBlindIndex || keyName2BlindIndex[secretName]
|
||||
}
|
||||
}
|
||||
],
|
||||
tx
|
||||
@@ -667,7 +673,7 @@ export const secretServiceFactory = ({
|
||||
if (!blindIndexCfg)
|
||||
throw new BadRequestError({ message: "Blind index not found", name: "Update secret" });
|
||||
|
||||
const { keyName2BlindIndex, secrets: secretsToBeUpdated } = await fnSecretBlindIndexCheck({
|
||||
const { keyName2BlindIndex } = await fnSecretBlindIndexCheck({
|
||||
inputSecrets,
|
||||
folderId,
|
||||
isNew: false,
|
||||
@@ -684,7 +690,6 @@ export const secretServiceFactory = ({
|
||||
blindIndexCfg
|
||||
});
|
||||
|
||||
const secsGroupedByBlindIndex = groupBy(secretsToBeUpdated, (el) => el.secretBlindIndex);
|
||||
// get all tags
|
||||
const tagIds = inputSecrets.flatMap(({ tags = [] }) => tags);
|
||||
const tags = tagIds.length ? await secretTagDal.findManyTagsById(projectId, tagIds) : [];
|
||||
@@ -694,13 +699,10 @@ export const secretServiceFactory = ({
|
||||
folderId,
|
||||
projectId,
|
||||
tx,
|
||||
inputSecrets: inputSecrets.map(({ secretName, newSecretName, ...el }) => {
|
||||
const { version, updatedAt, ...info } =
|
||||
secsGroupedByBlindIndex[keyName2BlindIndex[secretName]][0];
|
||||
return {
|
||||
inputSecrets: inputSecrets.map(({ secretName, newSecretName, ...el }) => ({
|
||||
filter: { secretBlindIndex: keyName2BlindIndex[secretName], type: SecretType.Shared },
|
||||
data: {
|
||||
...el,
|
||||
version: (version || 0) + 1,
|
||||
...info,
|
||||
folderId,
|
||||
type: SecretType.Shared,
|
||||
secretBlindIndex:
|
||||
@@ -709,8 +711,8 @@ export const secretServiceFactory = ({
|
||||
: keyName2BlindIndex[secretName],
|
||||
algorithm: SecretEncryptionAlgo.AES_256_GCM,
|
||||
keyEncoding: SecretKeyEncoding.UTF8
|
||||
};
|
||||
})
|
||||
}
|
||||
}))
|
||||
})
|
||||
);
|
||||
|
||||
@@ -806,7 +808,8 @@ export const secretServiceFactory = ({
|
||||
includeImports
|
||||
}: TGetSecretsRawDTO) => {
|
||||
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({
|
||||
actorId,
|
||||
@@ -842,7 +845,8 @@ export const secretServiceFactory = ({
|
||||
includeImports
|
||||
}: TGetASecretRawDTO) => {
|
||||
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({
|
||||
actorId,
|
||||
@@ -870,7 +874,8 @@ export const secretServiceFactory = ({
|
||||
skipMultilineEncoding
|
||||
}: TCreateSecretRawDTO) => {
|
||||
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 secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
|
||||
@@ -914,7 +919,8 @@ export const secretServiceFactory = ({
|
||||
skipMultilineEncoding
|
||||
}: TUpdateSecretRawDTO) => {
|
||||
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);
|
||||
|
||||
@@ -948,7 +954,8 @@ export const secretServiceFactory = ({
|
||||
secretPath
|
||||
}: TDeleteSecretRawDTO) => {
|
||||
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({
|
||||
secretName,
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
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";
|
||||
|
||||
export type TCreateSecretDTO = {
|
||||
@@ -182,7 +188,7 @@ export type TFnSecretBulkInsert = {
|
||||
export type TFnSecretBulkUpdate = {
|
||||
folderId: string;
|
||||
projectId: string;
|
||||
inputSecrets: Array<TSecretsUpdate & { tags?: string[]; id: string }>;
|
||||
inputSecrets: { filter: Partial<TSecrets>; data: TSecretsUpdate & { tags?: string[] } }[];
|
||||
tx?: Knex;
|
||||
};
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ services:
|
||||
profiles: ["test"]
|
||||
image: postgres:14-alpine
|
||||
ports:
|
||||
- "5432:5432"
|
||||
- "5430:5432"
|
||||
environment:
|
||||
POSTGRES_PASSWORD: infisical
|
||||
POSTGRES_USER: infisical
|
||||
@@ -54,6 +54,7 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- DB_CONNECTION_URI=postgres://infisical:infisical@db/infisical?sslmode=disable
|
||||
volumes:
|
||||
- ./backend-pg/src:/app/src
|
||||
|
||||
Reference in New Issue
Block a user