mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 11:29:42 +00:00
PIT: general improvements and fixes
This commit is contained in:
Vendored
+2
@@ -26,6 +26,7 @@ import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-con
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { TPitServiceFactory } from "@app/ee/services/pit/pit-service";
|
||||
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
|
||||
import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service";
|
||||
import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service";
|
||||
@@ -270,6 +271,7 @@ declare module "fastify" {
|
||||
assumePrivileges: TAssumePrivilegeServiceFactory;
|
||||
githubOrgSync: TGithubOrgSyncServiceFactory;
|
||||
folderCommit: TFolderCommitServiceFactory;
|
||||
pit: TPitServiceFactory;
|
||||
};
|
||||
// this is exclusive use for middlewares in which we need to inject data
|
||||
// everywhere else access using service layer
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,6 +57,7 @@ import { oidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
||||
import { oidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
|
||||
import { permissionDALFactory } from "@app/ee/services/permission/permission-dal";
|
||||
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { pitServiceFactory } from "@app/ee/services/pit/pit-service";
|
||||
import { projectTemplateDALFactory } from "@app/ee/services/project-template/project-template-dal";
|
||||
import { projectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
|
||||
import { projectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||
@@ -1520,6 +1521,13 @@ export const registerRoutes = async (
|
||||
permissionService
|
||||
});
|
||||
|
||||
const pitService = pitServiceFactory({
|
||||
folderCommitService,
|
||||
secretService,
|
||||
folderService,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const identityOidcAuthService = identityOidcAuthServiceFactory({
|
||||
identityOidcAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
@@ -1824,6 +1832,7 @@ export const registerRoutes = async (
|
||||
certificateTemplate: certificateTemplateService,
|
||||
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||
certificateEst: certificateEstService,
|
||||
pit: pitService,
|
||||
pkiAlert: pkiAlertService,
|
||||
pkiCollection: pkiCollectionService,
|
||||
pkiSubscriber: pkiSubscriberService,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
@@ -14,32 +15,57 @@ import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
export type TFolderCommitChangesDALFactory = ReturnType<typeof folderCommitChangesDALFactory>;
|
||||
|
||||
export type CommitChangeWithCommitInfo = TFolderCommitChanges & {
|
||||
// Base type with common fields
|
||||
type BaseCommitChangeInfo = TFolderCommitChanges & {
|
||||
actorMetadata: unknown;
|
||||
actorType: string;
|
||||
message?: string | null;
|
||||
folderId: string;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
secretId?: string;
|
||||
folderChangeId?: string;
|
||||
objectType?: string;
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
// Secret-specific change
|
||||
export type SecretCommitChange = BaseCommitChangeInfo & {
|
||||
resourceType: "secret";
|
||||
secretKey: string;
|
||||
changeType: string;
|
||||
secretVersionId?: string | null;
|
||||
secretVersion: string;
|
||||
secretId: string;
|
||||
versions?: {
|
||||
secretKey?: string;
|
||||
secretComment?: string;
|
||||
secretKey: string;
|
||||
secretComment: string;
|
||||
skipMultilineEncoding?: boolean | null;
|
||||
secretReminderRepeatDays?: number | null;
|
||||
secretReminderNote?: string | null;
|
||||
metadata?: unknown;
|
||||
tags?: string[] | null;
|
||||
secretReminderRecipients?: string[] | null;
|
||||
secretValue?: string;
|
||||
secretValue: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
// Folder-specific change
|
||||
export type FolderCommitChange = BaseCommitChangeInfo & {
|
||||
resourceType: "folder";
|
||||
folderName: string;
|
||||
folderVersion: string;
|
||||
folderChangeId: string;
|
||||
versions?: {
|
||||
name?: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
// Discriminated union
|
||||
export type CommitChangeWithCommitInfo = SecretCommitChange | FolderCommitChange;
|
||||
|
||||
// Type guards
|
||||
export const isSecretCommitChange = (change: CommitChangeWithCommitInfo): change is SecretCommitChange =>
|
||||
change.resourceType === "secret";
|
||||
|
||||
export const isFolderCommitChange = (change: CommitChangeWithCommitInfo): change is FolderCommitChange =>
|
||||
change.resourceType === "folder";
|
||||
|
||||
export const folderCommitChangesDALFactory = (db: TDbClient) => {
|
||||
const folderCommitChangesOrm = ormify(db, TableName.FolderCommitChanges);
|
||||
|
||||
@@ -50,7 +76,6 @@ export const folderCommitChangesDALFactory = (db: TDbClient) => {
|
||||
): Promise<CommitChangeWithCommitInfo[]> => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())<TFolderCommitChanges>(TableName.FolderCommitChanges)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.where(buildFindFilter({ folderCommitId }, TableName.FolderCommitChanges))
|
||||
.leftJoin<TFolderCommits>(
|
||||
TableName.FolderCommit,
|
||||
@@ -91,57 +116,108 @@ export const folderCommitChangesDALFactory = (db: TDbClient) => {
|
||||
db.ref("createdAt").withSchema(TableName.FolderCommit),
|
||||
db.ref("folderId").withSchema(TableName.FolderCommit)
|
||||
);
|
||||
return docs.map((doc) => ({
|
||||
...doc,
|
||||
folderVersion: doc.folderVersion?.toString(),
|
||||
secretVersion: doc.secretVersion?.toString()
|
||||
}));
|
||||
|
||||
return docs.map((doc) => {
|
||||
// Determine if this is a secret or folder change based on populated fields
|
||||
if (doc.secretKey && doc.secretVersion && doc.secretId) {
|
||||
return {
|
||||
...doc,
|
||||
resourceType: "secret",
|
||||
secretKey: doc.secretKey,
|
||||
secretVersion: doc.secretVersion.toString(),
|
||||
secretId: doc.secretId
|
||||
} as SecretCommitChange;
|
||||
}
|
||||
return {
|
||||
...doc,
|
||||
resourceType: "folder",
|
||||
folderName: doc.folderName,
|
||||
folderVersion: doc.folderVersion.toString(),
|
||||
folderChangeId: doc.folderChangeId
|
||||
} as FolderCommitChange;
|
||||
});
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByCommitId" });
|
||||
}
|
||||
};
|
||||
|
||||
const findBySecretVersionId = async (secretVersionId: string, tx?: Knex): Promise<CommitChangeWithCommitInfo[]> => {
|
||||
const findBySecretVersionId = async (secretVersionId: string, tx?: Knex): Promise<SecretCommitChange[]> => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())<
|
||||
TFolderCommitChanges &
|
||||
Pick<TFolderCommits, "actorMetadata" | "actorType" | "message" | "createdAt" | "folderId">
|
||||
>(TableName.FolderCommitChanges)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.where(buildFindFilter({ secretVersionId }, TableName.FolderCommitChanges))
|
||||
.select(selectAllTableCols(TableName.FolderCommitChanges))
|
||||
.join(TableName.FolderCommit, `${TableName.FolderCommitChanges}.folderCommitId`, `${TableName.FolderCommit}.id`)
|
||||
.leftJoin<TSecretVersionsV2>(
|
||||
TableName.SecretVersionV2,
|
||||
`${TableName.FolderCommitChanges}.secretVersionId`,
|
||||
`${TableName.SecretVersionV2}.id`
|
||||
)
|
||||
.select(
|
||||
db.ref("actorMetadata").withSchema(TableName.FolderCommit),
|
||||
db.ref("actorType").withSchema(TableName.FolderCommit),
|
||||
db.ref("message").withSchema(TableName.FolderCommit),
|
||||
db.ref("createdAt").withSchema(TableName.FolderCommit),
|
||||
db.ref("folderId").withSchema(TableName.FolderCommit)
|
||||
db.ref("folderId").withSchema(TableName.FolderCommit),
|
||||
db.ref("key").withSchema(TableName.SecretVersionV2).as("secretKey"),
|
||||
db.ref("version").withSchema(TableName.SecretVersionV2).as("secretVersion"),
|
||||
db.ref("secretId").withSchema(TableName.SecretVersionV2)
|
||||
);
|
||||
|
||||
return docs
|
||||
.filter((doc) => doc.secretKey && doc.secretVersion && doc.secretId)
|
||||
.map(
|
||||
(doc): SecretCommitChange => ({
|
||||
...doc,
|
||||
resourceType: "secret",
|
||||
secretKey: doc.secretKey,
|
||||
secretVersion: doc.secretVersion.toString(),
|
||||
secretId: doc.secretId
|
||||
})
|
||||
);
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindBySecretVersionId" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByFolderVersionId = async (folderVersionId: string, tx?: Knex): Promise<CommitChangeWithCommitInfo[]> => {
|
||||
const findByFolderVersionId = async (folderVersionId: string, tx?: Knex): Promise<FolderCommitChange[]> => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())<
|
||||
TFolderCommitChanges &
|
||||
Pick<TFolderCommits, "actorMetadata" | "actorType" | "message" | "createdAt" | "folderId">
|
||||
>(TableName.FolderCommitChanges)
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
.where(buildFindFilter({ folderVersionId }, TableName.FolderCommitChanges))
|
||||
.select(selectAllTableCols(TableName.FolderCommitChanges))
|
||||
.join(TableName.FolderCommit, `${TableName.FolderCommitChanges}.folderCommitId`, `${TableName.FolderCommit}.id`)
|
||||
.leftJoin<TSecretFolderVersions>(
|
||||
TableName.SecretFolderVersion,
|
||||
`${TableName.FolderCommitChanges}.folderVersionId`,
|
||||
`${TableName.SecretFolderVersion}.id`
|
||||
)
|
||||
.select(
|
||||
db.ref("actorMetadata").withSchema(TableName.FolderCommit),
|
||||
db.ref("actorType").withSchema(TableName.FolderCommit),
|
||||
db.ref("message").withSchema(TableName.FolderCommit),
|
||||
db.ref("createdAt").withSchema(TableName.FolderCommit),
|
||||
db.ref("folderId").withSchema(TableName.FolderCommit)
|
||||
db.ref("folderId").withSchema(TableName.FolderCommit),
|
||||
db.ref("name").withSchema(TableName.SecretFolderVersion).as("folderName"),
|
||||
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("folderChangeId"),
|
||||
db.ref("version").withSchema(TableName.SecretFolderVersion).as("folderVersion")
|
||||
);
|
||||
|
||||
return docs
|
||||
.filter((doc) => doc.folderName && doc.folderVersion && doc.folderChangeId)
|
||||
.map(
|
||||
(doc): FolderCommitChange => ({
|
||||
...doc,
|
||||
resourceType: "folder",
|
||||
folderName: doc.folderName,
|
||||
folderVersion: doc.folderVersion!.toString(),
|
||||
folderChangeId: doc.folderChangeId
|
||||
})
|
||||
);
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByFolderVersionId" });
|
||||
}
|
||||
|
||||
@@ -474,6 +474,24 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findCommitBefore = async (
|
||||
folderId: string,
|
||||
commitId: bigint,
|
||||
tx?: Knex
|
||||
): Promise<TFolderCommits | undefined> => {
|
||||
try {
|
||||
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||
.where({ folderId })
|
||||
.where("commitId", "<", commitId.toString())
|
||||
.select(selectAllTableCols(TableName.FolderCommit))
|
||||
.orderBy("commitId", "desc")
|
||||
.first();
|
||||
return doc;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindCommitBefore" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...restOfOrm,
|
||||
findByFolderId,
|
||||
@@ -489,6 +507,7 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
findAllFolderCommitsAfter,
|
||||
findPreviousCommitTo,
|
||||
findById,
|
||||
findByFolderIdPaginated
|
||||
findByFolderIdPaginated,
|
||||
findCommitBefore
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// Base schema shared by both secret and folder changes
|
||||
const baseChangeSchema = z.object({
|
||||
id: z.string(),
|
||||
folderCommitId: z.string(),
|
||||
changeType: z.string(),
|
||||
isUpdate: z.boolean().optional(),
|
||||
createdAt: z.union([z.string(), z.date()]),
|
||||
updatedAt: z.union([z.string(), z.date()]),
|
||||
actorMetadata: z
|
||||
.union([
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string().optional()
|
||||
}),
|
||||
z.unknown()
|
||||
])
|
||||
.optional(),
|
||||
actorType: z.string(),
|
||||
message: z.string().nullable().optional(),
|
||||
folderId: z.string()
|
||||
});
|
||||
|
||||
// Secret-specific versions schema
|
||||
const secretVersionSchema = z.object({
|
||||
secretKey: z.string(),
|
||||
secretComment: z.string(),
|
||||
skipMultilineEncoding: z.boolean().nullable().optional(),
|
||||
secretReminderRepeatDays: z.number().nullable().optional(),
|
||||
tags: z.array(z.string()).nullable().optional(),
|
||||
metadata: z.unknown().nullable().optional(),
|
||||
secretReminderNote: z.string().nullable().optional(),
|
||||
secretValue: z.string()
|
||||
});
|
||||
|
||||
// Folder-specific versions schema
|
||||
const folderVersionSchema = z.object({
|
||||
name: z.string().optional()
|
||||
});
|
||||
|
||||
// Secret commit change schema
|
||||
const secretCommitChangeSchema = baseChangeSchema.extend({
|
||||
resourceType: z.literal("secret"),
|
||||
secretVersionId: z.string().optional().nullable(),
|
||||
secretKey: z.string(),
|
||||
secretVersion: z.union([z.string(), z.number()]),
|
||||
secretId: z.string(),
|
||||
versions: z.array(secretVersionSchema).optional()
|
||||
});
|
||||
|
||||
// Folder commit change schema
|
||||
const folderCommitChangeSchema = baseChangeSchema.extend({
|
||||
resourceType: z.literal("folder"),
|
||||
folderVersionId: z.string().optional().nullable(),
|
||||
folderName: z.string(),
|
||||
folderChangeId: z.string(),
|
||||
folderVersion: z.union([z.string(), z.number()]),
|
||||
versions: z.array(folderVersionSchema).optional()
|
||||
});
|
||||
|
||||
// Discriminated union for commit changes
|
||||
export const commitChangeSchema = z.discriminatedUnion("resourceType", [
|
||||
secretCommitChangeSchema,
|
||||
folderCommitChangeSchema
|
||||
]);
|
||||
|
||||
// Commit schema
|
||||
const commitSchema = z.object({
|
||||
id: z.string(),
|
||||
commitId: z.string(),
|
||||
actorMetadata: z
|
||||
.union([
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string().optional()
|
||||
}),
|
||||
z.unknown()
|
||||
])
|
||||
.optional(),
|
||||
actorType: z.string(),
|
||||
message: z.string().nullable().optional(),
|
||||
folderId: z.string(),
|
||||
envId: z.string(),
|
||||
createdAt: z.union([z.string(), z.date()]),
|
||||
updatedAt: z.union([z.string(), z.date()]),
|
||||
changes: z.array(commitChangeSchema).optional()
|
||||
});
|
||||
|
||||
// Response schema
|
||||
export const commitChangesResponseSchema = z.object({
|
||||
changes: commitSchema
|
||||
});
|
||||
|
||||
// Base resource change schema for comparison results
|
||||
const baseResourceChangeSchema = z.object({
|
||||
id: z.string(),
|
||||
versionId: z.string(),
|
||||
oldVersionId: z.string().optional(),
|
||||
changeType: z.enum(["add", "delete", "update", "create"]),
|
||||
commitId: z.union([z.string(), z.bigint()]),
|
||||
createdAt: z.union([z.string(), z.date()]).optional(),
|
||||
parentId: z.string().optional(),
|
||||
isUpdate: z.boolean().optional(),
|
||||
fromVersion: z.union([z.string(), z.number()]).optional()
|
||||
});
|
||||
|
||||
// Secret resource change schema
|
||||
const secretResourceChangeSchema = baseResourceChangeSchema.extend({
|
||||
type: z.literal("secret"),
|
||||
secretKey: z.string().optional(),
|
||||
secretVersion: z.union([z.string(), z.number()]).optional(),
|
||||
secretId: z.string().optional(),
|
||||
versions: z
|
||||
.array(
|
||||
z.object({
|
||||
secretKey: z.string().optional(),
|
||||
secretComment: z.string().optional(),
|
||||
skipMultilineEncoding: z.boolean().nullable().optional(),
|
||||
secretReminderRepeatDays: z.number().nullable().optional(),
|
||||
tags: z.array(z.string()).nullable().optional(),
|
||||
metadata: z.unknown().nullable().optional(),
|
||||
secretReminderNote: z.string().nullable().optional(),
|
||||
secretValue: z.string().optional()
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
});
|
||||
|
||||
// Folder resource change schema
|
||||
const folderResourceChangeSchema = baseResourceChangeSchema.extend({
|
||||
type: z.literal("folder"),
|
||||
folderName: z.string().optional(),
|
||||
folderVersion: z.union([z.string(), z.number()]).optional(),
|
||||
versions: z.array(folderVersionSchema).optional()
|
||||
});
|
||||
|
||||
// Discriminated union for resource changes
|
||||
export const resourceChangeSchema = z.discriminatedUnion("type", [
|
||||
secretResourceChangeSchema,
|
||||
folderResourceChangeSchema
|
||||
]);
|
||||
@@ -1,3 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/return-await */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
import { Knex } from "knex";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
@@ -9,6 +12,7 @@ import {
|
||||
ChangeType,
|
||||
CommitType,
|
||||
folderCommitServiceFactory,
|
||||
ResourceType,
|
||||
TFolderCommitServiceFactory
|
||||
} from "./folder-commit-service";
|
||||
|
||||
@@ -518,8 +522,20 @@ describe("folderCommitServiceFactory", () => {
|
||||
const actorType = ActorType.USER;
|
||||
|
||||
const differences = [
|
||||
{ type: "secret", id: "secret-1", versionId: "v1", changeType: ChangeType.CREATE, commitId: BigInt(1) },
|
||||
{ type: "folder", id: "folder-1", versionId: "v2", changeType: ChangeType.UPDATE, commitId: BigInt(1) }
|
||||
{
|
||||
type: ResourceType.SECRET,
|
||||
id: "secret-1",
|
||||
versionId: "v1",
|
||||
changeType: ChangeType.CREATE,
|
||||
commitId: BigInt(1)
|
||||
},
|
||||
{
|
||||
type: ResourceType.FOLDER,
|
||||
id: "folder-1",
|
||||
versionId: "v2",
|
||||
changeType: ChangeType.UPDATE,
|
||||
commitId: BigInt(1)
|
||||
}
|
||||
];
|
||||
|
||||
const secretVersions = {
|
||||
|
||||
@@ -38,7 +38,7 @@ export enum CommitType {
|
||||
DELETE = "delete"
|
||||
}
|
||||
|
||||
enum ResourceType {
|
||||
export enum ResourceType {
|
||||
SECRET = "secret",
|
||||
FOLDER = "folder"
|
||||
}
|
||||
@@ -68,8 +68,7 @@ type TCommitChangeDTO = {
|
||||
folderVersionId?: string;
|
||||
};
|
||||
|
||||
export type ResourceChange = {
|
||||
type: string;
|
||||
type BaseChange = {
|
||||
id: string;
|
||||
versionId: string;
|
||||
oldVersionId?: string;
|
||||
@@ -78,12 +77,14 @@ export type ResourceChange = {
|
||||
createdAt?: Date;
|
||||
parentId?: string;
|
||||
isUpdate?: boolean;
|
||||
fromVersion?: string;
|
||||
};
|
||||
|
||||
type SecretChange = {
|
||||
type: ResourceType.SECRET;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
secretId?: string;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
fromVersion?: string;
|
||||
versions?: {
|
||||
secretKey?: string;
|
||||
secretComment?: string;
|
||||
@@ -94,10 +95,20 @@ export type ResourceChange = {
|
||||
tags?: string[] | null;
|
||||
secretReminderRecipients?: string[] | null;
|
||||
secretValue?: string;
|
||||
name?: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
type FolderChange = {
|
||||
type: ResourceType.FOLDER;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
versions?: {
|
||||
name: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type ResourceChange = BaseChange & (SecretChange | FolderChange);
|
||||
|
||||
type ActorInfo = {
|
||||
actorType: string;
|
||||
actorId?: string;
|
||||
@@ -150,7 +161,7 @@ export const folderCommitServiceFactory = ({
|
||||
}: TFolderCommitServiceFactoryDep) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const checkProjectPermission = async ({
|
||||
const checkProjectCommitReadPermission = async ({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
@@ -377,7 +388,7 @@ export const folderCommitServiceFactory = ({
|
||||
targetCommitId: string;
|
||||
defaultOperation?: "create" | "update" | "delete";
|
||||
tx?: Knex;
|
||||
}) => {
|
||||
}): Promise<ResourceChange[]> => {
|
||||
const targetCommit = await folderCommitDAL.findById(targetCommitId, tx);
|
||||
if (!targetCommit) {
|
||||
throw new NotFoundError({ message: `Commit with ID ${targetCommitId} not found` });
|
||||
@@ -387,17 +398,34 @@ export const folderCommitServiceFactory = ({
|
||||
if (!currentCommitId) {
|
||||
const targetState = await reconstructFolderState(targetCommitId, tx);
|
||||
|
||||
return targetState.map((resource) => ({
|
||||
type: resource.type,
|
||||
id: resource.id,
|
||||
versionId: resource.versionId,
|
||||
changeType: defaultOperation,
|
||||
commitId: targetCommit.commitId,
|
||||
secretKey: resource.secretKey,
|
||||
secretVersion: resource.secretVersion,
|
||||
folderName: resource.folderName,
|
||||
folderVersion: resource.folderVersion
|
||||
})) as ResourceChange[];
|
||||
return targetState
|
||||
.map((resource): ResourceChange | null => {
|
||||
if (resource.type === ResourceType.SECRET) {
|
||||
return {
|
||||
type: ResourceType.SECRET,
|
||||
id: resource.id,
|
||||
versionId: resource.versionId,
|
||||
changeType: defaultOperation as ChangeType,
|
||||
commitId: targetCommit.commitId,
|
||||
secretKey: resource.secretKey,
|
||||
secretVersion: resource.secretVersion,
|
||||
secretId: resource.id
|
||||
};
|
||||
}
|
||||
if (resource.type === ResourceType.FOLDER) {
|
||||
return {
|
||||
type: ResourceType.FOLDER,
|
||||
id: resource.id,
|
||||
versionId: resource.versionId,
|
||||
changeType: defaultOperation as ChangeType,
|
||||
commitId: targetCommit.commitId,
|
||||
folderName: resource.folderName,
|
||||
folderVersion: resource.folderVersion
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((change): change is ResourceChange => !!change);
|
||||
}
|
||||
|
||||
// Original logic for when currentCommitId is provided
|
||||
@@ -452,49 +480,88 @@ export const folderCommitServiceFactory = ({
|
||||
const targetResource = targetMap[key];
|
||||
|
||||
if (!targetResource) {
|
||||
differences.push({
|
||||
type: currentResource.type,
|
||||
id: currentResource.id,
|
||||
versionId: currentResource.versionId,
|
||||
changeType: ChangeType.DELETE,
|
||||
commitId: targetCommit.commitId,
|
||||
secretKey: currentResource.secretKey,
|
||||
secretVersion: currentResource.secretVersion,
|
||||
folderName: currentResource.folderName,
|
||||
folderVersion: currentResource.folderVersion,
|
||||
fromVersion: currentResource.versionId
|
||||
});
|
||||
// Resource was deleted
|
||||
if (currentResource.type === ResourceType.SECRET) {
|
||||
differences.push({
|
||||
type: ResourceType.SECRET,
|
||||
id: currentResource.id,
|
||||
versionId: currentResource.versionId,
|
||||
changeType: ChangeType.DELETE,
|
||||
commitId: targetCommit.commitId,
|
||||
secretKey: currentResource.secretKey,
|
||||
secretVersion: currentResource.secretVersion,
|
||||
secretId: currentResource.id,
|
||||
fromVersion: currentResource.versionId
|
||||
});
|
||||
} else if (currentResource.type === ResourceType.FOLDER) {
|
||||
differences.push({
|
||||
type: ResourceType.FOLDER,
|
||||
id: currentResource.id,
|
||||
versionId: currentResource.versionId,
|
||||
changeType: ChangeType.DELETE,
|
||||
commitId: targetCommit.commitId,
|
||||
folderName: currentResource.folderName,
|
||||
folderVersion: currentResource.folderVersion,
|
||||
fromVersion: currentResource.versionId
|
||||
});
|
||||
}
|
||||
} else if (currentResource.versionId !== targetResource.versionId) {
|
||||
differences.push({
|
||||
type: targetResource.type,
|
||||
id: targetResource.id,
|
||||
versionId: targetResource.versionId,
|
||||
changeType: ChangeType.UPDATE,
|
||||
commitId: targetCommit.commitId,
|
||||
secretKey: targetResource.secretKey,
|
||||
secretVersion: targetResource.secretVersion,
|
||||
folderName: targetResource.folderName,
|
||||
folderVersion: targetResource.folderVersion,
|
||||
fromVersion: currentResource.folderVersion || currentResource.secretVersion
|
||||
});
|
||||
// Resource was updated
|
||||
if (targetResource.type === ResourceType.SECRET) {
|
||||
differences.push({
|
||||
type: ResourceType.SECRET,
|
||||
id: targetResource.id,
|
||||
versionId: targetResource.versionId,
|
||||
changeType: ChangeType.UPDATE,
|
||||
commitId: targetCommit.commitId,
|
||||
secretKey: targetResource.secretKey,
|
||||
secretVersion: targetResource.secretVersion,
|
||||
secretId: targetResource.id,
|
||||
fromVersion: currentResource.secretVersion
|
||||
});
|
||||
} else if (targetResource.type === ResourceType.FOLDER) {
|
||||
differences.push({
|
||||
type: ResourceType.FOLDER,
|
||||
id: targetResource.id,
|
||||
versionId: targetResource.versionId,
|
||||
changeType: ChangeType.UPDATE,
|
||||
commitId: targetCommit.commitId,
|
||||
folderName: targetResource.folderName,
|
||||
folderVersion: targetResource.folderVersion,
|
||||
fromVersion: currentResource.folderVersion
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Find new resources
|
||||
Object.keys(targetMap).forEach((key) => {
|
||||
if (!currentMap[key]) {
|
||||
const targetResource = targetMap[key];
|
||||
differences.push({
|
||||
type: targetResource.type,
|
||||
id: targetResource.id,
|
||||
versionId: targetResource.versionId,
|
||||
changeType: ChangeType.CREATE,
|
||||
commitId: targetCommit.commitId,
|
||||
createdAt: targetCommit.createdAt,
|
||||
secretKey: targetResource.secretKey,
|
||||
secretVersion: targetResource.secretVersion,
|
||||
folderName: targetResource.folderName,
|
||||
folderVersion: targetResource.folderVersion
|
||||
});
|
||||
if (targetResource.type === ResourceType.SECRET) {
|
||||
differences.push({
|
||||
type: ResourceType.SECRET,
|
||||
id: targetResource.id,
|
||||
versionId: targetResource.versionId,
|
||||
changeType: ChangeType.CREATE,
|
||||
commitId: targetCommit.commitId,
|
||||
createdAt: targetCommit.createdAt,
|
||||
secretKey: targetResource.secretKey,
|
||||
secretVersion: targetResource.secretVersion,
|
||||
secretId: targetResource.id
|
||||
});
|
||||
} else if (targetResource.type === ResourceType.FOLDER) {
|
||||
differences.push({
|
||||
type: ResourceType.FOLDER,
|
||||
id: targetResource.id,
|
||||
versionId: targetResource.versionId,
|
||||
changeType: ChangeType.CREATE,
|
||||
commitId: targetCommit.commitId,
|
||||
createdAt: targetCommit.createdAt,
|
||||
folderName: targetResource.folderName,
|
||||
folderVersion: targetResource.folderVersion
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -640,13 +707,18 @@ export const folderCommitServiceFactory = ({
|
||||
) => {
|
||||
const commitChanges = [];
|
||||
|
||||
// Filter only secret changes using discriminated union
|
||||
const secretChanges = changes.filter(
|
||||
(change): change is ResourceChange & SecretChange => change.type === ResourceType.SECRET
|
||||
);
|
||||
|
||||
// Collect all secretIds for batch lookup
|
||||
const secretIds = changes.map((change) => secretVersions[change.id]?.secretId).filter(Boolean);
|
||||
const secretIds = secretChanges.map((change) => secretVersions[change.id]?.secretId).filter(Boolean);
|
||||
|
||||
// Fetch all latest versions in one call
|
||||
const latestVersionsMap = await secretVersionV2BridgeDAL.findLatestVersionMany(folderId, secretIds, tx);
|
||||
|
||||
for (const change of changes) {
|
||||
for (const change of secretChanges) {
|
||||
const secretVersion = secretVersions[change.id];
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!secretVersion) continue;
|
||||
@@ -800,7 +872,12 @@ export const folderCommitServiceFactory = ({
|
||||
) => {
|
||||
const commitChanges = [];
|
||||
|
||||
for (const change of changes) {
|
||||
// Filter only folder changes using discriminated union
|
||||
const folderChanges = changes.filter(
|
||||
(change): change is ResourceChange & FolderChange => change.type === ResourceType.FOLDER
|
||||
);
|
||||
|
||||
for (const change of folderChanges) {
|
||||
const folderVersion = folderVersions[change.id];
|
||||
|
||||
switch (change.changeType) {
|
||||
@@ -913,9 +990,14 @@ export const folderCommitServiceFactory = ({
|
||||
|
||||
return commitChanges;
|
||||
};
|
||||
// Group differences by type for more efficient processing
|
||||
const secretChanges = differences.filter((diff) => diff.type === ResourceType.SECRET);
|
||||
const folderChanges = differences.filter((diff) => diff.type === ResourceType.FOLDER);
|
||||
|
||||
// Group differences by type for more efficient processing using discriminated unions
|
||||
const secretChanges = differences.filter(
|
||||
(diff): diff is ResourceChange & SecretChange => diff.type === ResourceType.SECRET
|
||||
);
|
||||
const folderChanges = differences.filter(
|
||||
(diff): diff is ResourceChange & FolderChange => diff.type === ResourceType.FOLDER
|
||||
);
|
||||
|
||||
// Batch fetch necessary data
|
||||
const secretVersions = await secretVersionV2BridgeDAL.findByIdsWithLatestVersion(
|
||||
@@ -933,8 +1015,8 @@ export const folderCommitServiceFactory = ({
|
||||
|
||||
// Process changes in parallel
|
||||
const [secretCommitChanges, folderCommitChanges] = await Promise.all([
|
||||
processSecretChanges(secretChanges, secretVersions, actorInfo, folderId, tx),
|
||||
processFolderChanges(folderChanges, folderVersions)
|
||||
processSecretChanges(differences, secretVersions, actorInfo, folderId, tx),
|
||||
processFolderChanges(differences, folderVersions)
|
||||
]);
|
||||
|
||||
// Combine all changes
|
||||
@@ -988,8 +1070,31 @@ export const folderCommitServiceFactory = ({
|
||||
/**
|
||||
* Retrieve a commit by ID
|
||||
*/
|
||||
const getCommitById = async (id: string, tx?: Knex) => {
|
||||
return folderCommitDAL.findById(id, tx);
|
||||
const getCommitById = async ({
|
||||
commitId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId,
|
||||
tx
|
||||
}: {
|
||||
commitId: string;
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
projectId: string;
|
||||
tx?: Knex;
|
||||
}) => {
|
||||
await checkProjectCommitReadPermission({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId
|
||||
});
|
||||
return folderCommitDAL.findById(commitId, tx);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1024,7 +1129,7 @@ export const folderCommitServiceFactory = ({
|
||||
search?: string;
|
||||
sort: "asc" | "desc";
|
||||
}) => {
|
||||
await checkProjectPermission({
|
||||
await checkProjectCommitReadPermission({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
@@ -1063,7 +1168,7 @@ export const folderCommitServiceFactory = ({
|
||||
environment: string;
|
||||
path: string;
|
||||
}) => {
|
||||
await checkProjectPermission({
|
||||
await checkProjectCommitReadPermission({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
@@ -1099,7 +1204,7 @@ export const folderCommitServiceFactory = ({
|
||||
projectId: string;
|
||||
commitId: string;
|
||||
}) => {
|
||||
await checkProjectPermission({
|
||||
await checkProjectCommitReadPermission({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
@@ -1382,7 +1487,8 @@ export const folderCommitServiceFactory = ({
|
||||
envId: string,
|
||||
actorId: string,
|
||||
actorType: ActorType,
|
||||
projectId: string
|
||||
projectId: string,
|
||||
message?: string
|
||||
) => {
|
||||
await folderCommitDAL.transaction(async (tx) => {
|
||||
const targetCommit = await folderCommitDAL.findById(targetCommitId, tx);
|
||||
@@ -1480,7 +1586,7 @@ export const folderCommitServiceFactory = ({
|
||||
actorInfo: {
|
||||
actorType,
|
||||
actorId,
|
||||
message: "Deep rollback"
|
||||
message: message || "Deep rollback"
|
||||
},
|
||||
folderId: folder.id,
|
||||
projectId,
|
||||
@@ -1508,7 +1614,7 @@ export const folderCommitServiceFactory = ({
|
||||
actorOrgId: string;
|
||||
projectId: string;
|
||||
}) => {
|
||||
await checkProjectPermission({
|
||||
await checkProjectCommitReadPermission({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
@@ -1555,7 +1661,7 @@ export const folderCommitServiceFactory = ({
|
||||
ProjectPermissionSub.Commits
|
||||
);
|
||||
// Check permissions first
|
||||
await checkProjectPermission({
|
||||
await checkProjectCommitReadPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
@@ -1569,32 +1675,12 @@ export const folderCommitServiceFactory = ({
|
||||
throw new NotFoundError({ message: `Commit with ID ${commitId} not found` });
|
||||
}
|
||||
|
||||
// Get all commits for this folder
|
||||
const allCommits = await folderCommitDAL.findByFolderId(commitToRevert.folderId);
|
||||
if (!allCommits || allCommits.length === 0) {
|
||||
throw new NotFoundError({ message: `No commits found for folder ${commitToRevert.folderId}` });
|
||||
}
|
||||
const previousCommit = await folderCommitDAL.findCommitBefore(commitToRevert.folderId, commitToRevert.commitId);
|
||||
|
||||
// Sort commits by commitId (which appears to be numeric)
|
||||
const sortedCommits = allCommits.sort((a, b) => {
|
||||
if (a.commitId < b.commitId) return -1;
|
||||
if (a.commitId > b.commitId) return 1;
|
||||
return 0;
|
||||
});
|
||||
// Find the index of the commit to revert
|
||||
const commitIndex = sortedCommits.findIndex((c) => c.id === commitId);
|
||||
if (commitIndex === -1) {
|
||||
throw new NotFoundError({ message: `Commit ${commitId} not found in the commit history` });
|
||||
}
|
||||
|
||||
// If it's the first commit, we can't revert it (nothing before it)
|
||||
if (commitIndex === 0) {
|
||||
if (!previousCommit) {
|
||||
throw new BadRequestError({ message: "Cannot revert the first commit" });
|
||||
}
|
||||
|
||||
// Get the commit just before the one we want to revert
|
||||
const previousCommit = sortedCommits[commitIndex - 1];
|
||||
|
||||
// Calculate the changes needed to go from current commit back to the previous one
|
||||
const inverseChanges = await compareFolderStates({
|
||||
currentCommitId: commitToRevert.id,
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const baseChangeSchema = z.object({
|
||||
id: z.string(),
|
||||
folderCommitId: z.string(),
|
||||
changeType: z.string(),
|
||||
isUpdate: z.boolean().optional(),
|
||||
createdAt: z.union([z.string(), z.date()]),
|
||||
updatedAt: z.union([z.string(), z.date()]),
|
||||
actorMetadata: z
|
||||
.union([
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string().optional()
|
||||
}),
|
||||
z.unknown()
|
||||
])
|
||||
.optional(),
|
||||
actorType: z.string().optional(),
|
||||
message: z.string().optional().nullable(),
|
||||
folderId: z.string().optional()
|
||||
});
|
||||
|
||||
const commitChangeSchema = baseChangeSchema.extend({
|
||||
secretVersionId: z.string().optional().nullable(),
|
||||
folderVersionId: z.string().optional().nullable(),
|
||||
folderName: z.string().optional().nullable(),
|
||||
folderChangeId: z.string().optional().nullable(),
|
||||
secretKey: z.string().optional().nullable(),
|
||||
secretVersion: z.union([z.string(), z.number()]).optional().nullable(),
|
||||
secretId: z.string().optional().nullable(),
|
||||
folderVersion: z.union([z.string(), z.number()]).optional().nullable(),
|
||||
versions: z
|
||||
.array(
|
||||
z.object({
|
||||
secretKey: z.string().optional().nullable(),
|
||||
secretComment: z.string().optional().nullable(),
|
||||
skipMultilineEncoding: z.boolean().optional().nullable(),
|
||||
secretReminderRepeatDays: z.number().optional().nullable(),
|
||||
secretReminderNote: z.string().optional().nullable(),
|
||||
metadata: z.unknown().optional().nullable(),
|
||||
tags: z.array(z.string()).optional().nullable(),
|
||||
secretReminderRecipients: z.array(z.any()).optional().nullable(),
|
||||
secretValue: z.string().optional().nullable(),
|
||||
name: z.string().optional().nullable()
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
});
|
||||
|
||||
const commitSchema = z.object({
|
||||
id: z.string(),
|
||||
commitId: z.string(),
|
||||
actorMetadata: z
|
||||
.union([
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string().optional()
|
||||
}),
|
||||
z.unknown()
|
||||
])
|
||||
.optional(),
|
||||
actorType: z.string(),
|
||||
message: z.string().nullable().optional(),
|
||||
folderId: z.string(),
|
||||
envId: z.string(),
|
||||
createdAt: z.union([z.string(), z.date()]),
|
||||
updatedAt: z.union([z.string(), z.date()]),
|
||||
changes: z.array(commitChangeSchema).optional()
|
||||
});
|
||||
|
||||
export const commitChangesResponseSchema = z.object({
|
||||
changes: commitSchema
|
||||
});
|
||||
@@ -10,7 +10,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
|
||||
import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns";
|
||||
|
||||
import { CommitType, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||
import { ChangeType, CommitType, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TSecretFolderDALFactory } from "./secret-folder-dal";
|
||||
@@ -783,6 +783,27 @@ export const secretFolderServiceFactory = ({
|
||||
return versions;
|
||||
};
|
||||
|
||||
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 getFolderVersionsByIds({
|
||||
folderId,
|
||||
folderVersions:
|
||||
change.isUpdate || change.changeType === ChangeType.UPDATE ? [currentVersion, fromVersion] : [currentVersion]
|
||||
});
|
||||
return versions.map((v) => ({
|
||||
name: v.name
|
||||
}));
|
||||
};
|
||||
|
||||
return {
|
||||
createFolder,
|
||||
updateFolder,
|
||||
@@ -794,6 +815,7 @@ export const secretFolderServiceFactory = ({
|
||||
getFoldersMultiEnv,
|
||||
getFoldersDeepByEnvs,
|
||||
getProjectEnvironmentsFolders,
|
||||
getFolderVersionsByIds
|
||||
getFolderVersionsByIds,
|
||||
getFolderVersions
|
||||
};
|
||||
};
|
||||
|
||||
@@ -128,6 +128,7 @@ export const fnSecretBulkInsert = async ({
|
||||
userActorId,
|
||||
identityActorId,
|
||||
actorType,
|
||||
metadata: el.metadata ? JSON.stringify(el.metadata) : null,
|
||||
secretId: newSecretGroupedByKeyName[el.key][0].id
|
||||
})),
|
||||
tx
|
||||
@@ -273,7 +274,7 @@ export const fnSecretBulkUpdate = async ({
|
||||
userId,
|
||||
encryptedComment,
|
||||
version,
|
||||
metadata,
|
||||
metadata: metadata ? JSON.stringify(metadata) : null,
|
||||
reminderNote,
|
||||
encryptedValue,
|
||||
reminderRepeatDays,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "@app/ee/services/permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionCommitsActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSet,
|
||||
@@ -528,6 +529,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
skipMultilineEncoding: inputSecret.skipMultilineEncoding,
|
||||
key: inputSecret.newSecretName || secretName,
|
||||
tags: inputSecret.tagIds,
|
||||
metadata: JSON.stringify(secretMetadata),
|
||||
secretMetadata,
|
||||
...encryptedValue
|
||||
}
|
||||
@@ -2173,7 +2175,13 @@ export const secretV2BridgeServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits);
|
||||
|
||||
const canRead =
|
||||
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback) ||
|
||||
permission.can(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits);
|
||||
|
||||
if (!canRead) throw new ForbiddenRequestError({ message: "You do not have permission to read secret versions" });
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: folder.projectId
|
||||
@@ -2889,7 +2897,13 @@ export const secretV2BridgeServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits);
|
||||
|
||||
const canRead =
|
||||
permission.can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback) ||
|
||||
permission.can(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits);
|
||||
|
||||
if (!canRead) throw new ForbiddenRequestError({ message: "You do not have permission to read secret versions" });
|
||||
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: folder.projectId
|
||||
|
||||
@@ -199,13 +199,13 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
||||
.leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`)
|
||||
.leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`)
|
||||
.leftJoin(
|
||||
TableName.SecretV2JnTag,
|
||||
`${TableName.SecretV2}.id`,
|
||||
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
|
||||
TableName.SecretVersionV2Tag,
|
||||
`${TableName.SecretVersionV2}.id`,
|
||||
`${TableName.SecretVersionV2Tag}.${TableName.SecretVersionV2}Id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SecretTag,
|
||||
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
||||
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
|
||||
`${TableName.SecretTag}.id`
|
||||
)
|
||||
.where((qb) => {
|
||||
|
||||
@@ -44,7 +44,8 @@ import {
|
||||
TGetSecretsRawByFolderMappingsDTO
|
||||
} from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
import { ChangeType } from "../folder-commit/folder-commit-service";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
@@ -3303,6 +3304,51 @@ export const secretServiceFactory = ({
|
||||
return secrets;
|
||||
};
|
||||
|
||||
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
|
||||
) => {
|
||||
const currentVersion = change.secretVersion;
|
||||
const secretId = change.secretId ? change.secretId : change.id;
|
||||
if (!secretId) {
|
||||
return;
|
||||
}
|
||||
const versions = await 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,
|
||||
tags: v.tags?.map((tag) => tag.slug),
|
||||
metadata: v.metadata,
|
||||
secretReminderNote: v.secretReminderNote,
|
||||
secretValue: v.secretValue
|
||||
}));
|
||||
};
|
||||
|
||||
return {
|
||||
attachTags,
|
||||
detachTags,
|
||||
@@ -3334,6 +3380,7 @@ export const secretServiceFactory = ({
|
||||
getSecretAccessList,
|
||||
getSecretByIdRaw,
|
||||
getAccessibleSecrets,
|
||||
getSecretVersionsV2ByIds
|
||||
getSecretVersionsV2ByIds,
|
||||
getChangeVersions
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user