Configure connection log retention time

This commit is contained in:
Owen
2026-03-30 11:31:46 -07:00
parent caacd1e677
commit e0c96e7224
8 changed files with 162 additions and 6 deletions

View File

@@ -2398,6 +2398,8 @@
"logRetentionAccessDescription": "How long to retain access logs", "logRetentionAccessDescription": "How long to retain access logs",
"logRetentionActionLabel": "Action Log Retention", "logRetentionActionLabel": "Action Log Retention",
"logRetentionActionDescription": "How long to retain action logs", "logRetentionActionDescription": "How long to retain action logs",
"logRetentionConnectionLabel": "Connection Log Retention",
"logRetentionConnectionDescription": "How long to retain connection logs",
"logRetentionDisabled": "Disabled", "logRetentionDisabled": "Disabled",
"logRetention3Days": "3 days", "logRetention3Days": "3 days",
"logRetention7Days": "7 days", "logRetention7Days": "7 days",

View File

@@ -291,6 +291,7 @@ export const accessAuditLog = pgTable(
actor: varchar("actor", { length: 255 }), actor: varchar("actor", { length: 255 }),
actorId: varchar("actorId", { length: 255 }), actorId: varchar("actorId", { length: 255 }),
resourceId: integer("resourceId"), resourceId: integer("resourceId"),
siteResourceId: integer("siteResourceId"),
ip: varchar("ip", { length: 45 }), ip: varchar("ip", { length: 45 }),
type: varchar("type", { length: 100 }).notNull(), type: varchar("type", { length: 100 }).notNull(),
action: boolean("action").notNull(), action: boolean("action").notNull(),

View File

@@ -279,6 +279,7 @@ export const accessAuditLog = sqliteTable(
actor: text("actor"), actor: text("actor"),
actorId: text("actorId"), actorId: text("actorId"),
resourceId: integer("resourceId"), resourceId: integer("resourceId"),
siteResourceId: integer("siteResourceId"),
ip: text("ip"), ip: text("ip"),
location: text("location"), location: text("location"),
type: text("type").notNull(), type: text("type").notNull(),

View File

@@ -74,6 +74,7 @@ export async function logAccessAudit(data: {
type: string; type: string;
orgId: string; orgId: string;
resourceId?: number; resourceId?: number;
siteResourceId?: number;
user?: { username: string; userId: string }; user?: { username: string; userId: string };
apiKey?: { name: string | null; apiKeyId: string }; apiKey?: { name: string | null; apiKeyId: string };
metadata?: any; metadata?: any;
@@ -134,6 +135,7 @@ export async function logAccessAudit(data: {
type: data.type, type: data.type,
metadata, metadata,
resourceId: data.resourceId, resourceId: data.resourceId,
siteResourceId: data.siteResourceId,
userAgent: data.userAgent, userAgent: data.userAgent,
ip: clientIp, ip: clientIp,
location: countryCode location: countryCode

View File

@@ -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 // Apply updates if needed
if (needsUpdate) { if (needsUpdate) {
await db.update(orgs).set(updates).where(eq(orgs.orgId, orgId)); await db.update(orgs).set(updates).where(eq(orgs.orgId, orgId));
@@ -262,6 +274,10 @@ async function disableFeature(
await disableActionLogs(orgId); await disableActionLogs(orgId);
break; break;
case TierFeature.ConnectionLogs:
await disableConnectionLogs(orgId);
break;
case TierFeature.RotateCredentials: case TierFeature.RotateCredentials:
await disableRotateCredentials(orgId); await disableRotateCredentials(orgId);
break; break;
@@ -458,6 +474,15 @@ async function disableActionLogs(orgId: string): Promise<void> {
logger.info(`Disabled action logs for org ${orgId}`); logger.info(`Disabled action logs for org ${orgId}`);
} }
async function disableConnectionLogs(orgId: string): Promise<void> {
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<void> {} async function disableRotateCredentials(orgId: string): Promise<void> {}
async function disableMaintencePage(orgId: string): Promise<void> { async function disableMaintencePage(orgId: string): Promise<void> {

View File

@@ -488,7 +488,7 @@ export async function signSshKey(
action: true, action: true,
type: "ssh", type: "ssh",
orgId: orgId, orgId: orgId,
resourceId: resource.siteResourceId, siteResourceId: resource.siteResourceId,
user: req.user user: req.user
? { username: req.user.username ?? "", userId: req.user.userId } ? { username: req.user.username ?? "", userId: req.user.userId }
: undefined, : undefined,

View File

@@ -34,6 +34,10 @@ const updateOrgBodySchema = z
.min(build === "saas" ? 0 : -1) .min(build === "saas" ? 0 : -1)
.optional(), .optional(),
settingsLogRetentionDaysAction: z settingsLogRetentionDaysAction: z
.number()
.min(build === "saas" ? 0 : -1)
.optional(),
settingsLogRetentionDaysConnection: z
.number() .number()
.min(build === "saas" ? 0 : -1) .min(build === "saas" ? 0 : -1)
.optional() .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: settingsLogRetentionDaysAccess:
parsedBody.data.settingsLogRetentionDaysAccess, parsedBody.data.settingsLogRetentionDaysAccess,
settingsLogRetentionDaysAction: settingsLogRetentionDaysAction:
parsedBody.data.settingsLogRetentionDaysAction parsedBody.data.settingsLogRetentionDaysAction,
settingsLogRetentionDaysConnection:
parsedBody.data.settingsLogRetentionDaysConnection
}) })
.where(eq(orgs.orgId, orgId)) .where(eq(orgs.orgId, orgId))
.returning(); .returning();
@@ -197,6 +214,7 @@ export async function updateOrg(
await cache.del(`org_${orgId}_retentionDays`); await cache.del(`org_${orgId}_retentionDays`);
await cache.del(`org_${orgId}_actionDays`); await cache.del(`org_${orgId}_actionDays`);
await cache.del(`org_${orgId}_accessDays`); await cache.del(`org_${orgId}_accessDays`);
await cache.del(`org_${orgId}_connectionDays`);
return response(res, { return response(res, {
data: updatedOrg[0], data: updatedOrg[0],

View File

@@ -79,7 +79,8 @@ const SecurityFormSchema = z.object({
passwordExpiryDays: z.number().nullable().optional(), passwordExpiryDays: z.number().nullable().optional(),
settingsLogRetentionDaysRequest: z.number(), settingsLogRetentionDaysRequest: z.number(),
settingsLogRetentionDaysAccess: z.number(), settingsLogRetentionDaysAccess: z.number(),
settingsLogRetentionDaysAction: z.number() settingsLogRetentionDaysAction: z.number(),
settingsLogRetentionDaysConnection: z.number()
}); });
const LOG_RETENTION_OPTIONS = [ const LOG_RETENTION_OPTIONS = [
@@ -120,7 +121,8 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
SecurityFormSchema.pick({ SecurityFormSchema.pick({
settingsLogRetentionDaysRequest: true, settingsLogRetentionDaysRequest: true,
settingsLogRetentionDaysAccess: true, settingsLogRetentionDaysAccess: true,
settingsLogRetentionDaysAction: true settingsLogRetentionDaysAction: true,
settingsLogRetentionDaysConnection: true
}) })
), ),
defaultValues: { defaultValues: {
@@ -129,7 +131,9 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
settingsLogRetentionDaysAccess: settingsLogRetentionDaysAccess:
org.settingsLogRetentionDaysAccess ?? 15, org.settingsLogRetentionDaysAccess ?? 15,
settingsLogRetentionDaysAction: settingsLogRetentionDaysAction:
org.settingsLogRetentionDaysAction ?? 15 org.settingsLogRetentionDaysAction ?? 15,
settingsLogRetentionDaysConnection:
org.settingsLogRetentionDaysConnection ?? 15
}, },
mode: "onChange" mode: "onChange"
}); });
@@ -155,7 +159,9 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
settingsLogRetentionDaysAccess: settingsLogRetentionDaysAccess:
data.settingsLogRetentionDaysAccess, data.settingsLogRetentionDaysAccess,
settingsLogRetentionDaysAction: settingsLogRetentionDaysAction:
data.settingsLogRetentionDaysAction data.settingsLogRetentionDaysAction,
settingsLogRetentionDaysConnection:
data.settingsLogRetentionDaysConnection
} as any; } as any;
// Update organization // Update organization
@@ -473,6 +479,107 @@ function LogRetentionSectionForm({ org }: SectionFormProps) {
); );
}} }}
/> />
<FormField
control={form.control}
name="settingsLogRetentionDaysConnection"
render={({ field }) => {
const isDisabled = !isPaidUser(
tierMatrix.connectionLogs
);
return (
<FormItem>
<FormLabel>
{t(
"logRetentionConnectionLabel"
)}
</FormLabel>
<FormControl>
<Select
value={field.value.toString()}
onValueChange={(
value
) => {
if (
!isDisabled
) {
field.onChange(
parseInt(
value,
10
)
);
}
}}
disabled={
isDisabled
}
>
<SelectTrigger>
<SelectValue
placeholder={t(
"selectLogRetention"
)}
/>
</SelectTrigger>
<SelectContent>
{LOG_RETENTION_OPTIONS.filter(
(option) => {
if (build != "saas") {
return true;
}
let maxDays: number;
if (!subscriptionTier) {
// No tier
maxDays = 3;
} else if (subscriptionTier == "enterprise") {
// Enterprise - no limit
return true;
} else if (subscriptionTier == "tier3") {
maxDays = 90;
} else if (subscriptionTier == "tier2") {
maxDays = 30;
} else if (subscriptionTier == "tier1") {
maxDays = 7;
} else {
// Default to most restrictive
maxDays = 3;
}
// Filter out options that exceed the max
// Special values: -1 (forever) and 9001 (end of year) should be filtered
if (option.value < 0 || option.value > maxDays) {
return false;
}
return true;
}
).map(
(
option
) => (
<SelectItem
key={
option.value
}
value={option.value.toString()}
>
{t(
option.label
)}
</SelectItem>
)
)}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
</> </>
)} )}
</form> </form>