mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 03:25:48 +00: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),
|
version: z.number().default(1),
|
||||||
upgradeStatus: z.string().nullable().optional(),
|
upgradeStatus: z.string().nullable().optional(),
|
||||||
pitVersionLimit: z.number().default(10),
|
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>;
|
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||||
|
|||||||
@@ -45,18 +45,29 @@ export const auditLogQueueServiceFactory = ({
|
|||||||
const { actor, event, ipAddress, projectId, userAgent, userAgentType } = job.data;
|
const { actor, event, ipAddress, projectId, userAgent, userAgentType } = job.data;
|
||||||
let { orgId } = job.data;
|
let { orgId } = job.data;
|
||||||
const MS_IN_DAY = 24 * 60 * 60 * 1000;
|
const MS_IN_DAY = 24 * 60 * 60 * 1000;
|
||||||
|
let project;
|
||||||
|
|
||||||
if (!orgId) {
|
if (!orgId) {
|
||||||
// it will never be undefined for both org and project id
|
// it will never be undefined for both org and project id
|
||||||
// TODO(akhilmhdh): use caching here in dal to avoid db calls
|
// 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;
|
orgId = project.orgId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const plan = await licenseService.getPlan(orgId);
|
const plan = await licenseService.getPlan(orgId);
|
||||||
const ttl = plan.auditLogsRetentionDays * MS_IN_DAY;
|
if (plan.auditLogsRetentionDays === 0) {
|
||||||
// skip inserting if audit log retention is 0 meaning its not supported
|
// skip inserting if audit log retention is 0 meaning its not supported
|
||||||
if (ttl === 0) return;
|
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({
|
const auditLog = await auditLogDAL.create({
|
||||||
actor: actor.type,
|
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({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:workspaceId/integrations",
|
url: "/:workspaceId/integrations",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { createSecretBlindIndex } from "@app/lib/crypto";
|
import { createSecretBlindIndex } from "@app/lib/crypto";
|
||||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
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 { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
TListProjectCasDTO,
|
TListProjectCasDTO,
|
||||||
TListProjectCertsDTO,
|
TListProjectCertsDTO,
|
||||||
TToggleProjectAutoCapitalizationDTO,
|
TToggleProjectAutoCapitalizationDTO,
|
||||||
|
TUpdateAuditLogsRetentionDTO,
|
||||||
TUpdateProjectDTO,
|
TUpdateProjectDTO,
|
||||||
TUpdateProjectNameDTO,
|
TUpdateProjectNameDTO,
|
||||||
TUpdateProjectVersionLimitDTO,
|
TUpdateProjectVersionLimitDTO,
|
||||||
@@ -446,6 +447,43 @@ export const projectServiceFactory = ({
|
|||||||
return projectDAL.updateById(project.id, { pitVersionLimit });
|
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 ({
|
const updateName = async ({
|
||||||
projectId,
|
projectId,
|
||||||
actor,
|
actor,
|
||||||
@@ -621,6 +659,7 @@ export const projectServiceFactory = ({
|
|||||||
upgradeProject,
|
upgradeProject,
|
||||||
listProjectCas,
|
listProjectCas,
|
||||||
listProjectCertificates,
|
listProjectCertificates,
|
||||||
updateVersionLimit
|
updateVersionLimit,
|
||||||
|
updateAuditLogsRetention
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ export type TUpdateProjectVersionLimitDTO = {
|
|||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TUpdateAuditLogsRetentionDTO = {
|
||||||
|
auditLogsRetentionDays: number;
|
||||||
|
workspaceSlug: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TUpdateProjectNameDTO = {
|
export type TUpdateProjectNameDTO = {
|
||||||
name: string;
|
name: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
ToggleAutoCapitalizationDTO,
|
ToggleAutoCapitalizationDTO,
|
||||||
TUpdateWorkspaceIdentityRoleDTO,
|
TUpdateWorkspaceIdentityRoleDTO,
|
||||||
TUpdateWorkspaceUserRoleDTO,
|
TUpdateWorkspaceUserRoleDTO,
|
||||||
|
UpdateAuditLogsRetentionDTO,
|
||||||
UpdateEnvironmentDTO,
|
UpdateEnvironmentDTO,
|
||||||
UpdatePitVersionLimitDTO,
|
UpdatePitVersionLimitDTO,
|
||||||
Workspace
|
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 = () => {
|
export const useDeleteWorkspace = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export type Workspace = {
|
|||||||
autoCapitalization: boolean;
|
autoCapitalization: boolean;
|
||||||
environments: WorkspaceEnv[];
|
environments: WorkspaceEnv[];
|
||||||
pitVersionLimit: number;
|
pitVersionLimit: number;
|
||||||
|
auditLogsRetentionDays: number;
|
||||||
slug: string;
|
slug: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,6 +52,7 @@ export type CreateWorkspaceDTO = {
|
|||||||
|
|
||||||
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
|
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
|
||||||
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
|
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
|
||||||
|
export type UpdateAuditLogsRetentionDTO = { projectSlug: string; auditLogsRetentionDays: number };
|
||||||
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };
|
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };
|
||||||
|
|
||||||
export type DeleteWorkspaceDTO = { workspaceID: string };
|
export type DeleteWorkspaceDTO = { workspaceID: string };
|
||||||
|
|||||||
+128
@@ -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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { AuditLogsRetentionSection } from "./AuditLogsRetentionSection";
|
||||||
+2
@@ -1,3 +1,4 @@
|
|||||||
|
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
|
||||||
import { AutoCapitalizationSection } from "../AutoCapitalizationSection";
|
import { AutoCapitalizationSection } from "../AutoCapitalizationSection";
|
||||||
import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection";
|
import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSection";
|
||||||
import { DeleteProjectSection } from "../DeleteProjectSection";
|
import { DeleteProjectSection } from "../DeleteProjectSection";
|
||||||
@@ -17,6 +18,7 @@ export const ProjectGeneralTab = () => {
|
|||||||
<AutoCapitalizationSection />
|
<AutoCapitalizationSection />
|
||||||
<E2EESection />
|
<E2EESection />
|
||||||
<PointInTimeVersionLimitSection />
|
<PointInTimeVersionLimitSection />
|
||||||
|
<AuditLogsRetentionSection />
|
||||||
<BackfillSecretReferenceSecretion />
|
<BackfillSecretReferenceSecretion />
|
||||||
<RebuildSecretIndicesSection />
|
<RebuildSecretIndicesSection />
|
||||||
<DeleteProjectSection />
|
<DeleteProjectSection />
|
||||||
|
|||||||
Reference in New Issue
Block a user