mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 21:26:04 +00:00
Merge pull request #3676 from Infisical/revert-3675-revert-3546-feat/point-in-time-revamp
Revert "Revert "feat(PIT): Point In Time Revamp""
This commit is contained in:
@@ -84,6 +84,11 @@ const getZodDefaultValue = (type: unknown, value: string | number | boolean | Ob
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const bigIntegerColumns: Record<string, string[]> = {
|
||||||
|
"folder_commits": ["commitId"]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const tables = (
|
const tables = (
|
||||||
await db("information_schema.tables")
|
await db("information_schema.tables")
|
||||||
@@ -108,6 +113,9 @@ const main = async () => {
|
|||||||
const columnName = columnNames[colNum];
|
const columnName = columnNames[colNum];
|
||||||
const colInfo = columns[columnName];
|
const colInfo = columns[columnName];
|
||||||
let ztype = getZodPrimitiveType(colInfo.type);
|
let ztype = getZodPrimitiveType(colInfo.type);
|
||||||
|
if (bigIntegerColumns[tableName]?.includes(columnName)) {
|
||||||
|
ztype = "z.coerce.bigint()";
|
||||||
|
}
|
||||||
if (["zodBuffer"].includes(ztype)) {
|
if (["zodBuffer"].includes(ztype)) {
|
||||||
zodImportSet.add(ztype);
|
zodImportSet.add(ztype);
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+4
@@ -26,6 +26,7 @@ import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-con
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
|
import { TOidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-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 { 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 { 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";
|
import { TRateLimitServiceFactory } from "@app/ee/services/rate-limit/rate-limit-service";
|
||||||
@@ -59,6 +60,7 @@ import { TCertificateTemplateServiceFactory } from "@app/services/certificate-te
|
|||||||
import { TCmekServiceFactory } from "@app/services/cmek/cmek-service";
|
import { TCmekServiceFactory } from "@app/services/cmek/cmek-service";
|
||||||
import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
|
import { TExternalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
|
||||||
import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
|
import { TExternalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
|
||||||
|
import { TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
||||||
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
import { TGroupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
import { THsmServiceFactory } from "@app/services/hsm/hsm-service";
|
import { THsmServiceFactory } from "@app/services/hsm/hsm-service";
|
||||||
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
@@ -276,6 +278,8 @@ declare module "fastify" {
|
|||||||
microsoftTeams: TMicrosoftTeamsServiceFactory;
|
microsoftTeams: TMicrosoftTeamsServiceFactory;
|
||||||
assumePrivileges: TAssumePrivilegeServiceFactory;
|
assumePrivileges: TAssumePrivilegeServiceFactory;
|
||||||
githubOrgSync: TGithubOrgSyncServiceFactory;
|
githubOrgSync: TGithubOrgSyncServiceFactory;
|
||||||
|
folderCommit: TFolderCommitServiceFactory;
|
||||||
|
pit: TPitServiceFactory;
|
||||||
secretScanningV2: TSecretScanningV2ServiceFactory;
|
secretScanningV2: TSecretScanningV2ServiceFactory;
|
||||||
internalCertificateAuthority: TInternalCertificateAuthorityServiceFactory;
|
internalCertificateAuthority: TInternalCertificateAuthorityServiceFactory;
|
||||||
pkiTemplate: TPkiTemplatesServiceFactory;
|
pkiTemplate: TPkiTemplatesServiceFactory;
|
||||||
|
|||||||
Vendored
+48
@@ -80,6 +80,24 @@ import {
|
|||||||
TExternalKms,
|
TExternalKms,
|
||||||
TExternalKmsInsert,
|
TExternalKmsInsert,
|
||||||
TExternalKmsUpdate,
|
TExternalKmsUpdate,
|
||||||
|
TFolderCheckpointResources,
|
||||||
|
TFolderCheckpointResourcesInsert,
|
||||||
|
TFolderCheckpointResourcesUpdate,
|
||||||
|
TFolderCheckpoints,
|
||||||
|
TFolderCheckpointsInsert,
|
||||||
|
TFolderCheckpointsUpdate,
|
||||||
|
TFolderCommitChanges,
|
||||||
|
TFolderCommitChangesInsert,
|
||||||
|
TFolderCommitChangesUpdate,
|
||||||
|
TFolderCommits,
|
||||||
|
TFolderCommitsInsert,
|
||||||
|
TFolderCommitsUpdate,
|
||||||
|
TFolderTreeCheckpointResources,
|
||||||
|
TFolderTreeCheckpointResourcesInsert,
|
||||||
|
TFolderTreeCheckpointResourcesUpdate,
|
||||||
|
TFolderTreeCheckpoints,
|
||||||
|
TFolderTreeCheckpointsInsert,
|
||||||
|
TFolderTreeCheckpointsUpdate,
|
||||||
TGateways,
|
TGateways,
|
||||||
TGatewaysInsert,
|
TGatewaysInsert,
|
||||||
TGatewaysUpdate,
|
TGatewaysUpdate,
|
||||||
@@ -1122,6 +1140,36 @@ declare module "knex/types/tables" {
|
|||||||
TGithubOrgSyncConfigsInsert,
|
TGithubOrgSyncConfigsInsert,
|
||||||
TGithubOrgSyncConfigsUpdate
|
TGithubOrgSyncConfigsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.FolderCommit]: KnexOriginal.CompositeTableType<
|
||||||
|
TFolderCommits,
|
||||||
|
TFolderCommitsInsert,
|
||||||
|
TFolderCommitsUpdate
|
||||||
|
>;
|
||||||
|
[TableName.FolderCommitChanges]: KnexOriginal.CompositeTableType<
|
||||||
|
TFolderCommitChanges,
|
||||||
|
TFolderCommitChangesInsert,
|
||||||
|
TFolderCommitChangesUpdate
|
||||||
|
>;
|
||||||
|
[TableName.FolderCheckpoint]: KnexOriginal.CompositeTableType<
|
||||||
|
TFolderCheckpoints,
|
||||||
|
TFolderCheckpointsInsert,
|
||||||
|
TFolderCheckpointsUpdate
|
||||||
|
>;
|
||||||
|
[TableName.FolderCheckpointResources]: KnexOriginal.CompositeTableType<
|
||||||
|
TFolderCheckpointResources,
|
||||||
|
TFolderCheckpointResourcesInsert,
|
||||||
|
TFolderCheckpointResourcesUpdate
|
||||||
|
>;
|
||||||
|
[TableName.FolderTreeCheckpoint]: KnexOriginal.CompositeTableType<
|
||||||
|
TFolderTreeCheckpoints,
|
||||||
|
TFolderTreeCheckpointsInsert,
|
||||||
|
TFolderTreeCheckpointsUpdate
|
||||||
|
>;
|
||||||
|
[TableName.FolderTreeCheckpointResources]: KnexOriginal.CompositeTableType<
|
||||||
|
TFolderTreeCheckpointResources,
|
||||||
|
TFolderTreeCheckpointResourcesInsert,
|
||||||
|
TFolderTreeCheckpointResourcesUpdate
|
||||||
|
>;
|
||||||
[TableName.SecretScanningDataSource]: KnexOriginal.CompositeTableType<
|
[TableName.SecretScanningDataSource]: KnexOriginal.CompositeTableType<
|
||||||
TSecretScanningDataSources,
|
TSecretScanningDataSources,
|
||||||
TSecretScanningDataSourcesInsert,
|
TSecretScanningDataSourcesInsert,
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasFolderCommitTable = await knex.schema.hasTable(TableName.FolderCommit);
|
||||||
|
if (!hasFolderCommitTable) {
|
||||||
|
await knex.schema.createTable(TableName.FolderCommit, (t) => {
|
||||||
|
t.uuid("id").primary().defaultTo(knex.fn.uuid());
|
||||||
|
t.bigIncrements("commitId");
|
||||||
|
t.jsonb("actorMetadata").notNullable();
|
||||||
|
t.string("actorType").notNullable();
|
||||||
|
t.string("message");
|
||||||
|
t.uuid("folderId").notNullable();
|
||||||
|
t.uuid("envId").notNullable();
|
||||||
|
t.foreign("envId").references("id").inTable(TableName.Environment).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
|
||||||
|
t.index("folderId");
|
||||||
|
t.index("envId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFolderCommitChangesTable = await knex.schema.hasTable(TableName.FolderCommitChanges);
|
||||||
|
if (!hasFolderCommitChangesTable) {
|
||||||
|
await knex.schema.createTable(TableName.FolderCommitChanges, (t) => {
|
||||||
|
t.uuid("id").primary().defaultTo(knex.fn.uuid());
|
||||||
|
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");
|
||||||
|
t.foreign("folderVersionId").references("id").inTable(TableName.SecretFolderVersion).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
|
||||||
|
t.index("folderCommitId");
|
||||||
|
t.index("secretVersionId");
|
||||||
|
t.index("folderVersionId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFolderCheckpointTable = await knex.schema.hasTable(TableName.FolderCheckpoint);
|
||||||
|
if (!hasFolderCheckpointTable) {
|
||||||
|
await knex.schema.createTable(TableName.FolderCheckpoint, (t) => {
|
||||||
|
t.uuid("id").primary().defaultTo(knex.fn.uuid());
|
||||||
|
t.uuid("folderCommitId").notNullable();
|
||||||
|
t.foreign("folderCommitId").references("id").inTable(TableName.FolderCommit).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
|
||||||
|
t.index("folderCommitId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFolderCheckpointResourcesTable = await knex.schema.hasTable(TableName.FolderCheckpointResources);
|
||||||
|
if (!hasFolderCheckpointResourcesTable) {
|
||||||
|
await knex.schema.createTable(TableName.FolderCheckpointResources, (t) => {
|
||||||
|
t.uuid("id").primary().defaultTo(knex.fn.uuid());
|
||||||
|
t.uuid("folderCheckpointId").notNullable();
|
||||||
|
t.foreign("folderCheckpointId").references("id").inTable(TableName.FolderCheckpoint).onDelete("CASCADE");
|
||||||
|
t.uuid("secretVersionId");
|
||||||
|
t.foreign("secretVersionId").references("id").inTable(TableName.SecretVersionV2).onDelete("CASCADE");
|
||||||
|
t.uuid("folderVersionId");
|
||||||
|
t.foreign("folderVersionId").references("id").inTable(TableName.SecretFolderVersion).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
|
||||||
|
t.index("folderCheckpointId");
|
||||||
|
t.index("secretVersionId");
|
||||||
|
t.index("folderVersionId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFolderTreeCheckpointTable = await knex.schema.hasTable(TableName.FolderTreeCheckpoint);
|
||||||
|
if (!hasFolderTreeCheckpointTable) {
|
||||||
|
await knex.schema.createTable(TableName.FolderTreeCheckpoint, (t) => {
|
||||||
|
t.uuid("id").primary().defaultTo(knex.fn.uuid());
|
||||||
|
t.uuid("folderCommitId").notNullable();
|
||||||
|
t.foreign("folderCommitId").references("id").inTable(TableName.FolderCommit).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
|
||||||
|
t.index("folderCommitId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFolderTreeCheckpointResourcesTable = await knex.schema.hasTable(TableName.FolderTreeCheckpointResources);
|
||||||
|
if (!hasFolderTreeCheckpointResourcesTable) {
|
||||||
|
await knex.schema.createTable(TableName.FolderTreeCheckpointResources, (t) => {
|
||||||
|
t.uuid("id").primary().defaultTo(knex.fn.uuid());
|
||||||
|
t.uuid("folderTreeCheckpointId").notNullable();
|
||||||
|
t.foreign("folderTreeCheckpointId").references("id").inTable(TableName.FolderTreeCheckpoint).onDelete("CASCADE");
|
||||||
|
t.uuid("folderId").notNullable();
|
||||||
|
t.uuid("folderCommitId").notNullable();
|
||||||
|
t.foreign("folderCommitId").references("id").inTable(TableName.FolderCommit).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
|
||||||
|
t.index("folderTreeCheckpointId");
|
||||||
|
t.index("folderId");
|
||||||
|
t.index("folderCommitId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasFolderCommitTable) {
|
||||||
|
await createOnUpdateTrigger(knex, TableName.FolderCommit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasFolderCommitChangesTable) {
|
||||||
|
await createOnUpdateTrigger(knex, TableName.FolderCommitChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasFolderCheckpointTable) {
|
||||||
|
await createOnUpdateTrigger(knex, TableName.FolderCheckpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasFolderCheckpointResourcesTable) {
|
||||||
|
await createOnUpdateTrigger(knex, TableName.FolderCheckpointResources);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasFolderTreeCheckpointTable) {
|
||||||
|
await createOnUpdateTrigger(knex, TableName.FolderTreeCheckpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasFolderTreeCheckpointResourcesTable) {
|
||||||
|
await createOnUpdateTrigger(knex, TableName.FolderTreeCheckpointResources);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasFolderCheckpointResourcesTable = await knex.schema.hasTable(TableName.FolderCheckpointResources);
|
||||||
|
const hasFolderTreeCheckpointResourcesTable = await knex.schema.hasTable(TableName.FolderTreeCheckpointResources);
|
||||||
|
const hasFolderCommitTable = await knex.schema.hasTable(TableName.FolderCommit);
|
||||||
|
const hasFolderCommitChangesTable = await knex.schema.hasTable(TableName.FolderCommitChanges);
|
||||||
|
const hasFolderTreeCheckpointTable = await knex.schema.hasTable(TableName.FolderTreeCheckpoint);
|
||||||
|
const hasFolderCheckpointTable = await knex.schema.hasTable(TableName.FolderCheckpoint);
|
||||||
|
|
||||||
|
if (hasFolderTreeCheckpointResourcesTable) {
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.FolderTreeCheckpointResources);
|
||||||
|
await knex.schema.dropTableIfExists(TableName.FolderTreeCheckpointResources);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasFolderCheckpointResourcesTable) {
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.FolderCheckpointResources);
|
||||||
|
await knex.schema.dropTableIfExists(TableName.FolderCheckpointResources);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasFolderTreeCheckpointTable) {
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.FolderTreeCheckpoint);
|
||||||
|
await knex.schema.dropTableIfExists(TableName.FolderTreeCheckpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasFolderCheckpointTable) {
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.FolderCheckpoint);
|
||||||
|
await knex.schema.dropTableIfExists(TableName.FolderCheckpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasFolderCommitChangesTable) {
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.FolderCommitChanges);
|
||||||
|
await knex.schema.dropTableIfExists(TableName.FolderCommitChanges);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasFolderCommitTable) {
|
||||||
|
await dropOnUpdateTrigger(knex, TableName.FolderCommit);
|
||||||
|
await knex.schema.dropTableIfExists(TableName.FolderCommit);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasColumn(TableName.SecretFolderVersion, "description"))) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretFolderVersion, (t) => {
|
||||||
|
t.string("description").nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasColumn(TableName.SecretFolderVersion, "description")) {
|
||||||
|
await knex.schema.alterTable(TableName.SecretFolderVersion, (t) => {
|
||||||
|
t.dropColumn("description");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/* eslint-disable no-await-in-loop */
|
||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { chunkArray } from "@app/lib/fn";
|
||||||
|
import { selectAllTableCols } from "@app/lib/knex";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
|
import { SecretType, TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
logger.info("Starting secret version fix migration");
|
||||||
|
|
||||||
|
// Get all shared secret IDs first to optimize versions query
|
||||||
|
const secretIds = await knex(TableName.SecretV2)
|
||||||
|
.where("type", SecretType.Shared)
|
||||||
|
.select("id")
|
||||||
|
.then((rows) => rows.map((row) => row.id));
|
||||||
|
|
||||||
|
logger.info(`Found ${secretIds.length} shared secrets to process`);
|
||||||
|
|
||||||
|
if (secretIds.length === 0) {
|
||||||
|
logger.info("No shared secrets found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretIdChunks = chunkArray(secretIds, 5000);
|
||||||
|
|
||||||
|
for (let chunkIndex = 0; chunkIndex < secretIdChunks.length; chunkIndex += 1) {
|
||||||
|
const currentSecretIds = secretIdChunks[chunkIndex];
|
||||||
|
logger.info(`Processing chunk ${chunkIndex + 1} of ${secretIdChunks.length}`);
|
||||||
|
|
||||||
|
// Get secrets and versions for current chunk
|
||||||
|
const [sharedSecrets, allVersions] = await Promise.all([
|
||||||
|
knex(TableName.SecretV2).whereIn("id", currentSecretIds).select(selectAllTableCols(TableName.SecretV2)),
|
||||||
|
knex(TableName.SecretVersionV2).whereIn("secretId", currentSecretIds).select("secretId", "version")
|
||||||
|
]);
|
||||||
|
|
||||||
|
const versionsBySecretId = new Map<string, number[]>();
|
||||||
|
|
||||||
|
allVersions.forEach((v) => {
|
||||||
|
const versions = versionsBySecretId.get(v.secretId);
|
||||||
|
if (versions) {
|
||||||
|
versions.push(v.version);
|
||||||
|
} else {
|
||||||
|
versionsBySecretId.set(v.secretId, [v.version]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const versionsToAdd = [];
|
||||||
|
const secretsToUpdate = [];
|
||||||
|
|
||||||
|
// Process each shared secret
|
||||||
|
for (const secret of sharedSecrets) {
|
||||||
|
const existingVersions = versionsBySecretId.get(secret.id) || [];
|
||||||
|
|
||||||
|
if (existingVersions.length === 0) {
|
||||||
|
// No versions exist - add current version
|
||||||
|
versionsToAdd.push({
|
||||||
|
secretId: secret.id,
|
||||||
|
version: secret.version,
|
||||||
|
key: secret.key,
|
||||||
|
encryptedValue: secret.encryptedValue,
|
||||||
|
encryptedComment: secret.encryptedComment,
|
||||||
|
reminderNote: secret.reminderNote,
|
||||||
|
reminderRepeatDays: secret.reminderRepeatDays,
|
||||||
|
skipMultilineEncoding: secret.skipMultilineEncoding,
|
||||||
|
metadata: secret.metadata,
|
||||||
|
folderId: secret.folderId,
|
||||||
|
actorType: "platform"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const latestVersion = Math.max(...existingVersions);
|
||||||
|
|
||||||
|
if (latestVersion !== secret.version) {
|
||||||
|
// Latest version doesn't match - create new version and update secret
|
||||||
|
const nextVersion = latestVersion + 1;
|
||||||
|
|
||||||
|
versionsToAdd.push({
|
||||||
|
secretId: secret.id,
|
||||||
|
version: nextVersion,
|
||||||
|
key: secret.key,
|
||||||
|
encryptedValue: secret.encryptedValue,
|
||||||
|
encryptedComment: secret.encryptedComment,
|
||||||
|
reminderNote: secret.reminderNote,
|
||||||
|
reminderRepeatDays: secret.reminderRepeatDays,
|
||||||
|
skipMultilineEncoding: secret.skipMultilineEncoding,
|
||||||
|
metadata: secret.metadata,
|
||||||
|
folderId: secret.folderId,
|
||||||
|
actorType: "platform"
|
||||||
|
});
|
||||||
|
|
||||||
|
secretsToUpdate.push({
|
||||||
|
id: secret.id,
|
||||||
|
newVersion: nextVersion
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Chunk ${chunkIndex + 1}: Adding ${versionsToAdd.length} versions, updating ${secretsToUpdate.length} secrets`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Batch insert new versions
|
||||||
|
if (versionsToAdd.length > 0) {
|
||||||
|
const insertBatches = chunkArray(versionsToAdd, 9000);
|
||||||
|
for (let i = 0; i < insertBatches.length; i += 1) {
|
||||||
|
await knex.batchInsert(TableName.SecretVersionV2, insertBatches[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secretsToUpdate.length > 0) {
|
||||||
|
const updateBatches = chunkArray(secretsToUpdate, 1000);
|
||||||
|
|
||||||
|
for (const updateBatch of updateBatches) {
|
||||||
|
const ids = updateBatch.map((u) => u.id);
|
||||||
|
const versionCases = updateBatch.map((u) => `WHEN '${u.id}' THEN ${u.newVersion}`).join(" ");
|
||||||
|
|
||||||
|
await knex.raw(
|
||||||
|
`
|
||||||
|
UPDATE ${TableName.SecretV2}
|
||||||
|
SET version = CASE id ${versionCases} END,
|
||||||
|
"updatedAt" = NOW()
|
||||||
|
WHERE id IN (${ids.map(() => "?").join(",")})
|
||||||
|
`,
|
||||||
|
ids
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Secret version fix migration completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(): Promise<void> {
|
||||||
|
logger.info("Rollback not implemented for secret version fix migration");
|
||||||
|
// Note: Rolling back this migration would be complex and potentially destructive
|
||||||
|
// as it would require tracking which version entries were added
|
||||||
|
}
|
||||||
@@ -0,0 +1,345 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { chunkArray } from "@app/lib/fn";
|
||||||
|
import { selectAllTableCols } from "@app/lib/knex";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { ChangeType } from "@app/services/folder-commit/folder-commit-service";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ProjectType,
|
||||||
|
SecretType,
|
||||||
|
TableName,
|
||||||
|
TFolderCheckpoints,
|
||||||
|
TFolderCommits,
|
||||||
|
TFolderTreeCheckpoints,
|
||||||
|
TSecretFolders
|
||||||
|
} from "../schemas";
|
||||||
|
|
||||||
|
const sortFoldersByHierarchy = (folders: TSecretFolders[]) => {
|
||||||
|
// Create a map for quick lookup of children by parent ID
|
||||||
|
const childrenMap = new Map<string, TSecretFolders[]>();
|
||||||
|
|
||||||
|
// Set of all folder IDs
|
||||||
|
const allFolderIds = new Set<string>();
|
||||||
|
|
||||||
|
// Build the set of all folder IDs
|
||||||
|
folders.forEach((folder) => {
|
||||||
|
if (folder.id) {
|
||||||
|
allFolderIds.add(folder.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Group folders by their parentId
|
||||||
|
folders.forEach((folder) => {
|
||||||
|
if (folder.parentId) {
|
||||||
|
const children = childrenMap.get(folder.parentId) || [];
|
||||||
|
children.push(folder);
|
||||||
|
childrenMap.set(folder.parentId, children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find root folders - those with no parentId or with a parentId that doesn't exist
|
||||||
|
const rootFolders = folders.filter((folder) => !folder.parentId || !allFolderIds.has(folder.parentId));
|
||||||
|
|
||||||
|
// Process each level of the hierarchy
|
||||||
|
const result = [];
|
||||||
|
let currentLevel = rootFolders;
|
||||||
|
|
||||||
|
while (currentLevel.length > 0) {
|
||||||
|
result.push(...currentLevel);
|
||||||
|
|
||||||
|
const nextLevel = [];
|
||||||
|
for (const folder of currentLevel) {
|
||||||
|
if (folder.id) {
|
||||||
|
const children = childrenMap.get(folder.id) || [];
|
||||||
|
nextLevel.push(...children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLevel = nextLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.reverse();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSecretsByFolderIds = async (knex: Knex, folderIds: string[]): Promise<Record<string, string[]>> => {
|
||||||
|
const secrets = await knex(TableName.SecretV2)
|
||||||
|
.whereIn(`${TableName.SecretV2}.folderId`, folderIds)
|
||||||
|
.where(`${TableName.SecretV2}.type`, SecretType.Shared)
|
||||||
|
.join<TableName.SecretVersionV2>(TableName.SecretVersionV2, (queryBuilder) => {
|
||||||
|
void queryBuilder
|
||||||
|
.on(`${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`)
|
||||||
|
.andOn(`${TableName.SecretVersionV2}.version`, `${TableName.SecretV2}.version`);
|
||||||
|
})
|
||||||
|
.select(selectAllTableCols(TableName.SecretV2))
|
||||||
|
.select(knex.ref("id").withSchema(TableName.SecretVersionV2).as("secretVersionId"));
|
||||||
|
|
||||||
|
const secretsMap: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
secrets.forEach((secret) => {
|
||||||
|
if (!secretsMap[secret.folderId]) {
|
||||||
|
secretsMap[secret.folderId] = [];
|
||||||
|
}
|
||||||
|
secretsMap[secret.folderId].push(secret.secretVersionId);
|
||||||
|
});
|
||||||
|
|
||||||
|
return secretsMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFoldersByParentIds = async (knex: Knex, parentIds: string[]): Promise<Record<string, string[]>> => {
|
||||||
|
const folders = await knex(TableName.SecretFolder)
|
||||||
|
.whereIn(`${TableName.SecretFolder}.parentId`, parentIds)
|
||||||
|
.where(`${TableName.SecretFolder}.isReserved`, false)
|
||||||
|
.join<TableName.SecretFolderVersion>(TableName.SecretFolderVersion, (queryBuilder) => {
|
||||||
|
void queryBuilder
|
||||||
|
.on(`${TableName.SecretFolderVersion}.folderId`, `${TableName.SecretFolder}.id`)
|
||||||
|
.andOn(`${TableName.SecretFolderVersion}.version`, `${TableName.SecretFolder}.version`);
|
||||||
|
})
|
||||||
|
.select(selectAllTableCols(TableName.SecretFolder))
|
||||||
|
.select(knex.ref("id").withSchema(TableName.SecretFolderVersion).as("folderVersionId"));
|
||||||
|
|
||||||
|
const foldersMap: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
folders.forEach((folder) => {
|
||||||
|
if (!folder.parentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!foldersMap[folder.parentId]) {
|
||||||
|
foldersMap[folder.parentId] = [];
|
||||||
|
}
|
||||||
|
foldersMap[folder.parentId].push(folder.folderVersionId);
|
||||||
|
});
|
||||||
|
|
||||||
|
return foldersMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
logger.info("Initializing folder commits");
|
||||||
|
const hasFolderCommitTable = await knex.schema.hasTable(TableName.FolderCommit);
|
||||||
|
if (hasFolderCommitTable) {
|
||||||
|
// Get Projects to Initialize
|
||||||
|
const projects = await knex(TableName.Project)
|
||||||
|
.where(`${TableName.Project}.version`, 3)
|
||||||
|
.where(`${TableName.Project}.type`, ProjectType.SecretManager)
|
||||||
|
.select(selectAllTableCols(TableName.Project));
|
||||||
|
logger.info(`Found ${projects.length} projects to initialize`);
|
||||||
|
|
||||||
|
// Process Projects in batches of 100
|
||||||
|
const batches = chunkArray(projects, 100);
|
||||||
|
let i = 0;
|
||||||
|
for (const batch of batches) {
|
||||||
|
i += 1;
|
||||||
|
logger.info(`Processing project batch ${i} of ${batches.length}`);
|
||||||
|
let foldersCommitsList = [];
|
||||||
|
|
||||||
|
const rootFoldersMap: Record<string, string> = {};
|
||||||
|
const envRootFoldersMap: Record<string, string> = {};
|
||||||
|
|
||||||
|
// Get All Folders for the Project
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const folders = await knex(TableName.SecretFolder)
|
||||||
|
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
|
.whereIn(
|
||||||
|
`${TableName.Environment}.projectId`,
|
||||||
|
batch.map((project) => project.id)
|
||||||
|
)
|
||||||
|
.where(`${TableName.SecretFolder}.isReserved`, false)
|
||||||
|
.select(selectAllTableCols(TableName.SecretFolder));
|
||||||
|
logger.info(`Found ${folders.length} folders to initialize in project batch ${i} of ${batches.length}`);
|
||||||
|
|
||||||
|
// Sort Folders by Hierarchy (parents before nested folders)
|
||||||
|
const sortedFolders = sortFoldersByHierarchy(folders);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const folderSecretsMap = await getSecretsByFolderIds(
|
||||||
|
knex,
|
||||||
|
sortedFolders.map((folder) => folder.id)
|
||||||
|
);
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const folderFoldersMap = await getFoldersByParentIds(
|
||||||
|
knex,
|
||||||
|
sortedFolders.map((folder) => folder.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get folder commit changes
|
||||||
|
for (const folder of sortedFolders) {
|
||||||
|
const subFolderVersionIds = folderFoldersMap[folder.id];
|
||||||
|
const secretVersionIds = folderSecretsMap[folder.id];
|
||||||
|
const changes = [];
|
||||||
|
if (subFolderVersionIds) {
|
||||||
|
changes.push(
|
||||||
|
...subFolderVersionIds.map((folderVersionId) => ({
|
||||||
|
folderId: folder.id,
|
||||||
|
changeType: ChangeType.ADD,
|
||||||
|
secretVersionId: undefined,
|
||||||
|
folderVersionId,
|
||||||
|
isUpdate: false
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (secretVersionIds) {
|
||||||
|
changes.push(
|
||||||
|
...secretVersionIds.map((secretVersionId) => ({
|
||||||
|
folderId: folder.id,
|
||||||
|
changeType: ChangeType.ADD,
|
||||||
|
secretVersionId,
|
||||||
|
folderVersionId: undefined,
|
||||||
|
isUpdate: false
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (changes.length > 0) {
|
||||||
|
const folderCommit = {
|
||||||
|
commit: {
|
||||||
|
actorMetadata: {},
|
||||||
|
actorType: ActorType.PLATFORM,
|
||||||
|
message: "Initialized folder",
|
||||||
|
folderId: folder.id,
|
||||||
|
envId: folder.envId
|
||||||
|
},
|
||||||
|
changes
|
||||||
|
};
|
||||||
|
foldersCommitsList.push(folderCommit);
|
||||||
|
if (!folder.parentId) {
|
||||||
|
rootFoldersMap[folder.id] = folder.envId;
|
||||||
|
envRootFoldersMap[folder.envId] = folder.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info(`Retrieved folder changes for project batch ${i} of ${batches.length}`);
|
||||||
|
|
||||||
|
const filteredBrokenProjectFolders: string[] = [];
|
||||||
|
|
||||||
|
foldersCommitsList = foldersCommitsList.filter((folderCommit) => {
|
||||||
|
if (!envRootFoldersMap[folderCommit.commit.envId]) {
|
||||||
|
filteredBrokenProjectFolders.push(folderCommit.commit.folderId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Filtered ${filteredBrokenProjectFolders.length} broken project folders: ${JSON.stringify(filteredBrokenProjectFolders)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Insert New Commits in batches of 9000
|
||||||
|
const newCommits = foldersCommitsList.map((folderCommit) => folderCommit.commit);
|
||||||
|
const commitBatches = chunkArray(newCommits, 9000);
|
||||||
|
|
||||||
|
let j = 0;
|
||||||
|
for (const commitBatch of commitBatches) {
|
||||||
|
j += 1;
|
||||||
|
logger.info(`Inserting folder commits - batch ${j} of ${commitBatches.length}`);
|
||||||
|
// Create folder commit
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const newCommitsInserted = (await knex
|
||||||
|
.batchInsert(TableName.FolderCommit, commitBatch)
|
||||||
|
.returning("*")) as TFolderCommits[];
|
||||||
|
|
||||||
|
logger.info(`Finished inserting folder commits - batch ${j} of ${commitBatches.length}`);
|
||||||
|
|
||||||
|
const newCommitsMap: Record<string, string> = {};
|
||||||
|
const newCommitsMapInverted: Record<string, string> = {};
|
||||||
|
const newCheckpointsMap: Record<string, string> = {};
|
||||||
|
newCommitsInserted.forEach((commit) => {
|
||||||
|
newCommitsMap[commit.folderId] = commit.id;
|
||||||
|
newCommitsMapInverted[commit.id] = commit.folderId;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create folder checkpoints
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const newCheckpoints = (await knex
|
||||||
|
.batchInsert(
|
||||||
|
TableName.FolderCheckpoint,
|
||||||
|
Object.values(newCommitsMap).map((commitId) => ({
|
||||||
|
folderCommitId: commitId
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
.returning("*")) as TFolderCheckpoints[];
|
||||||
|
|
||||||
|
logger.info(`Finished inserting folder checkpoints - batch ${j} of ${commitBatches.length}`);
|
||||||
|
|
||||||
|
newCheckpoints.forEach((checkpoint) => {
|
||||||
|
newCheckpointsMap[newCommitsMapInverted[checkpoint.folderCommitId]] = checkpoint.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create folder commit changes
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await knex.batchInsert(
|
||||||
|
TableName.FolderCommitChanges,
|
||||||
|
foldersCommitsList
|
||||||
|
.map((folderCommit) => folderCommit.changes)
|
||||||
|
.flat()
|
||||||
|
.map((change) => ({
|
||||||
|
folderCommitId: newCommitsMap[change.folderId],
|
||||||
|
changeType: change.changeType,
|
||||||
|
secretVersionId: change.secretVersionId,
|
||||||
|
folderVersionId: change.folderVersionId,
|
||||||
|
isUpdate: false
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Finished inserting folder commit changes - batch ${j} of ${commitBatches.length}`);
|
||||||
|
|
||||||
|
// Create folder checkpoint resources
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await knex.batchInsert(
|
||||||
|
TableName.FolderCheckpointResources,
|
||||||
|
foldersCommitsList
|
||||||
|
.map((folderCommit) => folderCommit.changes)
|
||||||
|
.flat()
|
||||||
|
.map((change) => ({
|
||||||
|
folderCheckpointId: newCheckpointsMap[change.folderId],
|
||||||
|
folderVersionId: change.folderVersionId,
|
||||||
|
secretVersionId: change.secretVersionId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Finished inserting folder checkpoint resources - batch ${j} of ${commitBatches.length}`);
|
||||||
|
|
||||||
|
// Create Folder Tree Checkpoint
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const newTreeCheckpoints = (await knex
|
||||||
|
.batchInsert(
|
||||||
|
TableName.FolderTreeCheckpoint,
|
||||||
|
Object.keys(rootFoldersMap).map((folderId) => ({
|
||||||
|
folderCommitId: newCommitsMap[folderId]
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
.returning("*")) as TFolderTreeCheckpoints[];
|
||||||
|
|
||||||
|
logger.info(`Finished inserting folder tree checkpoints - batch ${j} of ${commitBatches.length}`);
|
||||||
|
|
||||||
|
const newTreeCheckpointsMap: Record<string, string> = {};
|
||||||
|
newTreeCheckpoints.forEach((checkpoint) => {
|
||||||
|
newTreeCheckpointsMap[rootFoldersMap[newCommitsMapInverted[checkpoint.folderCommitId]]] = checkpoint.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create Folder Tree Checkpoint Resources
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await knex
|
||||||
|
.batchInsert(
|
||||||
|
TableName.FolderTreeCheckpointResources,
|
||||||
|
newCommitsInserted.map((folderCommit) => ({
|
||||||
|
folderTreeCheckpointId: newTreeCheckpointsMap[folderCommit.envId],
|
||||||
|
folderId: folderCommit.folderId,
|
||||||
|
folderCommitId: folderCommit.id
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
.returning("*");
|
||||||
|
|
||||||
|
logger.info(`Finished inserting folder tree checkpoint resources - batch ${j} of ${commitBatches.length}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info("Folder commits initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasFolderCommitTable = await knex.schema.hasTable(TableName.FolderCommit);
|
||||||
|
if (hasFolderCommitTable) {
|
||||||
|
// delete all existing entries
|
||||||
|
await knex(TableName.FolderCommit).del();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasShowSnapshotsLegacyColumn = await knex.schema.hasColumn(TableName.Project, "showSnapshotsLegacy");
|
||||||
|
if (!hasShowSnapshotsLegacyColumn) {
|
||||||
|
await knex.schema.table(TableName.Project, (table) => {
|
||||||
|
table.boolean("showSnapshotsLegacy").notNullable().defaultTo(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasShowSnapshotsLegacyColumn = await knex.schema.hasColumn(TableName.Project, "showSnapshotsLegacy");
|
||||||
|
if (hasShowSnapshotsLegacyColumn) {
|
||||||
|
await knex.schema.table(TableName.Project, (table) => {
|
||||||
|
table.dropColumn("showSnapshotsLegacy");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,27 @@ import { Knex } from "knex";
|
|||||||
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
|
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
|
||||||
import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
import { hsmServiceFactory } from "@app/ee/services/hsm/hsm-service";
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
|
import { folderCheckpointDALFactory } from "@app/services/folder-checkpoint/folder-checkpoint-dal";
|
||||||
|
import { folderCheckpointResourcesDALFactory } from "@app/services/folder-checkpoint-resources/folder-checkpoint-resources-dal";
|
||||||
|
import { folderCommitDALFactory } from "@app/services/folder-commit/folder-commit-dal";
|
||||||
|
import { folderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
||||||
|
import { folderCommitChangesDALFactory } from "@app/services/folder-commit-changes/folder-commit-changes-dal";
|
||||||
|
import { folderTreeCheckpointDALFactory } from "@app/services/folder-tree-checkpoint/folder-tree-checkpoint-dal";
|
||||||
|
import { folderTreeCheckpointResourcesDALFactory } from "@app/services/folder-tree-checkpoint-resources/folder-tree-checkpoint-resources-dal";
|
||||||
|
import { identityDALFactory } from "@app/services/identity/identity-dal";
|
||||||
import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal";
|
import { internalKmsDALFactory } from "@app/services/kms/internal-kms-dal";
|
||||||
import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal";
|
import { kmskeyDALFactory } from "@app/services/kms/kms-key-dal";
|
||||||
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
import { kmsRootConfigDALFactory } from "@app/services/kms/kms-root-config-dal";
|
||||||
import { kmsServiceFactory } from "@app/services/kms/kms-service";
|
import { kmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { orgDALFactory } from "@app/services/org/org-dal";
|
import { orgDALFactory } from "@app/services/org/org-dal";
|
||||||
import { projectDALFactory } from "@app/services/project/project-dal";
|
import { projectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { resourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal";
|
||||||
|
import { secretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
|
import { secretFolderVersionDALFactory } from "@app/services/secret-folder/secret-folder-version-dal";
|
||||||
|
import { secretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
||||||
|
import { secretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
|
import { secretVersionV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret-version-dal";
|
||||||
|
import { userDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
import { TMigrationEnvConfig } from "./env-config";
|
import { TMigrationEnvConfig } from "./env-config";
|
||||||
|
|
||||||
@@ -50,3 +65,77 @@ export const getMigrationEncryptionServices = async ({ envConfig, db, keyStore }
|
|||||||
|
|
||||||
return { kmsService };
|
return { kmsService };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getMigrationPITServices = async ({
|
||||||
|
db,
|
||||||
|
keyStore,
|
||||||
|
envConfig
|
||||||
|
}: {
|
||||||
|
db: Knex;
|
||||||
|
keyStore: TKeyStoreFactory;
|
||||||
|
envConfig: TMigrationEnvConfig;
|
||||||
|
}) => {
|
||||||
|
const projectDAL = projectDALFactory(db);
|
||||||
|
const folderCommitDAL = folderCommitDALFactory(db);
|
||||||
|
const folderCommitChangesDAL = folderCommitChangesDALFactory(db);
|
||||||
|
const folderCheckpointDAL = folderCheckpointDALFactory(db);
|
||||||
|
const folderTreeCheckpointDAL = folderTreeCheckpointDALFactory(db);
|
||||||
|
const userDAL = userDALFactory(db);
|
||||||
|
const identityDAL = identityDALFactory(db);
|
||||||
|
const folderDAL = secretFolderDALFactory(db);
|
||||||
|
const folderVersionDAL = secretFolderVersionDALFactory(db);
|
||||||
|
const secretVersionV2BridgeDAL = secretVersionV2BridgeDALFactory(db);
|
||||||
|
const folderCheckpointResourcesDAL = folderCheckpointResourcesDALFactory(db);
|
||||||
|
const secretV2BridgeDAL = secretV2BridgeDALFactory({ db, keyStore });
|
||||||
|
const folderTreeCheckpointResourcesDAL = folderTreeCheckpointResourcesDALFactory(db);
|
||||||
|
const secretTagDAL = secretTagDALFactory(db);
|
||||||
|
|
||||||
|
const orgDAL = orgDALFactory(db);
|
||||||
|
const kmsRootConfigDAL = kmsRootConfigDALFactory(db);
|
||||||
|
const kmsDAL = kmskeyDALFactory(db);
|
||||||
|
const internalKmsDAL = internalKmsDALFactory(db);
|
||||||
|
const resourceMetadataDAL = resourceMetadataDALFactory(db);
|
||||||
|
|
||||||
|
const hsmModule = initializeHsmModule(envConfig);
|
||||||
|
hsmModule.initialize();
|
||||||
|
|
||||||
|
const hsmService = hsmServiceFactory({
|
||||||
|
hsmModule: hsmModule.getModule(),
|
||||||
|
envConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
const kmsService = kmsServiceFactory({
|
||||||
|
kmsRootConfigDAL,
|
||||||
|
keyStore,
|
||||||
|
kmsDAL,
|
||||||
|
internalKmsDAL,
|
||||||
|
orgDAL,
|
||||||
|
projectDAL,
|
||||||
|
hsmService,
|
||||||
|
envConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
await hsmService.startService();
|
||||||
|
await kmsService.startService();
|
||||||
|
|
||||||
|
const folderCommitService = folderCommitServiceFactory({
|
||||||
|
folderCommitDAL,
|
||||||
|
folderCommitChangesDAL,
|
||||||
|
folderCheckpointDAL,
|
||||||
|
folderTreeCheckpointDAL,
|
||||||
|
userDAL,
|
||||||
|
identityDAL,
|
||||||
|
folderDAL,
|
||||||
|
folderVersionDAL,
|
||||||
|
secretVersionV2BridgeDAL,
|
||||||
|
projectDAL,
|
||||||
|
folderCheckpointResourcesDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
folderTreeCheckpointResourcesDAL,
|
||||||
|
kmsService,
|
||||||
|
secretTagDAL,
|
||||||
|
resourceMetadataDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
return { folderCommitService };
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const FolderCheckpointResourcesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
folderCheckpointId: z.string().uuid(),
|
||||||
|
secretVersionId: z.string().uuid().nullable().optional(),
|
||||||
|
folderVersionId: z.string().uuid().nullable().optional(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TFolderCheckpointResources = z.infer<typeof FolderCheckpointResourcesSchema>;
|
||||||
|
export type TFolderCheckpointResourcesInsert = Omit<z.input<typeof FolderCheckpointResourcesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TFolderCheckpointResourcesUpdate = Partial<
|
||||||
|
Omit<z.input<typeof FolderCheckpointResourcesSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const FolderCheckpointsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
folderCommitId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TFolderCheckpoints = z.infer<typeof FolderCheckpointsSchema>;
|
||||||
|
export type TFolderCheckpointsInsert = Omit<z.input<typeof FolderCheckpointsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TFolderCheckpointsUpdate = Partial<Omit<z.input<typeof FolderCheckpointsSchema>, TImmutableDBKeys>>;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
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(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TFolderCommitChanges = z.infer<typeof FolderCommitChangesSchema>;
|
||||||
|
export type TFolderCommitChangesInsert = Omit<z.input<typeof FolderCommitChangesSchema>, TImmutableDBKeys>;
|
||||||
|
export type TFolderCommitChangesUpdate = Partial<Omit<z.input<typeof FolderCommitChangesSchema>, TImmutableDBKeys>>;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const FolderCommitsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
commitId: z.coerce.bigint(),
|
||||||
|
actorMetadata: z.unknown(),
|
||||||
|
actorType: z.string(),
|
||||||
|
message: z.string().nullable().optional(),
|
||||||
|
folderId: z.string().uuid(),
|
||||||
|
envId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TFolderCommits = z.infer<typeof FolderCommitsSchema>;
|
||||||
|
export type TFolderCommitsInsert = Omit<z.input<typeof FolderCommitsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TFolderCommitsUpdate = Partial<Omit<z.input<typeof FolderCommitsSchema>, TImmutableDBKeys>>;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const FolderTreeCheckpointResourcesSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
folderTreeCheckpointId: z.string().uuid(),
|
||||||
|
folderId: z.string().uuid(),
|
||||||
|
folderCommitId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TFolderTreeCheckpointResources = z.infer<typeof FolderTreeCheckpointResourcesSchema>;
|
||||||
|
export type TFolderTreeCheckpointResourcesInsert = Omit<
|
||||||
|
z.input<typeof FolderTreeCheckpointResourcesSchema>,
|
||||||
|
TImmutableDBKeys
|
||||||
|
>;
|
||||||
|
export type TFolderTreeCheckpointResourcesUpdate = Partial<
|
||||||
|
Omit<z.input<typeof FolderTreeCheckpointResourcesSchema>, TImmutableDBKeys>
|
||||||
|
>;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const FolderTreeCheckpointsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
folderCommitId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TFolderTreeCheckpoints = z.infer<typeof FolderTreeCheckpointsSchema>;
|
||||||
|
export type TFolderTreeCheckpointsInsert = Omit<z.input<typeof FolderTreeCheckpointsSchema>, TImmutableDBKeys>;
|
||||||
|
export type TFolderTreeCheckpointsUpdate = Partial<Omit<z.input<typeof FolderTreeCheckpointsSchema>, TImmutableDBKeys>>;
|
||||||
@@ -24,6 +24,12 @@ export * from "./dynamic-secrets";
|
|||||||
export * from "./external-certificate-authorities";
|
export * from "./external-certificate-authorities";
|
||||||
export * from "./external-group-org-role-mappings";
|
export * from "./external-group-org-role-mappings";
|
||||||
export * from "./external-kms";
|
export * from "./external-kms";
|
||||||
|
export * from "./folder-checkpoint-resources";
|
||||||
|
export * from "./folder-checkpoints";
|
||||||
|
export * from "./folder-commit-changes";
|
||||||
|
export * from "./folder-commits";
|
||||||
|
export * from "./folder-tree-checkpoint-resources";
|
||||||
|
export * from "./folder-tree-checkpoints";
|
||||||
export * from "./gateways";
|
export * from "./gateways";
|
||||||
export * from "./git-app-install-sessions";
|
export * from "./git-app-install-sessions";
|
||||||
export * from "./git-app-org";
|
export * from "./git-app-org";
|
||||||
|
|||||||
@@ -160,6 +160,12 @@ export enum TableName {
|
|||||||
ProjectMicrosoftTeamsConfigs = "project_microsoft_teams_configs",
|
ProjectMicrosoftTeamsConfigs = "project_microsoft_teams_configs",
|
||||||
SecretReminderRecipients = "secret_reminder_recipients",
|
SecretReminderRecipients = "secret_reminder_recipients",
|
||||||
GithubOrgSyncConfig = "github_org_sync_configs",
|
GithubOrgSyncConfig = "github_org_sync_configs",
|
||||||
|
FolderCommit = "folder_commits",
|
||||||
|
FolderCommitChanges = "folder_commit_changes",
|
||||||
|
FolderCheckpoint = "folder_checkpoints",
|
||||||
|
FolderCheckpointResources = "folder_checkpoint_resources",
|
||||||
|
FolderTreeCheckpoint = "folder_tree_checkpoints",
|
||||||
|
FolderTreeCheckpointResources = "folder_tree_checkpoint_resources",
|
||||||
SecretScanningDataSource = "secret_scanning_data_sources",
|
SecretScanningDataSource = "secret_scanning_data_sources",
|
||||||
SecretScanningResource = "secret_scanning_resources",
|
SecretScanningResource = "secret_scanning_resources",
|
||||||
SecretScanningScan = "secret_scanning_scans",
|
SecretScanningScan = "secret_scanning_scans",
|
||||||
@@ -167,7 +173,7 @@ export enum TableName {
|
|||||||
SecretScanningConfig = "secret_scanning_configs"
|
SecretScanningConfig = "secret_scanning_configs"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
|
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt" | "commitId";
|
||||||
|
|
||||||
export const UserDeviceSchema = z
|
export const UserDeviceSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ export const ProjectsSchema = z.object({
|
|||||||
type: z.string(),
|
type: z.string(),
|
||||||
enforceCapitalization: z.boolean().default(false),
|
enforceCapitalization: z.boolean().default(false),
|
||||||
hasDeleteProtection: z.boolean().default(false).nullable().optional(),
|
hasDeleteProtection: z.boolean().default(false).nullable().optional(),
|
||||||
secretSharing: z.boolean().default(true)
|
secretSharing: z.boolean().default(true),
|
||||||
|
showSnapshotsLegacy: z.boolean().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TProjects = z.infer<typeof ProjectsSchema>;
|
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ export const SecretFolderVersionsSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
envId: z.string().uuid(),
|
envId: z.string().uuid(),
|
||||||
folderId: z.string().uuid()
|
folderId: z.string().uuid(),
|
||||||
|
description: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TSecretFolderVersions = z.infer<typeof SecretFolderVersionsSchema>;
|
export type TSecretFolderVersions = z.infer<typeof SecretFolderVersionsSchema>;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { registerLdapRouter } from "./ldap-router";
|
|||||||
import { registerLicenseRouter } from "./license-router";
|
import { registerLicenseRouter } from "./license-router";
|
||||||
import { registerOidcRouter } from "./oidc-router";
|
import { registerOidcRouter } from "./oidc-router";
|
||||||
import { registerOrgRoleRouter } from "./org-role-router";
|
import { registerOrgRoleRouter } from "./org-role-router";
|
||||||
|
import { registerPITRouter } from "./pit-router";
|
||||||
import { registerProjectRoleRouter } from "./project-role-router";
|
import { registerProjectRoleRouter } from "./project-role-router";
|
||||||
import { registerProjectRouter } from "./project-router";
|
import { registerProjectRouter } from "./project-router";
|
||||||
import { registerRateLimitRouter } from "./rate-limit-router";
|
import { registerRateLimitRouter } from "./rate-limit-router";
|
||||||
@@ -53,6 +54,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
{ prefix: "/workspace" }
|
{ prefix: "/workspace" }
|
||||||
);
|
);
|
||||||
await server.register(registerSnapshotRouter, { prefix: "/secret-snapshot" });
|
await server.register(registerSnapshotRouter, { prefix: "/secret-snapshot" });
|
||||||
|
await server.register(registerPITRouter, { prefix: "/pit" });
|
||||||
await server.register(registerSecretApprovalPolicyRouter, { prefix: "/secret-approvals" });
|
await server.register(registerSecretApprovalPolicyRouter, { prefix: "/secret-approvals" });
|
||||||
await server.register(registerSecretApprovalRequestRouter, {
|
await server.register(registerSecretApprovalRequestRouter, {
|
||||||
prefix: "/secret-approval-requests"
|
prefix: "/secret-approval-requests"
|
||||||
|
|||||||
@@ -0,0 +1,416 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-base-to-string */
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
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 { commitChangesResponseSchema, resourceChangeSchema } from "@app/services/folder-commit/folder-commit-schemas";
|
||||||
|
|
||||||
|
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 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 commits count for a folder
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/commits/count",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
environment: z.string().trim(),
|
||||||
|
path: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
|
projectId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
count: z.number(),
|
||||||
|
folderId: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
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.projectId,
|
||||||
|
environment: req.query.environment,
|
||||||
|
path: req.query.path
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: req.query.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_PROJECT_PIT_COMMIT_COUNT,
|
||||||
|
metadata: {
|
||||||
|
environment: req.query.environment,
|
||||||
|
path: req.query.path,
|
||||||
|
commitCount: result.count.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all commits for a folder
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/commits",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
environment: z.string().trim(),
|
||||||
|
path: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
|
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(),
|
||||||
|
sort: z.enum(["asc", "desc"]).default("desc")
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
commits: commitHistoryItemSchema.array(),
|
||||||
|
total: z.number(),
|
||||||
|
hasMore: z.boolean()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
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.projectId,
|
||||||
|
environment: req.query.environment,
|
||||||
|
path: req.query.path,
|
||||||
|
offset: req.query.offset,
|
||||||
|
limit: req.query.limit,
|
||||||
|
search: req.query.search,
|
||||||
|
sort: req.query.sort
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: req.query.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_PROJECT_PIT_COMMITS,
|
||||||
|
metadata: {
|
||||||
|
environment: req.query.environment,
|
||||||
|
path: req.query.path,
|
||||||
|
commitCount: result.commits.length.toString(),
|
||||||
|
offset: req.query.offset.toString(),
|
||||||
|
limit: req.query.limit.toString(),
|
||||||
|
search: req.query.search,
|
||||||
|
sort: req.query.sort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get commit changes for a specific commit
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/commits/:commitId/changes",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
commitId: z.string().trim()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
projectId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: commitChangesResponseSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
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.projectId,
|
||||||
|
commitId: req.params.commitId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: req.query.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_PROJECT_PIT_COMMIT_CHANGES,
|
||||||
|
metadata: {
|
||||||
|
commitId: req.params.commitId,
|
||||||
|
changesCount: (result.changes.changes?.length || 0).toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Retrieve rollback changes for a commit
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/commits/:commitId/compare",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
commitId: z.string().trim()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
folderId: z.string().trim(),
|
||||||
|
environment: z.string().trim(),
|
||||||
|
deepRollback: booleanSchema.default(false),
|
||||||
|
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||||
|
projectId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.array(
|
||||||
|
z.object({
|
||||||
|
folderId: z.string(),
|
||||||
|
folderName: z.string(),
|
||||||
|
folderPath: z.string().optional(),
|
||||||
|
changes: z.array(resourceChangeSchema)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
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.projectId,
|
||||||
|
commitId: req.params.commitId,
|
||||||
|
folderId: req.query.folderId,
|
||||||
|
environment: req.query.environment,
|
||||||
|
deepRollback: req.query.deepRollback,
|
||||||
|
secretPath: req.query.secretPath
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
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: result.length.toString(),
|
||||||
|
environment: req.query.environment,
|
||||||
|
folderPath: req.query.secretPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rollback to a previous commit
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/commits/:commitId/rollback",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
commitId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
folderId: z.string().trim(),
|
||||||
|
deepRollback: z.boolean().default(false),
|
||||||
|
message: z.string().max(256).trim().optional(),
|
||||||
|
environment: z.string().trim(),
|
||||||
|
projectId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
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 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.projectId,
|
||||||
|
commitId: req.params.commitId,
|
||||||
|
folderId: req.body.folderId,
|
||||||
|
deepRollback: req.body.deepRollback,
|
||||||
|
message: req.body.message,
|
||||||
|
environment: req.body.environment
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: req.body.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.PIT_ROLLBACK_COMMIT,
|
||||||
|
metadata: {
|
||||||
|
targetCommitId: req.params.commitId,
|
||||||
|
environment: req.body.environment,
|
||||||
|
folderId: req.body.folderId,
|
||||||
|
deepRollback: req.body.deepRollback,
|
||||||
|
message: req.body.message || "Rollback to previous commit",
|
||||||
|
totalChanges: result.totalChanges?.toString() || "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Revert commit
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/commits/:commitId/revert",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
commitId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
projectId: 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 result = await server.services.pit.revertCommit({
|
||||||
|
actor: req.permission?.type,
|
||||||
|
actorId: req.permission?.id,
|
||||||
|
actorOrgId: req.permission?.orgId,
|
||||||
|
actorAuthMethod: req.permission?.authMethod,
|
||||||
|
projectId: req.body.projectId,
|
||||||
|
commitId: req.params.commitId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: req.body.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.PIT_REVERT_COMMIT,
|
||||||
|
metadata: {
|
||||||
|
commitId: req.params.commitId,
|
||||||
|
revertCommitId: result.revertCommitId,
|
||||||
|
changesReverted: result.changesReverted?.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Folder state at commit
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/commits/:commitId",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
commitId: z.string().trim()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
folderId: z.string().trim(),
|
||||||
|
projectId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: folderStateSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const result = await server.services.pit.getFolderStateAtCommit({
|
||||||
|
actor: req.permission?.type,
|
||||||
|
actorId: req.permission?.id,
|
||||||
|
actorOrgId: req.permission?.orgId,
|
||||||
|
actorAuthMethod: req.permission?.authMethod,
|
||||||
|
projectId: req.query.projectId,
|
||||||
|
commitId: req.params.commitId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: req.query.projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.PIT_GET_FOLDER_STATE,
|
||||||
|
metadata: {
|
||||||
|
commitId: req.params.commitId,
|
||||||
|
folderId: req.query.folderId,
|
||||||
|
resourceCount: result.length.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -65,9 +65,10 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
|||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
hide: false,
|
hide: true,
|
||||||
|
deprecated: true,
|
||||||
tags: [ApiDocsTags.Projects],
|
tags: [ApiDocsTags.Projects],
|
||||||
description: "Roll back project secrets to those captured in a secret snapshot version.",
|
description: "(Deprecated) Roll back project secrets to those captured in a secret snapshot version.",
|
||||||
security: [
|
security: [
|
||||||
{
|
{
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
@@ -84,6 +85,10 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
|
throw new Error(
|
||||||
|
"This endpoint is deprecated. Please use the new PIT recovery system. More information is available at: https://infisical.com/docs/documentation/platform/pit-recovery."
|
||||||
|
);
|
||||||
|
|
||||||
const secretSnapshot = await server.services.snapshot.rollbackSnapshot({
|
const secretSnapshot = await server.services.snapshot.rollbackSnapshot({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
|
|||||||
@@ -393,6 +393,13 @@ export enum EventType {
|
|||||||
PROJECT_ASSUME_PRIVILEGE_SESSION_START = "project-assume-privileges-session-start",
|
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",
|
||||||
SECRET_SCANNING_DATA_SOURCE_LIST = "secret-scanning-data-source-list",
|
SECRET_SCANNING_DATA_SOURCE_LIST = "secret-scanning-data-source-list",
|
||||||
SECRET_SCANNING_DATA_SOURCE_CREATE = "secret-scanning-data-source-create",
|
SECRET_SCANNING_DATA_SOURCE_CREATE = "secret-scanning-data-source-create",
|
||||||
SECRET_SCANNING_DATA_SOURCE_UPDATE = "secret-scanning-data-source-update",
|
SECRET_SCANNING_DATA_SOURCE_UPDATE = "secret-scanning-data-source-update",
|
||||||
@@ -2979,6 +2986,78 @@ interface MicrosoftTeamsWorkflowIntegrationUpdateEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GetProjectPitCommitsEvent {
|
||||||
|
type: EventType.GET_PROJECT_PIT_COMMITS;
|
||||||
|
metadata: {
|
||||||
|
commitCount: string;
|
||||||
|
environment: string;
|
||||||
|
path: string;
|
||||||
|
offset: string;
|
||||||
|
limit: string;
|
||||||
|
search?: string;
|
||||||
|
sort: 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;
|
||||||
|
environment: 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;
|
||||||
|
environment: string;
|
||||||
|
folderPath: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface SecretScanningDataSourceListEvent {
|
interface SecretScanningDataSourceListEvent {
|
||||||
type: EventType.SECRET_SCANNING_DATA_SOURCE_LIST;
|
type: EventType.SECRET_SCANNING_DATA_SOURCE_LIST;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -3397,6 +3476,13 @@ export type Event =
|
|||||||
| MicrosoftTeamsWorkflowIntegrationGetEvent
|
| MicrosoftTeamsWorkflowIntegrationGetEvent
|
||||||
| MicrosoftTeamsWorkflowIntegrationListEvent
|
| MicrosoftTeamsWorkflowIntegrationListEvent
|
||||||
| MicrosoftTeamsWorkflowIntegrationUpdateEvent
|
| MicrosoftTeamsWorkflowIntegrationUpdateEvent
|
||||||
|
| GetProjectPitCommitsEvent
|
||||||
|
| GetProjectPitCommitChangesEvent
|
||||||
|
| PitRollbackCommitEvent
|
||||||
|
| GetProjectPitCommitCountEvent
|
||||||
|
| PitRevertCommitEvent
|
||||||
|
| PitCompareFolderStatesEvent
|
||||||
|
| PitGetFolderStateEvent
|
||||||
| SecretScanningDataSourceListEvent
|
| SecretScanningDataSourceListEvent
|
||||||
| SecretScanningDataSourceGetEvent
|
| SecretScanningDataSourceGetEvent
|
||||||
| SecretScanningDataSourceCreateEvent
|
| SecretScanningDataSourceCreateEvent
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionCertificateActions,
|
ProjectPermissionCertificateActions,
|
||||||
ProjectPermissionCmekActions,
|
ProjectPermissionCmekActions,
|
||||||
|
ProjectPermissionCommitsActions,
|
||||||
ProjectPermissionDynamicSecretActions,
|
ProjectPermissionDynamicSecretActions,
|
||||||
ProjectPermissionGroupActions,
|
ProjectPermissionGroupActions,
|
||||||
ProjectPermissionIdentityActions,
|
ProjectPermissionIdentityActions,
|
||||||
@@ -90,6 +91,11 @@ const buildAdminPermissionRules = () => {
|
|||||||
ProjectPermissionSub.Certificates
|
ProjectPermissionSub.Certificates
|
||||||
);
|
);
|
||||||
|
|
||||||
|
can(
|
||||||
|
[ProjectPermissionCommitsActions.Read, ProjectPermissionCommitsActions.PerformRollback],
|
||||||
|
ProjectPermissionSub.Commits
|
||||||
|
);
|
||||||
|
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
ProjectPermissionSshHostActions.Edit,
|
ProjectPermissionSshHostActions.Edit,
|
||||||
@@ -292,6 +298,11 @@ const buildMemberPermissionRules = () => {
|
|||||||
ProjectPermissionSub.SecretImports
|
ProjectPermissionSub.SecretImports
|
||||||
);
|
);
|
||||||
|
|
||||||
|
can(
|
||||||
|
[ProjectPermissionCommitsActions.Read, ProjectPermissionCommitsActions.PerformRollback],
|
||||||
|
ProjectPermissionSub.Commits
|
||||||
|
);
|
||||||
|
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
|
||||||
can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation);
|
can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation);
|
||||||
|
|
||||||
@@ -479,6 +490,7 @@ const buildViewerPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
|
||||||
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
|
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
|
||||||
|
can(ProjectPermissionCommitsActions.Read, ProjectPermissionSub.Commits);
|
||||||
|
|
||||||
can(
|
can(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ export enum ProjectPermissionActions {
|
|||||||
Delete = "delete"
|
Delete = "delete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionCommitsActions {
|
||||||
|
Read = "read",
|
||||||
|
PerformRollback = "perform-rollback"
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionCertificateActions {
|
export enum ProjectPermissionCertificateActions {
|
||||||
Read = "read",
|
Read = "read",
|
||||||
Create = "create",
|
Create = "create",
|
||||||
@@ -172,6 +177,7 @@ export enum ProjectPermissionSub {
|
|||||||
SecretRollback = "secret-rollback",
|
SecretRollback = "secret-rollback",
|
||||||
SecretApproval = "secret-approval",
|
SecretApproval = "secret-approval",
|
||||||
SecretRotation = "secret-rotation",
|
SecretRotation = "secret-rotation",
|
||||||
|
Commits = "commits",
|
||||||
Identity = "identity",
|
Identity = "identity",
|
||||||
CertificateAuthorities = "certificate-authorities",
|
CertificateAuthorities = "certificate-authorities",
|
||||||
Certificates = "certificates",
|
Certificates = "certificates",
|
||||||
@@ -325,6 +331,7 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
||||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
|
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
|
||||||
|
| [ProjectPermissionCommitsActions, ProjectPermissionSub.Commits]
|
||||||
| [ProjectPermissionSecretScanningDataSourceActions, ProjectPermissionSub.SecretScanningDataSources]
|
| [ProjectPermissionSecretScanningDataSourceActions, ProjectPermissionSub.SecretScanningDataSources]
|
||||||
| [ProjectPermissionSecretScanningFindingActions, ProjectPermissionSub.SecretScanningFindings]
|
| [ProjectPermissionSecretScanningFindingActions, ProjectPermissionSub.SecretScanningFindings]
|
||||||
| [ProjectPermissionSecretScanningConfigActions, ProjectPermissionSub.SecretScanningConfigs];
|
| [ProjectPermissionSecretScanningConfigActions, ProjectPermissionSub.SecretScanningConfigs];
|
||||||
@@ -676,6 +683,12 @@ const GeneralPermissionSchema = [
|
|||||||
"Describe what action an entity can take."
|
"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."
|
||||||
|
)
|
||||||
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
subject: z
|
subject: z
|
||||||
.literal(ProjectPermissionSub.SecretScanningDataSources)
|
.literal(ProjectPermissionSub.SecretScanningDataSources)
|
||||||
|
|||||||
@@ -0,0 +1,485 @@
|
|||||||
|
/* 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 { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||||
|
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
|
||||||
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
|
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">;
|
||||||
|
folderDAL: Pick<TSecretFolderDALFactory, "findSecretPathByFolderIds">;
|
||||||
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TPitServiceFactory = ReturnType<typeof pitServiceFactory>;
|
||||||
|
|
||||||
|
export const pitServiceFactory = ({
|
||||||
|
folderCommitService,
|
||||||
|
secretService,
|
||||||
|
folderService,
|
||||||
|
permissionService,
|
||||||
|
folderDAL,
|
||||||
|
projectEnvDAL
|
||||||
|
}: 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
|
||||||
|
});
|
||||||
|
|
||||||
|
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(projectId, [changes.folderId]);
|
||||||
|
|
||||||
|
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,
|
||||||
|
changes.envId,
|
||||||
|
projectId,
|
||||||
|
folderWithPath?.path || ""
|
||||||
|
);
|
||||||
|
} 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,
|
||||||
|
environment,
|
||||||
|
deepRollback,
|
||||||
|
secretPath
|
||||||
|
}: {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorOrgId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
projectId: string;
|
||||||
|
commitId: string;
|
||||||
|
folderId: string;
|
||||||
|
environment: 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
|
||||||
|
});
|
||||||
|
|
||||||
|
const env = await projectEnvDAL.findOne({
|
||||||
|
projectId,
|
||||||
|
slug: environment
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!latestCommit) {
|
||||||
|
throw new NotFoundError({ message: "Latest commit not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
let diffs;
|
||||||
|
if (deepRollback) {
|
||||||
|
diffs = await folderCommitService.deepCompareFolder({
|
||||||
|
targetCommitId: targetCommit.id,
|
||||||
|
envId: env.id,
|
||||||
|
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,
|
||||||
|
env.id,
|
||||||
|
projectId,
|
||||||
|
diff.folderPath || ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} 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,
|
||||||
|
environment
|
||||||
|
}: {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorOrgId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
projectId: string;
|
||||||
|
commitId: string;
|
||||||
|
folderId: string;
|
||||||
|
deepRollback: boolean;
|
||||||
|
message?: string;
|
||||||
|
environment: 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
|
||||||
|
});
|
||||||
|
|
||||||
|
const env = await projectEnvDAL.findOne({
|
||||||
|
projectId,
|
||||||
|
slug: environment
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!targetCommit || targetCommit.folderId !== folderId || targetCommit.envId !== env.id) {
|
||||||
|
throw new NotFoundError({ message: "Target commit not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!latestCommit || latestCommit.envId !== env.id) {
|
||||||
|
throw new NotFoundError({ message: "Latest commit not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deepRollback) {
|
||||||
|
await folderCommitService.deepRollbackFolder(commitId, env.id, 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 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) => {
|
||||||
|
if (item.type === ResourceType.SECRET) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
secretVersion: Number(item.secretVersion)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.type === ResourceType.FOLDER) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
folderVersion: Number(item.folderVersion)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getCommitsCount,
|
||||||
|
getCommitsForFolder,
|
||||||
|
getCommitChanges,
|
||||||
|
compareCommitChanges,
|
||||||
|
rollbackToCommit,
|
||||||
|
revertCommit,
|
||||||
|
getFolderStateAtCommit
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -20,6 +20,7 @@ import { EnforcementLevel } from "@app/lib/types";
|
|||||||
import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification";
|
import { triggerWorkflowIntegrationNotification } from "@app/lib/workflow-integrations/trigger-notification";
|
||||||
import { TriggerFeature } from "@app/lib/workflow-integrations/types";
|
import { TriggerFeature } from "@app/lib/workflow-integrations/types";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||||
@@ -130,6 +131,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
|||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||||
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||||
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
|
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
|
||||||
@@ -161,7 +163,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
projectSlackConfigDAL,
|
projectSlackConfigDAL,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
projectMicrosoftTeamsConfigDAL,
|
projectMicrosoftTeamsConfigDAL,
|
||||||
microsoftTeamsService
|
microsoftTeamsService,
|
||||||
|
folderCommitService
|
||||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||||
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
||||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||||
@@ -597,6 +600,10 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
? await fnSecretV2BridgeBulkInsert({
|
? await fnSecretV2BridgeBulkInsert({
|
||||||
tx,
|
tx,
|
||||||
folderId,
|
folderId,
|
||||||
|
actor: {
|
||||||
|
actorId,
|
||||||
|
type: actor
|
||||||
|
},
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
inputSecrets: secretCreationCommits.map((el) => ({
|
inputSecrets: secretCreationCommits.map((el) => ({
|
||||||
tagIds: el?.tags.map(({ id }) => id),
|
tagIds: el?.tags.map(({ id }) => id),
|
||||||
@@ -619,13 +626,18 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL: secretVersionTagV2BridgeDAL
|
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
||||||
|
folderCommitService
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
const updatedSecrets = secretUpdationCommits.length
|
const updatedSecrets = secretUpdationCommits.length
|
||||||
? await fnSecretV2BridgeBulkUpdate({
|
? await fnSecretV2BridgeBulkUpdate({
|
||||||
folderId,
|
folderId,
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
|
actor: {
|
||||||
|
actorId,
|
||||||
|
type: actor
|
||||||
|
},
|
||||||
tx,
|
tx,
|
||||||
inputSecrets: secretUpdationCommits.map((el) => {
|
inputSecrets: secretUpdationCommits.map((el) => {
|
||||||
const encryptedValue =
|
const encryptedValue =
|
||||||
@@ -659,7 +671,8 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL,
|
||||||
|
folderCommitService
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
const deletedSecret = secretDeletionCommits.length
|
const deletedSecret = secretDeletionCommits.length
|
||||||
@@ -667,10 +680,13 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
projectId,
|
projectId,
|
||||||
folderId,
|
folderId,
|
||||||
tx,
|
tx,
|
||||||
actorId: "",
|
actorId,
|
||||||
|
actorType: actor,
|
||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
secretQueueService,
|
secretQueueService,
|
||||||
inputSecrets: secretDeletionCommits.map(({ key }) => ({ secretKey: key, type: SecretType.Shared }))
|
inputSecrets: secretDeletionCommits.map(({ key }) => ({ secretKey: key, type: SecretType.Shared })),
|
||||||
|
folderCommitService,
|
||||||
|
secretVersionDAL: secretVersionV2BridgeDAL
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
const updatedSecretApproval = await secretApprovalRequestDAL.updateById(
|
const updatedSecretApproval = await secretApprovalRequestDAL.updateById(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { logger } from "@app/lib/logger";
|
|||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
@@ -87,6 +88,7 @@ type TSecretReplicationServiceFactoryDep = {
|
|||||||
|
|
||||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretReplicationServiceFactory = ReturnType<typeof secretReplicationServiceFactory>;
|
export type TSecretReplicationServiceFactory = ReturnType<typeof secretReplicationServiceFactory>;
|
||||||
@@ -132,6 +134,7 @@ export const secretReplicationServiceFactory = ({
|
|||||||
secretVersionV2BridgeDAL,
|
secretVersionV2BridgeDAL,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
|
folderCommitService,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL
|
||||||
}: TSecretReplicationServiceFactoryDep) => {
|
}: TSecretReplicationServiceFactoryDep) => {
|
||||||
const $getReplicatedSecrets = (
|
const $getReplicatedSecrets = (
|
||||||
@@ -419,7 +422,7 @@ export const secretReplicationServiceFactory = ({
|
|||||||
return {
|
return {
|
||||||
op: operation,
|
op: operation,
|
||||||
requestId: approvalRequestDoc.id,
|
requestId: approvalRequestDoc.id,
|
||||||
metadata: doc.metadata,
|
metadata: doc.metadata ? JSON.stringify(doc.metadata) : [],
|
||||||
secretMetadata: JSON.stringify(doc.secretMetadata),
|
secretMetadata: JSON.stringify(doc.secretMetadata),
|
||||||
key: doc.key,
|
key: doc.key,
|
||||||
encryptedValue: doc.encryptedValue,
|
encryptedValue: doc.encryptedValue,
|
||||||
@@ -446,11 +449,12 @@ export const secretReplicationServiceFactory = ({
|
|||||||
tx,
|
tx,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
|
folderCommitService,
|
||||||
secretVersionTagDAL: secretVersionV2TagBridgeDAL,
|
secretVersionTagDAL: secretVersionV2TagBridgeDAL,
|
||||||
inputSecrets: locallyCreatedSecrets.map((doc) => {
|
inputSecrets: locallyCreatedSecrets.map((doc) => {
|
||||||
return {
|
return {
|
||||||
type: doc.type,
|
type: doc.type,
|
||||||
metadata: doc.metadata,
|
metadata: doc.metadata ? JSON.stringify(doc.metadata) : [],
|
||||||
key: doc.key,
|
key: doc.key,
|
||||||
encryptedValue: doc.encryptedValue,
|
encryptedValue: doc.encryptedValue,
|
||||||
encryptedComment: doc.encryptedComment,
|
encryptedComment: doc.encryptedComment,
|
||||||
@@ -466,6 +470,7 @@ export const secretReplicationServiceFactory = ({
|
|||||||
orgId,
|
orgId,
|
||||||
folderId: destinationReplicationFolderId,
|
folderId: destinationReplicationFolderId,
|
||||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||||
|
folderCommitService,
|
||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
tx,
|
tx,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
@@ -479,7 +484,7 @@ export const secretReplicationServiceFactory = ({
|
|||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
type: doc.type,
|
type: doc.type,
|
||||||
metadata: doc.metadata,
|
metadata: doc.metadata ? JSON.stringify(doc.metadata) : [],
|
||||||
key: doc.key,
|
key: doc.key,
|
||||||
encryptedValue: doc.encryptedValue as Buffer,
|
encryptedValue: doc.encryptedValue as Buffer,
|
||||||
encryptedComment: doc.encryptedComment,
|
encryptedComment: doc.encryptedComment,
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ import { TAppConnectionDALFactory } from "@app/services/app-connection/app-conne
|
|||||||
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
|
import { decryptAppConnection } from "@app/services/app-connection/app-connection-fns";
|
||||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
@@ -98,7 +99,7 @@ export type TSecretRotationV2ServiceFactoryDep = {
|
|||||||
TSecretV2BridgeDALFactory,
|
TSecretV2BridgeDALFactory,
|
||||||
"bulkUpdate" | "insertMany" | "deleteMany" | "upsertSecretReferences" | "find" | "invalidateSecretCacheByProjectId"
|
"bulkUpdate" | "insertMany" | "deleteMany" | "upsertSecretReferences" | "find" | "invalidateSecretCacheByProjectId"
|
||||||
>;
|
>;
|
||||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2" | "find">;
|
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2" | "find">;
|
||||||
@@ -106,6 +107,7 @@ export type TSecretRotationV2ServiceFactoryDep = {
|
|||||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||||
queueService: Pick<TQueueServiceFactory, "queuePg">;
|
queueService: Pick<TQueueServiceFactory, "queuePg">;
|
||||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
||||||
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>;
|
export type TSecretRotationV2ServiceFactory = ReturnType<typeof secretRotationV2ServiceFactory>;
|
||||||
@@ -145,6 +147,7 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
snapshotService,
|
snapshotService,
|
||||||
keyStore,
|
keyStore,
|
||||||
queueService,
|
queueService,
|
||||||
|
folderCommitService,
|
||||||
appConnectionDAL
|
appConnectionDAL
|
||||||
}: TSecretRotationV2ServiceFactoryDep) => {
|
}: TSecretRotationV2ServiceFactoryDep) => {
|
||||||
const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => {
|
const $queueSendSecretRotationStatusNotification = async (secretRotation: TSecretRotationV2Raw) => {
|
||||||
@@ -538,7 +541,12 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||||
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
resourceMetadataDAL
|
folderCommitService,
|
||||||
|
resourceMetadataDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor.type,
|
||||||
|
actorId: actor.id
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await secretRotationV2DAL.insertSecretMappings(
|
await secretRotationV2DAL.insertSecretMappings(
|
||||||
@@ -674,7 +682,12 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||||
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
resourceMetadataDAL
|
folderCommitService,
|
||||||
|
resourceMetadataDAL,
|
||||||
|
actor: {
|
||||||
|
type: actor.type,
|
||||||
|
actorId: actor.id
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
secretsMappingUpdated = true;
|
secretsMappingUpdated = true;
|
||||||
@@ -792,6 +805,9 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
projectId,
|
projectId,
|
||||||
folderId,
|
folderId,
|
||||||
actorId: actor.id, // not actually used since rotated secrets are shared
|
actorId: actor.id, // not actually used since rotated secrets are shared
|
||||||
|
actorType: actor.type,
|
||||||
|
folderCommitService,
|
||||||
|
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||||
tx
|
tx
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -935,6 +951,10 @@ export const secretRotationV2ServiceFactory = ({
|
|||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||||
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
||||||
|
folderCommitService,
|
||||||
|
actor: {
|
||||||
|
type: ActorType.PLATFORM
|
||||||
|
},
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL
|
||||||
});
|
});
|
||||||
|
|||||||
+20
-1
@@ -14,6 +14,7 @@ import { logger } from "@app/lib/logger";
|
|||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { CommitType, TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
@@ -53,6 +54,7 @@ type TSecretRotationQueueFactoryDep = {
|
|||||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||||
telemetryService: Pick<TTelemetryServiceFactory, "sendPostHogEvents">;
|
telemetryService: Pick<TTelemetryServiceFactory, "sendPostHogEvents">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
};
|
};
|
||||||
|
|
||||||
// These error should stop the repeatable job and ask user to reconfigure rotation
|
// These error should stop the repeatable job and ask user to reconfigure rotation
|
||||||
@@ -77,6 +79,7 @@ export const secretRotationQueueFactory = ({
|
|||||||
telemetryService,
|
telemetryService,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
secretVersionV2BridgeDAL,
|
secretVersionV2BridgeDAL,
|
||||||
|
folderCommitService,
|
||||||
kmsService
|
kmsService
|
||||||
}: TSecretRotationQueueFactoryDep) => {
|
}: TSecretRotationQueueFactoryDep) => {
|
||||||
const addToQueue = async (rotationId: string, interval: number) => {
|
const addToQueue = async (rotationId: string, interval: number) => {
|
||||||
@@ -330,7 +333,7 @@ export const secretRotationQueueFactory = ({
|
|||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
await secretVersionV2BridgeDAL.insertMany(
|
const secretVersions = await secretVersionV2BridgeDAL.insertMany(
|
||||||
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
updatedSecrets.map(({ id, updatedAt, createdAt, ...el }) => ({
|
||||||
...el,
|
...el,
|
||||||
actorType: ActorType.PLATFORM,
|
actorType: ActorType.PLATFORM,
|
||||||
@@ -338,6 +341,22 @@ export const secretRotationQueueFactory = ({
|
|||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await folderCommitService.createCommit(
|
||||||
|
{
|
||||||
|
actor: {
|
||||||
|
type: ActorType.PLATFORM
|
||||||
|
},
|
||||||
|
message: "Changed by Secret rotation",
|
||||||
|
folderId: secretVersions[0].folderId,
|
||||||
|
changes: secretVersions.map((sv) => ({
|
||||||
|
type: CommitType.ADD,
|
||||||
|
isUpdate: true,
|
||||||
|
secretVersionId: sv.id
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(secretRotation.projectId);
|
await secretV2BridgeDAL.invalidateSecretCacheByProjectId(secretRotation.projectId);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { InternalServerError, NotFoundError } from "@app/lib/errors";
|
|||||||
import { groupBy } from "@app/lib/fn";
|
import { groupBy } from "@app/lib/fn";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { CommitType, TFolderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
||||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
@@ -51,8 +52,8 @@ type TSecretSnapshotServiceFactoryDep = {
|
|||||||
snapshotSecretV2BridgeDAL: TSnapshotSecretV2DALFactory;
|
snapshotSecretV2BridgeDAL: TSnapshotSecretV2DALFactory;
|
||||||
snapshotFolderDAL: TSnapshotFolderDALFactory;
|
snapshotFolderDAL: TSnapshotFolderDALFactory;
|
||||||
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany" | "findLatestVersionByFolderId">;
|
secretVersionDAL: Pick<TSecretVersionDALFactory, "insertMany" | "findLatestVersionByFolderId">;
|
||||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionByFolderId">;
|
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionByFolderId" | "findOne">;
|
||||||
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "findLatestVersionByFolderId" | "insertMany">;
|
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "findLatestVersionByFolderId" | "insertMany" | "findOne">;
|
||||||
secretDAL: Pick<TSecretDALFactory, "delete" | "insertMany">;
|
secretDAL: Pick<TSecretDALFactory, "delete" | "insertMany">;
|
||||||
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "delete" | "insertMany">;
|
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "delete" | "insertMany">;
|
||||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret" | "saveTagsToSecretV2">;
|
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecret" | "saveTagsToSecretV2">;
|
||||||
@@ -63,6 +64,7 @@ type TSecretSnapshotServiceFactoryDep = {
|
|||||||
licenseService: Pick<TLicenseServiceFactory, "isValidLicense">;
|
licenseService: Pick<TLicenseServiceFactory, "isValidLicense">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
|
||||||
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretSnapshotServiceFactory = ReturnType<typeof secretSnapshotServiceFactory>;
|
export type TSecretSnapshotServiceFactory = ReturnType<typeof secretSnapshotServiceFactory>;
|
||||||
@@ -84,7 +86,8 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
snapshotSecretV2BridgeDAL,
|
snapshotSecretV2BridgeDAL,
|
||||||
secretVersionV2TagBridgeDAL,
|
secretVersionV2TagBridgeDAL,
|
||||||
kmsService,
|
kmsService,
|
||||||
projectBotService
|
projectBotService,
|
||||||
|
folderCommitService
|
||||||
}: TSecretSnapshotServiceFactoryDep) => {
|
}: TSecretSnapshotServiceFactoryDep) => {
|
||||||
const projectSecretSnapshotCount = async ({
|
const projectSecretSnapshotCount = async ({
|
||||||
environment,
|
environment,
|
||||||
@@ -403,6 +406,18 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
.filter((el) => el.isRotatedSecret)
|
.filter((el) => el.isRotatedSecret)
|
||||||
.map((el) => el.secretId);
|
.map((el) => el.secretId);
|
||||||
|
|
||||||
|
const deletedSecretsChanges = new Map(); // secretId -> version info
|
||||||
|
const deletedFoldersChanges = new Map(); // folderId -> version info
|
||||||
|
const addedSecretsChanges = new Map(); // secretId -> version info
|
||||||
|
const addedFoldersChanges = new Map(); // folderId -> version info
|
||||||
|
const commitChanges: {
|
||||||
|
type: string;
|
||||||
|
secretVersionId?: string;
|
||||||
|
folderVersionId?: string;
|
||||||
|
isUpdate?: boolean;
|
||||||
|
folderId?: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
// this will remove all secrets in current folder except rotated secrets which we ignore
|
// this will remove all secrets in current folder except rotated secrets which we ignore
|
||||||
const deletedTopLevelSecs = await secretV2BridgeDAL.delete(
|
const deletedTopLevelSecs = await secretV2BridgeDAL.delete(
|
||||||
{
|
{
|
||||||
@@ -424,7 +439,35 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
deletedTopLevelSecs.map(async (sec) => {
|
||||||
|
const version = await secretVersionV2BridgeDAL.findOne({ secretId: sec.id, version: sec.version }, tx);
|
||||||
|
deletedSecretsChanges.set(sec.id, {
|
||||||
|
id: sec.id,
|
||||||
|
version: sec.version,
|
||||||
|
// Store the version ID if available from the snapshot
|
||||||
|
versionId: version?.id
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const deletedTopLevelSecsGroupById = groupBy(deletedTopLevelSecs, (item) => item.id);
|
const deletedTopLevelSecsGroupById = groupBy(deletedTopLevelSecs, (item) => item.id);
|
||||||
|
|
||||||
|
const deletedFoldersData = await folderDAL.delete({ parentId: snapshot.folderId, isReserved: false }, tx);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
deletedFoldersData.map(async (folder) => {
|
||||||
|
const version = await folderVersionDAL.findOne({ folderId: folder.id, version: folder.version }, tx);
|
||||||
|
deletedFoldersChanges.set(folder.id, {
|
||||||
|
id: folder.id,
|
||||||
|
version: folder.version,
|
||||||
|
// Store the version ID if available
|
||||||
|
versionId: version?.id
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// this will remove all secrets and folders on child
|
// this will remove all secrets and folders on child
|
||||||
// due to sql foreign key and link list connection removing the folders removes everything below too
|
// due to sql foreign key and link list connection removing the folders removes everything below too
|
||||||
const deletedFolders = await folderDAL.delete({ parentId: snapshot.folderId, isReserved: false }, tx);
|
const deletedFolders = await folderDAL.delete({ parentId: snapshot.folderId, isReserved: false }, tx);
|
||||||
@@ -489,14 +532,21 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
});
|
});
|
||||||
await secretTagDAL.saveTagsToSecretV2(secretTagsToBeInsert, tx);
|
await secretTagDAL.saveTagsToSecretV2(secretTagsToBeInsert, tx);
|
||||||
const folderVersions = await folderVersionDAL.insertMany(
|
const folderVersions = await folderVersionDAL.insertMany(
|
||||||
folders.map(({ version, name, id, envId }) => ({
|
folders.map(({ version, name, id, envId, description }) => ({
|
||||||
name,
|
name,
|
||||||
version,
|
version,
|
||||||
folderId: id,
|
folderId: id,
|
||||||
envId
|
envId,
|
||||||
|
description
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Track added folders
|
||||||
|
folderVersions.forEach((fv) => {
|
||||||
|
addedFoldersChanges.set(fv.folderId, fv);
|
||||||
|
});
|
||||||
|
|
||||||
const userActorId = actor === ActorType.USER ? actorId : undefined;
|
const userActorId = actor === ActorType.USER ? actorId : undefined;
|
||||||
const identityActorId = actor !== ActorType.USER ? actorId : undefined;
|
const identityActorId = actor !== ActorType.USER ? actorId : undefined;
|
||||||
const actorType = actor || ActorType.PLATFORM;
|
const actorType = actor || ActorType.PLATFORM;
|
||||||
@@ -511,6 +561,11 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
secretVersions.forEach((sv) => {
|
||||||
|
addedSecretsChanges.set(sv.secretId, sv);
|
||||||
|
});
|
||||||
|
|
||||||
await secretVersionV2TagBridgeDAL.insertMany(
|
await secretVersionV2TagBridgeDAL.insertMany(
|
||||||
secretVersions.flatMap(({ secretId, id }) =>
|
secretVersions.flatMap(({ secretId, id }) =>
|
||||||
secretVerTagToBeInsert?.[secretId]?.length
|
secretVerTagToBeInsert?.[secretId]?.length
|
||||||
@@ -522,6 +577,70 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
),
|
),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Compute commit changes
|
||||||
|
// Handle secrets
|
||||||
|
deletedSecretsChanges.forEach((deletedInfo, secretId) => {
|
||||||
|
const addedSecret = addedSecretsChanges.get(secretId);
|
||||||
|
if (addedSecret) {
|
||||||
|
// Secret was deleted and re-added - this is an update only if versions are different
|
||||||
|
if (deletedInfo.versionId !== addedSecret.id) {
|
||||||
|
commitChanges.push({
|
||||||
|
type: CommitType.ADD, // In the commit system, updates are tracked as "add" with isUpdate=true
|
||||||
|
secretVersionId: addedSecret.id,
|
||||||
|
isUpdate: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Remove from addedSecrets since we've handled it
|
||||||
|
addedSecretsChanges.delete(secretId);
|
||||||
|
} else if (deletedInfo.versionId) {
|
||||||
|
// Secret was only deleted
|
||||||
|
commitChanges.push({
|
||||||
|
type: CommitType.DELETE,
|
||||||
|
secretVersionId: deletedInfo.versionId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Add remaining new secrets (not updates)
|
||||||
|
addedSecretsChanges.forEach((addedSecret) => {
|
||||||
|
commitChanges.push({
|
||||||
|
type: CommitType.ADD,
|
||||||
|
secretVersionId: addedSecret.id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle folders
|
||||||
|
deletedFoldersChanges.forEach((deletedInfo, folderId) => {
|
||||||
|
const addedFolder = addedFoldersChanges.get(folderId);
|
||||||
|
if (addedFolder) {
|
||||||
|
// Folder was deleted and re-added - this is an update only if versions are different
|
||||||
|
if (deletedInfo.versionId !== addedFolder.id) {
|
||||||
|
commitChanges.push({
|
||||||
|
type: CommitType.ADD,
|
||||||
|
folderVersionId: addedFolder.id,
|
||||||
|
isUpdate: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Remove from addedFolders since we've handled it
|
||||||
|
addedFoldersChanges.delete(folderId);
|
||||||
|
} else if (deletedInfo.versionId) {
|
||||||
|
// Folder was only deleted
|
||||||
|
commitChanges.push({
|
||||||
|
type: CommitType.DELETE,
|
||||||
|
folderVersionId: deletedInfo.versionId,
|
||||||
|
folderId: deletedInfo.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add remaining new folders (not updates)
|
||||||
|
addedFoldersChanges.forEach((addedFolder) => {
|
||||||
|
commitChanges.push({
|
||||||
|
type: CommitType.ADD,
|
||||||
|
folderVersionId: addedFolder.id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const newSnapshot = await snapshotDAL.create(
|
const newSnapshot = await snapshotDAL.create(
|
||||||
{
|
{
|
||||||
folderId: snapshot.folderId,
|
folderId: snapshot.folderId,
|
||||||
@@ -550,6 +669,22 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
if (commitChanges.length > 0) {
|
||||||
|
await folderCommitService.createCommit(
|
||||||
|
{
|
||||||
|
actor: {
|
||||||
|
type: actorType,
|
||||||
|
metadata: {
|
||||||
|
id: userActorId || identityActorId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
message: "Rollback to snapshot",
|
||||||
|
folderId: snapshot.folderId,
|
||||||
|
changes: commitChanges
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return { ...newSnapshot, snapshotSecrets, snapshotFolders };
|
return { ...newSnapshot, snapshotSecrets, snapshotFolders };
|
||||||
});
|
});
|
||||||
@@ -609,11 +744,12 @@ export const secretSnapshotServiceFactory = ({
|
|||||||
});
|
});
|
||||||
await secretTagDAL.saveTagsToSecret(secretTagsToBeInsert, tx);
|
await secretTagDAL.saveTagsToSecret(secretTagsToBeInsert, tx);
|
||||||
const folderVersions = await folderVersionDAL.insertMany(
|
const folderVersions = await folderVersionDAL.insertMany(
|
||||||
folders.map(({ version, name, id, envId }) => ({
|
folders.map(({ version, name, id, envId, description }) => ({
|
||||||
name,
|
name,
|
||||||
version,
|
version,
|
||||||
folderId: id,
|
folderId: id,
|
||||||
envId
|
envId,
|
||||||
|
description
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const KeyStorePrefixes = {
|
|||||||
KmsOrgDataKeyCreation: "kms-org-data-key-creation-lock",
|
KmsOrgDataKeyCreation: "kms-org-data-key-creation-lock",
|
||||||
WaitUntilReadyKmsOrgKeyCreation: "wait-until-ready-kms-org-key-creation-",
|
WaitUntilReadyKmsOrgKeyCreation: "wait-until-ready-kms-org-key-creation-",
|
||||||
WaitUntilReadyKmsOrgDataKeyCreation: "wait-until-ready-kms-org-data-key-creation-",
|
WaitUntilReadyKmsOrgDataKeyCreation: "wait-until-ready-kms-org-data-key-creation-",
|
||||||
|
FolderTreeCheckpoint: (envId: string) => `folder-tree-checkpoint-${envId}`,
|
||||||
|
|
||||||
WaitUntilReadyProjectEnvironmentOperation: (projectId: string) =>
|
WaitUntilReadyProjectEnvironmentOperation: (projectId: string) =>
|
||||||
`wait-until-ready-project-environments-operation-${projectId}`,
|
`wait-until-ready-project-environments-operation-${projectId}`,
|
||||||
|
|||||||
@@ -626,7 +626,8 @@ export const PROJECTS = {
|
|||||||
autoCapitalization: "Disable or enable auto-capitalization for the project.",
|
autoCapitalization: "Disable or enable auto-capitalization for the project.",
|
||||||
slug: "An optional slug for the project. (must be unique within the organization)",
|
slug: "An optional slug for the project. (must be unique within the organization)",
|
||||||
hasDeleteProtection: "Enable or disable delete protection for the project.",
|
hasDeleteProtection: "Enable or disable delete protection for the project.",
|
||||||
secretSharing: "Enable or disable secret sharing for the project."
|
secretSharing: "Enable or disable secret sharing for the project.",
|
||||||
|
showSnapshotsLegacy: "Enable or disable legacy snapshots for the project."
|
||||||
},
|
},
|
||||||
GET_KEY: {
|
GET_KEY: {
|
||||||
workspaceId: "The ID of the project to get the key from."
|
workspaceId: "The ID of the project to get the key from."
|
||||||
|
|||||||
@@ -261,6 +261,10 @@ const envSchema = z
|
|||||||
DATADOG_SERVICE: zpStr(z.string().optional().default("infisical-core")),
|
DATADOG_SERVICE: zpStr(z.string().optional().default("infisical-core")),
|
||||||
DATADOG_HOSTNAME: zpStr(z.string().optional()),
|
DATADOG_HOSTNAME: zpStr(z.string().optional()),
|
||||||
|
|
||||||
|
// PIT
|
||||||
|
PIT_CHECKPOINT_WINDOW: zpStr(z.string().optional().default("2")),
|
||||||
|
PIT_TREE_CHECKPOINT_WINDOW: zpStr(z.string().optional().default("30")),
|
||||||
|
|
||||||
/* CORS ----------------------------------------------------------------------------- */
|
/* CORS ----------------------------------------------------------------------------- */
|
||||||
CORS_ALLOWED_ORIGINS: zpStr(
|
CORS_ALLOWED_ORIGINS: zpStr(
|
||||||
z
|
z
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export enum QueueName {
|
|||||||
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
|
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
|
||||||
AppConnectionSecretSync = "app-connection-secret-sync",
|
AppConnectionSecretSync = "app-connection-secret-sync",
|
||||||
SecretRotationV2 = "secret-rotation-v2",
|
SecretRotationV2 = "secret-rotation-v2",
|
||||||
|
FolderTreeCheckpoint = "folder-tree-checkpoint",
|
||||||
InvalidateCache = "invalidate-cache",
|
InvalidateCache = "invalidate-cache",
|
||||||
SecretScanningV2 = "secret-scanning-v2"
|
SecretScanningV2 = "secret-scanning-v2"
|
||||||
}
|
}
|
||||||
@@ -94,6 +95,7 @@ export enum QueueJobs {
|
|||||||
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
|
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
|
||||||
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
|
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
|
||||||
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
|
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
|
||||||
|
CreateFolderTreeCheckpoint = "create-folder-tree-checkpoint",
|
||||||
InvalidateCache = "invalidate-cache",
|
InvalidateCache = "invalidate-cache",
|
||||||
SecretScanningV2FullScan = "secret-scanning-v2-full-scan",
|
SecretScanningV2FullScan = "secret-scanning-v2-full-scan",
|
||||||
SecretScanningV2DiffScan = "secret-scanning-v2-diff-scan",
|
SecretScanningV2DiffScan = "secret-scanning-v2-diff-scan",
|
||||||
@@ -209,6 +211,12 @@ export type TQueueJobTypes = {
|
|||||||
name: QueueJobs.ProjectV3Migration;
|
name: QueueJobs.ProjectV3Migration;
|
||||||
payload: { projectId: string };
|
payload: { projectId: string };
|
||||||
};
|
};
|
||||||
|
[QueueName.FolderTreeCheckpoint]: {
|
||||||
|
name: QueueJobs.CreateFolderTreeCheckpoint;
|
||||||
|
payload: {
|
||||||
|
envId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
[QueueName.ImportSecretsFromExternalSource]: {
|
[QueueName.ImportSecretsFromExternalSource]: {
|
||||||
name: QueueJobs.ImportSecretsFromExternalSource;
|
name: QueueJobs.ImportSecretsFromExternalSource;
|
||||||
payload: {
|
payload: {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import { oidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
|||||||
import { oidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
|
import { oidcConfigServiceFactory } from "@app/ee/services/oidc/oidc-config-service";
|
||||||
import { permissionDALFactory } from "@app/ee/services/permission/permission-dal";
|
import { permissionDALFactory } from "@app/ee/services/permission/permission-dal";
|
||||||
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
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 { projectTemplateDALFactory } from "@app/ee/services/project-template/project-template-dal";
|
||||||
import { projectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
|
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";
|
import { projectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||||
@@ -154,6 +155,14 @@ import { externalGroupOrgRoleMappingDALFactory } from "@app/services/external-gr
|
|||||||
import { externalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
|
import { externalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
|
||||||
import { externalMigrationQueueFactory } from "@app/services/external-migration/external-migration-queue";
|
import { externalMigrationQueueFactory } from "@app/services/external-migration/external-migration-queue";
|
||||||
import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
|
import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
|
||||||
|
import { folderCheckpointDALFactory } from "@app/services/folder-checkpoint/folder-checkpoint-dal";
|
||||||
|
import { folderCheckpointResourcesDALFactory } from "@app/services/folder-checkpoint-resources/folder-checkpoint-resources-dal";
|
||||||
|
import { folderCommitDALFactory } from "@app/services/folder-commit/folder-commit-dal";
|
||||||
|
import { folderCommitQueueServiceFactory } from "@app/services/folder-commit/folder-commit-queue";
|
||||||
|
import { folderCommitServiceFactory } from "@app/services/folder-commit/folder-commit-service";
|
||||||
|
import { folderCommitChangesDALFactory } from "@app/services/folder-commit-changes/folder-commit-changes-dal";
|
||||||
|
import { folderTreeCheckpointDALFactory } from "@app/services/folder-tree-checkpoint/folder-tree-checkpoint-dal";
|
||||||
|
import { folderTreeCheckpointResourcesDALFactory } from "@app/services/folder-tree-checkpoint-resources/folder-tree-checkpoint-resources-dal";
|
||||||
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { groupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
import { groupProjectMembershipRoleDALFactory } from "@app/services/group-project/group-project-membership-role-dal";
|
||||||
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
import { groupProjectServiceFactory } from "@app/services/group-project/group-project-service";
|
||||||
@@ -583,6 +592,41 @@ export const registerRoutes = async (
|
|||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
permissionService
|
permissionService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const folderCommitChangesDAL = folderCommitChangesDALFactory(db);
|
||||||
|
const folderCheckpointDAL = folderCheckpointDALFactory(db);
|
||||||
|
const folderCheckpointResourcesDAL = folderCheckpointResourcesDALFactory(db);
|
||||||
|
const folderTreeCheckpointDAL = folderTreeCheckpointDALFactory(db);
|
||||||
|
const folderCommitDAL = folderCommitDALFactory(db);
|
||||||
|
const folderTreeCheckpointResourcesDAL = folderTreeCheckpointResourcesDALFactory(db);
|
||||||
|
const folderCommitQueueService = folderCommitQueueServiceFactory({
|
||||||
|
queueService,
|
||||||
|
folderTreeCheckpointDAL,
|
||||||
|
keyStore,
|
||||||
|
folderTreeCheckpointResourcesDAL,
|
||||||
|
folderCommitDAL,
|
||||||
|
folderDAL
|
||||||
|
});
|
||||||
|
const folderCommitService = folderCommitServiceFactory({
|
||||||
|
folderCommitDAL,
|
||||||
|
folderCommitChangesDAL,
|
||||||
|
folderCheckpointDAL,
|
||||||
|
folderTreeCheckpointDAL,
|
||||||
|
userDAL,
|
||||||
|
identityDAL,
|
||||||
|
folderDAL,
|
||||||
|
folderVersionDAL,
|
||||||
|
secretVersionV2BridgeDAL,
|
||||||
|
projectDAL,
|
||||||
|
folderCheckpointResourcesDAL,
|
||||||
|
secretV2BridgeDAL,
|
||||||
|
folderTreeCheckpointResourcesDAL,
|
||||||
|
folderCommitQueueService,
|
||||||
|
permissionService,
|
||||||
|
kmsService,
|
||||||
|
secretTagDAL,
|
||||||
|
resourceMetadataDAL
|
||||||
|
});
|
||||||
const scimService = scimServiceFactory({
|
const scimService = scimServiceFactory({
|
||||||
licenseService,
|
licenseService,
|
||||||
scimDAL,
|
scimDAL,
|
||||||
@@ -987,6 +1031,7 @@ export const registerRoutes = async (
|
|||||||
projectMembershipDAL,
|
projectMembershipDAL,
|
||||||
projectBotDAL,
|
projectBotDAL,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
|
folderCommitService,
|
||||||
secretBlindIndexDAL,
|
secretBlindIndexDAL,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
@@ -1034,6 +1079,7 @@ export const registerRoutes = async (
|
|||||||
secretReminderRecipientsDAL,
|
secretReminderRecipientsDAL,
|
||||||
orgService,
|
orgService,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
|
folderCommitService,
|
||||||
secretSyncQueue
|
secretSyncQueue
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1110,6 +1156,7 @@ export const registerRoutes = async (
|
|||||||
snapshotDAL,
|
snapshotDAL,
|
||||||
snapshotFolderDAL,
|
snapshotFolderDAL,
|
||||||
snapshotSecretDAL,
|
snapshotSecretDAL,
|
||||||
|
folderCommitService,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
folderVersionDAL,
|
folderVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
@@ -1136,7 +1183,8 @@ export const registerRoutes = async (
|
|||||||
folderVersionDAL,
|
folderVersionDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
snapshotService,
|
snapshotService,
|
||||||
projectDAL
|
projectDAL,
|
||||||
|
folderCommitService
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretImportService = secretImportServiceFactory({
|
const secretImportService = secretImportServiceFactory({
|
||||||
@@ -1161,6 +1209,7 @@ export const registerRoutes = async (
|
|||||||
const secretV2BridgeService = secretV2BridgeServiceFactory({
|
const secretV2BridgeService = secretV2BridgeServiceFactory({
|
||||||
folderDAL,
|
folderDAL,
|
||||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||||
|
folderCommitService,
|
||||||
secretQueueService,
|
secretQueueService,
|
||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
@@ -1204,7 +1253,8 @@ export const registerRoutes = async (
|
|||||||
projectSlackConfigDAL,
|
projectSlackConfigDAL,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
projectMicrosoftTeamsConfigDAL,
|
projectMicrosoftTeamsConfigDAL,
|
||||||
microsoftTeamsService
|
microsoftTeamsService,
|
||||||
|
folderCommitService
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretService = secretServiceFactory({
|
const secretService = secretServiceFactory({
|
||||||
@@ -1291,7 +1341,8 @@ export const registerRoutes = async (
|
|||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
secretVersionV2TagBridgeDAL: secretVersionTagV2BridgeDAL,
|
secretVersionV2TagBridgeDAL: secretVersionTagV2BridgeDAL,
|
||||||
secretVersionV2BridgeDAL,
|
secretVersionV2BridgeDAL,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL,
|
||||||
|
folderCommitService
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretRotationQueue = secretRotationQueueFactory({
|
const secretRotationQueue = secretRotationQueueFactory({
|
||||||
@@ -1303,6 +1354,7 @@ export const registerRoutes = async (
|
|||||||
projectBotService,
|
projectBotService,
|
||||||
secretVersionV2BridgeDAL,
|
secretVersionV2BridgeDAL,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
|
folderCommitService,
|
||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1454,6 +1506,15 @@ export const registerRoutes = async (
|
|||||||
permissionService
|
permissionService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pitService = pitServiceFactory({
|
||||||
|
folderCommitService,
|
||||||
|
secretService,
|
||||||
|
folderService,
|
||||||
|
permissionService,
|
||||||
|
folderDAL,
|
||||||
|
projectEnvDAL
|
||||||
|
});
|
||||||
|
|
||||||
const identityOidcAuthService = identityOidcAuthServiceFactory({
|
const identityOidcAuthService = identityOidcAuthServiceFactory({
|
||||||
identityOidcAuthDAL,
|
identityOidcAuthDAL,
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
@@ -1597,7 +1658,9 @@ export const registerRoutes = async (
|
|||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
queueService,
|
queueService,
|
||||||
secretV2BridgeService,
|
secretV2BridgeService,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL,
|
||||||
|
folderCommitService,
|
||||||
|
folderVersionDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const migrationService = externalMigrationServiceFactory({
|
const migrationService = externalMigrationServiceFactory({
|
||||||
@@ -1707,6 +1770,7 @@ export const registerRoutes = async (
|
|||||||
auditLogService,
|
auditLogService,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
|
folderCommitService,
|
||||||
secretVersionTagV2BridgeDAL,
|
secretVersionTagV2BridgeDAL,
|
||||||
secretVersionV2BridgeDAL,
|
secretVersionV2BridgeDAL,
|
||||||
keyStore,
|
keyStore,
|
||||||
@@ -1895,6 +1959,7 @@ export const registerRoutes = async (
|
|||||||
certificateTemplate: certificateTemplateService,
|
certificateTemplate: certificateTemplateService,
|
||||||
certificateAuthorityCrl: certificateAuthorityCrlService,
|
certificateAuthorityCrl: certificateAuthorityCrlService,
|
||||||
certificateEst: certificateEstService,
|
certificateEst: certificateEstService,
|
||||||
|
pit: pitService,
|
||||||
pkiAlert: pkiAlertService,
|
pkiAlert: pkiAlertService,
|
||||||
pkiCollection: pkiCollectionService,
|
pkiCollection: pkiCollectionService,
|
||||||
pkiSubscriber: pkiSubscriberService,
|
pkiSubscriber: pkiSubscriberService,
|
||||||
@@ -1929,6 +1994,7 @@ export const registerRoutes = async (
|
|||||||
microsoftTeams: microsoftTeamsService,
|
microsoftTeams: microsoftTeamsService,
|
||||||
assumePrivileges: assumePrivilegeService,
|
assumePrivileges: assumePrivilegeService,
|
||||||
githubOrgSync: githubOrgSyncConfigService,
|
githubOrgSync: githubOrgSyncConfigService,
|
||||||
|
folderCommit: folderCommitService,
|
||||||
secretScanningV2: secretScanningV2Service
|
secretScanningV2: secretScanningV2Service
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -262,7 +262,8 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
|
|||||||
kmsCertificateKeyId: true,
|
kmsCertificateKeyId: true,
|
||||||
auditLogsRetentionDays: true,
|
auditLogsRetentionDays: true,
|
||||||
hasDeleteProtection: true,
|
hasDeleteProtection: true,
|
||||||
secretSharing: true
|
secretSharing: true,
|
||||||
|
showSnapshotsLegacy: true
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SanitizedTagSchema = SecretTagsSchema.pick({
|
export const SanitizedTagSchema = SecretTagsSchema.pick({
|
||||||
|
|||||||
@@ -376,7 +376,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.describe(PROJECTS.UPDATE.slug),
|
.describe(PROJECTS.UPDATE.slug),
|
||||||
secretSharing: z.boolean().optional().describe(PROJECTS.UPDATE.secretSharing)
|
secretSharing: z.boolean().optional().describe(PROJECTS.UPDATE.secretSharing),
|
||||||
|
showSnapshotsLegacy: z.boolean().optional().describe(PROJECTS.UPDATE.showSnapshotsLegacy)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -397,7 +398,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
autoCapitalization: req.body.autoCapitalization,
|
autoCapitalization: req.body.autoCapitalization,
|
||||||
hasDeleteProtection: req.body.hasDeleteProtection,
|
hasDeleteProtection: req.body.hasDeleteProtection,
|
||||||
slug: req.body.slug,
|
slug: req.body.slug,
|
||||||
secretSharing: req.body.secretSharing
|
secretSharing: req.body.secretSharing,
|
||||||
|
showSnapshotsLegacy: req.body.showSnapshotsLegacy
|
||||||
},
|
},
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { chunkArray } from "@app/lib/fn";
|
|||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
|
import { CommitType, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
@@ -18,6 +19,7 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
|||||||
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
|
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
|
||||||
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
|
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
|
||||||
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
|
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
|
||||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
import { fnSecretBulkInsert, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
|
import { fnSecretBulkInsert, getAllSecretReferences } from "../secret-v2-bridge/secret-v2-bridge-fns";
|
||||||
@@ -42,6 +44,8 @@ export type TImportDataIntoInfisicalDTO = {
|
|||||||
projectService: Pick<TProjectServiceFactory, "createProject">;
|
projectService: Pick<TProjectServiceFactory, "createProject">;
|
||||||
projectEnvService: Pick<TProjectEnvServiceFactory, "createEnvironment">;
|
projectEnvService: Pick<TProjectEnvServiceFactory, "createEnvironment">;
|
||||||
secretV2BridgeService: Pick<TSecretV2BridgeServiceFactory, "createManySecret">;
|
secretV2BridgeService: Pick<TSecretV2BridgeServiceFactory, "createManySecret">;
|
||||||
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
|
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "create">;
|
||||||
|
|
||||||
input: TImportInfisicalDataCreate;
|
input: TImportInfisicalDataCreate;
|
||||||
};
|
};
|
||||||
@@ -507,6 +511,8 @@ export const importDataIntoInfisicalFn = async ({
|
|||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
|
folderVersionDAL,
|
||||||
|
folderCommitService,
|
||||||
input: { data, actor, actorId, actorOrgId, actorAuthMethod }
|
input: { data, actor, actorId, actorOrgId, actorAuthMethod }
|
||||||
}: TImportDataIntoInfisicalDTO) => {
|
}: TImportDataIntoInfisicalDTO) => {
|
||||||
// Import data to infisical
|
// Import data to infisical
|
||||||
@@ -599,6 +605,36 @@ export const importDataIntoInfisicalFn = async ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const newFolderVersion = await folderVersionDAL.create(
|
||||||
|
{
|
||||||
|
name: newFolder.name,
|
||||||
|
envId: newFolder.envId,
|
||||||
|
version: newFolder.version,
|
||||||
|
folderId: newFolder.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await folderCommitService.createCommit(
|
||||||
|
{
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
metadata: {
|
||||||
|
id: actorId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
message: "Changed by external migration",
|
||||||
|
folderId: parentEnv.rootFolderId,
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
type: CommitType.ADD,
|
||||||
|
folderVersionId: newFolderVersion.id
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
originalToNewFolderId.set(folder.id, {
|
originalToNewFolderId.set(folder.id, {
|
||||||
folderId: newFolder.id,
|
folderId: newFolder.id,
|
||||||
projectId: parentEnv.projectId
|
projectId: parentEnv.projectId
|
||||||
@@ -772,6 +808,7 @@ export const importDataIntoInfisicalFn = async ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
folderCommitService,
|
||||||
actor: {
|
actor: {
|
||||||
type: actor,
|
type: actor,
|
||||||
actorId
|
actorId
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
|||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
|
|
||||||
|
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectServiceFactory } from "../project/project-service";
|
import { TProjectServiceFactory } from "../project/project-service";
|
||||||
@@ -10,6 +11,7 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
|||||||
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
|
import { TProjectEnvServiceFactory } from "../project-env/project-env-service";
|
||||||
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
|
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
import { TSecretFolderVersionDALFactory } from "../secret-folder/secret-folder-version-dal";
|
||||||
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
|
import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal";
|
||||||
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
|
||||||
import { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service";
|
import { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bridge-service";
|
||||||
@@ -36,6 +38,8 @@ export type TExternalMigrationQueueFactoryDep = {
|
|||||||
projectService: Pick<TProjectServiceFactory, "createProject">;
|
projectService: Pick<TProjectServiceFactory, "createProject">;
|
||||||
projectEnvService: Pick<TProjectEnvServiceFactory, "createEnvironment">;
|
projectEnvService: Pick<TProjectEnvServiceFactory, "createEnvironment">;
|
||||||
secretV2BridgeService: Pick<TSecretV2BridgeServiceFactory, "createManySecret">;
|
secretV2BridgeService: Pick<TSecretV2BridgeServiceFactory, "createManySecret">;
|
||||||
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
|
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "create">;
|
||||||
|
|
||||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||||
};
|
};
|
||||||
@@ -56,6 +60,8 @@ export const externalMigrationQueueFactory = ({
|
|||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
|
folderCommitService,
|
||||||
|
folderVersionDAL,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL
|
||||||
}: TExternalMigrationQueueFactoryDep) => {
|
}: TExternalMigrationQueueFactoryDep) => {
|
||||||
const startImport = async (dto: {
|
const startImport = async (dto: {
|
||||||
@@ -114,6 +120,8 @@ export const externalMigrationQueueFactory = ({
|
|||||||
projectService,
|
projectService,
|
||||||
projectEnvService,
|
projectEnvService,
|
||||||
secretV2BridgeService,
|
secretV2BridgeService,
|
||||||
|
folderCommitService,
|
||||||
|
folderVersionDAL,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import {
|
||||||
|
TableName,
|
||||||
|
TFolderCheckpointResources,
|
||||||
|
TFolderCheckpoints,
|
||||||
|
TSecretFolderVersions,
|
||||||
|
TSecretVersionsV2
|
||||||
|
} from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TFolderCheckpointResourcesDALFactory = ReturnType<typeof folderCheckpointResourcesDALFactory>;
|
||||||
|
|
||||||
|
export type ResourceWithCheckpointInfo = TFolderCheckpointResources & {
|
||||||
|
folderCommitId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const folderCheckpointResourcesDALFactory = (db: TDbClient) => {
|
||||||
|
const folderCheckpointResourcesOrm = ormify(db, TableName.FolderCheckpointResources);
|
||||||
|
|
||||||
|
const findByCheckpointId = async (
|
||||||
|
folderCheckpointId: string,
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<
|
||||||
|
(TFolderCheckpointResources & {
|
||||||
|
referencedSecretId?: string;
|
||||||
|
referencedFolderId?: string;
|
||||||
|
folderName?: string;
|
||||||
|
folderVersion?: string;
|
||||||
|
secretKey?: string;
|
||||||
|
secretVersion?: string;
|
||||||
|
})[]
|
||||||
|
> => {
|
||||||
|
try {
|
||||||
|
const docs = await (tx || db.replicaNode())<TFolderCheckpointResources>(TableName.FolderCheckpointResources)
|
||||||
|
.where({ folderCheckpointId })
|
||||||
|
.leftJoin<TSecretVersionsV2>(
|
||||||
|
TableName.SecretVersionV2,
|
||||||
|
`${TableName.FolderCheckpointResources}.secretVersionId`,
|
||||||
|
`${TableName.SecretVersionV2}.id`
|
||||||
|
)
|
||||||
|
.leftJoin<TSecretFolderVersions>(
|
||||||
|
TableName.SecretFolderVersion,
|
||||||
|
`${TableName.FolderCheckpointResources}.folderVersionId`,
|
||||||
|
`${TableName.SecretFolderVersion}.id`
|
||||||
|
)
|
||||||
|
.select(selectAllTableCols(TableName.FolderCheckpointResources))
|
||||||
|
.select(
|
||||||
|
db.ref("secretId").withSchema(TableName.SecretVersionV2).as("referencedSecretId"),
|
||||||
|
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.map((doc) => ({
|
||||||
|
...doc,
|
||||||
|
folderVersion: doc.folderVersion?.toString(),
|
||||||
|
secretVersion: doc.secretVersion?.toString()
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindByCheckpointId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findBySecretVersionId = async (secretVersionId: string, tx?: Knex): Promise<ResourceWithCheckpointInfo[]> => {
|
||||||
|
try {
|
||||||
|
const docs = await (tx || db.replicaNode())<
|
||||||
|
TFolderCheckpointResources & Pick<TFolderCheckpoints, "folderCommitId" | "createdAt">
|
||||||
|
>(TableName.FolderCheckpointResources)
|
||||||
|
.where({ secretVersionId })
|
||||||
|
.select(selectAllTableCols(TableName.FolderCheckpointResources))
|
||||||
|
.join(
|
||||||
|
TableName.FolderCheckpoint,
|
||||||
|
`${TableName.FolderCheckpointResources}.folderCheckpointId`,
|
||||||
|
`${TableName.FolderCheckpoint}.id`
|
||||||
|
)
|
||||||
|
.select(
|
||||||
|
db.ref("folderCommitId").withSchema(TableName.FolderCheckpoint),
|
||||||
|
db.ref("createdAt").withSchema(TableName.FolderCheckpoint)
|
||||||
|
);
|
||||||
|
return docs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindBySecretVersionId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findByFolderVersionId = async (folderVersionId: string, tx?: Knex): Promise<ResourceWithCheckpointInfo[]> => {
|
||||||
|
try {
|
||||||
|
const docs = await (tx || db.replicaNode())<
|
||||||
|
TFolderCheckpointResources & Pick<TFolderCheckpoints, "folderCommitId" | "createdAt">
|
||||||
|
>(TableName.FolderCheckpointResources)
|
||||||
|
.where({ folderVersionId })
|
||||||
|
.select(selectAllTableCols(TableName.FolderCheckpointResources))
|
||||||
|
.join(
|
||||||
|
TableName.FolderCheckpoint,
|
||||||
|
`${TableName.FolderCheckpointResources}.folderCheckpointId`,
|
||||||
|
`${TableName.FolderCheckpoint}.id`
|
||||||
|
)
|
||||||
|
.select(
|
||||||
|
db.ref("folderCommitId").withSchema(TableName.FolderCheckpoint),
|
||||||
|
db.ref("createdAt").withSchema(TableName.FolderCheckpoint)
|
||||||
|
);
|
||||||
|
return docs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindByFolderVersionId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...folderCheckpointResourcesOrm,
|
||||||
|
findByCheckpointId,
|
||||||
|
findBySecretVersionId,
|
||||||
|
findByFolderVersionId
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName, TFolderCheckpoints, TFolderCommits } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TFolderCheckpointDALFactory = ReturnType<typeof folderCheckpointDALFactory>;
|
||||||
|
|
||||||
|
type CheckpointWithCommitInfo = TFolderCheckpoints & {
|
||||||
|
actorMetadata: unknown;
|
||||||
|
actorType: string;
|
||||||
|
message?: string | null;
|
||||||
|
commitDate: Date;
|
||||||
|
folderId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const folderCheckpointDALFactory = (db: TDbClient) => {
|
||||||
|
const folderCheckpointOrm = ormify(db, TableName.FolderCheckpoint);
|
||||||
|
|
||||||
|
const findByCommitId = async (folderCommitId: string, tx?: Knex): Promise<TFolderCheckpoints | undefined> => {
|
||||||
|
try {
|
||||||
|
const doc = await (tx || db.replicaNode())<TFolderCheckpoints>(TableName.FolderCheckpoint)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
.where(buildFindFilter({ folderCommitId }, TableName.FolderCheckpoint))
|
||||||
|
.select(selectAllTableCols(TableName.FolderCheckpoint))
|
||||||
|
.first();
|
||||||
|
return doc;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindByCommitId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findByFolderId = async (folderId: string, limit?: number, tx?: Knex): Promise<CheckpointWithCommitInfo[]> => {
|
||||||
|
try {
|
||||||
|
let query = (tx || db.replicaNode())(TableName.FolderCheckpoint)
|
||||||
|
.join<TFolderCommits>(
|
||||||
|
TableName.FolderCommit,
|
||||||
|
`${TableName.FolderCheckpoint}.folderCommitId`,
|
||||||
|
`${TableName.FolderCommit}.id`
|
||||||
|
)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
.where(buildFindFilter({ folderId }, TableName.FolderCommit))
|
||||||
|
.select(selectAllTableCols(TableName.FolderCheckpoint))
|
||||||
|
.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).as("commitDate"),
|
||||||
|
db.ref("folderId").withSchema(TableName.FolderCommit)
|
||||||
|
)
|
||||||
|
.orderBy(`${TableName.FolderCheckpoint}.createdAt`, "desc");
|
||||||
|
|
||||||
|
if (limit !== undefined) {
|
||||||
|
query = query.limit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await query;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindByFolderId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findLatestByFolderId = async (folderId: string, tx?: Knex): Promise<CheckpointWithCommitInfo | undefined> => {
|
||||||
|
try {
|
||||||
|
const doc = await (tx || db.replicaNode())(TableName.FolderCheckpoint)
|
||||||
|
.join<TFolderCommits>(
|
||||||
|
TableName.FolderCommit,
|
||||||
|
`${TableName.FolderCheckpoint}.folderCommitId`,
|
||||||
|
`${TableName.FolderCommit}.id`
|
||||||
|
)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
.where(buildFindFilter({ folderId }, TableName.FolderCommit))
|
||||||
|
.select(selectAllTableCols(TableName.FolderCheckpoint))
|
||||||
|
.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).as("commitDate"),
|
||||||
|
db.ref("folderId").withSchema(TableName.FolderCommit)
|
||||||
|
)
|
||||||
|
.orderBy(`${TableName.FolderCheckpoint}.createdAt`, "desc")
|
||||||
|
.first();
|
||||||
|
return doc;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindLatestByFolderId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findNearestCheckpoint = async (
|
||||||
|
folderCommitId: bigint,
|
||||||
|
folderId: string,
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<(CheckpointWithCommitInfo & { commitId: bigint }) | undefined> => {
|
||||||
|
try {
|
||||||
|
// Get the checkpoint with the highest commitId that's still less than or equal to our commit
|
||||||
|
const nearestCheckpoint = await (tx || db.replicaNode())(TableName.FolderCheckpoint)
|
||||||
|
.join<TFolderCommits>(
|
||||||
|
TableName.FolderCommit,
|
||||||
|
`${TableName.FolderCheckpoint}.folderCommitId`,
|
||||||
|
`${TableName.FolderCommit}.id`
|
||||||
|
)
|
||||||
|
.where(`${TableName.FolderCommit}.folderId`, "=", folderId)
|
||||||
|
.where(`${TableName.FolderCommit}.commitId`, "<=", folderCommitId.toString())
|
||||||
|
.select(selectAllTableCols(TableName.FolderCheckpoint))
|
||||||
|
.select(
|
||||||
|
db.ref("actorMetadata").withSchema(TableName.FolderCommit),
|
||||||
|
db.ref("actorType").withSchema(TableName.FolderCommit),
|
||||||
|
db.ref("message").withSchema(TableName.FolderCommit),
|
||||||
|
db.ref("commitId").withSchema(TableName.FolderCommit),
|
||||||
|
db.ref("createdAt").withSchema(TableName.FolderCommit).as("commitDate"),
|
||||||
|
db.ref("folderId").withSchema(TableName.FolderCommit)
|
||||||
|
)
|
||||||
|
.orderBy(`${TableName.FolderCommit}.commitId`, "desc")
|
||||||
|
.first();
|
||||||
|
return nearestCheckpoint;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindNearestCheckpoint" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...folderCheckpointOrm,
|
||||||
|
findByCommitId,
|
||||||
|
findByFolderId,
|
||||||
|
findLatestByFolderId,
|
||||||
|
findNearestCheckpoint
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import {
|
||||||
|
TableName,
|
||||||
|
TFolderCommitChanges,
|
||||||
|
TFolderCommits,
|
||||||
|
TProjectEnvironments,
|
||||||
|
TSecretFolderVersions,
|
||||||
|
TSecretVersionsV2
|
||||||
|
} from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TFolderCommitChangesDALFactory = ReturnType<typeof folderCommitChangesDALFactory>;
|
||||||
|
|
||||||
|
// Base type with common fields
|
||||||
|
type BaseCommitChangeInfo = TFolderCommitChanges & {
|
||||||
|
actorMetadata: unknown;
|
||||||
|
actorType: string;
|
||||||
|
message?: string | null;
|
||||||
|
folderId: 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;
|
||||||
|
skipMultilineEncoding?: boolean | null;
|
||||||
|
secretReminderRepeatDays?: number | null;
|
||||||
|
secretReminderNote?: string | null;
|
||||||
|
metadata?: unknown;
|
||||||
|
tags?: string[] | null;
|
||||||
|
secretReminderRecipients?: string[] | null;
|
||||||
|
secretValue: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Folder-specific change
|
||||||
|
export type FolderCommitChange = BaseCommitChangeInfo & {
|
||||||
|
resourceType: "folder";
|
||||||
|
folderName: string;
|
||||||
|
folderVersion: string;
|
||||||
|
folderChangeId: string;
|
||||||
|
versions?: {
|
||||||
|
version: string;
|
||||||
|
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);
|
||||||
|
|
||||||
|
const findByCommitId = async (
|
||||||
|
folderCommitId: string,
|
||||||
|
projectId: string,
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<CommitChangeWithCommitInfo[]> => {
|
||||||
|
try {
|
||||||
|
const docs = await (tx || db.replicaNode())<TFolderCommitChanges>(TableName.FolderCommitChanges)
|
||||||
|
.where(buildFindFilter({ folderCommitId }, TableName.FolderCommitChanges))
|
||||||
|
.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`
|
||||||
|
)
|
||||||
|
.leftJoin<TProjectEnvironments>(
|
||||||
|
TableName.Environment,
|
||||||
|
`${TableName.FolderCommit}.envId`,
|
||||||
|
`${TableName.Environment}.id`
|
||||||
|
)
|
||||||
|
.where((qb) => {
|
||||||
|
if (projectId) {
|
||||||
|
void qb.where(`${TableName.Environment}.projectId`, "=", projectId);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.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) => {
|
||||||
|
// 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<SecretCommitChange[]> => {
|
||||||
|
try {
|
||||||
|
const docs = await (tx || db.replicaNode())<
|
||||||
|
TFolderCommitChanges &
|
||||||
|
Pick<TFolderCommits, "actorMetadata" | "actorType" | "message" | "createdAt" | "folderId">
|
||||||
|
>(TableName.FolderCommitChanges)
|
||||||
|
.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("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
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindBySecretVersionId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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)
|
||||||
|
.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("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
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindByFolderVersionId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...folderCommitChangesOrm,
|
||||||
|
findByCommitId,
|
||||||
|
findBySecretVersionId,
|
||||||
|
findByFolderVersionId
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,513 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import {
|
||||||
|
TableName,
|
||||||
|
TFolderCommitChanges,
|
||||||
|
TFolderCommits,
|
||||||
|
TProjectEnvironments,
|
||||||
|
TSecretFolderVersions,
|
||||||
|
TSecretVersionsV2
|
||||||
|
} from "@app/db/schemas";
|
||||||
|
import { DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TFolderCommitDALFactory = ReturnType<typeof folderCommitDALFactory>;
|
||||||
|
|
||||||
|
export const folderCommitDALFactory = (db: TDbClient) => {
|
||||||
|
const folderCommitOrm = ormify(db, TableName.FolderCommit);
|
||||||
|
const { delete: deleteOp, deleteById, ...restOfOrm } = folderCommitOrm;
|
||||||
|
|
||||||
|
const findByFolderId = async (folderId: string, tx?: Knex): Promise<TFolderCommits[]> => {
|
||||||
|
try {
|
||||||
|
const trx = tx || db.replicaNode();
|
||||||
|
|
||||||
|
// First, get all folder commits
|
||||||
|
const folderCommits = await trx(TableName.FolderCommit)
|
||||||
|
.where({ folderId })
|
||||||
|
.select("*")
|
||||||
|
.orderBy("createdAt", "desc");
|
||||||
|
|
||||||
|
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" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findLatestCommit = async (
|
||||||
|
folderId: string,
|
||||||
|
projectId?: string,
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<TFolderCommits | undefined> => {
|
||||||
|
try {
|
||||||
|
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
|
.where({ folderId })
|
||||||
|
.leftJoin(TableName.Environment, `${TableName.FolderCommit}.envId`, `${TableName.Environment}.id`)
|
||||||
|
.where((qb) => {
|
||||||
|
if (projectId) {
|
||||||
|
void qb.where(`${TableName.Environment}.projectId`, "=", projectId);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.select(selectAllTableCols(TableName.FolderCommit))
|
||||||
|
.orderBy("commitId", "desc")
|
||||||
|
.first();
|
||||||
|
return doc;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindLatestCommit" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findLatestCommitByFolderIds = async (folderIds: string[], tx?: Knex): Promise<TFolderCommits[] | undefined> => {
|
||||||
|
try {
|
||||||
|
// First get max commitId for each folderId
|
||||||
|
const maxCommitIdSubquery = (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
|
.select("folderId")
|
||||||
|
.max("commitId as maxCommitId")
|
||||||
|
.whereIn("folderId", folderIds)
|
||||||
|
.groupBy("folderId");
|
||||||
|
|
||||||
|
// Join with main table to get complete records for each max commitId
|
||||||
|
const docs = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
|
.select(selectAllTableCols(TableName.FolderCommit))
|
||||||
|
// eslint-disable-next-line func-names
|
||||||
|
.join<TFolderCommits>(maxCommitIdSubquery.as("latest"), function () {
|
||||||
|
this.on(`${TableName.FolderCommit}.folderId`, "=", "latest.folderId").andOn(
|
||||||
|
`${TableName.FolderCommit}.commitId`,
|
||||||
|
"=",
|
||||||
|
"latest.maxCommitId"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return docs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindLatestCommitByFolderIds" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findLatestEnvCommit = async (envId: string, tx?: Knex): Promise<TFolderCommits | undefined> => {
|
||||||
|
try {
|
||||||
|
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
|
.where(`${TableName.FolderCommit}.envId`, "=", envId)
|
||||||
|
.select(selectAllTableCols(TableName.FolderCommit))
|
||||||
|
.orderBy("commitId", "desc")
|
||||||
|
.first();
|
||||||
|
return doc;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindLatestCommit" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findMultipleLatestCommits = async (folderIds: string[], tx?: Knex): Promise<TFolderCommits[]> => {
|
||||||
|
try {
|
||||||
|
const knexInstance = tx || db.replicaNode();
|
||||||
|
|
||||||
|
// Get the latest commitId for each folderId
|
||||||
|
const subquery = knexInstance(TableName.FolderCommit)
|
||||||
|
.whereIn("folderId", folderIds)
|
||||||
|
.groupBy("folderId")
|
||||||
|
.select("folderId")
|
||||||
|
.max("commitId as maxCommitId");
|
||||||
|
|
||||||
|
// Then fetch the complete rows matching those latest commits
|
||||||
|
const docs = await knexInstance(TableName.FolderCommit)
|
||||||
|
// eslint-disable-next-line func-names
|
||||||
|
.innerJoin<TFolderCommits>(subquery.as("latest"), function () {
|
||||||
|
this.on(`${TableName.FolderCommit}.folderId`, "=", "latest.folderId").andOn(
|
||||||
|
`${TableName.FolderCommit}.commitId`,
|
||||||
|
"=",
|
||||||
|
"latest.maxCommitId"
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.select(selectAllTableCols(TableName.FolderCommit));
|
||||||
|
|
||||||
|
return docs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindMultipleLatestCommits" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNumberOfCommitsSince = async (folderId: string, folderCommitId: string, tx?: Knex): Promise<number> => {
|
||||||
|
try {
|
||||||
|
const referencedCommit = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
|
.where({ id: folderCommitId })
|
||||||
|
.select("commitId")
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (referencedCommit?.commitId) {
|
||||||
|
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
|
.where({ folderId })
|
||||||
|
.where("commitId", ">", referencedCommit.commitId)
|
||||||
|
.count();
|
||||||
|
return Number(doc?.[0].count);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "getNumberOfCommitsSince" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEnvNumberOfCommitsSince = async (envId: string, folderCommitId: string, tx?: Knex): Promise<number> => {
|
||||||
|
try {
|
||||||
|
const referencedCommit = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
|
.where({ id: folderCommitId })
|
||||||
|
.select("commitId")
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (referencedCommit?.commitId) {
|
||||||
|
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
|
.where(`${TableName.FolderCommit}.envId`, "=", envId)
|
||||||
|
.where("commitId", ">", referencedCommit.commitId)
|
||||||
|
.count();
|
||||||
|
return Number(doc?.[0].count);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "getNumberOfCommitsSince" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findCommitsToRecreate = async (
|
||||||
|
folderId: string,
|
||||||
|
targetCommitNumber: bigint,
|
||||||
|
checkpointCommitNumber: bigint,
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<
|
||||||
|
(TFolderCommits & {
|
||||||
|
changes: (TFolderCommitChanges & {
|
||||||
|
referencedSecretId?: string;
|
||||||
|
referencedFolderId?: string;
|
||||||
|
folderName?: string;
|
||||||
|
folderVersion?: string;
|
||||||
|
secretKey?: string;
|
||||||
|
secretVersion?: string;
|
||||||
|
})[];
|
||||||
|
})[]
|
||||||
|
> => {
|
||||||
|
try {
|
||||||
|
// First get all the commits in the range
|
||||||
|
const commits = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
.where(buildFindFilter({ folderId }, TableName.FolderCommit))
|
||||||
|
.andWhere(`${TableName.FolderCommit}.commitId`, ">", checkpointCommitNumber.toString())
|
||||||
|
.andWhere(`${TableName.FolderCommit}.commitId`, "<=", targetCommitNumber.toString())
|
||||||
|
.select(selectAllTableCols(TableName.FolderCommit))
|
||||||
|
.orderBy(`${TableName.FolderCommit}.commitId`, "asc");
|
||||||
|
|
||||||
|
// If no commits found, return empty array
|
||||||
|
if (!commits.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all the commit IDs
|
||||||
|
const commitIds = commits.map((commit) => commit.id);
|
||||||
|
|
||||||
|
// Get all changes for these commits in a single query
|
||||||
|
const allChanges = await (tx || db.replicaNode())(TableName.FolderCommitChanges)
|
||||||
|
.whereIn(`${TableName.FolderCommitChanges}.folderCommitId`, commitIds)
|
||||||
|
.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("secretId").withSchema(TableName.SecretVersionV2).as("referencedSecretId"),
|
||||||
|
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
|
||||||
|
const changesByCommitId = allChanges.reduce(
|
||||||
|
(acc, change) => {
|
||||||
|
if (!acc[change.folderCommitId]) {
|
||||||
|
acc[change.folderCommitId] = [];
|
||||||
|
}
|
||||||
|
acc[change.folderCommitId].push(change);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, TFolderCommitChanges[]>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Attach changes to each commit
|
||||||
|
return commits.map((commit) => ({
|
||||||
|
...commit,
|
||||||
|
changes: changesByCommitId[commit.id] || []
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindCommitsToRecreate" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findLatestCommitBetween = async ({
|
||||||
|
folderId,
|
||||||
|
startCommitId,
|
||||||
|
endCommitId,
|
||||||
|
tx
|
||||||
|
}: {
|
||||||
|
folderId: string;
|
||||||
|
startCommitId?: string;
|
||||||
|
endCommitId: string;
|
||||||
|
tx?: Knex;
|
||||||
|
}): Promise<TFolderCommits | undefined> => {
|
||||||
|
try {
|
||||||
|
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
|
.where("commitId", "<=", endCommitId)
|
||||||
|
.where({ folderId })
|
||||||
|
.where((qb) => {
|
||||||
|
if (startCommitId) {
|
||||||
|
void qb.where("commitId", ">=", startCommitId);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.select(selectAllTableCols(TableName.FolderCommit))
|
||||||
|
.orderBy("commitId", "desc")
|
||||||
|
.first();
|
||||||
|
return doc;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindLatestCommitBetween" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findAllCommitsBetween = async ({
|
||||||
|
envId,
|
||||||
|
startCommitId,
|
||||||
|
endCommitId,
|
||||||
|
tx
|
||||||
|
}: {
|
||||||
|
envId?: string;
|
||||||
|
startCommitId?: string;
|
||||||
|
endCommitId?: 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);
|
||||||
|
}
|
||||||
|
if (endCommitId) {
|
||||||
|
void qb.where("commitId", "<=", endCommitId);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.select(selectAllTableCols(TableName.FolderCommit))
|
||||||
|
.orderBy("commitId", "desc");
|
||||||
|
return docs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindLatestCommitBetween" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findAllFolderCommitsAfter = async ({
|
||||||
|
envId,
|
||||||
|
startCommitId,
|
||||||
|
tx
|
||||||
|
}: {
|
||||||
|
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" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findPreviousCommitTo = async (
|
||||||
|
folderId: string,
|
||||||
|
commitId: string,
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<TFolderCommits | undefined> => {
|
||||||
|
try {
|
||||||
|
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
|
.where({ folderId })
|
||||||
|
.where("commitId", "<=", commitId)
|
||||||
|
.select(selectAllTableCols(TableName.FolderCommit))
|
||||||
|
.orderBy("commitId", "desc")
|
||||||
|
.first();
|
||||||
|
return doc;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindPreviousCommitTo" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findById = async (id: string, tx?: Knex, projectId?: string): Promise<TFolderCommits> => {
|
||||||
|
try {
|
||||||
|
const doc = await (tx || db.replicaNode())(TableName.FolderCommit)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
.where(buildFindFilter({ id }, TableName.FolderCommit))
|
||||||
|
.leftJoin<TProjectEnvironments>(
|
||||||
|
TableName.Environment,
|
||||||
|
`${TableName.FolderCommit}.envId`,
|
||||||
|
`${TableName.Environment}.id`
|
||||||
|
)
|
||||||
|
.where((qb) => {
|
||||||
|
if (projectId) {
|
||||||
|
void qb.where(`${TableName.Environment}.projectId`, "=", projectId);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.select(selectAllTableCols(TableName.FolderCommit))
|
||||||
|
.orderBy("commitId", "desc")
|
||||||
|
.first();
|
||||||
|
if (!doc) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Folder commit not found for ID ${id}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return doc;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindById" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findByFolderIdPaginated = async (
|
||||||
|
folderId: string,
|
||||||
|
options: {
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
search?: string;
|
||||||
|
sort?: "asc" | "desc";
|
||||||
|
} = {},
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<{
|
||||||
|
commits: TFolderCommits[];
|
||||||
|
total: number;
|
||||||
|
hasMore: boolean;
|
||||||
|
}> => {
|
||||||
|
try {
|
||||||
|
const { offset = 0, limit = 20, search, sort = "desc" } = options;
|
||||||
|
const trx = tx || db.replicaNode();
|
||||||
|
|
||||||
|
// Build base query
|
||||||
|
let baseQuery = trx(TableName.FolderCommit).where({ folderId });
|
||||||
|
|
||||||
|
// Add search functionality
|
||||||
|
if (search) {
|
||||||
|
baseQuery = baseQuery.where((qb) => {
|
||||||
|
void qb.whereILike("message", `%${search}%`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get total count
|
||||||
|
const totalResult = await baseQuery.clone().count("*", { as: "count" }).first();
|
||||||
|
const total = Number(totalResult?.count || 0);
|
||||||
|
|
||||||
|
// Get paginated commits
|
||||||
|
const folderCommits = await baseQuery.select("*").orderBy("createdAt", sort).limit(limit).offset(offset);
|
||||||
|
|
||||||
|
if (folderCommits.length === 0) {
|
||||||
|
return { commits: [], total, hasMore: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all commit IDs for changes
|
||||||
|
const commitIds = folderCommits.map((commit) => commit.id);
|
||||||
|
|
||||||
|
// 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[]>
|
||||||
|
);
|
||||||
|
|
||||||
|
const commitsWithChanges = folderCommits.map((commit) => ({
|
||||||
|
...commit,
|
||||||
|
changes: changesMap[commit.id] || []
|
||||||
|
}));
|
||||||
|
|
||||||
|
const hasMore = offset + limit < total;
|
||||||
|
|
||||||
|
return {
|
||||||
|
commits: commitsWithChanges,
|
||||||
|
total,
|
||||||
|
hasMore
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindByFolderIdPaginated" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
findLatestCommit,
|
||||||
|
getNumberOfCommitsSince,
|
||||||
|
findCommitsToRecreate,
|
||||||
|
findMultipleLatestCommits,
|
||||||
|
findAllCommitsBetween,
|
||||||
|
findLatestCommitBetween,
|
||||||
|
findLatestEnvCommit,
|
||||||
|
getEnvNumberOfCommitsSince,
|
||||||
|
findLatestCommitByFolderIds,
|
||||||
|
findAllFolderCommitsAfter,
|
||||||
|
findPreviousCommitTo,
|
||||||
|
findById,
|
||||||
|
findByFolderIdPaginated,
|
||||||
|
findCommitBefore
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TSecretFolders } from "@app/db/schemas";
|
||||||
|
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
|
|
||||||
|
import { TFolderTreeCheckpointDALFactory } from "../folder-tree-checkpoint/folder-tree-checkpoint-dal";
|
||||||
|
import { TFolderTreeCheckpointResourcesDALFactory } from "../folder-tree-checkpoint-resources/folder-tree-checkpoint-resources-dal";
|
||||||
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
import { TFolderCommitDALFactory } from "./folder-commit-dal";
|
||||||
|
|
||||||
|
// Define types for job data
|
||||||
|
type TCreateFolderTreeCheckpointDTO = {
|
||||||
|
envId: string;
|
||||||
|
failedToAcquireLockCount?: number;
|
||||||
|
folderCommitId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TFolderCommitQueueServiceFactoryDep = {
|
||||||
|
queueService: TQueueServiceFactory;
|
||||||
|
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "getItem" | "deleteItem">;
|
||||||
|
folderTreeCheckpointDAL: Pick<
|
||||||
|
TFolderTreeCheckpointDALFactory,
|
||||||
|
"create" | "findLatestByEnvId" | "findNearestCheckpoint"
|
||||||
|
>;
|
||||||
|
folderTreeCheckpointResourcesDAL: Pick<
|
||||||
|
TFolderTreeCheckpointResourcesDALFactory,
|
||||||
|
"insertMany" | "findByTreeCheckpointId"
|
||||||
|
>;
|
||||||
|
folderCommitDAL: Pick<
|
||||||
|
TFolderCommitDALFactory,
|
||||||
|
"findLatestEnvCommit" | "getEnvNumberOfCommitsSince" | "findMultipleLatestCommits" | "findById"
|
||||||
|
>;
|
||||||
|
folderDAL: Pick<TSecretFolderDALFactory, "findByEnvId">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TFolderCommitQueueServiceFactory = ReturnType<typeof folderCommitQueueServiceFactory>;
|
||||||
|
|
||||||
|
export const folderCommitQueueServiceFactory = ({
|
||||||
|
queueService,
|
||||||
|
keyStore,
|
||||||
|
folderTreeCheckpointDAL,
|
||||||
|
folderTreeCheckpointResourcesDAL,
|
||||||
|
folderCommitDAL,
|
||||||
|
folderDAL
|
||||||
|
}: TFolderCommitQueueServiceFactoryDep) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
// Helper function to calculate delay for requeuing
|
||||||
|
const getRequeueDelay = (failureCount?: number) => {
|
||||||
|
if (!failureCount) return 0;
|
||||||
|
|
||||||
|
const baseDelay = 5000;
|
||||||
|
const maxDelay = 30000;
|
||||||
|
|
||||||
|
const delay = Math.min(baseDelay * 2 ** failureCount, maxDelay);
|
||||||
|
const jitter = delay * (0.5 + Math.random() * 0.5);
|
||||||
|
|
||||||
|
return jitter;
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleTreeCheckpoint = async (payload: TCreateFolderTreeCheckpointDTO) => {
|
||||||
|
const { envId, failedToAcquireLockCount = 0 } = payload;
|
||||||
|
|
||||||
|
// Create a unique jobId for each retry to prevent conflicts
|
||||||
|
const jobId =
|
||||||
|
failedToAcquireLockCount > 0 ? `${envId}-retry-${failedToAcquireLockCount}-${Date.now()}` : `${envId}`;
|
||||||
|
|
||||||
|
await queueService.queue(QueueName.FolderTreeCheckpoint, QueueJobs.CreateFolderTreeCheckpoint, payload, {
|
||||||
|
jobId,
|
||||||
|
delay: getRequeueDelay(failedToAcquireLockCount),
|
||||||
|
backoff: {
|
||||||
|
type: "exponential",
|
||||||
|
delay: 3000
|
||||||
|
},
|
||||||
|
removeOnFail: {
|
||||||
|
count: 3
|
||||||
|
},
|
||||||
|
removeOnComplete: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sort folders by hierarchy (copied from the source code)
|
||||||
|
const sortFoldersByHierarchy = (folders: TSecretFolders[]) => {
|
||||||
|
const childrenMap = new Map<string, TSecretFolders[]>();
|
||||||
|
const allFolderIds = new Set<string>();
|
||||||
|
|
||||||
|
folders.forEach((folder) => {
|
||||||
|
if (folder.id) allFolderIds.add(folder.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
folders.forEach((folder) => {
|
||||||
|
if (folder.parentId) {
|
||||||
|
const children = childrenMap.get(folder.parentId) || [];
|
||||||
|
children.push(folder);
|
||||||
|
childrenMap.set(folder.parentId, children);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const rootFolders = folders.filter((folder) => !folder.parentId || !allFolderIds.has(folder.parentId));
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
let currentLevel = rootFolders;
|
||||||
|
|
||||||
|
while (currentLevel.length > 0) {
|
||||||
|
result.push(...currentLevel);
|
||||||
|
|
||||||
|
const nextLevel = [];
|
||||||
|
for (const folder of currentLevel) {
|
||||||
|
if (folder.id) {
|
||||||
|
const children = childrenMap.get(folder.id) || [];
|
||||||
|
nextLevel.push(...children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLevel = nextLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFolderTreeCheckpoint = async (jobData: TCreateFolderTreeCheckpointDTO, tx?: Knex) => {
|
||||||
|
const { envId, folderCommitId, failedToAcquireLockCount = 0 } = jobData;
|
||||||
|
|
||||||
|
logger.info(`Folder tree checkpoint creation started [envId=${envId}] [attempt=${failedToAcquireLockCount + 1}]`);
|
||||||
|
|
||||||
|
// First, try to clear any stale locks before attempting to acquire
|
||||||
|
if (failedToAcquireLockCount > 1) {
|
||||||
|
try {
|
||||||
|
await keyStore.deleteItem(KeyStorePrefixes.FolderTreeCheckpoint(envId));
|
||||||
|
logger.info(`Cleared potential stale lock for envId ${envId} before attempt ${failedToAcquireLockCount + 1}`);
|
||||||
|
} catch (error) {
|
||||||
|
// This is fine if it fails, we'll still try to acquire the lock
|
||||||
|
logger.info(`No stale lock found for envId ${envId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let lock: Awaited<ReturnType<typeof keyStore.acquireLock>> | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Attempt to acquire the lock with a shorter timeout for first attempts
|
||||||
|
const timeout = failedToAcquireLockCount > 3 ? 60 * 1000 : 15 * 1000;
|
||||||
|
|
||||||
|
logger.info(`Attempting to acquire lock for envId=${envId} with timeout ${timeout}ms`);
|
||||||
|
|
||||||
|
lock = await keyStore.acquireLock([KeyStorePrefixes.FolderTreeCheckpoint(envId)], timeout);
|
||||||
|
|
||||||
|
logger.info(`Successfully acquired lock for envId=${envId}`);
|
||||||
|
} catch (e) {
|
||||||
|
logger.info(
|
||||||
|
`Failed to acquire lock for folder tree checkpoint [envId=${envId}] [attempt=${failedToAcquireLockCount + 1}]`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Requeue with incremented failure count if under max attempts
|
||||||
|
if (failedToAcquireLockCount < 10) {
|
||||||
|
// Force a delay between retries
|
||||||
|
const nextRetryCount = failedToAcquireLockCount + 1;
|
||||||
|
|
||||||
|
logger.info(`Scheduling retry #${nextRetryCount} for folder tree checkpoint [envId=${envId}]`);
|
||||||
|
|
||||||
|
// Create a new job with incremented counter
|
||||||
|
await scheduleTreeCheckpoint({
|
||||||
|
envId,
|
||||||
|
folderCommitId,
|
||||||
|
failedToAcquireLockCount: nextRetryCount
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Max retries reached
|
||||||
|
logger.error(`Maximum lock acquisition attempts (10) reached for envId ${envId}. Giving up.`);
|
||||||
|
// Try to force-clear the lock for next time
|
||||||
|
try {
|
||||||
|
await keyStore.deleteItem(KeyStorePrefixes.FolderTreeCheckpoint(envId));
|
||||||
|
} catch (clearError) {
|
||||||
|
logger.error(clearError, `Failed to clear lock after maximum retries for envId=${envId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lock) {
|
||||||
|
logger.error(`Lock is undefined after acquisition for envId=${envId}. This should never happen.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info(`Processing tree checkpoint data for envId=${envId}`);
|
||||||
|
|
||||||
|
const latestTreeCheckpoint = await folderTreeCheckpointDAL.findLatestByEnvId(envId, tx);
|
||||||
|
|
||||||
|
let latestCommit;
|
||||||
|
if (folderCommitId) {
|
||||||
|
latestCommit = await folderCommitDAL.findById(folderCommitId, tx);
|
||||||
|
} else {
|
||||||
|
latestCommit = await folderCommitDAL.findLatestEnvCommit(envId, tx);
|
||||||
|
}
|
||||||
|
if (!latestCommit) {
|
||||||
|
logger.info(`Latest commit ID not found for envId ${envId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const latestCommitId = latestCommit.id;
|
||||||
|
|
||||||
|
if (latestTreeCheckpoint) {
|
||||||
|
const commitsSinceLastCheckpoint = await folderCommitDAL.getEnvNumberOfCommitsSince(
|
||||||
|
envId,
|
||||||
|
latestTreeCheckpoint.folderCommitId,
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
if (commitsSinceLastCheckpoint < Number(appCfg.PIT_TREE_CHECKPOINT_WINDOW)) {
|
||||||
|
logger.info(
|
||||||
|
`Commits since last checkpoint ${commitsSinceLastCheckpoint} is less than ${appCfg.PIT_TREE_CHECKPOINT_WINDOW}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const folders = await folderDAL.findByEnvId(envId, tx);
|
||||||
|
const sortedFolders = sortFoldersByHierarchy(folders);
|
||||||
|
const filteredFoldersIds = sortedFolders.filter((folder) => !folder.isReserved).map((folder) => folder.id);
|
||||||
|
|
||||||
|
const folderCommits = await folderCommitDAL.findMultipleLatestCommits(filteredFoldersIds, tx);
|
||||||
|
const folderTreeCheckpoint = await folderTreeCheckpointDAL.create(
|
||||||
|
{
|
||||||
|
folderCommitId: latestCommitId
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
await folderTreeCheckpointResourcesDAL.insertMany(
|
||||||
|
folderCommits.map((folderCommit) => ({
|
||||||
|
folderTreeCheckpointId: folderTreeCheckpoint.id,
|
||||||
|
folderId: folderCommit.folderId,
|
||||||
|
folderCommitId: folderCommit.id
|
||||||
|
})),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(`Folder tree checkpoint created successfully: ${folderTreeCheckpoint.id}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, `Error processing folder tree checkpoint [envId=${envId}]`);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// Always release the lock
|
||||||
|
try {
|
||||||
|
if (lock) {
|
||||||
|
await lock.release();
|
||||||
|
logger.info(`Released lock for folder tree checkpoint [envId=${envId}]`);
|
||||||
|
} else {
|
||||||
|
logger.error(`No lock to release for envId=${envId}. This should never happen.`);
|
||||||
|
}
|
||||||
|
} catch (releaseError) {
|
||||||
|
logger.error(releaseError, `Error releasing lock for folder tree checkpoint [envId=${envId}]`);
|
||||||
|
// Try to force delete the lock if release fails
|
||||||
|
try {
|
||||||
|
await keyStore.deleteItem(KeyStorePrefixes.FolderTreeCheckpoint(envId));
|
||||||
|
logger.info(`Force deleted lock after release failure for envId=${envId}`);
|
||||||
|
} catch (deleteError) {
|
||||||
|
logger.error(deleteError, `Failed to force delete lock after release failure for envId=${envId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
queueService.start(QueueName.FolderTreeCheckpoint, async (job) => {
|
||||||
|
try {
|
||||||
|
if (job.name === QueueJobs.CreateFolderTreeCheckpoint) {
|
||||||
|
const jobData = job.data as TCreateFolderTreeCheckpointDTO;
|
||||||
|
await createFolderTreeCheckpoint(jobData);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error, "Error creating folder tree checkpoint:");
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
scheduleTreeCheckpoint: (envId: string) => scheduleTreeCheckpoint({ envId }),
|
||||||
|
createFolderTreeCheckpoint: (envId: string, folderCommitId?: string, tx?: Knex) =>
|
||||||
|
createFolderTreeCheckpoint({ envId, folderCommitId }, tx)
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
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(),
|
||||||
|
tags: z.array(z.string()).nullable().optional(),
|
||||||
|
metadata: z.unknown().nullable().optional(),
|
||||||
|
secretValue: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Folder-specific versions schema
|
||||||
|
const folderVersionSchema = z.object({
|
||||||
|
version: z.string().optional(),
|
||||||
|
name: z.string().optional(),
|
||||||
|
description: z.string().optional().nullable()
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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()]),
|
||||||
|
isLatest: z.boolean().default(false),
|
||||||
|
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(),
|
||||||
|
secretVersion: z.union([z.string(), z.number()]),
|
||||||
|
secretId: z.string(),
|
||||||
|
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(),
|
||||||
|
folderVersion: z.union([z.string(), z.number()]),
|
||||||
|
versions: z.array(folderVersionSchema).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Discriminated union for resource changes
|
||||||
|
export const resourceChangeSchema = z.discriminatedUnion("type", [
|
||||||
|
secretResourceChangeSchema,
|
||||||
|
folderResourceChangeSchema
|
||||||
|
]);
|
||||||
@@ -0,0 +1,671 @@
|
|||||||
|
/* 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";
|
||||||
|
|
||||||
|
import { ProjectType, TSecretFolderVersions, TSecretVersionsV2 } from "@app/db/schemas";
|
||||||
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
import {
|
||||||
|
ChangeType,
|
||||||
|
CommitType,
|
||||||
|
folderCommitServiceFactory,
|
||||||
|
ResourceChange,
|
||||||
|
TFolderCommitServiceFactory
|
||||||
|
} from "./folder-commit-service";
|
||||||
|
|
||||||
|
// Mock config
|
||||||
|
vi.mock("@app/lib/config/env", () => ({
|
||||||
|
getConfig: () => ({
|
||||||
|
PIT_CHECKPOINT_WINDOW: 5,
|
||||||
|
PIT_TREE_CHECKPOINT_WINDOW: 10
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock logger
|
||||||
|
vi.mock("@app/lib/logger", () => ({
|
||||||
|
logger: {
|
||||||
|
info: vi.fn(),
|
||||||
|
error: vi.fn()
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("folderCommitServiceFactory", () => {
|
||||||
|
// Properly type the mock functions
|
||||||
|
type TransactionCallback<T> = (trx: Knex) => Promise<T>;
|
||||||
|
|
||||||
|
// Mock dependencies
|
||||||
|
const mockFolderCommitDAL = {
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
findById: vi.fn().mockResolvedValue({}),
|
||||||
|
findByFolderId: vi.fn().mockResolvedValue([]),
|
||||||
|
findLatestCommit: vi.fn().mockResolvedValue({}),
|
||||||
|
transaction: vi.fn().mockImplementation(<T>(callback: TransactionCallback<T>) => callback({} as Knex)),
|
||||||
|
getNumberOfCommitsSince: vi.fn().mockResolvedValue(0),
|
||||||
|
getEnvNumberOfCommitsSince: vi.fn().mockResolvedValue(0),
|
||||||
|
findCommitsToRecreate: vi.fn().mockResolvedValue([]),
|
||||||
|
findMultipleLatestCommits: vi.fn().mockResolvedValue([]),
|
||||||
|
findLatestCommitBetween: vi.fn().mockResolvedValue({}),
|
||||||
|
findAllCommitsBetween: vi.fn().mockResolvedValue([]),
|
||||||
|
findLatestEnvCommit: vi.fn().mockResolvedValue({}),
|
||||||
|
findLatestCommitByFolderIds: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockKmsService = {
|
||||||
|
createCipherPairWithDataKey: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFolderCommitChangesDAL = {
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
findByCommitId: vi.fn().mockResolvedValue([]),
|
||||||
|
insertMany: vi.fn().mockResolvedValue([])
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFolderCheckpointDAL = {
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
findByFolderId: vi.fn().mockResolvedValue([]),
|
||||||
|
findLatestByFolderId: vi.fn().mockResolvedValue(null),
|
||||||
|
findNearestCheckpoint: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFolderCheckpointResourcesDAL = {
|
||||||
|
insertMany: vi.fn().mockResolvedValue([]),
|
||||||
|
findByCheckpointId: vi.fn().mockResolvedValue([])
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFolderTreeCheckpointDAL = {
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
findByProjectId: vi.fn().mockResolvedValue([]),
|
||||||
|
findLatestByProjectId: vi.fn().mockResolvedValue({}),
|
||||||
|
findNearestCheckpoint: vi.fn().mockResolvedValue({}),
|
||||||
|
findLatestByEnvId: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFolderTreeCheckpointResourcesDAL = {
|
||||||
|
insertMany: vi.fn().mockResolvedValue([]),
|
||||||
|
findByTreeCheckpointId: vi.fn().mockResolvedValue([])
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockUserDAL = {
|
||||||
|
findById: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockIdentityDAL = {
|
||||||
|
findById: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFolderDAL = {
|
||||||
|
findByParentId: vi.fn().mockResolvedValue([]),
|
||||||
|
findByProjectId: vi.fn().mockResolvedValue([]),
|
||||||
|
deleteById: vi.fn().mockResolvedValue({}),
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
updateById: vi.fn().mockResolvedValue({}),
|
||||||
|
update: vi.fn().mockResolvedValue({}),
|
||||||
|
find: vi.fn().mockResolvedValue([]),
|
||||||
|
findById: vi.fn().mockResolvedValue({}),
|
||||||
|
findByEnvId: vi.fn().mockResolvedValue([]),
|
||||||
|
findFoldersByRootAndIds: vi.fn().mockResolvedValue([])
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFolderVersionDAL = {
|
||||||
|
findLatestFolderVersions: vi.fn().mockResolvedValue({}),
|
||||||
|
findById: vi.fn().mockResolvedValue({}),
|
||||||
|
deleteById: vi.fn().mockResolvedValue({}),
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
updateById: vi.fn().mockResolvedValue({}),
|
||||||
|
find: vi.fn().mockResolvedValue({}), // Changed from [] to {} to match Object.values() expectation
|
||||||
|
findByIdsWithLatestVersion: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSecretVersionV2BridgeDAL = {
|
||||||
|
findLatestVersionByFolderId: vi.fn().mockResolvedValue([]),
|
||||||
|
findById: vi.fn().mockResolvedValue({}),
|
||||||
|
deleteById: vi.fn().mockResolvedValue({}),
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
updateById: vi.fn().mockResolvedValue({}),
|
||||||
|
find: vi.fn().mockResolvedValue([]),
|
||||||
|
findByIdsWithLatestVersion: vi.fn().mockResolvedValue({}),
|
||||||
|
findLatestVersionMany: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSecretV2BridgeDAL = {
|
||||||
|
deleteById: vi.fn().mockResolvedValue({}),
|
||||||
|
create: vi.fn().mockResolvedValue({}),
|
||||||
|
updateById: vi.fn().mockResolvedValue({}),
|
||||||
|
update: vi.fn().mockResolvedValue({}),
|
||||||
|
insertMany: vi.fn().mockResolvedValue([]),
|
||||||
|
invalidateSecretCacheByProjectId: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockProjectDAL = {
|
||||||
|
findById: vi.fn().mockResolvedValue({}),
|
||||||
|
findProjectByEnvId: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockFolderCommitQueueService = {
|
||||||
|
scheduleTreeCheckpoint: vi.fn().mockResolvedValue({}),
|
||||||
|
createFolderTreeCheckpoint: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockPermissionService = {
|
||||||
|
getProjectPermission: vi.fn().mockResolvedValue({})
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockSecretTagDAL = {
|
||||||
|
findSecretTagsByVersionId: vi.fn().mockResolvedValue([]),
|
||||||
|
saveTagsToSecretV2: vi.fn().mockResolvedValue([]),
|
||||||
|
findSecretTagsBySecretId: vi.fn().mockResolvedValue([]),
|
||||||
|
deleteTagsToSecretV2: vi.fn().mockResolvedValue([]),
|
||||||
|
saveTagsToSecretVersionV2: vi.fn().mockResolvedValue([])
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockResourceMetadataDAL = {
|
||||||
|
find: vi.fn().mockResolvedValue([]),
|
||||||
|
insertMany: vi.fn().mockResolvedValue([]),
|
||||||
|
delete: 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,
|
||||||
|
// @ts-expect-error - Mock implementation doesn't need all interface methods for testing
|
||||||
|
permissionService: mockPermissionService,
|
||||||
|
kmsService: mockKmsService,
|
||||||
|
secretTagDAL: mockSecretTagDAL,
|
||||||
|
resourceMetadataDAL: mockResourceMetadataDAL
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createCommit", () => {
|
||||||
|
it("should successfully create a commit with user actor", async () => {
|
||||||
|
// Arrange
|
||||||
|
const userData = { id: "user-id", username: "testuser" };
|
||||||
|
const folderData = { id: "folder-id", envId: "env-id" };
|
||||||
|
const commitData = { id: "commit-id", folderId: "folder-id" };
|
||||||
|
|
||||||
|
mockUserDAL.findById.mockResolvedValue(userData);
|
||||||
|
mockFolderDAL.findById.mockResolvedValue(folderData);
|
||||||
|
mockFolderCommitDAL.create.mockResolvedValue(commitData);
|
||||||
|
mockFolderCheckpointDAL.findLatestByFolderId.mockResolvedValue(null);
|
||||||
|
mockFolderCommitDAL.findLatestCommit.mockResolvedValue({ id: "latest-commit-id" });
|
||||||
|
mockFolderDAL.findByParentId.mockResolvedValue([]);
|
||||||
|
mockSecretVersionV2BridgeDAL.findLatestVersionByFolderId.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
actor: {
|
||||||
|
type: ActorType.USER,
|
||||||
|
metadata: { id: userData.id }
|
||||||
|
},
|
||||||
|
message: "Test commit",
|
||||||
|
folderId: folderData.id,
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
type: CommitType.ADD,
|
||||||
|
secretVersionId: "secret-version-1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await folderCommitService.createCommit(data);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockUserDAL.findById).toHaveBeenCalledWith(userData.id, undefined);
|
||||||
|
expect(mockFolderDAL.findById).toHaveBeenCalledWith(folderData.id, undefined);
|
||||||
|
expect(mockFolderCommitDAL.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
actorType: ActorType.USER,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
actorMetadata: expect.objectContaining({ name: userData.username }),
|
||||||
|
message: data.message,
|
||||||
|
folderId: data.folderId,
|
||||||
|
envId: folderData.envId
|
||||||
|
}),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(mockFolderCommitChangesDAL.insertMany).toHaveBeenCalledWith(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
folderCommitId: commitData.id,
|
||||||
|
changeType: data.changes[0].type,
|
||||||
|
secretVersionId: data.changes[0].secretVersionId
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(mockFolderCommitQueueService.scheduleTreeCheckpoint).toHaveBeenCalledWith(folderData.envId);
|
||||||
|
expect(result).toEqual(commitData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfully create a commit with identity actor", async () => {
|
||||||
|
// Arrange
|
||||||
|
const identityData = { id: "identity-id", name: "testidentity" };
|
||||||
|
const folderData = { id: "folder-id", envId: "env-id" };
|
||||||
|
const commitData = { id: "commit-id", folderId: "folder-id" };
|
||||||
|
|
||||||
|
mockIdentityDAL.findById.mockResolvedValue(identityData);
|
||||||
|
mockFolderDAL.findById.mockResolvedValue(folderData);
|
||||||
|
mockFolderCommitDAL.create.mockResolvedValue(commitData);
|
||||||
|
mockFolderCheckpointDAL.findLatestByFolderId.mockResolvedValue(null);
|
||||||
|
mockFolderCommitDAL.findLatestCommit.mockResolvedValue({ id: "latest-commit-id" });
|
||||||
|
mockFolderDAL.findByParentId.mockResolvedValue([]);
|
||||||
|
mockSecretVersionV2BridgeDAL.findLatestVersionByFolderId.mockResolvedValue([]);
|
||||||
|
|
||||||
|
// Mock folderVersionDAL.find to return an object with folder version data
|
||||||
|
mockFolderVersionDAL.find.mockResolvedValue({
|
||||||
|
"folder-version-1": {
|
||||||
|
id: "folder-version-1",
|
||||||
|
folderId: "sub-folder-id",
|
||||||
|
envId: "env-id",
|
||||||
|
name: "Test Folder",
|
||||||
|
version: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
actor: {
|
||||||
|
type: ActorType.IDENTITY,
|
||||||
|
metadata: { id: identityData.id }
|
||||||
|
},
|
||||||
|
message: "Test commit",
|
||||||
|
folderId: folderData.id,
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
type: CommitType.ADD,
|
||||||
|
folderVersionId: "folder-version-1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
omitIgnoreFilter: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await folderCommitService.createCommit(data);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockIdentityDAL.findById).toHaveBeenCalledWith(identityData.id, undefined);
|
||||||
|
expect(mockFolderDAL.findById).toHaveBeenCalledWith(folderData.id, undefined);
|
||||||
|
expect(mockFolderCommitDAL.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
actorType: ActorType.IDENTITY,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
actorMetadata: expect.objectContaining({ name: identityData.name }),
|
||||||
|
message: data.message,
|
||||||
|
folderId: data.folderId,
|
||||||
|
envId: folderData.envId
|
||||||
|
}),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(mockFolderCommitChangesDAL.insertMany).toHaveBeenCalledWith(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
folderCommitId: commitData.id,
|
||||||
|
changeType: data.changes[0].type,
|
||||||
|
folderVersionId: data.changes[0].folderVersionId
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(mockFolderCommitQueueService.scheduleTreeCheckpoint).toHaveBeenCalledWith(folderData.envId);
|
||||||
|
expect(result).toEqual(commitData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw NotFoundError when folder does not exist", async () => {
|
||||||
|
// Arrange
|
||||||
|
mockFolderDAL.findById.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
actor: {
|
||||||
|
type: ActorType.PLATFORM
|
||||||
|
},
|
||||||
|
message: "Test commit",
|
||||||
|
folderId: "non-existent-folder",
|
||||||
|
changes: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(folderCommitService.createCommit(data)).rejects.toThrow(NotFoundError);
|
||||||
|
expect(mockFolderDAL.findById).toHaveBeenCalledWith("non-existent-folder", undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("addCommitChange", () => {
|
||||||
|
it("should successfully add a change to an existing commit", async () => {
|
||||||
|
// Arrange
|
||||||
|
const commitData = { id: "commit-id", folderId: "folder-id" };
|
||||||
|
const changeData = { id: "change-id", folderCommitId: "commit-id" };
|
||||||
|
|
||||||
|
mockFolderCommitDAL.findById.mockResolvedValue(commitData);
|
||||||
|
mockFolderCommitChangesDAL.create.mockResolvedValue(changeData);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
folderCommitId: commitData.id,
|
||||||
|
changeType: CommitType.ADD,
|
||||||
|
secretVersionId: "secret-version-1"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await folderCommitService.addCommitChange(data);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockFolderCommitDAL.findById).toHaveBeenCalledWith(commitData.id, undefined);
|
||||||
|
expect(mockFolderCommitChangesDAL.create).toHaveBeenCalledWith(data, undefined);
|
||||||
|
expect(result).toEqual(changeData);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw BadRequestError when neither secretVersionId nor folderVersionId is provided", async () => {
|
||||||
|
// Arrange
|
||||||
|
const data = {
|
||||||
|
folderCommitId: "commit-id",
|
||||||
|
changeType: CommitType.ADD
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(folderCommitService.addCommitChange(data)).rejects.toThrow(BadRequestError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw NotFoundError when commit does not exist", async () => {
|
||||||
|
// Arrange
|
||||||
|
mockFolderCommitDAL.findById.mockResolvedValue(null);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
folderCommitId: "non-existent-commit",
|
||||||
|
changeType: CommitType.ADD,
|
||||||
|
secretVersionId: "secret-version-1"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(folderCommitService.addCommitChange(data)).rejects.toThrow(NotFoundError);
|
||||||
|
expect(mockFolderCommitDAL.findById).toHaveBeenCalledWith("non-existent-commit", undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note: reconstructFolderState is an internal function not exposed in the public API
|
||||||
|
// We'll test it indirectly through compareFolderStates
|
||||||
|
|
||||||
|
describe("compareFolderStates", () => {
|
||||||
|
it("should mark all resources as creates when currentCommitId is not provided", async () => {
|
||||||
|
// Arrange
|
||||||
|
const targetCommitId = "target-commit-id";
|
||||||
|
const targetCommit = { id: targetCommitId, commitId: 1, folderId: "folder-id" };
|
||||||
|
|
||||||
|
mockFolderCommitDAL.findById.mockResolvedValue(targetCommit);
|
||||||
|
// Mock how compareFolderStates would process the results internally
|
||||||
|
mockFolderCheckpointDAL.findNearestCheckpoint.mockResolvedValue({ id: "checkpoint-id", commitId: "hash-0" });
|
||||||
|
mockFolderCheckpointResourcesDAL.findByCheckpointId.mockResolvedValue([
|
||||||
|
{ secretVersionId: "secret-version-1", referencedSecretId: "secret-1" },
|
||||||
|
{ folderVersionId: "folder-version-1", referencedFolderId: "folder-1" }
|
||||||
|
]);
|
||||||
|
mockFolderCommitDAL.findCommitsToRecreate.mockResolvedValue([]);
|
||||||
|
mockProjectDAL.findProjectByEnvId.mockResolvedValue({
|
||||||
|
id: "project-id",
|
||||||
|
name: "test-project",
|
||||||
|
type: ProjectType.SecretManager
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await folderCommitService.compareFolderStates({
|
||||||
|
targetCommitId
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockFolderCommitDAL.findById).toHaveBeenCalledWith(targetCommitId, undefined);
|
||||||
|
|
||||||
|
// Verify we get resources marked as create
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
changeType: "create",
|
||||||
|
commitId: targetCommit.commitId
|
||||||
|
})
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createFolderCheckpoint", () => {
|
||||||
|
it("should successfully create a checkpoint when force is true", async () => {
|
||||||
|
// Arrange
|
||||||
|
const folderCommitId = "commit-id";
|
||||||
|
const folderId = "folder-id";
|
||||||
|
const checkpointData = { id: "checkpoint-id", folderCommitId };
|
||||||
|
|
||||||
|
mockFolderDAL.findByParentId.mockResolvedValue([{ id: "subfolder-id" }]);
|
||||||
|
mockFolderVersionDAL.findLatestFolderVersions.mockResolvedValue({ "subfolder-id": { id: "folder-version-1" } });
|
||||||
|
mockSecretVersionV2BridgeDAL.findLatestVersionByFolderId.mockResolvedValue([{ id: "secret-version-1" }]);
|
||||||
|
mockFolderCheckpointDAL.create.mockResolvedValue(checkpointData);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await folderCommitService.createFolderCheckpoint({
|
||||||
|
folderId,
|
||||||
|
folderCommitId,
|
||||||
|
force: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockFolderCheckpointDAL.create).toHaveBeenCalledWith({ folderCommitId }, undefined);
|
||||||
|
expect(mockFolderCheckpointResourcesDAL.insertMany).toHaveBeenCalled();
|
||||||
|
expect(result).toBe(folderCommitId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("deepRollbackFolder", () => {
|
||||||
|
it("should throw NotFoundError when commit doesn't exist", async () => {
|
||||||
|
// Arrange
|
||||||
|
const targetCommitId = "non-existent-commit";
|
||||||
|
const envId = "env-id";
|
||||||
|
const actorId = "user-id";
|
||||||
|
const actorType = ActorType.USER;
|
||||||
|
const projectId = "project-id";
|
||||||
|
|
||||||
|
// Mock the transaction to properly handle the error
|
||||||
|
mockFolderCommitDAL.transaction.mockImplementation(async (callback) => {
|
||||||
|
return await callback({} as Knex);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock findById to return null inside the transaction
|
||||||
|
mockFolderCommitDAL.findById.mockResolvedValue(null);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await expect(
|
||||||
|
folderCommitService.deepRollbackFolder(targetCommitId, envId, actorId, actorType, projectId)
|
||||||
|
).rejects.toThrow(NotFoundError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createFolderTreeCheckpoint", () => {
|
||||||
|
it("should create a tree checkpoint when checkpoint window is exceeded", async () => {
|
||||||
|
// Arrange
|
||||||
|
const envId = "env-id";
|
||||||
|
const folderCommitId = "commit-id";
|
||||||
|
const latestCommit = { id: folderCommitId };
|
||||||
|
const latestTreeCheckpoint = { id: "tree-checkpoint-id", folderCommitId: "old-commit-id" };
|
||||||
|
const folders = [
|
||||||
|
{ id: "folder-1", isReserved: false },
|
||||||
|
{ id: "folder-2", isReserved: false },
|
||||||
|
{ id: "folder-3", isReserved: true } // Reserved folders should be filtered out
|
||||||
|
];
|
||||||
|
const folderCommits = [
|
||||||
|
{ folderId: "folder-1", id: "commit-1" },
|
||||||
|
{ folderId: "folder-2", id: "commit-2" }
|
||||||
|
];
|
||||||
|
const treeCheckpoint = { id: "new-tree-checkpoint-id" };
|
||||||
|
|
||||||
|
mockFolderCommitDAL.findLatestEnvCommit.mockResolvedValue(latestCommit);
|
||||||
|
mockFolderTreeCheckpointDAL.findLatestByEnvId.mockResolvedValue(latestTreeCheckpoint);
|
||||||
|
mockFolderCommitDAL.getEnvNumberOfCommitsSince.mockResolvedValue(15); // More than PIT_TREE_CHECKPOINT_WINDOW (10)
|
||||||
|
mockFolderDAL.findByEnvId.mockResolvedValue(folders);
|
||||||
|
mockFolderCommitDAL.findMultipleLatestCommits.mockResolvedValue(folderCommits);
|
||||||
|
mockFolderTreeCheckpointDAL.create.mockResolvedValue(treeCheckpoint);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await folderCommitService.createFolderTreeCheckpoint(envId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockFolderCommitDAL.findLatestEnvCommit).toHaveBeenCalledWith(envId, undefined);
|
||||||
|
expect(mockFolderTreeCheckpointDAL.create).toHaveBeenCalledWith({ folderCommitId }, undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("applyFolderStateDifferences", () => {
|
||||||
|
it("should process changes correctly", async () => {
|
||||||
|
// Arrange
|
||||||
|
const folderId = "folder-id";
|
||||||
|
const projectId = "project-id";
|
||||||
|
const actorId = "user-id";
|
||||||
|
const actorType = ActorType.USER;
|
||||||
|
|
||||||
|
const differences = [
|
||||||
|
{
|
||||||
|
id: "secret-1",
|
||||||
|
versionId: "v1",
|
||||||
|
changeType: ChangeType.CREATE,
|
||||||
|
commitId: BigInt(1)
|
||||||
|
} as ResourceChange,
|
||||||
|
{
|
||||||
|
id: "folder-1",
|
||||||
|
versionId: "v2",
|
||||||
|
changeType: ChangeType.UPDATE,
|
||||||
|
commitId: BigInt(1),
|
||||||
|
folderName: "Test Folder",
|
||||||
|
folderVersion: "v2"
|
||||||
|
} as ResourceChange
|
||||||
|
];
|
||||||
|
|
||||||
|
const secretVersions = {
|
||||||
|
"secret-1": {
|
||||||
|
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: {}
|
||||||
|
} as TSecretVersionsV2
|
||||||
|
};
|
||||||
|
|
||||||
|
const folderVersions = {
|
||||||
|
"folder-1": {
|
||||||
|
folderId: "folder-1",
|
||||||
|
version: 1,
|
||||||
|
name: "Test Folder",
|
||||||
|
envId: "env-1"
|
||||||
|
} as TSecretFolderVersions
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock folder lookup for the folder being processed
|
||||||
|
mockFolderDAL.findById.mockImplementation((id) => {
|
||||||
|
if (id === folderId) {
|
||||||
|
return Promise.resolve({ id: folderId, envId: "env-1" });
|
||||||
|
}
|
||||||
|
return Promise.resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock latest commit lookup
|
||||||
|
mockFolderCommitDAL.findLatestCommit.mockImplementation((id) => {
|
||||||
|
if (id === folderId) {
|
||||||
|
return Promise.resolve({ id: "latest-commit-id", folderId });
|
||||||
|
}
|
||||||
|
return Promise.resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Make sure findByParentId returns an array, not undefined
|
||||||
|
mockFolderDAL.findByParentId.mockResolvedValue([]);
|
||||||
|
|
||||||
|
// Make sure other required functions return appropriate values
|
||||||
|
mockFolderCheckpointDAL.findLatestByFolderId.mockResolvedValue(null);
|
||||||
|
mockSecretVersionV2BridgeDAL.findLatestVersionByFolderId.mockResolvedValue([]);
|
||||||
|
|
||||||
|
// These mocks need to return objects with an id field
|
||||||
|
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));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await folderCommitService.applyFolderStateDifferences({
|
||||||
|
differences,
|
||||||
|
actorInfo: {
|
||||||
|
actorType,
|
||||||
|
actorId,
|
||||||
|
message: "Applying changes"
|
||||||
|
},
|
||||||
|
folderId,
|
||||||
|
projectId,
|
||||||
|
reconstructNewFolders: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(mockFolderCommitDAL.create).toHaveBeenCalled();
|
||||||
|
expect(mockSecretV2BridgeDAL.invalidateSecretCacheByProjectId).toHaveBeenCalledWith(projectId);
|
||||||
|
|
||||||
|
// Check that we got the right counts
|
||||||
|
expect(result.totalChanges).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
+44
@@ -0,0 +1,44 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName, TFolderTreeCheckpointResources } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TFolderTreeCheckpointResourcesDALFactory = ReturnType<typeof folderTreeCheckpointResourcesDALFactory>;
|
||||||
|
|
||||||
|
type TFolderTreeCheckpointResourcesWithCommitId = TFolderTreeCheckpointResources & {
|
||||||
|
commitId: bigint;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const folderTreeCheckpointResourcesDALFactory = (db: TDbClient) => {
|
||||||
|
const folderTreeCheckpointResourcesOrm = ormify(db, TableName.FolderTreeCheckpointResources);
|
||||||
|
|
||||||
|
const findByTreeCheckpointId = async (
|
||||||
|
folderTreeCheckpointId: string,
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<TFolderTreeCheckpointResourcesWithCommitId[]> => {
|
||||||
|
try {
|
||||||
|
const docs = await (tx || db.replicaNode())<TFolderTreeCheckpointResources>(
|
||||||
|
TableName.FolderTreeCheckpointResources
|
||||||
|
)
|
||||||
|
.join(
|
||||||
|
TableName.FolderCommit,
|
||||||
|
`${TableName.FolderTreeCheckpointResources}.folderCommitId`,
|
||||||
|
`${TableName.FolderCommit}.id`
|
||||||
|
)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
.where(buildFindFilter({ folderTreeCheckpointId }, TableName.FolderTreeCheckpointResources))
|
||||||
|
.select(selectAllTableCols(TableName.FolderTreeCheckpointResources))
|
||||||
|
.select(db.ref("commitId").withSchema(TableName.FolderCommit).as("commitId"));
|
||||||
|
return docs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindByTreeCheckpointId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...folderTreeCheckpointResourcesOrm,
|
||||||
|
findByTreeCheckpointId
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName, TFolderCommits, TFolderTreeCheckpoints } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { buildFindFilter, ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TFolderTreeCheckpointDALFactory = ReturnType<typeof folderTreeCheckpointDALFactory>;
|
||||||
|
|
||||||
|
type TreeCheckpointWithCommitInfo = TFolderTreeCheckpoints & {
|
||||||
|
commitId: bigint;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const folderTreeCheckpointDALFactory = (db: TDbClient) => {
|
||||||
|
const folderTreeCheckpointOrm = ormify(db, TableName.FolderTreeCheckpoint);
|
||||||
|
|
||||||
|
const findByCommitId = async (folderCommitId: string, tx?: Knex): Promise<TFolderTreeCheckpoints | undefined> => {
|
||||||
|
try {
|
||||||
|
const doc = await (tx || db.replicaNode())<TFolderTreeCheckpoints>(TableName.FolderTreeCheckpoint)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
.where(buildFindFilter({ folderCommitId }, TableName.FolderTreeCheckpoint))
|
||||||
|
.select(selectAllTableCols(TableName.FolderTreeCheckpoint))
|
||||||
|
.first();
|
||||||
|
return doc;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindByCommitId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findNearestCheckpoint = async (
|
||||||
|
folderCommitId: bigint,
|
||||||
|
envId: string,
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<TreeCheckpointWithCommitInfo | undefined> => {
|
||||||
|
try {
|
||||||
|
const nearestCheckpoint = await (tx || db.replicaNode())(TableName.FolderTreeCheckpoint)
|
||||||
|
.join<TFolderCommits>(
|
||||||
|
TableName.FolderCommit,
|
||||||
|
`${TableName.FolderTreeCheckpoint}.folderCommitId`,
|
||||||
|
`${TableName.FolderCommit}.id`
|
||||||
|
)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
.where(`${TableName.FolderCommit}.envId`, "=", envId)
|
||||||
|
.andWhere(`${TableName.FolderCommit}.commitId`, "<=", folderCommitId.toString())
|
||||||
|
.select(selectAllTableCols(TableName.FolderTreeCheckpoint))
|
||||||
|
.select(db.ref("commitId").withSchema(TableName.FolderCommit))
|
||||||
|
.orderBy(`${TableName.FolderCommit}.commitId`, "desc")
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return nearestCheckpoint;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindNearestCheckpoint" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findLatestByEnvId = async (envId: string, tx?: Knex): Promise<TFolderTreeCheckpoints | undefined> => {
|
||||||
|
try {
|
||||||
|
const doc = await (tx || db.replicaNode())<TFolderTreeCheckpoints>(TableName.FolderTreeCheckpoint)
|
||||||
|
.join<TFolderCommits>(
|
||||||
|
TableName.FolderCommit,
|
||||||
|
`${TableName.FolderTreeCheckpoint}.folderCommitId`,
|
||||||
|
`${TableName.FolderCommit}.id`
|
||||||
|
)
|
||||||
|
.where(`${TableName.FolderCommit}.envId`, "=", envId)
|
||||||
|
.orderBy(`${TableName.FolderTreeCheckpoint}.createdAt`, "desc")
|
||||||
|
.first();
|
||||||
|
return doc;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindLatestByEnvId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...folderTreeCheckpointOrm,
|
||||||
|
findByCommitId,
|
||||||
|
findNearestCheckpoint,
|
||||||
|
findLatestByEnvId
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
TProjectsUpdate
|
TProjectsUpdate
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
import { BadRequestError, DatabaseError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { Filter, ProjectFilterType, SearchProjectSortBy } from "./project-types";
|
import { Filter, ProjectFilterType, SearchProjectSortBy } from "./project-types";
|
||||||
@@ -475,6 +475,16 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
return { docs, totalCount: Number(docs?.[0]?.count ?? 0) };
|
return { docs, totalCount: Number(docs?.[0]?.count ?? 0) };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findProjectByEnvId = async (envId: string, tx?: Knex) => {
|
||||||
|
const project = await (tx || db.replicaNode())(TableName.Project)
|
||||||
|
.leftJoin(TableName.Environment, `${TableName.Environment}.projectId`, `${TableName.Project}.id`)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
.where(buildFindFilter({ id: envId }, TableName.Environment))
|
||||||
|
.select(selectAllTableCols(TableName.Project))
|
||||||
|
.first();
|
||||||
|
return project;
|
||||||
|
};
|
||||||
|
|
||||||
const countOfOrgProjects = async (orgId: string | null, tx?: Knex) => {
|
const countOfOrgProjects = async (orgId: string | null, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const doc = await (tx || db.replicaNode())(TableName.Project)
|
const doc = await (tx || db.replicaNode())(TableName.Project)
|
||||||
@@ -504,6 +514,7 @@ export const projectDALFactory = (db: TDbClient) => {
|
|||||||
checkProjectUpgradeStatus,
|
checkProjectUpgradeStatus,
|
||||||
getProjectFromSplitId,
|
getProjectFromSplitId,
|
||||||
searchProjects,
|
searchProjects,
|
||||||
|
findProjectByEnvId,
|
||||||
countOfOrgProjects
|
countOfOrgProjects
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -671,7 +671,8 @@ export const projectServiceFactory = ({
|
|||||||
enforceCapitalization: update.autoCapitalization,
|
enforceCapitalization: update.autoCapitalization,
|
||||||
hasDeleteProtection: update.hasDeleteProtection,
|
hasDeleteProtection: update.hasDeleteProtection,
|
||||||
slug: update.slug,
|
slug: update.slug,
|
||||||
secretSharing: update.secretSharing
|
secretSharing: update.secretSharing,
|
||||||
|
showSnapshotsLegacy: update.showSnapshotsLegacy
|
||||||
});
|
});
|
||||||
|
|
||||||
return updatedProject;
|
return updatedProject;
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export type TUpdateProjectDTO = {
|
|||||||
hasDeleteProtection?: boolean;
|
hasDeleteProtection?: boolean;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
secretSharing?: boolean;
|
secretSharing?: boolean;
|
||||||
|
showSnapshotsLegacy?: boolean;
|
||||||
};
|
};
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
|||||||
@@ -488,6 +488,75 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findFoldersByRootAndIds = async ({ rootId, folderIds }: { rootId: string; folderIds: string[] }, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
// First, get all descendant folders of rootId
|
||||||
|
const descendants = await (tx || db.replicaNode())
|
||||||
|
.withRecursive("descendants", (qb) =>
|
||||||
|
qb
|
||||||
|
.select(
|
||||||
|
selectAllTableCols(TableName.SecretFolder),
|
||||||
|
db.raw("0 as depth"),
|
||||||
|
db.raw(`'/' as path`),
|
||||||
|
db.ref(`${TableName.Environment}.slug`).as("environment")
|
||||||
|
)
|
||||||
|
.from(TableName.SecretFolder)
|
||||||
|
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
|
||||||
|
.where(`${TableName.SecretFolder}.id`, rootId)
|
||||||
|
.union((un) => {
|
||||||
|
void un
|
||||||
|
.select(
|
||||||
|
selectAllTableCols(TableName.SecretFolder),
|
||||||
|
db.raw("descendants.depth + 1 as depth"),
|
||||||
|
db.raw(
|
||||||
|
`CONCAT(
|
||||||
|
CASE WHEN descendants.path = '/' THEN '' ELSE descendants.path END,
|
||||||
|
CASE WHEN ${TableName.SecretFolder}."parentId" is NULL THEN '' ELSE CONCAT('/', secret_folders.name) END
|
||||||
|
)`
|
||||||
|
),
|
||||||
|
db.ref("descendants.environment")
|
||||||
|
)
|
||||||
|
.from(TableName.SecretFolder)
|
||||||
|
.where(`${TableName.SecretFolder}.isReserved`, false)
|
||||||
|
.join("descendants", `${TableName.SecretFolder}.parentId`, "descendants.id");
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.select<(TSecretFolders & { path: string; depth: number; environment: string })[]>("*")
|
||||||
|
.from("descendants")
|
||||||
|
.whereIn(`id`, folderIds)
|
||||||
|
.orderBy("depth")
|
||||||
|
.orderBy(`name`);
|
||||||
|
|
||||||
|
return descendants;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindFoldersByRootAndIds" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findByParentId = async (parentId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const folders = await (tx || db.replicaNode())(TableName.SecretFolder)
|
||||||
|
.where({ parentId })
|
||||||
|
.andWhere({ isReserved: false })
|
||||||
|
.select(selectAllTableCols(TableName.SecretFolder));
|
||||||
|
return folders;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "findByParentId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findByEnvId = async (envId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const folders = await (tx || db.replicaNode())(TableName.SecretFolder)
|
||||||
|
.where({ envId })
|
||||||
|
.andWhere({ isReserved: false })
|
||||||
|
.select(selectAllTableCols(TableName.SecretFolder));
|
||||||
|
return folders;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "findByEnvId" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...secretFolderOrm,
|
...secretFolderOrm,
|
||||||
update,
|
update,
|
||||||
@@ -499,6 +568,9 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
|||||||
findClosestFolder,
|
findClosestFolder,
|
||||||
findByProjectId,
|
findByProjectId,
|
||||||
findByMultiEnv,
|
findByMultiEnv,
|
||||||
findByEnvsDeep
|
findByEnvsDeep,
|
||||||
|
findByParentId,
|
||||||
|
findByEnvId,
|
||||||
|
findFoldersByRootAndIds
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
|||||||
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
|
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
|
||||||
import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns";
|
import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns";
|
||||||
|
|
||||||
|
import { ChangeType, CommitType, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TSecretFolderDALFactory } from "./secret-folder-dal";
|
import { TSecretFolderDALFactory } from "./secret-folder-dal";
|
||||||
@@ -29,7 +30,8 @@ type TSecretFolderServiceFactoryDep = {
|
|||||||
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
||||||
folderDAL: TSecretFolderDALFactory;
|
folderDAL: TSecretFolderDALFactory;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs" | "find">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs" | "find">;
|
||||||
folderVersionDAL: TSecretFolderVersionDALFactory;
|
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "findLatestFolderVersions" | "create" | "insertMany" | "find">;
|
||||||
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -41,6 +43,7 @@ export const secretFolderServiceFactory = ({
|
|||||||
permissionService,
|
permissionService,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
folderVersionDAL,
|
folderVersionDAL,
|
||||||
|
folderCommitService,
|
||||||
projectDAL
|
projectDAL
|
||||||
}: TSecretFolderServiceFactoryDep) => {
|
}: TSecretFolderServiceFactoryDep) => {
|
||||||
const createFolder = async ({
|
const createFolder = async ({
|
||||||
@@ -111,15 +114,33 @@ export const secretFolderServiceFactory = ({
|
|||||||
});
|
});
|
||||||
parentFolderId = newFolders.at(-1)?.id as string;
|
parentFolderId = newFolders.at(-1)?.id as string;
|
||||||
const docs = await folderDAL.insertMany(newFolders, tx);
|
const docs = await folderDAL.insertMany(newFolders, tx);
|
||||||
await folderVersionDAL.insertMany(
|
const folderVersions = await folderVersionDAL.insertMany(
|
||||||
docs.map((doc) => ({
|
docs.map((doc) => ({
|
||||||
name: doc.name,
|
name: doc.name,
|
||||||
envId: doc.envId,
|
envId: doc.envId,
|
||||||
version: doc.version,
|
version: doc.version,
|
||||||
folderId: doc.id
|
folderId: doc.id,
|
||||||
|
description: doc.description
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
await folderCommitService.createCommit(
|
||||||
|
{
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
metadata: {
|
||||||
|
id: actorId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
message: "Folder created",
|
||||||
|
folderId: parentFolderId,
|
||||||
|
changes: folderVersions.map((fv) => ({
|
||||||
|
type: CommitType.ADD,
|
||||||
|
folderVersionId: fv.id
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,12 +148,32 @@ export const secretFolderServiceFactory = ({
|
|||||||
{ name, envId: env.id, version: 1, parentId: parentFolderId, description },
|
{ name, envId: env.id, version: 1, parentId: parentFolderId, description },
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
await folderVersionDAL.create(
|
const folderVersion = await folderVersionDAL.create(
|
||||||
{
|
{
|
||||||
name: doc.name,
|
name: doc.name,
|
||||||
envId: doc.envId,
|
envId: doc.envId,
|
||||||
version: doc.version,
|
version: doc.version,
|
||||||
folderId: doc.id
|
folderId: doc.id,
|
||||||
|
description: doc.description
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await folderCommitService.createCommit(
|
||||||
|
{
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
metadata: {
|
||||||
|
id: actorId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
message: "Folder created",
|
||||||
|
folderId: parentFolderId,
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
type: CommitType.ADD,
|
||||||
|
folderVersionId: folderVersion.id
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -225,12 +266,33 @@ export const secretFolderServiceFactory = ({
|
|||||||
{ name, description },
|
{ name, description },
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
await folderVersionDAL.create(
|
const folderVersion = await folderVersionDAL.create(
|
||||||
{
|
{
|
||||||
name: doc.name,
|
name: doc.name,
|
||||||
envId: doc.envId,
|
envId: doc.envId,
|
||||||
version: doc.version,
|
version: doc.version,
|
||||||
folderId: doc.id
|
folderId: doc.id,
|
||||||
|
description: doc.description
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await folderCommitService.createCommit(
|
||||||
|
{
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
metadata: {
|
||||||
|
id: actorId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
message: "Folder updated",
|
||||||
|
folderId: parentFolder.id,
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
type: CommitType.ADD,
|
||||||
|
isUpdate: true,
|
||||||
|
folderVersionId: folderVersion.id
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -321,12 +383,33 @@ export const secretFolderServiceFactory = ({
|
|||||||
{ name, description },
|
{ name, description },
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
await folderVersionDAL.create(
|
const folderVersion = await folderVersionDAL.create(
|
||||||
{
|
{
|
||||||
name: doc.name,
|
name: doc.name,
|
||||||
envId: doc.envId,
|
envId: doc.envId,
|
||||||
version: doc.version,
|
version: doc.version,
|
||||||
folderId: doc.id
|
folderId: doc.id,
|
||||||
|
description: doc.description
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await folderCommitService.createCommit(
|
||||||
|
{
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
metadata: {
|
||||||
|
id: actorId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
message: "Folder updated",
|
||||||
|
folderId: parentFolder.id,
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
type: CommitType.ADD,
|
||||||
|
isUpdate: true,
|
||||||
|
folderVersionId: folderVersion.id
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -381,7 +464,31 @@ export const secretFolderServiceFactory = ({
|
|||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!doc) throw new NotFoundError({ message: `Failed to delete folder with ID '${idOrName}', not found` });
|
if (!doc) throw new NotFoundError({ message: `Failed to delete folder with ID '${idOrName}', not found` });
|
||||||
|
|
||||||
|
const folderVersions = await folderVersionDAL.findLatestFolderVersions([doc.id], tx);
|
||||||
|
|
||||||
|
await folderCommitService.createCommit(
|
||||||
|
{
|
||||||
|
actor: {
|
||||||
|
type: actor,
|
||||||
|
metadata: {
|
||||||
|
id: actorId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
message: "Folder deleted",
|
||||||
|
folderId: parentFolder.id,
|
||||||
|
changes: [
|
||||||
|
{
|
||||||
|
type: CommitType.DELETE,
|
||||||
|
folderVersionId: folderVersions[doc.id].id,
|
||||||
|
folderId: doc.id
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
return doc;
|
return doc;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -665,6 +772,45 @@ export const secretFolderServiceFactory = ({
|
|||||||
return environmentFolders;
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
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) => ({
|
||||||
|
version: v.version?.toString() || "1",
|
||||||
|
name: v.name,
|
||||||
|
description: v.description
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createFolder,
|
createFolder,
|
||||||
updateFolder,
|
updateFolder,
|
||||||
@@ -675,6 +821,8 @@ export const secretFolderServiceFactory = ({
|
|||||||
getProjectFolderCount,
|
getProjectFolderCount,
|
||||||
getFoldersMultiEnv,
|
getFoldersMultiEnv,
|
||||||
getFoldersDeepByEnvs,
|
getFoldersDeepByEnvs,
|
||||||
getProjectEnvironmentsFolders
|
getProjectEnvironmentsFolders,
|
||||||
|
getFolderVersionsByIds,
|
||||||
|
getFolderVersions
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => {
|
|||||||
const docs: Array<TSecretFolderVersions & { max: number }> = await (tx || db.replicaNode())(
|
const docs: Array<TSecretFolderVersions & { max: number }> = await (tx || db.replicaNode())(
|
||||||
TableName.SecretFolderVersion
|
TableName.SecretFolderVersion
|
||||||
)
|
)
|
||||||
.whereIn("folderId", folderIds)
|
.whereIn(`${TableName.SecretFolderVersion}.folderId`, folderIds)
|
||||||
.join(
|
.join(
|
||||||
(tx || db)(TableName.SecretFolderVersion)
|
(tx || db)(TableName.SecretFolderVersion)
|
||||||
.groupBy("folderId")
|
.groupBy("folderId")
|
||||||
@@ -85,6 +85,8 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => {
|
|||||||
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.Environment}.projectId`)
|
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.Environment}.projectId`)
|
||||||
.join("folder_cte", "folder_cte.id", `${TableName.SecretFolderVersion}.id`)
|
.join("folder_cte", "folder_cte.id", `${TableName.SecretFolderVersion}.id`)
|
||||||
.whereRaw(`folder_cte.row_num > ${TableName.Project}."pitVersionLimit"`)
|
.whereRaw(`folder_cte.row_num > ${TableName.Project}."pitVersionLimit"`)
|
||||||
|
// Projects with version >= 3 will require to have all folder versions for PIT
|
||||||
|
.andWhere(`${TableName.Project}.version`, "<", 3)
|
||||||
.delete();
|
.delete();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({
|
throw new DatabaseError({
|
||||||
@@ -95,5 +97,107 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => {
|
|||||||
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret folder versions completed`);
|
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret folder versions completed`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...secretFolderVerOrm, findLatestFolderVersions, findLatestVersionByFolderId, pruneExcessVersions };
|
// Get latest versions by folderIds
|
||||||
|
const getLatestFolderVersions = async (folderIds: string[], tx?: Knex): Promise<Array<TSecretFolderVersions>> => {
|
||||||
|
if (!folderIds.length) return [];
|
||||||
|
|
||||||
|
const knexInstance = tx || db.replicaNode();
|
||||||
|
return knexInstance(TableName.SecretFolderVersion)
|
||||||
|
.whereIn(`${TableName.SecretFolderVersion}.folderId`, folderIds)
|
||||||
|
.join(
|
||||||
|
knexInstance(TableName.SecretFolderVersion)
|
||||||
|
.groupBy("folderId")
|
||||||
|
.max("version")
|
||||||
|
.select("folderId")
|
||||||
|
.as("latestVersion"),
|
||||||
|
(bd) => {
|
||||||
|
bd.on(`${TableName.SecretFolderVersion}.folderId`, "latestVersion.folderId").andOn(
|
||||||
|
`${TableName.SecretFolderVersion}.version`,
|
||||||
|
"latestVersion.max"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get specific versions and update with max version
|
||||||
|
const getSpecificFolderVersionsWithLatest = async (
|
||||||
|
versionIds: string[],
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<Array<TSecretFolderVersions>> => {
|
||||||
|
if (!versionIds.length) return [];
|
||||||
|
|
||||||
|
const knexInstance = tx || db.replicaNode();
|
||||||
|
|
||||||
|
// Get specific versions
|
||||||
|
const specificVersions = await knexInstance(TableName.SecretFolderVersion).whereIn("id", versionIds);
|
||||||
|
|
||||||
|
// Get folderIds from these versions
|
||||||
|
const specificFolderIds = [...new Set(specificVersions.map((v) => v.folderId).filter(Boolean))];
|
||||||
|
|
||||||
|
if (!specificFolderIds.length) return specificVersions;
|
||||||
|
|
||||||
|
// Get max versions for these folderIds
|
||||||
|
const maxVersionsQuery = await knexInstance(TableName.SecretFolderVersion)
|
||||||
|
.whereIn("folderId", specificFolderIds)
|
||||||
|
.groupBy("folderId")
|
||||||
|
.select("folderId")
|
||||||
|
.max("version", { as: "maxVersion" });
|
||||||
|
|
||||||
|
// Create lookup map for max versions
|
||||||
|
const maxVersionMap = maxVersionsQuery.reduce<Record<string, number>>((acc, item) => {
|
||||||
|
if (item.maxVersion) {
|
||||||
|
acc[item.folderId] = item.maxVersion;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
// Replace version with max version
|
||||||
|
return specificVersions.map((version) => ({
|
||||||
|
...version,
|
||||||
|
version: maxVersionMap[version.folderId] || version.version
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const findByIdsWithLatestVersion = async (folderIds: string[], versionIds?: string[], tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
if (!folderIds.length && (!versionIds || !versionIds.length)) return {};
|
||||||
|
|
||||||
|
// Run both queries in parallel
|
||||||
|
const [latestVersions, specificVersionsWithLatest] = await Promise.all([
|
||||||
|
folderIds.length ? getLatestFolderVersions(folderIds, tx) : [],
|
||||||
|
versionIds?.length ? getSpecificFolderVersionsWithLatest(versionIds, tx) : []
|
||||||
|
]);
|
||||||
|
|
||||||
|
const allDocs = [...latestVersions, ...specificVersionsWithLatest];
|
||||||
|
|
||||||
|
// Convert array to record with folderId as key
|
||||||
|
return allDocs.reduce<Record<string, TSecretFolderVersions>>(
|
||||||
|
(prev, curr) => ({ ...prev, [curr.folderId || ""]: curr }),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindByIdsWithLatestVersion" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findLatestVersion = async (folderId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const doc = await (tx || db.replicaNode())(TableName.SecretFolderVersion)
|
||||||
|
.where(`${TableName.SecretFolderVersion}.folderId`, folderId)
|
||||||
|
.select(selectAllTableCols(TableName.SecretFolderVersion))
|
||||||
|
.first();
|
||||||
|
return doc;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "findLatestVersion" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...secretFolderVerOrm,
|
||||||
|
findLatestFolderVersions,
|
||||||
|
findLatestVersionByFolderId,
|
||||||
|
pruneExcessVersions,
|
||||||
|
findByIdsWithLatestVersion,
|
||||||
|
findLatestVersion
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/se
|
|||||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
|
|
||||||
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
||||||
|
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||||
|
|
||||||
export type TSecretSyncQueueFactory = ReturnType<typeof secretSyncQueueFactory>;
|
export type TSecretSyncQueueFactory = ReturnType<typeof secretSyncQueueFactory>;
|
||||||
|
|
||||||
@@ -94,6 +95,7 @@ type TSecretSyncQueueFactoryDep = {
|
|||||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||||
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -136,6 +138,7 @@ export const secretSyncQueueFactory = ({
|
|||||||
secretVersionV2BridgeDAL,
|
secretVersionV2BridgeDAL,
|
||||||
secretVersionTagV2BridgeDAL,
|
secretVersionTagV2BridgeDAL,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
|
folderCommitService,
|
||||||
licenseService
|
licenseService
|
||||||
}: TSecretSyncQueueFactoryDep) => {
|
}: TSecretSyncQueueFactoryDep) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@@ -167,7 +170,8 @@ export const secretSyncQueueFactory = ({
|
|||||||
secretVersionV2BridgeDAL,
|
secretVersionV2BridgeDAL,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
secretVersionTagV2BridgeDAL,
|
secretVersionTagV2BridgeDAL,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL,
|
||||||
|
folderCommitService
|
||||||
});
|
});
|
||||||
|
|
||||||
const $updateManySecretsRawFn = updateManySecretsRawFnFactory({
|
const $updateManySecretsRawFn = updateManySecretsRawFnFactory({
|
||||||
@@ -183,7 +187,8 @@ export const secretSyncQueueFactory = ({
|
|||||||
secretVersionV2BridgeDAL,
|
secretVersionV2BridgeDAL,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
secretVersionTagV2BridgeDAL,
|
secretVersionTagV2BridgeDAL,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL,
|
||||||
|
folderCommitService
|
||||||
});
|
});
|
||||||
|
|
||||||
const $getInfisicalSecrets = async (
|
const $getInfisicalSecrets = async (
|
||||||
@@ -373,7 +378,7 @@ export const secretSyncQueueFactory = ({
|
|||||||
|
|
||||||
if (Object.hasOwn(secretMap, key)) {
|
if (Object.hasOwn(secretMap, key)) {
|
||||||
// Only update secrets if the source value is not empty
|
// Only update secrets if the source value is not empty
|
||||||
if (value) {
|
if (value && value !== secretMap[key].value) {
|
||||||
secretsToUpdate.push(secret);
|
secretsToUpdate.push(secret);
|
||||||
if (importBehavior === SecretSyncImportBehavior.PrioritizeDestination) importedSecretMap[key] = secretData;
|
if (importBehavior === SecretSyncImportBehavior.PrioritizeDestination) importedSecretMap[key] = secretData;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const secretTagDALFactory = (db: TDbClient) => {
|
|||||||
const secretTagOrm = ormify(db, TableName.SecretTag);
|
const secretTagOrm = ormify(db, TableName.SecretTag);
|
||||||
const secretJnTagOrm = ormify(db, TableName.JnSecretTag);
|
const secretJnTagOrm = ormify(db, TableName.JnSecretTag);
|
||||||
const secretV2JnTagOrm = ormify(db, TableName.SecretV2JnTag);
|
const secretV2JnTagOrm = ormify(db, TableName.SecretV2JnTag);
|
||||||
|
const secretVersionV2TagOrm = ormify(db, TableName.SecretVersionV2Tag);
|
||||||
|
|
||||||
const findManyTagsById = async (projectId: string, ids: string[], tx?: Knex) => {
|
const findManyTagsById = async (projectId: string, ids: string[], tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
@@ -48,14 +49,39 @@ export const secretTagDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const findSecretTagsByVersionId = async (versionId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const tags = await (tx || db.replicaNode())(TableName.SecretVersionV2Tag)
|
||||||
|
.where(`${TableName.SecretVersionV2Tag}.${TableName.SecretVersionV2}Id`, versionId)
|
||||||
|
.select(selectAllTableCols(TableName.SecretVersionV2Tag));
|
||||||
|
return tags;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find all by version id" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findSecretTagsBySecretId = async (secretId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const tags = await (tx || db.replicaNode())(TableName.SecretV2JnTag)
|
||||||
|
.where(`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`, secretId)
|
||||||
|
.select(selectAllTableCols(TableName.SecretV2JnTag));
|
||||||
|
return tags;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find all by secret id" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...secretTagOrm,
|
...secretTagOrm,
|
||||||
saveTagsToSecret: secretJnTagOrm.insertMany,
|
saveTagsToSecret: secretJnTagOrm.insertMany,
|
||||||
deleteTagsToSecret: secretJnTagOrm.delete,
|
deleteTagsToSecret: secretJnTagOrm.delete,
|
||||||
saveTagsToSecretV2: secretV2JnTagOrm.batchInsert,
|
saveTagsToSecretV2: secretV2JnTagOrm.batchInsert,
|
||||||
deleteTagsToSecretV2: secretV2JnTagOrm.delete,
|
deleteTagsToSecretV2: secretV2JnTagOrm.delete,
|
||||||
|
saveTagsToSecretVersionV2: secretVersionV2TagOrm.insertMany,
|
||||||
findSecretTagsByProjectId,
|
findSecretTagsByProjectId,
|
||||||
deleteTagsManySecret,
|
deleteTagsManySecret,
|
||||||
findManyTagsById
|
findManyTagsById,
|
||||||
|
findSecretTagsByVersionId,
|
||||||
|
findSecretTagsBySecretId
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { groupBy } from "@app/lib/fn";
|
|||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
import { CommitType } from "../folder-commit/folder-commit-service";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
|
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
|
||||||
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns";
|
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns";
|
||||||
@@ -73,6 +74,7 @@ export const fnSecretBulkInsert = async ({
|
|||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
folderCommitService,
|
||||||
actor,
|
actor,
|
||||||
tx
|
tx
|
||||||
}: TFnSecretBulkInsert) => {
|
}: TFnSecretBulkInsert) => {
|
||||||
@@ -126,11 +128,36 @@ export const fnSecretBulkInsert = async ({
|
|||||||
userActorId,
|
userActorId,
|
||||||
identityActorId,
|
identityActorId,
|
||||||
actorType,
|
actorType,
|
||||||
|
metadata: el.metadata ? JSON.stringify(el.metadata) : [],
|
||||||
secretId: newSecretGroupedByKeyName[el.key][0].id
|
secretId: newSecretGroupedByKeyName[el.key][0].id
|
||||||
})),
|
})),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const commitChanges = secretVersions
|
||||||
|
.filter(({ type }) => type === SecretType.Shared)
|
||||||
|
.map((sv) => ({
|
||||||
|
type: CommitType.ADD,
|
||||||
|
secretVersionId: sv.id
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (commitChanges.length > 0) {
|
||||||
|
await folderCommitService.createCommit(
|
||||||
|
{
|
||||||
|
actor: {
|
||||||
|
type: actorType || ActorType.PLATFORM,
|
||||||
|
metadata: {
|
||||||
|
id: actor?.actorId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
message: "Secret Created",
|
||||||
|
folderId,
|
||||||
|
changes: commitChanges
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await secretDAL.upsertSecretReferences(
|
await secretDAL.upsertSecretReferences(
|
||||||
inputSecrets.map(({ references = [], key }) => ({
|
inputSecrets.map(({ references = [], key }) => ({
|
||||||
secretId: newSecretGroupedByKeyName[key][0].id,
|
secretId: newSecretGroupedByKeyName[key][0].id,
|
||||||
@@ -185,6 +212,7 @@ export const fnSecretBulkUpdate = async ({
|
|||||||
orgId,
|
orgId,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
|
folderCommitService,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
@@ -246,7 +274,7 @@ export const fnSecretBulkUpdate = async ({
|
|||||||
userId,
|
userId,
|
||||||
encryptedComment,
|
encryptedComment,
|
||||||
version,
|
version,
|
||||||
metadata,
|
metadata: metadata ? JSON.stringify(metadata) : [],
|
||||||
reminderNote,
|
reminderNote,
|
||||||
encryptedValue,
|
encryptedValue,
|
||||||
reminderRepeatDays,
|
reminderRepeatDays,
|
||||||
@@ -259,6 +287,7 @@ export const fnSecretBulkUpdate = async ({
|
|||||||
),
|
),
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
await secretDAL.upsertSecretReferences(
|
await secretDAL.upsertSecretReferences(
|
||||||
inputSecrets
|
inputSecrets
|
||||||
.filter(({ data: { references } }) => Boolean(references))
|
.filter(({ data: { references } }) => Boolean(references))
|
||||||
@@ -329,6 +358,31 @@ export const fnSecretBulkUpdate = async ({
|
|||||||
},
|
},
|
||||||
{ tx }
|
{ tx }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const commitChanges = secretVersions
|
||||||
|
.filter(({ type }) => type === SecretType.Shared)
|
||||||
|
.map((sv) => ({
|
||||||
|
type: CommitType.ADD,
|
||||||
|
isUpdate: true,
|
||||||
|
secretVersionId: sv.id
|
||||||
|
}));
|
||||||
|
if (commitChanges.length > 0) {
|
||||||
|
await folderCommitService.createCommit(
|
||||||
|
{
|
||||||
|
actor: {
|
||||||
|
type: actorType || ActorType.PLATFORM,
|
||||||
|
metadata: {
|
||||||
|
id: actor?.actorId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
message: "Secret Updated",
|
||||||
|
folderId,
|
||||||
|
changes: commitChanges
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id }));
|
return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id }));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -337,8 +391,11 @@ export const fnSecretBulkDelete = async ({
|
|||||||
inputSecrets,
|
inputSecrets,
|
||||||
tx,
|
tx,
|
||||||
actorId,
|
actorId,
|
||||||
|
actorType,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
secretQueueService
|
secretQueueService,
|
||||||
|
folderCommitService,
|
||||||
|
secretVersionDAL
|
||||||
}: TFnSecretBulkDelete) => {
|
}: TFnSecretBulkDelete) => {
|
||||||
const deletedSecrets = await secretDAL.deleteMany(
|
const deletedSecrets = await secretDAL.deleteMany(
|
||||||
inputSecrets.map(({ type, secretKey }) => ({
|
inputSecrets.map(({ type, secretKey }) => ({
|
||||||
@@ -358,6 +415,35 @@ export const fnSecretBulkDelete = async ({
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const secretVersions = await secretVersionDAL.findLatestVersionMany(
|
||||||
|
folderId,
|
||||||
|
deletedSecrets.map(({ id }) => id),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
const commitChanges = deletedSecrets
|
||||||
|
.filter(({ type }) => type === SecretType.Shared)
|
||||||
|
.map(({ id }) => ({
|
||||||
|
type: CommitType.DELETE,
|
||||||
|
secretVersionId: secretVersions[id].id
|
||||||
|
}));
|
||||||
|
if (commitChanges.length > 0) {
|
||||||
|
await folderCommitService.createCommit(
|
||||||
|
{
|
||||||
|
actor: {
|
||||||
|
type: actorType || ActorType.PLATFORM,
|
||||||
|
metadata: {
|
||||||
|
id: actorId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
message: "Secret Deleted",
|
||||||
|
folderId,
|
||||||
|
changes: commitChanges
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return deletedSecrets;
|
return deletedSecrets;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionCommitsActions,
|
||||||
ProjectPermissionSecretActions,
|
ProjectPermissionSecretActions,
|
||||||
ProjectPermissionSet,
|
ProjectPermissionSet,
|
||||||
ProjectPermissionSub
|
ProjectPermissionSub
|
||||||
@@ -34,6 +35,7 @@ import { logger } from "@app/lib/logger";
|
|||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
@@ -90,6 +92,7 @@ type TSecretV2BridgeServiceFactoryDep = {
|
|||||||
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
secretTagDAL: TSecretTagDALFactory;
|
secretTagDAL: TSecretTagDALFactory;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">;
|
||||||
folderDAL: Pick<
|
folderDAL: Pick<
|
||||||
TSecretFolderDALFactory,
|
TSecretFolderDALFactory,
|
||||||
@@ -124,6 +127,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
|
folderCommitService,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
snapshotService,
|
snapshotService,
|
||||||
@@ -321,12 +325,14 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
userId: inputSecret.type === SecretType.Personal ? actorId : null,
|
userId: inputSecret.type === SecretType.Personal ? actorId : null,
|
||||||
tagIds: inputSecret.tagIds,
|
tagIds: inputSecret.tagIds,
|
||||||
references: nestedReferences,
|
references: nestedReferences,
|
||||||
|
metadata: secretMetadata ? JSON.stringify(secretMetadata) : [],
|
||||||
secretMetadata
|
secretMetadata
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
|
folderCommitService,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
actor: {
|
actor: {
|
||||||
@@ -510,6 +516,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
folderId,
|
folderId,
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
|
folderCommitService,
|
||||||
inputSecrets: [
|
inputSecrets: [
|
||||||
{
|
{
|
||||||
filter: { id: secretId },
|
filter: { id: secretId },
|
||||||
@@ -523,6 +530,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
skipMultilineEncoding: inputSecret.skipMultilineEncoding,
|
skipMultilineEncoding: inputSecret.skipMultilineEncoding,
|
||||||
key: inputSecret.newSecretName || secretName,
|
key: inputSecret.newSecretName || secretName,
|
||||||
tags: inputSecret.tagIds,
|
tags: inputSecret.tagIds,
|
||||||
|
metadata: secretMetadata ? JSON.stringify(secretMetadata) : [],
|
||||||
secretMetadata,
|
secretMetadata,
|
||||||
...encryptedValue
|
...encryptedValue
|
||||||
}
|
}
|
||||||
@@ -650,6 +658,9 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
projectId,
|
projectId,
|
||||||
folderId,
|
folderId,
|
||||||
actorId,
|
actorId,
|
||||||
|
actorType: actor,
|
||||||
|
folderCommitService,
|
||||||
|
secretVersionDAL,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
secretQueueService,
|
secretQueueService,
|
||||||
inputSecrets: [
|
inputSecrets: [
|
||||||
@@ -1590,6 +1601,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
|
folderCommitService,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
@@ -1859,6 +1871,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
const bulkUpdatedSecrets = await fnSecretBulkUpdate({
|
const bulkUpdatedSecrets = await fnSecretBulkUpdate({
|
||||||
folderId,
|
folderId,
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
|
folderCommitService,
|
||||||
tx,
|
tx,
|
||||||
inputSecrets: secretsToUpdate.map((el) => {
|
inputSecrets: secretsToUpdate.map((el) => {
|
||||||
const originalSecret = secretsToUpdateInDBGroupedByKey[el.secretKey][0];
|
const originalSecret = secretsToUpdateInDBGroupedByKey[el.secretKey][0];
|
||||||
@@ -1928,6 +1941,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
|
folderCommitService,
|
||||||
actor: {
|
actor: {
|
||||||
type: actor,
|
type: actor,
|
||||||
actorId
|
actorId
|
||||||
@@ -2061,6 +2075,8 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
fnSecretBulkDelete({
|
fnSecretBulkDelete({
|
||||||
secretDAL,
|
secretDAL,
|
||||||
secretQueueService,
|
secretQueueService,
|
||||||
|
folderCommitService,
|
||||||
|
secretVersionDAL,
|
||||||
inputSecrets: inputSecrets.map(({ type, secretKey }) => ({
|
inputSecrets: inputSecrets.map(({ type, secretKey }) => ({
|
||||||
secretKey,
|
secretKey,
|
||||||
type: type || SecretType.Shared
|
type: type || SecretType.Shared
|
||||||
@@ -2068,6 +2084,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
projectId,
|
projectId,
|
||||||
folderId,
|
folderId,
|
||||||
actorId,
|
actorId,
|
||||||
|
actorType: actor,
|
||||||
tx
|
tx
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -2159,15 +2176,25 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.SecretManager
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
|
||||||
|
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({
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.SecretManager,
|
type: KmsDataKey.SecretManager,
|
||||||
projectId: folder.projectId
|
projectId: folder.projectId
|
||||||
});
|
});
|
||||||
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors(secretId, folder.projectId, {
|
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors({
|
||||||
offset,
|
secretId,
|
||||||
limit,
|
projectId: folder.projectId,
|
||||||
sort: [["createdAt", "desc"]]
|
findOpt: {
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
sort: [["createdAt", "desc"]]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return secretVersions.map((el) => {
|
return secretVersions.map((el) => {
|
||||||
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
|
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
|
||||||
@@ -2469,6 +2496,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
tx,
|
tx,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
|
folderCommitService,
|
||||||
secretVersionTagDAL,
|
secretVersionTagDAL,
|
||||||
actor: {
|
actor: {
|
||||||
type: actor,
|
type: actor,
|
||||||
@@ -2495,6 +2523,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
folderId: destinationFolder.id,
|
folderId: destinationFolder.id,
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
|
folderCommitService,
|
||||||
secretVersionDAL,
|
secretVersionDAL,
|
||||||
secretDAL,
|
secretDAL,
|
||||||
tx,
|
tx,
|
||||||
@@ -2840,6 +2869,76 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getSecretVersionsByIds = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
secretId,
|
||||||
|
secretVersionNumbers,
|
||||||
|
secretPath,
|
||||||
|
envId,
|
||||||
|
projectId
|
||||||
|
}: TGetSecretVersionsDTO & {
|
||||||
|
secretVersionNumbers: string[];
|
||||||
|
secretPath: string;
|
||||||
|
envId: string;
|
||||||
|
projectId: string;
|
||||||
|
}) => {
|
||||||
|
const environment = await projectEnvDAL.findOne({ id: envId, projectId });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors({
|
||||||
|
secretId,
|
||||||
|
projectId,
|
||||||
|
secretVersions: secretVersionNumbers
|
||||||
|
});
|
||||||
|
return secretVersions.map((el) => {
|
||||||
|
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
|
||||||
|
permission,
|
||||||
|
ProjectPermissionSecretActions.ReadValue,
|
||||||
|
{
|
||||||
|
environment: environment.slug,
|
||||||
|
secretPath,
|
||||||
|
secretName: el.key,
|
||||||
|
...(el.tags?.length && {
|
||||||
|
secretTags: el.tags.map((tag) => tag.slug)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return reshapeBridgeSecret(
|
||||||
|
projectId,
|
||||||
|
environment.slug,
|
||||||
|
secretPath,
|
||||||
|
{
|
||||||
|
...el,
|
||||||
|
value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "",
|
||||||
|
comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : ""
|
||||||
|
},
|
||||||
|
secretValueHidden
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createSecret,
|
createSecret,
|
||||||
deleteSecret,
|
deleteSecret,
|
||||||
@@ -2858,6 +2957,7 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
getSecretReferenceTree,
|
getSecretReferenceTree,
|
||||||
getSecretsByFolderMappings,
|
getSecretsByFolderMappings,
|
||||||
getSecretById,
|
getSecretById,
|
||||||
getAccessibleSecrets
|
getAccessibleSecrets,
|
||||||
|
getSecretVersionsByIds
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
|||||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
||||||
|
|
||||||
|
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||||
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
|
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
|
||||||
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
|
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
|
||||||
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
|
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
|
||||||
@@ -178,9 +179,10 @@ export type TFnSecretBulkInsert = {
|
|||||||
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
||||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "find">;
|
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "find">;
|
||||||
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
actor?: {
|
actor?: {
|
||||||
type: string;
|
type: string;
|
||||||
actorId: string;
|
actorId?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -206,9 +208,10 @@ export type TFnSecretBulkUpdate = {
|
|||||||
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
|
||||||
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2" | "find">;
|
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2" | "find">;
|
||||||
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
actor?: {
|
actor?: {
|
||||||
type: string;
|
type: string;
|
||||||
actorId: string;
|
actorId?: string;
|
||||||
};
|
};
|
||||||
tx?: Knex;
|
tx?: Knex;
|
||||||
};
|
};
|
||||||
@@ -218,11 +221,14 @@ export type TFnSecretBulkDelete = {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
inputSecrets: Array<{ type: SecretType; secretKey: string }>;
|
inputSecrets: Array<{ type: SecretType; secretKey: string }>;
|
||||||
actorId: string;
|
actorId: string;
|
||||||
|
actorType?: string;
|
||||||
tx?: Knex;
|
tx?: Knex;
|
||||||
secretDAL: Pick<TSecretV2BridgeDALFactory, "deleteMany">;
|
secretDAL: Pick<TSecretV2BridgeDALFactory, "deleteMany">;
|
||||||
secretQueueService: {
|
secretQueueService: {
|
||||||
removeSecretReminder: (data: TRemoveSecretReminderDTO, tx?: Knex) => Promise<void>;
|
removeSecretReminder: (data: TRemoveSecretReminderDTO, tx?: Knex) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
|
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "findLatestVersionMany">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type THandleReminderDTO = {
|
export type THandleReminderDTO = {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Knex } from "knex";
|
|||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { SecretVersionsV2Schema, TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
|
import { SecretVersionsV2Schema, TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
|
||||||
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex";
|
import { buildFindFilter, ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueName } from "@app/queue";
|
import { QueueName } from "@app/queue";
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "FindLatestVersinMany" });
|
throw new DatabaseError({ error, name: "FindLatestVersionMany" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -162,6 +162,8 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.Environment}.projectId`)
|
.join(TableName.Project, `${TableName.Project}.id`, `${TableName.Environment}.projectId`)
|
||||||
.join("version_cte", "version_cte.id", `${TableName.SecretVersionV2}.id`)
|
.join("version_cte", "version_cte.id", `${TableName.SecretVersionV2}.id`)
|
||||||
.whereRaw(`version_cte.row_num > ${TableName.Project}."pitVersionLimit"`)
|
.whereRaw(`version_cte.row_num > ${TableName.Project}."pitVersionLimit"`)
|
||||||
|
// Projects with version >= 3 will require to have all secret versions for PIT
|
||||||
|
.andWhere(`${TableName.Project}.version`, "<", 3)
|
||||||
.delete();
|
.delete();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({
|
throw new DatabaseError({
|
||||||
@@ -172,13 +174,21 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 completed`);
|
logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 completed`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const findVersionsBySecretIdWithActors = async (
|
const findVersionsBySecretIdWithActors = async ({
|
||||||
secretId: string,
|
secretId,
|
||||||
projectId: string,
|
projectId,
|
||||||
{ offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt<TSecretVersionsV2> = {},
|
secretVersions,
|
||||||
tx?: Knex
|
findOpt = {},
|
||||||
) => {
|
tx
|
||||||
|
}: {
|
||||||
|
secretId: string;
|
||||||
|
projectId: string;
|
||||||
|
secretVersions?: string[];
|
||||||
|
findOpt?: TFindOpt<TSecretVersionsV2>;
|
||||||
|
tx?: Knex;
|
||||||
|
}) => {
|
||||||
try {
|
try {
|
||||||
|
const { offset, limit, sort = [["createdAt", "desc"]] } = findOpt;
|
||||||
const query = (tx || db)(TableName.SecretVersionV2)
|
const query = (tx || db)(TableName.SecretVersionV2)
|
||||||
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretVersionV2}.userActorId`)
|
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretVersionV2}.userActorId`)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
@@ -189,22 +199,24 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
.leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`)
|
.leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`)
|
||||||
.leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`)
|
.leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.SecretV2JnTag,
|
TableName.SecretVersionV2Tag,
|
||||||
`${TableName.SecretV2}.id`,
|
`${TableName.SecretVersionV2}.id`,
|
||||||
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
|
`${TableName.SecretVersionV2Tag}.${TableName.SecretVersionV2}Id`
|
||||||
)
|
)
|
||||||
.leftJoin(
|
.leftJoin(
|
||||||
TableName.SecretTag,
|
TableName.SecretTag,
|
||||||
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
|
||||||
`${TableName.SecretTag}.id`
|
`${TableName.SecretTag}.id`
|
||||||
)
|
)
|
||||||
.where((qb) => {
|
.where((qb) => {
|
||||||
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
|
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
|
||||||
void qb.where(`${TableName.ProjectMembership}.projectId`, projectId);
|
void qb.where(`${TableName.ProjectMembership}.projectId`, projectId);
|
||||||
|
if (secretVersions?.length) void qb.whereIn(`${TableName.SecretVersionV2}.version`, secretVersions);
|
||||||
})
|
})
|
||||||
.orWhere((qb) => {
|
.orWhere((qb) => {
|
||||||
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
|
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
|
||||||
void qb.whereNull(`${TableName.ProjectMembership}.projectId`);
|
void qb.whereNull(`${TableName.ProjectMembership}.projectId`);
|
||||||
|
if (secretVersions?.length) void qb.whereIn(`${TableName.SecretVersionV2}.version`, secretVersions);
|
||||||
})
|
})
|
||||||
.select(
|
.select(
|
||||||
selectAllTableCols(TableName.SecretVersionV2),
|
selectAllTableCols(TableName.SecretVersionV2),
|
||||||
@@ -260,6 +272,178 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Function to fetch latest versions by secretIds
|
||||||
|
const getLatestVersionsBySecretIds = async (
|
||||||
|
folderId: string,
|
||||||
|
secretIds: string[],
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<Array<TSecretVersionsV2>> => {
|
||||||
|
if (!secretIds.length) return [];
|
||||||
|
|
||||||
|
const knexInstance = tx || db.replicaNode();
|
||||||
|
return knexInstance(TableName.SecretVersionV2)
|
||||||
|
.where("folderId", folderId)
|
||||||
|
.whereIn(`${TableName.SecretVersionV2}.secretId`, secretIds)
|
||||||
|
.join(
|
||||||
|
knexInstance(TableName.SecretVersionV2)
|
||||||
|
.groupBy("secretId")
|
||||||
|
.max("version")
|
||||||
|
.select("secretId")
|
||||||
|
.as("latestVersion"),
|
||||||
|
(bd) => {
|
||||||
|
bd.on(`${TableName.SecretVersionV2}.secretId`, "latestVersion.secretId").andOn(
|
||||||
|
`${TableName.SecretVersionV2}.version`,
|
||||||
|
"latestVersion.max"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to fetch specific versions by versionIds
|
||||||
|
const getSpecificVersionsWithLatestInfo = async (
|
||||||
|
folderId: string,
|
||||||
|
versionIds: string[],
|
||||||
|
tx?: Knex
|
||||||
|
): Promise<Array<TSecretVersionsV2>> => {
|
||||||
|
if (!versionIds.length) return [];
|
||||||
|
|
||||||
|
const knexInstance = tx || db.replicaNode();
|
||||||
|
|
||||||
|
// Get the specific versions
|
||||||
|
const specificVersions = await knexInstance(TableName.SecretVersionV2)
|
||||||
|
.where("folderId", folderId)
|
||||||
|
.whereIn("id", versionIds);
|
||||||
|
|
||||||
|
// Get the secretIds from these versions
|
||||||
|
const specificSecretIds = [...new Set(specificVersions.map((v) => v.secretId).filter(Boolean))];
|
||||||
|
|
||||||
|
if (!specificSecretIds.length) return specificVersions;
|
||||||
|
|
||||||
|
// Get max versions for these secretIds
|
||||||
|
const maxVersionsQuery = await knexInstance(TableName.SecretVersionV2)
|
||||||
|
.whereIn("secretId", specificSecretIds)
|
||||||
|
.groupBy("secretId")
|
||||||
|
.select("secretId")
|
||||||
|
.max("version", { as: "maxVersion" });
|
||||||
|
|
||||||
|
// Create a lookup map for max versions
|
||||||
|
const maxVersionMap = maxVersionsQuery.reduce(
|
||||||
|
(acc, item) => {
|
||||||
|
acc[item.secretId] = item.maxVersion;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, number>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update the version field with maxVersion when needed
|
||||||
|
return specificVersions.map((version) => {
|
||||||
|
// Replace version with maxVersion
|
||||||
|
return {
|
||||||
|
...version,
|
||||||
|
version: maxVersionMap[version.secretId] || version.version
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const findByIdsWithLatestVersion = async (
|
||||||
|
folderId: string,
|
||||||
|
secretIds: string[],
|
||||||
|
versionIds?: string[],
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
if (!secretIds.length && (!versionIds || !versionIds.length)) return {};
|
||||||
|
|
||||||
|
const [latestVersions, specificVersionsWithLatest] = await Promise.all([
|
||||||
|
secretIds.length ? getLatestVersionsBySecretIds(folderId, secretIds, tx) : [],
|
||||||
|
versionIds?.length ? getSpecificVersionsWithLatestInfo(folderId, versionIds, tx) : []
|
||||||
|
]);
|
||||||
|
|
||||||
|
const allDocs = [...latestVersions, ...specificVersionsWithLatest];
|
||||||
|
|
||||||
|
// Convert array to record with secretId as key
|
||||||
|
return allDocs.reduce<Record<string, TSecretVersionsV2>>(
|
||||||
|
(prev, curr) => ({ ...prev, [curr.secretId || ""]: curr }),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindByIdsWithLatestVersion" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findByIdAndPreviousVersion = async (secretVersionId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const targetSecretVersion = await (tx || db.replicaNode())(TableName.SecretVersionV2)
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
.where(buildFindFilter({ id: secretVersionId }, TableName.SecretVersionV2))
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SecretVersionV2Tag,
|
||||||
|
`${TableName.SecretVersionV2}.id`,
|
||||||
|
`${TableName.SecretVersionV2Tag}.${TableName.SecretVersionV2}Id`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SecretTag,
|
||||||
|
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
|
||||||
|
`${TableName.SecretTag}.id`
|
||||||
|
)
|
||||||
|
.select(selectAllTableCols(TableName.SecretVersionV2))
|
||||||
|
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||||
|
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||||
|
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||||
|
.first();
|
||||||
|
if (targetSecretVersion) {
|
||||||
|
const previousSecretVersion = await (tx || db.replicaNode())(TableName.SecretVersionV2)
|
||||||
|
.where(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
buildFindFilter(
|
||||||
|
{ version: targetSecretVersion.version - 1, secretId: targetSecretVersion.secretId },
|
||||||
|
TableName.SecretVersionV2
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SecretVersionV2Tag,
|
||||||
|
`${TableName.SecretVersionV2}.id`,
|
||||||
|
`${TableName.SecretVersionV2Tag}.${TableName.SecretVersionV2}Id`
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.SecretTag,
|
||||||
|
`${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`,
|
||||||
|
`${TableName.SecretTag}.id`
|
||||||
|
)
|
||||||
|
.select(selectAllTableCols(TableName.SecretVersionV2))
|
||||||
|
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||||
|
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||||
|
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||||
|
.first();
|
||||||
|
if (!previousSecretVersion) return [];
|
||||||
|
const docs = [previousSecretVersion, targetSecretVersion];
|
||||||
|
|
||||||
|
const data = sqlNestRelationships({
|
||||||
|
data: docs,
|
||||||
|
key: "id",
|
||||||
|
parentMapper: (el) => ({ _id: el.id, ...SecretVersionsV2Schema.parse(el) }),
|
||||||
|
childrenMapper: [
|
||||||
|
{
|
||||||
|
key: "tagId",
|
||||||
|
label: "tags" as const,
|
||||||
|
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
||||||
|
id,
|
||||||
|
color,
|
||||||
|
slug,
|
||||||
|
name: slug
|
||||||
|
})
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "FindByIdAndPreviousVersion" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...secretVersionV2Orm,
|
...secretVersionV2Orm,
|
||||||
pruneExcessVersions,
|
pruneExcessVersions,
|
||||||
@@ -267,6 +451,8 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
|
|||||||
bulkUpdate,
|
bulkUpdate,
|
||||||
findLatestVersionByFolderId,
|
findLatestVersionByFolderId,
|
||||||
findVersionsBySecretIdWithActors,
|
findVersionsBySecretIdWithActors,
|
||||||
findBySecretId
|
findBySecretId,
|
||||||
|
findByIdsWithLatestVersion,
|
||||||
|
findByIdAndPreviousVersion
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -778,6 +778,7 @@ export const createManySecretsRawFnFactory = ({
|
|||||||
secretVersionV2BridgeDAL,
|
secretVersionV2BridgeDAL,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
secretVersionTagV2BridgeDAL,
|
secretVersionTagV2BridgeDAL,
|
||||||
|
folderCommitService,
|
||||||
kmsService,
|
kmsService,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL
|
||||||
}: TCreateManySecretsRawFnFactory) => {
|
}: TCreateManySecretsRawFnFactory) => {
|
||||||
@@ -850,6 +851,7 @@ export const createManySecretsRawFnFactory = ({
|
|||||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
||||||
|
folderCommitService,
|
||||||
tx
|
tx
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -942,6 +944,7 @@ export const updateManySecretsRawFnFactory = ({
|
|||||||
secretVersionV2BridgeDAL,
|
secretVersionV2BridgeDAL,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
|
folderCommitService,
|
||||||
kmsService
|
kmsService
|
||||||
}: TUpdateManySecretsRawFnFactory) => {
|
}: TUpdateManySecretsRawFnFactory) => {
|
||||||
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL);
|
const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL);
|
||||||
@@ -1032,7 +1035,8 @@ export const updateManySecretsRawFnFactory = ({
|
|||||||
secretDAL: secretV2BridgeDAL,
|
secretDAL: secretV2BridgeDAL,
|
||||||
secretVersionDAL: secretVersionV2BridgeDAL,
|
secretVersionDAL: secretVersionV2BridgeDAL,
|
||||||
secretTagDAL,
|
secretTagDAL,
|
||||||
secretVersionTagDAL: secretVersionTagV2BridgeDAL
|
secretVersionTagDAL: secretVersionTagV2BridgeDAL,
|
||||||
|
folderCommitService
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import { TSecretSyncQueueFactory } from "@app/services/secret-sync/secret-sync-q
|
|||||||
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||||
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
|
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
|
||||||
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
|
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
|
||||||
@@ -112,6 +113,7 @@ type TSecretQueueFactoryDep = {
|
|||||||
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
|
||||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create">;
|
||||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||||
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
secretReminderRecipientsDAL: Pick<
|
secretReminderRecipientsDAL: Pick<
|
||||||
TSecretReminderRecipientsDALFactory,
|
TSecretReminderRecipientsDALFactory,
|
||||||
"delete" | "findUsersBySecretId" | "insertMany" | "transaction"
|
"delete" | "findUsersBySecretId" | "insertMany" | "transaction"
|
||||||
@@ -178,7 +180,8 @@ export const secretQueueFactory = ({
|
|||||||
projectKeyDAL,
|
projectKeyDAL,
|
||||||
resourceMetadataDAL,
|
resourceMetadataDAL,
|
||||||
secretReminderRecipientsDAL,
|
secretReminderRecipientsDAL,
|
||||||
secretSyncQueue
|
secretSyncQueue,
|
||||||
|
folderCommitService
|
||||||
}: TSecretQueueFactoryDep) => {
|
}: TSecretQueueFactoryDep) => {
|
||||||
const integrationMeter = opentelemetry.metrics.getMeter("Integrations");
|
const integrationMeter = opentelemetry.metrics.getMeter("Integrations");
|
||||||
const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", {
|
const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", {
|
||||||
@@ -366,7 +369,8 @@ export const secretQueueFactory = ({
|
|||||||
secretVersionV2BridgeDAL,
|
secretVersionV2BridgeDAL,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
secretVersionTagV2BridgeDAL,
|
secretVersionTagV2BridgeDAL,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL,
|
||||||
|
folderCommitService
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateManySecretsRawFn = updateManySecretsRawFnFactory({
|
const updateManySecretsRawFn = updateManySecretsRawFnFactory({
|
||||||
@@ -382,7 +386,8 @@ export const secretQueueFactory = ({
|
|||||||
secretVersionV2BridgeDAL,
|
secretVersionV2BridgeDAL,
|
||||||
secretV2BridgeDAL,
|
secretV2BridgeDAL,
|
||||||
secretVersionTagV2BridgeDAL,
|
secretVersionTagV2BridgeDAL,
|
||||||
resourceMetadataDAL
|
resourceMetadataDAL,
|
||||||
|
folderCommitService
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ import {
|
|||||||
TGetSecretsRawByFolderMappingsDTO
|
TGetSecretsRawByFolderMappingsDTO
|
||||||
} from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
|
} 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 { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
@@ -2521,6 +2522,36 @@ export const secretServiceFactory = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getSecretVersionsV2ByIds = async ({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
secretId,
|
||||||
|
secretVersions,
|
||||||
|
secretPath,
|
||||||
|
envId,
|
||||||
|
projectId
|
||||||
|
}: TGetSecretVersionsDTO & {
|
||||||
|
secretVersions: string[];
|
||||||
|
secretPath: string;
|
||||||
|
envId: string;
|
||||||
|
projectId: string;
|
||||||
|
}) => {
|
||||||
|
const secretVersionV2 = await secretV2BridgeService.getSecretVersionsByIds({
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
secretId,
|
||||||
|
secretVersionNumbers: secretVersions,
|
||||||
|
secretPath,
|
||||||
|
envId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
return secretVersionV2;
|
||||||
|
};
|
||||||
|
|
||||||
const attachTags = async ({
|
const attachTags = async ({
|
||||||
secretName,
|
secretName,
|
||||||
tagSlugs,
|
tagSlugs,
|
||||||
@@ -3279,6 +3310,53 @@ export const secretServiceFactory = ({
|
|||||||
return secrets;
|
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,
|
||||||
|
envId: string,
|
||||||
|
projectId: string,
|
||||||
|
secretPath: 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],
|
||||||
|
secretPath,
|
||||||
|
envId,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
return versions?.map((v) => ({
|
||||||
|
secretKey: v.secretKey,
|
||||||
|
secretComment: v.secretComment,
|
||||||
|
skipMultilineEncoding: v.skipMultilineEncoding,
|
||||||
|
tags: v.tags?.map((tag) => tag.slug),
|
||||||
|
metadata: v.metadata,
|
||||||
|
secretValue: v.secretValue
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attachTags,
|
attachTags,
|
||||||
detachTags,
|
detachTags,
|
||||||
@@ -3309,6 +3387,8 @@ export const secretServiceFactory = ({
|
|||||||
getSecretsRawByFolderMappings,
|
getSecretsRawByFolderMappings,
|
||||||
getSecretAccessList,
|
getSecretAccessList,
|
||||||
getSecretByIdRaw,
|
getSecretByIdRaw,
|
||||||
getAccessibleSecrets
|
getAccessibleSecrets,
|
||||||
|
getSecretVersionsV2ByIds,
|
||||||
|
getChangeVersions
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-fold
|
|||||||
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
|
import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal";
|
||||||
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
|
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
|
||||||
@@ -441,6 +442,7 @@ export type TCreateManySecretsRawFnFactory = {
|
|||||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
|
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
|
||||||
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateManySecretsRawFn = {
|
export type TCreateManySecretsRawFn = {
|
||||||
@@ -478,6 +480,7 @@ export type TUpdateManySecretsRawFnFactory = {
|
|||||||
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
|
||||||
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
|
||||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||||
|
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateManySecretsRawFn = {
|
export type TUpdateManySecretsRawFn = {
|
||||||
|
|||||||
@@ -4,38 +4,130 @@ description: "Learn how to rollback secrets and configurations to any snapshot w
|
|||||||
---
|
---
|
||||||
|
|
||||||
<Info>
|
<Info>
|
||||||
Point-in-Time Recovery is a paid feature.
|
Point-in-Time Recovery is a paid feature. If you're using Infisical Cloud,
|
||||||
|
then it is available under the **Pro Tier**. If you're self-hosting Infisical,
|
||||||
If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical,
|
then you should contact [email protected] to purchase an enterprise license
|
||||||
then you should contact [email protected] to purchase an enterprise license to use it.
|
to use it.
|
||||||
</Info>
|
</Info>
|
||||||
|
|
||||||
Infisical's point-in-time recovery functionality allows secrets to be rolled back to any point in time for any given [folder](./folder) or [environment](/documentation/platform/project#project-environments).
|
Infisical's point-in-time recovery functionality allows secrets to be rolled back to any point in time for any given [folder](./folder) or [environment](/documentation/platform/project#project-environments).
|
||||||
Every time a secret is updated, a new snapshot is taken – capturing the state of the folder and environment at that point of time.
|
|
||||||
|
|
||||||
## Snapshots
|
<Tabs>
|
||||||
|
<Tab title="Commits Interface">
|
||||||
|
## Understanding Commits
|
||||||
|
|
||||||
Similar to Git, a commit (also known as snapshot) in Infisical is the state of your project's secrets at a specific point in time scoped to
|
Similar to Git, a commit in Infisical represents a snapshot of changes made to your project's resources at a specific point in time. Each commit is scoped to an environment and [folder](./folder) within it. Unlike the legacy snapshot system, the new commits interface provides granular tracking of individual changes, allowing you to see exactly what was modified, added, or removed in each commit.
|
||||||
an environment and [folder](./folder) within it.
|
|
||||||
|
|
||||||
To view a list of snapshots for the current folder, press the **Commits** button.
|
### Accessing Commits
|
||||||
|
|
||||||

|
From your secrets management interface, you can access the commits functionality by clicking the **Commits Button**. This button is located in the top-right area of your secrets view and shows the number of commits for the current folder (e.g., "4 Commits").
|
||||||
|
|
||||||
This opens up a sidebar from which you can select to view a particular snapshot:
|

|
||||||
|
|
||||||

|
### Commits List View
|
||||||
|
|
||||||
## Rolling back
|
The commits page displays a comprehensive chronological history of all changes made to your environment and folders:
|
||||||
|
|
||||||
After pressing on a snapshot from the sidebar, you can view it and roll back the state
|

|
||||||
of the folder to that point in time by pressing the **Rollback** button.
|
|
||||||
|
|
||||||

|
- **Chronological Sorting**: Commits are grouped by date
|
||||||
|
- **Commit Information**: Each commit shows:
|
||||||
|
- Commit message
|
||||||
|
- Author information
|
||||||
|
- Relative timestamp
|
||||||
|
- Unique commit hash identifier
|
||||||
|
- **Search Functionality**: Use the search bar to quickly find specific commits
|
||||||
|
- **Sorting Options**: Sort commits by various criteria using the sort controls
|
||||||
|
|
||||||
Rolling back secrets to a past snapshot creates a creates a snapshot at the top of the stack and updates secret versions.
|
### Detailed Commit Inspection
|
||||||
|
|
||||||
<Note>
|
Clicking on any commit from the list opens a detailed view showing the list of changes made in that commit.
|
||||||
Rollbacks are localized to not affect other folders within the same environment. This means each [folder](./folder) maintains its own independent history of changes, offering precise and isolated control over rollback actions.
|
|
||||||
Put differently, every [folder](./folder) possesses a distinct and separate timeline, providing granular control when managing your secrets.
|

|
||||||
</Note>
|
|
||||||
|
#### Change Categories
|
||||||
|
|
||||||
|
The commit changes details can be grouped into the following categories:
|
||||||
|
|
||||||
|
**Folder Changes**
|
||||||
|
- Shows folder additions, modifications, or deletions
|
||||||
|
- Displays the folder properties changes in JSON format, including:
|
||||||
|
- Folder name
|
||||||
|
- Folder description
|
||||||
|
|
||||||
|
**Secret Changes**
|
||||||
|
- Lists all secrets that were added, updated, or removed
|
||||||
|
- Shows the complete secret configuration including:
|
||||||
|
- Secret key and value
|
||||||
|
- Comments, tags and metadata
|
||||||
|
- Encoding settings (e.g., skipMultilineEncoding)
|
||||||
|
- Values are displayed with appropriate masking for security
|
||||||
|
|
||||||
|
**Visual Indicators**
|
||||||
|
- Green "+" indicators show additions
|
||||||
|
- Red "-" indicators show deletions
|
||||||
|
- Modified content shows both old and new states
|
||||||
|
|
||||||
|
### Restoration Options
|
||||||
|
|
||||||
|
Each commit provides two distinct restoration methods accessible via the **Restore Options** dropdown:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Revert changes
|
||||||
|
This option provides surgical precision for undoing specific modifications:
|
||||||
|
|
||||||
|
- **Granular Control**: Reverts only the specific changes introduced in that individual commit
|
||||||
|
- **Selective Restoration**: Preserves all other changes made after the commit
|
||||||
|
- **Targeted Undo**: Perfect for reversing a specific problematic change without affecting other work
|
||||||
|
- **Minimal Impact**: Only affects the resources that were modified in that particular commit
|
||||||
|
- **Use Case**: Ideal when you want to undo a specific change while keeping all other modifications intact
|
||||||
|
|
||||||
|
#### Roll back to this commit
|
||||||
|
This option performs a complete restoration to the selected point in time:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- **Complete State Restoration**: Returns the entire folder to its exact state at the time of this commit
|
||||||
|
- **Restore All Child Folders**: If enabled, it'll also restore all nested folders to their exact state at the time of this commit.
|
||||||
|
- **Destructive Operation**: Discards ALL changes made after the selected commit
|
||||||
|
- **New Commit Creation**: Creates a new commit representing this rollback operation
|
||||||
|
- **Use Case**: Ideal when you want to completely undo a series of changes and return to a known good state
|
||||||
|
|
||||||
|
**Warning**: This operation will undo all modifications made after the selected commit, which may include multiple secrets and configuration changes.
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Legacy Snapshots (Deprecated)">
|
||||||
|
<Warning>
|
||||||
|
The snapshots interface is deprecated and will be removed in a future version. Please use the new Commits interface for more granular point-in-time recovery operations.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
|
## Snapshots
|
||||||
|
|
||||||
|
Similar to Git, a commit (also known as snapshot) in Infisical is the state of your project's secrets at a specific point in time scoped to
|
||||||
|
an environment and [folder](./folder) within it.
|
||||||
|
|
||||||
|
To view a list of snapshots for the current folder, press the **Commits** button.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This opens up a sidebar from which you can select to view a particular snapshot:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Rolling back
|
||||||
|
|
||||||
|
After pressing on a snapshot from the sidebar, you can view it and roll back the state
|
||||||
|
of the folder to that point in time by pressing the **Rollback** button.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Rolling back secrets to a past snapshot creates a snapshot at the top of the stack and updates secret versions.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Rollbacks are localized to not affect other folders within the same environment. This means each [folder](./folder) maintains its own independent history of changes, offering precise and isolated control over rollback actions.
|
||||||
|
Put differently, every [folder](./folder) possesses a distinct and separate timeline, providing granular control when managing your secrets.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1022 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 989 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
@@ -176,6 +176,13 @@ Supports conditions and permission inversion
|
|||||||
| `read` | View secret versions and snapshots |
|
| `read` | View secret versions and snapshots |
|
||||||
| `create` | Roll back secrets to snapshots |
|
| `create` | Roll back secrets to snapshots |
|
||||||
|
|
||||||
|
#### Subject: `commits`
|
||||||
|
|
||||||
|
| Action | Description |
|
||||||
|
| -------- | ---------------------------------- |
|
||||||
|
| `read` | View commits and changes across folders |
|
||||||
|
| `perform-rollback` | Roll back commits changes and restore folders to previous state|
|
||||||
|
|
||||||
#### Subject: `secret-approval`
|
#### Subject: `secret-approval`
|
||||||
|
|
||||||
| Action | Description |
|
| Action | Description |
|
||||||
|
|||||||
+1
-2
@@ -826,8 +826,7 @@
|
|||||||
"api-reference/endpoints/workspaces/delete-workspace",
|
"api-reference/endpoints/workspaces/delete-workspace",
|
||||||
"api-reference/endpoints/workspaces/get-workspace",
|
"api-reference/endpoints/workspaces/get-workspace",
|
||||||
"api-reference/endpoints/workspaces/update-workspace",
|
"api-reference/endpoints/workspaces/update-workspace",
|
||||||
"api-reference/endpoints/workspaces/secret-snapshots",
|
"api-reference/endpoints/workspaces/secret-snapshots"
|
||||||
"api-reference/endpoints/workspaces/rollback-snapshot"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,13 +14,15 @@ type Props = {
|
|||||||
selectedPathSegmentIndex: number;
|
selectedPathSegmentIndex: number;
|
||||||
environmentSlug: string;
|
environmentSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
disableCopy?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SecretDashboardPathBreadcrumb = ({
|
export const SecretDashboardPathBreadcrumb = ({
|
||||||
secretPathSegments,
|
secretPathSegments,
|
||||||
selectedPathSegmentIndex,
|
selectedPathSegmentIndex,
|
||||||
environmentSlug,
|
environmentSlug,
|
||||||
projectId
|
projectId,
|
||||||
|
disableCopy
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [, isCopying, setIsCopying] = useTimedReset({
|
const [, isCopying, setIsCopying] = useTimedReset({
|
||||||
initialState: false
|
initialState: false
|
||||||
@@ -32,7 +34,7 @@ export const SecretDashboardPathBreadcrumb = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
{isLastItem ? (
|
{isLastItem && !disableCopy ? (
|
||||||
<div className="group flex items-center space-x-2">
|
<div className="group flex items-center space-x-2">
|
||||||
<span
|
<span
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
|
|||||||
@@ -75,6 +75,18 @@ export const ROUTE_PATHS = Object.freeze({
|
|||||||
"/secret-manager/$projectId/secrets/$envSlug",
|
"/secret-manager/$projectId/secrets/$envSlug",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug"
|
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/secrets/$envSlug"
|
||||||
),
|
),
|
||||||
|
RollbackPreviewPage: setRoute(
|
||||||
|
"/secret-manager/$projectId/commits/$environment/$folderId/$commitId/restore",
|
||||||
|
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore"
|
||||||
|
),
|
||||||
|
CommitDetailsPage: setRoute(
|
||||||
|
"/secret-manager/$projectId/commits/$environment/$folderId/$commitId",
|
||||||
|
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId"
|
||||||
|
),
|
||||||
|
CommitsPage: setRoute(
|
||||||
|
"/secret-manager/$projectId/commits/$environment/$folderId",
|
||||||
|
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId"
|
||||||
|
),
|
||||||
OverviewPage: setRoute(
|
OverviewPage: setRoute(
|
||||||
"/secret-manager/$projectId/overview",
|
"/secret-manager/$projectId/overview",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/overview"
|
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/overview"
|
||||||
|
|||||||
@@ -152,6 +152,11 @@ export enum PermissionConditionOperators {
|
|||||||
$ELEMENTMATCH = "$elemMatch"
|
$ELEMENTMATCH = "$elemMatch"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionCommitsActions {
|
||||||
|
Read = "read",
|
||||||
|
PerformRollback = "perform-rollback"
|
||||||
|
}
|
||||||
|
|
||||||
export type IdentityManagementSubjectFields = {
|
export type IdentityManagementSubjectFields = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
};
|
};
|
||||||
@@ -229,6 +234,7 @@ export enum ProjectPermissionSub {
|
|||||||
Cmek = "cmek",
|
Cmek = "cmek",
|
||||||
SecretSyncs = "secret-syncs",
|
SecretSyncs = "secret-syncs",
|
||||||
Kmip = "kmip",
|
Kmip = "kmip",
|
||||||
|
Commits = "commits",
|
||||||
SecretScanningDataSources = "secret-scanning-data-sources",
|
SecretScanningDataSources = "secret-scanning-data-sources",
|
||||||
SecretScanningFindings = "secret-scanning-findings",
|
SecretScanningFindings = "secret-scanning-findings",
|
||||||
SecretScanningConfigs = "secret-scanning-configs"
|
SecretScanningConfigs = "secret-scanning-configs"
|
||||||
@@ -367,6 +373,7 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
|
| [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek]
|
||||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
|
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
|
||||||
| [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip]
|
| [ProjectPermissionKmipActions, ProjectPermissionSub.Kmip]
|
||||||
|
| [ProjectPermissionCommitsActions, ProjectPermissionSub.Commits]
|
||||||
| [
|
| [
|
||||||
ProjectPermissionSecretScanningDataSourceActions,
|
ProjectPermissionSecretScanningDataSourceActions,
|
||||||
ProjectPermissionSub.SecretScanningDataSources
|
ProjectPermissionSub.SecretScanningDataSources
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { useGetFolderCommitHistory, useGetFolderCommitsCount } from "./queries";
|
||||||
|
export * from "./types";
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { CommitHistoryItem, CommitWithChanges, RollbackPreview } from "./types";
|
||||||
|
|
||||||
|
export const commitKeys = {
|
||||||
|
count: ({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
directory
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
directory?: string;
|
||||||
|
}) => [{ workspaceId, environment, directory }, "folder-commits-count"] as const,
|
||||||
|
|
||||||
|
history: ({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
directory
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
directory?: string;
|
||||||
|
}) => [{ workspaceId, environment, directory }, "folder-commits"] as const,
|
||||||
|
|
||||||
|
details: ({ workspaceId, commitId }: { workspaceId: string; commitId: string }) =>
|
||||||
|
[{ workspaceId, commitId }, "commit-details"] as const,
|
||||||
|
|
||||||
|
rollbackPreview: ({
|
||||||
|
folderId,
|
||||||
|
commitId,
|
||||||
|
envSlug,
|
||||||
|
projectId,
|
||||||
|
deepRollback
|
||||||
|
}: {
|
||||||
|
folderId: string;
|
||||||
|
commitId: string;
|
||||||
|
envSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
deepRollback: boolean;
|
||||||
|
}) => [{ folderId, commitId, envSlug, projectId, deepRollback }, "rollback-preview"] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchFolderCommitsCount = async ({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
directory
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
directory?: string;
|
||||||
|
}) => {
|
||||||
|
const res = await apiRequest.get<{ count: number; folderId: string }>(
|
||||||
|
"/api/v1/pit/commits/count",
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
environment,
|
||||||
|
path: directory,
|
||||||
|
projectId: workspaceId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchFolderCommitHistory = async (
|
||||||
|
workspaceId: string,
|
||||||
|
environment: string,
|
||||||
|
directory: string,
|
||||||
|
offset: number = 0,
|
||||||
|
limit: number = 20,
|
||||||
|
search?: string,
|
||||||
|
sort: "asc" | "desc" = "desc"
|
||||||
|
): Promise<{
|
||||||
|
commits: CommitHistoryItem[];
|
||||||
|
total: number;
|
||||||
|
hasMore: boolean;
|
||||||
|
}> => {
|
||||||
|
const res = await apiRequest.get<{
|
||||||
|
commits: CommitHistoryItem[];
|
||||||
|
total: number;
|
||||||
|
hasMore: boolean;
|
||||||
|
}>("/api/v1/pit/commits", {
|
||||||
|
params: {
|
||||||
|
environment,
|
||||||
|
path: directory,
|
||||||
|
projectId: workspaceId,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
search,
|
||||||
|
sort
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchCommitDetails = async (workspaceId: string, commitId: string) => {
|
||||||
|
const { data } = await apiRequest.get<CommitWithChanges>(
|
||||||
|
`/api/v1/pit/commits/${commitId}/changes`,
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
projectId: workspaceId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchRollbackPreview = async (
|
||||||
|
folderId: string,
|
||||||
|
commitId: string,
|
||||||
|
envSlug: string,
|
||||||
|
workspaceId: string,
|
||||||
|
deepRollback: boolean,
|
||||||
|
secretPath: string
|
||||||
|
): Promise<RollbackPreview[]> => {
|
||||||
|
const { data } = await apiRequest.get<RollbackPreview[]>(
|
||||||
|
`/api/v1/pit/commits/${commitId}/compare`,
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
folderId,
|
||||||
|
environment: envSlug,
|
||||||
|
deepRollback,
|
||||||
|
secretPath,
|
||||||
|
projectId: workspaceId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchRollback = async (
|
||||||
|
folderId: string,
|
||||||
|
commitId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
deepRollback: boolean,
|
||||||
|
message?: string,
|
||||||
|
envSlug?: string
|
||||||
|
) => {
|
||||||
|
const { data } = await apiRequest.post<{ success: boolean }>(
|
||||||
|
`/api/v1/pit/commits/${commitId}/rollback`,
|
||||||
|
{
|
||||||
|
folderId,
|
||||||
|
deepRollback,
|
||||||
|
message,
|
||||||
|
environment: envSlug,
|
||||||
|
projectId: workspaceId
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchRevert = async (commitId: string, workspaceId: string) => {
|
||||||
|
const { data } = await apiRequest.post<{ success: boolean; message: string }>(
|
||||||
|
`/api/v1/pit/commits/${commitId}/revert`,
|
||||||
|
{
|
||||||
|
projectId: workspaceId
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCommitRevert = ({
|
||||||
|
commitId,
|
||||||
|
projectId,
|
||||||
|
environment,
|
||||||
|
directory
|
||||||
|
}: {
|
||||||
|
commitId: string;
|
||||||
|
projectId: string;
|
||||||
|
environment: string;
|
||||||
|
directory: string;
|
||||||
|
}) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => fetchRevert(commitId, projectId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [
|
||||||
|
commitKeys.details({ workspaceId: projectId, commitId }),
|
||||||
|
commitKeys.history({ workspaceId: projectId, environment, directory }),
|
||||||
|
commitKeys.count({ workspaceId: projectId, environment, directory })
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCommitRollback = ({
|
||||||
|
workspaceId,
|
||||||
|
commitId,
|
||||||
|
folderId,
|
||||||
|
deepRollback,
|
||||||
|
environment,
|
||||||
|
directory,
|
||||||
|
envSlug
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
commitId: string;
|
||||||
|
folderId: string;
|
||||||
|
deepRollback: boolean;
|
||||||
|
environment: string;
|
||||||
|
directory: string;
|
||||||
|
envSlug: string;
|
||||||
|
}) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (message: string) =>
|
||||||
|
fetchRollback(folderId, commitId, workspaceId, deepRollback, message, envSlug),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [
|
||||||
|
commitKeys.details({ workspaceId, commitId }),
|
||||||
|
commitKeys.history({ workspaceId, environment, directory }),
|
||||||
|
commitKeys.count({ workspaceId, environment, directory })
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetFolderCommitsCount = ({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
directory,
|
||||||
|
isPaused
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
directory: string;
|
||||||
|
isPaused?: boolean;
|
||||||
|
}) =>
|
||||||
|
useQuery({
|
||||||
|
enabled: Boolean(workspaceId && environment) && !isPaused,
|
||||||
|
queryKey: commitKeys.count({ workspaceId, environment, directory }),
|
||||||
|
queryFn: () => fetchFolderCommitsCount({ workspaceId, environment, directory })
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useGetFolderCommitHistory = ({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
directory,
|
||||||
|
offset = 0,
|
||||||
|
limit = 20,
|
||||||
|
search,
|
||||||
|
sort = "desc"
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
directory: string;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
search?: string;
|
||||||
|
sort?: "asc" | "desc";
|
||||||
|
}) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [
|
||||||
|
commitKeys.history({ workspaceId, environment, directory }),
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
search,
|
||||||
|
sort
|
||||||
|
],
|
||||||
|
queryFn: () =>
|
||||||
|
fetchFolderCommitHistory(workspaceId, environment, directory, offset, limit, search, sort),
|
||||||
|
enabled: Boolean(workspaceId && environment)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetCommitDetails = (workspaceId: string, commitId: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: commitKeys.details({ workspaceId, commitId }),
|
||||||
|
queryFn: () => fetchCommitDetails(workspaceId, commitId),
|
||||||
|
enabled: Boolean(workspaceId) && Boolean(commitId)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetRollbackPreview = (
|
||||||
|
folderId: string,
|
||||||
|
commitId: string,
|
||||||
|
envSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
deepRollback: boolean,
|
||||||
|
secretPath: string
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: commitKeys.rollbackPreview({ folderId, commitId, envSlug, projectId, deepRollback }),
|
||||||
|
queryFn: () =>
|
||||||
|
fetchRollbackPreview(folderId, commitId, envSlug, projectId, deepRollback, secretPath),
|
||||||
|
enabled: Boolean(folderId) && Boolean(commitId)
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { CommitType, SecretVersions } from "../types";
|
||||||
|
|
||||||
|
export type CommitHistoryItem = {
|
||||||
|
id: string;
|
||||||
|
commitId: string;
|
||||||
|
actorMetadata: {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
actorType: string;
|
||||||
|
message: string;
|
||||||
|
folderId: string;
|
||||||
|
envId: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
isLatest: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TFolderCommitChanges = {
|
||||||
|
id: string;
|
||||||
|
folderCommitId: string;
|
||||||
|
changeType: CommitType;
|
||||||
|
isUpdate: boolean;
|
||||||
|
secretVersionId: string | null;
|
||||||
|
folderVersionId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
versions: SecretVersions[];
|
||||||
|
secretKey?: string;
|
||||||
|
folderName?: string;
|
||||||
|
secretVersion?: string;
|
||||||
|
folderVersion?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FolderReconstructedItem = {
|
||||||
|
type: string;
|
||||||
|
id: string;
|
||||||
|
versionId: string;
|
||||||
|
folderName?: string;
|
||||||
|
folderVersion?: number;
|
||||||
|
secretKey?: string;
|
||||||
|
secretVersion?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CommitWithChanges = {
|
||||||
|
changes: CommitHistoryItem & {
|
||||||
|
changes: TFolderCommitChanges[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RollbackChange = {
|
||||||
|
type: "folder" | "secret";
|
||||||
|
id: string;
|
||||||
|
versionId: string;
|
||||||
|
changeType: "create" | "update" | "delete";
|
||||||
|
commitId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RollbackPreview = {
|
||||||
|
folderId: string;
|
||||||
|
folderName: string;
|
||||||
|
folderPath: string;
|
||||||
|
changes: RollbackChange[];
|
||||||
|
};
|
||||||
@@ -11,7 +11,8 @@ export enum ApprovalStatus {
|
|||||||
export enum CommitType {
|
export enum CommitType {
|
||||||
DELETE = "delete",
|
DELETE = "delete",
|
||||||
UPDATE = "update",
|
UPDATE = "update",
|
||||||
CREATE = "create"
|
CREATE = "create",
|
||||||
|
ADD = "add"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TSecretApprovalSecChangeData = {
|
export type TSecretApprovalSecChangeData = {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
|
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
|
||||||
|
|
||||||
|
import { commitKeys } from "../folderCommits/queries";
|
||||||
import { secretSnapshotKeys } from "../secretSnapshots/queries";
|
import { secretSnapshotKeys } from "../secretSnapshots/queries";
|
||||||
import {
|
import {
|
||||||
TCreateFolderDTO,
|
TCreateFolderDTO,
|
||||||
@@ -166,6 +167,9 @@ export const useCreateFolder = () => {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: secretSnapshotKeys.count({ workspaceId: projectId, environment, directory: path })
|
queryKey: secretSnapshotKeys.count({ workspaceId: projectId, environment, directory: path })
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: commitKeys.count({ workspaceId: projectId, environment, directory: path })
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -200,6 +204,12 @@ export const useUpdateFolder = () => {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: secretSnapshotKeys.count({ workspaceId: projectId, environment, directory: path })
|
queryKey: secretSnapshotKeys.count({ workspaceId: projectId, environment, directory: path })
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: commitKeys.count({ workspaceId: projectId, environment, directory: path })
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: commitKeys.history({ workspaceId: projectId, environment, directory: path })
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -234,6 +244,12 @@ export const useDeleteFolder = () => {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: secretSnapshotKeys.count({ workspaceId: projectId, environment, directory: path })
|
queryKey: secretSnapshotKeys.count({ workspaceId: projectId, environment, directory: path })
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: commitKeys.count({ workspaceId: projectId, environment, directory: path })
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: commitKeys.history({ workspaceId: projectId, environment, directory: path })
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -279,6 +295,20 @@ export const useUpdateFolderBatch = () => {
|
|||||||
directory: folder.path
|
directory: folder.path
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: commitKeys.count({
|
||||||
|
workspaceId: projectId,
|
||||||
|
environment: folder.environment,
|
||||||
|
directory: folder.path
|
||||||
|
})
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: commitKeys.history({
|
||||||
|
workspaceId: projectId,
|
||||||
|
environment: folder.environment,
|
||||||
|
directory: folder.path
|
||||||
|
})
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { MutationOptions, useMutation, useQueryClient } from "@tanstack/react-qu
|
|||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
|
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
|
||||||
|
|
||||||
|
import { commitKeys } from "../folderCommits/queries";
|
||||||
import { secretApprovalRequestKeys } from "../secretApprovalRequest/queries";
|
import { secretApprovalRequestKeys } from "../secretApprovalRequest/queries";
|
||||||
import { secretSnapshotKeys } from "../secretSnapshots/queries";
|
import { secretSnapshotKeys } from "../secretSnapshots/queries";
|
||||||
import { secretKeys } from "./queries";
|
import { secretKeys } from "./queries";
|
||||||
@@ -59,6 +60,12 @@ export const useCreateSecretV3 = ({
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
|
queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath })
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath })
|
||||||
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) });
|
queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) });
|
||||||
},
|
},
|
||||||
...options
|
...options
|
||||||
@@ -118,6 +125,12 @@ export const useUpdateSecretV3 = ({
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
|
queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath })
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath })
|
||||||
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) });
|
queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) });
|
||||||
},
|
},
|
||||||
...options
|
...options
|
||||||
@@ -164,6 +177,12 @@ export const useDeleteSecretV3 = ({
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
|
queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath })
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath })
|
||||||
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) });
|
queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) });
|
||||||
},
|
},
|
||||||
...options
|
...options
|
||||||
@@ -200,6 +219,12 @@ export const useCreateSecretBatch = ({
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
|
queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath })
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath })
|
||||||
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) });
|
queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) });
|
||||||
},
|
},
|
||||||
...options
|
...options
|
||||||
@@ -236,6 +261,12 @@ export const useUpdateSecretBatch = ({
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
|
queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath })
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath })
|
||||||
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) });
|
queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) });
|
||||||
},
|
},
|
||||||
...options
|
...options
|
||||||
@@ -274,6 +305,12 @@ export const useDeleteSecretBatch = ({
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
|
queryKey: secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: commitKeys.count({ workspaceId, environment, directory: secretPath })
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: commitKeys.history({ workspaceId, environment, directory: secretPath })
|
||||||
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) });
|
queryClient.invalidateQueries({ queryKey: secretApprovalRequestKeys.count({ workspaceId }) });
|
||||||
},
|
},
|
||||||
...options
|
...options
|
||||||
@@ -347,6 +384,20 @@ export const useMoveSecrets = ({
|
|||||||
directory: sourceSecretPath
|
directory: sourceSecretPath
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: commitKeys.count({
|
||||||
|
workspaceId: projectId,
|
||||||
|
environment: sourceEnvironment,
|
||||||
|
directory: sourceSecretPath
|
||||||
|
})
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: commitKeys.history({
|
||||||
|
workspaceId: projectId,
|
||||||
|
environment: sourceEnvironment,
|
||||||
|
directory: sourceSecretPath
|
||||||
|
})
|
||||||
|
});
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: secretApprovalRequestKeys.count({ workspaceId: projectId })
|
queryKey: secretApprovalRequestKeys.count({ workspaceId: projectId })
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -282,7 +282,8 @@ export const useUpdateProject = () => {
|
|||||||
newProjectName,
|
newProjectName,
|
||||||
newProjectDescription,
|
newProjectDescription,
|
||||||
newSlug,
|
newSlug,
|
||||||
secretSharing
|
secretSharing,
|
||||||
|
showSnapshotsLegacy
|
||||||
}) => {
|
}) => {
|
||||||
const { data } = await apiRequest.patch<{ workspace: Workspace }>(
|
const { data } = await apiRequest.patch<{ workspace: Workspace }>(
|
||||||
`/api/v1/workspace/${projectID}`,
|
`/api/v1/workspace/${projectID}`,
|
||||||
@@ -290,7 +291,8 @@ export const useUpdateProject = () => {
|
|||||||
name: newProjectName,
|
name: newProjectName,
|
||||||
description: newProjectDescription,
|
description: newProjectDescription,
|
||||||
slug: newSlug,
|
slug: newSlug,
|
||||||
secretSharing
|
secretSharing,
|
||||||
|
showSnapshotsLegacy
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return data.workspace;
|
return data.workspace;
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export type Workspace = {
|
|||||||
roles?: TProjectRole[];
|
roles?: TProjectRole[];
|
||||||
hasDeleteProtection: boolean;
|
hasDeleteProtection: boolean;
|
||||||
secretSharing: boolean;
|
secretSharing: boolean;
|
||||||
|
showSnapshotsLegacy: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkspaceEnv = {
|
export type WorkspaceEnv = {
|
||||||
@@ -79,6 +80,7 @@ export type UpdateProjectDTO = {
|
|||||||
newProjectDescription?: string;
|
newProjectDescription?: string;
|
||||||
newSlug?: string;
|
newSlug?: string;
|
||||||
secretSharing?: boolean;
|
secretSharing?: boolean;
|
||||||
|
showSnapshotsLegacy?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
|
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { useGetProjectTypeFromRoute } from "@app/hooks";
|
|||||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
EXCLUDED_PERMISSION_SUBS,
|
||||||
isConditionalSubjects,
|
isConditionalSubjects,
|
||||||
PROJECT_PERMISSION_OBJECT,
|
PROJECT_PERMISSION_OBJECT,
|
||||||
ProjectTypePermissionSubjects,
|
ProjectTypePermissionSubjects,
|
||||||
@@ -66,6 +67,7 @@ const Content = ({ onClose }: ContentProps) => {
|
|||||||
subject as ProjectPermissionSub
|
subject as ProjectPermissionSub
|
||||||
] && (search ? title.toLowerCase().includes(search.toLowerCase()) : true)
|
] && (search ? title.toLowerCase().includes(search.toLowerCase()) : true)
|
||||||
)
|
)
|
||||||
|
.filter(([subject]) => !EXCLUDED_PERMISSION_SUBS.includes(subject as ProjectPermissionSub))
|
||||||
.sort((a, b) => a[1].title.localeCompare(b[1].title))
|
.sort((a, b) => a[1].title.localeCompare(b[1].title))
|
||||||
.map(([subject]) => subject);
|
.map(([subject]) => subject);
|
||||||
|
|
||||||
|
|||||||
+37
-1
@@ -12,6 +12,7 @@ import {
|
|||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import {
|
import {
|
||||||
PermissionConditionOperators,
|
PermissionConditionOperators,
|
||||||
|
ProjectPermissionCommitsActions,
|
||||||
ProjectPermissionDynamicSecretActions,
|
ProjectPermissionDynamicSecretActions,
|
||||||
ProjectPermissionGroupActions,
|
ProjectPermissionGroupActions,
|
||||||
ProjectPermissionIdentityActions,
|
ProjectPermissionIdentityActions,
|
||||||
@@ -92,6 +93,11 @@ const SecretSyncPolicyActionSchema = z.object({
|
|||||||
[ProjectPermissionSecretSyncActions.RemoveSecrets]: z.boolean().optional()
|
[ProjectPermissionSecretSyncActions.RemoveSecrets]: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const CommitPolicyActionSchema = z.object({
|
||||||
|
[ProjectPermissionCommitsActions.Read]: z.boolean().optional(),
|
||||||
|
[ProjectPermissionCommitsActions.PerformRollback]: z.boolean().optional()
|
||||||
|
});
|
||||||
|
|
||||||
const SecretRotationPolicyActionSchema = z.object({
|
const SecretRotationPolicyActionSchema = z.object({
|
||||||
[ProjectPermissionSecretRotationActions.Read]: z.boolean().optional(),
|
[ProjectPermissionSecretRotationActions.Read]: z.boolean().optional(),
|
||||||
[ProjectPermissionSecretRotationActions.ReadGeneratedCredentials]: z.boolean().optional(),
|
[ProjectPermissionSecretRotationActions.ReadGeneratedCredentials]: z.boolean().optional(),
|
||||||
@@ -285,6 +291,7 @@ export const projectRoleFormSchema = z.object({
|
|||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.default([]),
|
.default([]),
|
||||||
|
[ProjectPermissionSub.Commits]: CommitPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.Member]: MemberPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.Member]: MemberPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.Groups]: GroupPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.Groups]: GroupPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.Role]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.Role]: GeneralPolicyActionSchema.array().default([]),
|
||||||
@@ -885,6 +892,17 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subject === ProjectPermissionSub.Commits) {
|
||||||
|
const canRead = action.includes(ProjectPermissionCommitsActions.Read);
|
||||||
|
const canPerformRollback = action.includes(ProjectPermissionCommitsActions.PerformRollback);
|
||||||
|
|
||||||
|
if (!formVal[subject]) formVal[subject] = [{}];
|
||||||
|
if (canRead) formVal[subject]![0][ProjectPermissionCommitsActions.Read] = true;
|
||||||
|
if (canPerformRollback)
|
||||||
|
formVal[subject]![0][ProjectPermissionCommitsActions.PerformRollback] = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (subject === ProjectPermissionSub.PkiSubscribers) {
|
if (subject === ProjectPermissionSub.PkiSubscribers) {
|
||||||
if (!formVal[subject]) formVal[subject] = [];
|
if (!formVal[subject]) formVal[subject] = [];
|
||||||
|
|
||||||
@@ -1032,6 +1050,8 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
|||||||
return permissions;
|
return permissions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const EXCLUDED_PERMISSION_SUBS = [ProjectPermissionSub.SecretRollback];
|
||||||
|
|
||||||
export type TProjectPermissionObject = {
|
export type TProjectPermissionObject = {
|
||||||
[K in ProjectPermissionSub]: {
|
[K in ProjectPermissionSub]: {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -1227,6 +1247,13 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
|||||||
{ label: "Remove", value: "delete" }
|
{ label: "Remove", value: "delete" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
[ProjectPermissionSub.Commits]: {
|
||||||
|
title: "Commits",
|
||||||
|
actions: [
|
||||||
|
{ label: "View", value: ProjectPermissionCommitsActions.Read },
|
||||||
|
{ label: "Perform Rollback", value: ProjectPermissionCommitsActions.PerformRollback }
|
||||||
|
]
|
||||||
|
},
|
||||||
[ProjectPermissionSub.Tags]: {
|
[ProjectPermissionSub.Tags]: {
|
||||||
title: "Tags",
|
title: "Tags",
|
||||||
actions: [
|
actions: [
|
||||||
@@ -1518,7 +1545,8 @@ const SecretsManagerPermissionSubjects = (enabled = false) => ({
|
|||||||
[ProjectPermissionSub.IpAllowList]: enabled,
|
[ProjectPermissionSub.IpAllowList]: enabled,
|
||||||
[ProjectPermissionSub.SecretRollback]: enabled,
|
[ProjectPermissionSub.SecretRollback]: enabled,
|
||||||
[ProjectPermissionSub.SecretRotation]: enabled,
|
[ProjectPermissionSub.SecretRotation]: enabled,
|
||||||
[ProjectPermissionSub.ServiceTokens]: enabled
|
[ProjectPermissionSub.ServiceTokens]: enabled,
|
||||||
|
[ProjectPermissionSub.Commits]: enabled
|
||||||
});
|
});
|
||||||
|
|
||||||
const KmsPermissionSubjects = (enabled = false) => ({
|
const KmsPermissionSubjects = (enabled = false) => ({
|
||||||
@@ -1898,6 +1926,10 @@ export const RoleTemplates: Record<ProjectType, RoleTemplate[]> = {
|
|||||||
{
|
{
|
||||||
subject: ProjectPermissionSub.SecretSyncs,
|
subject: ProjectPermissionSub.SecretSyncs,
|
||||||
actions: [ProjectPermissionSecretSyncActions.Read]
|
actions: [ProjectPermissionSecretSyncActions.Read]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: ProjectPermissionSub.Commits,
|
||||||
|
actions: [ProjectPermissionCommitsActions.Read]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1955,6 +1987,10 @@ export const RoleTemplates: Record<ProjectType, RoleTemplate[]> = {
|
|||||||
{
|
{
|
||||||
subject: ProjectPermissionSub.SecretSyncs,
|
subject: ProjectPermissionSub.SecretSyncs,
|
||||||
actions: Object.values(ProjectPermissionSecretSyncActions)
|
actions: Object.values(ProjectPermissionSecretSyncActions)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: ProjectPermissionSub.Commits,
|
||||||
|
actions: Object.values(ProjectPermissionCommitsActions)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { PermissionEmptyState } from "./PermissionEmptyState";
|
|||||||
import { PkiSubscriberPermissionConditions } from "./PkiSubscriberPermissionConditions";
|
import { PkiSubscriberPermissionConditions } from "./PkiSubscriberPermissionConditions";
|
||||||
import { PkiTemplatePermissionConditions } from "./PkiTemplatePermissionConditions";
|
import { PkiTemplatePermissionConditions } from "./PkiTemplatePermissionConditions";
|
||||||
import {
|
import {
|
||||||
|
EXCLUDED_PERMISSION_SUBS,
|
||||||
formRolePermission2API,
|
formRolePermission2API,
|
||||||
isConditionalSubjects,
|
isConditionalSubjects,
|
||||||
PROJECT_PERMISSION_OBJECT,
|
PROJECT_PERMISSION_OBJECT,
|
||||||
@@ -176,6 +177,7 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
|
|||||||
<div className="py-4">
|
<div className="py-4">
|
||||||
{!isPending && <PermissionEmptyState />}
|
{!isPending && <PermissionEmptyState />}
|
||||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[])
|
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[])
|
||||||
|
.filter((subject) => !EXCLUDED_PERMISSION_SUBS.includes(subject))
|
||||||
.filter((subject) => ProjectTypePermissionSubjects[currentWorkspace.type][subject])
|
.filter((subject) => ProjectTypePermissionSubjects[currentWorkspace.type][subject])
|
||||||
.map((subject) => (
|
.map((subject) => (
|
||||||
<GeneralPermissionPolicies
|
<GeneralPermissionPolicies
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import {
|
||||||
|
ProjectPermissionCommitsActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
|
import { CommitDetailsTab } from "./components/CommitDetailsTab";
|
||||||
|
|
||||||
|
export const CommitDetailsPage = () => {
|
||||||
|
const envSlug = useParams({
|
||||||
|
from: ROUTE_PATHS.SecretManager.CommitDetailsPage.id,
|
||||||
|
select: (el) => el.environment
|
||||||
|
});
|
||||||
|
const selectedCommitId = useParams({
|
||||||
|
from: ROUTE_PATHS.SecretManager.CommitDetailsPage.id,
|
||||||
|
select: (el) => el.commitId
|
||||||
|
});
|
||||||
|
const folderId = useParams({
|
||||||
|
from: ROUTE_PATHS.SecretManager.CommitDetailsPage.id,
|
||||||
|
select: (el) => el.folderId
|
||||||
|
});
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const routerQueryParams: { secretPath?: string } = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.CommitDetailsPage.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretPath = (routerQueryParams.secretPath as string) || "/";
|
||||||
|
|
||||||
|
const handleGoBackToHistory = () => {
|
||||||
|
navigate({
|
||||||
|
to: `/${ProjectType.SecretManager}/$projectId/commits/$environment/$folderId` as const,
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id,
|
||||||
|
folderId,
|
||||||
|
environment: envSlug
|
||||||
|
},
|
||||||
|
search: (query) => ({
|
||||||
|
...query,
|
||||||
|
secretPath
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGoToRollbackPreview = () => {
|
||||||
|
navigate({
|
||||||
|
to: `/${ProjectType.SecretManager}/$projectId/commits/$environment/$folderId/$commitId/restore` as const,
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id,
|
||||||
|
folderId,
|
||||||
|
environment: envSlug,
|
||||||
|
commitId: selectedCommitId
|
||||||
|
},
|
||||||
|
search: (query) => ({
|
||||||
|
...query,
|
||||||
|
secretPath
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto flex w-full max-w-7xl justify-center bg-bunker-800 pb-4 pt-2 text-white">
|
||||||
|
<div className="w-full max-w-[75vw]">
|
||||||
|
<ProjectPermissionCan
|
||||||
|
renderGuardBanner
|
||||||
|
I={ProjectPermissionCommitsActions.Read}
|
||||||
|
a={ProjectPermissionSub.Commits}
|
||||||
|
>
|
||||||
|
<CommitDetailsTab
|
||||||
|
selectedCommitId={selectedCommitId}
|
||||||
|
workspaceId={currentWorkspace.id}
|
||||||
|
goBackToHistory={handleGoBackToHistory}
|
||||||
|
envSlug={envSlug}
|
||||||
|
goToRollbackPreview={handleGoToRollbackPreview}
|
||||||
|
/>
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+362
@@ -0,0 +1,362 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { faAngleDown } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { DropdownMenuItem } from "@radix-ui/react-dropdown-menu";
|
||||||
|
import { useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
DeleteActionModal,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
IconButton,
|
||||||
|
Spinner
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import {
|
||||||
|
ProjectPermissionCommitsActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { CommitWithChanges } from "@app/hooks/api/folderCommits";
|
||||||
|
import { useCommitRevert, useGetCommitDetails } from "@app/hooks/api/folderCommits/queries";
|
||||||
|
import { CommitType } from "@app/hooks/api/types";
|
||||||
|
|
||||||
|
import { SecretVersionDiffView } from "../SecretVersionDiffView";
|
||||||
|
import { MergedItem } from "./types";
|
||||||
|
|
||||||
|
const formatDisplayDate = (dateString: string): string => {
|
||||||
|
try {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const options: Intl.DateTimeFormatOptions = {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "numeric",
|
||||||
|
hour12: true
|
||||||
|
};
|
||||||
|
return new Intl.DateTimeFormat("en-US", options).format(date);
|
||||||
|
} catch {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CommitDetailsTab = ({
|
||||||
|
selectedCommitId,
|
||||||
|
workspaceId,
|
||||||
|
envSlug,
|
||||||
|
goBackToHistory,
|
||||||
|
goToRollbackPreview
|
||||||
|
}: {
|
||||||
|
selectedCommitId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
envSlug: string;
|
||||||
|
goBackToHistory: () => void;
|
||||||
|
goToRollbackPreview: () => void;
|
||||||
|
}): JSX.Element => {
|
||||||
|
// State for tracking collapsed items (empty by default means all are expanded)
|
||||||
|
const [collapsedItems, setCollapsedItems] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
|
"revertChanges"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const { data: commitDetails, isLoading } = useGetCommitDetails(workspaceId, selectedCommitId);
|
||||||
|
|
||||||
|
const routerQueryParams: { secretPath?: string } = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.CommitDetailsPage.id
|
||||||
|
});
|
||||||
|
const secretPath = (routerQueryParams.secretPath as string) || "/";
|
||||||
|
|
||||||
|
const { mutateAsync: revert } = useCommitRevert({
|
||||||
|
commitId: selectedCommitId,
|
||||||
|
projectId: workspaceId,
|
||||||
|
environment: envSlug,
|
||||||
|
directory: secretPath
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCollapsedItems({});
|
||||||
|
}, [selectedCommitId]);
|
||||||
|
|
||||||
|
const toggleItemCollapsed = (itemId: string): void => {
|
||||||
|
setCollapsedItems((prev) => ({ ...prev, [itemId]: !prev[itemId] }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRevertChanges = async (): Promise<void> => {
|
||||||
|
const response = await revert();
|
||||||
|
if (!response.success) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: response.message
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: response.message
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpClose("revertChanges");
|
||||||
|
|
||||||
|
goBackToHistory();
|
||||||
|
};
|
||||||
|
|
||||||
|
// If no commit is selected or data is loading, show appropriate message
|
||||||
|
if (!selectedCommitId) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-64 items-center justify-center">
|
||||||
|
<p className="text-gray-400">Select a commit to view details</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-64 items-center justify-center">
|
||||||
|
<Spinner size="lg" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!commitDetails) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-64 items-center justify-center">
|
||||||
|
<p className="text-gray-400">No details found for this commit</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the commit details if it's a string
|
||||||
|
let parsedCommitDetails: CommitWithChanges;
|
||||||
|
try {
|
||||||
|
parsedCommitDetails =
|
||||||
|
typeof commitDetails === "string" ? JSON.parse(commitDetails) : commitDetails;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to parse commit details:", error);
|
||||||
|
return (
|
||||||
|
<div className="flex h-64 items-center justify-center">
|
||||||
|
<p className="text-gray-400">Error parsing commit details</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all changes from the commit
|
||||||
|
const commitChanges = parsedCommitDetails.changes?.changes || [];
|
||||||
|
|
||||||
|
// Separate changes by type
|
||||||
|
const addedChanges = commitChanges.filter((c) => c.changeType === CommitType.ADD && !c.isUpdate);
|
||||||
|
const updatedChanges = commitChanges.filter((c) => c.changeType === CommitType.ADD && c.isUpdate);
|
||||||
|
const deletedChanges = commitChanges.filter((c) => c.changeType === CommitType.DELETE);
|
||||||
|
|
||||||
|
// Create merged item list from changes only
|
||||||
|
const changedItems: MergedItem[] = [];
|
||||||
|
|
||||||
|
// Add items from added changes
|
||||||
|
addedChanges.forEach((change) => {
|
||||||
|
changedItems.push({
|
||||||
|
id: change.id,
|
||||||
|
type: change.secretVersionId || change.secretKey ? "secret" : "folder",
|
||||||
|
versionId: change.secretVersionId || change.id,
|
||||||
|
folderName: change.folderName,
|
||||||
|
folderVersion: change.folderVersion,
|
||||||
|
secretKey: change.secretKey,
|
||||||
|
secretVersion: change.secretVersion,
|
||||||
|
isAdded: true,
|
||||||
|
versions: change.versions,
|
||||||
|
changeId: change.id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add items from updated changes
|
||||||
|
updatedChanges.forEach((change) => {
|
||||||
|
changedItems.push({
|
||||||
|
id: change.id,
|
||||||
|
type: change.secretVersionId || change.secretKey ? "secret" : "folder",
|
||||||
|
versionId: change.secretVersionId || change.id,
|
||||||
|
folderName: change.folderName,
|
||||||
|
folderVersion: change.folderVersion,
|
||||||
|
secretKey: change.secretKey,
|
||||||
|
secretVersion: change.secretVersion,
|
||||||
|
isUpdated: true,
|
||||||
|
versions: change.versions,
|
||||||
|
changeId: change.id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add deleted items
|
||||||
|
deletedChanges.forEach((change) => {
|
||||||
|
changedItems.push({
|
||||||
|
id: change.id,
|
||||||
|
type: change.secretVersionId || change.secretKey ? "secret" : "folder",
|
||||||
|
secretKey: change.secretKey,
|
||||||
|
folderName: change.folderName,
|
||||||
|
secretVersion: change.secretVersion,
|
||||||
|
folderVersion: change.folderVersion,
|
||||||
|
isDeleted: true,
|
||||||
|
versions: change.versions,
|
||||||
|
changeId: change.id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort items: deleted first, then folders, then secrets, all alphabetically
|
||||||
|
const sortedChangedItems = [...changedItems].sort((a, b) => {
|
||||||
|
// First sort deleted items to the top
|
||||||
|
if (a.isDeleted !== b.isDeleted) {
|
||||||
|
return a.isDeleted ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then sort by type (folders before secrets)
|
||||||
|
if (a.type !== b.type) {
|
||||||
|
return a.type === "folder" ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally sort alphabetically by name
|
||||||
|
const aName = a.type === "folder" ? a.folderName || "" : a.secretKey || "";
|
||||||
|
const bName = b.type === "folder" ? b.folderName || "" : b.secretKey || "";
|
||||||
|
return aName.localeCompare(bName);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render an item from the merged list
|
||||||
|
const renderMergedItem = (item: MergedItem): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div key={item.id} className="mb-2">
|
||||||
|
<SecretVersionDiffView
|
||||||
|
item={item}
|
||||||
|
isCollapsed={collapsedItems[item.id]}
|
||||||
|
onToggleCollapse={(id) => toggleItemCollapsed(id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format actor display
|
||||||
|
const actorDisplay =
|
||||||
|
parsedCommitDetails.changes?.actorMetadata?.name ||
|
||||||
|
parsedCommitDetails.changes?.actorType ||
|
||||||
|
"Unknown";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div>
|
||||||
|
<div className="flex justify-between pb-2">
|
||||||
|
<div className="w-5/6">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<h1 className="mr-4 truncate text-3xl font-semibold text-white">
|
||||||
|
{parsedCommitDetails.changes?.message || "No message"}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="font-small mb-4 mt-2 flex items-center text-sm">
|
||||||
|
<p>
|
||||||
|
<span> Commited by </span>
|
||||||
|
<b>{actorDisplay}</b>
|
||||||
|
<span> on </span>
|
||||||
|
<b>
|
||||||
|
{formatDisplayDate(
|
||||||
|
parsedCommitDetails.changes?.createdAt || new Date().toISOString()
|
||||||
|
)}
|
||||||
|
</b>
|
||||||
|
{parsedCommitDetails.changes?.isLatest && (
|
||||||
|
<span className="ml-1 italic text-gray-400">(Latest)</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-start">
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionCommitsActions.PerformRollback}
|
||||||
|
a={ProjectPermissionSub.Commits}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
asChild
|
||||||
|
disabled={!isAllowed}
|
||||||
|
className={`${!isAllowed ? "cursor-not-allowed" : ""}`}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="commit-options"
|
||||||
|
variant="outline_bg"
|
||||||
|
className="h-10 rounded border border-mineshaft-600 bg-mineshaft-800 px-4 py-2 text-sm font-medium"
|
||||||
|
>
|
||||||
|
<p className="mr-2">Restore Options</p>
|
||||||
|
<FontAwesomeIcon icon={faAngleDown} />
|
||||||
|
</IconButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="end"
|
||||||
|
sideOffset={2}
|
||||||
|
className="animate-in fade-in-50 zoom-in-95 min-w-[240px] rounded-md bg-mineshaft-800 p-1 shadow-lg"
|
||||||
|
style={{ marginTop: "0" }}
|
||||||
|
>
|
||||||
|
{!parsedCommitDetails.changes.isLatest && (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="group cursor-pointer rounded-md px-3 py-3 transition-colors hover:bg-mineshaft-700"
|
||||||
|
onClick={() => goToRollbackPreview()}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium text-white">
|
||||||
|
Roll back to this commit
|
||||||
|
</span>
|
||||||
|
<span className="max-w-[180px] whitespace-normal break-words text-xs leading-snug text-gray-400">
|
||||||
|
Return this folder to its exact state at the time of this commit,
|
||||||
|
discarding all other changes made after it
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="group cursor-pointer rounded-md px-3 py-3 transition-colors hover:bg-mineshaft-700"
|
||||||
|
onClick={() => handlePopUpOpen("revertChanges")}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium text-white">Revert changes</span>
|
||||||
|
<span className="max-w-[180px] whitespace-normal break-words text-xs leading-snug text-gray-400">
|
||||||
|
Will restore to the previous version of affected resources
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="py-2">
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{sortedChangedItems.length > 0 ? (
|
||||||
|
sortedChangedItems.map((item) => renderMergedItem(item))
|
||||||
|
) : (
|
||||||
|
<div className="flex h-32 items-center justify-center rounded-lg border border-mineshaft-600 bg-mineshaft-800">
|
||||||
|
<p className="text-gray-400">No changed items found</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.revertChanges.isOpen}
|
||||||
|
deleteKey="revert"
|
||||||
|
title="Are you sure you want to revert all changes made in this commit?"
|
||||||
|
subTitle="This will undo all changes made in this commit."
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("revertChanges", isOpen)}
|
||||||
|
onDeleteApproved={handleRevertChanges}
|
||||||
|
buttonText="Yes, revert changes"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { CommitDetailsTab } from "./CommitDetailsTab";
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
export interface MergedItem {
|
||||||
|
id: string;
|
||||||
|
type: "secret" | "folder";
|
||||||
|
versionId?: string;
|
||||||
|
folderName?: string;
|
||||||
|
folderVersion?: string;
|
||||||
|
secretKey?: string;
|
||||||
|
secretVersion?: string;
|
||||||
|
isAdded?: boolean;
|
||||||
|
isUpdated?: boolean;
|
||||||
|
isDeleted?: boolean;
|
||||||
|
versions?: any[];
|
||||||
|
changeId: string;
|
||||||
|
}
|
||||||
+389
@@ -0,0 +1,389 @@
|
|||||||
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { faFolder, faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DeleteActionModal,
|
||||||
|
Input,
|
||||||
|
PageHeader,
|
||||||
|
Spinner,
|
||||||
|
Switch,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import {
|
||||||
|
ProjectPermissionCommitsActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useCommitRollback, useGetRollbackPreview } from "@app/hooks/api/folderCommits/queries";
|
||||||
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
|
import { SecretVersionDiffView } from "../SecretVersionDiffView";
|
||||||
|
|
||||||
|
interface Version {
|
||||||
|
// Common fields
|
||||||
|
id?: string;
|
||||||
|
version: number;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
|
||||||
|
// Secret-specific fields
|
||||||
|
secretKey?: string;
|
||||||
|
secretValue?: string;
|
||||||
|
secretComment?: string;
|
||||||
|
skipMultilineEncoding?: boolean;
|
||||||
|
secretReminderRepeatDays?: number | null;
|
||||||
|
secretReminderNote?: string | null;
|
||||||
|
secretReminderRecipients?: string[];
|
||||||
|
tags?: string[];
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
|
|
||||||
|
// Folder-specific fields
|
||||||
|
name?: string;
|
||||||
|
envId?: string;
|
||||||
|
folderId?: string;
|
||||||
|
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RollbackChange {
|
||||||
|
type: "secret" | "folder";
|
||||||
|
id: string;
|
||||||
|
versionId: string;
|
||||||
|
fromVersion?: number;
|
||||||
|
changeType: "create" | "update" | "delete";
|
||||||
|
commitId: string;
|
||||||
|
secretKey?: string;
|
||||||
|
secretVersion?: number;
|
||||||
|
folderName?: string;
|
||||||
|
folderVersion?: number;
|
||||||
|
versions?: Version[];
|
||||||
|
createdAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FolderChanges {
|
||||||
|
folderId: string;
|
||||||
|
folderName: string;
|
||||||
|
folderPath: string;
|
||||||
|
changes: RollbackChange[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RollbackPreviewTab = (): JSX.Element => {
|
||||||
|
const [deepRollback, setDeepRollback] = useState<boolean>(false);
|
||||||
|
const [message, setMessage] = useState<string>("");
|
||||||
|
const [selectedFolderId, setSelectedFolderId] = useState<string | null>(null);
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const envSlug = useParams({
|
||||||
|
from: ROUTE_PATHS.SecretManager.RollbackPreviewPage.id,
|
||||||
|
select: (el) => el.environment
|
||||||
|
});
|
||||||
|
const selectedCommitId = useParams({
|
||||||
|
from: ROUTE_PATHS.SecretManager.RollbackPreviewPage.id,
|
||||||
|
select: (el) => el.commitId
|
||||||
|
});
|
||||||
|
const folderId = useParams({
|
||||||
|
from: ROUTE_PATHS.SecretManager.RollbackPreviewPage.id,
|
||||||
|
select: (el) => el.folderId
|
||||||
|
});
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const routerQueryParams = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.RollbackPreviewPage.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretPath = (routerQueryParams.secretPath as string) || "/";
|
||||||
|
|
||||||
|
const goBackToHistory = () => {
|
||||||
|
navigate({
|
||||||
|
to: `/${ProjectType.SecretManager}/$projectId/commits/$environment/$folderId` as const,
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id,
|
||||||
|
folderId,
|
||||||
|
environment: envSlug
|
||||||
|
},
|
||||||
|
search: (query) => ({
|
||||||
|
...query,
|
||||||
|
secretPath
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||||
|
"rollbackConfirm"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const { mutateAsync: rollback } = useCommitRollback({
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
commitId: selectedCommitId,
|
||||||
|
folderId,
|
||||||
|
deepRollback,
|
||||||
|
environment: envSlug,
|
||||||
|
directory: secretPath,
|
||||||
|
envSlug
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: rollbackChangesNested, isLoading } = useGetRollbackPreview(
|
||||||
|
folderId,
|
||||||
|
selectedCommitId,
|
||||||
|
envSlug,
|
||||||
|
currentWorkspace.id,
|
||||||
|
deepRollback,
|
||||||
|
secretPath
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRollback = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await rollback(message);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Rollback completed successfully"
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpClose("rollbackConfirm");
|
||||||
|
goBackToHistory();
|
||||||
|
} catch (error) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: error instanceof Error ? error.message : "Failed to rollback changes"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const folderChanges: FolderChanges[] = rollbackChangesNested || [];
|
||||||
|
|
||||||
|
const currentFolderChanges: FolderChanges = folderChanges.find(
|
||||||
|
(folder) => folder.folderId === folderId
|
||||||
|
) || {
|
||||||
|
folderId: folderId || "",
|
||||||
|
folderName: "Current Folder",
|
||||||
|
folderPath: secretPath,
|
||||||
|
changes: []
|
||||||
|
};
|
||||||
|
const nestedFolderChanges: FolderChanges[] = folderChanges.filter(
|
||||||
|
(folder) => folder.folderId !== folderId
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Select the current folder by default
|
||||||
|
if (folderChanges.length > 0) {
|
||||||
|
setSelectedFolderId(currentFolderChanges.folderId);
|
||||||
|
}
|
||||||
|
}, [folderChanges, currentFolderChanges.folderId]);
|
||||||
|
|
||||||
|
if (!selectedCommitId) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-64 items-center justify-center">
|
||||||
|
<p className="text-gray-400">Select a commit to view rollback preview</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-64 items-center justify-center">
|
||||||
|
<Spinner size="lg" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderSidebar = (): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<div className="h-[70vh] w-72 overflow-y-auto bg-mineshaft-900">
|
||||||
|
<div
|
||||||
|
className={`cursor-pointer border-b border-mineshaft-600 ${
|
||||||
|
selectedFolderId === currentFolderChanges.folderId ? "bg-mineshaft-700" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setSelectedFolderId(currentFolderChanges.folderId)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between px-4 py-3">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FontAwesomeIcon icon={faFolder} className="mr-2 text-yellow-500" />
|
||||||
|
<span className="font-medium text-white">
|
||||||
|
{currentFolderChanges.folderPath || currentFolderChanges.folderName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{currentFolderChanges.changes.length > 0 && (
|
||||||
|
<span className="ml-2 rounded-full bg-mineshaft-600 px-2 py-0.5 text-xs text-gray-300">
|
||||||
|
{currentFolderChanges.changes.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{deepRollback && nestedFolderChanges.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="border-b border-mineshaft-600 bg-mineshaft-800 px-4 py-2">
|
||||||
|
<span className="text-sm font-semibold text-white">Child folders to be restored</span>
|
||||||
|
</div>
|
||||||
|
{nestedFolderChanges.map((folder) => (
|
||||||
|
<div
|
||||||
|
key={folder.folderId}
|
||||||
|
className={`cursor-pointer border-b border-mineshaft-600 ${
|
||||||
|
selectedFolderId === folder.folderId ? "bg-mineshaft-700" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setSelectedFolderId(folder.folderId)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between px-4 py-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FontAwesomeIcon icon={faFolder} className="mr-2 text-yellow-500" size="sm" />
|
||||||
|
<span className="max-w-[150px] truncate text-sm font-medium text-white">
|
||||||
|
{folder.folderPath || folder.folderName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{folder.changes.length > 0 && (
|
||||||
|
<span className="rounded-full bg-mineshaft-600 px-2 py-0.5 text-xs text-gray-300">
|
||||||
|
{folder.changes.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSelectedFolderChanges = (): RollbackChange[] => {
|
||||||
|
if (!selectedFolderId) return [];
|
||||||
|
|
||||||
|
const folder = folderChanges.find((f) => f.folderId === selectedFolderId);
|
||||||
|
return folder?.changes || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderMainContent = (): JSX.Element => {
|
||||||
|
const selectedFolderChanges = getSelectedFolderChanges();
|
||||||
|
const selectedFolder = folderChanges.find((f) => f.folderId === selectedFolderId);
|
||||||
|
|
||||||
|
if (!selectedFolder || selectedFolderChanges.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-[70vh] w-full items-center justify-center border border-mineshaft-600">
|
||||||
|
<p className="text-gray-400">No changes in selected folder</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-[70vh] w-full overflow-y-auto border-l border-mineshaft-600">
|
||||||
|
<div className="space-y-4 p-4">
|
||||||
|
{selectedFolderChanges.map((change) => (
|
||||||
|
<div key={change.id} className="mb-4">
|
||||||
|
<SecretVersionDiffView
|
||||||
|
item={{
|
||||||
|
id: change.id,
|
||||||
|
type: change.type,
|
||||||
|
isAdded: change.changeType === "create",
|
||||||
|
isDeleted: change.changeType === "delete",
|
||||||
|
isUpdated: change.changeType === "update",
|
||||||
|
versions: change.versions,
|
||||||
|
isRollback: true,
|
||||||
|
secretKey: change.secretKey,
|
||||||
|
folderName: change.folderName
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto flex w-full max-w-7xl justify-center bg-bunker-800 pb-4 pt-2 text-white">
|
||||||
|
<ProjectPermissionCan
|
||||||
|
renderGuardBanner
|
||||||
|
I={ProjectPermissionCommitsActions.PerformRollback}
|
||||||
|
a={ProjectPermissionSub.Commits}
|
||||||
|
>
|
||||||
|
<div className="w-full max-w-[75vw]">
|
||||||
|
<div className="h-full w-full">
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
title={`Restore folder at commit ${selectedCommitId.substring(0, 8)}`}
|
||||||
|
description={`Will return all changes in this folder to how they appeared at the point of commit ${selectedCommitId.substring(0, 8)}. Any modifications made after this commit will be undone.`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex w-full border border-mineshaft-600">
|
||||||
|
{renderSidebar()}
|
||||||
|
{renderMainContent()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-x border-mineshaft-600 bg-mineshaft-800 px-6 py-3">
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Tooltip content="Will rollback all nested folders to their state at the time of this commit">
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} className="mr-2 text-mineshaft-400" />
|
||||||
|
</Tooltip>
|
||||||
|
<Switch
|
||||||
|
className="ml-2 bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/50"
|
||||||
|
thumbClassName="bg-mineshaft-800"
|
||||||
|
isChecked={deepRollback}
|
||||||
|
onCheckedChange={setDeepRollback}
|
||||||
|
id="deep-rollback"
|
||||||
|
>
|
||||||
|
Restore All Child Folders
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-mineshaft-600 py-4">
|
||||||
|
<div className="flex w-full items-center justify-between gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder="Restore Message"
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
className="w-full border-mineshaft-500 bg-mineshaft-700 py-2 text-sm"
|
||||||
|
maxLength={256}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("rollbackConfirm");
|
||||||
|
}}
|
||||||
|
colorSchema="primary"
|
||||||
|
className="px-6 py-2"
|
||||||
|
isDisabled={
|
||||||
|
message.length === 0 ||
|
||||||
|
!rollbackChangesNested ||
|
||||||
|
!rollbackChangesNested?.some((folder) => {
|
||||||
|
return folder.changes.length > 0;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Restore
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.rollbackConfirm.isOpen}
|
||||||
|
deleteKey="restore"
|
||||||
|
title={`Are you sure you want to restore to commit ${selectedCommitId?.substring(0, 8)}?`}
|
||||||
|
subTitle={`
|
||||||
|
${
|
||||||
|
deepRollback
|
||||||
|
? "This will revert this folder and all child folders to how they appeared at the point in time of this commit."
|
||||||
|
: "This will revert all changes to how they appeared at the point in time of this commit."
|
||||||
|
}
|
||||||
|
Any changes made after this commit will be permanently removed.
|
||||||
|
`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("rollbackConfirm", isOpen)}
|
||||||
|
onDeleteApproved={handleRollback}
|
||||||
|
buttonText="Restore"
|
||||||
|
/>
|
||||||
|
</div>{" "}
|
||||||
|
</div>{" "}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { RollbackPreviewTab } from "./RollbackPreviewTab";
|
||||||
+91
@@ -0,0 +1,91 @@
|
|||||||
|
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretDashboardPathBreadcrumb } from "@app/components/navigation/SecretDashboardPathBreadcrumb";
|
||||||
|
import { BreadcrumbTypes } from "@app/components/v2";
|
||||||
|
|
||||||
|
import { RollbackPreviewTab } from "./RollbackPreviewTab";
|
||||||
|
|
||||||
|
const RollbackPreviewTabQueryParamsSchema = z.object({
|
||||||
|
secretPath: z.string().catch("/")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/restore"
|
||||||
|
)({
|
||||||
|
component: RollbackPreviewTab,
|
||||||
|
validateSearch: zodValidator(RollbackPreviewTabQueryParamsSchema),
|
||||||
|
search: {
|
||||||
|
middlewares: [stripSearchParams({ secretPath: "/" })]
|
||||||
|
},
|
||||||
|
beforeLoad: ({ context, params, search }) => {
|
||||||
|
const secretPathSegments = search.secretPath.split("/").filter(Boolean);
|
||||||
|
|
||||||
|
return {
|
||||||
|
breadcrumbs: [
|
||||||
|
...context.breadcrumbs,
|
||||||
|
{
|
||||||
|
type: BreadcrumbTypes.Dropdown,
|
||||||
|
label:
|
||||||
|
context.project.environments.find((el) => el.slug === params.environment)?.name || "",
|
||||||
|
dropdownTitle: "Environments",
|
||||||
|
links: context.project.environments.map((el) => ({
|
||||||
|
label: el.name,
|
||||||
|
link: linkOptions({
|
||||||
|
to: "/secret-manager/$projectId/secrets/$envSlug",
|
||||||
|
params: {
|
||||||
|
projectId: params.projectId,
|
||||||
|
envSlug: el.slug
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
...secretPathSegments.map((_, index) => ({
|
||||||
|
type: BreadcrumbTypes.Component,
|
||||||
|
component: () => (
|
||||||
|
<SecretDashboardPathBreadcrumb
|
||||||
|
secretPathSegments={secretPathSegments}
|
||||||
|
selectedPathSegmentIndex={index}
|
||||||
|
environmentSlug={params.environment}
|
||||||
|
projectId={params.projectId}
|
||||||
|
disableCopy
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
label: "Commits",
|
||||||
|
link: linkOptions({
|
||||||
|
to: "/secret-manager/$projectId/commits/$environment/$folderId",
|
||||||
|
params: {
|
||||||
|
projectId: params.projectId,
|
||||||
|
environment: params.environment,
|
||||||
|
folderId: params.folderId
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
secretPath: search.secretPath
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: params.commitId,
|
||||||
|
link: linkOptions({
|
||||||
|
to: "/secret-manager/$projectId/commits/$environment/$folderId/$commitId",
|
||||||
|
params: {
|
||||||
|
projectId: params.projectId,
|
||||||
|
environment: params.environment,
|
||||||
|
folderId: params.folderId,
|
||||||
|
commitId: params.commitId
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
secretPath: search.secretPath
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Restore"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
+652
@@ -0,0 +1,652 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
export interface Version {
|
||||||
|
id?: string;
|
||||||
|
version: number;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
||||||
|
type JsonObject = { [key: string]: JsonValue };
|
||||||
|
type JsonArray = JsonValue[];
|
||||||
|
|
||||||
|
export interface DiffViewItem {
|
||||||
|
type: "secret" | "folder";
|
||||||
|
isAdded?: boolean;
|
||||||
|
isDeleted?: boolean;
|
||||||
|
isUpdated?: boolean;
|
||||||
|
versions?: Version[];
|
||||||
|
isRollback?: boolean;
|
||||||
|
id: string;
|
||||||
|
secretKey?: string;
|
||||||
|
folderName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SecretVersionDiffViewProps {
|
||||||
|
item: DiffViewItem;
|
||||||
|
isCollapsed?: boolean;
|
||||||
|
onToggleCollapse?: (id: string) => void;
|
||||||
|
showHeader?: boolean;
|
||||||
|
customHeader?: JSX.Element;
|
||||||
|
excludedFieldsHighlight?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const isObject = (obj: JsonValue): obj is JsonObject => {
|
||||||
|
return obj !== null && typeof obj === "object" && !Array.isArray(obj);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isArray = (obj: JsonValue): obj is JsonArray => {
|
||||||
|
return Array.isArray(obj);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deepEqual = (a: JsonValue, b: JsonValue): boolean => {
|
||||||
|
if (a === b) return true;
|
||||||
|
if (a == null || b == null) return false;
|
||||||
|
if (typeof a !== typeof b) return false;
|
||||||
|
|
||||||
|
if (isArray(a) && isArray(b)) {
|
||||||
|
if (a.length !== b.length) return false;
|
||||||
|
return a.every((item: JsonValue, index: number) => deepEqual(item, b[index]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isObject(a) && isObject(b)) {
|
||||||
|
const keysA = Object.keys(a);
|
||||||
|
const keysB = Object.keys(b);
|
||||||
|
if (keysA.length !== keysB.length) return false;
|
||||||
|
return keysA.every((key) => keysB.includes(key) && deepEqual(a[key], b[key]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDiffPaths = (oldObj: JsonValue, newObj: JsonValue, path: string = ""): Set<string> => {
|
||||||
|
const diffPaths = new Set<string>();
|
||||||
|
|
||||||
|
if (oldObj === newObj) return diffPaths;
|
||||||
|
|
||||||
|
if (oldObj == null || newObj == null) {
|
||||||
|
diffPaths.add(path || "root");
|
||||||
|
return diffPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof oldObj !== typeof newObj) {
|
||||||
|
diffPaths.add(path || "root");
|
||||||
|
return diffPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArray(oldObj) && isArray(newObj)) {
|
||||||
|
return diffPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isObject(oldObj) && isObject(newObj)) {
|
||||||
|
const allKeys = new Set([...Object.keys(oldObj), ...Object.keys(newObj)]);
|
||||||
|
|
||||||
|
allKeys.forEach((key) => {
|
||||||
|
const currentPath = path ? `${path}.${key}` : key;
|
||||||
|
|
||||||
|
if (path.includes("[")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(key in oldObj) || !(key in newObj) || !deepEqual(oldObj[key], newObj[key])) {
|
||||||
|
diffPaths.add(currentPath);
|
||||||
|
|
||||||
|
if (
|
||||||
|
key in oldObj &&
|
||||||
|
key in newObj &&
|
||||||
|
(isObject(oldObj[key]) || isArray(oldObj[key])) &&
|
||||||
|
(isObject(newObj[key]) || isArray(newObj[key]))
|
||||||
|
) {
|
||||||
|
const nestedDiffs = getDiffPaths(oldObj[key], newObj[key], currentPath);
|
||||||
|
nestedDiffs.forEach((p) => diffPaths.add(p));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return diffPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldObj !== newObj) {
|
||||||
|
diffPaths.add(path || "root");
|
||||||
|
}
|
||||||
|
|
||||||
|
return diffPaths;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNestedValue = (obj: JsonValue, path: string): JsonValue => {
|
||||||
|
if (!path) return obj;
|
||||||
|
|
||||||
|
const parts = path.split(/[.[\]]+/).filter(Boolean);
|
||||||
|
let current: JsonValue = obj;
|
||||||
|
|
||||||
|
parts.forEach((part) => {
|
||||||
|
if (current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isObject(current)) {
|
||||||
|
current = current[part];
|
||||||
|
} else if (isArray(current)) {
|
||||||
|
const index = parseInt(part, 10);
|
||||||
|
if (!Number.isNaN(index)) {
|
||||||
|
current = current[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return current;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPathDifferent = (jsonPath: string, diffPaths: Set<string>): boolean => {
|
||||||
|
if (diffPaths.has(jsonPath)) return true;
|
||||||
|
|
||||||
|
const diffPathsArray = Array.from(diffPaths);
|
||||||
|
return diffPathsArray.some((diffPath) => {
|
||||||
|
return (
|
||||||
|
jsonPath.startsWith(`${diffPath}.`) ||
|
||||||
|
jsonPath.startsWith(`${diffPath}[`) ||
|
||||||
|
diffPath.startsWith(`${jsonPath}.`) ||
|
||||||
|
diffPath.startsWith(`${jsonPath}[`)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isContainerActuallyChanged = (
|
||||||
|
path: string,
|
||||||
|
diffPaths: Set<string>,
|
||||||
|
oldObj: JsonValue,
|
||||||
|
newObj: JsonValue
|
||||||
|
): boolean => {
|
||||||
|
if (diffPaths.has(path)) {
|
||||||
|
if (oldObj == null || newObj == null) return true;
|
||||||
|
if (typeof oldObj !== typeof newObj) return true;
|
||||||
|
if (isArray(oldObj) !== isArray(newObj)) return true;
|
||||||
|
if (isObject(oldObj) !== isObject(newObj)) return true;
|
||||||
|
|
||||||
|
if (!isObject(oldObj) && !isArray(oldObj)) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArray(oldObj) && isArray(newObj)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isObject(oldObj) && isObject(newObj)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldObj !== newObj;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderJsonWithDiffs = (
|
||||||
|
obj: JsonValue,
|
||||||
|
diffPaths: Set<string>,
|
||||||
|
isOldVersion: boolean,
|
||||||
|
path: string = "",
|
||||||
|
indentLevel: number = 0,
|
||||||
|
keyName?: string,
|
||||||
|
isLastItem: boolean = false,
|
||||||
|
excludedFieldsHighlight: string[] = [],
|
||||||
|
oldVersionObj?: JsonValue,
|
||||||
|
newVersionObj?: JsonValue
|
||||||
|
): JSX.Element => {
|
||||||
|
const indent = " ".repeat(indentLevel);
|
||||||
|
|
||||||
|
let isDifferent = false;
|
||||||
|
|
||||||
|
if (path.includes("[") && oldVersionObj && newVersionObj) {
|
||||||
|
const arrayMatch = path.match(/^([^[]+)\[(\d+)\]/);
|
||||||
|
if (arrayMatch) {
|
||||||
|
const arrayPath = arrayMatch[1];
|
||||||
|
const itemIndex = parseInt(arrayMatch[2], 10);
|
||||||
|
|
||||||
|
const oldArray = getNestedValue(oldVersionObj, arrayPath);
|
||||||
|
const newArray = getNestedValue(newVersionObj, arrayPath);
|
||||||
|
|
||||||
|
if (isArray(oldArray) && isArray(newArray)) {
|
||||||
|
const currentItem = isOldVersion ? oldArray[itemIndex] : newArray[itemIndex];
|
||||||
|
|
||||||
|
if (isOldVersion) {
|
||||||
|
isDifferent = !newArray.some((newItem: JsonValue) => deepEqual(currentItem, newItem));
|
||||||
|
} else {
|
||||||
|
isDifferent = !oldArray.some((oldItem: JsonValue) => deepEqual(currentItem, oldItem));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isDifferent = isPathDifferent(path, diffPaths);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isDifferent = isPathDifferent(path, diffPaths);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isDifferent = isPathDifferent(path, diffPaths);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLineClass = (different: boolean) => {
|
||||||
|
if (!different) return "flex";
|
||||||
|
return isOldVersion ? "flex bg-red-950 text-red-300" : "flex bg-green-950 text-green-300";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHighlightClass = (different: boolean) => {
|
||||||
|
if (!different) return "";
|
||||||
|
return isOldVersion ? "bg-red-900 rounded px-1" : "bg-green-900 rounded px-1";
|
||||||
|
};
|
||||||
|
|
||||||
|
const prefix = isDifferent ? (isOldVersion ? "-" : "+") : " ";
|
||||||
|
const keyDisplay = keyName ? `"${keyName}": ` : "";
|
||||||
|
const comma = !isLastItem ? "," : "";
|
||||||
|
|
||||||
|
const reactKey = `${path || "root"}-${keyName || "value"}-${indentLevel}-${typeof obj}`;
|
||||||
|
|
||||||
|
if (
|
||||||
|
obj === null ||
|
||||||
|
typeof obj === "string" ||
|
||||||
|
typeof obj === "number" ||
|
||||||
|
typeof obj === "boolean"
|
||||||
|
) {
|
||||||
|
let valueDisplay = "";
|
||||||
|
if (obj === null) valueDisplay = "null";
|
||||||
|
else if (typeof obj === "string") valueDisplay = `"${obj}"`;
|
||||||
|
else valueDisplay = String(obj);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={reactKey} className={getLineClass(isDifferent)}>
|
||||||
|
<div className="w-4 flex-shrink-0">{prefix}</div>
|
||||||
|
<div>
|
||||||
|
{indent}
|
||||||
|
{keyName && <span className={getHighlightClass(isDifferent)}>{keyDisplay}</span>}
|
||||||
|
<span className={getHighlightClass(isDifferent)}>{valueDisplay}</span>
|
||||||
|
{comma}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArray(obj) && obj.length === 0) {
|
||||||
|
return (
|
||||||
|
<div key={reactKey} className={getLineClass(isDifferent)}>
|
||||||
|
<div className="w-4 flex-shrink-0">{prefix}</div>
|
||||||
|
<div>
|
||||||
|
{indent}
|
||||||
|
{keyName && <span className={getHighlightClass(isDifferent)}>{keyDisplay}</span>}
|
||||||
|
<span className={getHighlightClass(isDifferent)}>[]</span>
|
||||||
|
{comma}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isObject(obj) && Object.keys(obj).length === 0) {
|
||||||
|
return (
|
||||||
|
<div key={reactKey} className={getLineClass(isDifferent)}>
|
||||||
|
<div className="w-4 flex-shrink-0">{prefix}</div>
|
||||||
|
<div>
|
||||||
|
{indent}
|
||||||
|
{keyName && <span className={getHighlightClass(isDifferent)}>{keyDisplay}</span>}
|
||||||
|
<span className={getHighlightClass(isDifferent)}>{"{}"}</span>
|
||||||
|
{comma}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let isContainerAddedOrRemoved = false;
|
||||||
|
|
||||||
|
if (oldVersionObj && newVersionObj) {
|
||||||
|
const oldValue = getNestedValue(oldVersionObj, path);
|
||||||
|
const newValue = getNestedValue(newVersionObj, path);
|
||||||
|
|
||||||
|
if (oldValue == null || newValue == null) {
|
||||||
|
isContainerAddedOrRemoved = true;
|
||||||
|
} else if (typeof oldValue !== typeof newValue) {
|
||||||
|
isContainerAddedOrRemoved = true;
|
||||||
|
} else if (isArray(oldValue) !== isArray(newValue)) {
|
||||||
|
isContainerAddedOrRemoved = true;
|
||||||
|
} else if (isObject(oldValue) !== isObject(newValue)) {
|
||||||
|
isContainerAddedOrRemoved = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isContainerAddedOrRemoved = isContainerActuallyChanged(
|
||||||
|
path,
|
||||||
|
diffPaths,
|
||||||
|
isOldVersion ? obj : oldVersionObj || null,
|
||||||
|
isOldVersion ? newVersionObj || null : obj
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArray(obj)) {
|
||||||
|
return (
|
||||||
|
<div key={reactKey}>
|
||||||
|
<div className={getLineClass(isContainerAddedOrRemoved)}>
|
||||||
|
<div className="w-4 flex-shrink-0">
|
||||||
|
{isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{indent}
|
||||||
|
{keyName && (
|
||||||
|
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>
|
||||||
|
{keyDisplay}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>[</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{obj.map((item: JsonValue, index: number) => {
|
||||||
|
const itemPath = path ? `${path}[${index}]` : `[${index}]`;
|
||||||
|
const isLast = index === obj.length - 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={`${reactKey}-item-${index + 1}`}>
|
||||||
|
{renderJsonWithDiffs(
|
||||||
|
item,
|
||||||
|
diffPaths,
|
||||||
|
isOldVersion,
|
||||||
|
itemPath,
|
||||||
|
indentLevel + 1,
|
||||||
|
undefined,
|
||||||
|
isLast,
|
||||||
|
excludedFieldsHighlight,
|
||||||
|
oldVersionObj,
|
||||||
|
newVersionObj
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div className={getLineClass(isContainerAddedOrRemoved)}>
|
||||||
|
<div className="w-4 flex-shrink-0">
|
||||||
|
{isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{indent}
|
||||||
|
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>]</span>
|
||||||
|
{comma}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isObject(obj)) {
|
||||||
|
const keys = Object.keys(obj);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={reactKey}>
|
||||||
|
<div className={getLineClass(isContainerAddedOrRemoved)}>
|
||||||
|
<div className="w-4 flex-shrink-0">
|
||||||
|
{isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{indent}
|
||||||
|
{keyName && (
|
||||||
|
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>
|
||||||
|
{keyDisplay}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>{"{"}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{keys.map((key, index) => {
|
||||||
|
const keyPath = path ? `${path}.${key}` : key;
|
||||||
|
const isLast = index === keys.length - 1;
|
||||||
|
const propKey = `${reactKey}-prop-${key}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={propKey}>
|
||||||
|
{renderJsonWithDiffs(
|
||||||
|
obj[key],
|
||||||
|
diffPaths,
|
||||||
|
isOldVersion,
|
||||||
|
keyPath,
|
||||||
|
indentLevel + 1,
|
||||||
|
key,
|
||||||
|
isLast,
|
||||||
|
excludedFieldsHighlight,
|
||||||
|
oldVersionObj,
|
||||||
|
newVersionObj
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div className={getLineClass(isContainerAddedOrRemoved)}>
|
||||||
|
<div className="w-4 flex-shrink-0">
|
||||||
|
{isContainerAddedOrRemoved ? (isOldVersion ? "-" : "+") : " "}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{indent}
|
||||||
|
<span className={isContainerAddedOrRemoved ? getHighlightClass(true) : ""}>{"}"}</span>
|
||||||
|
{comma}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={reactKey} className={getLineClass(isDifferent)}>
|
||||||
|
<div className="w-4 flex-shrink-0">{prefix}</div>
|
||||||
|
<div>
|
||||||
|
{indent}
|
||||||
|
{keyDisplay}
|
||||||
|
{String(obj)}
|
||||||
|
{comma}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatAddedJson = (json: JsonValue): JSX.Element => {
|
||||||
|
const lines = JSON.stringify(json, null, 2).split("\n");
|
||||||
|
return (
|
||||||
|
<div className="font-mono text-sm text-green-300">
|
||||||
|
{lines.map((line, lineIndex) => {
|
||||||
|
const lineKey = `added-${line.slice(0, 30)}-${lineIndex}`;
|
||||||
|
return (
|
||||||
|
<div key={lineKey} className="flex">
|
||||||
|
<div className="w-4 flex-shrink-0">+</div>
|
||||||
|
<div>{line}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDeletedJson = (json: JsonValue): JSX.Element => {
|
||||||
|
const lines = JSON.stringify(json, null, 2).split("\n");
|
||||||
|
return (
|
||||||
|
<div className="font-mono text-sm text-red-300">
|
||||||
|
{lines.map((line, lineIndex) => {
|
||||||
|
const lineKey = `deleted-${line.slice(0, 30)}-${lineIndex}`;
|
||||||
|
return (
|
||||||
|
<div key={lineKey} className="flex">
|
||||||
|
<div className="w-4 flex-shrink-0">-</div>
|
||||||
|
<div>{line}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanVersionForComparison = (version: Version): JsonValue => {
|
||||||
|
const { id, version: versionNumber, ...cleanVersion } = version;
|
||||||
|
return cleanVersion;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretVersionDiffView = ({
|
||||||
|
item,
|
||||||
|
isCollapsed = false,
|
||||||
|
onToggleCollapse,
|
||||||
|
showHeader = true,
|
||||||
|
customHeader,
|
||||||
|
excludedFieldsHighlight = ["metadata", "tags"]
|
||||||
|
}: SecretVersionDiffViewProps) => {
|
||||||
|
const oldContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const newContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [internalCollapsed, setInternalCollapsed] = useState(isCollapsed);
|
||||||
|
|
||||||
|
const handleToggle = useCallback(() => {
|
||||||
|
if (onToggleCollapse && item.id) {
|
||||||
|
onToggleCollapse(item.id);
|
||||||
|
} else {
|
||||||
|
setInternalCollapsed((prev) => !prev);
|
||||||
|
}
|
||||||
|
}, [onToggleCollapse, item.id]);
|
||||||
|
|
||||||
|
const collapsed = onToggleCollapse ? isCollapsed : internalCollapsed;
|
||||||
|
|
||||||
|
if (!item.versions || item.versions.length === 0) {
|
||||||
|
return <div className="px-6 py-3 text-gray-400">No details available</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedVersions = [...item.versions].sort((a, b) => b.version - a.version);
|
||||||
|
let oldVersion = null;
|
||||||
|
let newVersion = null;
|
||||||
|
let oldVersionContent = null;
|
||||||
|
let newVersionContent = null;
|
||||||
|
let diffPaths = new Set<string>();
|
||||||
|
|
||||||
|
if (item.isUpdated && sortedVersions.length >= 2) {
|
||||||
|
if (item.isRollback) {
|
||||||
|
[oldVersion, newVersion] = sortedVersions;
|
||||||
|
} else {
|
||||||
|
[newVersion, oldVersion] = sortedVersions;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanOldVersion = cleanVersionForComparison(oldVersion);
|
||||||
|
const cleanNewVersion = cleanVersionForComparison(newVersion);
|
||||||
|
diffPaths = getDiffPaths(cleanOldVersion, cleanNewVersion);
|
||||||
|
|
||||||
|
if (diffPaths.size === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
oldVersionContent = (
|
||||||
|
<div className="font-mono text-sm">
|
||||||
|
{renderJsonWithDiffs(
|
||||||
|
cleanOldVersion,
|
||||||
|
diffPaths,
|
||||||
|
true,
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
excludedFieldsHighlight,
|
||||||
|
cleanOldVersion,
|
||||||
|
cleanNewVersion
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
newVersionContent = (
|
||||||
|
<div className="font-mono text-sm">
|
||||||
|
{renderJsonWithDiffs(
|
||||||
|
cleanNewVersion,
|
||||||
|
diffPaths,
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
excludedFieldsHighlight,
|
||||||
|
cleanOldVersion,
|
||||||
|
cleanNewVersion
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (item.isAdded) {
|
||||||
|
[newVersion] = sortedVersions;
|
||||||
|
const cleanNewVersion = cleanVersionForComparison(newVersion);
|
||||||
|
newVersionContent = formatAddedJson(cleanNewVersion);
|
||||||
|
} else if (item.isDeleted) {
|
||||||
|
[oldVersion] = sortedVersions;
|
||||||
|
const cleanOldVersion = cleanVersionForComparison(oldVersion);
|
||||||
|
oldVersionContent = formatDeletedJson(cleanOldVersion);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderHeader = () => {
|
||||||
|
if (customHeader) {
|
||||||
|
return customHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSecret = item.type === "secret";
|
||||||
|
const key = isSecret ? item.secretKey || "Unnamed Secret" : item.folderName || "Unnamed Folder";
|
||||||
|
let textStyle = "text-white";
|
||||||
|
let changeBadge = null;
|
||||||
|
|
||||||
|
if (item.isDeleted) {
|
||||||
|
textStyle = "line-through text-red-300";
|
||||||
|
changeBadge = (
|
||||||
|
<span className="ml-2 rounded-md bg-mineshaft-600 px-2 py-0.5 text-xs font-medium">
|
||||||
|
{isSecret ? "Secret" : "Folder"} Deleted
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else if (item.isAdded) {
|
||||||
|
changeBadge = (
|
||||||
|
<span className="ml-2 rounded-md bg-mineshaft-600 px-2 py-0.5 text-xs font-medium">
|
||||||
|
{isSecret ? "Secret" : "Folder"} Added
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else if (item.isUpdated) {
|
||||||
|
changeBadge = (
|
||||||
|
<span className="ml-2 rounded-md bg-mineshaft-600 px-2 py-0.5 text-xs font-medium">
|
||||||
|
{isSecret ? "Secret" : "Folder"} Updated
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex cursor-pointer items-center justify-between p-4 hover:bg-mineshaft-700"
|
||||||
|
onClick={handleToggle}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
handleToggle();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-expanded={!collapsed}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className={textStyle}>{key}</span>
|
||||||
|
{changeBadge}
|
||||||
|
</div>
|
||||||
|
<FontAwesomeIcon icon={collapsed ? faChevronDown : faChevronUp} className="text-gray-400" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden rounded-lg border border-mineshaft-600 bg-mineshaft-800">
|
||||||
|
{showHeader && renderHeader()}
|
||||||
|
|
||||||
|
{!collapsed && (
|
||||||
|
<div className="border-t border-mineshaft-700 bg-mineshaft-900 px-6 py-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div
|
||||||
|
ref={oldContainerRef}
|
||||||
|
className="thin-scrollbar max-h-96 overflow-auto whitespace-pre rounded border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||||
|
>
|
||||||
|
{oldVersionContent}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={newContainerRef}
|
||||||
|
className="thin-scrollbar max-h-96 overflow-auto whitespace-pre rounded border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||||
|
>
|
||||||
|
{newVersionContent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { SecretVersionDiffView } from "./SecretVersionDiffView";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { CommitDetailsPage } from "./CommitDetailsPage";
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretDashboardPathBreadcrumb } from "@app/components/navigation/SecretDashboardPathBreadcrumb";
|
||||||
|
import { BreadcrumbTypes } from "@app/components/v2";
|
||||||
|
|
||||||
|
import { CommitDetailsPage } from "./CommitDetailsPage";
|
||||||
|
|
||||||
|
const CommitDetailsPageQueryParamsSchema = z.object({
|
||||||
|
secretPath: z.string().catch("/")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
"/_authenticate/_inject-org-details/_org-layout/secret-manager/$projectId/_secret-manager-layout/commits/$environment/$folderId/$commitId/"
|
||||||
|
)({
|
||||||
|
component: CommitDetailsPage,
|
||||||
|
validateSearch: zodValidator(CommitDetailsPageQueryParamsSchema),
|
||||||
|
search: {
|
||||||
|
middlewares: [stripSearchParams({ secretPath: "/" })]
|
||||||
|
},
|
||||||
|
beforeLoad: ({ context, params, search }) => {
|
||||||
|
const secretPathSegments = search.secretPath.split("/").filter(Boolean);
|
||||||
|
|
||||||
|
return {
|
||||||
|
breadcrumbs: [
|
||||||
|
...context.breadcrumbs,
|
||||||
|
{
|
||||||
|
type: BreadcrumbTypes.Dropdown,
|
||||||
|
label:
|
||||||
|
context.project.environments.find((el) => el.slug === params.environment)?.name || "",
|
||||||
|
dropdownTitle: "Environments",
|
||||||
|
links: context.project.environments.map((el) => ({
|
||||||
|
label: el.name,
|
||||||
|
link: linkOptions({
|
||||||
|
to: "/secret-manager/$projectId/secrets/$envSlug",
|
||||||
|
params: {
|
||||||
|
projectId: params.projectId,
|
||||||
|
envSlug: el.slug
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
...secretPathSegments.map((_, index) => ({
|
||||||
|
type: BreadcrumbTypes.Component,
|
||||||
|
component: () => (
|
||||||
|
<SecretDashboardPathBreadcrumb
|
||||||
|
secretPathSegments={secretPathSegments}
|
||||||
|
selectedPathSegmentIndex={index}
|
||||||
|
environmentSlug={params.environment}
|
||||||
|
projectId={params.projectId}
|
||||||
|
disableCopy
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
label: "Commits",
|
||||||
|
link: linkOptions({
|
||||||
|
to: "/secret-manager/$projectId/commits/$environment/$folderId",
|
||||||
|
params: {
|
||||||
|
projectId: params.projectId,
|
||||||
|
environment: params.environment,
|
||||||
|
folderId: params.folderId
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
secretPath: search.secretPath
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: params.commitId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import { PageHeader } from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import {
|
||||||
|
ProjectPermissionCommitsActions,
|
||||||
|
ProjectPermissionSub
|
||||||
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
|
import { CommitHistoryTab } from "./components/CommitHistoryTab";
|
||||||
|
|
||||||
|
export const CommitsPage = () => {
|
||||||
|
const envSlug = useParams({
|
||||||
|
from: ROUTE_PATHS.SecretManager.CommitsPage.id,
|
||||||
|
select: (el) => el.environment
|
||||||
|
});
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const folderId = useParams({
|
||||||
|
from: ROUTE_PATHS.SecretManager.CommitsPage.id,
|
||||||
|
select: (el) => el.folderId
|
||||||
|
});
|
||||||
|
const routerQueryParams: { secretPath?: string } = useSearch({
|
||||||
|
from: ROUTE_PATHS.SecretManager.CommitsPage.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretPath = routerQueryParams?.secretPath || "/";
|
||||||
|
|
||||||
|
const handleSelectCommit = (commitId: string) => {
|
||||||
|
navigate({
|
||||||
|
to: `/${ProjectType.SecretManager}/$projectId/commits/$environment/$folderId/$commitId` as const,
|
||||||
|
params: {
|
||||||
|
projectId: currentWorkspace.id,
|
||||||
|
folderId,
|
||||||
|
environment: envSlug,
|
||||||
|
commitId
|
||||||
|
},
|
||||||
|
search: (query) => ({
|
||||||
|
...query,
|
||||||
|
secretPath
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto flex h-full w-full max-w-7xl justify-center bg-bunker-800 py-4 text-white">
|
||||||
|
<div className="w-full max-w-[75vw]">
|
||||||
|
<PageHeader
|
||||||
|
title="Commits"
|
||||||
|
description="Track, inspect, and restore your secrets and folders with confidence. View the complete history of changes made to your environment, examine specific modifications at each commit point, and preview the exact impact before rolling back to previous states."
|
||||||
|
/>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
renderGuardBanner
|
||||||
|
I={ProjectPermissionCommitsActions.Read}
|
||||||
|
a={ProjectPermissionSub.Commits}
|
||||||
|
>
|
||||||
|
<CommitHistoryTab
|
||||||
|
onSelectCommit={handleSelectCommit}
|
||||||
|
projectId={currentWorkspace.id}
|
||||||
|
environment={envSlug}
|
||||||
|
secretPath={secretPath}
|
||||||
|
/>
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user