PIT: general improvements and fixes

This commit is contained in:
carlosmonastyrski
2025-05-25 00:12:31 -03:00
parent 28a3bf0b94
commit b7b90aea33
21 changed files with 1588 additions and 896 deletions
+62 -269
View File
@@ -1,18 +1,13 @@
/* eslint-disable @typescript-eslint/no-base-to-string */
import { ForbiddenError } from "@casl/ability";
import { z } from "zod";
import { ActionProjectType } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ProjectPermissionCommitsActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { NotFoundError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { booleanSchema } from "@app/server/routes/sanitizedSchemas";
import { ActorAuthMethod, ActorType, AuthMode } from "@app/services/auth/auth-type";
import { ChangeType } from "@app/services/folder-commit/folder-commit-service";
import { commitChangesResponseSchema } from "@app/services/folder-commit/folder-commit-types";
import { AuthMode } from "@app/services/auth/auth-type";
import { commitChangesResponseSchema, resourceChangeSchema } from "@app/services/folder-commit/folder-commit-schemas";
const commitHistoryItemSchema = z.object({
id: z.string(),
@@ -49,7 +44,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
querystring: z.object({
environment: z.string().trim(),
path: z.string().trim().default("/").transform(removeTrailingSlash),
workspaceId: z.string().trim()
projectId: z.string().trim()
}),
response: {
200: z.object({
@@ -60,30 +55,30 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const res = await server.services.folderCommit.getCommitsCount({
const result = await server.services.pit.getCommitsCount({
actor: req.permission?.type,
actorId: req.permission?.id,
actorOrgId: req.permission?.orgId,
actorAuthMethod: req.permission?.authMethod,
projectId: req.query.workspaceId,
projectId: req.query.projectId,
environment: req.query.environment,
path: req.query.path
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.query.workspaceId,
projectId: req.query.projectId,
event: {
type: EventType.GET_PROJECT_PIT_COMMIT_COUNT,
metadata: {
environment: req.query.environment,
path: req.query.path,
commitCount: res.count.toString()
commitCount: result.count.toString()
}
}
});
return res;
return result;
}
});
@@ -98,7 +93,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
querystring: z.object({
environment: z.string().trim(),
path: z.string().trim().default("/").transform(removeTrailingSlash),
workspaceId: z.string().trim(),
projectId: z.string().trim(),
offset: z.coerce.number().min(0).default(0),
limit: z.coerce.number().min(1).max(100).default(20),
search: z.string().trim().optional(),
@@ -114,12 +109,12 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const result = await server.services.folderCommit.getCommitsForFolder({
const result = await server.services.pit.getCommitsForFolder({
actor: req.permission?.type,
actorId: req.permission?.id,
actorOrgId: req.permission?.orgId,
actorAuthMethod: req.permission?.authMethod,
projectId: req.query.workspaceId,
projectId: req.query.projectId,
environment: req.query.environment,
path: req.query.path,
offset: req.query.offset,
@@ -130,7 +125,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.query.workspaceId,
projectId: req.query.projectId,
event: {
type: EventType.GET_PROJECT_PIT_COMMITS,
metadata: {
@@ -145,87 +140,10 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
}
});
return {
commits: result.commits.map((commit) => ({
...commit,
commitId: commit.commitId.toString()
})),
total: result.total,
hasMore: result.hasMore
};
return result;
}
});
const getChangeVersions = async (
change: {
secretVersion?: string;
secretId?: string;
id?: string;
isUpdate?: boolean;
changeType?: string;
},
previousVersion: string,
actorId: string,
actor: ActorType,
actorOrgId: string,
actorAuthMethod: ActorAuthMethod,
folderId: string
) => {
if (change.secretVersion) {
const currentVersion = change.secretVersion || "1";
const secretId = change.secretId ? change.secretId : change.id;
if (!secretId) {
return;
}
// eslint-disable-next-line no-await-in-loop
const versions = await server.services.secret.getSecretVersionsV2ByIds({
actorId,
actor,
actorOrgId,
actorAuthMethod,
secretId,
// if it's update add also the previous secretversionid
secretVersions:
change.isUpdate || change.changeType === ChangeType.UPDATE
? [currentVersion, previousVersion]
: [currentVersion],
folderId
});
return versions?.map((v) => ({
secretKey: v.secretKey,
secretComment: v.secretComment,
skipMultilineEncoding: v.skipMultilineEncoding,
secretReminderRepeatDays: v.secretReminderRepeatDays,
secretReminderNote: v.secretReminderNote,
metadata: v.metadata,
tags: v.tags?.map((t) => t.name),
secretReminderRecipients: v.secretReminderRecipients?.map((r) => r.toString()),
secretValue: v.secretValue
}));
}
};
const getFolderVersions = async (
change: {
folderVersion?: string;
isUpdate?: boolean;
changeType?: string;
},
fromVersion: string,
folderId: string
) => {
const currentVersion = change.folderVersion || "1";
// eslint-disable-next-line no-await-in-loop
const versions = await server.services.folder.getFolderVersionsByIds({
folderId,
folderVersions:
change.isUpdate || change.changeType === ChangeType.UPDATE ? [currentVersion, fromVersion] : [currentVersion]
});
return versions.map((v) => ({
name: v.name
}));
};
// Get commit changes for a specific commit
server.route({
method: "GET",
@@ -238,7 +156,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
commitId: z.string().trim()
}),
querystring: z.object({
workspaceId: z.string().trim()
projectId: z.string().trim()
}),
response: {
200: commitChangesResponseSchema
@@ -246,54 +164,28 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const changes = await server.services.folderCommit.getCommitChanges({
const result = await server.services.pit.getCommitChanges({
actor: req.permission?.type,
actorId: req.permission?.id,
actorOrgId: req.permission?.orgId,
actorAuthMethod: req.permission?.authMethod,
projectId: req.query.workspaceId,
projectId: req.query.projectId,
commitId: req.params.commitId
});
for (const change of changes.changes) {
if (change.secretVersionId && change.secretVersion) {
// eslint-disable-next-line no-await-in-loop
change.versions = await getChangeVersions(
change,
(Number.parseInt(change.secretVersion, 10) - 1).toString(),
req.permission.id,
req.permission.type,
req.permission.orgId,
req.permission.authMethod,
change.folderId
);
} else if (change.folderVersionId && change.folderChangeId && change.folderVersion) {
// eslint-disable-next-line no-await-in-loop
change.versions = await getFolderVersions(
change,
(Number.parseInt(change.folderVersion, 10) - 1).toString(),
change.folderChangeId
);
}
}
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.query.workspaceId,
projectId: req.query.projectId,
event: {
type: EventType.GET_PROJECT_PIT_COMMIT_CHANGES,
metadata: {
commitId: req.params.commitId,
changesCount: (changes.changes?.length || 0).toString()
changesCount: (result.changes.changes?.length || 0).toString()
}
}
});
return {
changes: {
...changes,
commitId: changes.commitId.toString()
}
};
return result;
}
});
@@ -313,7 +205,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
envId: z.string().trim(),
deepRollback: booleanSchema.default(false),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
workspaceId: z.string().trim()
projectId: z.string().trim()
}),
response: {
200: z.array(
@@ -321,89 +213,43 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
folderId: z.string(),
folderName: z.string(),
folderPath: z.string().optional(),
changes: z.any()
changes: z.array(resourceChangeSchema)
})
)
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const latestCommit = await server.services.folderCommit.getLatestCommit({
folderId: req.query.folderId,
const result = await server.services.pit.compareCommitChanges({
actor: req.permission?.type,
actorId: req.permission?.id,
actorOrgId: req.permission?.orgId,
actorAuthMethod: req.permission?.authMethod,
projectId: req.query.workspaceId
projectId: req.query.projectId,
commitId: req.params.commitId,
folderId: req.query.folderId,
envId: req.query.envId,
deepRollback: req.query.deepRollback,
secretPath: req.query.secretPath
});
if (!latestCommit) {
throw new NotFoundError({ message: "Latest commit not found" });
}
let diffs;
if (req.query.deepRollback) {
diffs = await server.services.folderCommit.deepCompareFolder({
targetCommitId: req.params.commitId,
envId: req.query.envId,
projectId: req.query.workspaceId
});
} else {
const folder = await server.services.folder.getFolderById({
actor: req.permission?.type,
actorId: req.permission?.id,
actorOrgId: req.permission?.orgId,
actorAuthMethod: req.permission?.authMethod,
id: req.query.folderId
});
diffs = [
{
folderId: folder.id,
folderName: folder.name,
folderPath: req.query.secretPath,
changes: await server.services.folderCommit.compareFolderStates({
targetCommitId: req.params.commitId,
currentCommitId: latestCommit.id
})
}
];
}
for (const diff of diffs) {
for (const change of diff.changes) {
if (change.secretKey) {
// eslint-disable-next-line no-await-in-loop
change.versions = await getChangeVersions(
change,
change.fromVersion || "1",
req.permission.id,
req.permission.type,
req.permission.orgId,
req.permission.authMethod,
diff.folderId
);
}
if (change.folderVersion) {
// eslint-disable-next-line no-await-in-loop
change.versions = await getFolderVersions(change, change.fromVersion || "1", change.id);
}
}
}
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.query.workspaceId,
projectId: req.query.projectId,
event: {
type: EventType.PIT_COMPARE_FOLDER_STATES,
metadata: {
targetCommitId: req.params.commitId,
folderId: req.query.folderId,
deepRollback: req.query.deepRollback,
diffsCount: diffs.length.toString()
diffsCount: result.length.toString(),
env: req.query.envId,
folderPath: req.query.secretPath
}
}
});
return diffs;
return result;
}
});
@@ -423,7 +269,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
deepRollback: z.boolean().default(false),
message: z.string().trim().optional(),
envId: z.string().trim(),
workspaceId: z.string().trim()
projectId: z.string().trim()
}),
response: {
200: z.object({
@@ -436,79 +282,36 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { permission } = await server.services.permission.getProjectPermission({
actor: req.permission?.type,
actorId: req.permission?.id,
projectId: req.body.workspaceId,
actorAuthMethod: req.permission?.authMethod,
actorOrgId: req.permission?.orgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCommitsActions.PerformRollback,
ProjectPermissionSub.Commits
);
const latestCommit = await server.services.folderCommit.getLatestCommit({
folderId: req.body.folderId,
const result = await server.services.pit.rollbackToCommit({
actor: req.permission?.type,
actorId: req.permission?.id,
actorOrgId: req.permission?.orgId,
actorAuthMethod: req.permission?.authMethod,
projectId: req.body.workspaceId
});
if (!latestCommit) {
throw new NotFoundError({ message: "Latest commit not found" });
}
if (req.body.deepRollback) {
await server.services.folderCommit.deepRollbackFolder(
req.params.commitId,
req.body.envId,
req.permission.id,
req.permission.type,
req.body.workspaceId
);
return { success: true };
}
const diff = await server.services.folderCommit.compareFolderStates({
currentCommitId: latestCommit.id,
targetCommitId: req.params.commitId
});
const response = await server.services.folderCommit.applyFolderStateDifferences({
differences: diff,
actorInfo: {
actorType: req.permission.type,
actorId: req.permission.id,
message: req.body.message || "Rollback to previous commit"
},
projectId: req.body.projectId,
commitId: req.params.commitId,
folderId: req.body.folderId,
projectId: req.body.workspaceId,
reconstructNewFolders: req.body.deepRollback
deepRollback: req.body.deepRollback,
message: req.body.message,
envId: req.body.envId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.body.workspaceId,
projectId: req.body.projectId,
event: {
type: EventType.PIT_ROLLBACK_COMMIT,
metadata: {
targetCommitId: req.params.commitId,
envId: req.body.envId,
folderId: req.body.folderId,
deepRollback: req.body.deepRollback,
message: req.body.message || "Rollback to previous commit",
totalChanges: response.totalChanges.toString()
totalChanges: result.totalChanges?.toString() || "0"
}
}
});
return {
success: true,
secretChangesCount: response.secretChangesCount,
folderChangesCount: response.folderChangesCount,
totalChanges: response.totalChanges
};
return result;
}
});
@@ -523,8 +326,8 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
params: z.object({
commitId: z.string().trim()
}),
querystring: z.object({
workspaceId: z.string().trim()
body: z.object({
projectId: z.string().trim()
}),
response: {
200: z.object({
@@ -538,29 +341,29 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const response = await server.services.folderCommit.revertCommitChanges({
commitId: req.params.commitId,
const result = await server.services.pit.revertCommit({
actor: req.permission?.type,
actorId: req.permission?.id,
actorAuthMethod: req.permission?.authMethod,
actorOrgId: req.permission?.orgId,
projectId: req.query.workspaceId
actorAuthMethod: req.permission?.authMethod,
projectId: req.body.projectId,
commitId: req.params.commitId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.query.workspaceId,
projectId: req.body.projectId,
event: {
type: EventType.PIT_REVERT_COMMIT,
metadata: {
commitId: req.params.commitId,
revertCommitId: response.revertCommitId,
changesReverted: response.changesReverted?.toString()
revertCommitId: result.revertCommitId,
changesReverted: result.changesReverted?.toString()
}
}
});
return response;
return result;
}
});
@@ -577,7 +380,7 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
}),
querystring: z.object({
folderId: z.string().trim(),
workspaceId: z.string().trim()
projectId: z.string().trim()
}),
response: {
200: folderStateSchema
@@ -585,39 +388,29 @@ export const registerPITRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { permission } = await server.services.permission.getProjectPermission({
const result = await server.services.pit.getFolderStateAtCommit({
actor: req.permission?.type,
actorId: req.permission?.id,
projectId: req.query.workspaceId,
actorAuthMethod: req.permission?.authMethod,
actorOrgId: req.permission?.orgId,
actionProjectType: ActionProjectType.SecretManager
actorAuthMethod: req.permission?.authMethod,
projectId: req.query.projectId,
commitId: req.params.commitId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionCommitsActions.Read,
ProjectPermissionSub.Commits
);
const response = await server.services.folderCommit.reconstructFolderState(req.params.commitId);
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.query.workspaceId,
projectId: req.query.projectId,
event: {
type: EventType.PIT_GET_FOLDER_STATE,
metadata: {
commitId: req.params.commitId,
folderId: req.query.folderId,
resourceCount: response.length.toString()
resourceCount: result.length.toString()
}
}
});
return response.map((item) => ({
...item,
secretVersion: item.secretVersion ? Number(item.secretVersion) : undefined,
folderVersion: item.folderVersion ? Number(item.folderVersion) : undefined
}));
return result;
}
});
};
@@ -2959,6 +2959,7 @@ interface PitRollbackCommitEvent {
deepRollback: boolean;
message: string;
totalChanges: string;
envId: string;
};
}
@@ -2987,6 +2988,8 @@ interface PitCompareFolderStatesEvent {
folderId: string;
deepRollback: boolean;
diffsCount: string;
env: string;
folderPath: string;
};
}
+465
View File
@@ -0,0 +1,465 @@
/* eslint-disable no-await-in-loop */
import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas";
import { ProjectPermissionCommitsActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { ResourceType, TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
import {
isFolderCommitChange,
isSecretCommitChange
} from "@app/services/folder-commit-changes/folder-commit-changes-dal";
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
import { TPermissionServiceFactory } from "../permission/permission-service";
type TPitServiceFactoryDep = {
folderCommitService: TFolderCommitServiceFactory;
secretService: Pick<TSecretServiceFactory, "getSecretVersionsV2ByIds" | "getChangeVersions">;
folderService: Pick<TSecretFolderServiceFactory, "getFolderById" | "getFolderVersions">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
export type TPitServiceFactory = ReturnType<typeof pitServiceFactory>;
export const pitServiceFactory = ({
folderCommitService,
secretService,
folderService,
permissionService
}: TPitServiceFactoryDep) => {
const getCommitsCount = async ({
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId,
environment,
path
}: {
actor: ActorType;
actorId: string;
actorOrgId: string;
actorAuthMethod: ActorAuthMethod;
projectId: string;
environment: string;
path: string;
}) => {
const result = await folderCommitService.getCommitsCount({
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId,
environment,
path
});
return result;
};
const getCommitsForFolder = async ({
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId,
environment,
path,
offset,
limit,
search,
sort
}: {
actor: ActorType;
actorId: string;
actorOrgId: string;
actorAuthMethod: ActorAuthMethod;
projectId: string;
environment: string;
path: string;
offset: number;
limit: number;
search?: string;
sort: "asc" | "desc";
}) => {
const result = await folderCommitService.getCommitsForFolder({
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId,
environment,
path,
offset,
limit,
search,
sort
});
return {
commits: result.commits.map((commit) => ({
...commit,
commitId: commit.commitId.toString()
})),
total: result.total,
hasMore: result.hasMore
};
};
const getCommitChanges = async ({
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId,
commitId
}: {
actor: ActorType;
actorId: string;
actorOrgId: string;
actorAuthMethod: ActorAuthMethod;
projectId: string;
commitId: string;
}) => {
const changes = await folderCommitService.getCommitChanges({
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId,
commitId
});
for (const change of changes.changes) {
if (isSecretCommitChange(change)) {
change.versions = await secretService.getChangeVersions(
{
secretVersion: change.secretVersion,
secretId: change.secretId,
id: change.id,
isUpdate: change.isUpdate,
changeType: change.changeType
},
(Number.parseInt(change.secretVersion, 10) - 1).toString(),
actorId,
actor,
actorOrgId,
actorAuthMethod,
change.folderId
);
} else if (isFolderCommitChange(change)) {
change.versions = await folderService.getFolderVersions(
change,
(Number.parseInt(change.folderVersion, 10) - 1).toString(),
change.folderChangeId
);
}
}
return {
changes: {
...changes,
commitId: changes.commitId.toString()
}
};
};
const compareCommitChanges = async ({
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId,
commitId,
folderId,
envId,
deepRollback,
secretPath
}: {
actor: ActorType;
actorId: string;
actorOrgId: string;
actorAuthMethod: ActorAuthMethod;
projectId: string;
commitId: string;
folderId: string;
envId: string;
deepRollback: boolean;
secretPath: string;
}) => {
const latestCommit = await folderCommitService.getLatestCommit({
folderId,
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId
});
const targetCommit = await folderCommitService.getCommitById({
commitId,
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId
});
if (!latestCommit) {
throw new NotFoundError({ message: "Latest commit not found" });
}
let diffs;
if (deepRollback) {
diffs = await folderCommitService.deepCompareFolder({
targetCommitId: targetCommit.id,
envId,
projectId
});
} else {
const folderData = await folderService.getFolderById({
actor,
actorId,
actorOrgId,
actorAuthMethod,
id: folderId
});
diffs = [
{
folderId: folderData.id,
folderName: folderData.name,
folderPath: secretPath,
changes: await folderCommitService.compareFolderStates({
targetCommitId: commitId,
currentCommitId: latestCommit.id
})
}
];
}
for (const diff of diffs) {
for (const change of diff.changes) {
// Use discriminated union type checking
if (change.type === ResourceType.SECRET) {
// TypeScript now knows this is a SecretChange
if (change.secretKey && change.secretVersion && change.secretId) {
change.versions = await secretService.getChangeVersions(
{
secretVersion: change.secretVersion,
secretId: change.secretId,
id: change.id,
isUpdate: change.isUpdate,
changeType: change.changeType
},
change.fromVersion || "1",
actorId,
actor,
actorOrgId,
actorAuthMethod,
diff.folderId
);
}
} else if (change.type === ResourceType.FOLDER) {
// TypeScript now knows this is a FolderChange
if (change.folderVersion) {
change.versions = await folderService.getFolderVersions(change, change.fromVersion || "1", change.id);
}
}
}
}
return diffs;
};
const rollbackToCommit = async ({
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId,
commitId,
folderId,
deepRollback,
message,
envId
}: {
actor: ActorType;
actorId: string;
actorOrgId: string;
actorAuthMethod: ActorAuthMethod;
projectId: string;
commitId: string;
folderId: string;
deepRollback: boolean;
message?: string;
envId: string;
}) => {
const { permission: userPermission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(userPermission).throwUnlessCan(
ProjectPermissionCommitsActions.PerformRollback,
ProjectPermissionSub.Commits
);
const latestCommit = await folderCommitService.getLatestCommit({
folderId,
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId
});
if (!latestCommit) {
throw new NotFoundError({ message: "Latest commit not found" });
}
logger.info(`PIT - Attempting to rollback folder ${folderId} from commit ${latestCommit.id} to commit ${commitId}`);
const targetCommit = await folderCommitService.getCommitById({
commitId,
actor,
actorId,
actorAuthMethod,
actorOrgId,
projectId
});
if (!targetCommit || targetCommit.folderId !== folderId || targetCommit.envId !== envId) {
throw new NotFoundError({ message: "Target commit not found" });
}
if (!latestCommit || latestCommit.envId !== envId) {
throw new NotFoundError({ message: "Latest commit not found" });
}
if (deepRollback) {
await folderCommitService.deepRollbackFolder(commitId, envId, actorId, actor, projectId, message);
return { success: true };
}
const diff = await folderCommitService.compareFolderStates({
currentCommitId: latestCommit.id,
targetCommitId: commitId
});
const response = await folderCommitService.applyFolderStateDifferences({
differences: diff,
actorInfo: {
actorType: actor,
actorId,
message: message || "Rollback to previous commit"
},
folderId,
projectId,
reconstructNewFolders: deepRollback
});
return {
success: true,
secretChangesCount: response.secretChangesCount,
folderChangesCount: response.folderChangesCount,
totalChanges: response.totalChanges
};
};
const revertCommit = async ({
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId,
commitId
}: {
actor: ActorType;
actorId: string;
actorOrgId: string;
actorAuthMethod: ActorAuthMethod;
projectId: string;
commitId: string;
}) => {
const response = await folderCommitService.revertCommitChanges({
commitId,
actor,
actorId,
actorAuthMethod,
actorOrgId,
projectId
});
return response;
};
const getFolderStateAtCommit = async ({
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId,
commitId
}: {
actor: ActorType;
actorId: string;
actorOrgId: string;
actorAuthMethod: ActorAuthMethod;
projectId: string;
commitId: string;
}) => {
const { permission: userPermission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.SecretManager
});
ForbiddenError.from(userPermission).throwUnlessCan(
ProjectPermissionCommitsActions.Read,
ProjectPermissionSub.Commits
);
const commit = await folderCommitService.getCommitById({
commitId,
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId
});
if (!commit) {
throw new NotFoundError({ message: `Commit with ID ${commitId} not found` });
}
const response = await folderCommitService.reconstructFolderState(commitId);
return response.map((item) => ({
...item,
secretVersion: item.secretVersion ? Number(item.secretVersion) : undefined,
folderVersion: item.folderVersion ? Number(item.folderVersion) : undefined
}));
};
return {
getCommitsCount,
getCommitsForFolder,
getCommitChanges,
compareCommitChanges,
rollbackToCommit,
revertCommit,
getFolderStateAtCommit
};
};
@@ -8,7 +8,7 @@ import { InternalServerError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { ActorType } from "@app/services/auth/auth-type";
import { TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
import { CommitType, TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
@@ -584,7 +584,7 @@ export const secretSnapshotServiceFactory = ({
// Secret was deleted and re-added - this is an update only if versions are different
if (deletedInfo.versionId !== addedSecret.id) {
commitChanges.push({
type: "add", // In the commit system, updates are tracked as "add" with isUpdate=true
type: CommitType.ADD, // In the commit system, updates are tracked as "add" with isUpdate=true
secretVersionId: addedSecret.id,
isUpdate: true
});
@@ -594,7 +594,7 @@ export const secretSnapshotServiceFactory = ({
} else if (deletedInfo.versionId) {
// Secret was only deleted
commitChanges.push({
type: "delete",
type: CommitType.DELETE,
secretVersionId: deletedInfo.versionId
});
}
@@ -602,7 +602,7 @@ export const secretSnapshotServiceFactory = ({
// Add remaining new secrets (not updates)
addedSecretsChanges.forEach((addedSecret) => {
commitChanges.push({
type: "add",
type: CommitType.ADD,
secretVersionId: addedSecret.id
});
});
@@ -614,7 +614,7 @@ export const secretSnapshotServiceFactory = ({
// Folder was deleted and re-added - this is an update only if versions are different
if (deletedInfo.versionId !== addedFolder.id) {
commitChanges.push({
type: "add",
type: CommitType.ADD,
folderVersionId: addedFolder.id,
isUpdate: true
});
@@ -624,7 +624,7 @@ export const secretSnapshotServiceFactory = ({
} else if (deletedInfo.versionId) {
// Folder was only deleted
commitChanges.push({
type: "delete",
type: CommitType.DELETE,
folderVersionId: deletedInfo.versionId
});
}
@@ -633,7 +633,7 @@ export const secretSnapshotServiceFactory = ({
// Add remaining new folders (not updates)
addedFoldersChanges.forEach((addedFolder) => {
commitChanges.push({
type: "add",
type: CommitType.ADD,
folderVersionId: addedFolder.id
});
});