mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge pull request #2448 from Infisical/daniel/org-level-audit-logs
feat(audit-logs): moved audit logs to organization-level
This commit is contained in:
@@ -87,6 +87,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Daniel: This endpoint is no longer is use.
|
||||
* We are keeping it for now because it has been exposed in our public api docs for a while, so by removing it we are likely to break users workflows.
|
||||
*
|
||||
* Please refer to the new endpoint, GET /api/v1/organization/audit-logs, for the same (and more) functionality.
|
||||
*/
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/audit-logs",
|
||||
@@ -101,7 +107,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.workspaceId)
|
||||
workspaceId: z.string().trim().describe(AUDIT_LOGS.EXPORT.projectId)
|
||||
}),
|
||||
querystring: z.object({
|
||||
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType),
|
||||
@@ -122,10 +128,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
.merge(
|
||||
z.object({
|
||||
project: z.object({
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
}),
|
||||
project: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
})
|
||||
.optional(),
|
||||
event: z.object({
|
||||
type: z.string(),
|
||||
metadata: z.any()
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Knex } from "knex";
|
||||
import { TDbClient } from "@app/db";
|
||||
import { AuditLogsSchema, TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, stripUndefinedInWhere } from "@app/lib/knex";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueName } from "@app/queue";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
@@ -48,47 +48,61 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
},
|
||||
tx?: Knex
|
||||
) => {
|
||||
if (!orgId && !projectId) {
|
||||
throw new Error("Either orgId or projectId must be provided");
|
||||
}
|
||||
|
||||
try {
|
||||
// Find statements
|
||||
const sqlQuery = (tx || db.replicaNode())(TableName.AuditLog)
|
||||
.where(
|
||||
stripUndefinedInWhere({
|
||||
projectId,
|
||||
[`${TableName.AuditLog}.orgId`]: orgId,
|
||||
userAgentType
|
||||
})
|
||||
)
|
||||
|
||||
.leftJoin(TableName.Project, `${TableName.AuditLog}.projectId`, `${TableName.Project}.id`)
|
||||
// eslint-disable-next-line func-names
|
||||
.where(function () {
|
||||
if (orgId) {
|
||||
void this.where(`${TableName.Project}.orgId`, orgId).orWhere(`${TableName.AuditLog}.orgId`, orgId);
|
||||
} else if (projectId) {
|
||||
void this.where(`${TableName.AuditLog}.projectId`, projectId);
|
||||
}
|
||||
});
|
||||
|
||||
if (userAgentType) {
|
||||
void sqlQuery.where("userAgentType", userAgentType);
|
||||
}
|
||||
|
||||
// Select statements
|
||||
void sqlQuery
|
||||
.select(selectAllTableCols(TableName.AuditLog))
|
||||
|
||||
.select(
|
||||
db.ref("name").withSchema(TableName.Project).as("projectName"),
|
||||
db.ref("slug").withSchema(TableName.Project).as("projectSlug")
|
||||
)
|
||||
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
.orderBy(`${TableName.AuditLog}.createdAt`, "desc");
|
||||
|
||||
// Special case: Filter by actor ID
|
||||
if (actorId) {
|
||||
void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actorId]);
|
||||
}
|
||||
|
||||
// Special case: Filter by key/value pairs in eventMetadata field
|
||||
if (eventMetadata && Object.keys(eventMetadata).length) {
|
||||
Object.entries(eventMetadata).forEach(([key, value]) => {
|
||||
void sqlQuery.whereRaw(`"eventMetadata"->>'${key}' = ?`, [value]);
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by actor type
|
||||
if (actorType) {
|
||||
void sqlQuery.where("actor", actorType);
|
||||
}
|
||||
|
||||
// Filter by event types
|
||||
if (eventType?.length) {
|
||||
void sqlQuery.whereIn("eventType", eventType);
|
||||
}
|
||||
|
||||
// Filter by date range
|
||||
if (startDate) {
|
||||
void sqlQuery.where(`${TableName.AuditLog}.createdAt`, ">=", startDate);
|
||||
}
|
||||
@@ -97,13 +111,21 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
const docs = await sqlQuery;
|
||||
|
||||
return docs.map((doc) => ({
|
||||
...AuditLogsSchema.parse(doc),
|
||||
project: {
|
||||
name: doc.projectName,
|
||||
slug: doc.projectSlug
|
||||
}
|
||||
}));
|
||||
return docs.map((doc) => {
|
||||
// Our type system refuses to acknowledge that the project name and slug are present in the doc, due to the disjointed query structure above.
|
||||
// This is a quick and dirty way to get around the types.
|
||||
const projectDoc = doc as unknown as { projectName: string; projectSlug: string };
|
||||
|
||||
return {
|
||||
...AuditLogsSchema.parse(doc),
|
||||
...(projectDoc?.projectSlug && {
|
||||
project: {
|
||||
name: projectDoc.projectName,
|
||||
slug: projectDoc.projectSlug
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error });
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ export const auditLogServiceFactory = ({
|
||||
permissionService
|
||||
}: TAuditLogServiceFactoryDep) => {
|
||||
const listAuditLogs = async ({ actorAuthMethod, actorId, actorOrgId, actor, filter }: TListProjectAuditLogDTO) => {
|
||||
// Filter logs for specific project
|
||||
if (filter.projectId) {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@@ -34,6 +35,7 @@ export const auditLogServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
||||
} else {
|
||||
// Organization-wide logs
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
@@ -44,13 +46,12 @@ export const auditLogServiceFactory = ({
|
||||
|
||||
/**
|
||||
* NOTE (dangtony98): Update this to organization-level audit log permission check once audit logs are moved
|
||||
* to the organization level
|
||||
* to the organization level ✅
|
||||
*/
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||
}
|
||||
|
||||
// If project ID is not provided, then we need to return all the audit logs for the organization itself.
|
||||
|
||||
const auditLogs = await auditLogDAL.find({
|
||||
startDate: filter.startDate,
|
||||
endDate: filter.endDate,
|
||||
|
||||
@@ -25,7 +25,8 @@ export enum OrgPermissionSubjects {
|
||||
SecretScanning = "secret-scanning",
|
||||
Identity = "identity",
|
||||
Kms = "kms",
|
||||
AdminConsole = "organization-admin-console"
|
||||
AdminConsole = "organization-admin-console",
|
||||
AuditLogs = "audit-logs"
|
||||
}
|
||||
|
||||
export type OrgPermissionSet =
|
||||
@@ -43,6 +44,7 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||
|
||||
const buildAdminPermission = () => {
|
||||
@@ -111,6 +113,11 @@ const buildAdminPermission = () => {
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.AuditLogs);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.AuditLogs);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.AuditLogs);
|
||||
|
||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||
|
||||
return rules;
|
||||
@@ -140,6 +147,8 @@ const buildMemberPermission = () => {
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
|
||||
@@ -145,6 +145,8 @@ export const fullProjectPermissionSet: [ProjectPermissionActions, ProjectPermiss
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.Tags],
|
||||
[ProjectPermissionActions.Delete, ProjectPermissionSub.Tags],
|
||||
|
||||
// TODO(Daniel): Remove the audit logs permissions from project-level permissions.
|
||||
// TODO: We haven't done this yet because it might break existing roles, since those roles will become "invalid" since the audit log permission defined on those roles, no longer exist in the project-level defined permissions.
|
||||
[ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs],
|
||||
[ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs],
|
||||
[ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs],
|
||||
|
||||
@@ -731,9 +731,12 @@ export const DASHBOARD = {
|
||||
|
||||
export const AUDIT_LOGS = {
|
||||
EXPORT: {
|
||||
workspaceId: "The ID of the project to export audit logs from.",
|
||||
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.",
|
||||
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.",
|
||||
startDate: "The date to start the export from.",
|
||||
endDate: "The date to end the export at.",
|
||||
offset: "The offset to start from. If you enter 10, it will start from the 10th audit log.",
|
||||
|
||||
@@ -74,7 +74,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
schema: {
|
||||
description: "Get all audit logs for an organization",
|
||||
querystring: z.object({
|
||||
projectId: z.string().optional(),
|
||||
projectId: z.string().optional().describe(AUDIT_LOGS.EXPORT.projectId),
|
||||
actorType: z.nativeEnum(ActorType).optional(),
|
||||
// eventType is split with , for multiple values, we need to transform it to array
|
||||
eventType: z
|
||||
@@ -102,7 +102,8 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
{} as Record<string, string>
|
||||
);
|
||||
}),
|
||||
})
|
||||
.describe(AUDIT_LOGS.EXPORT.eventMetadata),
|
||||
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
|
||||
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
|
||||
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
|
||||
@@ -120,10 +121,12 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
.merge(
|
||||
z.object({
|
||||
project: z.object({
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
}),
|
||||
project: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
})
|
||||
.optional(),
|
||||
event: z.object({
|
||||
type: z.string(),
|
||||
metadata: z.any()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
---
|
||||
title: "Export"
|
||||
openapi: "GET /api/v1/workspace/{workspaceId}/audit-logs"
|
||||
openapi: "GET /api/v1/organization/audit-logs"
|
||||
---
|
||||
|
||||
@@ -21,7 +21,8 @@ export enum OrgPermissionSubjects {
|
||||
SecretScanning = "secret-scanning",
|
||||
Identity = "identity",
|
||||
Kms = "kms",
|
||||
AdminConsole = "organization-admin-console"
|
||||
AdminConsole = "organization-admin-console",
|
||||
AuditLogs = "audit-logs"
|
||||
}
|
||||
|
||||
export enum OrgPermissionAdminConsoleAction {
|
||||
@@ -43,6 +44,7 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole];
|
||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs];
|
||||
|
||||
export type TOrgPermission = MongoAbility<OrgPermissionSet>;
|
||||
|
||||
@@ -8,6 +8,7 @@ export type TGetAuditLogsFilter = {
|
||||
userAgentType?: UserAgentType;
|
||||
eventMetadata?: Record<string, string>;
|
||||
actorType?: ActorType;
|
||||
projectId?: string;
|
||||
actorId?: string; // user ID format
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
@@ -885,7 +886,7 @@ export type AuditLog = {
|
||||
userAgentType: UserAgentType;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
project: {
|
||||
project?: {
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
@@ -675,18 +675,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/project/${currentWorkspace?.id}/audit-logs`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/project/${currentWorkspace?.id}/audit-logs`
|
||||
}
|
||||
icon="system-outline-168-view-headline"
|
||||
>
|
||||
Audit Logs
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/project/${currentWorkspace?.id}/settings`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
@@ -755,6 +743,16 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
<Link href={`/org/${currentOrg?.id}/audit-logs`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={router.asPath === `/org/${currentOrg?.id}/audit-logs`}
|
||||
icon="system-outline-168-view-headline"
|
||||
>
|
||||
Audit Logs
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/org/${currentOrg?.id}/settings`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { AuditLogsPage } from "@app/views/Project/AuditLogsPage";
|
||||
import { AuditLogsPage } from "@app/views/Org/AuditLogsPage";
|
||||
|
||||
const Logs = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="h-full bg-bunker-800">
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("settings.project.title") })}</title>
|
||||
<title>Infisical | Audit Logs</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
<meta property="og:image" content="/images/message.png" />
|
||||
</Head>
|
||||
@@ -4,7 +4,7 @@ import { EmptyState } from "@app/components/v2";
|
||||
import { useSubscription } from "@app/context";
|
||||
import { EventType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
|
||||
import { LogsSection } from "@app/views/Project/AuditLogsPage/components";
|
||||
import { LogsSection } from "@app/views/Org/AuditLogsPage/components";
|
||||
|
||||
// Add more events if needed
|
||||
const INTEGRATION_EVENTS = [EventType.INTEGRATION_SYNCED];
|
||||
|
||||
21
frontend/src/views/Org/AuditLogsPage/AuditLogsPage.tsx
Normal file
21
frontend/src/views/Org/AuditLogsPage/AuditLogsPage.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
|
||||
import { LogsSection } from "./components";
|
||||
|
||||
export const AuditLogsPage = withPermission(
|
||||
() => {
|
||||
return (
|
||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl px-6">
|
||||
<div className="bg-bunker-800 py-6">
|
||||
<p className="text-3xl font-semibold text-gray-200">Audit Logs</p>
|
||||
<div />
|
||||
</div>
|
||||
<LogsSection filterClassName="static p-2" showFilters isOrgAuditLogs />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.AuditLogs }
|
||||
);
|
||||
@@ -40,16 +40,24 @@ type Props = {
|
||||
eventType?: EventType[];
|
||||
};
|
||||
className?: string;
|
||||
isOrgAuditLogs?: boolean;
|
||||
control: Control<AuditLogFilterFormData>;
|
||||
reset: UseFormReset<AuditLogFilterFormData>;
|
||||
watch: UseFormWatch<AuditLogFilterFormData>;
|
||||
};
|
||||
|
||||
export const LogsFilter = ({ presets, className, control, reset, watch }: Props) => {
|
||||
export const LogsFilter = ({
|
||||
presets,
|
||||
isOrgAuditLogs,
|
||||
className,
|
||||
control,
|
||||
reset,
|
||||
watch
|
||||
}: Props) => {
|
||||
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
||||
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { currentWorkspace, workspaces } = useWorkspace();
|
||||
const { data, isLoading } = useGetAuditLogActorFilterOpts(currentWorkspace?.id ?? "");
|
||||
|
||||
const renderActorSelectItem = (actor: Actor) => {
|
||||
@@ -112,7 +120,7 @@ export const LogsFilter = ({ presets, className, control, reset, watch }: Props)
|
||||
? eventTypes.find((eventType) => eventType.value === selectedEventTypes[0])
|
||||
?.label
|
||||
: selectedEventTypes?.length === 0
|
||||
? "Select event types"
|
||||
? "All events"
|
||||
: `${selectedEventTypes?.length} events selected`}
|
||||
<FontAwesomeIcon icon={faChevronDown} className="ml-2 text-xs" />
|
||||
</div>
|
||||
@@ -191,7 +199,7 @@ export const LogsFilter = ({ presets, className, control, reset, watch }: Props)
|
||||
<Controller
|
||||
control={control}
|
||||
name="userAgentType"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Source"
|
||||
errorText={error?.message}
|
||||
@@ -199,13 +207,22 @@ export const LogsFilter = ({ presets, className, control, reset, watch }: Props)
|
||||
className="w-40"
|
||||
>
|
||||
<Select
|
||||
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
|
||||
value={value === undefined ? "all" : value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full border border-mineshaft-500 bg-mineshaft-700 text-mineshaft-100"
|
||||
onValueChange={(e) => {
|
||||
if (e === "all") onChange(undefined);
|
||||
else onChange(e);
|
||||
}}
|
||||
className={twMerge(
|
||||
"w-full border border-mineshaft-500 bg-mineshaft-700 text-mineshaft-100",
|
||||
value === undefined && "text-mineshaft-400"
|
||||
)}
|
||||
>
|
||||
{userAgentTypes.map(({ label, value }) => (
|
||||
<SelectItem value={String(value || "")} key={label}>
|
||||
<SelectItem value="all" key="all">
|
||||
All sources
|
||||
</SelectItem>
|
||||
{userAgentTypes.map(({ label, value: userAgent }) => (
|
||||
<SelectItem value={userAgent} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
@@ -213,6 +230,43 @@ export const LogsFilter = ({ presets, className, control, reset, watch }: Props)
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
{isOrgAuditLogs && workspaces.length > 0 && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="projectId"
|
||||
render={({ field: { onChange, value, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="w-40"
|
||||
>
|
||||
<Select
|
||||
value={value === undefined ? "all" : value}
|
||||
{...field}
|
||||
onValueChange={(e) => {
|
||||
if (e === "all") onChange(undefined);
|
||||
else onChange(e);
|
||||
}}
|
||||
className={twMerge(
|
||||
"w-full border border-mineshaft-500 bg-mineshaft-700 text-mineshaft-100",
|
||||
value === undefined && "text-mineshaft-400"
|
||||
)}
|
||||
>
|
||||
<SelectItem value="all" key="all">
|
||||
All projects
|
||||
</SelectItem>
|
||||
{workspaces.map((project) => (
|
||||
<SelectItem value={String(project.id || "")} key={project.id}>
|
||||
{project.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
name="startDate"
|
||||
control={control}
|
||||
@@ -272,7 +326,8 @@ export const LogsFilter = ({ presets, className, control, reset, watch }: Props)
|
||||
actor: presets?.actorId,
|
||||
userAgentType: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined
|
||||
endDate: undefined,
|
||||
projectId: undefined
|
||||
})
|
||||
}
|
||||
>
|
||||
@@ -47,6 +47,7 @@ export const LogsSection = ({
|
||||
const { control, reset, watch } = useForm<AuditLogFilterFormData>({
|
||||
resolver: yupResolver(auditLogFilterFormSchema),
|
||||
defaultValues: {
|
||||
projectId: undefined,
|
||||
actor: presets?.actorId,
|
||||
eventType: presets?.eventType || [],
|
||||
page: 1,
|
||||
@@ -65,6 +66,7 @@ export const LogsSection = ({
|
||||
const eventType = watch("eventType") as EventType[] | undefined;
|
||||
const userAgentType = watch("userAgentType") as UserAgentType | undefined;
|
||||
const actor = watch("actor");
|
||||
const projectId = watch("projectId");
|
||||
|
||||
const startDate = watch("startDate");
|
||||
const endDate = watch("endDate");
|
||||
@@ -73,6 +75,7 @@ export const LogsSection = ({
|
||||
<div>
|
||||
{showFilters && (
|
||||
<LogsFilter
|
||||
isOrgAuditLogs
|
||||
className={filterClassName}
|
||||
presets={presets}
|
||||
control={control}
|
||||
@@ -87,6 +90,7 @@ export const LogsSection = ({
|
||||
showActorColumn={!!showActorColumn && !isOrgAuditLogs}
|
||||
filter={{
|
||||
eventMetadata: presets?.eventMetadata,
|
||||
projectId,
|
||||
actorType: presets?.actorType,
|
||||
limit: 15,
|
||||
eventType,
|
||||
@@ -41,12 +41,23 @@ export const LogsTable = ({
|
||||
}: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
// Determine the project ID for filtering
|
||||
const filterProjectId =
|
||||
// Use the projectId from the filter if it exists
|
||||
filter?.projectId ??
|
||||
// Otherwise, if we're not looking at org-wide audit logs
|
||||
(!isOrgAuditLogs
|
||||
? // Use the current workspace ID (or an empty string if that's null)
|
||||
currentWorkspace?.id ?? ""
|
||||
: // For org-wide audit logs, use null (no specific project filter)
|
||||
null);
|
||||
|
||||
const { data, isLoading, isFetchingNextPage, hasNextPage, fetchNextPage } = useGetAuditLogs(
|
||||
{
|
||||
...filter,
|
||||
limit: AUDIT_LOG_LIMIT
|
||||
},
|
||||
!isOrgAuditLogs ? currentWorkspace?.id ?? "" : null,
|
||||
filterProjectId,
|
||||
{
|
||||
refetchInterval
|
||||
}
|
||||
@@ -39,6 +39,8 @@ export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Prop
|
||||
};
|
||||
|
||||
const renderMetadata = (event: Event) => {
|
||||
const metadataKeys = Object.keys(event.metadata);
|
||||
|
||||
switch (event.type) {
|
||||
case EventType.GET_SECRETS:
|
||||
return (
|
||||
@@ -476,7 +478,47 @@ export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Prop
|
||||
</Tooltip>
|
||||
</Td>
|
||||
);
|
||||
|
||||
case EventType.GET_WORKSPACE_KEY:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Key ID: ${event.metadata.keyId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
|
||||
case EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH:
|
||||
case EventType.ADD_IDENTITY_UNIVERSAL_AUTH:
|
||||
case EventType.UPDATE_IDENTITY_UNIVERSAL_AUTH:
|
||||
case EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Identity ID: ${event.metadata.identityId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
|
||||
case EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET:
|
||||
case EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET:
|
||||
return (
|
||||
<Td>
|
||||
<p>{`Identity ID: ${event.metadata.identityId}`}</p>
|
||||
<p>{`Client Secret ID: ${event.metadata.clientSecretId}`}</p>
|
||||
</Td>
|
||||
);
|
||||
|
||||
// ? If for some reason non the above events are matched, we will display the first 3 metadata items in the metadata object.
|
||||
default:
|
||||
if (metadataKeys.length) {
|
||||
const maxMetadataLength = metadataKeys.length > 3 ? 3 : metadataKeys.length;
|
||||
return (
|
||||
<Td>
|
||||
{Object.entries(event.metadata)
|
||||
.slice(0, maxMetadataLength)
|
||||
.map(([key, value]) => {
|
||||
return <p key={`audit-log-metadata-${key}`}>{`${key}: ${value}`}</p>;
|
||||
})}
|
||||
</Td>
|
||||
);
|
||||
}
|
||||
return <Td />;
|
||||
}
|
||||
};
|
||||
@@ -531,7 +573,7 @@ export const LogsTableRow = ({ auditLog, isOrgAuditLogs, showActorColumn }: Prop
|
||||
<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.project.name}</Td>}
|
||||
{isOrgAuditLogs && <Td>{auditLog?.project?.name ?? "N/A"}</Td>}
|
||||
{showActorColumn && renderActor(auditLog.actor)}
|
||||
{renderSource()}
|
||||
{renderMetadata(auditLog.event)}
|
||||
@@ -5,6 +5,7 @@ import { EventType, UserAgentType } from "@app/hooks/api/auditLogs/enums";
|
||||
export const auditLogFilterFormSchema = yup
|
||||
.object({
|
||||
eventMetadata: yup.object({}).optional(),
|
||||
projectId: yup.string().optional(),
|
||||
eventType: yup.array(yup.string().oneOf(Object.values(EventType), "Invalid event type")),
|
||||
actor: yup.string(),
|
||||
userAgentType: yup.string().oneOf(Object.values(UserAgentType), "Invalid user agent type"),
|
||||
@@ -32,6 +32,8 @@ export const formSchema = z.object({
|
||||
create: z.boolean().optional()
|
||||
})
|
||||
.optional(),
|
||||
|
||||
"audit-logs": generalPermissionSchema,
|
||||
member: generalPermissionSchema,
|
||||
groups: generalPermissionSchema,
|
||||
role: generalPermissionSchema,
|
||||
|
||||
@@ -41,6 +41,10 @@ const SIMPLE_PERMISSION_OPTIONS = [
|
||||
title: "Incident Contacts",
|
||||
formName: "incident-contact"
|
||||
},
|
||||
{
|
||||
title: "Audit Logs",
|
||||
formName: "audit-logs"
|
||||
},
|
||||
{
|
||||
title: "Organization Profile",
|
||||
formName: "settings"
|
||||
|
||||
@@ -7,7 +7,7 @@ import { EmptyState, IconButton, Tooltip } from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
|
||||
import { withPermission } from "@app/hoc";
|
||||
import { OrgUser } from "@app/hooks/api/types";
|
||||
import { LogsSection } from "@app/views/Project/AuditLogsPage/components";
|
||||
import { LogsSection } from "@app/views/Org/AuditLogsPage/components";
|
||||
|
||||
type Props = {
|
||||
orgMembership: OrgUser;
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { withProjectPermission } from "@app/hoc";
|
||||
|
||||
import { LogsSection } from "./components";
|
||||
|
||||
export const AuditLogsPage = withProjectPermission(
|
||||
() => {
|
||||
return (
|
||||
<div className="flex h-full w-full justify-center bg-bunker-800 text-white">
|
||||
<div className="w-full max-w-7xl px-6">
|
||||
<div className="sticky top-0 z-10 bg-bunker-800 py-6">
|
||||
<p className="text-3xl font-semibold text-gray-200">Audit Logs</p>
|
||||
<div />
|
||||
</div>
|
||||
<LogsSection showFilters />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.AuditLogs }
|
||||
);
|
||||
@@ -47,7 +47,6 @@ export const formSchema = z.object({
|
||||
settings: generalPermissionSchema,
|
||||
environments: generalPermissionSchema,
|
||||
tags: generalPermissionSchema,
|
||||
"audit-logs": generalPermissionSchema,
|
||||
"ip-allowlist": generalPermissionSchema,
|
||||
"certificate-authorities": generalPermissionSchema,
|
||||
certificates: generalPermissionSchema,
|
||||
|
||||
@@ -65,10 +65,6 @@ const SINGLE_PERMISSION_LIST = [
|
||||
title: "Tags",
|
||||
formName: "tags"
|
||||
},
|
||||
{
|
||||
title: "Audit Logs",
|
||||
formName: "audit-logs"
|
||||
},
|
||||
{
|
||||
title: "IP Allowlist",
|
||||
formName: "ip-allowlist"
|
||||
|
||||
@@ -5,39 +5,40 @@ import { useBackfillSecretReference } from "@app/hooks/api";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
|
||||
export const BackfillSecretReferenceSecretion = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { membership } = useProjectPermission();
|
||||
const backfillSecretReferences = useBackfillSecretReference();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { membership } = useProjectPermission();
|
||||
const backfillSecretReferences = useBackfillSecretReference();
|
||||
|
||||
if (!currentWorkspace) return null;
|
||||
if (!currentWorkspace) return null;
|
||||
|
||||
const handleBackfill = async () => {
|
||||
if (backfillSecretReferences.isLoading) return;
|
||||
try {
|
||||
await backfillSecretReferences.mutateAsync({ projectId: currentWorkspace.id || "" });
|
||||
createNotification({ text: "Successfully re-indexed secret references", type: "success" });
|
||||
} catch {
|
||||
createNotification({ text: "Failed to re-index secret references", type: "error" });
|
||||
}
|
||||
};
|
||||
const handleBackfill = async () => {
|
||||
if (backfillSecretReferences.isLoading) return;
|
||||
try {
|
||||
await backfillSecretReferences.mutateAsync({ projectId: currentWorkspace.id || "" });
|
||||
createNotification({ text: "Successfully re-indexed secret references", type: "success" });
|
||||
} catch {
|
||||
createNotification({ text: "Failed to re-index secret references", type: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
const isAdmin = membership.roles.includes(ProjectMembershipRole.Admin);
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<p className="text-xl font-semibold">Index Secret References</p>
|
||||
</div>
|
||||
<p className="mb-4 mt-2 max-w-2xl text-sm text-gray-400">
|
||||
This will index all secret references, enabling integrations to be triggered when their values change going forward.
|
||||
</p>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
isLoading={backfillSecretReferences.isLoading}
|
||||
onClick={handleBackfill}
|
||||
isDisabled={!isAdmin}
|
||||
>
|
||||
Index Secret References
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
const isAdmin = membership.roles.includes(ProjectMembershipRole.Admin);
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<p className="text-xl font-semibold">Index Secret References</p>
|
||||
</div>
|
||||
<p className="mb-4 mt-2 max-w-2xl text-sm text-gray-400">
|
||||
This will index all secret references, enabling integrations to be triggered when their
|
||||
values change going forward. This happens automatically when secrets are created or updated.
|
||||
</p>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
isLoading={backfillSecretReferences.isLoading}
|
||||
onClick={handleBackfill}
|
||||
isDisabled={!isAdmin}
|
||||
>
|
||||
Index Secret References
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user