Merge pull request #1721 from akhilmhdh/feat/audit-log-stream

Audit log streams
This commit is contained in:
Maidul Islam
2024-05-03 12:48:55 -04:00
committed by GitHub
45 changed files with 1322 additions and 21 deletions
@@ -0,0 +1,6 @@
export {
useCreateAuditLogStream,
useDeleteAuditLogStream,
useUpdateAuditLogStream
} from "./mutations";
export { useGetAuditLogStreamDetails, useGetAuditLogStreams } from "./queries";
@@ -0,0 +1,61 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { auditLogStreamKeys } from "./queries";
import {
TAuditLogStream,
TCreateAuditLogStreamDTO,
TDeleteAuditLogStreamDTO,
TUpdateAuditLogStreamDTO
} from "./types";
export const useCreateAuditLogStream = () => {
const queryClient = useQueryClient();
return useMutation<{ auditLogStream: TAuditLogStream }, {}, TCreateAuditLogStreamDTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.post<{ auditLogStream: TAuditLogStream }>(
"/api/v1/audit-log-streams",
dto
);
return data;
},
onSuccess: (_, { orgId }) => {
queryClient.invalidateQueries(auditLogStreamKeys.list(orgId));
}
});
};
export const useUpdateAuditLogStream = () => {
const queryClient = useQueryClient();
return useMutation<{ auditLogStream: TAuditLogStream }, {}, TUpdateAuditLogStreamDTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.patch<{ auditLogStream: TAuditLogStream }>(
`/api/v1/audit-log-streams/${dto.id}`,
dto
);
return data;
},
onSuccess: (_, { orgId }) => {
queryClient.invalidateQueries(auditLogStreamKeys.list(orgId));
}
});
};
export const useDeleteAuditLogStream = () => {
const queryClient = useQueryClient();
return useMutation<{ auditLogStream: TAuditLogStream }, {}, TDeleteAuditLogStreamDTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.delete<{ auditLogStream: TAuditLogStream }>(
`/api/v1/audit-log-streams/${dto.id}`
);
return data;
},
onSuccess: (_, { orgId }) => {
queryClient.invalidateQueries(auditLogStreamKeys.list(orgId));
}
});
};
@@ -0,0 +1,40 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TAuditLogStream } from "./types";
export const auditLogStreamKeys = {
list: (orgId: string) => ["audit-log-stream", { orgId }],
getById: (id: string) => ["audit-log-stream-details", { id }]
};
const fetchAuditLogStreams = async () => {
const { data } = await apiRequest.get<{ auditLogStreams: TAuditLogStream[] }>(
"/api/v1/audit-log-streams"
);
return data.auditLogStreams;
};
export const useGetAuditLogStreams = (orgId: string) =>
useQuery({
queryKey: auditLogStreamKeys.list(orgId),
queryFn: () => fetchAuditLogStreams(),
enabled: Boolean(orgId)
});
const fetchAuditLogStreamDetails = async (id: string) => {
const { data } = await apiRequest.get<{ auditLogStream: TAuditLogStream }>(
`/api/v1/audit-log-streams/${id}`
);
return data.auditLogStream;
};
export const useGetAuditLogStreamDetails = (id: string) =>
useQuery({
queryKey: auditLogStreamKeys.getById(id),
queryFn: () => fetchAuditLogStreamDetails(id),
enabled: Boolean(id)
});
@@ -0,0 +1,28 @@
export type LogStreamHeaders = {
key: string;
value: string;
};
export type TAuditLogStream = {
id: string;
url: string;
headers?: LogStreamHeaders[];
};
export type TCreateAuditLogStreamDTO = {
url: string;
headers?: LogStreamHeaders[];
orgId: string;
};
export type TUpdateAuditLogStreamDTO = {
id: string;
url?: string;
headers?: LogStreamHeaders[];
orgId: string;
};
export type TDeleteAuditLogStreamDTO = {
id: string;
orgId: string;
};
+1
View File
@@ -1,6 +1,7 @@
export * from "./admin";
export * from "./apiKeys";
export * from "./auditLogs";
export * from "./auditLogStreams";
export * from "./auth";
export * from "./bots";
export * from "./dynamicSecret";
@@ -5,6 +5,8 @@ export type SubscriptionPlan = {
auditLogs: boolean;
dynamicSecret: boolean;
auditLogsRetentionDays: number;
auditLogStreamLimit: number;
auditLogStreams: boolean;
customAlerts: boolean;
customRateLimits: boolean;
pitRecovery: boolean;
+9 -8
View File
@@ -1,5 +1,6 @@
import { ZodIssue } from "zod";
export type { TAuditLogStream } from "./auditLogStreams/types";
export type { GetAuthTokenAPI } from "./auth/types";
export type { IncidentContact } from "./incidentContacts/types";
export type { IntegrationAuth } from "./integrationAuth/types";
@@ -48,13 +49,13 @@ export enum ApiErrorTypes {
export type TApiErrors =
| {
error: ApiErrorTypes.ValidationError;
message: ZodIssue[];
statusCode: 403;
}
error: ApiErrorTypes.ValidationError;
message: ZodIssue[];
statusCode: 403;
}
| { error: ApiErrorTypes.ForbiddenError; message: string; statusCode: 401 }
| {
statusCode: 400;
message: string;
error: ApiErrorTypes.BadRequestError;
};
statusCode: 400;
message: string;
error: ApiErrorTypes.BadRequestError;
};
@@ -0,0 +1,206 @@
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, FormLabel, IconButton, Input, Spinner } from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useCreateAuditLogStream,
useGetAuditLogStreamDetails,
useUpdateAuditLogStream
} from "@app/hooks/api";
type Props = {
id?: string;
onClose: () => void;
};
const formSchema = z.object({
url: z.string().url().min(1),
headers: z
.object({
key: z.string(),
value: z.string()
})
.array()
.optional()
});
type TForm = z.infer<typeof formSchema>;
export const AuditLogStreamForm = ({ id = "", onClose }: Props) => {
const isEdit = Boolean(id);
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const auditLogStream = useGetAuditLogStreamDetails(id);
const createAuditLogStream = useCreateAuditLogStream();
const updateAuditLogStream = useUpdateAuditLogStream();
const {
handleSubmit,
control,
setValue,
getValues,
formState: { isSubmitting }
} = useForm<TForm>({
values: auditLogStream?.data,
defaultValues: {
headers: [{ key: "", value: "" }]
}
});
const headerFields = useFieldArray({
control,
name: "headers"
});
const handleAuditLogStreamEdit = async ({ headers, url }: TForm) => {
if (!id) return;
try {
await updateAuditLogStream.mutateAsync({
id,
orgId,
headers,
url
});
createNotification({
type: "success",
text: "Successfully updated stream"
});
onClose();
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to update stream"
});
}
};
const handleFormSubmit = async ({ headers = [], url }: TForm) => {
if (isSubmitting) return;
const sanitizedHeaders = headers.filter(({ key, value }) => Boolean(key) && Boolean(value));
const streamHeaders = sanitizedHeaders.length ? sanitizedHeaders : undefined;
if (isEdit) {
await handleAuditLogStreamEdit({ headers: streamHeaders, url });
return;
}
try {
await createAuditLogStream.mutateAsync({
orgId,
headers: streamHeaders,
url
});
createNotification({
type: "success",
text: "Successfully created stream"
});
onClose();
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to create stream"
});
}
};
if (isEdit && auditLogStream.isLoading) {
return (
<div className="flex items-center justify-center p-8">
<Spinner size="lg" />
</div>
);
}
return (
<form onSubmit={handleSubmit(handleFormSubmit)} autoComplete="off">
<div>
<Controller
control={control}
name="url"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Endpoint URL"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<Input {...field} />
</FormControl>
)}
/>
<FormLabel label="Headers" isOptional />
{headerFields.fields.map(({ id: headerFieldId }, i) => (
<div key={headerFieldId} className="flex space-x-2">
<Controller
control={control}
name={`headers.${i}.key`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="w-1/3"
>
<Input {...field} placeholder="Authorization" />
</FormControl>
)}
/>
<Controller
control={control}
name={`headers.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="flex-grow"
>
<Input
{...field}
type="password"
placeholder="Bearer <token>"
autoComplete="new-password"
/>
</FormControl>
)}
/>
<IconButton
ariaLabel="delete key"
className="h-9"
variant="outline_bg"
onClick={() => {
const header = getValues("headers");
if (header && header?.length > 1) {
headerFields.remove(i);
} else {
setValue("headers", [{ key: "", value: "" }]);
}
}}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
))}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => headerFields.append({ value: "", key: "" })}
>
Add Key
</Button>
</div>
</div>
<div className="mt-8 flex items-center">
<Button className="mr-4" type="submit" isLoading={isSubmitting}>
{isEdit ? "Save" : "Create"}
</Button>
<Button variant="plain" colorSchema="secondary" onClick={onClose}>
Cancel
</Button>
</div>
</form>
);
};
@@ -0,0 +1,197 @@
import { faPlug, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
EmptyState,
Modal,
ModalContent,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
THead,
Tr,
UpgradePlanModal
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useSubscription
} from "@app/context";
import { withPermission } from "@app/hoc";
import { usePopUp } from "@app/hooks";
import { useDeleteAuditLogStream, useGetAuditLogStreams } from "@app/hooks/api";
import { AuditLogStreamForm } from "./AuditLogStreamForm";
export const AuditLogStreamsTab = withPermission(
() => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"auditLogStreamForm",
"deleteAuditLogStream",
"upgradePlan"
] as const);
const { subscription } = useSubscription();
const { data: auditLogStreams, isLoading: isAuditLogStreamsLoading } =
useGetAuditLogStreams(orgId);
// mutation
const { mutateAsync: deleteAuditLogStream } = useDeleteAuditLogStream();
const handleAuditLogStreamDelete = async () => {
try {
const auditLogStreamId = popUp?.deleteAuditLogStream?.data as string;
await deleteAuditLogStream({
id: auditLogStreamId,
orgId
});
handlePopUpClose("deleteAuditLogStream");
createNotification({
type: "success",
text: "Successfully deleted stream"
});
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to delete stream"
});
}
};
return (
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex justify-between">
<p className="text-xl font-semibold text-mineshaft-100">Audit Log Streams</p>
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Settings}>
{(isAllowed) => (
<Button
onClick={() => {
if (subscription && !subscription?.auditLogStreams) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("auditLogStreamForm");
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={!isAllowed}
>
Create
</Button>
)}
</OrgPermissionCan>
</div>
<p className="mb-8 text-gray-400">
Send audit logs from Infisical to external logging providers via HTTP
</p>
<div>
<TableContainer>
<Table>
<THead>
<Tr>
<Td>URL</Td>
<Td className="text-right">Action</Td>
</Tr>
</THead>
<TBody>
{isAuditLogStreamsLoading && (
<TableSkeleton columns={2} innerKey="stream-loading" />
)}
{!isAuditLogStreamsLoading && auditLogStreams && auditLogStreams?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState title="No audit log streams found" icon={faPlug} />
</Td>
</Tr>
)}
{!isAuditLogStreamsLoading &&
auditLogStreams?.map(({ id, url }) => (
<Tr key={id}>
<Td className="max-w-xs overflow-hidden text-ellipsis hover:overflow-auto hover:break-all">
{url}
</Td>
<Td>
<div className="flex items-center justify-end space-x-2">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Settings}
>
{(isAllowed) => (
<Button
variant="outline_bg"
size="xs"
isDisabled={!isAllowed}
onClick={() => handlePopUpOpen("auditLogStreamForm", id)}
>
Edit
</Button>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Settings}
>
{(isAllowed) => (
<Button
variant="outline_bg"
className="border-red-800 bg-red-800 hover:border-red-700 hover:bg-red-700"
colorSchema="danger"
size="xs"
isDisabled={!isAllowed}
onClick={() => handlePopUpOpen("deleteAuditLogStream", id)}
>
Delete
</Button>
)}
</OrgPermissionCan>
</div>
</Td>
</Tr>
))}
</TBody>
</Table>
</TableContainer>
</div>
<Modal
isOpen={popUp.auditLogStreamForm.isOpen}
onOpenChange={(isModalOpen) => {
handlePopUpToggle("auditLogStreamForm", isModalOpen);
}}
>
<ModalContent
title={`${popUp?.auditLogStreamForm?.data ? "Update" : "Create"} Audit Log Stream `}
subTitle="Continuously stream logs from Infisical to third-party logging providers."
>
<AuditLogStreamForm
id={popUp?.auditLogStreamForm?.data as string}
onClose={() => handlePopUpToggle("auditLogStreamForm")}
/>
</ModalContent>
</Modal>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can add audit log streams if you switch to Infisical's Enterprise plan."
/>
<DeleteActionModal
isOpen={popUp.deleteAuditLogStream.isOpen}
deleteKey="delete"
title="Are you sure you want to remove this stream?"
onChange={(isOpen) => handlePopUpToggle("deleteAuditLogStream", isOpen)}
onClose={() => handlePopUpClose("deleteAuditLogStream")}
onDeleteApproved={handleAuditLogStreamDelete}
/>
</div>
);
},
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.Settings }
);
@@ -0,0 +1 @@
export { AuditLogStreamsTab } from "./AuditLogStreamTab";
@@ -1,12 +1,14 @@
import { Fragment } from "react";
import { Tab } from "@headlessui/react";
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
import { OrgAuthTab } from "../OrgAuthTab";
import { OrgGeneralTab } from "../OrgGeneralTab";
const tabs = [
{ name: "General", key: "tab-org-general" },
{ name: "Security", key: "tab-org-security" }
{ name: "Security", key: "tab-org-security" },
{ name: "Audit Log Streams", key: "tag-audit-log-streams" }
];
export const OrgTabGroup = () => {
return (
@@ -17,9 +19,8 @@ export const OrgTabGroup = () => {
{({ selected }) => (
<button
type="button"
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${
selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
>
{tab.name}
</button>
@@ -34,6 +35,9 @@ export const OrgTabGroup = () => {
<Tab.Panel>
<OrgAuthTab />
</Tab.Panel>
<Tab.Panel>
<AuditLogStreamsTab />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
);
@@ -7,7 +7,7 @@ import { WebhooksTab } from "./components/WebhooksTab";
const tabs = [
{ name: "General", key: "tab-project-general" },
{ name: "Webhooks", key: "tab-project-webhooks" }
{ name: "Webhooks", key: "tab-project-webhooks" },
];
export const ProjectSettingsPage = () => {
@@ -25,9 +25,8 @@ export const ProjectSettingsPage = () => {
{({ selected }) => (
<button
type="button"
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${
selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
className={`w-30 mx-2 mr-4 py-2 text-sm font-medium outline-none ${selected ? "border-b border-white text-white" : "text-mineshaft-400"
}`}
>
{tab.name}
</button>