mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-14 16:40:30 +00:00
PIT: UI views
This commit is contained in:
@@ -29,6 +29,7 @@ export async function up(knex: Knex): Promise<void> {
|
||||
t.uuid("folderCommitId").notNullable();
|
||||
t.foreign("folderCommitId").references("id").inTable(TableName.FolderCommit).onDelete("CASCADE");
|
||||
t.string("changeType").notNullable();
|
||||
t.boolean("isUpdate").notNullable().defaultTo(false);
|
||||
t.uuid("secretVersionId");
|
||||
t.foreign("secretVersionId").references("id").inTable(TableName.SecretVersionV2).onDelete("CASCADE");
|
||||
t.uuid("folderVersionId");
|
||||
|
||||
@@ -11,6 +11,7 @@ export const FolderCommitChangesSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
folderCommitId: z.string().uuid(),
|
||||
changeType: z.string(),
|
||||
isUpdate: z.boolean().default(false),
|
||||
secretVersionId: z.string().uuid().nullable().optional(),
|
||||
folderVersionId: z.string().uuid().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
|
||||
@@ -1,144 +1,612 @@
|
||||
/* eslint-disable @typescript-eslint/no-base-to-string */
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
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 { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ChangeType } from "@app/services/folder-commit/folder-commit-service";
|
||||
|
||||
const commitHistoryItemSchema = z.object({
|
||||
id: z.string(),
|
||||
folderId: z.string(),
|
||||
actorType: z.string(),
|
||||
actorMetadata: z.unknown().optional(),
|
||||
message: z.string().optional().nullable(),
|
||||
commitId: z.string(),
|
||||
createdAt: z.string().or(z.date()),
|
||||
envId: z.string()
|
||||
});
|
||||
|
||||
const versionSchema = z.object({
|
||||
secretKey: z.string().optional(),
|
||||
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()
|
||||
});
|
||||
|
||||
const folderStateSchema = z.array(
|
||||
z.object({
|
||||
type: z.string(),
|
||||
id: z.string(),
|
||||
versionId: z.string(),
|
||||
secretKey: z.string().optional(),
|
||||
secretVersion: z.number().optional(),
|
||||
folderName: z.string().optional(),
|
||||
folderVersion: z.number().optional()
|
||||
})
|
||||
);
|
||||
|
||||
export const registerPITRouter = async (server: FastifyZodProvider) => {
|
||||
// Get all commits for a folder
|
||||
// Get commits count for a folder
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/commits/:folderId",
|
||||
url: "/commits/count/:workspaceId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
folderId: z.string().trim()
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
environment: z.string().trim(),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash)
|
||||
}),
|
||||
response: {
|
||||
200: z.any()
|
||||
200: z.object({
|
||||
count: z.number(),
|
||||
folderId: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const commits = await server.services.folderCommit.getCommitsByFolderId(req.params.folderId);
|
||||
return commits;
|
||||
const res = await server.services.folderCommit.getCommitsCount({
|
||||
actor: req.permission?.type,
|
||||
actorId: req.permission?.id,
|
||||
actorOrgId: req.permission?.orgId,
|
||||
actorAuthMethod: req.permission?.authMethod,
|
||||
projectId: req.params.workspaceId,
|
||||
environment: req.query.environment,
|
||||
path: req.query.path
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.GET_PROJECT_PIT_COMMIT_COUNT,
|
||||
metadata: {
|
||||
environment: req.query.environment,
|
||||
path: req.query.path,
|
||||
commitCount: res.count.toString()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
});
|
||||
|
||||
// Get all commits for a folder
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/commits/:workspaceId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
environment: z.string().trim(),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash)
|
||||
}),
|
||||
response: {
|
||||
200: commitHistoryItemSchema.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const commits = await server.services.folderCommit.getCommitsForFolder({
|
||||
actor: req.permission?.type,
|
||||
actorId: req.permission?.id,
|
||||
actorOrgId: req.permission?.orgId,
|
||||
actorAuthMethod: req.permission?.authMethod,
|
||||
projectId: req.params.workspaceId,
|
||||
environment: req.query.environment,
|
||||
path: req.query.path
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.GET_PROJECT_PIT_COMMITS,
|
||||
metadata: {
|
||||
environment: req.query.environment,
|
||||
path: req.query.path,
|
||||
commitCount: commits.length.toString()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return commits.map((commit) => ({
|
||||
...commit,
|
||||
commitId: commit.commitId.toString()
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
// Get commit changes for a specific commit
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/commits/:commitId/changes",
|
||||
url: "/commits/:workspaceId/:commitId/changes",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
commitId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.any()
|
||||
200: z.object({
|
||||
changes: 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().optional().nullable(),
|
||||
folderId: z.string(),
|
||||
envId: z.string(),
|
||||
createdAt: z.string().or(z.date()),
|
||||
updatedAt: z.string().or(z.date()),
|
||||
changes: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
folderCommitId: z.string(),
|
||||
changeType: z.string(),
|
||||
isUpdate: z.boolean().optional(),
|
||||
secretVersionId: z.string().optional().nullable(),
|
||||
folderVersionId: z.string().optional().nullable(),
|
||||
// Fix these two fields to accept either string or Date objects
|
||||
createdAt: z.union([z.string(), z.date()]),
|
||||
updatedAt: z.union([z.string(), z.date()]),
|
||||
folderName: z.string().optional().nullable(),
|
||||
folderChangeId: z.string().optional().nullable(),
|
||||
folderVersion: z.union([z.string(), z.number()]).optional().nullable(),
|
||||
secretKey: z.string().optional().nullable(),
|
||||
secretVersion: z.union([z.string(), z.number()]).optional().nullable(),
|
||||
secretId: z.string().optional().nullable(),
|
||||
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().nullable(),
|
||||
versions: z.array(versionSchema).optional()
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const changes = await server.services.folderCommit.getCommitChanges(req.params.commitId);
|
||||
return changes;
|
||||
const changes = await server.services.folderCommit.getCommitChanges({
|
||||
actor: req.permission?.type,
|
||||
actorId: req.permission?.id,
|
||||
actorOrgId: req.permission?.orgId,
|
||||
actorAuthMethod: req.permission?.authMethod,
|
||||
projectId: req.params.workspaceId,
|
||||
commitId: req.params.commitId
|
||||
});
|
||||
for (const change of changes.changes) {
|
||||
if (change.secretVersionId) {
|
||||
const currentVersion = change.secretVersion || "1";
|
||||
const previousVersion = (Number.parseInt(currentVersion, 10) - 1).toString();
|
||||
if (change.secretId) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const versions = await server.services.secret.getSecretVersionsV2ByIds({
|
||||
actorId: req.permission?.id,
|
||||
actor: req.permission?.type,
|
||||
actorOrgId: req.permission?.orgId,
|
||||
actorAuthMethod: req.permission?.authMethod,
|
||||
secretId: change.secretId,
|
||||
secretVersions: change.isUpdate ? [currentVersion, previousVersion] : [currentVersion],
|
||||
folderId: change.folderId
|
||||
});
|
||||
change.versions = versions?.map((v) => ({
|
||||
secretKey: v.secretKey,
|
||||
secretComment: v.secretComment,
|
||||
skipMultilineEncoding: v.skipMultilineEncoding,
|
||||
secretReminderRepeatDays: v.secretReminderRepeatDays,
|
||||
secretReminderNote: v.secretReminderNote,
|
||||
metadata: v.secretMetadata,
|
||||
tags: v.tags?.map((t) => t.name),
|
||||
secretReminderRecipients: v.secretReminderRecipients?.map((r) => r.toString()),
|
||||
secretValue: v.secretValue
|
||||
}));
|
||||
}
|
||||
} else if (change.folderVersionId && change.folderChangeId) {
|
||||
const currentVersion = change.folderVersion || "1";
|
||||
const previousVersion = (Number.parseInt(currentVersion, 10) - 1).toString();
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const versions = await server.services.folder.getFolderVersionsByIds({
|
||||
folderId: change.folderChangeId,
|
||||
folderVersions: change.isUpdate ? [currentVersion, previousVersion] : [currentVersion]
|
||||
});
|
||||
change.versions = versions.map((v) => ({
|
||||
name: v.name
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.GET_PROJECT_PIT_COMMIT_CHANGES,
|
||||
metadata: {
|
||||
commitId: req.params.commitId,
|
||||
changesCount: (changes.changes?.length || 0).toString()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
changes: {
|
||||
...changes,
|
||||
commitId: changes.commitId.toString()
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Compare folder states between commits
|
||||
// Retrieve rollback changes for a commit
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/compare",
|
||||
url: "/commits/:workspaceId/:commitId/compare",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
commitId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
fromCommit: z.string().trim(),
|
||||
toCommit: z.string().trim()
|
||||
folderId: z.string().trim(),
|
||||
envId: z.string().trim(),
|
||||
deepRollback: booleanSchema.default(false),
|
||||
secretPath: z.string().trim().default("/").transform(removeTrailingSlash)
|
||||
}),
|
||||
response: {
|
||||
200: z.any()
|
||||
200: z.array(
|
||||
z.object({
|
||||
folderId: z.string(),
|
||||
folderName: z.string(),
|
||||
folderPath: z.string().optional(),
|
||||
changes: z.any()
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const diff = await server.services.folderCommit.compareFolderStates({
|
||||
currentCommitId: req.query.fromCommit,
|
||||
targetCommitId: req.query.toCommit
|
||||
const latestCommit = await server.services.folderCommit.getLatestCommit({
|
||||
folderId: req.query.folderId,
|
||||
actor: req.permission?.type,
|
||||
actorId: req.permission?.id,
|
||||
actorOrgId: req.permission?.orgId,
|
||||
actorAuthMethod: req.permission?.authMethod,
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
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,
|
||||
actorId: req.permission?.id,
|
||||
actorType: req.permission?.type,
|
||||
projectId: req.params.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) {
|
||||
const currentVersion = change.secretVersion || "1";
|
||||
const previousVersion = change.fromVersion || "1";
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const versions = await server.services.secret.getSecretVersionsV2ByIds({
|
||||
actorId: req.permission?.id,
|
||||
actor: req.permission?.type,
|
||||
actorOrgId: req.permission?.orgId,
|
||||
actorAuthMethod: req.permission?.authMethod,
|
||||
secretId: change.id,
|
||||
// if it's update add also the previous secretversionid
|
||||
secretVersions:
|
||||
change.changeType === ChangeType.UPDATE ? [currentVersion, previousVersion] : [currentVersion],
|
||||
folderId: req.query.folderId
|
||||
});
|
||||
change.versions = 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
|
||||
}));
|
||||
}
|
||||
if (change.folderVersion) {
|
||||
const currentVersion = change.folderVersion || "1";
|
||||
const previousVersion = change.fromVersion || "1";
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const versions = await server.services.folder.getFolderVersionsByIds({
|
||||
folderId: change.id,
|
||||
folderVersions:
|
||||
change.changeType === ChangeType.UPDATE ? [currentVersion, previousVersion] : [currentVersion]
|
||||
});
|
||||
change.versions = versions.map((v) => ({
|
||||
name: v.name
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.PIT_COMPARE_FOLDER_STATES,
|
||||
metadata: {
|
||||
targetCommitId: req.params.commitId,
|
||||
folderId: req.query.folderId,
|
||||
deepRollback: req.query.deepRollback,
|
||||
diffsCount: diffs.length.toString()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return diff;
|
||||
return diffs;
|
||||
}
|
||||
});
|
||||
|
||||
// Rollback to a previous commit
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/rollback",
|
||||
url: "/commits/:workspaceId/:commitId/rollback",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
commitId: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
fromCommit: z.string().trim(),
|
||||
toCommit: z.string().trim(),
|
||||
folderId: z.string().trim(),
|
||||
projectId: z.string().trim(),
|
||||
reconstructNewFolders: z.boolean().default(false)
|
||||
deepRollback: z.boolean().default(false),
|
||||
message: z.string().trim().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.any()
|
||||
200: z.object({
|
||||
success: z.boolean(),
|
||||
secretChangesCount: z.number().optional(),
|
||||
folderChangesCount: z.number().optional(),
|
||||
totalChanges: z.number().optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const latestCommit = await server.services.folderCommit.getLatestCommit({
|
||||
folderId: req.body.folderId,
|
||||
actor: req.permission?.type,
|
||||
actorId: req.permission?.id,
|
||||
actorOrgId: req.permission?.orgId,
|
||||
actorAuthMethod: req.permission?.authMethod,
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
if (!latestCommit) {
|
||||
throw new NotFoundError({ message: "Latest commit not found" });
|
||||
}
|
||||
|
||||
if (req.body.deepRollback) {
|
||||
await server.services.folderCommit.deepRollbackFolder(
|
||||
latestCommit.id,
|
||||
req.body.folderId,
|
||||
req.permission.id,
|
||||
req.permission.type,
|
||||
req.params.workspaceId
|
||||
);
|
||||
return { success: true };
|
||||
}
|
||||
const diff = await server.services.folderCommit.compareFolderStates({
|
||||
currentCommitId: req.body.fromCommit,
|
||||
targetCommitId: req.body.toCommit
|
||||
currentCommitId: latestCommit.id,
|
||||
targetCommitId: req.params.commitId
|
||||
});
|
||||
|
||||
const response = await server.services.folderCommit.applyFolderStateDifferences({
|
||||
differences: diff,
|
||||
actorInfo: {
|
||||
actorType: req.permission?.type || "PLATFORM",
|
||||
actorId: req.permission?.id,
|
||||
message: "Rollback to previous commit"
|
||||
actorType: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
message: req.body.message || "Rollback to previous commit"
|
||||
},
|
||||
folderId: req.body.folderId,
|
||||
projectId: req.body.projectId,
|
||||
reconstructNewFolders: req.body.reconstructNewFolders
|
||||
projectId: req.params.workspaceId,
|
||||
reconstructNewFolders: req.body.deepRollback
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.PIT_ROLLBACK_COMMIT,
|
||||
metadata: {
|
||||
targetCommitId: req.params.commitId,
|
||||
folderId: req.body.folderId,
|
||||
deepRollback: req.body.deepRollback,
|
||||
message: req.body.message || "Rollback to previous commit",
|
||||
totalChanges: response.totalChanges.toString()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
secretChangesCount: response.secretChangesCount,
|
||||
folderChangesCount: response.folderChangesCount,
|
||||
totalChanges: response.totalChanges
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Revert commit
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/commits/:workspaceId/:commitId/revert",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
commitId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
success: z.boolean(),
|
||||
message: z.string(),
|
||||
originalCommitId: z.string(),
|
||||
revertCommitId: z.string().optional(),
|
||||
changesReverted: z.number().optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const response = await server.services.folderCommit.revertCommitChanges({
|
||||
commitId: req.params.commitId,
|
||||
actor: req.permission?.type,
|
||||
actorId: req.permission?.id,
|
||||
actorAuthMethod: req.permission?.authMethod,
|
||||
actorOrgId: req.permission?.orgId,
|
||||
projectId: req.params.workspaceId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.PIT_REVERT_COMMIT,
|
||||
metadata: {
|
||||
commitId: req.params.commitId,
|
||||
revertCommitId: response.revertCommitId,
|
||||
changesReverted: response.changesReverted?.toString()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
});
|
||||
|
||||
// Deep rollback to a specific commit
|
||||
// Folder state at commit
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/deep-rollback",
|
||||
method: "GET",
|
||||
url: "/commits/:workspaceId/:commitId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
commitId: z.string().trim(),
|
||||
envId: z.string().trim(),
|
||||
projectId: z.string().trim()
|
||||
params: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
commitId: z.string().trim()
|
||||
}),
|
||||
querystring: z.object({
|
||||
folderId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.any()
|
||||
200: folderStateSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
await server.services.folderCommit.deepRollbackFolder(
|
||||
req.body.commitId,
|
||||
req.body.envId,
|
||||
req.permission?.id || "PLATFORM",
|
||||
req.permission?.type || "PLATFORM",
|
||||
req.body.projectId
|
||||
);
|
||||
const response = await server.services.folderCommit.reconstructFolderState(req.params.commitId);
|
||||
|
||||
return { success: true };
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.params.workspaceId,
|
||||
event: {
|
||||
type: EventType.PIT_GET_FOLDER_STATE,
|
||||
metadata: {
|
||||
commitId: req.params.commitId,
|
||||
folderId: req.query.folderId,
|
||||
resourceCount: response.length.toString()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return response.map((item) => ({
|
||||
...item,
|
||||
secretVersion: item.secretVersion ? Number(item.secretVersion) : undefined,
|
||||
folderVersion: item.folderVersion ? Number(item.folderVersion) : undefined
|
||||
}));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -343,7 +343,15 @@ export enum EventType {
|
||||
MICROSOFT_TEAMS_WORKFLOW_INTEGRATION_LIST = "microsoft-teams-workflow-integration-list",
|
||||
|
||||
PROJECT_ASSUME_PRIVILEGE_SESSION_START = "project-assume-privileges-session-start",
|
||||
PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end"
|
||||
PROJECT_ASSUME_PRIVILEGE_SESSION_END = "project-assume-privileges-session-end",
|
||||
|
||||
GET_PROJECT_PIT_COMMITS = "get-project-pit-commits",
|
||||
GET_PROJECT_PIT_COMMIT_CHANGES = "get-project-pit-commit-changes",
|
||||
GET_PROJECT_PIT_COMMIT_COUNT = "get-project-pit-commit-count",
|
||||
PIT_ROLLBACK_COMMIT = "pit-rollback-commit",
|
||||
PIT_REVERT_COMMIT = "pit-revert-commit",
|
||||
PIT_GET_FOLDER_STATE = "pit-get-folder-state",
|
||||
PIT_COMPARE_FOLDER_STATES = "pit-compare-folder-states"
|
||||
}
|
||||
|
||||
export const filterableSecretEvents: EventType[] = [
|
||||
@@ -2694,6 +2702,71 @@ interface MicrosoftTeamsWorkflowIntegrationUpdateEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetProjectPitCommitsEvent {
|
||||
type: EventType.GET_PROJECT_PIT_COMMITS;
|
||||
metadata: {
|
||||
commitCount: string;
|
||||
environment: string;
|
||||
path: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetProjectPitCommitChangesEvent {
|
||||
type: EventType.GET_PROJECT_PIT_COMMIT_CHANGES;
|
||||
metadata: {
|
||||
changesCount: string;
|
||||
commitId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetProjectPitCommitCountEvent {
|
||||
type: EventType.GET_PROJECT_PIT_COMMIT_COUNT;
|
||||
metadata: {
|
||||
environment: string;
|
||||
path: string;
|
||||
commitCount: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PitRollbackCommitEvent {
|
||||
type: EventType.PIT_ROLLBACK_COMMIT;
|
||||
metadata: {
|
||||
targetCommitId: string;
|
||||
folderId: string;
|
||||
deepRollback: boolean;
|
||||
message: string;
|
||||
totalChanges: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PitRevertCommitEvent {
|
||||
type: EventType.PIT_REVERT_COMMIT;
|
||||
metadata: {
|
||||
commitId: string;
|
||||
revertCommitId?: string;
|
||||
changesReverted?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PitGetFolderStateEvent {
|
||||
type: EventType.PIT_GET_FOLDER_STATE;
|
||||
metadata: {
|
||||
commitId: string;
|
||||
folderId: string;
|
||||
resourceCount: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface PitCompareFolderStatesEvent {
|
||||
type: EventType.PIT_COMPARE_FOLDER_STATES;
|
||||
metadata: {
|
||||
targetCommitId: string;
|
||||
folderId: string;
|
||||
deepRollback: boolean;
|
||||
diffsCount: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@@ -2941,4 +3014,11 @@ export type Event =
|
||||
| MicrosoftTeamsWorkflowIntegrationGetTeamsEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationGetEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationListEvent
|
||||
| MicrosoftTeamsWorkflowIntegrationUpdateEvent;
|
||||
| MicrosoftTeamsWorkflowIntegrationUpdateEvent
|
||||
| GetProjectPitCommitsEvent
|
||||
| GetProjectPitCommitChangesEvent
|
||||
| PitRollbackCommitEvent
|
||||
| GetProjectPitCommitCountEvent
|
||||
| PitRevertCommitEvent
|
||||
| PitCompareFolderStatesEvent
|
||||
| PitGetFolderStateEvent;
|
||||
|
||||
@@ -17,6 +17,11 @@ export enum ProjectPermissionActions {
|
||||
Delete = "delete"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionCommitsActions {
|
||||
Read = "read",
|
||||
PerformRollback = "perform-rollback"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretActions {
|
||||
DescribeAndReadValue = "read",
|
||||
DescribeSecret = "describeSecret",
|
||||
@@ -126,6 +131,7 @@ export enum ProjectPermissionSub {
|
||||
SecretRollback = "secret-rollback",
|
||||
SecretApproval = "secret-approval",
|
||||
SecretRotation = "secret-rotation",
|
||||
Commits = "commits",
|
||||
Identity = "identity",
|
||||
CertificateAuthorities = "certificate-authorities",
|
||||
Certificates = "certificates",
|
||||
@@ -251,7 +257,8 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Project]
|
||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
|
||||
| [ProjectPermissionCommitsActions, ProjectPermissionSub.Commits];
|
||||
|
||||
const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'";
|
||||
const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([
|
||||
@@ -557,6 +564,12 @@ const GeneralPermissionSchema = [
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionKmipActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Commits).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionCommitsActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
})
|
||||
];
|
||||
|
||||
@@ -829,6 +842,11 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSub.SecretRotation
|
||||
);
|
||||
|
||||
can(
|
||||
[ProjectPermissionCommitsActions.Read, ProjectPermissionCommitsActions.PerformRollback],
|
||||
ProjectPermissionSub.Commits
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
@@ -1011,6 +1029,11 @@ const buildMemberPermissionRules = () => {
|
||||
ProjectPermissionSub.SecretSyncs
|
||||
);
|
||||
|
||||
can(
|
||||
[ProjectPermissionCommitsActions.Read, ProjectPermissionCommitsActions.PerformRollback],
|
||||
ProjectPermissionSub.Commits
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
@@ -1046,6 +1069,7 @@ const buildViewerPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
||||
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
|
||||
can(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
@@ -351,6 +351,7 @@ export const secretRotationQueueFactory = ({
|
||||
folderId: secretVersions[0].folderId,
|
||||
changes: secretVersions.map((sv) => ({
|
||||
type: "add",
|
||||
isUpdate: true,
|
||||
secretVersionId: sv.id
|
||||
}))
|
||||
},
|
||||
|
||||
@@ -587,7 +587,8 @@ export const registerRoutes = async (
|
||||
folderCheckpointResourcesDAL,
|
||||
secretV2BridgeDAL,
|
||||
folderTreeCheckpointResourcesDAL,
|
||||
folderCommitQueueService
|
||||
folderCommitQueueService,
|
||||
permissionService
|
||||
});
|
||||
const scimService = scimServiceFactory({
|
||||
licenseService,
|
||||
|
||||
+14
-2
@@ -27,6 +27,10 @@ export const folderCheckpointResourcesDALFactory = (db: TDbClient) => {
|
||||
(TFolderCheckpointResources & {
|
||||
referencedSecretId?: string;
|
||||
referencedFolderId?: string;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
})[]
|
||||
> => {
|
||||
try {
|
||||
@@ -45,9 +49,17 @@ export const folderCheckpointResourcesDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.FolderCheckpointResources))
|
||||
.select(
|
||||
db.ref("secretId").withSchema(TableName.SecretVersionV2).as("referencedSecretId"),
|
||||
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("referencedFolderId")
|
||||
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("referencedFolderId"),
|
||||
db.ref("name").withSchema(TableName.SecretFolderVersion).as("folderName"),
|
||||
db.ref("version").withSchema(TableName.SecretFolderVersion).as("folderVersion"),
|
||||
db.ref("key").withSchema(TableName.SecretVersionV2).as("secretKey"),
|
||||
db.ref("version").withSchema(TableName.SecretVersionV2).as("secretVersion")
|
||||
);
|
||||
return docs;
|
||||
return docs.map((doc) => ({
|
||||
...doc,
|
||||
folderVersion: doc.folderVersion?.toString(),
|
||||
secretVersion: doc.secretVersion?.toString()
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByCheckpointId" });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName, TFolderCommitChanges, TFolderCommits } from "@app/db/schemas";
|
||||
import {
|
||||
TableName,
|
||||
TFolderCommitChanges,
|
||||
TFolderCommits,
|
||||
TSecretFolderVersions,
|
||||
TSecretVersionsV2
|
||||
} from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
@@ -12,17 +18,67 @@ type CommitChangeWithCommitInfo = TFolderCommitChanges & {
|
||||
actorType: string;
|
||||
message?: string | null;
|
||||
folderId: string;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
secretId?: string;
|
||||
folderChangeId?: string;
|
||||
versions?: {
|
||||
secretKey?: string;
|
||||
secretComment?: string;
|
||||
skipMultilineEncoding?: boolean | null;
|
||||
secretReminderRepeatDays?: number | null;
|
||||
secretReminderNote?: string | null;
|
||||
metadata?: unknown;
|
||||
tags?: string[] | null;
|
||||
secretReminderRecipients?: string[] | null;
|
||||
secretValue?: string;
|
||||
name?: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export const folderCommitChangesDALFactory = (db: TDbClient) => {
|
||||
const folderCommitChangesOrm = ormify(db, TableName.FolderCommitChanges);
|
||||
|
||||
const findByCommitId = async (folderCommitId: string, tx?: Knex): Promise<TFolderCommitChanges[]> => {
|
||||
const findByCommitId = async (folderCommitId: string, tx?: Knex): Promise<CommitChangeWithCommitInfo[]> => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())<TFolderCommitChanges>(TableName.FolderCommitChanges)
|
||||
.where({ folderCommitId })
|
||||
.select(selectAllTableCols(TableName.FolderCommitChanges));
|
||||
return docs;
|
||||
.leftJoin<TFolderCommits>(
|
||||
TableName.FolderCommit,
|
||||
`${TableName.FolderCommitChanges}.folderCommitId`,
|
||||
`${TableName.FolderCommit}.id`
|
||||
)
|
||||
.leftJoin<TSecretVersionsV2>(
|
||||
TableName.SecretVersionV2,
|
||||
`${TableName.FolderCommitChanges}.secretVersionId`,
|
||||
`${TableName.SecretVersionV2}.id`
|
||||
)
|
||||
.leftJoin<TSecretFolderVersions>(
|
||||
TableName.SecretFolderVersion,
|
||||
`${TableName.FolderCommitChanges}.folderVersionId`,
|
||||
`${TableName.SecretFolderVersion}.id`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.FolderCommitChanges))
|
||||
.select(
|
||||
db.ref("name").withSchema(TableName.SecretFolderVersion).as("folderName"),
|
||||
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("folderChangeId"),
|
||||
db.ref("version").withSchema(TableName.SecretFolderVersion).as("folderVersion"),
|
||||
db.ref("key").withSchema(TableName.SecretVersionV2).as("secretKey"),
|
||||
db.ref("version").withSchema(TableName.SecretVersionV2).as("secretVersion"),
|
||||
db.ref("secretId").withSchema(TableName.SecretVersionV2),
|
||||
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)
|
||||
);
|
||||
return docs.map((doc) => ({
|
||||
...doc,
|
||||
folderVersion: doc.folderVersion?.toString(),
|
||||
secretVersion: doc.secretVersion?.toString()
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByCommitId" });
|
||||
}
|
||||
|
||||
@@ -19,11 +19,36 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
|
||||
const findByFolderId = async (folderId: string, tx?: Knex): Promise<TFolderCommits[]> => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||
const trx = tx || db.replicaNode();
|
||||
|
||||
// First, get all folder commits
|
||||
const folderCommits = await trx(TableName.FolderCommit)
|
||||
.where({ folderId })
|
||||
.select(selectAllTableCols(TableName.FolderCommit))
|
||||
.select("*")
|
||||
.orderBy("createdAt", "desc");
|
||||
return docs;
|
||||
|
||||
if (folderCommits.length === 0) return [];
|
||||
|
||||
// Get all commit IDs
|
||||
const commitIds = folderCommits.map((commit) => commit.id);
|
||||
|
||||
// Then get all related changes
|
||||
const changes = await trx(TableName.FolderCommitChanges).whereIn("folderCommitId", commitIds).select("*");
|
||||
|
||||
const changesMap = changes.reduce(
|
||||
(acc, change) => {
|
||||
const { folderCommitId } = change;
|
||||
if (!acc[folderCommitId]) acc[folderCommitId] = [];
|
||||
acc[folderCommitId].push(change);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, TFolderCommitChanges[]>
|
||||
);
|
||||
|
||||
return folderCommits.map((commit) => ({
|
||||
...commit,
|
||||
changes: changesMap[commit.id] || []
|
||||
}));
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindByFolderId" });
|
||||
}
|
||||
@@ -158,7 +183,14 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
tx?: Knex
|
||||
): Promise<
|
||||
(TFolderCommits & {
|
||||
changes: (TFolderCommitChanges & { referencedSecretId?: string; referencedFolderId?: string })[];
|
||||
changes: (TFolderCommitChanges & {
|
||||
referencedSecretId?: string;
|
||||
referencedFolderId?: string;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
})[];
|
||||
})[]
|
||||
> => {
|
||||
try {
|
||||
@@ -195,7 +227,11 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.FolderCommitChanges))
|
||||
.select(
|
||||
db.ref("secretId").withSchema(TableName.SecretVersionV2).as("referencedSecretId"),
|
||||
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("referencedFolderId")
|
||||
db.ref("folderId").withSchema(TableName.SecretFolderVersion).as("referencedFolderId"),
|
||||
db.ref("name").withSchema(TableName.SecretFolderVersion).as("folderName"),
|
||||
db.ref("version").withSchema(TableName.SecretFolderVersion).as("folderVersion"),
|
||||
db.ref("key").withSchema(TableName.SecretVersionV2).as("secretKey"),
|
||||
db.ref("version").withSchema(TableName.SecretVersionV2).as("secretVersion")
|
||||
);
|
||||
|
||||
// Organize changes by commit ID
|
||||
@@ -280,6 +316,34 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findAllFolderCommitsAfter = async ({
|
||||
envId,
|
||||
startCommitId,
|
||||
tx
|
||||
}: {
|
||||
folderId?: string;
|
||||
envId?: string;
|
||||
startCommitId?: string;
|
||||
tx?: Knex;
|
||||
}): Promise<TFolderCommits[]> => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||
.where((qb) => {
|
||||
if (envId) {
|
||||
void qb.where(`${TableName.FolderCommit}.envId`, "=", envId);
|
||||
}
|
||||
if (startCommitId) {
|
||||
void qb.where("commitId", ">=", startCommitId);
|
||||
}
|
||||
})
|
||||
.select(selectAllTableCols(TableName.FolderCommit))
|
||||
.orderBy("commitId", "desc");
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "FindLatestCommitBetween" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...restOfOrm,
|
||||
findByFolderId,
|
||||
@@ -291,6 +355,7 @@ export const folderCommitDALFactory = (db: TDbClient) => {
|
||||
findLatestCommitBetween,
|
||||
findLatestEnvCommit,
|
||||
getEnvNumberOfCommitsSince,
|
||||
findLatestCommitByFolderIds
|
||||
findLatestCommitByFolderIds,
|
||||
findAllFolderCommitsAfter
|
||||
};
|
||||
};
|
||||
|
||||
@@ -113,7 +113,8 @@ describe("folderCommitServiceFactory", () => {
|
||||
create: vi.fn().mockResolvedValue({}),
|
||||
updateById: vi.fn().mockResolvedValue({}),
|
||||
find: vi.fn().mockResolvedValue([]),
|
||||
findByIdsWithLatestVersion: vi.fn().mockResolvedValue({})
|
||||
findByIdsWithLatestVersion: vi.fn().mockResolvedValue({}),
|
||||
findLatestVersionMany: vi.fn().mockResolvedValue({})
|
||||
};
|
||||
|
||||
const mockSecretV2BridgeDAL = {
|
||||
@@ -133,26 +134,44 @@ describe("folderCommitServiceFactory", () => {
|
||||
scheduleTreeCheckpoint: vi.fn().mockResolvedValue({})
|
||||
};
|
||||
|
||||
const mockPermissionService = {
|
||||
getProjectPermission: vi.fn().mockResolvedValue({})
|
||||
};
|
||||
|
||||
let folderCommitService: TFolderCommitServiceFactory;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
folderCommitService = folderCommitServiceFactory({
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
folderCommitDAL: mockFolderCommitDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
folderCommitChangesDAL: mockFolderCommitChangesDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
folderCheckpointDAL: mockFolderCheckpointDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
folderCheckpointResourcesDAL: mockFolderCheckpointResourcesDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
folderTreeCheckpointDAL: mockFolderTreeCheckpointDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
folderTreeCheckpointResourcesDAL: mockFolderTreeCheckpointResourcesDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
userDAL: mockUserDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
identityDAL: mockIdentityDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
folderDAL: mockFolderDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
folderVersionDAL: mockFolderVersionDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
secretVersionV2BridgeDAL: mockSecretVersionV2BridgeDAL,
|
||||
projectDAL: mockProjectDAL,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
secretV2BridgeDAL: mockSecretV2BridgeDAL,
|
||||
folderCommitQueueService: mockFolderCommitQueueService
|
||||
folderCommitQueueService: mockFolderCommitQueueService,
|
||||
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||
permissionService: mockPermissionService
|
||||
});
|
||||
});
|
||||
|
||||
@@ -532,13 +551,31 @@ describe("folderCommitServiceFactory", () => {
|
||||
mockSecretVersionV2BridgeDAL.findLatestVersionByFolderId.mockResolvedValue([]);
|
||||
|
||||
// These mocks need to return objects with an id field
|
||||
mockSecretVersionV2BridgeDAL.findByIdsWithLatestVersion.mockResolvedValue(secretVersions);
|
||||
mockFolderVersionDAL.findByIdsWithLatestVersion.mockResolvedValue(folderVersions);
|
||||
mockSecretVersionV2BridgeDAL.findByIdsWithLatestVersion.mockResolvedValue(Object.values(secretVersions));
|
||||
mockFolderVersionDAL.findByIdsWithLatestVersion.mockResolvedValue(Object.values(folderVersions));
|
||||
mockSecretV2BridgeDAL.insertMany.mockResolvedValue([{ id: "new-secret-1" }]);
|
||||
mockSecretVersionV2BridgeDAL.create.mockResolvedValue({ id: "new-secret-version-1" });
|
||||
mockFolderDAL.updateById.mockResolvedValue({ id: "updated-folder-1" });
|
||||
mockFolderVersionDAL.create.mockResolvedValue({ id: "new-folder-version-1" });
|
||||
mockFolderCommitDAL.create.mockResolvedValue({ id: "new-commit-id" });
|
||||
mockSecretVersionV2BridgeDAL.findLatestVersionMany.mockResolvedValue([
|
||||
{
|
||||
id: "secret-version-1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
type: "shared",
|
||||
folderId: "folder-1",
|
||||
secretId: "secret-1",
|
||||
version: 1,
|
||||
key: "SECRET_KEY",
|
||||
encryptedValue: Buffer.from("encrypted"),
|
||||
encryptedComment: Buffer.from("comment"),
|
||||
skipMultilineEncoding: false,
|
||||
userId: "user-1",
|
||||
envId: "env-1",
|
||||
metadata: {}
|
||||
}
|
||||
]);
|
||||
|
||||
// Mock transaction
|
||||
mockFolderCommitDAL.transaction.mockImplementation(<T>(callback: TransactionCallback<T>) => callback({} as Knex));
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TSecretFolders, TSecretFolderVersions, TSecretVersionsV2 } from "@app/db/schemas";
|
||||
import { ActionProjectType, TSecretFolders, TSecretFolderVersions, TSecretVersionsV2 } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionCommitsActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||
import { TFolderCheckpointDALFactory } from "../folder-checkpoint/folder-checkpoint-dal";
|
||||
import { TFolderCheckpointResourcesDALFactory } from "../folder-checkpoint-resources/folder-checkpoint-resources-dal";
|
||||
import { TFolderCommitChangesDALFactory } from "../folder-commit-changes/folder-commit-changes-dal";
|
||||
@@ -22,7 +25,6 @@ import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TFolderCommitDALFactory } from "./folder-commit-dal";
|
||||
import { TFolderCommitQueueServiceFactory } from "./folder-commit-queue";
|
||||
|
||||
// Define enums for better type safety
|
||||
export enum ChangeType {
|
||||
ADD = "add",
|
||||
DELETE = "delete",
|
||||
@@ -35,7 +37,6 @@ enum ResourceType {
|
||||
FOLDER = "folder"
|
||||
}
|
||||
|
||||
// Improved types for DTO objects
|
||||
type TCreateCommitDTO = {
|
||||
actor: {
|
||||
type: string;
|
||||
@@ -50,6 +51,7 @@ type TCreateCommitDTO = {
|
||||
type: string;
|
||||
secretVersionId?: string;
|
||||
folderVersionId?: string;
|
||||
isUpdate?: boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
@@ -69,6 +71,23 @@ export type ResourceChange = {
|
||||
commitId: number;
|
||||
createdAt?: Date;
|
||||
parentId?: string;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
fromVersion?: string;
|
||||
versions?: {
|
||||
secretKey?: string;
|
||||
secretComment?: string;
|
||||
skipMultilineEncoding?: boolean | null;
|
||||
secretReminderRepeatDays?: number | null;
|
||||
secretReminderNote?: string | null;
|
||||
metadata?: unknown;
|
||||
tags?: string[] | null;
|
||||
secretReminderRecipients?: string[] | null;
|
||||
secretValue?: string;
|
||||
name?: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
type ActorInfo = {
|
||||
@@ -84,77 +103,21 @@ type StateChangeResult = {
|
||||
};
|
||||
|
||||
type TFolderCommitServiceFactoryDep = {
|
||||
folderCommitDAL: Pick<
|
||||
TFolderCommitDALFactory,
|
||||
| "create"
|
||||
| "findById"
|
||||
| "findByFolderId"
|
||||
| "findLatestCommit"
|
||||
| "transaction"
|
||||
| "getNumberOfCommitsSince"
|
||||
| "getEnvNumberOfCommitsSince"
|
||||
| "findCommitsToRecreate"
|
||||
| "findMultipleLatestCommits"
|
||||
| "findLatestCommitBetween"
|
||||
| "findAllCommitsBetween"
|
||||
| "findLatestEnvCommit"
|
||||
| "findLatestCommitByFolderIds"
|
||||
>;
|
||||
folderCommitChangesDAL: Pick<TFolderCommitChangesDALFactory, "create" | "findByCommitId" | "insertMany">;
|
||||
folderCheckpointDAL: Pick<
|
||||
TFolderCheckpointDALFactory,
|
||||
"create" | "findByFolderId" | "findLatestByFolderId" | "findNearestCheckpoint"
|
||||
>;
|
||||
folderCheckpointResourcesDAL: Pick<TFolderCheckpointResourcesDALFactory, "insertMany" | "findByCheckpointId">;
|
||||
folderTreeCheckpointDAL: Pick<
|
||||
TFolderTreeCheckpointDALFactory,
|
||||
"create" | "findNearestCheckpoint" | "findLatestByEnvId"
|
||||
>;
|
||||
folderTreeCheckpointResourcesDAL: Pick<
|
||||
TFolderTreeCheckpointResourcesDALFactory,
|
||||
"insertMany" | "findByTreeCheckpointId"
|
||||
>;
|
||||
userDAL: Pick<TUserDALFactory, "findById">;
|
||||
identityDAL: Pick<TIdentityDALFactory, "findById">;
|
||||
folderDAL: Pick<
|
||||
TSecretFolderDALFactory,
|
||||
| "findByParentId"
|
||||
| "findByProjectId"
|
||||
| "deleteById"
|
||||
| "create"
|
||||
| "updateById"
|
||||
| "update"
|
||||
| "find"
|
||||
| "findById"
|
||||
| "findByEnvId"
|
||||
| "findFoldersByRootAndIds"
|
||||
>;
|
||||
folderVersionDAL: Pick<
|
||||
TSecretFolderVersionDALFactory,
|
||||
| "findLatestFolderVersions"
|
||||
| "findById"
|
||||
| "deleteById"
|
||||
| "create"
|
||||
| "updateById"
|
||||
| "find"
|
||||
| "findByIdsWithLatestVersion"
|
||||
>;
|
||||
secretVersionV2BridgeDAL: Pick<
|
||||
TSecretVersionV2DALFactory,
|
||||
| "findLatestVersionByFolderId"
|
||||
| "findById"
|
||||
| "deleteById"
|
||||
| "create"
|
||||
| "updateById"
|
||||
| "find"
|
||||
| "findByIdsWithLatestVersion"
|
||||
>;
|
||||
secretV2BridgeDAL: Pick<
|
||||
secretV2BridgeDal.TSecretV2BridgeDALFactory,
|
||||
"deleteById" | "create" | "updateById" | "update" | "insertMany" | "invalidateSecretCacheByProjectId"
|
||||
>;
|
||||
folderCommitDAL: TFolderCommitDALFactory;
|
||||
folderCommitChangesDAL: TFolderCommitChangesDALFactory;
|
||||
folderCheckpointDAL: TFolderCheckpointDALFactory;
|
||||
folderCheckpointResourcesDAL: TFolderCheckpointResourcesDALFactory;
|
||||
folderTreeCheckpointDAL: TFolderTreeCheckpointDALFactory;
|
||||
folderTreeCheckpointResourcesDAL: TFolderTreeCheckpointResourcesDALFactory;
|
||||
userDAL: TUserDALFactory;
|
||||
identityDAL: TIdentityDALFactory;
|
||||
folderDAL: TSecretFolderDALFactory;
|
||||
folderVersionDAL: TSecretFolderVersionDALFactory;
|
||||
secretVersionV2BridgeDAL: TSecretVersionV2DALFactory;
|
||||
secretV2BridgeDAL: secretV2BridgeDal.TSecretV2BridgeDALFactory;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
folderCommitQueueService?: Pick<TFolderCommitQueueServiceFactory, "scheduleTreeCheckpoint">;
|
||||
permissionService?: TPermissionServiceFactory;
|
||||
};
|
||||
|
||||
export const folderCommitServiceFactory = ({
|
||||
@@ -171,10 +134,39 @@ export const folderCommitServiceFactory = ({
|
||||
projectDAL,
|
||||
secretV2BridgeDAL,
|
||||
folderTreeCheckpointResourcesDAL,
|
||||
folderCommitQueueService
|
||||
folderCommitQueueService,
|
||||
permissionService
|
||||
}: TFolderCommitServiceFactoryDep) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
const checkProjectPermission = async ({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
projectId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
}) => {
|
||||
if (!permissionService) {
|
||||
throw new Error("Permission service not initialized");
|
||||
}
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches all resources within a folder
|
||||
*/
|
||||
@@ -255,7 +247,17 @@ export const folderCommitServiceFactory = ({
|
||||
const reconstructFolderState = async (
|
||||
folderCommitId: string,
|
||||
tx?: Knex
|
||||
): Promise<{ type: string; id: string; versionId: string }[]> => {
|
||||
): Promise<
|
||||
{
|
||||
type: string;
|
||||
id: string;
|
||||
versionId: string;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
}[]
|
||||
> => {
|
||||
const targetCommit = await folderCommitDAL.findById(folderCommitId, tx);
|
||||
if (!targetCommit) {
|
||||
throw new NotFoundError({ message: `Commit with ID ${folderCommitId} not found` });
|
||||
@@ -268,7 +270,18 @@ export const folderCommitServiceFactory = ({
|
||||
|
||||
const checkpointResources = await folderCheckpointResourcesDAL.findByCheckpointId(nearestCheckpoint.id, tx);
|
||||
|
||||
const folderState: Record<string, { type: string; id: string; versionId: string }> = {};
|
||||
const folderState: Record<
|
||||
string,
|
||||
{
|
||||
type: string;
|
||||
id: string;
|
||||
versionId: string;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
}
|
||||
> = {};
|
||||
|
||||
// Add all checkpoint resources to initial state
|
||||
checkpointResources.forEach((resource) => {
|
||||
@@ -276,13 +289,17 @@ export const folderCommitServiceFactory = ({
|
||||
folderState[`secret-${resource.referencedSecretId}`] = {
|
||||
type: "secret",
|
||||
id: resource.referencedSecretId,
|
||||
versionId: resource.secretVersionId
|
||||
versionId: resource.secretVersionId,
|
||||
secretKey: resource.secretKey,
|
||||
secretVersion: resource.secretVersion
|
||||
};
|
||||
} else if (resource.folderVersionId && resource.referencedFolderId) {
|
||||
folderState[`folder-${resource.referencedFolderId}`] = {
|
||||
type: "folder",
|
||||
id: resource.referencedFolderId,
|
||||
versionId: resource.folderVersionId
|
||||
versionId: resource.folderVersionId,
|
||||
folderName: resource.folderName,
|
||||
folderVersion: resource.folderVersion
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -307,7 +324,9 @@ export const folderCommitServiceFactory = ({
|
||||
folderState[key] = {
|
||||
type: "secret",
|
||||
id: change.referencedSecretId,
|
||||
versionId: change.secretVersionId
|
||||
versionId: change.secretVersionId,
|
||||
secretKey: change.secretKey,
|
||||
secretVersion: change.secretVersion
|
||||
};
|
||||
} else if (change.changeType.toLowerCase() === "delete") {
|
||||
delete folderState[key];
|
||||
@@ -319,7 +338,9 @@ export const folderCommitServiceFactory = ({
|
||||
folderState[key] = {
|
||||
type: "folder",
|
||||
id: change.referencedFolderId,
|
||||
versionId: change.folderVersionId
|
||||
versionId: change.folderVersionId,
|
||||
folderName: change.folderName,
|
||||
folderVersion: change.folderVersion
|
||||
};
|
||||
} else if (change.changeType.toLowerCase() === "delete") {
|
||||
delete folderState[key];
|
||||
@@ -356,7 +377,11 @@ export const folderCommitServiceFactory = ({
|
||||
id: resource.id,
|
||||
versionId: resource.versionId,
|
||||
changeType: "create",
|
||||
commitId: targetCommit.commitId
|
||||
commitId: targetCommit.commitId,
|
||||
secretKey: resource.secretKey,
|
||||
secretVersion: resource.secretVersion,
|
||||
folderName: resource.folderName,
|
||||
folderVersion: resource.folderVersion
|
||||
})) as ResourceChange[];
|
||||
}
|
||||
|
||||
@@ -365,8 +390,32 @@ export const folderCommitServiceFactory = ({
|
||||
const targetState = await reconstructFolderState(targetCommitId, tx);
|
||||
|
||||
// Create lookup maps for easier comparison
|
||||
const currentMap: Record<string, { type: string; id: string; versionId: string }> = {};
|
||||
const targetMap: Record<string, { type: string; id: string; versionId: string }> = {};
|
||||
const currentMap: Record<
|
||||
string,
|
||||
{
|
||||
type: string;
|
||||
id: string;
|
||||
versionId: string;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
fromVersion?: string;
|
||||
}
|
||||
> = {};
|
||||
const targetMap: Record<
|
||||
string,
|
||||
{
|
||||
type: string;
|
||||
id: string;
|
||||
versionId: string;
|
||||
secretKey?: string;
|
||||
secretVersion?: string;
|
||||
folderName?: string;
|
||||
folderVersion?: string;
|
||||
fromVersion?: string;
|
||||
}
|
||||
> = {};
|
||||
|
||||
// Build lookup maps
|
||||
currentState.forEach((resource) => {
|
||||
@@ -393,7 +442,12 @@ export const folderCommitServiceFactory = ({
|
||||
id: currentResource.id,
|
||||
versionId: currentResource.versionId,
|
||||
changeType: ChangeType.DELETE,
|
||||
commitId: targetCommit.commitId
|
||||
commitId: targetCommit.commitId,
|
||||
secretKey: currentResource.secretKey,
|
||||
secretVersion: currentResource.secretVersion,
|
||||
folderName: currentResource.folderName,
|
||||
folderVersion: currentResource.folderVersion,
|
||||
fromVersion: currentResource.versionId
|
||||
});
|
||||
} else if (currentResource.versionId !== targetResource.versionId) {
|
||||
differences.push({
|
||||
@@ -401,12 +455,16 @@ export const folderCommitServiceFactory = ({
|
||||
id: targetResource.id,
|
||||
versionId: targetResource.versionId,
|
||||
changeType: ChangeType.UPDATE,
|
||||
commitId: targetCommit.commitId
|
||||
commitId: targetCommit.commitId,
|
||||
secretKey: targetResource.secretKey,
|
||||
secretVersion: targetResource.secretVersion,
|
||||
folderName: targetResource.folderName,
|
||||
folderVersion: targetResource.folderVersion,
|
||||
fromVersion: currentResource.folderVersion || currentResource.secretVersion
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Find creates
|
||||
Object.keys(targetMap).forEach((key) => {
|
||||
if (!currentMap[key]) {
|
||||
const targetResource = targetMap[key];
|
||||
@@ -416,7 +474,11 @@ export const folderCommitServiceFactory = ({
|
||||
versionId: targetResource.versionId,
|
||||
changeType: ChangeType.CREATE,
|
||||
commitId: targetCommit.commitId,
|
||||
createdAt: targetCommit.createdAt
|
||||
createdAt: targetCommit.createdAt,
|
||||
secretKey: targetResource.secretKey,
|
||||
secretVersion: targetResource.secretVersion,
|
||||
folderName: targetResource.folderName,
|
||||
folderVersion: targetResource.folderVersion
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -485,7 +547,8 @@ export const folderCommitServiceFactory = ({
|
||||
folderCommitId: newCommit.id,
|
||||
changeType: change.type,
|
||||
secretVersionId: change.secretVersionId,
|
||||
folderVersionId: change.folderVersionId
|
||||
folderVersionId: change.folderVersionId,
|
||||
isUpdate: change.isUpdate || false
|
||||
})),
|
||||
tx
|
||||
);
|
||||
@@ -515,11 +578,21 @@ export const folderCommitServiceFactory = ({
|
||||
) => {
|
||||
const commitChanges = [];
|
||||
|
||||
// Collect all secretIds for batch lookup
|
||||
const secretIds = changes.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) {
|
||||
const secretVersion = secretVersions[change.id];
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!secretVersion) continue;
|
||||
|
||||
// Get the latest version from our batch result
|
||||
const latestVersion = latestVersionsMap[secretVersion.secretId];
|
||||
const nextVersion = latestVersion ? latestVersion.version + 1 : 1;
|
||||
|
||||
switch (change.changeType) {
|
||||
case "create":
|
||||
{
|
||||
@@ -527,7 +600,7 @@ export const folderCommitServiceFactory = ({
|
||||
{
|
||||
id: change.id,
|
||||
skipMultilineEncoding: secretVersion.skipMultilineEncoding,
|
||||
version: secretVersion.version + 1,
|
||||
version: nextVersion,
|
||||
type: secretVersion.type,
|
||||
key: secretVersion.key,
|
||||
reminderNote: secretVersion.reminderNote,
|
||||
@@ -545,7 +618,7 @@ export const folderCommitServiceFactory = ({
|
||||
{
|
||||
folderId,
|
||||
secretId: secretVersion.secretId,
|
||||
version: secretVersion.version + 1,
|
||||
version: nextVersion,
|
||||
encryptedValue: secretVersion.encryptedValue,
|
||||
key: secretVersion.key,
|
||||
encryptedComment: secretVersion.encryptedComment,
|
||||
@@ -575,7 +648,7 @@ export const folderCommitServiceFactory = ({
|
||||
change.id,
|
||||
{
|
||||
skipMultilineEncoding: secretVersion?.skipMultilineEncoding,
|
||||
version: secretVersion?.version,
|
||||
version: nextVersion,
|
||||
type: secretVersion?.type,
|
||||
key: secretVersion?.key,
|
||||
reminderNote: secretVersion?.reminderNote,
|
||||
@@ -590,7 +663,7 @@ export const folderCommitServiceFactory = ({
|
||||
|
||||
const newVersion = await secretVersionV2BridgeDAL.create(
|
||||
{
|
||||
version: secretVersion.version + 1,
|
||||
version: nextVersion,
|
||||
encryptedValue: secretVersion.encryptedValue,
|
||||
key: secretVersion.key,
|
||||
encryptedComment: secretVersion.encryptedComment,
|
||||
@@ -611,14 +684,15 @@ export const folderCommitServiceFactory = ({
|
||||
|
||||
commitChanges.push({
|
||||
type: ChangeType.ADD,
|
||||
isUpdate: true,
|
||||
secretVersionId: newVersion.id
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
// Delete case remains unchanged
|
||||
case "delete":
|
||||
await secretV2BridgeDAL.deleteById(change.id, tx);
|
||||
|
||||
commitChanges.push({
|
||||
type: ChangeType.DELETE,
|
||||
secretVersionId: change.versionId
|
||||
@@ -724,8 +798,13 @@ export const folderCommitServiceFactory = ({
|
||||
|
||||
case "update":
|
||||
if (change.versionId) {
|
||||
const versionDetails = await folderVersionDAL.findById(change.versionId, tx);
|
||||
if (versionDetails) {
|
||||
const latestVersionDetails = await folderVersionDAL.findByIdsWithLatestVersion(
|
||||
[change.versionId],
|
||||
undefined,
|
||||
tx
|
||||
);
|
||||
if (latestVersionDetails && Object.keys(latestVersionDetails).length > 0) {
|
||||
const versionDetails = latestVersionDetails[0];
|
||||
await folderDAL.updateById(
|
||||
change.id,
|
||||
{
|
||||
@@ -749,6 +828,7 @@ export const folderCommitServiceFactory = ({
|
||||
|
||||
commitChanges.push({
|
||||
type: ChangeType.ADD,
|
||||
isUpdate: true,
|
||||
folderVersionId: newFolderVersion.id
|
||||
});
|
||||
}
|
||||
@@ -855,11 +935,103 @@ export const folderCommitServiceFactory = ({
|
||||
return folderCommitDAL.findByFolderId(folderId, tx);
|
||||
};
|
||||
|
||||
const getCommitsForFolder = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId,
|
||||
environment,
|
||||
path
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
path: string;
|
||||
}) => {
|
||||
await checkProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId
|
||||
});
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
if (!folder) {
|
||||
throw new NotFoundError({
|
||||
message: `Folder not found for project ID ${projectId}, environment ${environment}, path ${path}`
|
||||
});
|
||||
}
|
||||
const folderCommits = await folderCommitDAL.findByFolderId(folder.id);
|
||||
return folderCommits;
|
||||
};
|
||||
|
||||
const getCommitsCount = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId,
|
||||
environment,
|
||||
path
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
path: string;
|
||||
}) => {
|
||||
await checkProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId
|
||||
});
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
if (!folder) {
|
||||
throw new NotFoundError({
|
||||
message: `Folder not found for project ID ${projectId}, environment ${environment}, path ${path}`
|
||||
});
|
||||
}
|
||||
const folderCommits = await folderCommitDAL.findByFolderId(folder.id);
|
||||
return { count: folderCommits.length, folderId: folder.id };
|
||||
};
|
||||
|
||||
/**
|
||||
* Get changes for a commit
|
||||
*/
|
||||
const getCommitChanges = async (commitId: string, tx?: Knex) => {
|
||||
return folderCommitChangesDAL.findByCommitId(commitId, tx);
|
||||
const getCommitChanges = async ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId,
|
||||
commitId
|
||||
}: {
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
projectId: string;
|
||||
commitId: string;
|
||||
}) => {
|
||||
await checkProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId
|
||||
});
|
||||
const changes = await folderCommitChangesDAL.findByCommitId(commitId);
|
||||
const commit = await folderCommitDAL.findById(commitId);
|
||||
return { ...commit, changes };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1021,6 +1193,100 @@ export const folderCommitServiceFactory = ({
|
||||
);
|
||||
};
|
||||
|
||||
const deepCompareFolder = async ({
|
||||
targetCommitId,
|
||||
envId,
|
||||
projectId,
|
||||
tx
|
||||
}: {
|
||||
targetCommitId: string;
|
||||
envId: string;
|
||||
actorId: string;
|
||||
actorType: ActorType;
|
||||
projectId: string;
|
||||
tx?: Knex;
|
||||
}) => {
|
||||
const targetCommit = await folderCommitDAL.findById(targetCommitId, tx);
|
||||
if (!targetCommit) {
|
||||
throw new NotFoundError({ message: `No commit found for commit ID ${targetCommitId}` });
|
||||
}
|
||||
|
||||
const checkpoint = await folderTreeCheckpointDAL.findNearestCheckpoint(targetCommitId, envId, tx);
|
||||
if (!checkpoint) {
|
||||
throw new NotFoundError({ message: `No checkpoint found for commit ID ${targetCommitId}` });
|
||||
}
|
||||
|
||||
const folderCheckpointCommits = await folderTreeCheckpointResourcesDAL.findByTreeCheckpointId(checkpoint.id, tx);
|
||||
const folderCommits = await folderCommitDAL.findAllCommitsBetween({
|
||||
envId,
|
||||
endCommitId: targetCommit.commitId.toString(),
|
||||
startCommitId: checkpoint.commitId.toString(),
|
||||
tx
|
||||
});
|
||||
|
||||
// Group commits by folderId and keep only the latest
|
||||
const folderGroups = new Map<string, { createdAt: Date; id: string }>();
|
||||
|
||||
if (folderCheckpointCommits && folderCheckpointCommits.length > 0) {
|
||||
for (const commit of folderCheckpointCommits) {
|
||||
folderGroups.set(commit.folderId, {
|
||||
createdAt: commit.createdAt,
|
||||
id: commit.folderCommitId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (folderCommits && folderCommits.length > 0) {
|
||||
for (const commit of folderCommits) {
|
||||
const { folderId, createdAt, id } = commit;
|
||||
const existingCommit = folderGroups.get(folderId);
|
||||
|
||||
if (!existingCommit || createdAt.getTime() > existingCommit.createdAt.getTime()) {
|
||||
folderGroups.set(folderId, { createdAt, id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const folderDiffs = new Map<string, ResourceChange[]>();
|
||||
|
||||
// Process each folder to determine differences
|
||||
await Promise.all(
|
||||
Array.from(folderGroups.entries()).map(async ([folderId, commit]) => {
|
||||
const latestFolderCommit = await folderCommitDAL.findLatestCommit(folderId, tx);
|
||||
if (latestFolderCommit && latestFolderCommit.id !== commit.id) {
|
||||
const diff = await compareFolderStates({
|
||||
currentCommitId: latestFolderCommit.id,
|
||||
targetCommitId: commit.id,
|
||||
tx
|
||||
});
|
||||
if (diff?.length > 0) {
|
||||
folderDiffs.set(folderId, diff);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Apply changes in hierarchical order
|
||||
const folderIds = Array.from(folderDiffs.keys());
|
||||
const folders = await folderDAL.findFoldersByRootAndIds({ rootId: targetCommit.folderId, folderIds }, tx);
|
||||
const sortedFolders = sortFoldersByHierarchy(folders);
|
||||
|
||||
const response = [];
|
||||
for (const folder of sortedFolders) {
|
||||
const diff = folderDiffs.get(folder.id);
|
||||
if (diff) {
|
||||
const folderPath = await folderDAL.findSecretPathByFolderIds(projectId, [folder.id]);
|
||||
response.push({
|
||||
folderId: folder.id,
|
||||
folderName: folder.name,
|
||||
changes: diff,
|
||||
folderPath: folderPath?.[0]?.path
|
||||
});
|
||||
}
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
/**
|
||||
* Roll back a folder tree to a specific commit
|
||||
*/
|
||||
@@ -1117,7 +1383,139 @@ export const folderCommitServiceFactory = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Return the public interface
|
||||
const getLatestCommit = async ({
|
||||
folderId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId
|
||||
}: {
|
||||
folderId: string;
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
projectId: string;
|
||||
}) => {
|
||||
await checkProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId
|
||||
});
|
||||
return folderCommitDAL.findLatestCommit(folderId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Revert changes made in a specific commit
|
||||
*/
|
||||
const revertCommitChanges = async ({
|
||||
commitId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
projectId,
|
||||
message = "Revert commit changes"
|
||||
}: {
|
||||
commitId: string;
|
||||
actor: ActorType;
|
||||
actorId: string;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
projectId: string;
|
||||
message?: string;
|
||||
}) => {
|
||||
// Check permissions first
|
||||
await checkProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
});
|
||||
|
||||
// Get the commit to revert
|
||||
const commitToRevert = await folderCommitDAL.findById(commitId);
|
||||
if (!commitToRevert) {
|
||||
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}` });
|
||||
}
|
||||
|
||||
// Sort commits by commitId (which appears to be numeric)
|
||||
const sortedCommits = allCommits.sort((a, b) => a.commitId - b.commitId);
|
||||
|
||||
// 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) {
|
||||
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,
|
||||
targetCommitId: previousCommit.id
|
||||
});
|
||||
|
||||
const latestCommit = await folderCommitDAL.findLatestCommit(commitToRevert.folderId);
|
||||
if (!latestCommit) {
|
||||
throw new NotFoundError({ message: `Latest commit not found for folder ${commitToRevert.folderId}` });
|
||||
}
|
||||
const currentState = await reconstructFolderState(latestCommit.id);
|
||||
|
||||
const filteredChanges = inverseChanges.filter(
|
||||
(change) =>
|
||||
((change.changeType === ChangeType.DELETE || change.changeType === ChangeType.UPDATE) &&
|
||||
(currentState.some((c) => c.id === change.id) || currentState.some((c) => c.id === change.id))) ||
|
||||
(change.changeType === ChangeType.CREATE &&
|
||||
(currentState.every((c) => c.id !== change.id) || currentState.every((c) => c.id !== change.id)))
|
||||
);
|
||||
|
||||
if (!filteredChanges || filteredChanges.length === 0) {
|
||||
return {
|
||||
success: true,
|
||||
message: "No changes to revert",
|
||||
originalCommitId: commitId
|
||||
};
|
||||
}
|
||||
|
||||
// Apply the changes to revert the commit
|
||||
const revertResult = await applyFolderStateDifferences({
|
||||
differences: filteredChanges,
|
||||
actorInfo: {
|
||||
actorType: actor,
|
||||
actorId,
|
||||
message: message || `Reverted changes from commit ${commitId}`
|
||||
},
|
||||
folderId: commitToRevert.folderId,
|
||||
projectId,
|
||||
reconstructNewFolders: false
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Changes reverted successfully",
|
||||
originalCommitId: commitId,
|
||||
revertCommitId: latestCommit?.id,
|
||||
changesReverted: revertResult.totalChanges
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createCommit,
|
||||
addCommitChange,
|
||||
@@ -1132,7 +1530,13 @@ export const folderCommitServiceFactory = ({
|
||||
compareFolderStates,
|
||||
applyFolderStateDifferences,
|
||||
createFolderTreeCheckpoint,
|
||||
deepRollbackFolder
|
||||
deepRollbackFolder,
|
||||
getCommitsCount,
|
||||
getLatestCommit,
|
||||
deepCompareFolder,
|
||||
reconstructFolderState,
|
||||
getCommitsForFolder,
|
||||
revertCommitChanges
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ type TSecretFolderServiceFactoryDep = {
|
||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||
folderDAL: TSecretFolderDALFactory;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs" | "find">;
|
||||
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "findLatestFolderVersions" | "create" | "insertMany">;
|
||||
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "findLatestFolderVersions" | "create" | "insertMany" | "find">;
|
||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||
};
|
||||
@@ -286,6 +286,7 @@ export const secretFolderServiceFactory = ({
|
||||
changes: [
|
||||
{
|
||||
type: "add",
|
||||
isUpdate: true,
|
||||
folderVersionId: folderVersion.id
|
||||
}
|
||||
]
|
||||
@@ -401,6 +402,7 @@ export const secretFolderServiceFactory = ({
|
||||
changes: [
|
||||
{
|
||||
type: "add",
|
||||
isUpdate: true,
|
||||
folderVersionId: folderVersion.id
|
||||
}
|
||||
]
|
||||
@@ -765,6 +767,22 @@ export const secretFolderServiceFactory = ({
|
||||
return environmentFolders;
|
||||
};
|
||||
|
||||
const getFolderVersionsByIds = async ({
|
||||
folderId,
|
||||
folderVersions
|
||||
}: {
|
||||
folderId: string;
|
||||
folderVersions: string[];
|
||||
}) => {
|
||||
const versions = await folderVersionDAL.find({
|
||||
folderId,
|
||||
$in: {
|
||||
version: folderVersions.map((v) => Number.parseInt(v, 10))
|
||||
}
|
||||
});
|
||||
return versions;
|
||||
};
|
||||
|
||||
return {
|
||||
createFolder,
|
||||
updateFolder,
|
||||
@@ -775,6 +793,7 @@ export const secretFolderServiceFactory = ({
|
||||
getProjectFolderCount,
|
||||
getFoldersMultiEnv,
|
||||
getFoldersDeepByEnvs,
|
||||
getProjectEnvironmentsFolders
|
||||
getProjectEnvironmentsFolders,
|
||||
getFolderVersionsByIds
|
||||
};
|
||||
};
|
||||
|
||||
@@ -148,7 +148,7 @@ export const fnSecretBulkInsert = async ({
|
||||
id: actor?.actorId
|
||||
}
|
||||
},
|
||||
message: "Secret Creation",
|
||||
message: "Secret Created",
|
||||
folderId,
|
||||
changes: commitChanges
|
||||
},
|
||||
@@ -290,6 +290,7 @@ export const fnSecretBulkUpdate = async ({
|
||||
.filter(({ type }) => type === SecretType.Shared)
|
||||
.map((sv) => ({
|
||||
type: "add",
|
||||
isUpdate: true,
|
||||
secretVersionId: sv.id
|
||||
}));
|
||||
if (commitChanges.length > 0) {
|
||||
@@ -301,7 +302,7 @@ export const fnSecretBulkUpdate = async ({
|
||||
id: actor?.actorId
|
||||
}
|
||||
},
|
||||
message: "Secret Update",
|
||||
message: "Secret Updated",
|
||||
folderId,
|
||||
changes: commitChanges
|
||||
},
|
||||
@@ -432,7 +433,7 @@ export const fnSecretBulkDelete = async ({
|
||||
id: actorId
|
||||
}
|
||||
},
|
||||
message: "Secret Delete",
|
||||
message: "Secret Deleted",
|
||||
folderId,
|
||||
changes: commitChanges
|
||||
},
|
||||
|
||||
@@ -2178,10 +2178,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: folder.projectId
|
||||
});
|
||||
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors(secretId, folder.projectId, {
|
||||
offset,
|
||||
limit,
|
||||
sort: [["createdAt", "desc"]]
|
||||
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors({
|
||||
secretId,
|
||||
projectId: folder.projectId,
|
||||
findOpt: {
|
||||
offset,
|
||||
limit,
|
||||
sort: [["createdAt", "desc"]]
|
||||
}
|
||||
});
|
||||
return secretVersions.map((el) => {
|
||||
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
|
||||
@@ -2856,6 +2860,73 @@ export const secretV2BridgeServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const getSecretVersionsByIds = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
secretId,
|
||||
secretVersionNumbers,
|
||||
folderId
|
||||
}: TGetSecretVersionsDTO & {
|
||||
secretVersionNumbers: string[];
|
||||
folderId: string;
|
||||
}) => {
|
||||
const folder = await folderDAL.findById(folderId);
|
||||
if (!folder) throw new NotFoundError({ message: `Folder with ID '${folderId}' not found` });
|
||||
|
||||
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]);
|
||||
|
||||
if (!folderWithPath) {
|
||||
throw new NotFoundError({ message: `Folder with ID '${folder.id}' not found` });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: folder.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId: folder.projectId
|
||||
});
|
||||
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors({
|
||||
secretId,
|
||||
projectId: folder.projectId,
|
||||
secretVersions: secretVersionNumbers
|
||||
});
|
||||
return secretVersions.map((el) => {
|
||||
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
|
||||
permission,
|
||||
ProjectPermissionSecretActions.ReadValue,
|
||||
{
|
||||
environment: folder.environment.envSlug,
|
||||
secretPath: folderWithPath.path,
|
||||
secretName: el.key,
|
||||
...(el.tags?.length && {
|
||||
secretTags: el.tags.map((tag) => tag.slug)
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
return reshapeBridgeSecret(
|
||||
folder.projectId,
|
||||
folder.environment.envSlug,
|
||||
folderWithPath.path,
|
||||
{
|
||||
...el,
|
||||
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
|
||||
},
|
||||
secretValueHidden
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
createSecret,
|
||||
deleteSecret,
|
||||
@@ -2874,6 +2945,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
getSecretReferenceTree,
|
||||
getSecretsByFolderMappings,
|
||||
getSecretById,
|
||||
getAccessibleSecrets
|
||||
getAccessibleSecrets,
|
||||
getSecretVersionsByIds
|
||||
};
|
||||
};
|
||||
|
||||
@@ -173,13 +173,21 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 completed`);
|
||||
};
|
||||
|
||||
const findVersionsBySecretIdWithActors = async (
|
||||
secretId: string,
|
||||
projectId: string,
|
||||
{ offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt<TSecretVersionsV2> = {},
|
||||
tx?: Knex
|
||||
) => {
|
||||
const findVersionsBySecretIdWithActors = async ({
|
||||
secretId,
|
||||
projectId,
|
||||
secretVersions,
|
||||
findOpt = {},
|
||||
tx
|
||||
}: {
|
||||
secretId: string;
|
||||
projectId: string;
|
||||
secretVersions?: string[];
|
||||
findOpt?: TFindOpt<TSecretVersionsV2>;
|
||||
tx?: Knex;
|
||||
}) => {
|
||||
try {
|
||||
const { offset, limit, sort = [["createdAt", "desc"]] } = findOpt;
|
||||
const query = (tx || db)(TableName.SecretVersionV2)
|
||||
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretVersionV2}.userActorId`)
|
||||
.leftJoin(
|
||||
@@ -202,10 +210,12 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
||||
.where((qb) => {
|
||||
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
|
||||
void qb.where(`${TableName.ProjectMembership}.projectId`, projectId);
|
||||
if (secretVersions?.length) void qb.whereIn(`${TableName.SecretVersionV2}.version`, secretVersions);
|
||||
})
|
||||
.orWhere((qb) => {
|
||||
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
|
||||
void qb.whereNull(`${TableName.ProjectMembership}.projectId`);
|
||||
if (secretVersions?.length) void qb.whereIn(`${TableName.SecretVersionV2}.version`, secretVersions);
|
||||
})
|
||||
.select(
|
||||
selectAllTableCols(TableName.SecretVersionV2),
|
||||
|
||||
@@ -2521,6 +2521,36 @@ export const secretServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const getSecretVersionsV2ByIds = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
secretId,
|
||||
secretVersions,
|
||||
folderId
|
||||
}: TGetSecretVersionsDTO & {
|
||||
secretVersions: string[];
|
||||
folderId: string;
|
||||
}) => {
|
||||
const secretVersionV2 = await secretV2BridgeService
|
||||
.getSecretVersionsByIds({
|
||||
actorId,
|
||||
actor,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
secretId,
|
||||
folderId,
|
||||
secretVersionNumbers: secretVersions
|
||||
})
|
||||
.catch((err) => {
|
||||
if ((err as Error).message === "BadRequest: Failed to find secret") {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
return secretVersionV2;
|
||||
};
|
||||
|
||||
const attachTags = async ({
|
||||
secretName,
|
||||
tagSlugs,
|
||||
@@ -3309,6 +3339,7 @@ export const secretServiceFactory = ({
|
||||
getSecretsRawByFolderMappings,
|
||||
getSecretAccessList,
|
||||
getSecretByIdRaw,
|
||||
getAccessibleSecrets
|
||||
getAccessibleSecrets,
|
||||
getSecretVersionsV2ByIds
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user