Merge pull request #3045 from Infisical/daniel/auditlogs-secret-path-query

feat(audit-logs): query by secret path
This commit is contained in:
Daniel Hougaard
2025-01-28 21:17:42 +01:00
committed by GitHub
9 changed files with 75 additions and 30 deletions

View File

@@ -39,11 +39,13 @@ export const auditLogDALFactory = (db: TDbClient) => {
offset = 0, offset = 0,
actorId, actorId,
actorType, actorType,
secretPath,
eventType, eventType,
eventMetadata eventMetadata
}: Omit<TFindQuery, "actor" | "eventType"> & { }: Omit<TFindQuery, "actor" | "eventType"> & {
actorId?: string; actorId?: string;
actorType?: ActorType; actorType?: ActorType;
secretPath?: string;
eventType?: EventType[]; eventType?: EventType[];
eventMetadata?: Record<string, string>; eventMetadata?: Record<string, string>;
}, },
@@ -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 // Filter by actor type
if (actorType) { if (actorType) {
void sqlQuery.where("actor", actorType); void sqlQuery.where("actor", actorType);

View File

@@ -46,10 +46,6 @@ export const auditLogServiceFactory = ({
actorOrgId 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); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
} }
@@ -64,6 +60,7 @@ export const auditLogServiceFactory = ({
actorId: filter.auditLogActorId, actorId: filter.auditLogActorId,
actorType: filter.actorType, actorType: filter.actorType,
eventMetadata: filter.eventMetadata, eventMetadata: filter.eventMetadata,
secretPath: filter.secretPath,
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId }) ...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId })
}); });

View File

@@ -32,6 +32,7 @@ export type TListProjectAuditLogDTO = {
projectId?: string; projectId?: string;
auditLogActorId?: string; auditLogActorId?: string;
actorType?: ActorType; actorType?: ActorType;
secretPath?: string;
eventMetadata?: Record<string, string>; eventMetadata?: Record<string, string>;
}; };
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;

View File

@@ -828,6 +828,8 @@ export const AUDIT_LOGS = {
projectId: projectId:
"Optionally filter logs by project ID. If not provided, logs from the entire organization will be returned.", "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.", 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.", userAgentType: "Choose which consuming application to export audit logs for.",
eventMetadata: eventMetadata:
"Filter by event metadata key-value pairs. Formatted as `key1=value1,key2=value2`, with comma-separation.", "Filter by event metadata key-value pairs. Formatted as `key1=value1,key2=value2`, with comma-separation.",

View File

@@ -11,7 +11,7 @@ import {
} from "@app/db/schemas"; } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs"; 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 { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas"; import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -113,6 +113,12 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
querystring: z.object({ querystring: z.object({
projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId), projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId),
actorType: z.nativeEnum(ActorType).optional(), 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 is split with , for multiple values, we need to transform it to array
eventType: z eventType: z
.string() .string()

View File

@@ -10,6 +10,7 @@ export type TGetAuditLogsFilter = {
actorType?: ActorType; actorType?: ActorType;
projectId?: string; projectId?: string;
actor?: string; // user ID format actor?: string; // user ID format
secretPath?: string;
startDate?: Date; startDate?: Date;
endDate?: Date; endDate?: Date;
limit: number; limit: number;

View File

@@ -14,6 +14,7 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
FilterableSelect, FilterableSelect,
FormControl, FormControl,
Input,
Select, Select,
SelectItem SelectItem
} from "@app/components/v2"; } from "@app/components/v2";
@@ -22,6 +23,7 @@ import { useGetAuditLogActorFilterOpts, useGetUserWorkspaces } from "@app/hooks/
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants"; import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums"; import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
import { Actor } from "@app/hooks/api/auditLogs/types"; import { Actor } from "@app/hooks/api/auditLogs/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { AuditLogFilterFormData } from "./types"; import { AuditLogFilterFormData } from "./types";
@@ -50,6 +52,7 @@ export const LogsFilter = ({
className, className,
control, control,
reset, reset,
setValue,
watch watch
}: Props) => { }: Props) => {
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false); const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
@@ -101,6 +104,7 @@ export const LogsFilter = ({
}; };
const selectedEventTypes = watch("eventType") as EventType[] | undefined; const selectedEventTypes = watch("eventType") as EventType[] | undefined;
const selectedProject = watch("project");
return ( return (
<div <div
@@ -109,6 +113,7 @@ export const LogsFilter = ({
className className
)} )}
> >
<div className="flex items-center gap-4">
{isOrgAuditLogs && workspacesInOrg.length > 0 && ( {isOrgAuditLogs && workspacesInOrg.length > 0 && (
<Controller <Controller
control={control} control={control}
@@ -118,14 +123,19 @@ export const LogsFilter = ({
label="Project" label="Project"
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
className="mr-12 w-64" className="w-64"
> >
<FilterableSelect <FilterableSelect
value={value} value={value}
isClearable isClearable
onChange={onChange} onChange={(e) => {
if (e === null) {
setValue("secretPath", "");
}
onChange(e);
}}
placeholder="Select a project..." placeholder="Select a project..."
options={workspacesInOrg.map(({ name, id }) => ({ name, id }))} options={workspacesInOrg.map(({ name, id, type }) => ({ name, id, type }))}
getOptionValue={(option) => option.id} getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name} getOptionLabel={(option) => option.name}
/> />
@@ -133,6 +143,18 @@ export const LogsFilter = ({
)} )}
/> />
)} )}
{selectedProject?.type === ProjectType.SecretManager && (
<Controller
control={control}
name="secretPath"
render={({ field: { onChange, value, ...field } }) => (
<FormControl label="Secret path" className="w-40">
<Input {...field} value={value} onChange={(e) => onChange(e.target.value)} />
</FormControl>
)}
/>
)}
</div>
<div className="mt-1 flex items-center space-x-2"> <div className="mt-1 flex items-center space-x-2">
<Controller <Controller
control={control} control={control}

View File

@@ -5,6 +5,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context"; import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { withPermission } from "@app/hoc"; import { withPermission } from "@app/hoc";
import { useDebounce } from "@app/hooks";
import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums"; import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
@@ -67,10 +68,13 @@ export const LogsSection = withPermission(
const userAgentType = watch("userAgentType") as UserAgentType | undefined; const userAgentType = watch("userAgentType") as UserAgentType | undefined;
const actor = watch("actor"); const actor = watch("actor");
const projectId = watch("project")?.id; const projectId = watch("project")?.id;
const secretPath = watch("secretPath");
const startDate = watch("startDate"); const startDate = watch("startDate");
const endDate = watch("endDate"); const endDate = watch("endDate");
const [debouncedSecretPath] = useDebounce<string>(secretPath!, 500);
return ( return (
<div> <div>
{showFilters && ( {showFilters && (
@@ -90,6 +94,7 @@ export const LogsSection = withPermission(
isOrgAuditLogs={isOrgAuditLogs} isOrgAuditLogs={isOrgAuditLogs}
showActorColumn={!!showActorColumn} showActorColumn={!!showActorColumn}
filter={{ filter={{
secretPath: debouncedSecretPath || undefined,
eventMetadata: presets?.eventMetadata, eventMetadata: presets?.eventMetadata,
projectId, projectId,
actorType: presets?.actorType, actorType: presets?.actorType,

View File

@@ -1,14 +1,19 @@
import { z } from "zod"; import { z } from "zod";
import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums"; import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
import { ProjectType } from "@app/hooks/api/workspace/types";
export const auditLogFilterFormSchema = z export const auditLogFilterFormSchema = z
.object({ .object({
eventMetadata: z.object({}).optional(), eventMetadata: z.object({}).optional(),
project: z.object({ id: z.string(), name: z.string() }).optional().nullable(), project: z
.object({ id: z.string(), name: z.string(), type: z.nativeEnum(ProjectType) })
.optional()
.nullable(),
eventType: z.nativeEnum(EventType).array(), eventType: z.nativeEnum(EventType).array(),
actor: z.string().optional(), actor: z.string().optional(),
userAgentType: z.nativeEnum(UserAgentType), userAgentType: z.nativeEnum(UserAgentType),
secretPath: z.string().optional(),
startDate: z.date().optional(), startDate: z.date().optional(),
endDate: z.date().optional(), endDate: z.date().optional(),
page: z.coerce.number().optional(), page: z.coerce.number().optional(),