Filtering working on both access and request

This commit is contained in:
Owen
2025-10-23 14:34:56 -07:00
parent eae2c37388
commit 264bf46798
12 changed files with 936 additions and 136 deletions

View File

@@ -49,7 +49,6 @@ export async function exportAccessAuditLogs(
)
);
}
const { timeStart, timeEnd, limit, offset } = parsedQuery.data;
const parsedParams = queryAccessAuditLogsParams.safeParse(req.params);
if (!parsedParams.success) {
@@ -60,16 +59,17 @@ export async function exportAccessAuditLogs(
)
);
}
const { orgId } = parsedParams.data;
const baseQuery = queryAccess(timeStart, timeEnd, orgId);
const data = { ...parsedQuery.data, ...parsedParams.data };
const log = await baseQuery.limit(limit).offset(offset);
const baseQuery = queryAccess(data);
const log = await baseQuery.limit(data.limit).offset(data.offset);
const csvData = generateCSV(log);
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', `attachment; filename="access-audit-logs-${orgId}-${Date.now()}.csv"`);
res.setHeader('Content-Disposition', `attachment; filename="access-audit-logs-${data.orgId}-${Date.now()}.csv"`);
return res.send(csvData);
} catch (error) {

View File

@@ -49,7 +49,6 @@ export async function exportActionAuditLogs(
)
);
}
const { timeStart, timeEnd, limit, offset } = parsedQuery.data;
const parsedParams = queryActionAuditLogsParams.safeParse(req.params);
if (!parsedParams.success) {
@@ -60,16 +59,17 @@ export async function exportActionAuditLogs(
)
);
}
const { orgId } = parsedParams.data;
const baseQuery = queryAction(timeStart, timeEnd, orgId);
const data = { ...parsedQuery.data, ...parsedParams.data };
const log = await baseQuery.limit(limit).offset(offset);
const baseQuery = queryAction(data);
const log = await baseQuery.limit(data.limit).offset(data.offset);
const csvData = generateCSV(log);
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', `attachment; filename="action-audit-logs-${orgId}-${Date.now()}.csv"`);
res.setHeader('Content-Disposition', `attachment; filename="action-audit-logs-${data.orgId}-${Date.now()}.csv"`);
return res.send(csvData);
} catch (error) {

View File

@@ -41,6 +41,20 @@ export const queryAccessAuditLogsQuery = z.object({
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
.optional()
.default(new Date().toISOString()),
action: z
.union([z.boolean(), z.string()])
.transform((val) => (typeof val === "string" ? val === "true" : val))
.optional(),
actorType: z.string().optional(),
actorId: z.string().optional(),
resourceId: z
.string()
.optional()
.transform(Number)
.pipe(z.number().int().positive())
.optional(),
actor: z.string().optional(),
type: z.string().optional(),
limit: z
.string()
.optional()
@@ -59,7 +73,32 @@ export const queryAccessAuditLogsParams = z.object({
orgId: z.string()
});
export function queryAccess(timeStart: number, timeEnd: number, orgId: string) {
export const queryAccessAuditLogsCombined = queryAccessAuditLogsQuery.merge(
queryAccessAuditLogsParams
);
type Q = z.infer<typeof queryAccessAuditLogsCombined>;
function getWhere(data: Q) {
return and(
gt(accessAuditLog.timestamp, data.timeStart),
lt(accessAuditLog.timestamp, data.timeEnd),
eq(accessAuditLog.orgId, data.orgId),
data.resourceId
? eq(accessAuditLog.resourceId, data.resourceId)
: undefined,
data.actor ? eq(accessAuditLog.actor, data.actor) : undefined,
data.actorType
? eq(accessAuditLog.actorType, data.actorType)
: undefined,
data.actorId ? eq(accessAuditLog.actorId, data.actorId) : undefined,
data.type ? eq(accessAuditLog.type, data.type) : undefined,
data.action !== undefined
? eq(accessAuditLog.action, data.action)
: undefined
);
}
export function queryAccess(data: Q) {
return db
.select({
orgId: accessAuditLog.orgId,
@@ -78,31 +117,69 @@ export function queryAccess(timeStart: number, timeEnd: number, orgId: string) {
actor: accessAuditLog.actor
})
.from(accessAuditLog)
.leftJoin(resources, eq(accessAuditLog.resourceId, resources.resourceId))
.where(
and(
gt(accessAuditLog.timestamp, timeStart),
lt(accessAuditLog.timestamp, timeEnd),
eq(accessAuditLog.orgId, orgId)
)
.leftJoin(
resources,
eq(accessAuditLog.resourceId, resources.resourceId)
)
.where(getWhere(data))
.orderBy(accessAuditLog.timestamp);
}
export function countAccessQuery(timeStart: number, timeEnd: number, orgId: string) {
const countQuery = db
.select({ count: count() })
.from(accessAuditLog)
.where(
and(
gt(accessAuditLog.timestamp, timeStart),
lt(accessAuditLog.timestamp, timeEnd),
eq(accessAuditLog.orgId, orgId)
)
);
export function countAccessQuery(data: Q) {
const countQuery = db
.select({ count: count() })
.from(accessAuditLog)
.where(getWhere(data));
return countQuery;
}
async function queryUniqueFilterAttributes(
timeStart: number,
timeEnd: number,
orgId: string
) {
const baseConditions = and(
gt(accessAuditLog.timestamp, timeStart),
lt(accessAuditLog.timestamp, timeEnd),
eq(accessAuditLog.orgId, orgId)
);
// Get unique actors
const uniqueActors = await db
.selectDistinct({
actor: accessAuditLog.actor
})
.from(accessAuditLog)
.where(baseConditions);
// Get unique locations
const uniqueLocations = await db
.selectDistinct({
locations: accessAuditLog.location
})
.from(accessAuditLog)
.where(baseConditions);
// Get unique resources with names
const uniqueResources = await db
.selectDistinct({
id: accessAuditLog.resourceId,
name: resources.name
})
.from(accessAuditLog)
.leftJoin(
resources,
eq(accessAuditLog.resourceId, resources.resourceId)
)
.where(baseConditions);
return {
actors: uniqueActors.map(row => row.actor).filter((actor): actor is string => actor !== null),
resources: uniqueResources.filter((row): row is { id: number; name: string | null } => row.id !== null),
locations: uniqueLocations.map(row => row.locations).filter((location): location is string => location !== null)
};
}
registry.registerPath({
method: "get",
path: "/org/{orgId}/logs/access",
@@ -130,8 +207,6 @@ export async function queryAccessAuditLogs(
)
);
}
const { timeStart, timeEnd, limit, offset } = parsedQuery.data;
const parsedParams = queryAccessAuditLogsParams.safeParse(req.params);
if (!parsedParams.success) {
return next(
@@ -141,23 +216,31 @@ export async function queryAccessAuditLogs(
)
);
}
const { orgId } = parsedParams.data;
const baseQuery = queryAccess(timeStart, timeEnd, orgId);
const data = { ...parsedQuery.data, ...parsedParams.data };
const log = await baseQuery.limit(limit).offset(offset);
const baseQuery = queryAccess(data);
const totalCountResult = await countAccessQuery(timeStart, timeEnd, orgId);
const log = await baseQuery.limit(data.limit).offset(data.offset);
const totalCountResult = await countAccessQuery(data);
const totalCount = totalCountResult[0].count;
const filterAttributes = await queryUniqueFilterAttributes(
data.timeStart,
data.timeEnd,
data.orgId
);
return response<QueryAccessAuditLogResponse>(res, {
data: {
log: log,
pagination: {
total: totalCount,
limit,
offset
}
limit: data.limit,
offset: data.offset
},
filterAttributes
},
success: true,
error: false,

View File

@@ -24,7 +24,6 @@ import { fromError } from "zod-validation-error";
import { QueryActionAuditLogResponse } from "@server/routers/auditLogs/types";
import response from "@server/lib/response";
import logger from "@server/logger";
import { metadata } from "@app/app/[orgId]/settings/layout";
export const queryActionAuditLogsQuery = z.object({
// iso string just validate its a parseable date
@@ -42,6 +41,10 @@ export const queryActionAuditLogsQuery = z.object({
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
.optional()
.default(new Date().toISOString()),
action: z.string().optional(),
actorType: z.string().optional(),
actorId: z.string().optional(),
actor: z.string().optional(),
limit: z
.string()
.optional()
@@ -60,7 +63,23 @@ export const queryActionAuditLogsParams = z.object({
orgId: z.string()
});
export function queryAction(timeStart: number, timeEnd: number, orgId: string) {
export const queryActionAuditLogsCombined =
queryActionAuditLogsQuery.merge(queryActionAuditLogsParams);
type Q = z.infer<typeof queryActionAuditLogsCombined>;
function getWhere(data: Q) {
return and(
gt(actionAuditLog.timestamp, data.timeStart),
lt(actionAuditLog.timestamp, data.timeEnd),
eq(actionAuditLog.orgId, data.orgId),
data.actor ? eq(actionAuditLog.actor, data.actor) : undefined,
data.actorType ? eq(actionAuditLog.actorType, data.actorType) : undefined,
data.actorId ? eq(actionAuditLog.actorId, data.actorId) : undefined,
data.action ? eq(actionAuditLog.action, data.action) : undefined
);
}
export function queryAction(data: Q) {
return db
.select({
orgId: actionAuditLog.orgId,
@@ -72,27 +91,15 @@ export function queryAction(timeStart: number, timeEnd: number, orgId: string) {
actor: actionAuditLog.actor
})
.from(actionAuditLog)
.where(
and(
gt(actionAuditLog.timestamp, timeStart),
lt(actionAuditLog.timestamp, timeEnd),
eq(actionAuditLog.orgId, orgId)
)
)
.where(getWhere(data))
.orderBy(actionAuditLog.timestamp);
}
export function countActionQuery(timeStart: number, timeEnd: number, orgId: string) {
const countQuery = db
.select({ count: count() })
.from(actionAuditLog)
.where(
and(
gt(actionAuditLog.timestamp, timeStart),
lt(actionAuditLog.timestamp, timeEnd),
eq(actionAuditLog.orgId, orgId)
)
);
export function countActionQuery(data: Q) {
const countQuery = db
.select({ count: count() })
.from(actionAuditLog)
.where(getWhere(data));
return countQuery;
}
@@ -123,8 +130,6 @@ export async function queryActionAuditLogs(
)
);
}
const { timeStart, timeEnd, limit, offset } = parsedQuery.data;
const parsedParams = queryActionAuditLogsParams.safeParse(req.params);
if (!parsedParams.success) {
return next(
@@ -134,13 +139,14 @@ export async function queryActionAuditLogs(
)
);
}
const { orgId } = parsedParams.data;
const baseQuery = queryAction(timeStart, timeEnd, orgId);
const data = { ...parsedQuery.data, ...parsedParams.data };
const log = await baseQuery.limit(limit).offset(offset);
const baseQuery = queryAction(data);
const totalCountResult = await countActionQuery(timeStart, timeEnd, orgId);
const log = await baseQuery.limit(data.limit).offset(data.offset);
const totalCountResult = await countActionQuery(data);
const totalCount = totalCountResult[0].count;
return response<QueryActionAuditLogResponse>(res, {
@@ -148,8 +154,8 @@ export async function queryActionAuditLogs(
log: log,
pagination: {
total: totalCount,
limit,
offset
limit: data.limit,
offset: data.offset
}
},
success: true,

View File

@@ -28,11 +28,26 @@ export const queryAccessAuditLogsQuery = z.object({
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
.optional()
.default(new Date().toISOString()),
action: z.boolean().optional(),
action: z
.union([z.boolean(), z.string()])
.transform((val) => (typeof val === "string" ? val === "true" : val))
.optional(),
method: z.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).optional(),
reason: z.number().optional(),
resourceId: z.number().optional(),
reason: z
.string()
.optional()
.transform(Number)
.pipe(z.number().int().positive())
.optional(),
resourceId: z
.string()
.optional()
.transform(Number)
.pipe(z.number().int().positive())
.optional(),
actor: z.string().optional(),
host: z.string().optional(),
path: z.string().optional(),
limit: z
.string()
.optional()
@@ -65,7 +80,12 @@ function getWhere(data: Q) {
: undefined,
data.actor ? eq(requestAuditLog.actor, data.actor) : undefined,
data.method ? eq(requestAuditLog.method, data.method) : undefined,
data.reason ? eq(requestAuditLog.reason, data.reason) : undefined
data.reason ? eq(requestAuditLog.reason, data.reason) : undefined,
data.host ? eq(requestAuditLog.host, data.host) : undefined,
data.path ? eq(requestAuditLog.path, data.path) : undefined,
data.action !== undefined
? eq(requestAuditLog.action, data.action)
: undefined
);
}
@@ -124,6 +144,71 @@ registry.registerPath({
responses: {}
});
async function queryUniqueFilterAttributes(
timeStart: number,
timeEnd: number,
orgId: string
) {
const baseConditions = and(
gt(requestAuditLog.timestamp, timeStart),
lt(requestAuditLog.timestamp, timeEnd),
eq(requestAuditLog.orgId, orgId)
);
// Get unique actors
const uniqueActors = await db
.selectDistinct({
actor: requestAuditLog.actor
})
.from(requestAuditLog)
.where(baseConditions);
// Get unique locations
const uniqueLocations = await db
.selectDistinct({
locations: requestAuditLog.location
})
.from(requestAuditLog)
.where(baseConditions);
// Get unique actors
const uniqueHosts = await db
.selectDistinct({
hosts: requestAuditLog.host
})
.from(requestAuditLog)
.where(baseConditions);
// Get unique actors
const uniquePaths = await db
.selectDistinct({
paths: requestAuditLog.path
})
.from(requestAuditLog)
.where(baseConditions);
// Get unique resources with names
const uniqueResources = await db
.selectDistinct({
id: requestAuditLog.resourceId,
name: resources.name
})
.from(requestAuditLog)
.leftJoin(
resources,
eq(requestAuditLog.resourceId, resources.resourceId)
)
.where(baseConditions);
return {
actors: uniqueActors.map(row => row.actor).filter((actor): actor is string => actor !== null),
resources: uniqueResources.filter((row): row is { id: number; name: string | null } => row.id !== null),
locations: uniqueLocations.map(row => row.locations).filter((location): location is string => location !== null),
hosts: uniqueHosts.map(row => row.hosts).filter((host): host is string => host !== null),
paths: uniquePaths.map(row => row.paths).filter((path): path is string => path !== null)
};
}
export async function queryRequestAuditLogs(
req: Request,
res: Response,
@@ -159,6 +244,12 @@ export async function queryRequestAuditLogs(
const totalCountResult = await countRequestQuery(data);
const totalCount = totalCountResult[0].count;
const filterAttributes = await queryUniqueFilterAttributes(
data.timeStart,
data.timeEnd,
data.orgId
);
return response<QueryRequestAuditLogResponse>(res, {
data: {
log: log,
@@ -166,7 +257,8 @@ export async function queryRequestAuditLogs(
total: totalCount,
limit: data.limit,
offset: data.offset
}
},
filterAttributes
},
success: true,
error: false,

View File

@@ -45,6 +45,16 @@ export type QueryRequestAuditLogResponse = {
limit: number;
offset: number;
};
filterAttributes: {
actors: string[];
resources: {
id: number;
name: string | null;
}[];
locations: string[];
hosts: string[];
paths: string[];
};
};
export type QueryAccessAuditLogResponse = {
@@ -69,4 +79,12 @@ export type QueryAccessAuditLogResponse = {
limit: number;
offset: number;
};
};
filterAttributes: {
actors: string[];
resources: {
id: number;
name: string | null;
}[];
locations: string[];
};
};