mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 12:26:13 +00:00
Merge branch 'main' into ENG-2773
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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasConfigColumn = await knex.schema.hasColumn(TableName.DynamicSecretLease, "config");
|
||||||
|
if (!hasConfigColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.DynamicSecretLease, (table) => {
|
||||||
|
table.jsonb("config");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasConfigColumn = await knex.schema.hasColumn(TableName.DynamicSecretLease, "config");
|
||||||
|
if (hasConfigColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.DynamicSecretLease, (table) => {
|
||||||
|
table.dropColumn("config");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+45
@@ -0,0 +1,45 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
const BATCH_SIZE = 1000;
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasKubernetesHostColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "kubernetesHost");
|
||||||
|
|
||||||
|
if (hasKubernetesHostColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
|
||||||
|
table.string("kubernetesHost").nullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasKubernetesHostColumn = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "kubernetesHost");
|
||||||
|
|
||||||
|
// find all rows where kubernetesHost is null
|
||||||
|
const rows = await knex(TableName.IdentityKubernetesAuth)
|
||||||
|
.whereNull("kubernetesHost")
|
||||||
|
.select(selectAllTableCols(TableName.IdentityKubernetesAuth));
|
||||||
|
|
||||||
|
if (rows.length > 0) {
|
||||||
|
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
|
||||||
|
const batch = rows.slice(i, i + BATCH_SIZE);
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await knex(TableName.IdentityKubernetesAuth)
|
||||||
|
.whereIn(
|
||||||
|
"id",
|
||||||
|
batch.map((row) => row.id)
|
||||||
|
)
|
||||||
|
.update({ kubernetesHost: "" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasKubernetesHostColumn) {
|
||||||
|
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (table) => {
|
||||||
|
table.string("kubernetesHost").notNullable().alter();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
|
};
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export const DynamicSecretLeasesSchema = z.object({
|
|||||||
statusDetails: z.string().nullable().optional(),
|
statusDetails: z.string().nullable().optional(),
|
||||||
dynamicSecretId: z.string().uuid(),
|
dynamicSecretId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
config: z.unknown().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TDynamicSecretLeases = z.infer<typeof DynamicSecretLeasesSchema>;
|
export type TDynamicSecretLeases = z.infer<typeof DynamicSecretLeasesSchema>;
|
||||||
|
|||||||
@@ -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>>;
|
||||||
@@ -18,7 +18,7 @@ export const IdentityKubernetesAuthsSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
identityId: z.string().uuid(),
|
identityId: z.string().uuid(),
|
||||||
kubernetesHost: z.string(),
|
kubernetesHost: z.string().nullable().optional(),
|
||||||
encryptedCaCert: z.string().nullable().optional(),
|
encryptedCaCert: z.string().nullable().optional(),
|
||||||
caCertIV: z.string().nullable().optional(),
|
caCertIV: z.string().nullable().optional(),
|
||||||
caCertTag: z.string().nullable().optional(),
|
caCertTag: z.string().nullable().optional(),
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
|
|||||||
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
}),
|
}),
|
||||||
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
|
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
|
||||||
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.path)
|
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.environmentSlug),
|
||||||
|
config: z.any().optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
|
||||||
|
import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
|
||||||
|
import { daysToMillisecond } from "@app/lib/dates";
|
||||||
|
import { removeTrailingSlash } from "@app/lib/fn";
|
||||||
|
import { ms } from "@app/lib/ms";
|
||||||
|
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerKubernetesDynamicSecretLeaseRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.DynamicSecrets],
|
||||||
|
body: z.object({
|
||||||
|
dynamicSecretName: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.dynamicSecretName).toLowerCase(),
|
||||||
|
projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.projectSlug),
|
||||||
|
ttl: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe(DYNAMIC_SECRET_LEASES.CREATE.ttl)
|
||||||
|
.superRefine((val, ctx) => {
|
||||||
|
if (!val) return;
|
||||||
|
const valMs = ms(val);
|
||||||
|
if (valMs < 60 * 1000)
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be greater than 1min" });
|
||||||
|
if (valMs > daysToMillisecond(1))
|
||||||
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
|
||||||
|
}),
|
||||||
|
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
|
||||||
|
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.environmentSlug),
|
||||||
|
config: z
|
||||||
|
.object({
|
||||||
|
namespace: z.string().min(1).optional().describe(DYNAMIC_SECRET_LEASES.KUBERNETES.CREATE.config.namespace)
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
lease: DynamicSecretLeasesSchema,
|
||||||
|
dynamicSecret: SanitizedDynamicSecretSchema,
|
||||||
|
data: z.unknown()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { data, lease, dynamicSecret } = await server.services.dynamicSecretLease.create({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
name: req.body.dynamicSecretName,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
return { lease, data, dynamicSecret };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@ import { registerAssumePrivilegeRouter } from "./assume-privilege-router";
|
|||||||
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
import { registerAuditLogStreamRouter } from "./audit-log-stream-router";
|
||||||
import { registerCaCrlRouter } from "./certificate-authority-crl-router";
|
import { registerCaCrlRouter } from "./certificate-authority-crl-router";
|
||||||
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
|
||||||
|
import { registerKubernetesDynamicSecretLeaseRouter } from "./dynamic-secret-lease-routers/kubernetes-lease-router";
|
||||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||||
import { registerExternalKmsRouter } from "./external-kms-router";
|
import { registerExternalKmsRouter } from "./external-kms-router";
|
||||||
import { registerGatewayRouter } from "./gateway-router";
|
import { registerGatewayRouter } from "./gateway-router";
|
||||||
@@ -18,6 +19,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 +55,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"
|
||||||
@@ -69,6 +72,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
async (dynamicSecretRouter) => {
|
async (dynamicSecretRouter) => {
|
||||||
await dynamicSecretRouter.register(registerDynamicSecretRouter);
|
await dynamicSecretRouter.register(registerDynamicSecretRouter);
|
||||||
await dynamicSecretRouter.register(registerDynamicSecretLeaseRouter, { prefix: "/leases" });
|
await dynamicSecretRouter.register(registerDynamicSecretLeaseRouter, { prefix: "/leases" });
|
||||||
|
await dynamicSecretRouter.register(registerKubernetesDynamicSecretLeaseRouter, { prefix: "/leases/kubernetes" });
|
||||||
},
|
},
|
||||||
{ prefix: "/dynamic-secrets" }
|
{ prefix: "/dynamic-secrets" }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import {
|
|||||||
TSecretSyncRaw,
|
TSecretSyncRaw,
|
||||||
TUpdateSecretSyncDTO
|
TUpdateSecretSyncDTO
|
||||||
} from "@app/services/secret-sync/secret-sync-types";
|
} from "@app/services/secret-sync/secret-sync-types";
|
||||||
|
import { TWebhookPayloads } from "@app/services/webhook/webhook-types";
|
||||||
import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types";
|
import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types";
|
||||||
|
|
||||||
import { KmipPermission } from "../kmip/kmip-enum";
|
import { KmipPermission } from "../kmip/kmip-enum";
|
||||||
@@ -206,6 +207,7 @@ export enum EventType {
|
|||||||
CREATE_WEBHOOK = "create-webhook",
|
CREATE_WEBHOOK = "create-webhook",
|
||||||
UPDATE_WEBHOOK_STATUS = "update-webhook-status",
|
UPDATE_WEBHOOK_STATUS = "update-webhook-status",
|
||||||
DELETE_WEBHOOK = "delete-webhook",
|
DELETE_WEBHOOK = "delete-webhook",
|
||||||
|
WEBHOOK_TRIGGERED = "webhook-triggered",
|
||||||
GET_SECRET_IMPORTS = "get-secret-imports",
|
GET_SECRET_IMPORTS = "get-secret-imports",
|
||||||
GET_SECRET_IMPORT = "get-secret-import",
|
GET_SECRET_IMPORT = "get-secret-import",
|
||||||
CREATE_SECRET_IMPORT = "create-secret-import",
|
CREATE_SECRET_IMPORT = "create-secret-import",
|
||||||
@@ -393,6 +395,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",
|
||||||
@@ -1440,6 +1449,14 @@ interface DeleteWebhookEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WebhookTriggeredEvent {
|
||||||
|
type: EventType.WEBHOOK_TRIGGERED;
|
||||||
|
metadata: {
|
||||||
|
webhookId: string;
|
||||||
|
status: string;
|
||||||
|
} & TWebhookPayloads;
|
||||||
|
}
|
||||||
|
|
||||||
interface GetSecretImportsEvent {
|
interface GetSecretImportsEvent {
|
||||||
type: EventType.GET_SECRET_IMPORTS;
|
type: EventType.GET_SECRET_IMPORTS;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -2979,6 +2996,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: {
|
||||||
@@ -3221,6 +3310,7 @@ export type Event =
|
|||||||
| CreateWebhookEvent
|
| CreateWebhookEvent
|
||||||
| UpdateWebhookStatusEvent
|
| UpdateWebhookStatusEvent
|
||||||
| DeleteWebhookEvent
|
| DeleteWebhookEvent
|
||||||
|
| WebhookTriggeredEvent
|
||||||
| GetSecretImportsEvent
|
| GetSecretImportsEvent
|
||||||
| GetSecretImportEvent
|
| GetSecretImportEvent
|
||||||
| CreateSecretImportEvent
|
| CreateSecretImportEvent
|
||||||
@@ -3397,6 +3487,13 @@ export type Event =
|
|||||||
| MicrosoftTeamsWorkflowIntegrationGetEvent
|
| MicrosoftTeamsWorkflowIntegrationGetEvent
|
||||||
| MicrosoftTeamsWorkflowIntegrationListEvent
|
| MicrosoftTeamsWorkflowIntegrationListEvent
|
||||||
| MicrosoftTeamsWorkflowIntegrationUpdateEvent
|
| MicrosoftTeamsWorkflowIntegrationUpdateEvent
|
||||||
|
| GetProjectPitCommitsEvent
|
||||||
|
| GetProjectPitCommitChangesEvent
|
||||||
|
| PitRollbackCommitEvent
|
||||||
|
| GetProjectPitCommitCountEvent
|
||||||
|
| PitRevertCommitEvent
|
||||||
|
| PitCompareFolderStatesEvent
|
||||||
|
| PitGetFolderStateEvent
|
||||||
| SecretScanningDataSourceListEvent
|
| SecretScanningDataSourceListEvent
|
||||||
| SecretScanningDataSourceGetEvent
|
| SecretScanningDataSourceGetEvent
|
||||||
| SecretScanningDataSourceCreateEvent
|
| SecretScanningDataSourceCreateEvent
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
|
|||||||
import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
|
import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
|
||||||
import { DynamicSecretProviders, TDynamicProviderFns } from "../dynamic-secret/providers/models";
|
import { DynamicSecretProviders, TDynamicProviderFns } from "../dynamic-secret/providers/models";
|
||||||
import { TDynamicSecretLeaseDALFactory } from "./dynamic-secret-lease-dal";
|
import { TDynamicSecretLeaseDALFactory } from "./dynamic-secret-lease-dal";
|
||||||
|
import { TDynamicSecretLeaseConfig } from "./dynamic-secret-lease-types";
|
||||||
|
|
||||||
type TDynamicSecretLeaseQueueServiceFactoryDep = {
|
type TDynamicSecretLeaseQueueServiceFactoryDep = {
|
||||||
queueService: TQueueServiceFactory;
|
queueService: TQueueServiceFactory;
|
||||||
@@ -134,10 +135,15 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
|||||||
|
|
||||||
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
|
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
dynamicSecretLeases.map(({ externalEntityId }) =>
|
dynamicSecretLeases.map(({ externalEntityId, config }) =>
|
||||||
selectedProvider.revoke(decryptedStoredInput, externalEntityId, {
|
selectedProvider.revoke(
|
||||||
projectId: folder.projectId
|
decryptedStoredInput,
|
||||||
})
|
externalEntityId,
|
||||||
|
{
|
||||||
|
projectId: folder.projectId
|
||||||
|
},
|
||||||
|
config as TDynamicSecretLeaseConfig
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
TCreateDynamicSecretLeaseDTO,
|
TCreateDynamicSecretLeaseDTO,
|
||||||
TDeleteDynamicSecretLeaseDTO,
|
TDeleteDynamicSecretLeaseDTO,
|
||||||
TDetailsDynamicSecretLeaseDTO,
|
TDetailsDynamicSecretLeaseDTO,
|
||||||
|
TDynamicSecretLeaseConfig,
|
||||||
TListDynamicSecretLeasesDTO,
|
TListDynamicSecretLeasesDTO,
|
||||||
TRenewDynamicSecretLeaseDTO
|
TRenewDynamicSecretLeaseDTO
|
||||||
} from "./dynamic-secret-lease-types";
|
} from "./dynamic-secret-lease-types";
|
||||||
@@ -77,7 +78,8 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
ttl
|
ttl,
|
||||||
|
config
|
||||||
}: TCreateDynamicSecretLeaseDTO) => {
|
}: TCreateDynamicSecretLeaseDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
@@ -163,7 +165,8 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
expireAt: expireAt.getTime(),
|
expireAt: expireAt.getTime(),
|
||||||
usernameTemplate: dynamicSecretCfg.usernameTemplate,
|
usernameTemplate: dynamicSecretCfg.usernameTemplate,
|
||||||
identity,
|
identity,
|
||||||
metadata: { projectId }
|
metadata: { projectId },
|
||||||
|
config
|
||||||
});
|
});
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (error && typeof error === "object" && error !== null && "sqlMessage" in error) {
|
if (error && typeof error === "object" && error !== null && "sqlMessage" in error) {
|
||||||
@@ -177,8 +180,10 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
expireAt,
|
expireAt,
|
||||||
version: 1,
|
version: 1,
|
||||||
dynamicSecretId: dynamicSecretCfg.id,
|
dynamicSecretId: dynamicSecretCfg.id,
|
||||||
externalEntityId: entityId
|
externalEntityId: entityId,
|
||||||
|
config
|
||||||
});
|
});
|
||||||
|
|
||||||
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date()));
|
await dynamicSecretQueueService.setLeaseRevocation(dynamicSecretLease.id, Number(expireAt) - Number(new Date()));
|
||||||
return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data };
|
return { lease: dynamicSecretLease, dynamicSecret: dynamicSecretCfg, data };
|
||||||
};
|
};
|
||||||
@@ -342,7 +347,12 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
) as object;
|
) as object;
|
||||||
|
|
||||||
const revokeResponse = await selectedProvider
|
const revokeResponse = await selectedProvider
|
||||||
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId, { projectId })
|
.revoke(
|
||||||
|
decryptedStoredInput,
|
||||||
|
dynamicSecretLease.externalEntityId,
|
||||||
|
{ projectId },
|
||||||
|
dynamicSecretLease.config as TDynamicSecretLeaseConfig
|
||||||
|
)
|
||||||
.catch(async (err) => {
|
.catch(async (err) => {
|
||||||
// only propogate this error if forced is false
|
// only propogate this error if forced is false
|
||||||
if (!isForced) return { error: err as Error };
|
if (!isForced) return { error: err as Error };
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export type TCreateDynamicSecretLeaseDTO = {
|
|||||||
environmentSlug: string;
|
environmentSlug: string;
|
||||||
ttl?: string;
|
ttl?: string;
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
|
config?: TDynamicSecretLeaseConfig;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TDetailsDynamicSecretLeaseDTO = {
|
export type TDetailsDynamicSecretLeaseDTO = {
|
||||||
@@ -41,3 +42,9 @@ export type TRenewDynamicSecretLeaseDTO = {
|
|||||||
ttl?: string;
|
ttl?: string;
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDynamicSecretKubernetesLeaseConfig = {
|
||||||
|
namespace?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDynamicSecretLeaseConfig = TDynamicSecretKubernetesLeaseConfig;
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import axios from "axios";
|
import axios, { AxiosError } from "axios";
|
||||||
import handlebars from "handlebars";
|
import handlebars from "handlebars";
|
||||||
import https from "https";
|
import https from "https";
|
||||||
|
|
||||||
import { InternalServerError } from "@app/lib/errors";
|
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||||
import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||||
import { TKubernetesTokenRequest } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types";
|
import { TKubernetesTokenRequest } from "@app/services/identity-kubernetes-auth/identity-kubernetes-auth-types";
|
||||||
|
|
||||||
|
import { TDynamicSecretKubernetesLeaseConfig } from "../../dynamic-secret-lease/dynamic-secret-lease-types";
|
||||||
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
import { TGatewayServiceFactory } from "../../gateway/gateway-service";
|
||||||
import {
|
import {
|
||||||
DynamicSecretKubernetesSchema,
|
DynamicSecretKubernetesSchema,
|
||||||
@@ -19,6 +20,9 @@ import {
|
|||||||
|
|
||||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||||
|
|
||||||
|
// This value is just a placeholder. When using gateway auth method, the url is irrelevant.
|
||||||
|
const GATEWAY_AUTH_DEFAULT_URL = "https://kubernetes.default.svc.cluster.local";
|
||||||
|
|
||||||
type TKubernetesProviderDTO = {
|
type TKubernetesProviderDTO = {
|
||||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||||
};
|
};
|
||||||
@@ -36,7 +40,7 @@ const generateUsername = (usernameTemplate?: string | null) => {
|
|||||||
export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): TDynamicProviderFns => {
|
export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO): TDynamicProviderFns => {
|
||||||
const validateProviderInputs = async (inputs: unknown) => {
|
const validateProviderInputs = async (inputs: unknown) => {
|
||||||
const providerInputs = await DynamicSecretKubernetesSchema.parseAsync(inputs);
|
const providerInputs = await DynamicSecretKubernetesSchema.parseAsync(inputs);
|
||||||
if (!providerInputs.gatewayId) {
|
if (!providerInputs.gatewayId && providerInputs.url) {
|
||||||
await blockLocalAndPrivateIpAddresses(providerInputs.url);
|
await blockLocalAndPrivateIpAddresses(providerInputs.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,135 +107,173 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
const serviceAccountName = generateUsername();
|
const serviceAccountName = generateUsername();
|
||||||
const roleBindingName = `${serviceAccountName}-role-binding`;
|
const roleBindingName = `${serviceAccountName}-role-binding`;
|
||||||
|
|
||||||
// 1. Create a test service account
|
const namespaces = providerInputs.namespace.split(",").map((namespace) => namespace.trim());
|
||||||
await axios.post(
|
|
||||||
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts`,
|
|
||||||
{
|
|
||||||
metadata: {
|
|
||||||
name: serviceAccountName,
|
|
||||||
namespace: providerInputs.namespace
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
|
||||||
httpsAgent
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 2. Create a test role binding
|
// Test each namespace sequentially instead of in parallel to simplify cleanup
|
||||||
const roleBindingUrl =
|
for await (const namespace of namespaces) {
|
||||||
providerInputs.roleType === KubernetesRoleType.ClusterRole
|
try {
|
||||||
? `${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings`
|
// 1. Create a test service account
|
||||||
: `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings`;
|
await axios.post(
|
||||||
|
`${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts`,
|
||||||
const roleBindingMetadata = {
|
|
||||||
name: roleBindingName,
|
|
||||||
...(providerInputs.roleType !== KubernetesRoleType.ClusterRole && { namespace: providerInputs.namespace })
|
|
||||||
};
|
|
||||||
|
|
||||||
await axios.post(
|
|
||||||
roleBindingUrl,
|
|
||||||
{
|
|
||||||
metadata: roleBindingMetadata,
|
|
||||||
roleRef: {
|
|
||||||
kind: providerInputs.roleType === KubernetesRoleType.ClusterRole ? "ClusterRole" : "Role",
|
|
||||||
name: providerInputs.role,
|
|
||||||
apiGroup: "rbac.authorization.k8s.io"
|
|
||||||
},
|
|
||||||
subjects: [
|
|
||||||
{
|
{
|
||||||
kind: "ServiceAccount",
|
metadata: {
|
||||||
name: serviceAccountName,
|
name: serviceAccountName,
|
||||||
namespace: providerInputs.namespace
|
namespace
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
}
|
}
|
||||||
]
|
);
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
|
||||||
httpsAgent
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. Request a token for the test service account
|
// 2. Create a test role binding
|
||||||
await axios.post(
|
const roleBindingUrl =
|
||||||
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${serviceAccountName}/token`,
|
providerInputs.roleType === KubernetesRoleType.ClusterRole
|
||||||
{
|
? `${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings`
|
||||||
spec: {
|
: `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${namespace}/rolebindings`;
|
||||||
expirationSeconds: 600, // 10 minutes
|
|
||||||
...(providerInputs.audiences?.length ? { audiences: providerInputs.audiences } : {})
|
const roleBindingMetadata = {
|
||||||
|
name: roleBindingName,
|
||||||
|
...(providerInputs.roleType !== KubernetesRoleType.ClusterRole && { namespace })
|
||||||
|
};
|
||||||
|
|
||||||
|
await axios.post(
|
||||||
|
roleBindingUrl,
|
||||||
|
{
|
||||||
|
metadata: roleBindingMetadata,
|
||||||
|
roleRef: {
|
||||||
|
kind: providerInputs.roleType === KubernetesRoleType.ClusterRole ? "ClusterRole" : "Role",
|
||||||
|
name: providerInputs.role,
|
||||||
|
apiGroup: "rbac.authorization.k8s.io"
|
||||||
|
},
|
||||||
|
subjects: [
|
||||||
|
{
|
||||||
|
kind: "ServiceAccount",
|
||||||
|
name: serviceAccountName,
|
||||||
|
namespace
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Request a token for the test service account
|
||||||
|
await axios.post(
|
||||||
|
`${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts/${serviceAccountName}/token`,
|
||||||
|
{
|
||||||
|
spec: {
|
||||||
|
expirationSeconds: 600, // 10 minutes
|
||||||
|
...(providerInputs.audiences?.length ? { audiences: providerInputs.audiences } : {})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Cleanup: delete role binding and service account
|
||||||
|
if (providerInputs.roleType === KubernetesRoleType.Role) {
|
||||||
|
await axios.delete(
|
||||||
|
`${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${namespace}/rolebindings/${roleBindingName}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await axios.delete(`${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/${roleBindingName}`, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
|
||||||
httpsAgent
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 4. Cleanup: delete role binding and service account
|
await axios.delete(`${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts/${serviceAccountName}`, {
|
||||||
if (providerInputs.roleType === KubernetesRoleType.Role) {
|
|
||||||
await axios.delete(
|
|
||||||
`${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings/${roleBindingName}`,
|
|
||||||
{
|
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
},
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
httpsAgent
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const cleanupInfo = `You may need to manually clean up the following resources in namespace "${namespace}": Service Account - ${serviceAccountName}, ${providerInputs.roleType === KubernetesRoleType.Role ? "Role" : "Cluster Role"} Binding - ${roleBindingName}.`;
|
||||||
|
let mainErrorMessage = "Unknown error";
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
mainErrorMessage = (error.response?.data as { message: string })?.message;
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
mainErrorMessage = error.message;
|
||||||
}
|
}
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await axios.delete(`${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/${roleBindingName}`, {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
|
||||||
httpsAgent
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await axios.delete(
|
throw new Error(`${mainErrorMessage}. ${cleanupInfo}`);
|
||||||
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${serviceAccountName}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
|
||||||
},
|
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
|
||||||
httpsAgent
|
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const serviceAccountStaticCallback = async (host: string, port: number, httpsAgent?: https.Agent) => {
|
const serviceAccountStaticCallback = async (host: string, port: number, httpsAgent?: https.Agent) => {
|
||||||
@@ -247,17 +289,23 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
},
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
httpsAgent
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = new URL(providerInputs.url);
|
const rawUrl =
|
||||||
|
providerInputs.authMethod === KubernetesAuthMethod.Gateway ? GATEWAY_AUTH_DEFAULT_URL : providerInputs.url || "";
|
||||||
|
const url = new URL(rawUrl);
|
||||||
const k8sGatewayHost = url.hostname;
|
const k8sGatewayHost = url.hostname;
|
||||||
const k8sPort = url.port ? Number(url.port) : 443;
|
const k8sPort = url.port ? Number(url.port) : 443;
|
||||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||||
@@ -315,11 +363,13 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
const create = async ({
|
const create = async ({
|
||||||
inputs,
|
inputs,
|
||||||
expireAt,
|
expireAt,
|
||||||
usernameTemplate
|
usernameTemplate,
|
||||||
|
config
|
||||||
}: {
|
}: {
|
||||||
inputs: unknown;
|
inputs: unknown;
|
||||||
expireAt: number;
|
expireAt: number;
|
||||||
usernameTemplate?: string | null;
|
usernameTemplate?: string | null;
|
||||||
|
config?: TDynamicSecretKubernetesLeaseConfig;
|
||||||
}) => {
|
}) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
|
||||||
@@ -331,26 +381,44 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
const baseUrl = port ? `${host}:${port}` : host;
|
const baseUrl = port ? `${host}:${port}` : host;
|
||||||
const serviceAccountName = generateUsername(usernameTemplate);
|
const serviceAccountName = generateUsername(usernameTemplate);
|
||||||
const roleBindingName = `${serviceAccountName}-role-binding`;
|
const roleBindingName = `${serviceAccountName}-role-binding`;
|
||||||
|
const allowedNamespaces = providerInputs.namespace.split(",").map((namespace) => namespace.trim());
|
||||||
|
|
||||||
|
if (config?.namespace && !allowedNamespaces?.includes(config?.namespace)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Namespace ${config?.namespace} is not allowed. Allowed namespaces: ${allowedNamespaces?.join(", ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const namespace = config?.namespace || allowedNamespaces[0];
|
||||||
|
if (!namespace) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "No namespace provided"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Create the service account
|
// 1. Create the service account
|
||||||
await axios.post(
|
await axios.post(
|
||||||
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts`,
|
`${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts`,
|
||||||
{
|
{
|
||||||
metadata: {
|
metadata: {
|
||||||
name: serviceAccountName,
|
name: serviceAccountName,
|
||||||
namespace: providerInputs.namespace
|
namespace
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
},
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
httpsAgent
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -358,11 +426,11 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
const roleBindingUrl =
|
const roleBindingUrl =
|
||||||
providerInputs.roleType === KubernetesRoleType.ClusterRole
|
providerInputs.roleType === KubernetesRoleType.ClusterRole
|
||||||
? `${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings`
|
? `${baseUrl}/apis/rbac.authorization.k8s.io/v1/clusterrolebindings`
|
||||||
: `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings`;
|
: `${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${namespace}/rolebindings`;
|
||||||
|
|
||||||
const roleBindingMetadata = {
|
const roleBindingMetadata = {
|
||||||
name: roleBindingName,
|
name: roleBindingName,
|
||||||
...(providerInputs.roleType !== KubernetesRoleType.ClusterRole && { namespace: providerInputs.namespace })
|
...(providerInputs.roleType !== KubernetesRoleType.ClusterRole && { namespace })
|
||||||
};
|
};
|
||||||
|
|
||||||
await axios.post(
|
await axios.post(
|
||||||
@@ -378,7 +446,7 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
{
|
{
|
||||||
kind: "ServiceAccount",
|
kind: "ServiceAccount",
|
||||||
name: serviceAccountName,
|
name: serviceAccountName,
|
||||||
namespace: providerInputs.namespace
|
namespace
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -386,18 +454,22 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
},
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
httpsAgent
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 3. Request a token for the service account
|
// 3. Request a token for the service account
|
||||||
const res = await axios.post<TKubernetesTokenRequest>(
|
const res = await axios.post<TKubernetesTokenRequest>(
|
||||||
`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${serviceAccountName}/token`,
|
`${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts/${serviceAccountName}/token`,
|
||||||
{
|
{
|
||||||
spec: {
|
spec: {
|
||||||
expirationSeconds: Math.floor((expireAt - Date.now()) / 1000),
|
expirationSeconds: Math.floor((expireAt - Date.now()) / 1000),
|
||||||
@@ -408,12 +480,16 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
},
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
httpsAgent
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -425,6 +501,12 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
throw new Error("invalid callback");
|
throw new Error("invalid callback");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config?.namespace && config.namespace !== providerInputs.namespace) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Namespace ${config?.namespace} is not allowed. Allowed namespace: ${providerInputs.namespace}.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const baseUrl = port ? `${host}:${port}` : host;
|
const baseUrl = port ? `${host}:${port}` : host;
|
||||||
|
|
||||||
const res = await axios.post<TKubernetesTokenRequest>(
|
const res = await axios.post<TKubernetesTokenRequest>(
|
||||||
@@ -439,19 +521,25 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
},
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
httpsAgent
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return { ...res.data, serviceAccountName: providerInputs.serviceAccountName };
|
return { ...res.data, serviceAccountName: providerInputs.serviceAccountName };
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = new URL(providerInputs.url);
|
const rawUrl =
|
||||||
|
providerInputs.authMethod === KubernetesAuthMethod.Gateway ? GATEWAY_AUTH_DEFAULT_URL : providerInputs.url || "";
|
||||||
|
const url = new URL(rawUrl);
|
||||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||||
const k8sGatewayHost = url.hostname;
|
const k8sGatewayHost = url.hostname;
|
||||||
const k8sPort = url.port ? Number(url.port) : 443;
|
const k8sPort = url.port ? Number(url.port) : 443;
|
||||||
@@ -511,7 +599,13 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const revoke = async (inputs: unknown, entityId: string) => {
|
const revoke = async (
|
||||||
|
inputs: unknown,
|
||||||
|
entityId: string,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
_metadata: { projectId: string },
|
||||||
|
config?: TDynamicSecretKubernetesLeaseConfig
|
||||||
|
) => {
|
||||||
const providerInputs = await validateProviderInputs(inputs);
|
const providerInputs = await validateProviderInputs(inputs);
|
||||||
|
|
||||||
const serviceAccountDynamicCallback = async (host: string, port: number, httpsAgent?: https.Agent) => {
|
const serviceAccountDynamicCallback = async (host: string, port: number, httpsAgent?: https.Agent) => {
|
||||||
@@ -522,19 +616,25 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
const baseUrl = port ? `${host}:${port}` : host;
|
const baseUrl = port ? `${host}:${port}` : host;
|
||||||
const roleBindingName = `${entityId}-role-binding`;
|
const roleBindingName = `${entityId}-role-binding`;
|
||||||
|
|
||||||
|
const namespace = config?.namespace ?? providerInputs.namespace.split(",")[0].trim();
|
||||||
|
|
||||||
if (providerInputs.roleType === KubernetesRoleType.Role) {
|
if (providerInputs.roleType === KubernetesRoleType.Role) {
|
||||||
await axios.delete(
|
await axios.delete(
|
||||||
`${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${providerInputs.namespace}/rolebindings/${roleBindingName}`,
|
`${baseUrl}/apis/rbac.authorization.k8s.io/v1/namespaces/${namespace}/rolebindings/${roleBindingName}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
},
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
httpsAgent
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -542,31 +642,44 @@ export const KubernetesProvider = ({ gatewayService }: TKubernetesProviderDTO):
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
},
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
httpsAgent
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the service account
|
// Delete the service account
|
||||||
await axios.delete(`${baseUrl}/api/v1/namespaces/${providerInputs.namespace}/serviceaccounts/${entityId}`, {
|
await axios.delete(`${baseUrl}/api/v1/namespaces/${namespace}/serviceaccounts/${entityId}`, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
...(providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
? { "x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken }
|
? { "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount }
|
||||||
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
: { Authorization: `Bearer ${providerInputs.clusterToken}` })
|
||||||
},
|
},
|
||||||
|
...(providerInputs.authMethod === KubernetesAuthMethod.Api
|
||||||
|
? {
|
||||||
|
httpsAgent
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
signal: AbortSignal.timeout(EXTERNAL_REQUEST_TIMEOUT),
|
||||||
timeout: EXTERNAL_REQUEST_TIMEOUT,
|
timeout: EXTERNAL_REQUEST_TIMEOUT
|
||||||
httpsAgent
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (providerInputs.credentialType === KubernetesCredentialType.Dynamic) {
|
if (providerInputs.credentialType === KubernetesCredentialType.Dynamic) {
|
||||||
const url = new URL(providerInputs.url);
|
const rawUrl =
|
||||||
|
providerInputs.authMethod === KubernetesAuthMethod.Gateway
|
||||||
|
? GATEWAY_AUTH_DEFAULT_URL
|
||||||
|
: providerInputs.url || "";
|
||||||
|
|
||||||
|
const url = new URL(rawUrl);
|
||||||
const k8sGatewayHost = url.hostname;
|
const k8sGatewayHost = url.hostname;
|
||||||
const k8sPort = url.port ? Number(url.port) : 443;
|
const k8sPort = url.port ? Number(url.port) : 443;
|
||||||
const k8sHost = `${url.protocol}//${url.hostname}`;
|
const k8sHost = `${url.protocol}//${url.hostname}`;
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
|
import RE2 from "re2";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||||
|
|
||||||
|
import { TDynamicSecretLeaseConfig } from "../../dynamic-secret-lease/dynamic-secret-lease-types";
|
||||||
|
|
||||||
export type PasswordRequirements = {
|
export type PasswordRequirements = {
|
||||||
length: number;
|
length: number;
|
||||||
required: {
|
required: {
|
||||||
@@ -323,24 +328,54 @@ export const LdapSchema = z.union([
|
|||||||
export const DynamicSecretKubernetesSchema = z
|
export const DynamicSecretKubernetesSchema = z
|
||||||
.discriminatedUnion("credentialType", [
|
.discriminatedUnion("credentialType", [
|
||||||
z.object({
|
z.object({
|
||||||
url: z.string().url().trim().min(1),
|
url: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.refine((val: string | undefined) => !val || new RE2(/^https?:\/\/.+/).test(val), {
|
||||||
|
message: "Invalid URL. Must start with http:// or https:// (e.g. https://example.com)"
|
||||||
|
}),
|
||||||
clusterToken: z.string().trim().optional(),
|
clusterToken: z.string().trim().optional(),
|
||||||
ca: z.string().optional(),
|
ca: z.string().optional(),
|
||||||
sslEnabled: z.boolean().default(false),
|
sslEnabled: z.boolean().default(false),
|
||||||
credentialType: z.literal(KubernetesCredentialType.Static),
|
credentialType: z.literal(KubernetesCredentialType.Static),
|
||||||
serviceAccountName: z.string().trim().min(1),
|
serviceAccountName: z.string().trim().min(1),
|
||||||
namespace: z.string().trim().min(1),
|
namespace: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.refine((val) => !val.includes(","), "Namespace must be a single value, not a comma-separated list")
|
||||||
|
.refine(
|
||||||
|
(val) => characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen])(val),
|
||||||
|
"Invalid namespace format"
|
||||||
|
),
|
||||||
gatewayId: z.string().optional(),
|
gatewayId: z.string().optional(),
|
||||||
audiences: z.array(z.string().trim().min(1)),
|
audiences: z.array(z.string().trim().min(1)),
|
||||||
authMethod: z.nativeEnum(KubernetesAuthMethod).default(KubernetesAuthMethod.Api)
|
authMethod: z.nativeEnum(KubernetesAuthMethod).default(KubernetesAuthMethod.Api)
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
url: z.string().url().trim().min(1),
|
url: z
|
||||||
|
.string()
|
||||||
|
.url()
|
||||||
|
.optional()
|
||||||
|
.refine((val: string | undefined) => !val || new RE2(/^https?:\/\/.+/).test(val), {
|
||||||
|
message: "Invalid URL. Must start with http:// or https:// (e.g. https://example.com)"
|
||||||
|
}),
|
||||||
clusterToken: z.string().trim().optional(),
|
clusterToken: z.string().trim().optional(),
|
||||||
ca: z.string().optional(),
|
ca: z.string().optional(),
|
||||||
sslEnabled: z.boolean().default(false),
|
sslEnabled: z.boolean().default(false),
|
||||||
credentialType: z.literal(KubernetesCredentialType.Dynamic),
|
credentialType: z.literal(KubernetesCredentialType.Dynamic),
|
||||||
namespace: z.string().trim().min(1),
|
namespace: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.refine((val) => {
|
||||||
|
const namespaces = val.split(",").map((ns) => ns.trim());
|
||||||
|
return (
|
||||||
|
namespaces.length > 0 &&
|
||||||
|
namespaces.every((ns) => ns.length > 0) &&
|
||||||
|
namespaces.every((ns) => characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen])(ns))
|
||||||
|
);
|
||||||
|
}, "Must be a valid comma-separated list of namespace values"),
|
||||||
gatewayId: z.string().optional(),
|
gatewayId: z.string().optional(),
|
||||||
audiences: z.array(z.string().trim().min(1)),
|
audiences: z.array(z.string().trim().min(1)),
|
||||||
roleType: z.nativeEnum(KubernetesRoleType),
|
roleType: z.nativeEnum(KubernetesRoleType),
|
||||||
@@ -356,12 +391,21 @@ export const DynamicSecretKubernetesSchema = z
|
|||||||
message: "When auth method is set to Gateway, a gateway must be selected"
|
message: "When auth method is set to Gateway, a gateway must be selected"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if ((data.authMethod === KubernetesAuthMethod.Api || !data.authMethod) && !data.clusterToken) {
|
if (data.authMethod === KubernetesAuthMethod.Api || !data.authMethod) {
|
||||||
ctx.addIssue({
|
if (!data.clusterToken) {
|
||||||
path: ["clusterToken"],
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
path: ["clusterToken"],
|
||||||
message: "When auth method is set to Manual Token, a cluster token must be provided"
|
code: z.ZodIssueCode.custom,
|
||||||
});
|
message: "When auth method is set to Token, a cluster token must be provided"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!data.url) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ["url"],
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "When auth method is set to Token, a cluster URL must be provided"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -475,10 +519,16 @@ export type TDynamicProviderFns = {
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
metadata: { projectId: string };
|
metadata: { projectId: string };
|
||||||
|
config?: TDynamicSecretLeaseConfig;
|
||||||
}) => Promise<{ entityId: string; data: unknown }>;
|
}) => Promise<{ entityId: string; data: unknown }>;
|
||||||
validateConnection: (inputs: unknown, metadata: { projectId: string }) => Promise<boolean>;
|
validateConnection: (inputs: unknown, metadata: { projectId: string }) => Promise<boolean>;
|
||||||
validateProviderInputs: (inputs: object, metadata: { projectId: string }) => Promise<unknown>;
|
validateProviderInputs: (inputs: object, metadata: { projectId: string }) => Promise<unknown>;
|
||||||
revoke: (inputs: unknown, entityId: string, metadata: { projectId: string }) => Promise<{ entityId: string }>;
|
revoke: (
|
||||||
|
inputs: unknown,
|
||||||
|
entityId: string,
|
||||||
|
metadata: { projectId: string },
|
||||||
|
config?: TDynamicSecretLeaseConfig
|
||||||
|
) => Promise<{ entityId: string }>;
|
||||||
renew: (
|
renew: (
|
||||||
inputs: unknown,
|
inputs: unknown,
|
||||||
entityId: string,
|
entityId: string,
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
+42
-15
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -20,6 +21,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 +132,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 +164,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" });
|
||||||
@@ -243,7 +247,7 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
|
||||||
|
|
||||||
const { policy } = secretApprovalRequest;
|
const { policy } = secretApprovalRequest;
|
||||||
const { hasRole } = await permissionService.getProjectPermission({
|
const { hasRole, permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -259,6 +263,12 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
|
throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasSecretReadAccess = permission.can(
|
||||||
|
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||||
|
ProjectPermissionSub.Secrets
|
||||||
|
);
|
||||||
|
const hiddenSecretValue = "******";
|
||||||
|
|
||||||
let secrets;
|
let secrets;
|
||||||
if (shouldUseSecretV2Bridge) {
|
if (shouldUseSecretV2Bridge) {
|
||||||
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
@@ -275,9 +285,9 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
version: el.version,
|
version: el.version,
|
||||||
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
|
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
|
||||||
isRotatedSecret: el.secret?.isRotatedSecret ?? false,
|
isRotatedSecret: el.secret?.isRotatedSecret ?? false,
|
||||||
secretValue:
|
secretValue: !hasSecretReadAccess
|
||||||
// eslint-disable-next-line no-nested-ternary
|
? hiddenSecretValue
|
||||||
el.secret && el.secret.isRotatedSecret
|
: el.secret && el.secret.isRotatedSecret
|
||||||
? undefined
|
? undefined
|
||||||
: el.encryptedValue
|
: el.encryptedValue
|
||||||
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
|
||||||
@@ -290,9 +300,11 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretKey: el.secret.key,
|
secretKey: el.secret.key,
|
||||||
id: el.secret.id,
|
id: el.secret.id,
|
||||||
version: el.secret.version,
|
version: el.secret.version,
|
||||||
secretValue: el.secret.encryptedValue
|
secretValue: !hasSecretReadAccess
|
||||||
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
|
? hiddenSecretValue
|
||||||
: "",
|
: el.secret.encryptedValue
|
||||||
|
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
|
||||||
|
: "",
|
||||||
secretComment: el.secret.encryptedComment
|
secretComment: el.secret.encryptedComment
|
||||||
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedComment }).toString()
|
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedComment }).toString()
|
||||||
: ""
|
: ""
|
||||||
@@ -303,9 +315,11 @@ export const secretApprovalRequestServiceFactory = ({
|
|||||||
secretKey: el.secretVersion.key,
|
secretKey: el.secretVersion.key,
|
||||||
id: el.secretVersion.id,
|
id: el.secretVersion.id,
|
||||||
version: el.secretVersion.version,
|
version: el.secretVersion.version,
|
||||||
secretValue: el.secretVersion.encryptedValue
|
secretValue: !hasSecretReadAccess
|
||||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
|
? hiddenSecretValue
|
||||||
: "",
|
: el.secretVersion.encryptedValue
|
||||||
|
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
|
||||||
|
: "",
|
||||||
secretComment: el.secretVersion.encryptedComment
|
secretComment: el.secretVersion.encryptedComment
|
||||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
|
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString()
|
||||||
: "",
|
: "",
|
||||||
@@ -597,6 +611,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 +637,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 +682,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 +691,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,
|
||||||
|
|||||||
+46
@@ -101,10 +101,56 @@ export const azureClientSecretRotationFactory: TRotationFactory<
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a credential with the given keyId exists.
|
||||||
|
*/
|
||||||
|
const credentialExists = async (keyId: string): Promise<boolean> => {
|
||||||
|
const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService);
|
||||||
|
const endpoint = `${GRAPH_API_BASE}/applications/${objectId}/passwordCredentials`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await request.get<{ value: Array<{ keyId: string }> }>(endpoint, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.value?.some((credential) => credential.keyId === keyId) || false;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
let message;
|
||||||
|
if (
|
||||||
|
error.response?.data &&
|
||||||
|
typeof error.response.data === "object" &&
|
||||||
|
"error" in error.response.data &&
|
||||||
|
typeof (error.response.data as AzureErrorResponse).error.message === "string"
|
||||||
|
) {
|
||||||
|
message = (error.response.data as AzureErrorResponse).error.message;
|
||||||
|
}
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to check credential existence for app ${objectId}: ${
|
||||||
|
message || error.message || "Unknown error"
|
||||||
|
}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to validate connection: verify credentials"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Revokes a client secret from the Azure app using its keyId.
|
* Revokes a client secret from the Azure app using its keyId.
|
||||||
|
* First checks if the credential exists before attempting revocation.
|
||||||
*/
|
*/
|
||||||
const revokeCredential = async (keyId: string) => {
|
const revokeCredential = async (keyId: string) => {
|
||||||
|
// Check if credential exists before attempting revocation
|
||||||
|
const exists = await credentialExists(keyId);
|
||||||
|
if (!exists) {
|
||||||
|
return; // Credential doesn't exist, nothing to revoke
|
||||||
|
}
|
||||||
|
|
||||||
const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService);
|
const accessToken = await getAzureConnectionAccessToken(connection.id, appConnectionDAL, kmsService);
|
||||||
const endpoint = `${GRAPH_API_BASE}/applications/${objectId}/removePassword`;
|
const endpoint = `${GRAPH_API_BASE}/applications/${objectId}/removePassword`;
|
||||||
|
|
||||||
|
|||||||
@@ -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>;
|
||||||
@@ -146,6 +148,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) => {
|
||||||
@@ -539,7 +542,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(
|
||||||
@@ -675,7 +683,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;
|
||||||
@@ -793,6 +806,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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -936,6 +952,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."
|
||||||
@@ -1112,6 +1113,14 @@ export const DYNAMIC_SECRET_LEASES = {
|
|||||||
leaseId: "The ID of the dynamic secret lease.",
|
leaseId: "The ID of the dynamic secret lease.",
|
||||||
isForced:
|
isForced:
|
||||||
"A boolean flag to delete the the dynamic secret from Infisical without trying to remove it from external provider. Used when the dynamic secret got modified externally."
|
"A boolean flag to delete the the dynamic secret from Infisical without trying to remove it from external provider. Used when the dynamic secret got modified externally."
|
||||||
|
},
|
||||||
|
KUBERNETES: {
|
||||||
|
CREATE: {
|
||||||
|
config: {
|
||||||
|
namespace:
|
||||||
|
"The Kubernetes namespace to create the lease in. If not specified, the first namespace defined in the configuration will be used."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} as const;
|
} as const;
|
||||||
export const SECRET_TAGS = {
|
export const SECRET_TAGS = {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -149,8 +149,8 @@ const setupProxyServer = async ({
|
|||||||
protocol = GatewayProxyProtocol.Tcp,
|
protocol = GatewayProxyProtocol.Tcp,
|
||||||
httpsAgent
|
httpsAgent
|
||||||
}: {
|
}: {
|
||||||
targetHost: string;
|
targetHost?: string;
|
||||||
targetPort: number;
|
targetPort?: number;
|
||||||
relayPort: number;
|
relayPort: number;
|
||||||
relayHost: string;
|
relayHost: string;
|
||||||
tlsOptions: TGatewayTlsOptions;
|
tlsOptions: TGatewayTlsOptions;
|
||||||
@@ -183,27 +183,44 @@ const setupProxyServer = async ({
|
|||||||
let command: string;
|
let command: string;
|
||||||
|
|
||||||
if (protocol === GatewayProxyProtocol.Http) {
|
if (protocol === GatewayProxyProtocol.Http) {
|
||||||
const targetUrl = `${targetHost}:${targetPort}`; // note(daniel): targetHost MUST include the scheme (https|http)
|
if (!targetHost && !targetPort) {
|
||||||
command = `FORWARD-HTTP ${targetUrl}`;
|
command = `FORWARD-HTTP`;
|
||||||
logger.debug(`Using HTTP proxy mode: ${command.trim()}`);
|
logger.debug(`Using HTTP proxy mode, no target URL provided [command=${command.trim()}]`);
|
||||||
|
} else {
|
||||||
|
if (!targetHost || targetPort === undefined) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Target host and port are required for HTTP proxy mode with custom target`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// extract ca certificate from httpsAgent if present
|
const targetUrl = `${targetHost}:${targetPort}`; // note(daniel): targetHost MUST include the scheme (https|http)
|
||||||
if (httpsAgent && targetHost.startsWith("https://")) {
|
command = `FORWARD-HTTP ${targetUrl}`;
|
||||||
const agentOptions = httpsAgent.options;
|
logger.debug(`Using HTTP proxy mode, custom target URL provided [command=${command.trim()}]`);
|
||||||
if (agentOptions && agentOptions.ca) {
|
|
||||||
const caCert = Array.isArray(agentOptions.ca) ? agentOptions.ca.join("\n") : agentOptions.ca;
|
|
||||||
const caB64 = Buffer.from(caCert as string).toString("base64");
|
|
||||||
command += ` ca=${caB64}`;
|
|
||||||
|
|
||||||
const rejectUnauthorized = agentOptions.rejectUnauthorized !== false;
|
// extract ca certificate from httpsAgent if present
|
||||||
command += ` verify=${rejectUnauthorized}`;
|
if (httpsAgent && targetHost.startsWith("https://")) {
|
||||||
|
const agentOptions = httpsAgent.options;
|
||||||
|
if (agentOptions && agentOptions.ca) {
|
||||||
|
const caCert = Array.isArray(agentOptions.ca) ? agentOptions.ca.join("\n") : agentOptions.ca;
|
||||||
|
const caB64 = Buffer.from(caCert as string).toString("base64");
|
||||||
|
command += ` ca=${caB64}`;
|
||||||
|
|
||||||
logger.debug(`Using HTTP proxy mode [command=${command.trim()}]`);
|
const rejectUnauthorized = agentOptions.rejectUnauthorized !== false;
|
||||||
|
command += ` verify=${rejectUnauthorized}`;
|
||||||
|
|
||||||
|
logger.debug(`Using HTTP proxy mode, custom target URL provided [command=${command.trim()}]`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
command += "\n";
|
command += "\n";
|
||||||
} else if (protocol === GatewayProxyProtocol.Tcp) {
|
} else if (protocol === GatewayProxyProtocol.Tcp) {
|
||||||
|
if (!targetHost || !targetPort) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Target host and port are required for TCP proxy mode`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// For TCP mode, send FORWARD-TCP with host:port
|
// For TCP mode, send FORWARD-TCP with host:port
|
||||||
command = `FORWARD-TCP ${targetHost}:${targetPort}\n`;
|
command = `FORWARD-TCP ${targetHost}:${targetPort}\n`;
|
||||||
logger.debug(`Using TCP proxy mode: ${command.trim()}`);
|
logger.debug(`Using TCP proxy mode: ${command.trim()}`);
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ export enum GatewayProxyProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum GatewayHttpProxyActions {
|
export enum GatewayHttpProxyActions {
|
||||||
InjectGatewayK8sServiceAccountToken = "inject-k8s-sa-auth-token"
|
InjectGatewayK8sServiceAccountToken = "inject-k8s-sa-auth-token",
|
||||||
|
UseGatewayK8sServiceAccount = "use-k8s-sa"
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IGatewayProxyOptions {
|
export interface IGatewayProxyOptions {
|
||||||
targetHost: string;
|
targetHost?: string;
|
||||||
targetPort: number;
|
targetPort?: number;
|
||||||
relayHost: string;
|
relayHost: string;
|
||||||
relayPort: number;
|
relayPort: number;
|
||||||
tlsOptions: TGatewayTlsOptions;
|
tlsOptions: TGatewayTlsOptions;
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -57,9 +57,12 @@ export const registerServeUI = async (
|
|||||||
reply.callNotFound();
|
reply.callNotFound();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// reference: https://github.com/fastify/fastify-static?tab=readme-ov-file#managing-cache-control-headers
|
|
||||||
// to avoid ui bundle skew on new deployment
|
// This should help avoid caching any chunks (temp fix)
|
||||||
return reply.sendFile("index.html", { maxAge: 0, immutable: false });
|
void reply.header("Cache-Control", "no-cache, no-store, must-revalidate, private, max-age=0");
|
||||||
|
void reply.header("Pragma", "no-cache");
|
||||||
|
void reply.header("Expires", "0");
|
||||||
|
return reply.sendFile("index.html");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -108,17 +108,21 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
|||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1)
|
.min(1)
|
||||||
|
.nullable()
|
||||||
.describe(KUBERNETES_AUTH.ATTACH.kubernetesHost)
|
.describe(KUBERNETES_AUTH.ATTACH.kubernetesHost)
|
||||||
.refine(
|
.refine(
|
||||||
(val) =>
|
(val) => {
|
||||||
characterValidator([
|
if (val === null) return true;
|
||||||
|
|
||||||
|
return characterValidator([
|
||||||
CharacterType.Alphabets,
|
CharacterType.Alphabets,
|
||||||
CharacterType.Numbers,
|
CharacterType.Numbers,
|
||||||
CharacterType.Colon,
|
CharacterType.Colon,
|
||||||
CharacterType.Period,
|
CharacterType.Period,
|
||||||
CharacterType.ForwardSlash,
|
CharacterType.ForwardSlash,
|
||||||
CharacterType.Hyphen
|
CharacterType.Hyphen
|
||||||
])(val),
|
])(val);
|
||||||
|
},
|
||||||
{
|
{
|
||||||
message:
|
message:
|
||||||
"Kubernetes host must only contain alphabets, numbers, colons, periods, hyphen, and forward slashes."
|
"Kubernetes host must only contain alphabets, numbers, colons, periods, hyphen, and forward slashes."
|
||||||
@@ -164,6 +168,13 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
|||||||
.describe(KUBERNETES_AUTH.ATTACH.accessTokenNumUsesLimit)
|
.describe(KUBERNETES_AUTH.ATTACH.accessTokenNumUsesLimit)
|
||||||
})
|
})
|
||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
|
if (data.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api && !data.kubernetesHost) {
|
||||||
|
ctx.addIssue({
|
||||||
|
path: ["kubernetesHost"],
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "When token review mode is set to API, a Kubernetes host must be provided"
|
||||||
|
});
|
||||||
|
}
|
||||||
if (data.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway && !data.gatewayId) {
|
if (data.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway && !data.gatewayId) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
path: ["gatewayId"],
|
path: ["gatewayId"],
|
||||||
@@ -171,6 +182,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
|||||||
message: "When token review mode is set to Gateway, a gateway must be selected"
|
message: "When token review mode is set to Gateway, a gateway must be selected"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.accessTokenTTL > data.accessTokenMaxTTL) {
|
if (data.accessTokenTTL > data.accessTokenMaxTTL) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
path: ["accessTokenTTL"],
|
path: ["accessTokenTTL"],
|
||||||
@@ -203,7 +215,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
|||||||
type: EventType.ADD_IDENTITY_KUBERNETES_AUTH,
|
type: EventType.ADD_IDENTITY_KUBERNETES_AUTH,
|
||||||
metadata: {
|
metadata: {
|
||||||
identityId: identityKubernetesAuth.identityId,
|
identityId: identityKubernetesAuth.identityId,
|
||||||
kubernetesHost: identityKubernetesAuth.kubernetesHost,
|
kubernetesHost: identityKubernetesAuth.kubernetesHost ?? "",
|
||||||
allowedNamespaces: identityKubernetesAuth.allowedNamespaces,
|
allowedNamespaces: identityKubernetesAuth.allowedNamespaces,
|
||||||
allowedNames: identityKubernetesAuth.allowedNames,
|
allowedNames: identityKubernetesAuth.allowedNames,
|
||||||
accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
|
accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
|
||||||
@@ -243,6 +255,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
|||||||
.string()
|
.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(1)
|
.min(1)
|
||||||
|
.nullable()
|
||||||
.optional()
|
.optional()
|
||||||
.describe(KUBERNETES_AUTH.UPDATE.kubernetesHost)
|
.describe(KUBERNETES_AUTH.UPDATE.kubernetesHost)
|
||||||
.refine(
|
.refine(
|
||||||
@@ -345,7 +358,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
|
|||||||
type: EventType.UPDATE_IDENTITY_KUBENETES_AUTH,
|
type: EventType.UPDATE_IDENTITY_KUBENETES_AUTH,
|
||||||
metadata: {
|
metadata: {
|
||||||
identityId: identityKubernetesAuth.identityId,
|
identityId: identityKubernetesAuth.identityId,
|
||||||
kubernetesHost: identityKubernetesAuth.kubernetesHost,
|
kubernetesHost: identityKubernetesAuth.kubernetesHost ?? "",
|
||||||
allowedNamespaces: identityKubernetesAuth.allowedNamespaces,
|
allowedNamespaces: identityKubernetesAuth.allowedNamespaces,
|
||||||
allowedNames: identityKubernetesAuth.allowedNames,
|
allowedNames: identityKubernetesAuth.allowedNames,
|
||||||
accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
|
accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
|
||||||
|
|||||||
@@ -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 !== null && 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 !== null && 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 !== null && 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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -72,8 +72,8 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
const $gatewayProxyWrapper = async <T>(
|
const $gatewayProxyWrapper = async <T>(
|
||||||
inputs: {
|
inputs: {
|
||||||
gatewayId: string;
|
gatewayId: string;
|
||||||
targetHost: string;
|
targetHost?: string;
|
||||||
targetPort: number;
|
targetPort?: number;
|
||||||
caCert?: string;
|
caCert?: string;
|
||||||
reviewTokenThroughGateway: boolean;
|
reviewTokenThroughGateway: boolean;
|
||||||
},
|
},
|
||||||
@@ -104,11 +104,15 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
cert: relayDetails.certificate,
|
cert: relayDetails.certificate,
|
||||||
key: relayDetails.privateKey.toString()
|
key: relayDetails.privateKey.toString()
|
||||||
},
|
},
|
||||||
// we always pass this, because its needed for both tcp and http protocol
|
// only needed for TCP protocol, because the gateway as reviewer will use the pod's CA cert for auth directly
|
||||||
httpsAgent: new https.Agent({
|
...(!inputs.reviewTokenThroughGateway
|
||||||
ca: inputs.caCert,
|
? {
|
||||||
rejectUnauthorized: Boolean(inputs.caCert)
|
httpsAgent: new https.Agent({
|
||||||
})
|
ca: inputs.caCert,
|
||||||
|
rejectUnauthorized: Boolean(inputs.caCert)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
: {})
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -142,8 +146,15 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString();
|
caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokenReviewCallbackRaw = async (host: string = identityKubernetesAuth.kubernetesHost, port?: number) => {
|
const tokenReviewCallbackRaw = async (host = identityKubernetesAuth.kubernetesHost, port?: number) => {
|
||||||
logger.info({ host, port }, "tokenReviewCallbackRaw: Processing kubernetes token review using raw API");
|
logger.info({ host, port }, "tokenReviewCallbackRaw: Processing kubernetes token review using raw API");
|
||||||
|
|
||||||
|
if (!host || !identityKubernetesAuth.kubernetesHost) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Kubernetes host is required when token review mode is set to API"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let tokenReviewerJwt = "";
|
let tokenReviewerJwt = "";
|
||||||
if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) {
|
if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) {
|
||||||
tokenReviewerJwt = decryptor({
|
tokenReviewerJwt = decryptor({
|
||||||
@@ -211,11 +222,7 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
return res.data;
|
return res.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const tokenReviewCallbackThroughGateway = async (
|
const tokenReviewCallbackThroughGateway = async (host: string, port?: number) => {
|
||||||
host: string = identityKubernetesAuth.kubernetesHost,
|
|
||||||
port?: number,
|
|
||||||
httpsAgent?: https.Agent
|
|
||||||
) => {
|
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
host,
|
host,
|
||||||
@@ -224,11 +231,9 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
"tokenReviewCallbackThroughGateway: Processing kubernetes token review using gateway"
|
"tokenReviewCallbackThroughGateway: Processing kubernetes token review using gateway"
|
||||||
);
|
);
|
||||||
|
|
||||||
const baseUrl = port ? `${host}:${port}` : host;
|
|
||||||
|
|
||||||
const res = await axios
|
const res = await axios
|
||||||
.post<TCreateTokenReviewResponse>(
|
.post<TCreateTokenReviewResponse>(
|
||||||
`${baseUrl}/apis/authentication.k8s.io/v1/tokenreviews`,
|
`${host}:${port}/apis/authentication.k8s.io/v1/tokenreviews`,
|
||||||
{
|
{
|
||||||
apiVersion: "authentication.k8s.io/v1",
|
apiVersion: "authentication.k8s.io/v1",
|
||||||
kind: "TokenReview",
|
kind: "TokenReview",
|
||||||
@@ -240,11 +245,10 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"x-infisical-action": GatewayHttpProxyActions.InjectGatewayK8sServiceAccountToken
|
"x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount
|
||||||
},
|
},
|
||||||
signal: AbortSignal.timeout(10000),
|
signal: AbortSignal.timeout(10000),
|
||||||
timeout: 10000,
|
timeout: 10000
|
||||||
...(httpsAgent ? { httpsAgent } : {})
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -273,29 +277,6 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
let data: TCreateTokenReviewResponse | undefined;
|
let data: TCreateTokenReviewResponse | undefined;
|
||||||
|
|
||||||
if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway) {
|
if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway) {
|
||||||
const { kubernetesHost } = identityKubernetesAuth;
|
|
||||||
|
|
||||||
let urlString = kubernetesHost;
|
|
||||||
if (!kubernetesHost.startsWith("http://") && !kubernetesHost.startsWith("https://")) {
|
|
||||||
urlString = `https://${kubernetesHost}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL(urlString);
|
|
||||||
let { port: k8sPort } = url;
|
|
||||||
const { protocol, hostname: k8sHost } = url;
|
|
||||||
|
|
||||||
const cleanedProtocol = new RE2(/[^a-zA-Z0-9]/g).replace(protocol, "").toLowerCase();
|
|
||||||
|
|
||||||
if (!["https", "http"].includes(cleanedProtocol)) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Invalid Kubernetes host URL, must start with http:// or https://"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!k8sPort) {
|
|
||||||
k8sPort = cleanedProtocol === "https" ? "443" : "80";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!identityKubernetesAuth.gatewayId) {
|
if (!identityKubernetesAuth.gatewayId) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Gateway ID is required when token review mode is set to Gateway"
|
message: "Gateway ID is required when token review mode is set to Gateway"
|
||||||
@@ -305,14 +286,17 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
data = await $gatewayProxyWrapper(
|
data = await $gatewayProxyWrapper(
|
||||||
{
|
{
|
||||||
gatewayId: identityKubernetesAuth.gatewayId,
|
gatewayId: identityKubernetesAuth.gatewayId,
|
||||||
targetHost: `${cleanedProtocol}://${k8sHost}`, // note(daniel): must include the protocol (https|http)
|
|
||||||
targetPort: k8sPort ? Number(k8sPort) : 443,
|
|
||||||
caCert,
|
|
||||||
reviewTokenThroughGateway: true
|
reviewTokenThroughGateway: true
|
||||||
},
|
},
|
||||||
tokenReviewCallbackThroughGateway
|
tokenReviewCallbackThroughGateway
|
||||||
);
|
);
|
||||||
} else if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api) {
|
} else if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api) {
|
||||||
|
if (!identityKubernetesAuth.kubernetesHost) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Kubernetes host is required when token review mode is set to API"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let { kubernetesHost } = identityKubernetesAuth;
|
let { kubernetesHost } = identityKubernetesAuth;
|
||||||
if (kubernetesHost.startsWith("https://") || kubernetesHost.startsWith("http://")) {
|
if (kubernetesHost.startsWith("https://") || kubernetesHost.startsWith("http://")) {
|
||||||
kubernetesHost = new RE2("^https?:\\/\\/").replace(kubernetesHost, "");
|
kubernetesHost = new RE2("^https?:\\/\\/").replace(kubernetesHost, "");
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export enum IdentityKubernetesAuthTokenReviewMode {
|
|||||||
|
|
||||||
export type TAttachKubernetesAuthDTO = {
|
export type TAttachKubernetesAuthDTO = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
kubernetesHost: string;
|
kubernetesHost: string | null;
|
||||||
caCert: string;
|
caCert: string;
|
||||||
tokenReviewerJwt?: string;
|
tokenReviewerJwt?: string;
|
||||||
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode;
|
tokenReviewMode: IdentityKubernetesAuthTokenReviewMode;
|
||||||
@@ -29,7 +29,7 @@ export type TAttachKubernetesAuthDTO = {
|
|||||||
|
|
||||||
export type TUpdateKubernetesAuthDTO = {
|
export type TUpdateKubernetesAuthDTO = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
kubernetesHost?: string;
|
kubernetesHost?: string | null;
|
||||||
caCert?: string;
|
caCert?: string;
|
||||||
tokenReviewerJwt?: string | null;
|
tokenReviewerJwt?: string | null;
|
||||||
tokenReviewMode?: IdentityKubernetesAuthTokenReviewMode;
|
tokenReviewMode?: IdentityKubernetesAuthTokenReviewMode;
|
||||||
|
|||||||
@@ -122,9 +122,9 @@ export const identityUaServiceFactory = ({
|
|||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
accessTokenTTL: identityUa.accessTokenPeriod,
|
accessTokenTTL: identityUa.accessTokenPeriod,
|
||||||
// Setting Max TTL to 2 × period ensures that clients can always renew their token
|
// We set a very large Max TTL for periodic tokens to ensure that clients (even outdated ones) can always renew their token
|
||||||
// at least once, and matches client logic that checks if renewing would exceed Max TTL.
|
// without them having to update their SDKs, CLIs, etc. This workaround sets it to 30 years to emulate "forever"
|
||||||
accessTokenMaxTTL: 2 * identityUa.accessTokenPeriod
|
accessTokenMaxTTL: 1000000000
|
||||||
};
|
};
|
||||||
|
|
||||||
const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
|
const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
|
||||||
|
|||||||
@@ -1211,8 +1211,8 @@ export const orgServiceFactory = ({
|
|||||||
subjectLine: "Infisical organization invitation",
|
subjectLine: "Infisical organization invitation",
|
||||||
recipients: [el.email],
|
recipients: [el.email],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
inviterFirstName: invitingUser.firstName,
|
inviterFirstName: invitingUser?.firstName,
|
||||||
inviterUsername: invitingUser.email,
|
inviterUsername: invitingUser?.email,
|
||||||
organizationName: org?.name,
|
organizationName: org?.name,
|
||||||
email: el.email,
|
email: el.email,
|
||||||
organizationId: org?.id.toString(),
|
organizationId: org?.id.toString(),
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1581,6 +1586,7 @@ export const secretQueueFactory = ({
|
|||||||
projectDAL,
|
projectDAL,
|
||||||
webhookDAL,
|
webhookDAL,
|
||||||
event: job.data,
|
event: job.data,
|
||||||
|
auditLogService,
|
||||||
secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString()
|
secretManagerDecryptor: (value) => secretManagerDecryptor({ cipherTextBlob: value }).toString()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
|
|||||||
|
|
||||||
interface OrganizationInvitationTemplateProps extends Omit<BaseEmailWrapperProps, "preview" | "title"> {
|
interface OrganizationInvitationTemplateProps extends Omit<BaseEmailWrapperProps, "preview" | "title"> {
|
||||||
metadata?: string;
|
metadata?: string;
|
||||||
inviterFirstName: string;
|
inviterFirstName?: string;
|
||||||
inviterUsername: string;
|
inviterUsername?: string;
|
||||||
organizationName: string;
|
organizationName: string;
|
||||||
email: string;
|
email: string;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
@@ -38,11 +38,19 @@ export const OrganizationInvitationTemplate = ({
|
|||||||
</Heading>
|
</Heading>
|
||||||
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50">
|
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50">
|
||||||
<Text className="text-black text-[14px] leading-[24px]">
|
<Text className="text-black text-[14px] leading-[24px]">
|
||||||
<strong>{inviterFirstName}</strong> (
|
{inviterFirstName && inviterUsername ? (
|
||||||
<Link href={`mailto:${inviterUsername}`} className="text-slate-700 no-underline">
|
<>
|
||||||
{inviterUsername}
|
<strong>{inviterFirstName}</strong> (
|
||||||
</Link>
|
<Link href={`mailto:${inviterUsername}`} className="text-slate-700 no-underline">
|
||||||
) has invited you to collaborate on <strong>{organizationName}</strong>.
|
{inviterUsername}
|
||||||
|
</Link>
|
||||||
|
) has invited you to collaborate on <strong>{organizationName}</strong>.
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
You have been invited to collaborate on <strong>{organizationName}</strong>.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</Section>
|
</Section>
|
||||||
<Section className="text-center mt-[28px]">
|
<Section className="text-center mt-[28px]">
|
||||||
|
|||||||
@@ -107,7 +107,8 @@ export const userServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
await userDAL.updateById(user.id, {
|
await userDAL.updateById(user.id, {
|
||||||
isEmailVerified: true
|
isEmailVerified: true,
|
||||||
|
username: usersByusername.length === 1 && user.email ? user.email.toLowerCase() : undefined
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import { AxiosError } from "axios";
|
|||||||
import picomatch from "picomatch";
|
import picomatch from "picomatch";
|
||||||
|
|
||||||
import { TWebhooks } from "@app/db/schemas";
|
import { TWebhooks } from "@app/db/schemas";
|
||||||
|
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||||
|
import { EventType, WebhookTriggeredEvent } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
import { NotFoundError } from "@app/lib/errors";
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
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";
|
||||||
@@ -163,6 +166,7 @@ export type TFnTriggerWebhookDTO = {
|
|||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
secretManagerDecryptor: (value: Buffer) => string;
|
secretManagerDecryptor: (value: Buffer) => string;
|
||||||
|
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||||
};
|
};
|
||||||
|
|
||||||
// this is reusable function
|
// this is reusable function
|
||||||
@@ -175,7 +179,8 @@ export const fnTriggerWebhook = async ({
|
|||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
event,
|
event,
|
||||||
secretManagerDecryptor,
|
secretManagerDecryptor,
|
||||||
projectDAL
|
projectDAL,
|
||||||
|
auditLogService
|
||||||
}: TFnTriggerWebhookDTO) => {
|
}: TFnTriggerWebhookDTO) => {
|
||||||
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
|
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
|
||||||
const toBeTriggeredHooks = webhooks.filter(
|
const toBeTriggeredHooks = webhooks.filter(
|
||||||
@@ -200,16 +205,43 @@ export const fnTriggerWebhook = async ({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const eventPayloads: WebhookTriggeredEvent["metadata"][] = [];
|
||||||
// filter hooks by status
|
// filter hooks by status
|
||||||
const successWebhooks = webhooksTriggered
|
const successWebhooks = webhooksTriggered
|
||||||
.filter(({ status }) => status === "fulfilled")
|
.filter(({ status }) => status === "fulfilled")
|
||||||
.map((_, i) => toBeTriggeredHooks[i].id);
|
.map((_, i) => {
|
||||||
|
eventPayloads.push({
|
||||||
|
webhookId: toBeTriggeredHooks[i].id,
|
||||||
|
type: event.type,
|
||||||
|
payload: {
|
||||||
|
type: toBeTriggeredHooks[i].type!,
|
||||||
|
...event.payload,
|
||||||
|
projectName
|
||||||
|
},
|
||||||
|
status: "success"
|
||||||
|
} as WebhookTriggeredEvent["metadata"]);
|
||||||
|
|
||||||
|
return toBeTriggeredHooks[i].id;
|
||||||
|
});
|
||||||
const failedWebhooks = webhooksTriggered
|
const failedWebhooks = webhooksTriggered
|
||||||
.filter(({ status }) => status === "rejected")
|
.filter(({ status }) => status === "rejected")
|
||||||
.map((data, i) => ({
|
.map((data, i) => {
|
||||||
id: toBeTriggeredHooks[i].id,
|
eventPayloads.push({
|
||||||
error: data.status === "rejected" ? (data.reason as AxiosError).message : ""
|
webhookId: toBeTriggeredHooks[i].id,
|
||||||
}));
|
type: event.type,
|
||||||
|
payload: {
|
||||||
|
type: toBeTriggeredHooks[i].type!,
|
||||||
|
...event.payload,
|
||||||
|
projectName
|
||||||
|
},
|
||||||
|
status: "failed"
|
||||||
|
} as WebhookTriggeredEvent["metadata"]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: toBeTriggeredHooks[i].id,
|
||||||
|
error: data.status === "rejected" ? (data.reason as AxiosError).message : ""
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
await webhookDAL.transaction(async (tx) => {
|
await webhookDAL.transaction(async (tx) => {
|
||||||
const env = await projectEnvDAL.findOne({ projectId, slug: environment }, tx);
|
const env = await projectEnvDAL.findOne({ projectId, slug: environment }, tx);
|
||||||
@@ -236,5 +268,21 @@ export const fnTriggerWebhook = async ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const eventPayload of eventPayloads) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await auditLogService.createAuditLog({
|
||||||
|
actor: {
|
||||||
|
type: ActorType.PLATFORM,
|
||||||
|
metadata: {}
|
||||||
|
},
|
||||||
|
projectId,
|
||||||
|
event: {
|
||||||
|
type: EventType.WEBHOOK_TRIGGERED,
|
||||||
|
metadata: eventPayload
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
logger.info({ environment, secretPath, projectId }, "Secret webhook job ended");
|
logger.info({ environment, secretPath, projectId }, "Secret webhook job ended");
|
||||||
};
|
};
|
||||||
|
|||||||
+2
-2
@@ -14,7 +14,7 @@ require (
|
|||||||
github.com/fatih/semgroup v1.2.0
|
github.com/fatih/semgroup v1.2.0
|
||||||
github.com/gitleaks/go-gitdiff v0.9.1
|
github.com/gitleaks/go-gitdiff v0.9.1
|
||||||
github.com/h2non/filetype v1.1.3
|
github.com/h2non/filetype v1.1.3
|
||||||
github.com/infisical/go-sdk v0.5.95
|
github.com/infisical/go-sdk v0.5.96
|
||||||
github.com/infisical/infisical-kmip v0.3.5
|
github.com/infisical/infisical-kmip v0.3.5
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
|
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
|
||||||
@@ -25,6 +25,7 @@ require (
|
|||||||
github.com/pion/logging v0.2.3
|
github.com/pion/logging v0.2.3
|
||||||
github.com/pion/turn/v4 v4.0.0
|
github.com/pion/turn/v4 v4.0.0
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
|
||||||
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a
|
github.com/posthog/posthog-go v0.0.0-20221221115252-24dfed35d71a
|
||||||
github.com/quic-go/quic-go v0.50.0
|
github.com/quic-go/quic-go v0.50.0
|
||||||
github.com/rs/cors v1.11.0
|
github.com/rs/cors v1.11.0
|
||||||
@@ -106,7 +107,6 @@ require (
|
|||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
|
|||||||
+2
-6
@@ -292,12 +292,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
|
|||||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/infisical/go-sdk v0.5.92 h1:PoCnVndrd6Dbkipuxl9fFiwlD5vCKsabtQo09mo8lUE=
|
github.com/infisical/go-sdk v0.5.96 h1:huky6bQ1Y3oRdPb5MO3Ru868qZaPHUxZ7kP7FPNRn48=
|
||||||
github.com/infisical/go-sdk v0.5.92/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
|
github.com/infisical/go-sdk v0.5.96/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
|
||||||
github.com/infisical/go-sdk v0.5.94 h1:wKBj+KpJEe+ZzOJ7koXQZDR0dLL9bt0Kqgf/1q+7tG4=
|
|
||||||
github.com/infisical/go-sdk v0.5.94/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
|
|
||||||
github.com/infisical/go-sdk v0.5.95 h1:so0YwPofbT7j6Ao8Xcxee/o3ia33meuEVDU2vWr9yfs=
|
|
||||||
github.com/infisical/go-sdk v0.5.95/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
|
|
||||||
github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE=
|
github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE=
|
||||||
github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs=
|
github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs=
|
||||||
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
|
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
|
||||||
|
|||||||
+57
-18
@@ -576,7 +576,7 @@ func (tm *AgentManager) FetchUniversalAuthAccessToken() (credential infisicalSdk
|
|||||||
}
|
}
|
||||||
|
|
||||||
tm.cachedUniversalAuthClientSecret = clientSecret
|
tm.cachedUniversalAuthClientSecret = clientSecret
|
||||||
if tm.removeUniversalAuthClientSecretOnRead {
|
if universalAuthConfig.RemoveClientSecretOnRead {
|
||||||
defer os.Remove(universalAuthConfig.ClientSecretPath)
|
defer os.Remove(universalAuthConfig.ClientSecretPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -718,7 +718,7 @@ func (tm *AgentManager) FetchNewAccessToken() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Refreshes the existing access token
|
// Refreshes the existing access token
|
||||||
func (tm *AgentManager) RefreshAccessToken() error {
|
func (tm *AgentManager) RefreshAccessToken(accessToken string) error {
|
||||||
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -728,7 +728,6 @@ func (tm *AgentManager) RefreshAccessToken() error {
|
|||||||
SetRetryMaxWaitTime(20 * time.Second).
|
SetRetryMaxWaitTime(20 * time.Second).
|
||||||
SetRetryWaitTime(5 * time.Second)
|
SetRetryWaitTime(5 * time.Second)
|
||||||
|
|
||||||
accessToken := tm.GetToken()
|
|
||||||
response, err := api.CallMachineIdentityRefreshAccessToken(httpClient, api.UniversalAuthRefreshRequest{AccessToken: accessToken})
|
response, err := api.CallMachineIdentityRefreshAccessToken(httpClient, api.UniversalAuthRefreshRequest{AccessToken: accessToken})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -752,18 +751,37 @@ func (tm *AgentManager) ManageTokenLifecycle() {
|
|||||||
accessTokenRefreshedTime = tm.accessTokenFetchedTime
|
accessTokenRefreshedTime = tm.accessTokenFetchedTime
|
||||||
}
|
}
|
||||||
|
|
||||||
nextAccessTokenExpiresInTime := accessTokenRefreshedTime.Add(tm.accessTokenTTL - (5 * time.Second))
|
// Calculate next expiry time at 2/3 of the TTL
|
||||||
|
nextAccessTokenExpiresInTime := accessTokenRefreshedTime.Add(tm.accessTokenTTL * 2 / 3)
|
||||||
|
|
||||||
if tm.accessTokenFetchedTime.IsZero() && tm.accessTokenRefreshedTime.IsZero() {
|
if tm.accessTokenFetchedTime.IsZero() && tm.accessTokenRefreshedTime.IsZero() {
|
||||||
// case: init login to get access token
|
// try to fetch token from sink files first
|
||||||
log.Info().Msg("attempting to authenticate...")
|
// if token is found, refresh the token right away and continue from there
|
||||||
err := tm.FetchNewAccessToken()
|
isSavedTokenValid := false
|
||||||
if err != nil {
|
token := tm.FetchTokenFromFiles()
|
||||||
log.Error().Msgf("unable to authenticate because %v. Will retry in 30 seconds", err)
|
if token != "" {
|
||||||
|
log.Info().Msg("found existing token in file, attempting to refresh...")
|
||||||
|
err := tm.RefreshAccessToken(token)
|
||||||
|
isSavedTokenValid = err == nil
|
||||||
|
if isSavedTokenValid {
|
||||||
|
log.Info().Msg("token refreshed successfully from saved file")
|
||||||
|
tm.accessTokenFetchedTime = time.Now()
|
||||||
|
} else {
|
||||||
|
log.Error().Msg("unable to refresh token from saved file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// wait a bit before trying again
|
if !isSavedTokenValid {
|
||||||
time.Sleep((30 * time.Second))
|
// case: init login to get access token
|
||||||
continue
|
log.Info().Msg("attempting to authenticate...")
|
||||||
|
err := tm.FetchNewAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msgf("unable to authenticate because %v. Will retry in 30 seconds", err)
|
||||||
|
|
||||||
|
// wait a bit before trying again
|
||||||
|
time.Sleep((30 * time.Second))
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if time.Now().After(accessTokenMaxTTLExpiresInTime) {
|
} else if time.Now().After(accessTokenMaxTTLExpiresInTime) {
|
||||||
// case: token has reached max ttl and we should re-authenticate entirely (cannot refresh)
|
// case: token has reached max ttl and we should re-authenticate entirely (cannot refresh)
|
||||||
@@ -779,7 +797,7 @@ func (tm *AgentManager) ManageTokenLifecycle() {
|
|||||||
} else {
|
} else {
|
||||||
// case: token ttl has expired, but the token is still within max ttl, so we can refresh
|
// case: token ttl has expired, but the token is still within max ttl, so we can refresh
|
||||||
log.Info().Msgf("attempting to refresh existing token...")
|
log.Info().Msgf("attempting to refresh existing token...")
|
||||||
err := tm.RefreshAccessToken()
|
err := tm.RefreshAccessToken(tm.GetToken())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msgf("unable to refresh token because %v. Will retry in 30 seconds", err)
|
log.Error().Msgf("unable to refresh token because %v. Will retry in 30 seconds", err)
|
||||||
|
|
||||||
@@ -800,15 +818,18 @@ func (tm *AgentManager) ManageTokenLifecycle() {
|
|||||||
accessTokenRefreshedTime = tm.accessTokenRefreshedTime
|
accessTokenRefreshedTime = tm.accessTokenRefreshedTime
|
||||||
}
|
}
|
||||||
|
|
||||||
nextAccessTokenExpiresInTime = accessTokenRefreshedTime.Add(tm.accessTokenTTL - (5 * time.Second))
|
// Recalculate next expiry time at 2/3 of the TTL
|
||||||
|
nextAccessTokenExpiresInTime = accessTokenRefreshedTime.Add(tm.accessTokenTTL * 2 / 3)
|
||||||
accessTokenMaxTTLExpiresInTime = tm.accessTokenFetchedTime.Add(tm.accessTokenMaxTTL - (5 * time.Second))
|
accessTokenMaxTTLExpiresInTime = tm.accessTokenFetchedTime.Add(tm.accessTokenMaxTTL - (5 * time.Second))
|
||||||
|
|
||||||
if nextAccessTokenExpiresInTime.After(accessTokenMaxTTLExpiresInTime) {
|
if nextAccessTokenExpiresInTime.After(accessTokenMaxTTLExpiresInTime) {
|
||||||
// case: Refreshed so close that the next refresh would occur beyond max ttl (this is because currently, token renew tries to add +access-token-ttl amount of time)
|
// case: Refreshed so close that the next refresh would occur beyond max ttl
|
||||||
// example: access token ttl is 11 sec and max ttl is 30 sec. So it will start with 11 seconds, then 22 seconds but the next time you call refresh it would try to extend it to 33 but max ttl only allows 30, so the token will be valid until 30 before we need to reauth
|
// Sleep until we're at 2/3 of the remaining time to max TTL
|
||||||
time.Sleep(tm.accessTokenTTL - nextAccessTokenExpiresInTime.Sub(accessTokenMaxTTLExpiresInTime))
|
remainingTime := accessTokenMaxTTLExpiresInTime.Sub(time.Now())
|
||||||
|
time.Sleep(remainingTime * 2 / 3)
|
||||||
} else {
|
} else {
|
||||||
time.Sleep(tm.accessTokenTTL - (5 * time.Second))
|
// Sleep until we're at 2/3 of the TTL
|
||||||
|
time.Sleep(tm.accessTokenTTL * 2 / 3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -830,6 +851,24 @@ func (tm *AgentManager) WriteTokenToFiles() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tm *AgentManager) FetchTokenFromFiles() string {
|
||||||
|
for _, sinkFile := range tm.filePaths {
|
||||||
|
if sinkFile.Type == "file" {
|
||||||
|
tokenBytes, err := ioutil.ReadFile(sinkFile.Config.Path)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Msgf("unable to read token from file '%s' because %v", sinkFile.Config.Path, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
token := string(tokenBytes)
|
||||||
|
if token != "" {
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (tm *AgentManager) WriteTemplateToFile(bytes *bytes.Buffer, template *Template) {
|
func (tm *AgentManager) WriteTemplateToFile(bytes *bytes.Buffer, template *Template) {
|
||||||
if err := WriteBytesToFile(bytes, template.DestinationPath); err != nil {
|
if err := WriteBytesToFile(bytes, template.DestinationPath); err != nil {
|
||||||
log.Error().Msgf("template engine: unable to write secrets to path because %s. Will try again on next cycle", err)
|
log.Error().Msgf("template engine: unable to write secrets to path because %s. Will try again on next cycle", err)
|
||||||
|
|||||||
@@ -49,6 +49,11 @@ func getDynamicSecretList(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
projectSlug, err := cmd.Flags().GetString("project-slug")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
secretsPath, err := cmd.Flags().GetString("path")
|
secretsPath, err := cmd.Flags().GetString("path")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to parse path flag")
|
util.HandleError(err, "Unable to parse path flag")
|
||||||
@@ -60,10 +65,10 @@ func getDynamicSecretList(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to get resty client with custom headers")
|
util.HandleError(err, "Unable to get resty client with custom headers")
|
||||||
}
|
}
|
||||||
|
|
||||||
if projectId == "" {
|
if projectId == "" && projectSlug == "" {
|
||||||
workspaceFile, err := util.GetWorkSpaceFromFile()
|
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --projectId flag")
|
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project, pass in project slug with --project-slug flag, or pass in project id with --projectId flag")
|
||||||
}
|
}
|
||||||
projectId = workspaceFile.WorkspaceId
|
projectId = workspaceFile.WorkspaceId
|
||||||
}
|
}
|
||||||
@@ -100,13 +105,16 @@ func getDynamicSecretList(cmd *cobra.Command, args []string) {
|
|||||||
})
|
})
|
||||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||||
|
|
||||||
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
|
if projectSlug == "" {
|
||||||
if err != nil {
|
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
|
||||||
util.HandleError(err, "To fetch project details")
|
if err != nil {
|
||||||
|
util.HandleError(err, "To fetch project details")
|
||||||
|
}
|
||||||
|
projectSlug = projectDetails.Slug
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamicSecretRootCredentials, err := infisicalClient.DynamicSecrets().List(infisicalSdk.ListDynamicSecretsRootCredentialsOptions{
|
dynamicSecretRootCredentials, err := infisicalClient.DynamicSecrets().List(infisicalSdk.ListDynamicSecretsRootCredentialsOptions{
|
||||||
ProjectSlug: projectDetails.Slug,
|
ProjectSlug: projectSlug,
|
||||||
SecretPath: secretsPath,
|
SecretPath: secretsPath,
|
||||||
EnvironmentSlug: environmentName,
|
EnvironmentSlug: environmentName,
|
||||||
})
|
})
|
||||||
@@ -156,6 +164,11 @@ func createDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
projectSlug, err := cmd.Flags().GetString("project-slug")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
ttl, err := cmd.Flags().GetString("ttl")
|
ttl, err := cmd.Flags().GetString("ttl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
@@ -177,10 +190,10 @@ func createDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to get resty client with custom headers")
|
util.HandleError(err, "Unable to get resty client with custom headers")
|
||||||
}
|
}
|
||||||
|
|
||||||
if projectId == "" {
|
if projectId == "" && projectSlug == "" {
|
||||||
workspaceFile, err := util.GetWorkSpaceFromFile()
|
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --projectId flag")
|
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project, pass in project id with --projectId flag, or pass in project slug with --project-slug flag")
|
||||||
}
|
}
|
||||||
projectId = workspaceFile.WorkspaceId
|
projectId = workspaceFile.WorkspaceId
|
||||||
}
|
}
|
||||||
@@ -216,14 +229,17 @@ func createDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
|||||||
})
|
})
|
||||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||||
|
|
||||||
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
|
if projectSlug == "" {
|
||||||
if err != nil {
|
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
|
||||||
util.HandleError(err, "To fetch project details")
|
if err != nil {
|
||||||
|
util.HandleError(err, "To fetch project details")
|
||||||
|
}
|
||||||
|
projectSlug = projectDetails.Slug
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamicSecretRootCredential, err := infisicalClient.DynamicSecrets().GetByName(infisicalSdk.GetDynamicSecretRootCredentialByNameOptions{
|
dynamicSecretRootCredential, err := infisicalClient.DynamicSecrets().GetByName(infisicalSdk.GetDynamicSecretRootCredentialByNameOptions{
|
||||||
DynamicSecretName: dynamicSecretRootCredentialName,
|
DynamicSecretName: dynamicSecretRootCredentialName,
|
||||||
ProjectSlug: projectDetails.Slug,
|
ProjectSlug: projectSlug,
|
||||||
SecretPath: secretsPath,
|
SecretPath: secretsPath,
|
||||||
EnvironmentSlug: environmentName,
|
EnvironmentSlug: environmentName,
|
||||||
})
|
})
|
||||||
@@ -232,13 +248,26 @@ func createDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "To fetch dynamic secret root credentials details")
|
util.HandleError(err, "To fetch dynamic secret root credentials details")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for Kubernetes dynamic secrets only
|
||||||
|
kubernetesNamespace, err := cmd.Flags().GetString("kubernetes-namespace")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
config := map[string]any{}
|
||||||
|
if kubernetesNamespace != "" {
|
||||||
|
config["namespace"] = kubernetesNamespace
|
||||||
|
}
|
||||||
|
|
||||||
leaseCredentials, _, leaseDetails, err := infisicalClient.DynamicSecrets().Leases().Create(infisicalSdk.CreateDynamicSecretLeaseOptions{
|
leaseCredentials, _, leaseDetails, err := infisicalClient.DynamicSecrets().Leases().Create(infisicalSdk.CreateDynamicSecretLeaseOptions{
|
||||||
DynamicSecretName: dynamicSecretRootCredential.Name,
|
DynamicSecretName: dynamicSecretRootCredential.Name,
|
||||||
ProjectSlug: projectDetails.Slug,
|
ProjectSlug: projectSlug,
|
||||||
TTL: ttl,
|
TTL: ttl,
|
||||||
SecretPath: secretsPath,
|
SecretPath: secretsPath,
|
||||||
EnvironmentSlug: environmentName,
|
EnvironmentSlug: environmentName,
|
||||||
|
Config: config,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "To lease dynamic secret")
|
util.HandleError(err, "To lease dynamic secret")
|
||||||
}
|
}
|
||||||
@@ -291,6 +320,11 @@ func renewDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
projectSlug, err := cmd.Flags().GetString("project-slug")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
ttl, err := cmd.Flags().GetString("ttl")
|
ttl, err := cmd.Flags().GetString("ttl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
@@ -307,10 +341,10 @@ func renewDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to get resty client with custom headers")
|
util.HandleError(err, "Unable to get resty client with custom headers")
|
||||||
}
|
}
|
||||||
|
|
||||||
if projectId == "" {
|
if projectId == "" && projectSlug == "" {
|
||||||
workspaceFile, err := util.GetWorkSpaceFromFile()
|
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --projectId flag")
|
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project, pass in project slug with --project-slug flag, or pass in project id with --projectId flag")
|
||||||
}
|
}
|
||||||
projectId = workspaceFile.WorkspaceId
|
projectId = workspaceFile.WorkspaceId
|
||||||
}
|
}
|
||||||
@@ -347,9 +381,12 @@ func renewDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
|||||||
})
|
})
|
||||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||||
|
|
||||||
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
|
if projectSlug == "" {
|
||||||
if err != nil {
|
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
|
||||||
util.HandleError(err, "To fetch project details")
|
if err != nil {
|
||||||
|
util.HandleError(err, "To fetch project details")
|
||||||
|
}
|
||||||
|
projectSlug = projectDetails.Slug
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -357,7 +394,7 @@ func renewDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
leaseDetails, err := infisicalClient.DynamicSecrets().Leases().RenewById(infisicalSdk.RenewDynamicSecretLeaseOptions{
|
leaseDetails, err := infisicalClient.DynamicSecrets().Leases().RenewById(infisicalSdk.RenewDynamicSecretLeaseOptions{
|
||||||
ProjectSlug: projectDetails.Slug,
|
ProjectSlug: projectSlug,
|
||||||
TTL: ttl,
|
TTL: ttl,
|
||||||
SecretPath: secretsPath,
|
SecretPath: secretsPath,
|
||||||
EnvironmentSlug: environmentName,
|
EnvironmentSlug: environmentName,
|
||||||
@@ -403,6 +440,11 @@ func revokeDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
projectSlug, err := cmd.Flags().GetString("project-slug")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
secretsPath, err := cmd.Flags().GetString("path")
|
secretsPath, err := cmd.Flags().GetString("path")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to parse path flag")
|
util.HandleError(err, "Unable to parse path flag")
|
||||||
@@ -414,10 +456,10 @@ func revokeDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to get resty client with custom headers")
|
util.HandleError(err, "Unable to get resty client with custom headers")
|
||||||
}
|
}
|
||||||
|
|
||||||
if projectId == "" {
|
if projectId == "" && projectSlug == "" {
|
||||||
workspaceFile, err := util.GetWorkSpaceFromFile()
|
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --projectId flag")
|
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project, pass in project slug with --project-slug flag, or pass in project id with --projectId flag")
|
||||||
}
|
}
|
||||||
projectId = workspaceFile.WorkspaceId
|
projectId = workspaceFile.WorkspaceId
|
||||||
}
|
}
|
||||||
@@ -454,9 +496,12 @@ func revokeDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
|||||||
})
|
})
|
||||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||||
|
|
||||||
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
|
if projectSlug == "" {
|
||||||
if err != nil {
|
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
|
||||||
util.HandleError(err, "To fetch project details")
|
if err != nil {
|
||||||
|
util.HandleError(err, "To fetch project details")
|
||||||
|
}
|
||||||
|
projectSlug = projectDetails.Slug
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -464,11 +509,12 @@ func revokeDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
leaseDetails, err := infisicalClient.DynamicSecrets().Leases().DeleteById(infisicalSdk.DeleteDynamicSecretLeaseOptions{
|
leaseDetails, err := infisicalClient.DynamicSecrets().Leases().DeleteById(infisicalSdk.DeleteDynamicSecretLeaseOptions{
|
||||||
ProjectSlug: projectDetails.Slug,
|
ProjectSlug: projectSlug,
|
||||||
SecretPath: secretsPath,
|
SecretPath: secretsPath,
|
||||||
EnvironmentSlug: environmentName,
|
EnvironmentSlug: environmentName,
|
||||||
LeaseId: dynamicSecretLeaseId,
|
LeaseId: dynamicSecretLeaseId,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "To revoke dynamic secret lease")
|
util.HandleError(err, "To revoke dynamic secret lease")
|
||||||
}
|
}
|
||||||
@@ -509,6 +555,11 @@ func listDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to parse flag")
|
util.HandleError(err, "Unable to parse flag")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
projectSlug, err := cmd.Flags().GetString("project-slug")
|
||||||
|
if err != nil {
|
||||||
|
util.HandleError(err, "Unable to parse flag")
|
||||||
|
}
|
||||||
|
|
||||||
secretsPath, err := cmd.Flags().GetString("path")
|
secretsPath, err := cmd.Flags().GetString("path")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to parse path flag")
|
util.HandleError(err, "Unable to parse path flag")
|
||||||
@@ -520,10 +571,10 @@ func listDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
|||||||
util.HandleError(err, "Unable to get resty client with custom headers")
|
util.HandleError(err, "Unable to get resty client with custom headers")
|
||||||
}
|
}
|
||||||
|
|
||||||
if projectId == "" {
|
if projectId == "" && projectSlug == "" {
|
||||||
workspaceFile, err := util.GetWorkSpaceFromFile()
|
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --projectId flag")
|
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project, pass in project slug with --project-slug flag, or pass in project id with --projectId flag")
|
||||||
}
|
}
|
||||||
projectId = workspaceFile.WorkspaceId
|
projectId = workspaceFile.WorkspaceId
|
||||||
}
|
}
|
||||||
@@ -559,14 +610,17 @@ func listDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
|||||||
})
|
})
|
||||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||||
|
|
||||||
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
|
if projectSlug == "" {
|
||||||
if err != nil {
|
projectDetails, err := api.CallGetProjectById(httpClient, projectId)
|
||||||
util.HandleError(err, "To fetch project details")
|
if err != nil {
|
||||||
|
util.HandleError(err, "To fetch project details")
|
||||||
|
}
|
||||||
|
projectSlug = projectDetails.Slug
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamicSecretLeases, err := infisicalClient.DynamicSecrets().Leases().List(infisicalSdk.ListDynamicSecretLeasesOptions{
|
dynamicSecretLeases, err := infisicalClient.DynamicSecrets().Leases().List(infisicalSdk.ListDynamicSecretLeasesOptions{
|
||||||
DynamicSecretName: dynamicSecretRootCredentialName,
|
DynamicSecretName: dynamicSecretRootCredentialName,
|
||||||
ProjectSlug: projectDetails.Slug,
|
ProjectSlug: projectSlug,
|
||||||
SecretPath: secretsPath,
|
SecretPath: secretsPath,
|
||||||
EnvironmentSlug: environmentName,
|
EnvironmentSlug: environmentName,
|
||||||
})
|
})
|
||||||
@@ -583,30 +637,39 @@ func init() {
|
|||||||
dynamicSecretLeaseCreateCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
|
dynamicSecretLeaseCreateCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
|
||||||
dynamicSecretLeaseCreateCmd.Flags().String("token", "", "Create dynamic secret leases using machine identity access token")
|
dynamicSecretLeaseCreateCmd.Flags().String("token", "", "Create dynamic secret leases using machine identity access token")
|
||||||
dynamicSecretLeaseCreateCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
|
dynamicSecretLeaseCreateCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
|
||||||
|
dynamicSecretLeaseCreateCmd.Flags().String("project-slug", "", "Manually set the project-slug to create lease in")
|
||||||
dynamicSecretLeaseCreateCmd.Flags().String("ttl", "", "The lease lifetime TTL. If not provided the default TTL of dynamic secret will be used.")
|
dynamicSecretLeaseCreateCmd.Flags().String("ttl", "", "The lease lifetime TTL. If not provided the default TTL of dynamic secret will be used.")
|
||||||
dynamicSecretLeaseCreateCmd.Flags().Bool("plain", false, "Print leased credentials without formatting, one per line")
|
dynamicSecretLeaseCreateCmd.Flags().Bool("plain", false, "Print leased credentials without formatting, one per line")
|
||||||
|
|
||||||
|
// Kubernetes specific flags
|
||||||
|
dynamicSecretLeaseCreateCmd.Flags().String("kubernetes-namespace", "", "The namespace to create the lease in. Only used for Kubernetes dynamic secrets.")
|
||||||
|
|
||||||
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseCreateCmd)
|
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseCreateCmd)
|
||||||
|
|
||||||
dynamicSecretLeaseListCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
|
dynamicSecretLeaseListCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
|
||||||
dynamicSecretLeaseListCmd.Flags().String("token", "", "Fetch dynamic secret leases machine identity access token")
|
dynamicSecretLeaseListCmd.Flags().String("token", "", "Fetch dynamic secret leases machine identity access token")
|
||||||
dynamicSecretLeaseListCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
|
dynamicSecretLeaseListCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
|
||||||
|
dynamicSecretLeaseListCmd.Flags().String("project-slug", "", "Manually set the project-slug to list leases from")
|
||||||
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseListCmd)
|
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseListCmd)
|
||||||
|
|
||||||
dynamicSecretLeaseRenewCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
|
dynamicSecretLeaseRenewCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
|
||||||
dynamicSecretLeaseRenewCmd.Flags().String("token", "", "Renew dynamic secrets machine identity access token")
|
dynamicSecretLeaseRenewCmd.Flags().String("token", "", "Renew dynamic secrets machine identity access token")
|
||||||
dynamicSecretLeaseRenewCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
|
dynamicSecretLeaseRenewCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
|
||||||
|
dynamicSecretLeaseRenewCmd.Flags().String("project-slug", "", "Manually set the project-slug to renew lease in")
|
||||||
dynamicSecretLeaseRenewCmd.Flags().String("ttl", "", "The lease lifetime TTL. If not provided the default TTL of dynamic secret will be used.")
|
dynamicSecretLeaseRenewCmd.Flags().String("ttl", "", "The lease lifetime TTL. If not provided the default TTL of dynamic secret will be used.")
|
||||||
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseRenewCmd)
|
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseRenewCmd)
|
||||||
|
|
||||||
dynamicSecretLeaseRevokeCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
|
dynamicSecretLeaseRevokeCmd.Flags().StringP("path", "p", "/", "The path from where dynamic secret should be leased from")
|
||||||
dynamicSecretLeaseRevokeCmd.Flags().String("token", "", "Delete dynamic secrets using machine identity access token")
|
dynamicSecretLeaseRevokeCmd.Flags().String("token", "", "Delete dynamic secrets using machine identity access token")
|
||||||
dynamicSecretLeaseRevokeCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
|
dynamicSecretLeaseRevokeCmd.Flags().String("projectId", "", "Manually set the projectId to fetch leased from when using machine identity based auth")
|
||||||
|
dynamicSecretLeaseRevokeCmd.Flags().String("project-slug", "", "Manually set the project-slug to revoke lease from")
|
||||||
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseRevokeCmd)
|
dynamicSecretLeaseCmd.AddCommand(dynamicSecretLeaseRevokeCmd)
|
||||||
|
|
||||||
dynamicSecretCmd.AddCommand(dynamicSecretLeaseCmd)
|
dynamicSecretCmd.AddCommand(dynamicSecretLeaseCmd)
|
||||||
|
|
||||||
dynamicSecretCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token")
|
dynamicSecretCmd.Flags().String("token", "", "Fetch secrets using service token or machine identity access token")
|
||||||
dynamicSecretCmd.Flags().String("projectId", "", "Manually set the projectId to fetch dynamic-secret when using machine identity based auth")
|
dynamicSecretCmd.Flags().String("projectId", "", "Manually set the projectId to fetch dynamic-secret when using machine identity based auth")
|
||||||
|
dynamicSecretCmd.Flags().String("project-slug", "", "Manually set the project-slug to fetch dynamic-secret from")
|
||||||
dynamicSecretCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
|
dynamicSecretCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on")
|
||||||
dynamicSecretCmd.Flags().String("path", "/", "get dynamic secret within a folder path")
|
dynamicSecretCmd.Flags().String("path", "/", "get dynamic secret within a folder path")
|
||||||
rootCmd.AddCommand(dynamicSecretCmd)
|
rootCmd.AddCommand(dynamicSecretCmd)
|
||||||
|
|||||||
@@ -108,17 +108,17 @@ func handleStream(stream quic.Stream, quicConn quic.Connection) {
|
|||||||
return
|
return
|
||||||
|
|
||||||
case "FORWARD-HTTP":
|
case "FORWARD-HTTP":
|
||||||
|
targetURL := ""
|
||||||
argParts := bytes.Split(args, []byte(" "))
|
argParts := bytes.Split(args, []byte(" "))
|
||||||
if len(argParts) == 0 {
|
|
||||||
log.Error().Msg("FORWARD-HTTP requires target URL")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
targetURL := string(argParts[0])
|
if len(argParts) == 0 || len(argParts[0]) == 0 {
|
||||||
|
log.Warn().Msg("FORWARD-HTTP used without a target URL.")
|
||||||
if !isValidURL(targetURL) {
|
} else {
|
||||||
log.Error().Msgf("Invalid target URL: %s", targetURL)
|
targetURL = string(argParts[0])
|
||||||
return
|
if !isValidURL(targetURL) {
|
||||||
|
log.Error().Msgf("Invalid target URL: %s", targetURL)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse optional parameters
|
// Parse optional parameters
|
||||||
@@ -183,11 +183,6 @@ func handleHTTPProxy(stream quic.Stream, reader *bufio.Reader, targetURL string,
|
|||||||
transport.TLSClientConfig = tlsConfig
|
transport.TLSClientConfig = tlsConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{
|
|
||||||
Transport: transport,
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Loop to handle multiple HTTP requests on the same stream
|
// Loop to handle multiple HTTP requests on the same stream
|
||||||
for {
|
for {
|
||||||
req, err := http.ReadRequest(reader)
|
req, err := http.ReadRequest(reader)
|
||||||
@@ -201,18 +196,53 @@ func handleHTTPProxy(stream quic.Stream, reader *bufio.Reader, targetURL string,
|
|||||||
}
|
}
|
||||||
log.Info().Msgf("Received HTTP request: %s", req.URL.Path)
|
log.Info().Msgf("Received HTTP request: %s", req.URL.Path)
|
||||||
|
|
||||||
actionHeader := req.Header.Get("x-infisical-action")
|
actionHeader := HttpProxyAction(req.Header.Get(INFISICAL_HTTP_PROXY_ACTION_HEADER))
|
||||||
if actionHeader != "" {
|
if actionHeader != "" {
|
||||||
if actionHeader == "inject-k8s-sa-auth-token" {
|
if actionHeader == HttpProxyActionInjectGatewayK8sServiceAccountToken {
|
||||||
token, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
|
token, err := os.ReadFile(KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
stream.Write([]byte(buildHttpInternalServerError("failed to read k8s sa auth token")))
|
stream.Write([]byte(buildHttpInternalServerError("failed to read k8s sa auth token")))
|
||||||
continue // Continue to next request instead of returning
|
continue // Continue to next request instead of returning
|
||||||
}
|
}
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(token)))
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(token)))
|
||||||
log.Info().Msgf("Injected gateway k8s SA auth token in request to %s", targetURL)
|
log.Info().Msgf("Injected gateway k8s SA auth token in request to %s", targetURL)
|
||||||
|
} else if actionHeader == HttpProxyActionUseGatewayK8sServiceAccount { // will work without a target URL set
|
||||||
|
// set the ca cert to the pod's k8s service account ca cert:
|
||||||
|
caCert, err := os.ReadFile(KUBERNETES_SERVICE_ACCOUNT_CA_CERT_PATH)
|
||||||
|
if err != nil {
|
||||||
|
stream.Write([]byte(buildHttpInternalServerError("failed to read k8s sa ca cert")))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
caCertPool := x509.NewCertPool()
|
||||||
|
if ok := caCertPool.AppendCertsFromPEM(caCert); !ok {
|
||||||
|
stream.Write([]byte(buildHttpInternalServerError("failed to parse k8s sa ca cert")))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
transport.TLSClientConfig = &tls.Config{
|
||||||
|
RootCAs: caCertPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// set authorization header to the pod's k8s service account token:
|
||||||
|
token, err := os.ReadFile(KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH)
|
||||||
|
if err != nil {
|
||||||
|
stream.Write([]byte(buildHttpInternalServerError("failed to read k8s sa auth token")))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", string(token)))
|
||||||
|
|
||||||
|
// update the target URL to point to the kubernetes API server:
|
||||||
|
kubernetesServiceHost := os.Getenv(KUBERNETES_SERVICE_HOST_ENV_NAME)
|
||||||
|
kubernetesServicePort := os.Getenv(KUBERNETES_SERVICE_PORT_HTTPS_ENV_NAME)
|
||||||
|
|
||||||
|
fullBaseUrl := fmt.Sprintf("https://%s:%s", kubernetesServiceHost, kubernetesServicePort)
|
||||||
|
targetURL = fullBaseUrl
|
||||||
|
|
||||||
|
log.Info().Msgf("Redirected request to Kubernetes API server: %s", targetURL)
|
||||||
}
|
}
|
||||||
req.Header.Del("x-infisical-action")
|
|
||||||
|
req.Header.Del(INFISICAL_HTTP_PROXY_ACTION_HEADER)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build full target URL
|
// Build full target URL
|
||||||
@@ -242,6 +272,11 @@ func handleHTTPProxy(stream quic.Stream, reader *bufio.Reader, targetURL string,
|
|||||||
|
|
||||||
log.Info().Msgf("Proxying %s %s to %s", req.Method, req.URL.Path, targetFullURL)
|
log.Info().Msgf("Proxying %s %s to %s", req.Method, req.URL.Path, targetFullURL)
|
||||||
|
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := client.Do(proxyReq)
|
resp, err := client.Do(proxyReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Msgf("Failed to reach target: %v", err)
|
log.Error().Msgf("Failed to reach target: %v", err)
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package gateway
|
||||||
|
|
||||||
|
const (
|
||||||
|
KUBERNETES_SERVICE_HOST_ENV_NAME = "KUBERNETES_SERVICE_HOST"
|
||||||
|
KUBERNETES_SERVICE_PORT_HTTPS_ENV_NAME = "KUBERNETES_SERVICE_PORT_HTTPS"
|
||||||
|
KUBERNETES_SERVICE_ACCOUNT_CA_CERT_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
|
||||||
|
KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token"
|
||||||
|
|
||||||
|
INFISICAL_HTTP_PROXY_ACTION_HEADER = "x-infisical-action"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HttpProxyAction string
|
||||||
|
|
||||||
|
const (
|
||||||
|
HttpProxyActionInjectGatewayK8sServiceAccountToken HttpProxyAction = "inject-k8s-sa-auth-token"
|
||||||
|
HttpProxyActionUseGatewayK8sServiceAccount HttpProxyAction = "use-k8s-sa"
|
||||||
|
)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Create Kubernetes Lease"
|
||||||
|
openapi: "POST /api/v1/dynamic-secrets/leases/kubernetes"
|
||||||
|
---
|
||||||
@@ -49,7 +49,7 @@ export INFISICAL_DISABLE_UPDATE_CHECK=true
|
|||||||
### Flags
|
### Flags
|
||||||
|
|
||||||
<Accordion title="--projectId">
|
<Accordion title="--projectId">
|
||||||
The project ID to fetch dynamic secrets from. This is required when using a machine identity to authenticate.
|
The project ID to fetch dynamic secrets from.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Example
|
# Example
|
||||||
@@ -58,6 +58,16 @@ infisical dynamic-secrets --projectId=<project-id>
|
|||||||
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--project-slug">
|
||||||
|
The project slug to fetch dynamic secrets from.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets --project-slug=<project-slug>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="--token">
|
<Accordion title="--token">
|
||||||
The authenticated token to fetch dynamic secrets from. This is required when using a machine identity to authenticate.
|
The authenticated token to fetch dynamic secrets from. This is required when using a machine identity to authenticate.
|
||||||
|
|
||||||
@@ -119,7 +129,7 @@ infisical dynamic-secrets lease create <dynamic-secret-name> --path="/" --env=de
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="--projectId">
|
<Accordion title="--projectId">
|
||||||
The project ID of the dynamic secrets to lease from. This is required when using a machine identity to authenticate.
|
The project ID of the dynamic secrets to lease from.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Example
|
# Example
|
||||||
@@ -128,6 +138,16 @@ infisical dynamic-secrets lease create <dynamic-secret-name> --projectId=<projec
|
|||||||
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--project-slug">
|
||||||
|
The project slug of the dynamic secrets to lease from.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets lease create <dynamic-secret-name> --project-slug=<project-slug>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="--token">
|
<Accordion title="--token">
|
||||||
The authenticated token to create dynamic secret leases. This is required when using a machine identity to authenticate.
|
The authenticated token to create dynamic secret leases. This is required when using a machine identity to authenticate.
|
||||||
|
|
||||||
@@ -148,6 +168,22 @@ infisical dynamic-secrets lease create <dynamic-secret-name> --ttl=<ttl>
|
|||||||
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
|
### Provider-specific flags
|
||||||
|
|
||||||
|
The following flags are specific to certain providers or integrations:
|
||||||
|
|
||||||
|
<Accordion title="Kubernetes">
|
||||||
|
<Accordion title="--kubernetes-namespace">
|
||||||
|
The namespace to create the lease in. Only used for Kubernetes dynamic secrets.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets lease create <dynamic-secret-name> --kubernetes-namespace=<namespace>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Accordion title="infisical dynamic-secrets lease list">
|
<Accordion title="infisical dynamic-secrets lease list">
|
||||||
This command is used to list leases for a dynamic secret.
|
This command is used to list leases for a dynamic secret.
|
||||||
@@ -174,7 +210,7 @@ infisical dynamic-secrets lease list <dynamic-secret-name> --path="/" --env=dev
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="--projectId">
|
<Accordion title="--projectId">
|
||||||
The project ID of the dynamic secrets to list leases from. This is required when using a machine identity to authenticate.
|
The project ID of the dynamic secrets to list leases from.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Example
|
# Example
|
||||||
@@ -183,6 +219,16 @@ infisical dynamic-secrets lease list <dynamic-secret-name> --projectId=<project-
|
|||||||
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--project-slug">
|
||||||
|
The project slug of the dynamic secrets to list leases from.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets lease list <dynamic-secret-name> --project-slug=<project-slug>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="--token">
|
<Accordion title="--token">
|
||||||
The authenticated token to list dynamic secret leases. This is required when using a machine identity to authenticate.
|
The authenticated token to list dynamic secret leases. This is required when using a machine identity to authenticate.
|
||||||
|
|
||||||
@@ -219,7 +265,7 @@ infisical dynamic-secrets lease renew <lease-id> --path="/" --env=dev
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="--projectId">
|
<Accordion title="--projectId">
|
||||||
The project ID of the dynamic secret's lease from. This is required when using a machine identity to authenticate.
|
The project ID of the dynamic secret to lease from.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Example
|
# Example
|
||||||
@@ -228,6 +274,16 @@ infisical dynamic-secrets lease renew <lease-id> --projectId=<project-id>
|
|||||||
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--project-slug">
|
||||||
|
The project slug of the dynamic secret to lease from.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets lease renew <lease-id> --project-slug=<project-slug>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="--token">
|
<Accordion title="--token">
|
||||||
The authenticated token to create dynamic secret leases. This is required when using a machine identity to authenticate.
|
The authenticated token to create dynamic secret leases. This is required when using a machine identity to authenticate.
|
||||||
|
|
||||||
@@ -274,7 +330,7 @@ infisical dynamic-secrets lease delete <lease-id> --path="/" --env=dev
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="--projectId">
|
<Accordion title="--projectId">
|
||||||
The project ID of the dynamic secret's lease from. This is required when using a machine identity to authenticate.
|
The project ID of the dynamic secret to delete lease from.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Example
|
# Example
|
||||||
@@ -283,6 +339,16 @@ infisical dynamic-secrets lease delete <lease-id> --projectId=<project-id>
|
|||||||
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--project-slug">
|
||||||
|
The project slug of the dynamic secret to delete lease from.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical dynamic-secrets lease delete <lease-id> --project-slug=<project-slug>
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="--token">
|
<Accordion title="--token">
|
||||||
The authenticated token to delete dynamic secret leases. This is required when using a machine identity to authenticate.
|
The authenticated token to delete dynamic secret leases. This is required when using a machine identity to authenticate.
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,12 @@ The CLI is designed for a variety of secret management applications ranging from
|
|||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Starting with CLI version v0.4.0, you can now choose to log in via Infisical Cloud (US/EU) or your own self-hosted instance by simply running `infisical login` and following the on-screen instructions — no need to manually set the `INFISICAL_API_URL` environment variable.
|
||||||
|
|
||||||
|
For versions prior to v0.4.0, the CLI defaults to the US Cloud. To connect to the EU Cloud or a self-hosted instance, set the `INFISICAL_API_URL` environment variable to `https://eu.infisical.com` or your custom URL.
|
||||||
|
</Note>
|
||||||
|
|
||||||
<Tip>
|
<Tip>
|
||||||
## Custom Request Headers
|
## Custom Request Headers
|
||||||
|
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ Each log entry sent to the external logging provider will follow the same struct
|
|||||||
<ParamField path="eventType" type="string" required>
|
<ParamField path="eventType" type="string" required>
|
||||||
The type of event that occurred. Below you can see a list of possible event types. More event types will be added in the future as we expand our audit logs further.
|
The type of event that occurred. Below you can see a list of possible event types. More event types will be added in the future as we expand our audit logs further.
|
||||||
|
|
||||||
`get-secrets`, `delete-secrets`, `get-secret`, `create-secret`, `update-secret`, `delete-secret`, `get-workspace-key`, `authorize-integration`, `update-integration-auth`, `unauthorize-integration`, `create-integration`, `delete-integration`, `add-trusted-ip`, `update-trusted-ip`, `delete-trusted-ip`, `create-service-token`, `delete-service-token`, `create-identity`, `update-identity`, `delete-identity`, `login-identity-universal-auth`, `add-identity-universal-auth`, `update-identity-universal-auth`, `get-identity-universal-auth`, `create-identity-universal-auth-client-secret`, `revoke-identity-universal-auth-client-secret`, `get-identity-universal-auth-client-secret`, `create-environment`, `update-environment`, `delete-environment`, `add-workspace-member`, `remove-workspace-member`, `create-folder`, `update-folder`, `delete-folder`, `create-webhook`, `update-webhook-status`, `delete-webhook`, `get-secret-imports`, `create-secret-import`, `update-secret-import`, `delete-secret-import`, `update-user-workspace-role`, `update-user-workspace-denied-permissions`, `create-certificate-authority`, `get-certificate-authority`, `update-certificate-authority`, `delete-certificate-authority`, `get-certificate-authority-csr`, `get-certificate-authority-cert`, `sign-intermediate`, `import-certificate-authority-cert`, `get-certificate-authority-crl`, `issue-cert`, `get-cert`, `delete-cert`, `revoke-cert`, `get-cert-body`, `create-pki-alert`, `get-pki-alert`, `update-pki-alert`, `delete-pki-alert`, `create-pki-collection`, `get-pki-collection`, `update-pki-collection`, `delete-pki-collection`, `get-pki-collection-items`, `add-pki-collection-item`, `delete-pki-collection-item`, `org-admin-accessed-project`, `create-certificate-template`, `update-certificate-template`, `delete-certificate-template`, `get-certificate-template`, `create-certificate-template-est-config`, `update-certificate-template-est-config`, `get-certificate-template-est-config`, `update-project-slack-config`, `get-project-slack-config`, `integration-synced`, `create-shared-secret`, `delete-shared-secret`, `read-shared-secret`.
|
`get-secrets`, `delete-secrets`, `get-secret`, `create-secret`, `update-secret`, `delete-secret`, `get-workspace-key`, `authorize-integration`, `update-integration-auth`, `unauthorize-integration`, `create-integration`, `delete-integration`, `add-trusted-ip`, `update-trusted-ip`, `delete-trusted-ip`, `create-service-token`, `delete-service-token`, `create-identity`, `update-identity`, `delete-identity`, `login-identity-universal-auth`, `add-identity-universal-auth`, `update-identity-universal-auth`, `get-identity-universal-auth`, `create-identity-universal-auth-client-secret`, `revoke-identity-universal-auth-client-secret`, `get-identity-universal-auth-client-secret`, `create-environment`, `update-environment`, `delete-environment`, `add-workspace-member`, `remove-workspace-member`, `create-folder`, `update-folder`, `delete-folder`, `create-webhook`, `update-webhook-status`, `delete-webhook`, `webhook-triggered`, `get-secret-imports`, `create-secret-import`, `update-secret-import`, `delete-secret-import`, `update-user-workspace-role`, `update-user-workspace-denied-permissions`, `create-certificate-authority`, `get-certificate-authority`, `update-certificate-authority`, `delete-certificate-authority`, `get-certificate-authority-csr`, `get-certificate-authority-cert`, `sign-intermediate`, `import-certificate-authority-cert`, `get-certificate-authority-crl`, `issue-cert`, `get-cert`, `delete-cert`, `revoke-cert`, `get-cert-body`, `create-pki-alert`, `get-pki-alert`, `update-pki-alert`, `delete-pki-alert`, `create-pki-collection`, `get-pki-collection`, `update-pki-collection`, `delete-pki-collection`, `get-pki-collection-items`, `add-pki-collection-item`, `delete-pki-collection-item`, `org-admin-accessed-project`, `create-certificate-template`, `update-certificate-template`, `delete-certificate-template`, `get-certificate-template`, `create-certificate-template-est-config`, `update-certificate-template-est-config`, `get-certificate-template-est-config`, `update-project-slack-config`, `get-project-slack-config`, `integration-synced`, `create-shared-secret`, `delete-shared-secret`, `read-shared-secret`.
|
||||||
</ParamField>
|
</ParamField>
|
||||||
|
|
||||||
<ParamField path="eventMetadata" type="object" required>
|
<ParamField path="eventMetadata" type="object" required>
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ Infisical needs an initial AWS IAM user with the required permissions to create
|
|||||||
"iam:ListUserPolicies",
|
"iam:ListUserPolicies",
|
||||||
"iam:PutUserPolicy",
|
"iam:PutUserPolicy",
|
||||||
"iam:AddUserToGroup",
|
"iam:AddUserToGroup",
|
||||||
"iam:RemoveUserFromGroup"
|
"iam:RemoveUserFromGroup",
|
||||||
|
"iam:TagUser"
|
||||||
],
|
],
|
||||||
"Resource": ["*"]
|
"Resource": ["*"]
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user