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:
Daniel Hougaard
2024-09-21 02:54:06 +04:00
committed by GitHub
28 changed files with 288 additions and 127 deletions

View File

@@ -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()

View File

@@ -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 });
}

View File

@@ -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,

View File

@@ -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;
};

View File

@@ -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],

View File

@@ -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.",

View File

@@ -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()

View File

@@ -1,4 +1,4 @@
---
title: "Export"
openapi: "GET /api/v1/workspace/{workspaceId}/audit-logs"
openapi: "GET /api/v1/organization/audit-logs"
---

View File

@@ -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>;

View File

@@ -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;
};

View File

@@ -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

View File

@@ -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>

View File

@@ -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];

View 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 }
);

View File

@@ -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
})
}
>

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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)}

View File

@@ -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"),

View File

@@ -32,6 +32,8 @@ export const formSchema = z.object({
create: z.boolean().optional()
})
.optional(),
"audit-logs": generalPermissionSchema,
member: generalPermissionSchema,
groups: generalPermissionSchema,
role: generalPermissionSchema,

View File

@@ -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"

View File

@@ -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;

View File

@@ -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 }
);

View File

@@ -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,

View File

@@ -65,10 +65,6 @@ const SINGLE_PERMISSION_LIST = [
title: "Tags",
formName: "tags"
},
{
title: "Audit Logs",
formName: "audit-logs"
},
{
title: "IP Allowlist",
formName: "ip-allowlist"

View File

@@ -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>
);
};