feat: added audit log prune, resolved env update and pino file transport on prod

This commit is contained in:
Akhil Mohan
2024-01-31 22:07:49 +05:30
parent 7bf1f4708a
commit 560f8d4a9b
9 changed files with 161 additions and 56 deletions
+3 -1
View File
@@ -16,9 +16,11 @@ export const mockQueue = (): TQueueServiceFactory => {
queues[name] = jobFn; queues[name] = jobFn;
workers[name] = jobFn; workers[name] = jobFn;
}, },
listen: async (name, event) => { listen: (name, event) => {
events[name] = event; events[name] = event;
}, },
clearQueue: async () => {},
stopJobById: async () => {},
stopRepeatableJobByJobId: async () => true stopRepeatableJobByJobId: async () => true
}; };
}; };
@@ -2,6 +2,7 @@ import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { ormify, stripUndefinedInWhere } from "@app/lib/knex"; import { ormify, stripUndefinedInWhere } from "@app/lib/knex";
export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>; export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>;
@@ -25,27 +26,42 @@ export const auditLogDALFactory = (db: TDbClient) => {
{ orgId, projectId, userAgentType, startDate, endDate, limit = 20, offset = 0, actor, eventType }: TFindQuery, { orgId, projectId, userAgentType, startDate, endDate, limit = 20, offset = 0, actor, eventType }: TFindQuery,
tx?: Knex tx?: Knex
) => { ) => {
const sqlQuery = (tx || db)(TableName.AuditLog) try {
.where( const sqlQuery = (tx || db)(TableName.AuditLog)
stripUndefinedInWhere({ .where(
projectId, stripUndefinedInWhere({
orgId, projectId,
eventType, orgId,
actor, eventType,
userAgentType actor,
}) userAgentType
) })
.limit(limit) )
.offset(offset); .limit(limit)
if (startDate) { .offset(offset);
void sqlQuery.where("createdAt", ">=", startDate); if (startDate) {
void sqlQuery.where("createdAt", ">=", startDate);
}
if (endDate) {
void sqlQuery.where("createdAt", "<=", endDate);
}
const docs = await sqlQuery;
return docs;
} catch (error) {
throw new DatabaseError({ error });
} }
if (endDate) {
void sqlQuery.where("createdAt", "<=", endDate);
}
const docs = await sqlQuery;
return docs;
}; };
return { ...auditLogOrm, find }; // delete all audit log that have expired
const pruneAuditLog = async (tx?: Knex) => {
try {
const today = new Date();
const docs = await (tx || db)(TableName.AuditLog).where("expiresAt", "<", today).del();
return docs;
} catch (error) {
throw new DatabaseError({ error, name: "PruneAuditLog" });
}
};
return { ...auditLogOrm, pruneAuditLog, find };
}; };
@@ -1,3 +1,4 @@
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -14,6 +15,19 @@ type TAuditLogQueueServiceFactoryDep = {
export type TAuditLogQueueServiceFactory = ReturnType<typeof auditLogQueueServiceFactory>; export type TAuditLogQueueServiceFactory = ReturnType<typeof auditLogQueueServiceFactory>;
const getTimeDiffForNextAuditLogPrune = (mills: number) => {
const today = new Date(mills);
// Get UTC midnight timestamp for today
const nextUtcMidnight = new Date(Date.UTC(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0));
// Check if we have already passed UTC midnight today
if (today.getTime() >= nextUtcMidnight.getTime()) {
// Add one day to get the timestamp for tomorrow's UTC midnight
nextUtcMidnight.setDate(nextUtcMidnight.getDate() + 1);
}
return nextUtcMidnight.getTime();
};
export const auditLogQueueServiceFactory = ({ export const auditLogQueueServiceFactory = ({
auditLogDAL, auditLogDAL,
queueService, queueService,
@@ -43,6 +57,8 @@ export const auditLogQueueServiceFactory = ({
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
const ttl = plan.auditLogsRetentionDays * MS_IN_DAY; const ttl = plan.auditLogsRetentionDays * MS_IN_DAY;
// skip inserting if audit log retension is 0 meaning its not supported
if (ttl === 0) return;
await auditLogDAL.create({ await auditLogDAL.create({
actor: actor.type, actor: actor.type,
actorMetadata: actor.metadata, actorMetadata: actor.metadata,
@@ -57,7 +73,47 @@ export const auditLogQueueServiceFactory = ({
}); });
}); });
queueService.start(
QueueName.AuditLogPrune,
async () => {
logger.info("Started audit log pruning");
await auditLogDAL.pruneAuditLog();
// calculate next utc time delay
// const nextPruneTime = getTimeDiffForNextAuditLogPrune();
// await queueService.stopJobById(QueueName.AuditLogPrune, "audit-log-prune");
logger.info("Finished audit log pruning");
},
{
settings: {
repeatStrategy: getTimeDiffForNextAuditLogPrune
}
}
);
// we are not using repeat because we want to run the in a predictable time of midnight UTC
// repeat has only cron job and every so we do the repeat manually
const startAuditLogPruneJob = async () => {
// clear previous job
await queueService.stopRepeatableJob(
QueueName.AuditLogPrune,
QueueJobs.AuditLogPrune,
{},
QueueName.AuditLogPrune // just a job id
);
await queueService.queue(QueueName.AuditLogPrune, QueueJobs.AuditLogPrune, undefined, {
delay: 5000,
jobId: QueueName.AuditLogPrune,
repeat: {}
});
};
queueService.listen(QueueName.AuditLogPrune, "error", (err) => {
logger.error("Audit log pruning failed");
logger.error(err);
});
return { return {
pushToLog pushToLog,
startAuditLogPruneJob
}; };
}; };
View File
+13 -4
View File
@@ -26,14 +26,23 @@ const loggerConfig = z.object({
AWS_CLOUDWATCH_LOG_REGION: z.string().default("us-east-1"), AWS_CLOUDWATCH_LOG_REGION: z.string().default("us-east-1"),
AWS_CLOUDWATCH_LOG_ACCESS_KEY_ID: z.string().min(1).optional(), AWS_CLOUDWATCH_LOG_ACCESS_KEY_ID: z.string().min(1).optional(),
AWS_CLOUDWATCH_LOG_ACCESS_KEY_SECRET: z.string().min(1).optional(), AWS_CLOUDWATCH_LOG_ACCESS_KEY_SECRET: z.string().min(1).optional(),
AWS_CLOUDWATCH_LOG_INTERVAL: z.coerce.number().default(1000) AWS_CLOUDWATCH_LOG_INTERVAL: z.coerce.number().default(1000),
NODE_ENV: z.enum(["development", "test", "production"]).default("production")
}); });
export const initLogger = async () => { export const initLogger = async () => {
const targets: pino.TransportMultiOptions["targets"][number][] = [
{ level: "info", target: "pino/file", options: {} }
];
const cfg = loggerConfig.parse(process.env); const cfg = loggerConfig.parse(process.env);
const targets: pino.TransportMultiOptions["targets"][number][] = [
{
level: "info",
target: "pino/file",
options: {
destination: cfg.NODE_ENV === "development" ? 1 : "/var/log/infisical",
mkdir: true
}
}
];
if (cfg.AWS_CLOUDWATCH_LOG_ACCESS_KEY_ID && cfg.AWS_CLOUDWATCH_LOG_ACCESS_KEY_SECRET) { if (cfg.AWS_CLOUDWATCH_LOG_ACCESS_KEY_ID && cfg.AWS_CLOUDWATCH_LOG_ACCESS_KEY_SECRET) {
targets.push({ targets.push({
target: "@serdnam/pino-cloudwatch-transport", target: "@serdnam/pino-cloudwatch-transport",
+24 -3
View File
@@ -1,4 +1,4 @@
import { Job, JobsOptions, Queue, RepeatOptions, Worker, WorkerListener } from "bullmq"; import { Job, JobsOptions, Queue, QueueOptions, RepeatOptions, Worker, WorkerListener } from "bullmq";
import Redis from "ioredis"; import Redis from "ioredis";
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
@@ -11,6 +11,7 @@ export enum QueueName {
SecretRotation = "secret-rotation", SecretRotation = "secret-rotation",
SecretReminder = "secret-reminder", SecretReminder = "secret-reminder",
AuditLog = "audit-log", AuditLog = "audit-log",
AuditLogPrune = "audit-log-prune",
IntegrationSync = "sync-integrations", IntegrationSync = "sync-integrations",
SecretWebhook = "secret-webhook", SecretWebhook = "secret-webhook",
SecretFullRepoScan = "secret-full-repo-scan", SecretFullRepoScan = "secret-full-repo-scan",
@@ -21,6 +22,7 @@ export enum QueueJobs {
SecretReminder = "secret-reminder-job", SecretReminder = "secret-reminder-job",
SecretRotation = "secret-rotation-job", SecretRotation = "secret-rotation-job",
AuditLog = "audit-log-job", AuditLog = "audit-log-job",
AuditLogPrune = "audit-log-prune-job",
SecWebhook = "secret-webhook-trigger", SecWebhook = "secret-webhook-trigger",
IntegrationSync = "secret-integration-pull", IntegrationSync = "secret-integration-pull",
SecretScan = "secret-scan" SecretScan = "secret-scan"
@@ -45,6 +47,10 @@ export type TQueueJobTypes = {
name: QueueJobs.AuditLog; name: QueueJobs.AuditLog;
payload: TCreateAuditLogDTO; payload: TCreateAuditLogDTO;
}; };
[QueueName.AuditLogPrune]: {
name: QueueJobs.AuditLogPrune;
payload: undefined;
};
[QueueName.SecretWebhook]: { [QueueName.SecretWebhook]: {
name: QueueJobs.SecWebhook; name: QueueJobs.SecWebhook;
payload: { projectId: string; environment: string; secretPath: string }; payload: { projectId: string; environment: string; secretPath: string };
@@ -74,16 +80,20 @@ export const queueServiceFactory = (redisUrl: string) => {
const start = <T extends QueueName>( const start = <T extends QueueName>(
name: T, name: T,
jobFn: (job: Job<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>) => Promise<void> jobFn: (job: Job<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>) => Promise<void>,
queueSettings: Omit<QueueOptions, "connection"> = {}
) => { ) => {
if (queueContainer[name]) { if (queueContainer[name]) {
throw new Error(`${name} queue is already initialized`); throw new Error(`${name} queue is already initialized`);
} }
queueContainer[name] = new Queue<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>(name as string, { queueContainer[name] = new Queue<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>(name as string, {
...queueSettings,
connection connection
}); });
workerContainer[name] = new Worker<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>(name, jobFn, { workerContainer[name] = new Worker<TQueueJobTypes[T]["payload"], void, TQueueJobTypes[T]["name"]>(name, jobFn, {
...queueSettings,
connection connection
}); });
}; };
@@ -129,9 +139,20 @@ export const queueServiceFactory = (redisUrl: string) => {
return q.removeRepeatableByKey(job.repeatJobKey); return q.removeRepeatableByKey(job.repeatJobKey);
}; };
const stopJobById = async <T extends QueueName>(name: T, jobId: string) => {
const q = queueContainer[name];
const job = await q.getJob(jobId);
return job?.remove().catch(() => undefined);
};
const clearQueue = async (name: QueueName) => {
const q = queueContainer[name];
await q.drain();
};
const shutdown = async () => { const shutdown = async () => {
await Promise.all(Object.values(workerContainer).map((worker) => worker.close())); await Promise.all(Object.values(workerContainer).map((worker) => worker.close()));
}; };
return { start, listen, queue, shutdown, stopRepeatableJob, stopRepeatableJobByJobId }; return { start, listen, queue, shutdown, stopRepeatableJob, stopRepeatableJobByJobId, clearQueue, stopJobById };
}; };
+1
View File
@@ -444,6 +444,7 @@ export const registerRoutes = async (
}); });
await superAdminService.initServerCfg(); await superAdminService.initServerCfg();
await auditLogQueue.startAuditLogPruneJob();
// setup the communication with license key server // setup the communication with license key server
await licenseService.init(); await licenseService.init();
// inject all services // inject all services
@@ -67,7 +67,7 @@ export const projectEnvServiceFactory = ({
if (!oldEnv) throw new BadRequestError({ message: "Environment not found" }); if (!oldEnv) throw new BadRequestError({ message: "Environment not found" });
if (slug) { if (slug) {
const existingEnv = await projectEnvDAL.findOne({ slug }); const existingEnv = await projectEnvDAL.findOne({ slug, projectId });
if (existingEnv && existingEnv.id !== id) { if (existingEnv && existingEnv.id !== id) {
throw new BadRequestError({ throw new BadRequestError({
message: "Environment with slug already exist", message: "Environment with slug already exist",
+26 -26
View File
@@ -96,32 +96,32 @@ services:
- 1025:1025 # SMTP server - 1025:1025 # SMTP server
- 8025:8025 # Web UI - 8025:8025 # Web UI
mongo: # mongo:
image: mongo # image: mongo
container_name: infisical-dev-mongo # container_name: infisical-dev-mongo
restart: always # restart: always
env_file: .env # env_file: .env
environment: # environment:
- MONGO_INITDB_ROOT_USERNAME=root # - MONGO_INITDB_ROOT_USERNAME=root
- MONGO_INITDB_ROOT_PASSWORD=example # - MONGO_INITDB_ROOT_PASSWORD=example
volumes: # volumes:
- mongo-data:/data/db # - mongo-data:/data/db
ports: # ports:
- 27017:27017 # - 27017:27017
#
mongo-express: # mongo-express:
container_name: infisical-dev-mongo-express # container_name: infisical-dev-mongo-express
image: mongo-express # image: mongo-express
restart: always # restart: always
depends_on: # depends_on:
- mongo # - mongo
env_file: .env # env_file: .env
environment: # environment:
- ME_CONFIG_MONGODB_ADMINUSERNAME=root # - ME_CONFIG_MONGODB_ADMINUSERNAME=root
- ME_CONFIG_MONGODB_ADMINPASSWORD=example # - ME_CONFIG_MONGODB_ADMINPASSWORD=example
- ME_CONFIG_MONGODB_URL=mongodb://root:example@mongo:27017/ # - ME_CONFIG_MONGODB_URL=mongodb://root:example@mongo:27017/
ports: # ports:
- 8081:8081 # - 8081:8081
volumes: volumes:
postgres-data: postgres-data: