From 27efc908e2189234d197ac05da2b0f9a410875ba Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 27 Jan 2025 15:53:07 +0100 Subject: [PATCH 1/3] feat(audit-logs): query by secret path --- .../ee/services/audit-log/audit-log-dal.ts | 6 ++ .../services/audit-log/audit-log-service.ts | 5 +- .../ee/services/audit-log/audit-log-types.ts | 1 + backend/src/lib/api-docs/constants.ts | 2 + .../server/routes/v1/organization-router.ts | 8 ++- frontend/src/hooks/api/auditLogs/types.tsx | 1 + .../AuditLogsPage/components/LogsFilter.tsx | 70 ++++++++++++------- .../AuditLogsPage/components/LogsSection.tsx | 5 ++ .../AuditLogsPage/components/types.tsx | 3 +- 9 files changed, 71 insertions(+), 30 deletions(-) diff --git a/backend/src/ee/services/audit-log/audit-log-dal.ts b/backend/src/ee/services/audit-log/audit-log-dal.ts index bcef06e10..21f785835 100644 --- a/backend/src/ee/services/audit-log/audit-log-dal.ts +++ b/backend/src/ee/services/audit-log/audit-log-dal.ts @@ -39,11 +39,13 @@ export const auditLogDALFactory = (db: TDbClient) => { offset = 0, actorId, actorType, + secretPath, eventType, eventMetadata }: Omit & { actorId?: string; actorType?: ActorType; + secretPath?: string; eventType?: EventType[]; eventMetadata?: Record; }, @@ -88,6 +90,10 @@ export const auditLogDALFactory = (db: TDbClient) => { }); } + if (projectId && secretPath) { + void sqlQuery.whereRaw(`"eventMetadata" @> jsonb_build_object('secretPath', ?::text)`, [secretPath]); + } + // Filter by actor type if (actorType) { void sqlQuery.where("actor", actorType); diff --git a/backend/src/ee/services/audit-log/audit-log-service.ts b/backend/src/ee/services/audit-log/audit-log-service.ts index ff7dede5f..3b860864f 100644 --- a/backend/src/ee/services/audit-log/audit-log-service.ts +++ b/backend/src/ee/services/audit-log/audit-log-service.ts @@ -46,10 +46,6 @@ export const auditLogServiceFactory = ({ actorOrgId ); - /** - * NOTE (dangtony98): Update this to organization-level audit log permission check once audit logs are moved - * to the organization level ✅ - */ ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs); } @@ -64,6 +60,7 @@ export const auditLogServiceFactory = ({ actorId: filter.auditLogActorId, actorType: filter.actorType, eventMetadata: filter.eventMetadata, + secretPath: filter.secretPath, ...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId }) }); diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 9c19cd3cc..6e8314731 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -32,6 +32,7 @@ export type TListProjectAuditLogDTO = { projectId?: string; auditLogActorId?: string; actorType?: ActorType; + secretPath?: string; eventMetadata?: Record; }; } & Omit; diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 800788179..eca11e983 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -828,6 +828,8 @@ export const AUDIT_LOGS = { projectId: "Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.", eventType: "The type of the event to export.", + secretPath: + "The path of the secret to query audit logs for. Note that the projectId parameter must also be provided.", userAgentType: "Choose which consuming application to export audit logs for.", eventMetadata: "Filter by event metadata key-value pairs. Formatted as `key1=value1,key2=value2`, with comma-separation.", diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index 104898099..db0008ebe 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -11,7 +11,7 @@ import { } from "@app/db/schemas"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs"; -import { getLastMidnightDateISO } from "@app/lib/fn"; +import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -113,6 +113,12 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { querystring: z.object({ projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId), actorType: z.nativeEnum(ActorType).optional(), + secretPath: z + .string() + .optional() + .transform((val) => (!val ? val : removeTrailingSlash(val))) + .describe(AUDIT_LOGS.EXPORT.secretPath), + // eventType is split with , for multiple values, we need to transform it to array eventType: z .string() diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index 3a5070ef5..338671cab 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -10,6 +10,7 @@ export type TGetAuditLogsFilter = { actorType?: ActorType; projectId?: string; actor?: string; // user ID format + secretPath?: string; startDate?: Date; endDate?: Date; limit: number; diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx index 3a21958ae..a1246c695 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx @@ -14,6 +14,7 @@ import { DropdownMenuTrigger, FilterableSelect, FormControl, + Input, Select, SelectItem } from "@app/components/v2"; @@ -50,6 +51,7 @@ export const LogsFilter = ({ className, control, reset, + setValue, watch }: Props) => { const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false); @@ -101,6 +103,7 @@ export const LogsFilter = ({ }; const selectedEventTypes = watch("eventType") as EventType[] | undefined; + const selectedProjectId = watch("project")?.id; return (
- {isOrgAuditLogs && workspacesInOrg.length > 0 && ( - ( - - ({ name, id }))} - getOptionValue={(option) => option.id} - getOptionLabel={(option) => option.name} - /> - - )} - /> - )} +
+ {isOrgAuditLogs && workspacesInOrg.length > 0 && ( + ( + + { + console.log(e); + if (e === null) { + setValue("secretPath", ""); + } + onChange(e); + }} + placeholder="Select a project..." + options={workspacesInOrg.map(({ name, id }) => ({ name, id }))} + getOptionValue={(option) => option.id} + getOptionLabel={(option) => option.name} + /> + + )} + /> + )} + {selectedProjectId && ( + ( + + onChange(e.target.value)} /> + + )} + /> + )} +
(secretPath!, 500); + return (
{showFilters && ( @@ -90,6 +94,7 @@ export const LogsSection = withPermission( isOrgAuditLogs={isOrgAuditLogs} showActorColumn={!!showActorColumn} filter={{ + secretPath: debouncedSecretPath || undefined, eventMetadata: presets?.eventMetadata, projectId, actorType: presets?.actorType, diff --git a/frontend/src/pages/organization/AuditLogsPage/components/types.tsx b/frontend/src/pages/organization/AuditLogsPage/components/types.tsx index 05c44fa4d..0854bd8cf 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/types.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/types.tsx @@ -12,7 +12,8 @@ export const auditLogFilterFormSchema = z startDate: z.date().optional(), endDate: z.date().optional(), page: z.coerce.number().optional(), - perPage: z.coerce.number().optional() + perPage: z.coerce.number().optional(), + secretPath: z.string().optional() }) .superRefine((el, ctx) => { if (el.endDate && el.startDate && el.endDate < el.startDate) { From 939ee892e030ba9c0fe4ddd475b107b854cbf934 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 28 Jan 2025 01:02:18 +0100 Subject: [PATCH 2/3] chore: cleanup --- .../organization/AuditLogsPage/components/LogsFilter.tsx | 1 - .../src/pages/organization/AuditLogsPage/components/types.tsx | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx index a1246c695..8c7ef3446 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx @@ -128,7 +128,6 @@ export const LogsFilter = ({ value={value} isClearable onChange={(e) => { - console.log(e); if (e === null) { setValue("secretPath", ""); } diff --git a/frontend/src/pages/organization/AuditLogsPage/components/types.tsx b/frontend/src/pages/organization/AuditLogsPage/components/types.tsx index 0854bd8cf..533870040 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/types.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/types.tsx @@ -9,11 +9,11 @@ export const auditLogFilterFormSchema = z eventType: z.nativeEnum(EventType).array(), actor: z.string().optional(), userAgentType: z.nativeEnum(UserAgentType), + secretPath: z.string().optional(), startDate: z.date().optional(), endDate: z.date().optional(), page: z.coerce.number().optional(), - perPage: z.coerce.number().optional(), - secretPath: z.string().optional() + perPage: z.coerce.number().optional() }) .superRefine((el, ctx) => { if (el.endDate && el.startDate && el.endDate < el.startDate) { From a24ef46d7d22285e3664a3b64995a2d05b263664 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 28 Jan 2025 20:44:45 +0100 Subject: [PATCH 3/3] requested changes --- .../organization/AuditLogsPage/components/LogsFilter.tsx | 7 ++++--- .../pages/organization/AuditLogsPage/components/types.tsx | 6 +++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx index 8c7ef3446..5c7e020b7 100644 --- a/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx +++ b/frontend/src/pages/organization/AuditLogsPage/components/LogsFilter.tsx @@ -23,6 +23,7 @@ import { useGetAuditLogActorFilterOpts, useGetUserWorkspaces } from "@app/hooks/ import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants"; import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums"; import { Actor } from "@app/hooks/api/auditLogs/types"; +import { ProjectType } from "@app/hooks/api/workspace/types"; import { AuditLogFilterFormData } from "./types"; @@ -103,7 +104,7 @@ export const LogsFilter = ({ }; const selectedEventTypes = watch("eventType") as EventType[] | undefined; - const selectedProjectId = watch("project")?.id; + const selectedProject = watch("project"); return (
({ name, id }))} + options={workspacesInOrg.map(({ name, id, type }) => ({ name, id, type }))} getOptionValue={(option) => option.id} getOptionLabel={(option) => option.name} /> @@ -142,7 +143,7 @@ export const LogsFilter = ({ )} /> )} - {selectedProjectId && ( + {selectedProject?.type === ProjectType.SecretManager && (