diff --git a/backend/src/db/migrations/20250606134139_add-project-snapshots-legacy-option.ts b/backend/src/db/migrations/20250606134139_add-project-snapshots-legacy-option.ts new file mode 100644 index 000000000..f5f73d7fe --- /dev/null +++ b/backend/src/db/migrations/20250606134139_add-project-snapshots-legacy-option.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasShowSnapshotsLegacyColumn = await knex.schema.hasColumn(TableName.Project, "showSnapshotsLegacy"); + if (!hasShowSnapshotsLegacyColumn) { + await knex.schema.table(TableName.Project, (table) => { + table.boolean("showSnapshotsLegacy").notNullable().defaultTo(false); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasShowSnapshotsLegacyColumn = await knex.schema.hasColumn(TableName.Project, "showSnapshotsLegacy"); + if (hasShowSnapshotsLegacyColumn) { + await knex.schema.table(TableName.Project, (table) => { + table.dropColumn("showSnapshotsLegacy"); + }); + } +} diff --git a/backend/src/db/schemas/projects.ts b/backend/src/db/schemas/projects.ts index c1e96e8ce..b4c98d8a2 100644 --- a/backend/src/db/schemas/projects.ts +++ b/backend/src/db/schemas/projects.ts @@ -28,7 +28,8 @@ export const ProjectsSchema = z.object({ type: z.string(), enforceCapitalization: z.boolean().default(false), hasDeleteProtection: z.boolean().default(false).nullable().optional(), - secretSharing: z.boolean().default(true) + secretSharing: z.boolean().default(true), + showSnapshotsLegacy: z.boolean().default(false) }); export type TProjects = z.infer; diff --git a/backend/src/ee/routes/v1/snapshot-router.ts b/backend/src/ee/routes/v1/snapshot-router.ts index 3ee80adce..c020961be 100644 --- a/backend/src/ee/routes/v1/snapshot-router.ts +++ b/backend/src/ee/routes/v1/snapshot-router.ts @@ -65,9 +65,10 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { rateLimit: writeLimit }, schema: { - hide: false, + hide: true, + deprecated: true, tags: [ApiDocsTags.Projects], - description: "Roll back project secrets to those captured in a secret snapshot version.", + description: "(Deprecated) Roll back project secrets to those captured in a secret snapshot version.", security: [ { bearerAuth: [] @@ -84,6 +85,8 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { + throw new Error("This endpoint is deprecated and no longer available"); + const secretSnapshot = await server.services.snapshot.rollbackSnapshot({ actor: req.permission.type, actorId: req.permission.id, diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 3734cbf21..8dab426bf 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -625,7 +625,8 @@ export const PROJECTS = { autoCapitalization: "Disable or enable auto-capitalization for the project.", slug: "An optional slug for the project. (must be unique within the organization)", hasDeleteProtection: "Enable or disable delete protection for the project.", - secretSharing: "Enable or disable secret sharing for the project." + secretSharing: "Enable or disable secret sharing for the project.", + showSnapshotsLegacy: "Enable or disable legacy snapshots for the project." }, GET_KEY: { workspaceId: "The ID of the project to get the key from." diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index a26293ac8..ce51b1079 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -262,7 +262,8 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({ kmsCertificateKeyId: true, auditLogsRetentionDays: true, hasDeleteProtection: true, - secretSharing: true + secretSharing: true, + showSnapshotsLegacy: true }); export const SanitizedTagSchema = SecretTagsSchema.pick({ diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 2a868864e..cc94adede 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -376,7 +376,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }) .optional() .describe(PROJECTS.UPDATE.slug), - secretSharing: z.boolean().optional().describe(PROJECTS.UPDATE.secretSharing) + secretSharing: z.boolean().optional().describe(PROJECTS.UPDATE.secretSharing), + showSnapshotsLegacy: z.boolean().optional().describe(PROJECTS.UPDATE.showSnapshotsLegacy) }), response: { 200: z.object({ @@ -397,7 +398,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { autoCapitalization: req.body.autoCapitalization, hasDeleteProtection: req.body.hasDeleteProtection, slug: req.body.slug, - secretSharing: req.body.secretSharing + secretSharing: req.body.secretSharing, + showSnapshotsLegacy: req.body.showSnapshotsLegacy }, actorAuthMethod: req.permission.authMethod, actorId: req.permission.id, diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index 1ca5eb754..f128d2df6 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -667,7 +667,8 @@ export const projectServiceFactory = ({ enforceCapitalization: update.autoCapitalization, hasDeleteProtection: update.hasDeleteProtection, slug: update.slug, - secretSharing: update.secretSharing + secretSharing: update.secretSharing, + showSnapshotsLegacy: update.showSnapshotsLegacy }); return updatedProject; diff --git a/backend/src/services/project/project-types.ts b/backend/src/services/project/project-types.ts index be052f1cb..8ef72492a 100644 --- a/backend/src/services/project/project-types.ts +++ b/backend/src/services/project/project-types.ts @@ -94,6 +94,7 @@ export type TUpdateProjectDTO = { hasDeleteProtection?: boolean; slug?: string; secretSharing?: boolean; + showSnapshotsLegacy?: boolean; }; } & Omit; diff --git a/docs/documentation/platform/pit-recovery.mdx b/docs/documentation/platform/pit-recovery.mdx index 448faddbb..ee801ba64 100644 --- a/docs/documentation/platform/pit-recovery.mdx +++ b/docs/documentation/platform/pit-recovery.mdx @@ -4,38 +4,131 @@ description: "Learn how to rollback secrets and configurations to any snapshot w --- - Point-in-Time Recovery is a paid feature. - - If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, - then you should contact sales@infisical.com to purchase an enterprise license to use it. + Point-in-Time Recovery is a paid feature. If you're using Infisical Cloud, + then it is available under the **Pro Tier**. If you're self-hosting Infisical, + then you should contact sales@infisical.com to purchase an enterprise license + to use it. Infisical's point-in-time recovery functionality allows secrets to be rolled back to any point in time for any given [folder](./folder) or [environment](/documentation/platform/project#project-environments). -Every time a secret is updated, a new snapshot is taken – capturing the state of the folder and environment at that point of time. +Every time a secret is updated, a new commit is created – capturing the state of the folder and environment at that point of time. -## Snapshots + + + ## Understanding Commits -Similar to Git, a commit (also known as snapshot) in Infisical is the state of your project's secrets at a specific point in time scoped to -an environment and [folder](./folder) within it. + Similar to Git, a commit in Infisical represents a snapshot of changes made to your project's secrets at a specific point in time. Each commit is scoped to an environment and [folder](./folder) within it. Unlike the legacy snapshot system, the new commits interface provides granular tracking of individual changes, allowing you to see exactly what was modified, added, or removed in each commit. -To view a list of snapshots for the current folder, press the **Commits** button. + ### Accessing Commits -![PIT commits](../../images/platform/pit-recovery/pit-recovery-commits.png) + From your secrets management interface, you can access the commits functionality by clicking the **Commits Button**. This button is located in the top-right area of your secrets view and shows the number of commits for the current folder (e.g., "4 Commits"). -This opens up a sidebar from which you can select to view a particular snapshot: + ![Commits Button](../../images/platform/pit-recovery/pit-recovery-revamp/pit-commits-button.png) -![PIT snapshots](../../images/platform/pit-recovery/pit-recovery-commits-drawer.png) + ### Commits List View -## Rolling back + The commits page displays a comprehensive chronological history of all changes made to your environment and folders: -After pressing on a snapshot from the sidebar, you can view it and roll back the state -of the folder to that point in time by pressing the **Rollback** button. + ![Commits List View](../../images/platform/pit-recovery/pit-recovery-revamp/pit-commits-history.png) -![PIT snapshot](../../images/platform/pit-recovery/pit-recovery-rollback.png) + - **Chronological Sorting**: Commits are grouped by date + - **Commit Information**: Each commit shows: + - Commit message + - Author information + - Relative timestamp + - Unique commit hash identifier + - **Search Functionality**: Use the search bar to quickly find specific commits + - **Sorting Options**: Sort commits by various criteria using the sort controls -Rolling back secrets to a past snapshot creates a creates a snapshot at the top of the stack and updates secret versions. + ### Detailed Commit Inspection - -Rollbacks are localized to not affect other folders within the same environment. This means each [folder](./folder) maintains its own independent history of changes, offering precise and isolated control over rollback actions. -Put differently, every [folder](./folder) possesses a distinct and separate timeline, providing granular control when managing your secrets. - \ No newline at end of file + Clicking on any commit from the list opens a detailed view showing the list of changes made in that commit. + + ![Detailed Commit Inspection](../../images/platform/pit-recovery/pit-recovery-revamp/pit-commit-changes.png) + + #### Change Categories + + The commit changes details can be grouped into the following categories: + + **Folder Changes** + - Shows folder additions, modifications, or deletions + - Displays the folder properties changes in JSON format, including: + - Folder name + - Folder description + + **Secret Changes** + - Lists all secrets that were added, updated, or removed + - Shows the complete secret configuration including: + - Secret key and value + - Comments and metadata + - Tags and metadata + - Encoding settings (e.g., skipMultilineEncoding) + - Values are displayed with appropriate masking for security + + **Visual Indicators** + - Green "+" indicators show additions + - Red "-" indicators show deletions + - Modified content shows both old and new states + + ### Restoration Options + + Each commit provides two distinct restoration methods accessible via the **Restore Options** dropdown: + + ![Restore Options](../../images/platform/pit-recovery/pit-recovery-revamp/pit-commit-changes-options.png) + + #### Revert changes + This option provides surgical precision for undoing specific modifications: + + - **Granular Control**: Reverts only the specific changes introduced in that individual commit + - **Selective Restoration**: Preserves all other changes made after the commit + - **Targeted Undo**: Perfect for reversing a specific problematic change without affecting other work + - **Minimal Impact**: Only affects the resources that were modified in that particular commit + - **Use Case**: Ideal when you want to undo a specific change while keeping all other modifications intact + + #### Roll back to this commit + This option performs a complete restoration to the selected point in time: + + ![Rollback](../../images/platform/pit-recovery/pit-recovery-revamp/pit-commit-restore.png) + + - **Complete State Restoration**: Returns the entire folder to its exact state at the time of this commit + - **Destructive Operation**: Discards ALL changes made after the selected commit + - **New Commit Creation**: Creates a new commit representing this rollback operation + - **Use Case**: Ideal when you want to completely undo a series of changes and return to a known good state + + **Warning**: This operation will undo all modifications made after the selected commit, which may include multiple secrets and configuration changes. + + + + + The snapshots interface is deprecated and will be removed in a future version. Please use the new Commits interface for more granular point-in-time recovery operations. + + + ## Snapshots + + Similar to Git, a commit (also known as snapshot) in Infisical is the state of your project's secrets at a specific point in time scoped to + an environment and [folder](./folder) within it. + + To view a list of snapshots for the current folder, press the **Commits** button. + + ![PIT commits](../../images/platform/pit-recovery/pit-recovery-commits.png) + + This opens up a sidebar from which you can select to view a particular snapshot: + + ![PIT snapshots](../../images/platform/pit-recovery/pit-recovery-commits-drawer.png) + + ## Rolling back + + After pressing on a snapshot from the sidebar, you can view it and roll back the state + of the folder to that point in time by pressing the **Rollback** button. + + ![PIT snapshot](../../images/platform/pit-recovery/pit-recovery-rollback.png) + + Rolling back secrets to a past snapshot creates a creates a snapshot at the top of the stack and updates secret versions. + + + Rollbacks are localized to not affect other folders within the same environment. This means each [folder](./folder) maintains its own independent history of changes, offering precise and isolated control over rollback actions. + Put differently, every [folder](./folder) possesses a distinct and separate timeline, providing granular control when managing your secrets. + + + + diff --git a/docs/images/platform/pit-recovery/pit-recovery-revamp/pit-commit-changes-options.png b/docs/images/platform/pit-recovery/pit-recovery-revamp/pit-commit-changes-options.png new file mode 100644 index 000000000..60d9dff01 Binary files /dev/null and b/docs/images/platform/pit-recovery/pit-recovery-revamp/pit-commit-changes-options.png differ diff --git a/docs/images/platform/pit-recovery/pit-recovery-revamp/pit-commit-changes.png b/docs/images/platform/pit-recovery/pit-recovery-revamp/pit-commit-changes.png new file mode 100644 index 000000000..56357e9ef Binary files /dev/null and b/docs/images/platform/pit-recovery/pit-recovery-revamp/pit-commit-changes.png differ diff --git a/docs/images/platform/pit-recovery/pit-recovery-revamp/pit-commit-restore.png b/docs/images/platform/pit-recovery/pit-recovery-revamp/pit-commit-restore.png new file mode 100644 index 000000000..3ed22c63b Binary files /dev/null and b/docs/images/platform/pit-recovery/pit-recovery-revamp/pit-commit-restore.png differ diff --git a/docs/images/platform/pit-recovery/pit-recovery-revamp/pit-commits-button.png b/docs/images/platform/pit-recovery/pit-recovery-revamp/pit-commits-button.png new file mode 100644 index 000000000..273760ca5 Binary files /dev/null and b/docs/images/platform/pit-recovery/pit-recovery-revamp/pit-commits-button.png differ diff --git a/docs/images/platform/pit-recovery/pit-recovery-revamp/pit-commits-history.png b/docs/images/platform/pit-recovery/pit-recovery-revamp/pit-commits-history.png new file mode 100644 index 000000000..3832209f4 Binary files /dev/null and b/docs/images/platform/pit-recovery/pit-recovery-revamp/pit-commits-history.png differ diff --git a/docs/mint.json b/docs/mint.json index 64b827f55..4b17d06fc 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -825,8 +825,7 @@ "api-reference/endpoints/workspaces/delete-workspace", "api-reference/endpoints/workspaces/get-workspace", "api-reference/endpoints/workspaces/update-workspace", - "api-reference/endpoints/workspaces/secret-snapshots", - "api-reference/endpoints/workspaces/rollback-snapshot" + "api-reference/endpoints/workspaces/secret-snapshots" ] }, { diff --git a/frontend/src/hooks/api/workspace/queries.tsx b/frontend/src/hooks/api/workspace/queries.tsx index c040a1267..a423de568 100644 --- a/frontend/src/hooks/api/workspace/queries.tsx +++ b/frontend/src/hooks/api/workspace/queries.tsx @@ -282,7 +282,8 @@ export const useUpdateProject = () => { newProjectName, newProjectDescription, newSlug, - secretSharing + secretSharing, + showSnapshotsLegacy }) => { const { data } = await apiRequest.patch<{ workspace: Workspace }>( `/api/v1/workspace/${projectID}`, @@ -290,7 +291,8 @@ export const useUpdateProject = () => { name: newProjectName, description: newProjectDescription, slug: newSlug, - secretSharing + secretSharing, + showSnapshotsLegacy } ); return data.workspace; diff --git a/frontend/src/hooks/api/workspace/types.ts b/frontend/src/hooks/api/workspace/types.ts index 481bdcc08..fbaea7742 100644 --- a/frontend/src/hooks/api/workspace/types.ts +++ b/frontend/src/hooks/api/workspace/types.ts @@ -39,6 +39,7 @@ export type Workspace = { roles?: TProjectRole[]; hasDeleteProtection: boolean; secretSharing: boolean; + showSnapshotsLegacy: boolean; }; export type WorkspaceEnv = { @@ -79,6 +80,7 @@ export type UpdateProjectDTO = { newProjectDescription?: string; newSlug?: string; secretSharing?: boolean; + showSnapshotsLegacy?: boolean; }; export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index 1082ce2e2..84edeec9a 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -300,7 +300,7 @@ const Page = () => { isPaused: !canDoReadRollback }); - const isPITEnabled = true; + const isPITEnabled = !currentWorkspace?.showSnapshotsLegacy; const changesCount = useMemo(() => { return isPITEnabled ? folderCommitsCount : snapshotCount; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SnapshotView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SnapshotView.tsx index c84e75429..aefb18736 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SnapshotView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SnapshotView/SnapshotView.tsx @@ -5,7 +5,8 @@ import { faCodeCommit, faFolder, faMagnifyingGlass, - faUndo + faUndo, + faWarning } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -60,6 +61,7 @@ export const SnapshotView = ({ const rollingFolder = snapshotData?.folders || []; const rollingSecrets = snapshotData?.secrets || []; + const isAllowedRollback = false; const folderDiffView = useMemo(() => { const folderGroupById = folders.reduce>( @@ -153,6 +155,16 @@ export const SnapshotView = ({ return ( <> +
+ +
+ Deprecation Notice +

+ Snapshots are being deprecated in favor of Commits. They will be officially removed on + November 2025. +

+
+
Snapshot
{new Date(snapshotData?.createdAt || "").toLocaleString()} @@ -168,17 +180,19 @@ export const SnapshotView = ({ />
-
- -
+ {isAllowedRollback && ( +
+ +
+ )}
-
- - {(isAllowed) => ( - - )} - -
+ {isAllowedRollback && ( +
+ + {(isAllowed) => ( + + )} + +
+ )}
diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx index 8ada43347..2843300e8 100644 --- a/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx +++ b/frontend/src/pages/secret-manager/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx @@ -11,6 +11,7 @@ import { EnvironmentSection } from "../EnvironmentSection"; import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection"; import { RebuildSecretIndicesSection } from "../RebuildSecretIndicesSection/RebuildSecretIndicesSection"; import { SecretSharingSection } from "../SecretSharingSection"; +import { SecretSnapshotsLegacySection } from "../SecretSnapshotsLegacySection"; import { SecretTagsSection } from "../SecretTagsSection"; export const ProjectGeneralTab = () => { @@ -24,6 +25,7 @@ export const ProjectGeneralTab = () => { {isSecretManager && } {isSecretManager && } {isSecretManager && } + {isSecretManager && } {isSecretManager && } {isSecretManager && } diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretSnapshotsLegacySection/SecretSnapshotsLegacySection.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretSnapshotsLegacySection/SecretSnapshotsLegacySection.tsx new file mode 100644 index 000000000..6bd5d76b6 --- /dev/null +++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretSnapshotsLegacySection/SecretSnapshotsLegacySection.tsx @@ -0,0 +1,65 @@ +import { useState } from "react"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { Checkbox } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { useUpdateProject } from "@app/hooks/api/workspace/queries"; + +export const SecretSnapshotsLegacySection = () => { + const { currentWorkspace } = useWorkspace(); + const { mutateAsync: updateProject } = useUpdateProject(); + + const [isLoading, setIsLoading] = useState(false); + + const handleToggle = async (state: boolean) => { + setIsLoading(true); + + try { + if (!currentWorkspace?.id) { + setIsLoading(false); + return; + } + + await updateProject({ + projectID: currentWorkspace.id, + showSnapshotsLegacy: state + }); + + createNotification({ + text: `Successfully ${state ? "enabled" : "disabled"} secret snapshots legacy for this project`, + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to update secret snapshots legacy for this project", + type: "error" + }); + } finally { + setIsLoading(false); + } + }; + + return ( +
+

Show Secret Snapshots Legacy

+ + {(isAllowed) => ( +
+ handleToggle(state as boolean)} + > + This feature enables your project members to view secret snapshots in the legacy + format. + +
+ )} +
+
+ ); +}; diff --git a/frontend/src/pages/secret-manager/SettingsPage/components/SecretSnapshotsLegacySection/index.tsx b/frontend/src/pages/secret-manager/SettingsPage/components/SecretSnapshotsLegacySection/index.tsx new file mode 100644 index 000000000..2ad1242cd --- /dev/null +++ b/frontend/src/pages/secret-manager/SettingsPage/components/SecretSnapshotsLegacySection/index.tsx @@ -0,0 +1 @@ +export { SecretSnapshotsLegacySection } from "./SecretSnapshotsLegacySection";