mirror of
https://github.com/Infisical/infisical.git
synced 2026-05-02 03:02:03 -04:00
Merge pull request #2020 from Infisical/feat/allow-audit-log-retention-to-be-configurable
feat: enabled customization of project audit logs retention period
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.Project, "auditLogsRetentionDays"))) {
|
||||
await knex.schema.alterTable(TableName.Project, (tb) => {
|
||||
tb.integer("auditLogsRetentionDays").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.Project, "auditLogsRetentionDays")) {
|
||||
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||
t.dropColumn("auditLogsRetentionDays");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,8 @@ export const ProjectsSchema = z.object({
|
||||
version: z.number().default(1),
|
||||
upgradeStatus: z.string().nullable().optional(),
|
||||
pitVersionLimit: z.number().default(10),
|
||||
kmsCertificateKeyId: z.string().uuid().nullable().optional()
|
||||
kmsCertificateKeyId: z.string().uuid().nullable().optional(),
|
||||
auditLogsRetentionDays: z.number().nullable().optional()
|
||||
});
|
||||
|
||||
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||
|
||||
@@ -45,18 +45,29 @@ export const auditLogQueueServiceFactory = ({
|
||||
const { actor, event, ipAddress, projectId, userAgent, userAgentType } = job.data;
|
||||
let { orgId } = job.data;
|
||||
const MS_IN_DAY = 24 * 60 * 60 * 1000;
|
||||
let project;
|
||||
|
||||
if (!orgId) {
|
||||
// it will never be undefined for both org and project id
|
||||
// TODO(akhilmhdh): use caching here in dal to avoid db calls
|
||||
const project = await projectDAL.findById(projectId as string);
|
||||
project = await projectDAL.findById(projectId as string);
|
||||
orgId = project.orgId;
|
||||
}
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
const ttl = plan.auditLogsRetentionDays * MS_IN_DAY;
|
||||
// skip inserting if audit log retention is 0 meaning its not supported
|
||||
if (ttl === 0) return;
|
||||
if (plan.auditLogsRetentionDays === 0) {
|
||||
// skip inserting if audit log retention is 0 meaning its not supported
|
||||
return;
|
||||
}
|
||||
|
||||
// For project actions, set TTL to project-level audit log retention config
|
||||
// This condition ensures that the plan's audit log retention days cannot be bypassed
|
||||
const ttlInDays =
|
||||
project?.auditLogsRetentionDays && project.auditLogsRetentionDays < plan.auditLogsRetentionDays
|
||||
? project.auditLogsRetentionDays
|
||||
: plan.auditLogsRetentionDays;
|
||||
|
||||
const ttl = ttlInDays * MS_IN_DAY;
|
||||
|
||||
const auditLog = await auditLogDAL.create({
|
||||
actor: actor.type,
|
||||
|
||||
@@ -372,6 +372,44 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PUT",
|
||||
url: "/:workspaceSlug/audit-logs-retention",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceSlug: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
auditLogsRetentionDays: z.number().min(0)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string(),
|
||||
workspace: ProjectsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const workspace = await server.services.project.updateAuditLogsRetention({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
workspaceSlug: req.params.workspaceSlug,
|
||||
auditLogsRetentionDays: req.body.auditLogsRetentionDays
|
||||
});
|
||||
|
||||
return {
|
||||
message: "Successfully updated project's audit logs retention period",
|
||||
workspace
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:workspaceId/integrations",
|
||||
|
||||
@@ -11,7 +11,7 @@ import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { createSecretBlindIndex } from "@app/lib/crypto";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
TListProjectCasDTO,
|
||||
TListProjectCertsDTO,
|
||||
TToggleProjectAutoCapitalizationDTO,
|
||||
TUpdateAuditLogsRetentionDTO,
|
||||
TUpdateProjectDTO,
|
||||
TUpdateProjectNameDTO,
|
||||
TUpdateProjectVersionLimitDTO,
|
||||
@@ -446,6 +447,43 @@ export const projectServiceFactory = ({
|
||||
return projectDAL.updateById(project.id, { pitVersionLimit });
|
||||
};
|
||||
|
||||
const updateAuditLogsRetention = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
auditLogsRetentionDays,
|
||||
workspaceSlug
|
||||
}: TUpdateAuditLogsRetentionDTO) => {
|
||||
const project = await projectDAL.findProjectBySlug(workspaceSlug, actorOrgId);
|
||||
if (!project) {
|
||||
throw new NotFoundError({
|
||||
message: "Project not found."
|
||||
});
|
||||
}
|
||||
|
||||
const { hasRole } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
project.id,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
if (!hasRole(ProjectMembershipRole.Admin)) {
|
||||
throw new BadRequestError({ message: "Only admins are allowed to take this action" });
|
||||
}
|
||||
|
||||
const plan = await licenseService.getPlan(project.orgId);
|
||||
if (!plan.auditLogs || auditLogsRetentionDays > plan.auditLogsRetentionDays) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to update audit logs retention due to plan limit reached. Upgrade plan to increase."
|
||||
});
|
||||
}
|
||||
|
||||
return projectDAL.updateById(project.id, { auditLogsRetentionDays });
|
||||
};
|
||||
|
||||
const updateName = async ({
|
||||
projectId,
|
||||
actor,
|
||||
@@ -621,6 +659,7 @@ export const projectServiceFactory = ({
|
||||
upgradeProject,
|
||||
listProjectCas,
|
||||
listProjectCertificates,
|
||||
updateVersionLimit
|
||||
updateVersionLimit,
|
||||
updateAuditLogsRetention
|
||||
};
|
||||
};
|
||||
|
||||
@@ -49,6 +49,11 @@ export type TUpdateProjectVersionLimitDTO = {
|
||||
workspaceSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateAuditLogsRetentionDTO = {
|
||||
auditLogsRetentionDays: number;
|
||||
workspaceSlug: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateProjectNameDTO = {
|
||||
name: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
ToggleAutoCapitalizationDTO,
|
||||
TUpdateWorkspaceIdentityRoleDTO,
|
||||
TUpdateWorkspaceUserRoleDTO,
|
||||
UpdateAuditLogsRetentionDTO,
|
||||
UpdateEnvironmentDTO,
|
||||
UpdatePitVersionLimitDTO,
|
||||
Workspace
|
||||
@@ -284,6 +285,21 @@ export const useUpdateWorkspaceVersionLimit = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateWorkspaceAuditLogsRetention = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, UpdateAuditLogsRetentionDTO>({
|
||||
mutationFn: ({ projectSlug, auditLogsRetentionDays }) => {
|
||||
return apiRequest.put(`/api/v1/workspace/${projectSlug}/audit-logs-retention`, {
|
||||
auditLogsRetentionDays
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteWorkspace = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export type Workspace = {
|
||||
autoCapitalization: boolean;
|
||||
environments: WorkspaceEnv[];
|
||||
pitVersionLimit: number;
|
||||
auditLogsRetentionDays: number;
|
||||
slug: string;
|
||||
};
|
||||
|
||||
@@ -51,6 +52,7 @@ export type CreateWorkspaceDTO = {
|
||||
|
||||
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
|
||||
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
|
||||
export type UpdateAuditLogsRetentionDTO = { projectSlug: string; auditLogsRetentionDays: number };
|
||||
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };
|
||||
|
||||
export type DeleteWorkspaceDTO = { workspaceID: string };
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button, FormControl, Input, UpgradePlanModal } from "@app/components/v2";
|
||||
import { useProjectPermission, useSubscription, useWorkspace } from "@app/context";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||
import { useUpdateWorkspaceAuditLogsRetention } from "@app/hooks/api/workspace/queries";
|
||||
|
||||
const formSchema = z.object({
|
||||
auditLogsRetentionDays: z.coerce.number().min(0)
|
||||
});
|
||||
|
||||
type TForm = z.infer<typeof formSchema>;
|
||||
|
||||
export const AuditLogsRetentionSection = () => {
|
||||
const { mutateAsync: updateAuditLogsRetention } = useUpdateWorkspaceAuditLogsRetention();
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { membership } = useProjectPermission();
|
||||
const { subscription } = useSubscription();
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting, isDirty },
|
||||
handleSubmit
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: {
|
||||
auditLogsRetentionDays:
|
||||
currentWorkspace?.auditLogsRetentionDays ?? subscription?.auditLogsRetentionDays ?? 0
|
||||
}
|
||||
});
|
||||
|
||||
if (!currentWorkspace) return null;
|
||||
|
||||
const handleAuditLogsRetentionSubmit = async ({ auditLogsRetentionDays }: TForm) => {
|
||||
try {
|
||||
if (!subscription?.auditLogs) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
description: "You can only configure audit logs retention if you upgrade your plan."
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (subscription && auditLogsRetentionDays > subscription?.auditLogsRetentionDays) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
description:
|
||||
"To update your audit logs retention period to a higher value, upgrade your plan."
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await updateAuditLogsRetention({
|
||||
auditLogsRetentionDays,
|
||||
projectSlug: currentWorkspace.slug
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated audit logs retention period",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
createNotification({
|
||||
text: "Failed updating audit logs retention period",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// render only for dedicated/self-hosted instances of Infisical
|
||||
if (
|
||||
window.location.origin.includes("https://app.infisical.com") ||
|
||||
window.location.origin.includes("https://gamma.infisical.com")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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">Audit Logs Retention</p>
|
||||
</div>
|
||||
<p className="mb-4 mt-2 max-w-2xl text-sm text-gray-400">
|
||||
Set the number of days to keep your project audit logs.
|
||||
</p>
|
||||
<form onSubmit={handleSubmit(handleAuditLogsRetentionSubmit)} autoComplete="off">
|
||||
<div className="max-w-xs">
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue={0}
|
||||
name="auditLogsRetentionDays"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Number of days"
|
||||
>
|
||||
<Input {...field} type="number" min={1} step={1} isDisabled={!isAdmin} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
disabled={!isAdmin || !isDirty}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text={(popUp.upgradePlan?.data as { description: string })?.description}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { AuditLogsRetentionSection } from "./AuditLogsRetentionSection";
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
|
||||
import { AutoCapitalizationSection } from "../AutoCapitalizationSection";
|
||||
import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection";
|
||||
import { DeleteProjectSection } from "../DeleteProjectSection";
|
||||
@@ -17,6 +18,7 @@ export const ProjectGeneralTab = () => {
|
||||
<AutoCapitalizationSection />
|
||||
<E2EESection />
|
||||
<PointInTimeVersionLimitSection />
|
||||
<AuditLogsRetentionSection />
|
||||
<BackfillSecretReferenceSecretion />
|
||||
<RebuildSecretIndicesSection />
|
||||
<DeleteProjectSection />
|
||||
|
||||
Reference in New Issue
Block a user