From e0c96e722429831b7593eb997d03440852822b11 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 30 Mar 2026 11:31:46 -0700 Subject: [PATCH] Configure connection log retention time --- messages/en-US.json | 2 + server/db/pg/schema/privateSchema.ts | 1 + server/db/sqlite/schema/privateSchema.ts | 1 + server/private/lib/logAccessAudit.ts | 2 + .../routers/billing/featureLifecycle.ts | 25 ++++ server/private/routers/ssh/signSshKey.ts | 2 +- server/routers/org/updateOrg.ts | 20 ++- .../settings/general/security/page.tsx | 115 +++++++++++++++++- 8 files changed, 162 insertions(+), 6 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 7a3fde1d4..11c87f798 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2398,6 +2398,8 @@ "logRetentionAccessDescription": "How long to retain access logs", "logRetentionActionLabel": "Action Log Retention", "logRetentionActionDescription": "How long to retain action logs", + "logRetentionConnectionLabel": "Connection Log Retention", + "logRetentionConnectionDescription": "How long to retain connection logs", "logRetentionDisabled": "Disabled", "logRetention3Days": "3 days", "logRetention7Days": "7 days", diff --git a/server/db/pg/schema/privateSchema.ts b/server/db/pg/schema/privateSchema.ts index bb1e866c4..1f0de4e7d 100644 --- a/server/db/pg/schema/privateSchema.ts +++ b/server/db/pg/schema/privateSchema.ts @@ -291,6 +291,7 @@ export const accessAuditLog = pgTable( actor: varchar("actor", { length: 255 }), actorId: varchar("actorId", { length: 255 }), resourceId: integer("resourceId"), + siteResourceId: integer("siteResourceId"), ip: varchar("ip", { length: 45 }), type: varchar("type", { length: 100 }).notNull(), action: boolean("action").notNull(), diff --git a/server/db/sqlite/schema/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts index 5913497b3..d651c1a38 100644 --- a/server/db/sqlite/schema/privateSchema.ts +++ b/server/db/sqlite/schema/privateSchema.ts @@ -279,6 +279,7 @@ export const accessAuditLog = sqliteTable( actor: text("actor"), actorId: text("actorId"), resourceId: integer("resourceId"), + siteResourceId: integer("siteResourceId"), ip: text("ip"), location: text("location"), type: text("type").notNull(), diff --git a/server/private/lib/logAccessAudit.ts b/server/private/lib/logAccessAudit.ts index 91db548f7..e56490795 100644 --- a/server/private/lib/logAccessAudit.ts +++ b/server/private/lib/logAccessAudit.ts @@ -74,6 +74,7 @@ export async function logAccessAudit(data: { type: string; orgId: string; resourceId?: number; + siteResourceId?: number; user?: { username: string; userId: string }; apiKey?: { name: string | null; apiKeyId: string }; metadata?: any; @@ -134,6 +135,7 @@ export async function logAccessAudit(data: { type: data.type, metadata, resourceId: data.resourceId, + siteResourceId: data.siteResourceId, userAgent: data.userAgent, ip: clientIp, location: countryCode diff --git a/server/private/routers/billing/featureLifecycle.ts b/server/private/routers/billing/featureLifecycle.ts index f6f2d513a..d86e23cf0 100644 --- a/server/private/routers/billing/featureLifecycle.ts +++ b/server/private/routers/billing/featureLifecycle.ts @@ -120,6 +120,18 @@ async function capRetentionDays( ); } + // Cap action log retention if it exceeds the limit + if ( + org.settingsLogRetentionDaysConnection !== null && + org.settingsLogRetentionDaysConnection > maxRetentionDays + ) { + updates.settingsLogRetentionDaysConnection = maxRetentionDays; + needsUpdate = true; + logger.info( + `Capping connection log retention from ${org.settingsLogRetentionDaysConnection} to ${maxRetentionDays} days for org ${orgId}` + ); + } + // Apply updates if needed if (needsUpdate) { await db.update(orgs).set(updates).where(eq(orgs.orgId, orgId)); @@ -262,6 +274,10 @@ async function disableFeature( await disableActionLogs(orgId); break; + case TierFeature.ConnectionLogs: + await disableConnectionLogs(orgId); + break; + case TierFeature.RotateCredentials: await disableRotateCredentials(orgId); break; @@ -458,6 +474,15 @@ async function disableActionLogs(orgId: string): Promise { logger.info(`Disabled action logs for org ${orgId}`); } +async function disableConnectionLogs(orgId: string): Promise { + await db + .update(orgs) + .set({ settingsLogRetentionDaysConnection: 0 }) + .where(eq(orgs.orgId, orgId)); + + logger.info(`Disabled connection logs for org ${orgId}`); +} + async function disableRotateCredentials(orgId: string): Promise {} async function disableMaintencePage(orgId: string): Promise { diff --git a/server/private/routers/ssh/signSshKey.ts b/server/private/routers/ssh/signSshKey.ts index e8de55c54..b02d2b23c 100644 --- a/server/private/routers/ssh/signSshKey.ts +++ b/server/private/routers/ssh/signSshKey.ts @@ -488,7 +488,7 @@ export async function signSshKey( action: true, type: "ssh", orgId: orgId, - resourceId: resource.siteResourceId, + siteResourceId: resource.siteResourceId, user: req.user ? { username: req.user.username ?? "", userId: req.user.userId } : undefined, diff --git a/server/routers/org/updateOrg.ts b/server/routers/org/updateOrg.ts index 5049ac1fa..4eca9a9a6 100644 --- a/server/routers/org/updateOrg.ts +++ b/server/routers/org/updateOrg.ts @@ -34,6 +34,10 @@ const updateOrgBodySchema = z .min(build === "saas" ? 0 : -1) .optional(), settingsLogRetentionDaysAction: z + .number() + .min(build === "saas" ? 0 : -1) + .optional(), + settingsLogRetentionDaysConnection: z .number() .min(build === "saas" ? 0 : -1) .optional() @@ -164,6 +168,17 @@ export async function updateOrg( ) ); } + if ( + parsedBody.data.settingsLogRetentionDaysConnection !== undefined && + parsedBody.data.settingsLogRetentionDaysConnection > maxRetentionDays + ) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + `You are not allowed to set log retention days greater than ${maxRetentionDays} with your current subscription` + ) + ); + } } } @@ -179,7 +194,9 @@ export async function updateOrg( settingsLogRetentionDaysAccess: parsedBody.data.settingsLogRetentionDaysAccess, settingsLogRetentionDaysAction: - parsedBody.data.settingsLogRetentionDaysAction + parsedBody.data.settingsLogRetentionDaysAction, + settingsLogRetentionDaysConnection: + parsedBody.data.settingsLogRetentionDaysConnection }) .where(eq(orgs.orgId, orgId)) .returning(); @@ -197,6 +214,7 @@ export async function updateOrg( await cache.del(`org_${orgId}_retentionDays`); await cache.del(`org_${orgId}_actionDays`); await cache.del(`org_${orgId}_accessDays`); + await cache.del(`org_${orgId}_connectionDays`); return response(res, { data: updatedOrg[0], diff --git a/src/app/[orgId]/settings/general/security/page.tsx b/src/app/[orgId]/settings/general/security/page.tsx index 2c51e9ecb..e7d0d85c8 100644 --- a/src/app/[orgId]/settings/general/security/page.tsx +++ b/src/app/[orgId]/settings/general/security/page.tsx @@ -79,7 +79,8 @@ const SecurityFormSchema = z.object({ passwordExpiryDays: z.number().nullable().optional(), settingsLogRetentionDaysRequest: z.number(), settingsLogRetentionDaysAccess: z.number(), - settingsLogRetentionDaysAction: z.number() + settingsLogRetentionDaysAction: z.number(), + settingsLogRetentionDaysConnection: z.number() }); const LOG_RETENTION_OPTIONS = [ @@ -120,7 +121,8 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { SecurityFormSchema.pick({ settingsLogRetentionDaysRequest: true, settingsLogRetentionDaysAccess: true, - settingsLogRetentionDaysAction: true + settingsLogRetentionDaysAction: true, + settingsLogRetentionDaysConnection: true }) ), defaultValues: { @@ -129,7 +131,9 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { settingsLogRetentionDaysAccess: org.settingsLogRetentionDaysAccess ?? 15, settingsLogRetentionDaysAction: - org.settingsLogRetentionDaysAction ?? 15 + org.settingsLogRetentionDaysAction ?? 15, + settingsLogRetentionDaysConnection: + org.settingsLogRetentionDaysConnection ?? 15 }, mode: "onChange" }); @@ -155,7 +159,9 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { settingsLogRetentionDaysAccess: data.settingsLogRetentionDaysAccess, settingsLogRetentionDaysAction: - data.settingsLogRetentionDaysAction + data.settingsLogRetentionDaysAction, + settingsLogRetentionDaysConnection: + data.settingsLogRetentionDaysConnection } as any; // Update organization @@ -473,6 +479,107 @@ function LogRetentionSectionForm({ org }: SectionFormProps) { ); }} /> + { + const isDisabled = !isPaidUser( + tierMatrix.connectionLogs + ); + + return ( + + + {t( + "logRetentionConnectionLabel" + )} + + + + + + + ); + }} + /> )}