mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 22:51:53 +00:00
Merge pull request #3272 from akhilmhdh/feat/metadata-audit-log
Permission metadata data in audit log
This commit is contained in:
@@ -18,7 +18,7 @@ export const AuditLogsPage = () => {
|
||||
title="Audit logs"
|
||||
description="Audit logs for security and compliance teams to monitor information access."
|
||||
/>
|
||||
<LogsSection filterClassName="static py-2" showFilters isOrgAuditLogs showActorColumn />
|
||||
<LogsSection filterClassName="static py-2" showFilters />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ActorType, EventType, UserAgentType } from "@app/hooks/api/auditLogs/en
|
||||
import { usePopUp } from "@app/hooks/usePopUp";
|
||||
|
||||
import { LogsFilter } from "./LogsFilter";
|
||||
import { LogsTable, TAuditLogTableHeader } from "./LogsTable";
|
||||
import { LogsTable } from "./LogsTable";
|
||||
import { AuditLogFilterFormData, auditLogFilterFormSchema } from "./types";
|
||||
|
||||
type Props = {
|
||||
@@ -25,22 +25,11 @@ type Props = {
|
||||
|
||||
showFilters?: boolean;
|
||||
filterClassName?: string;
|
||||
isOrgAuditLogs?: boolean;
|
||||
showActorColumn?: boolean;
|
||||
remappedHeaders?: Partial<Record<TAuditLogTableHeader, string>>;
|
||||
refetchInterval?: number;
|
||||
};
|
||||
|
||||
export const LogsSection = withPermission(
|
||||
({
|
||||
presets,
|
||||
filterClassName,
|
||||
remappedHeaders,
|
||||
isOrgAuditLogs,
|
||||
showActorColumn,
|
||||
refetchInterval,
|
||||
showFilters
|
||||
}: Props) => {
|
||||
({ presets, filterClassName, refetchInterval, showFilters }: Props) => {
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
||||
@@ -90,9 +79,6 @@ export const LogsSection = withPermission(
|
||||
)}
|
||||
<LogsTable
|
||||
refetchInterval={refetchInterval}
|
||||
remappedHeaders={remappedHeaders}
|
||||
isOrgAuditLogs={isOrgAuditLogs}
|
||||
showActorColumn={!!showActorColumn}
|
||||
filter={{
|
||||
secretPath: debouncedSecretPath || undefined,
|
||||
eventMetadata: presets?.eventMetadata,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Fragment } from "react";
|
||||
import { faFile } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faFile, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
Button,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useGetAuditLogs } from "@app/hooks/api";
|
||||
@@ -19,32 +21,13 @@ import { TGetAuditLogsFilter } from "@app/hooks/api/auditLogs/types";
|
||||
import { LogsTableRow } from "./LogsTableRow";
|
||||
|
||||
type Props = {
|
||||
isOrgAuditLogs?: boolean;
|
||||
showActorColumn: boolean;
|
||||
filter?: TGetAuditLogsFilter;
|
||||
remappedHeaders?: Partial<Record<TAuditLogTableHeader, string>>;
|
||||
refetchInterval?: number;
|
||||
};
|
||||
|
||||
const AUDIT_LOG_LIMIT = 15;
|
||||
|
||||
const TABLE_HEADERS = [
|
||||
"Timestamp (MM/DD/YYYY)",
|
||||
"Event",
|
||||
"Project",
|
||||
"Actor",
|
||||
"Source",
|
||||
"Metadata"
|
||||
] as const;
|
||||
export type TAuditLogTableHeader = (typeof TABLE_HEADERS)[number];
|
||||
|
||||
export const LogsTable = ({
|
||||
showActorColumn,
|
||||
isOrgAuditLogs,
|
||||
filter,
|
||||
remappedHeaders,
|
||||
refetchInterval
|
||||
}: Props) => {
|
||||
export const LogsTable = ({ filter, refetchInterval }: Props) => {
|
||||
// Determine the project ID for filtering
|
||||
const filterProjectId =
|
||||
// Use the projectId from the filter if it exists
|
||||
@@ -69,38 +52,37 @@ export const LogsTable = ({
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
{TABLE_HEADERS.map((header, idx) => {
|
||||
if (
|
||||
(header === "Project" && !isOrgAuditLogs) ||
|
||||
(header === "Actor" && !showActorColumn)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Th key={`table-header-${idx + 1}`}>{remappedHeaders?.[header] || header}</Th>
|
||||
);
|
||||
})}
|
||||
<Th className="w-24" />
|
||||
<Th className="w-64">
|
||||
Timestamp
|
||||
<Tooltip
|
||||
className="normal-case"
|
||||
content="Time displayed in your system's time zone."
|
||||
sideOffset={10}
|
||||
>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="ml-1" />
|
||||
</Tooltip>
|
||||
</Th>
|
||||
<Th>Event</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{!isPending &&
|
||||
data?.pages?.map((group, i) => (
|
||||
<Fragment key={`audit-log-fragment-${i + 1}`}>
|
||||
{group.map((auditLog) => (
|
||||
{group.map((auditLog, index) => (
|
||||
<LogsTableRow
|
||||
showActorColumn={showActorColumn}
|
||||
isOrgAuditLogs={isOrgAuditLogs}
|
||||
rowNumber={index + i * AUDIT_LOG_LIMIT + 1}
|
||||
auditLog={auditLog}
|
||||
key={`audit-log-${auditLog.id}`}
|
||||
/>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
{isPending && <TableSkeleton innerKey="logs-table" columns={5} key="logs" />}
|
||||
{isPending && <TableSkeleton innerKey="logs-table" columns={3} key="logs-loading" />}
|
||||
{isEmpty && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<Td colSpan={3}>
|
||||
<EmptyState title="No audit logs on file" icon={faFile} />
|
||||
</Td>
|
||||
</Tr>
|
||||
|
||||
@@ -1,128 +1,76 @@
|
||||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCaretDown, faCaretRight } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { Td, Tooltip, Tr } from "@app/components/v2";
|
||||
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
|
||||
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { Actor, AuditLog } from "@app/hooks/api/auditLogs/types";
|
||||
import { Td, Tr } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { AuditLog } from "@app/hooks/api/auditLogs/types";
|
||||
|
||||
type Props = {
|
||||
auditLog: AuditLog;
|
||||
isOrgAuditLogs?: boolean;
|
||||
showActorColumn: boolean;
|
||||
rowNumber: number;
|
||||
};
|
||||
|
||||
export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Props) => {
|
||||
const renderActor = (actor: Actor) => {
|
||||
if (!actor) {
|
||||
return <Td />;
|
||||
}
|
||||
|
||||
switch (actor.type) {
|
||||
case ActorType.USER:
|
||||
return (
|
||||
<Td>
|
||||
<p>{actor.metadata.email}</p>
|
||||
<p>User</p>
|
||||
</Td>
|
||||
);
|
||||
case ActorType.SERVICE:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`${actor.metadata.name}`}</p>
|
||||
<p>Service token</p>
|
||||
</Td>
|
||||
);
|
||||
case ActorType.IDENTITY:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`${actor.metadata.name}`}</p>
|
||||
<p>Machine Identity</p>
|
||||
</Td>
|
||||
);
|
||||
case ActorType.PLATFORM:
|
||||
return (
|
||||
<Td>
|
||||
<p>Platform</p>
|
||||
</Td>
|
||||
);
|
||||
case ActorType.KMIP_CLIENT:
|
||||
return (
|
||||
<Td>
|
||||
<p>{actor.metadata.name}</p>
|
||||
<p>KMIP Client</p>
|
||||
</Td>
|
||||
);
|
||||
case ActorType.UNKNOWN_USER:
|
||||
return (
|
||||
<Td>
|
||||
<div className="flex items-center gap-2">
|
||||
<p>Unknown User</p>
|
||||
<Tooltip content="This action was performed by a user who was not authenticated at the time.">
|
||||
<FontAwesomeIcon className="text-mineshaft-400" icon={faQuestionCircle} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Td>
|
||||
);
|
||||
default:
|
||||
return <Td />;
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateToFormat: string) => {
|
||||
const date = new Date(dateToFormat);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(date.getDate()).padStart(2, "0");
|
||||
|
||||
let hours = date.getHours();
|
||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
||||
|
||||
// convert from 24h to 12h format
|
||||
const period = hours >= 12 ? "PM" : "AM";
|
||||
hours %= 12;
|
||||
hours = hours || 12; // the hour '0' should be '12'
|
||||
|
||||
const formattedDate = `${month}-${day}-${year} at ${hours}:${minutes} ${period}`;
|
||||
return formattedDate;
|
||||
};
|
||||
|
||||
const renderSource = () => {
|
||||
const { event, actor } = auditLog;
|
||||
|
||||
if (event.type === EventType.INTEGRATION_SYNCED) {
|
||||
if (actor.type === ActorType.USER) {
|
||||
return (
|
||||
<Td>
|
||||
<p>Manually triggered by {actor.metadata.email}</p>
|
||||
</Td>
|
||||
);
|
||||
}
|
||||
|
||||
// Platform / automatic syncs
|
||||
return (
|
||||
<Td>
|
||||
<p>Automatically synced by Infisical</p>
|
||||
</Td>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Td>
|
||||
<p>{userAgentTTypeoNameMap[auditLog.userAgentType]}</p>
|
||||
<p>{auditLog.ipAddress}</p>
|
||||
</Td>
|
||||
);
|
||||
};
|
||||
type TagProps = {
|
||||
label: string;
|
||||
value?: string;
|
||||
};
|
||||
const Tag = ({ label, value }: TagProps) => {
|
||||
if (!value) return null;
|
||||
|
||||
return (
|
||||
<Tr className={`log-${auditLog.id} h-10 border-x-0 border-b border-t-0`}>
|
||||
<Td>{formatDate(auditLog.createdAt)}</Td>
|
||||
<Td>{`${eventToNameMap[auditLog.event.type]}`}</Td>
|
||||
{isOrgAuditLogs && <Td>{auditLog?.projectName ?? auditLog?.projectId ?? "N/A"}</Td>}
|
||||
{showActorColumn && renderActor(auditLog.actor)}
|
||||
{renderSource()}
|
||||
<Td className="max-w-xs break-all">{JSON.stringify(auditLog.event.metadata || {})}</Td>
|
||||
</Tr>
|
||||
<div className="flex items-center space-x-1.5">
|
||||
<div className="rounded bg-mineshaft-600 p-0.5 pl-1 font-mono">{label}:</div>
|
||||
<div>{value}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const LogsTableRow = ({ auditLog, rowNumber }: Props) => {
|
||||
const [isOpen, setIsOpen] = useToggle();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tr
|
||||
className="h-10 cursor-pointer border-x-0 border-b border-t-0 hover:bg-mineshaft-700"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => setIsOpen.toggle()}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") setIsOpen.toggle();
|
||||
}}
|
||||
isHoverable
|
||||
>
|
||||
<Td className="flex items-center gap-2 pr-0 align-top">
|
||||
<FontAwesomeIcon icon={isOpen ? faCaretDown : faCaretRight} />
|
||||
{rowNumber}
|
||||
</Td>
|
||||
<Td className="align-top">
|
||||
{format(new Date(auditLog.createdAt), "MMM do yyyy, hh:mm a")}
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex flex-wrap gap-4 text-sm">
|
||||
<Tag label="event" value={auditLog.event.type} />
|
||||
<Tag label="actor" value={auditLog.actor.type} />
|
||||
{auditLog.actor.type === ActorType.USER && (
|
||||
<Tag label="user_email" value={auditLog.actor.metadata.email} />
|
||||
)}
|
||||
{auditLog.actor.type === ActorType.IDENTITY && (
|
||||
<Tag label="identity_name" value={auditLog.actor.metadata.name} />
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
{isOpen && (
|
||||
<Tr className={`log-${auditLog.id} h-10 border-x-0 border-b border-t-0`}>
|
||||
<Td colSpan={3} className="px-3 py-2">
|
||||
<div className="thin-scrollbar my-1 max-h-96 overflow-auto whitespace-pre-wrap rounded border border-mineshaft-600 bg-bunker-800 p-2 font-mono leading-6">
|
||||
{JSON.stringify(auditLog, null, 4)}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
-1
@@ -45,7 +45,6 @@ export const UserAuditLogsSection = withPermission(
|
||||
presets={{
|
||||
actorId: orgMembership.user.id
|
||||
}}
|
||||
isOrgAuditLogs
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
-3
@@ -29,9 +29,6 @@ export const IntegrationAuditLogsSection = ({ integration }: Props) => {
|
||||
</div>
|
||||
<LogsSection
|
||||
refetchInterval={4000}
|
||||
remappedHeaders={{
|
||||
Metadata: "Sync Status"
|
||||
}}
|
||||
showFilters={false}
|
||||
presets={{
|
||||
eventMetadata: { integrationId: integration.id },
|
||||
|
||||
-3
@@ -35,9 +35,6 @@ export const SecretSyncAuditLogsSection = ({ secretSync }: Props) => {
|
||||
{subscription.auditLogs ? (
|
||||
<LogsSection
|
||||
refetchInterval={4000}
|
||||
remappedHeaders={{
|
||||
Metadata: "Sync Status"
|
||||
}}
|
||||
showFilters={false}
|
||||
presets={{
|
||||
eventMetadata: { syncId: secretSync.id },
|
||||
|
||||
Reference in New Issue
Block a user