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