mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 18:27:36 +00:00
feat(infisical-pg): fixed secret rotation and secret approval limit offset
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
+13
-10
@@ -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
|
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
|
|||||||
+6
-6
@@ -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]
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
+35
-13
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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()),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user