diff --git a/backend/src/db/migrations/20240730181850_secret-v2.ts b/backend/src/db/migrations/20240730181850_secret-v2.ts index a9188c608..d44c67cf1 100644 --- a/backend/src/db/migrations/20240730181850_secret-v2.ts +++ b/backend/src/db/migrations/20240730181850_secret-v2.ts @@ -1,178 +1,8 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import { Knex } from "knex"; -import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; -import { selectAllTableCols } from "@app/lib/knex/select"; - -import { SecretKeyEncoding, SecretType, TableName } from "../schemas"; +import { SecretType, TableName } from "../schemas"; import { createJunctionTable, createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; -import { getSecretManagerDataKey } from "./utils/kms"; - -const backfillWebhooks = async (knex: Knex) => { - const hasEncryptedSecretKeyWithKms = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKeyWithKms"); - const hasEncryptedWebhookUrl = await knex.schema.hasColumn(TableName.Webhook, "encryptedUrl"); - const hasUrlCipherText = await knex.schema.hasColumn(TableName.Webhook, "urlCipherText"); - const hasUrlIV = await knex.schema.hasColumn(TableName.Webhook, "urlIV"); - const hasUrlTag = await knex.schema.hasColumn(TableName.Webhook, "urlTag"); - const hasEncryptedSecretKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKey"); - const hasIV = await knex.schema.hasColumn(TableName.Webhook, "iv"); - const hasTag = await knex.schema.hasColumn(TableName.Webhook, "tag"); - const hasKeyEncoding = await knex.schema.hasColumn(TableName.Webhook, "keyEncoding"); - const hasAlgorithm = await knex.schema.hasColumn(TableName.Webhook, "algorithm"); - const hasUrl = await knex.schema.hasColumn(TableName.Webhook, "url"); - - await knex.schema.alterTable(TableName.Webhook, (t) => { - if (!hasEncryptedSecretKeyWithKms) t.binary("encryptedSecretKeyWithKms"); - if (!hasEncryptedWebhookUrl) t.binary("encryptedUrl"); - if (hasUrl) t.string("url").nullable().alter(); - }); - - const kmsEncryptorGroupByProjectId: Record>["encryptor"]> = - {}; - if (hasUrlCipherText && hasUrlIV && hasUrlTag && hasEncryptedSecretKey && hasIV && hasTag) { - // eslint-disable-next-line - const webhooksToFill = await knex(TableName.Webhook) - .join(TableName.Environment, `${TableName.Environment}.id`, `${TableName.Webhook}.envId`) - .whereNull("encryptedUrl") - // eslint-disable-next-line - // @ts-ignore knex migration fails - .select(selectAllTableCols(TableName.Webhook)) - .select("projectId"); - - const updatedWebhooks = []; - for (const webhook of webhooksToFill) { - if (!kmsEncryptorGroupByProjectId[webhook.projectId]) { - // eslint-disable-next-line - const { encryptor } = await getSecretManagerDataKey(knex, webhook.projectId); - kmsEncryptorGroupByProjectId[webhook.projectId] = encryptor; - } - - const kmsEncryptor = kmsEncryptorGroupByProjectId[webhook.projectId]; - - // @ts-ignore post migration fails - let webhookUrl = webhook.url; - let webhookSecretKey; - - // @ts-ignore post migration fails - if (webhook.urlTag && webhook.urlCipherText && webhook.urlIV) { - webhookUrl = infisicalSymmetricDecrypt({ - // @ts-ignore post migration fails - keyEncoding: webhook.keyEncoding as SecretKeyEncoding, - // @ts-ignore post migration fails - ciphertext: webhook.urlCipherText, - // @ts-ignore post migration fails - iv: webhook.urlIV, - // @ts-ignore post migration fails - tag: webhook.urlTag - }); - } - // @ts-ignore post migration fails - if (webhook.encryptedSecretKey && webhook.iv && webhook.tag) { - webhookSecretKey = infisicalSymmetricDecrypt({ - // @ts-ignore post migration fails - keyEncoding: webhook.keyEncoding as SecretKeyEncoding, - // @ts-ignore post migration fails - ciphertext: webhook.encryptedSecretKey, - // @ts-ignore post migration fails - iv: webhook.iv, - // @ts-ignore post migration fails - tag: webhook.tag - }); - } - const { projectId, ...el } = webhook; - updatedWebhooks.push({ - ...el, - encryptedSecretKeyWithKms: webhookSecretKey - ? kmsEncryptor({ plainText: Buffer.from(webhookSecretKey) }).cipherTextBlob - : null, - encryptedUrl: kmsEncryptor({ plainText: Buffer.from(webhookUrl) }).cipherTextBlob - }); - } - if (updatedWebhooks.length) { - // eslint-disable-next-line - await knex(TableName.Webhook).insert(updatedWebhooks).onConflict("id").merge(); - } - } - await knex.schema.alterTable(TableName.Webhook, (t) => { - t.binary("encryptedUrl").notNullable().alter(); - - if (hasUrlIV) t.dropColumn("urlIV"); - if (hasUrlCipherText) t.dropColumn("urlCipherText"); - if (hasUrlTag) t.dropColumn("urlTag"); - if (hasIV) t.dropColumn("iv"); - if (hasTag) t.dropColumn("tag"); - if (hasEncryptedSecretKey) t.dropColumn("encryptedSecretKey"); - if (hasKeyEncoding) t.dropColumn("keyEncoding"); - if (hasAlgorithm) t.dropColumn("algorithm"); - if (hasUrl) t.dropColumn("url"); - }); -}; - -const backfillDynamicSecretConfigs = async (knex: Knex) => { - const hasEncryptedConfig = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedConfig"); - - const hasInputCipherText = await knex.schema.hasColumn(TableName.DynamicSecret, "inputCiphertext"); - const hasInputIV = await knex.schema.hasColumn(TableName.DynamicSecret, "inputIV"); - const hasInputTag = await knex.schema.hasColumn(TableName.DynamicSecret, "inputTag"); - const hasKeyEncoding = await knex.schema.hasColumn(TableName.DynamicSecret, "keyEncoding"); - const hasAlgorithm = await knex.schema.hasColumn(TableName.DynamicSecret, "algorithm"); - - await knex.schema.alterTable(TableName.DynamicSecret, (t) => { - if (!hasEncryptedConfig) t.binary("encryptedConfig"); - }); - const kmsEncryptorGroupByProjectId: Record>["encryptor"]> = - {}; - if (hasInputCipherText && hasInputIV && hasInputTag) { - // eslint-disable-next-line - const dynamicSecretConfigs = await knex(TableName.DynamicSecret) - .join(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.DynamicSecret}.folderId`) - .join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`) - .whereNull("encryptedConfig") - // @ts-ignore post migration fails - .select(selectAllTableCols(TableName.DynamicSecret)) - .select("projectId"); - - const updatedConfigs = []; - for (const dynamicSecretConfig of dynamicSecretConfigs) { - if (!kmsEncryptorGroupByProjectId[dynamicSecretConfig.projectId]) { - // eslint-disable-next-line - const { encryptor } = await getSecretManagerDataKey(knex, dynamicSecretConfig.projectId); - kmsEncryptorGroupByProjectId[dynamicSecretConfig.projectId] = encryptor; - } - - const kmsEncryptor = kmsEncryptorGroupByProjectId[dynamicSecretConfig.projectId]; - const inputConfig = infisicalSymmetricDecrypt({ - // @ts-ignore post migration fails - keyEncoding: dynamicSecretConfig.keyEncoding as SecretKeyEncoding, - // @ts-ignore post migration fails - ciphertext: dynamicSecretConfig.inputCiphertext as string, - // @ts-ignore post migration fails - iv: dynamicSecretConfig.inputIV as string, - // @ts-ignore post migration fails - tag: dynamicSecretConfig.inputTag as string - }); - - const { projectId, ...el } = dynamicSecretConfig; - updatedConfigs.push({ - ...el, - encryptedConfig: kmsEncryptor({ plainText: Buffer.from(inputConfig) }).cipherTextBlob - }); - } - if (updatedConfigs.length) { - // eslint-disable-next-line - await knex(TableName.DynamicSecret).insert(updatedConfigs).onConflict("id").merge(); - } - } - await knex.schema.alterTable(TableName.DynamicSecret, (t) => { - t.binary("encryptedConfig").notNullable().alter(); - - if (hasInputTag) t.dropColumn("inputTag"); - if (hasInputIV) t.dropColumn("inputIV"); - if (hasInputCipherText) t.dropColumn("inputCiphertext"); - if (hasKeyEncoding) t.dropColumn("keyEncoding"); - if (hasAlgorithm) t.dropColumn("algorithm"); - }); -}; export async function up(knex: Knex): Promise { const doesSecretV2TableExist = await knex.schema.hasTable(TableName.SecretV2); @@ -314,14 +144,6 @@ export async function up(knex: Knex): Promise { t.foreign("rotationId").references("id").inTable(TableName.SecretRotation).onDelete("CASCADE"); }); } - - if (await knex.schema.hasTable(TableName.Webhook)) { - await backfillWebhooks(knex); - } - - if (await knex.schema.hasTable(TableName.DynamicSecret)) { - await backfillDynamicSecretConfigs(knex); - } } export async function down(knex: Knex): Promise { @@ -356,49 +178,4 @@ export async function down(knex: Knex): Promise { if (hasEncryptedAwsIamAssumRole) t.dropColumn("encryptedAwsAssumeIamRoleArn"); }); } - if (await knex.schema.hasTable(TableName.Webhook)) { - const hasEncryptedWebhookSecretKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKeyWithKms"); - const hasEncryptedWebhookUrl = await knex.schema.hasColumn(TableName.Webhook, "encryptedUrl"); - const hasUrlCipherText = await knex.schema.hasColumn(TableName.Webhook, "urlCipherText"); - const hasUrlIV = await knex.schema.hasColumn(TableName.Webhook, "urlIV"); - const hasUrlTag = await knex.schema.hasColumn(TableName.Webhook, "urlTag"); - const hasEncryptedSecretKey = await knex.schema.hasColumn(TableName.Webhook, "encryptedSecretKey"); - const hasIV = await knex.schema.hasColumn(TableName.Webhook, "iv"); - const hasTag = await knex.schema.hasColumn(TableName.Webhook, "tag"); - const hasKeyEncoding = await knex.schema.hasColumn(TableName.Webhook, "keyEncoding"); - const hasAlgorithm = await knex.schema.hasColumn(TableName.Webhook, "algorithm"); - const hasUrl = await knex.schema.hasColumn(TableName.Webhook, "url"); - - await knex.schema.alterTable(TableName.Webhook, (t) => { - if (hasEncryptedWebhookSecretKey) t.dropColumn("encryptedSecretKeyWithKms"); - if (hasEncryptedWebhookUrl) t.dropColumn("encryptedUrl"); - if (!hasUrl) t.string("url"); - if (!hasEncryptedSecretKey) t.string("encryptedSecretKey"); - if (!hasIV) t.string("iv"); - if (!hasTag) t.string("tag"); - if (!hasAlgorithm) t.string("algorithm"); - if (!hasKeyEncoding) t.string("keyEncoding"); - if (!hasUrlCipherText) t.string("urlCipherText"); - if (!hasUrlIV) t.string("urlIV"); - if (!hasUrlTag) t.string("urlTag"); - }); - } - - if (await knex.schema.hasTable(TableName.DynamicSecret)) { - const hasEncryptedConfig = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedConfig"); - - const hasInputIV = await knex.schema.hasColumn(TableName.DynamicSecret, "inputIV"); - const hasInputCipherText = await knex.schema.hasColumn(TableName.DynamicSecret, "inputCiphertext"); - const hasInputTag = await knex.schema.hasColumn(TableName.DynamicSecret, "inputTag"); - const hasAlgorithm = await knex.schema.hasColumn(TableName.DynamicSecret, "algorithm"); - const hasKeyEncoding = await knex.schema.hasColumn(TableName.DynamicSecret, "keyEncoding"); - await knex.schema.alterTable(TableName.DynamicSecret, (t) => { - if (hasEncryptedConfig) t.dropColumn("encryptedConfig"); - if (!hasInputIV) t.string("inputIV"); - if (!hasInputCipherText) t.text("inputCiphertext"); - if (!hasInputTag) t.string("inputTag"); - if (!hasAlgorithm) t.string("algorithm"); - if (!hasKeyEncoding) t.string("keyEncoding"); - }); - } } diff --git a/backend/src/db/schemas/dynamic-secrets.ts b/backend/src/db/schemas/dynamic-secrets.ts index d90f1f7d2..b27da396c 100644 --- a/backend/src/db/schemas/dynamic-secrets.ts +++ b/backend/src/db/schemas/dynamic-secrets.ts @@ -5,8 +5,6 @@ import { z } from "zod"; -import { zodBuffer } from "@app/lib/zod"; - import { TImmutableDBKeys } from "./models"; export const DynamicSecretsSchema = z.object({ @@ -16,12 +14,16 @@ export const DynamicSecretsSchema = z.object({ type: z.string(), defaultTTL: z.string(), maxTTL: z.string().nullable().optional(), + inputIV: z.string(), + inputCiphertext: z.string(), + inputTag: z.string(), + algorithm: z.string().default("aes-256-gcm"), + keyEncoding: z.string().default("utf8"), folderId: z.string().uuid(), status: z.string().nullable().optional(), statusDetails: z.string().nullable().optional(), createdAt: z.date(), - updatedAt: z.date(), - encryptedConfig: zodBuffer + updatedAt: z.date() }); export type TDynamicSecrets = z.infer; diff --git a/backend/src/db/schemas/webhooks.ts b/backend/src/db/schemas/webhooks.ts index 3f670497f..a7aac2933 100644 --- a/backend/src/db/schemas/webhooks.ts +++ b/backend/src/db/schemas/webhooks.ts @@ -5,22 +5,27 @@ import { z } from "zod"; -import { zodBuffer } from "@app/lib/zod"; - import { TImmutableDBKeys } from "./models"; export const WebhooksSchema = z.object({ id: z.string().uuid(), secretPath: z.string().default("/"), + url: z.string(), lastStatus: z.string().nullable().optional(), lastRunErrorMessage: z.string().nullable().optional(), isDisabled: z.boolean().default(false), + encryptedSecretKey: z.string().nullable().optional(), + iv: z.string().nullable().optional(), + tag: z.string().nullable().optional(), + algorithm: z.string().nullable().optional(), + keyEncoding: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date(), envId: z.string().uuid(), - type: z.string().default("general").nullable().optional(), - encryptedSecretKeyWithKms: zodBuffer.nullable().optional(), - encryptedUrl: zodBuffer + urlCipherText: z.string().nullable().optional(), + urlIV: z.string().nullable().optional(), + urlTag: z.string().nullable().optional(), + type: z.string().default("general").nullable().optional() }); export type TWebhooks = z.infer; diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts index 97a682933..810628030 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal.ts @@ -12,10 +12,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { const countLeasesForDynamicSecret = async (dynamicSecretId: string, tx?: Knex) => { try { - const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease) - .count("*") - .where({ dynamicSecretId }) - .first(); + const doc = await (tx || db)(TableName.DynamicSecretLease).count("*").where({ dynamicSecretId }).first(); return parseInt(doc || "0", 10); } catch (error) { throw new DatabaseError({ error, name: "DynamicSecretCountLeases" }); @@ -24,7 +21,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { try { - const doc = await (tx || db.replicaNode())(TableName.DynamicSecretLease) + const doc = await (tx || db)(TableName.DynamicSecretLease) .where({ [`${TableName.DynamicSecretLease}.id` as "id"]: id }) .first() .join( @@ -40,10 +37,14 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { db.ref("type").withSchema(TableName.DynamicSecret).as("dynType"), db.ref("defaultTTL").withSchema(TableName.DynamicSecret).as("dynDefaultTTL"), db.ref("maxTTL").withSchema(TableName.DynamicSecret).as("dynMaxTTL"), + db.ref("inputIV").withSchema(TableName.DynamicSecret).as("dynInputIV"), + db.ref("inputTag").withSchema(TableName.DynamicSecret).as("dynInputTag"), + db.ref("inputCiphertext").withSchema(TableName.DynamicSecret).as("dynInputCiphertext"), + db.ref("algorithm").withSchema(TableName.DynamicSecret).as("dynAlgorithm"), + db.ref("keyEncoding").withSchema(TableName.DynamicSecret).as("dynKeyEncoding"), db.ref("folderId").withSchema(TableName.DynamicSecret).as("dynFolderId"), db.ref("status").withSchema(TableName.DynamicSecret).as("dynStatus"), db.ref("statusDetails").withSchema(TableName.DynamicSecret).as("dynStatusDetails"), - db.ref("encryptedConfig").withSchema(TableName.DynamicSecret).as("dynEncryptedConfig"), db.ref("createdAt").withSchema(TableName.DynamicSecret).as("dynCreatedAt"), db.ref("updatedAt").withSchema(TableName.DynamicSecret).as("dynUpdatedAt") ); @@ -58,12 +59,16 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => { type: doc.dynType, defaultTTL: doc.dynDefaultTTL, maxTTL: doc.dynMaxTTL, + inputIV: doc.dynInputIV, + inputTag: doc.dynInputTag, + inputCiphertext: doc.dynInputCiphertext, + algorithm: doc.dynAlgorithm, + keyEncoding: doc.dynKeyEncoding, folderId: doc.dynFolderId, status: doc.dynStatus, statusDetails: doc.dynStatusDetails, createdAt: doc.dynCreatedAt, - updatedAt: doc.dynUpdatedAt, - encryptedConfig: doc.dynEncryptedConfig + updatedAt: doc.dynUpdatedAt } }; } catch (error) { diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts index 82248bd8c..9bdb1c24e 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue.ts @@ -1,9 +1,8 @@ +import { SecretKeyEncoding } from "@app/db/schemas"; import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; -import { TKmsServiceFactory } from "@app/services/kms/kms-service"; -import { KmsDataKey } from "@app/services/kms/kms-types"; -import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal"; import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types"; @@ -15,8 +14,6 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = { dynamicSecretLeaseDAL: Pick; dynamicSecretDAL: Pick; dynamicSecretProviders: Record; - kmsService: Pick; - folderDAL: Pick; }; export type TDynamicSecretLeaseQueueServiceFactory = ReturnType; @@ -25,9 +22,7 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ queueService, dynamicSecretDAL, dynamicSecretProviders, - dynamicSecretLeaseDAL, - kmsService, - folderDAL + dynamicSecretLeaseDAL }: TDynamicSecretLeaseQueueServiceFactoryDep) => { const pruneDynamicSecret = async (dynamicSecretCfgId: string) => { await queueService.queue( @@ -82,20 +77,15 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" }); const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; - const folder = await folderDAL.findById(dynamicSecretCfg.folderId); - if (!folder) throw new DisableRotationErrors({ message: "Folder not found" }); - const { projectId } = folder; - - const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); - - const dynamicSecretInputConfig = secretManagerDecryptor({ - cipherTextBlob: dynamicSecretCfg.encryptedConfig - }).toString(); const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; - const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object; + const decryptedStoredInput = JSON.parse( + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) + ) as object; await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId); await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id); @@ -110,22 +100,17 @@ export const dynamicSecretLeaseQueueServiceFactory = ({ if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting) throw new DisableRotationErrors({ message: "Document not deleted" }); - const folder = await folderDAL.findById(dynamicSecretCfg.folderId); - if (!folder) throw new DisableRotationErrors({ message: "Folder not found" }); - const { projectId } = folder; - const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); - const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfgId }); if (dynamicSecretLeases.length) { const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; - - const dynamicSecretInputConfig = secretManagerDecryptor({ - cipherTextBlob: dynamicSecretCfg.encryptedConfig - }).toString(); - const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object; + const decryptedStoredInput = JSON.parse( + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) + ) as object; await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id))); await Promise.all( diff --git a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts index eb0c6f171..1e5487d22 100644 --- a/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts +++ b/backend/src/ee/services/dynamic-secret-lease/dynamic-secret-lease-service.ts @@ -1,14 +1,14 @@ import { ForbiddenError, subject } from "@casl/ability"; import ms from "ms"; +import { SecretKeyEncoding } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; -import { TKmsServiceFactory } from "@app/services/kms/kms-service"; -import { KmsDataKey } from "@app/services/kms/kms-types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; @@ -34,7 +34,6 @@ type TDynamicSecretLeaseServiceFactoryDep = { folderDAL: Pick; permissionService: Pick; projectDAL: Pick; - kmsService: Pick; }; export type TDynamicSecretLeaseServiceFactory = ReturnType; @@ -47,8 +46,7 @@ export const dynamicSecretLeaseServiceFactory = ({ permissionService, dynamicSecretQueueService, projectDAL, - licenseService, - kmsService + licenseService }: TDynamicSecretLeaseServiceFactoryDep) => { const create = async ({ environmentSlug, @@ -96,12 +94,14 @@ export const dynamicSecretLeaseServiceFactory = ({ throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` }); const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; - const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); - const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString(); - const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object; + const decryptedStoredInput = JSON.parse( + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) + ) as object; const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL; const { maxTTL } = dynamicSecretCfg; @@ -164,12 +164,14 @@ export const dynamicSecretLeaseServiceFactory = ({ const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; - const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); - const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString(); - const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object; + const decryptedStoredInput = JSON.parse( + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) + ) as object; const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL; const { maxTTL } = dynamicSecretCfg; @@ -229,12 +231,14 @@ export const dynamicSecretLeaseServiceFactory = ({ const dynamicSecretCfg = dynamicSecretLease.dynamicSecret; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; - const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); - const decryptedStoredInputJson = kmsDecryptor({ cipherTextBlob: dynamicSecretCfg.encryptedConfig }).toString(); - const decryptedStoredInput = JSON.parse(decryptedStoredInputJson) as object; + const decryptedStoredInput = JSON.parse( + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) + ) as object; const revokeResponse = await selectedProvider .revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId) diff --git a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts index 508c263ee..1aef3cc86 100644 --- a/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts +++ b/backend/src/ee/services/dynamic-secret/dynamic-secret-service.ts @@ -1,11 +1,11 @@ import { ForbiddenError, subject } from "@casl/ability"; +import { SecretKeyEncoding } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; -import { TKmsServiceFactory } from "@app/services/kms/kms-service"; -import { KmsDataKey } from "@app/services/kms/kms-types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; @@ -34,7 +34,6 @@ type TDynamicSecretServiceFactoryDep = { folderDAL: Pick; projectDAL: Pick; permissionService: Pick; - kmsService: Pick; }; export type TDynamicSecretServiceFactory = ReturnType; @@ -47,8 +46,7 @@ export const dynamicSecretServiceFactory = ({ dynamicSecretProviders, permissionService, dynamicSecretQueueService, - projectDAL, - kmsService + projectDAL }: TDynamicSecretServiceFactoryDep) => { const create = async ({ path, @@ -98,16 +96,16 @@ export const dynamicSecretServiceFactory = ({ const isConnected = await selectedProvider.validateConnection(provider.inputs); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); - const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); - const encryptedConfig = secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob; + const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs)); const dynamicSecretCfg = await dynamicSecretDAL.create({ type: provider.type, version: 1, - encryptedConfig, + inputIV: encryptedInput.iv, + inputTag: encryptedInput.tag, + inputCiphertext: encryptedInput.ciphertext, + algorithm: encryptedInput.algorithm, + keyEncoding: encryptedInput.encoding, maxTTL, defaultTTL, folderId: folder.id, @@ -167,28 +165,27 @@ export const dynamicSecretServiceFactory = ({ } const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; - const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } = - await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); - const dynamicSecretInputConfig = secretManagerDecryptor({ - cipherTextBlob: dynamicSecretCfg.encryptedConfig - }).toString(); - - const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object; + const decryptedStoredInput = JSON.parse( + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) + ) as object; const newInput = { ...decryptedStoredInput, ...(inputs || {}) }; const updatedInput = await selectedProvider.validateProviderInputs(newInput); const isConnected = await selectedProvider.validateConnection(newInput); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); - const encryptedConfig = secretManagerEncryptor({ - plainText: Buffer.from(JSON.stringify(updatedInput)) - }).cipherTextBlob; - + const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(updatedInput)); const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, { - encryptedConfig, + inputIV: encryptedInput.iv, + inputTag: encryptedInput.tag, + inputCiphertext: encryptedInput.ciphertext, + algorithm: encryptedInput.algorithm, + keyEncoding: encryptedInput.encoding, maxTTL, defaultTTL, name: newName ?? name, @@ -289,16 +286,14 @@ export const dynamicSecretServiceFactory = ({ const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id }); if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" }); - const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); - - const dynamicSecretInputConfig = secretManagerDecryptor({ - cipherTextBlob: dynamicSecretCfg.encryptedConfig - }).toString(); - - const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object; + const decryptedStoredInput = JSON.parse( + infisicalSymmetricDecrypt({ + keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding, + ciphertext: dynamicSecretCfg.inputCiphertext, + tag: dynamicSecretCfg.inputTag, + iv: dynamicSecretCfg.inputIV + }) + ) as object; const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders]; const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object; return { ...dynamicSecretCfg, inputs: providerInputs }; diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 7401d8dcd..b942cdd83 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -23,6 +23,7 @@ export enum ProjectPermissionSub { IpAllowList = "ip-allowlist", Project = "workspace", Secrets = "secrets", + SecretFolders = "secret-folders", SecretRollback = "secret-rollback", SecretApproval = "secret-approval", SecretRotation = "secret-rotation", @@ -42,6 +43,10 @@ export type ProjectPermissionSet = ProjectPermissionActions, ProjectPermissionSub.Secrets | (ForcedSubject & SubjectFields) ] + | [ + ProjectPermissionActions, + ProjectPermissionSub.SecretFolders | (ForcedSubject & SubjectFields) + ] | [ProjectPermissionActions, ProjectPermissionSub.Role] | [ProjectPermissionActions, ProjectPermissionSub.Tags] | [ProjectPermissionActions, ProjectPermissionSub.Member] diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 4e0dd4e45..e8f80f020 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -635,7 +635,8 @@ export const registerRoutes = async ( projectUserMembershipRoleDAL, identityProjectMembershipRoleDAL, keyStore, - kmsService + kmsService, + projectBotDAL }); const projectEnvService = projectEnvServiceFactory({ @@ -677,8 +678,7 @@ export const registerRoutes = async ( permissionService, webhookDAL, projectEnvDAL, - projectDAL, - kmsService + projectDAL }); const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService }); @@ -988,9 +988,7 @@ export const registerRoutes = async ( queueService, dynamicSecretLeaseDAL, dynamicSecretProviders, - dynamicSecretDAL, - kmsService, - folderDAL + dynamicSecretDAL }); const dynamicSecretService = dynamicSecretServiceFactory({ projectDAL, @@ -1000,8 +998,7 @@ export const registerRoutes = async ( dynamicSecretProviders, folderDAL, permissionService, - licenseService, - kmsService + licenseService }); const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({ projectDAL, @@ -1011,8 +1008,7 @@ export const registerRoutes = async ( dynamicSecretLeaseDAL, dynamicSecretProviders, folderDAL, - licenseService, - kmsService + licenseService }); const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ auditLogDAL, diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index ab0557e93..a10a962cf 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -129,7 +129,11 @@ export const SanitizedRoleSchema = ProjectRolesSchema.extend({ }); export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({ - encryptedConfig: true + inputIV: true, + inputTag: true, + inputCiphertext: true, + keyEncoding: true, + algorithm: true }); export const SanitizedAuditLogStreamSchema = z.object({ diff --git a/backend/src/services/project-membership/project-membership-service.ts b/backend/src/services/project-membership/project-membership-service.ts index 45e3f9ab5..a03aec934 100644 --- a/backend/src/services/project-membership/project-membership-service.ts +++ b/backend/src/services/project-membership/project-membership-service.ts @@ -540,7 +540,7 @@ export const projectMembershipServiceFactory = ({ const project = await projectDAL.findById(projectId); if (!project) throw new BadRequestError({ message: "Project not found" }); - if (project.version !== ProjectVersion.V2) { + if (project.version === ProjectVersion.V1) { throw new BadRequestError({ message: "Please ask your project administrator to upgrade the project before leaving." }); diff --git a/backend/src/services/project/project-service.ts b/backend/src/services/project/project-service.ts index d1f393e7b..fd22349db 100644 --- a/backend/src/services/project/project-service.ts +++ b/backend/src/services/project/project-service.ts @@ -22,6 +22,7 @@ import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/id import { TKmsServiceFactory } from "../kms/kms-service"; import { TOrgDALFactory } from "../org/org-dal"; import { TOrgServiceFactory } from "../org/org-service"; +import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; @@ -74,6 +75,7 @@ type TProjectServiceFactoryDep = { licenseService: Pick; orgDAL: Pick; keyStore: Pick; + projectBotDAL: Pick; kmsService: Pick< TKmsServiceFactory, | "updateProjectSecretManagerKmsKey" @@ -106,7 +108,8 @@ export const projectServiceFactory = ({ certificateAuthorityDAL, certificateDAL, keyStore, - kmsService + kmsService, + projectBotDAL }: TProjectServiceFactoryDep) => { /* * Create workspace. Make user the admin @@ -206,7 +209,26 @@ export const projectServiceFactory = ({ tx ); - // const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey); + const { iv, tag, ciphertext, encoding, algorithm } = infisicalSymmetricEncypt(ghostUser.keys.plainPrivateKey); + + // 5. Create & a bot for the project + await projectBotDAL.create( + { + name: "Infisical Bot (Ghost)", + projectId: project.id, + tag, + iv, + encryptedProjectKey, + encryptedProjectKeyNonce: encryptedProjectKeyIv, + encryptedPrivateKey: ciphertext, + isActive: true, + publicKey: ghostUser.keys.publicKey, + senderId: ghostUser.user.id, + algorithm, + keyEncoding: encoding + }, + tx + ); // Find the ghost users latest key const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx); diff --git a/backend/src/services/secret-folder/secret-folder-fns.ts b/backend/src/services/secret-folder/secret-folder-fns.ts new file mode 100644 index 000000000..c8f7d885e --- /dev/null +++ b/backend/src/services/secret-folder/secret-folder-fns.ts @@ -0,0 +1,6 @@ +import { RawRule } from "@casl/ability"; + +import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; + +export const shouldCheckFolderPermission = (rules: RawRule[]) => + rules.some((rule) => (rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders)); diff --git a/backend/src/services/secret-folder/secret-folder-service.ts b/backend/src/services/secret-folder/secret-folder-service.ts index 9d6c29454..45b5205b2 100644 --- a/backend/src/services/secret-folder/secret-folder-service.ts +++ b/backend/src/services/secret-folder/secret-folder-service.ts @@ -11,6 +11,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TSecretFolderDALFactory } from "./secret-folder-dal"; +import { shouldCheckFolderPermission } from "./secret-folder-fns"; import { TCreateFolderDTO, TDeleteFolderDTO, @@ -57,10 +58,21 @@ export const secretFolderServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); + + // we do this because we've split Secret and SecretFolder resources + // previously, if one can create/update/read/delete secrets then they can do the same for folders + // for backwards compatibility, we handle authorization only when SecretFolders subject is used + if (shouldCheckFolderPermission(permission.rules)) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.SecretFolders, { environment, secretPath }) + ); + } else { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Create, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } const env = await projectEnvDAL.findOne({ projectId, slug: environment }); if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" }); @@ -148,10 +160,20 @@ export const secretFolderServiceFactory = ({ ); folders.forEach(({ environment, path: secretPath }) => { - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); + // we do this because we've split Secret and SecretFolder resources + // previously, if one can create/update/read/delete secrets then they can do the same for folders + // for backwards compatibility, we handle authorization only when SecretFolders subject is used + if (shouldCheckFolderPermission(permission.rules)) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.SecretFolders, { environment, secretPath }) + ); + } else { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } }); const result = await folderDAL.transaction(async (tx) => @@ -243,10 +265,21 @@ export const secretFolderServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Edit, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); + + // we do this because we've split Secret and SecretFolder resources + // previously, if one can create/update/read/delete secrets then they can do the same for folders + // for backwards compatibility, we handle authorization differently only when SecretFolders subject is used + if (shouldCheckFolderPermission(permission.rules)) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.SecretFolders, { environment, secretPath }) + ); + } else { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Edit, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" }); @@ -316,10 +349,21 @@ export const secretFolderServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Delete, - subject(ProjectPermissionSub.Secrets, { environment, secretPath }) - ); + + // we do this because we've split Secret and SecretFolder resources + // previously, if one can create/update/read/delete secrets then they can do the same for folders + // for backwards compatibility, we handle authorization differently only when SecretFolders subject is used + if (shouldCheckFolderPermission(permission.rules)) { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.SecretFolders, { environment, secretPath }) + ); + } else { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Delete, + subject(ProjectPermissionSub.Secrets, { environment, secretPath }) + ); + } const env = await projectEnvDAL.findOne({ projectId, slug: environment }); if (!env) throw new BadRequestError({ message: "Environment not found", name: "Create folder" }); diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 6626dff7f..28053f4b3 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -1133,7 +1133,7 @@ export const secretQueueFactory = ({ }); queueService.start(QueueName.SecretWebhook, async (job) => { - await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL, kmsService }); + await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL }); }); return { diff --git a/backend/src/services/webhook/webhook-fns.ts b/backend/src/services/webhook/webhook-fns.ts index 7f91ca2db..4690edba9 100644 --- a/backend/src/services/webhook/webhook-fns.ts +++ b/backend/src/services/webhook/webhook-fns.ts @@ -3,12 +3,12 @@ import crypto from "node:crypto"; import { AxiosError } from "axios"; import picomatch from "picomatch"; +import { SecretKeyEncoding, TWebhooks } from "@app/db/schemas"; import { request } from "@app/lib/config/request"; +import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; -import { TKmsServiceFactory } from "../kms/kms-service"; -import { KmsDataKey } from "../kms/kms-types"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TWebhookDALFactory } from "./webhook-dal"; @@ -16,12 +16,40 @@ import { WebhookType } from "./webhook-types"; const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000; -export const triggerWebhookRequest = async ( - { webhookSecretKey: secretKey, webhookUrl: url }: { webhookSecretKey?: string; webhookUrl: string }, - data: Record -) => { +export const decryptWebhookDetails = (webhook: TWebhooks) => { + const { keyEncoding, iv, encryptedSecretKey, tag, urlCipherText, urlIV, urlTag, url } = webhook; + + let decryptedSecretKey = ""; + let decryptedUrl = url; + + if (encryptedSecretKey) { + decryptedSecretKey = infisicalSymmetricDecrypt({ + keyEncoding: keyEncoding as SecretKeyEncoding, + ciphertext: encryptedSecretKey, + iv: iv as string, + tag: tag as string + }); + } + + if (urlCipherText) { + decryptedUrl = infisicalSymmetricDecrypt({ + keyEncoding: keyEncoding as SecretKeyEncoding, + ciphertext: urlCipherText, + iv: urlIV as string, + tag: urlTag as string + }); + } + + return { + secretKey: decryptedSecretKey, + url: decryptedUrl + }; +}; + +export const triggerWebhookRequest = async (webhook: TWebhooks, data: Record) => { const headers: Record = {}; const payload = { ...data, timestamp: Date.now() }; + const { secretKey, url } = decryptWebhookDetails(webhook); if (secretKey) { const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex"); @@ -96,7 +124,6 @@ export type TFnTriggerWebhookDTO = { webhookDAL: Pick; projectEnvDAL: Pick; projectDAL: Pick; - kmsService: Pick; }; // this is reusable function @@ -107,8 +134,7 @@ export const fnTriggerWebhook = async ({ projectId, webhookDAL, projectEnvDAL, - projectDAL, - kmsService + projectDAL }: TFnTriggerWebhookDTO) => { const webhooks = await webhookDAL.findAllWebhooks(projectId, environment); const toBeTriggeredHooks = webhooks.filter( @@ -118,20 +144,10 @@ export const fnTriggerWebhook = async ({ if (!toBeTriggeredHooks.length) return; logger.info("Secret webhook job started", { environment, secretPath, projectId }); const project = await projectDAL.findById(projectId); - const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ - projectId, - type: KmsDataKey.SecretManager - }); - const webhooksTriggered = await Promise.allSettled( - toBeTriggeredHooks.map((hook) => { - const webhookUrl = kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedUrl }).toString(); - const webhookSecretKey = hook.encryptedSecretKeyWithKms - ? kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedSecretKeyWithKms }).toString() - : undefined; - - return triggerWebhookRequest( - { webhookUrl, webhookSecretKey }, + toBeTriggeredHooks.map((hook) => + triggerWebhookRequest( + hook, getWebhookPayload("secrets.modified", { workspaceName: project.name, workspaceId: projectId, @@ -139,8 +155,8 @@ export const fnTriggerWebhook = async ({ secretPath, type: hook.type }) - ); - }) + ) + ) ); // filter hooks by status diff --git a/backend/src/services/webhook/webhook-service.ts b/backend/src/services/webhook/webhook-service.ts index 2698b1ea2..41dacd34b 100644 --- a/backend/src/services/webhook/webhook-service.ts +++ b/backend/src/services/webhook/webhook-service.ts @@ -1,15 +1,15 @@ import { ForbiddenError } from "@casl/ability"; +import { TWebhooksInsert } from "@app/db/schemas"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; +import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { BadRequestError } from "@app/lib/errors"; -import { TKmsServiceFactory } from "../kms/kms-service"; -import { KmsDataKey } from "../kms/kms-types"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TWebhookDALFactory } from "./webhook-dal"; -import { getWebhookPayload, triggerWebhookRequest } from "./webhook-fns"; +import { decryptWebhookDetails, getWebhookPayload, triggerWebhookRequest } from "./webhook-fns"; import { TCreateWebhookDTO, TDeleteWebhookDTO, @@ -23,7 +23,6 @@ type TWebhookServiceFactoryDep = { projectEnvDAL: TProjectEnvDALFactory; projectDAL: Pick; permissionService: Pick; - kmsService: Pick; }; export type TWebhookServiceFactory = ReturnType; @@ -32,8 +31,7 @@ export const webhookServiceFactory = ({ webhookDAL, projectEnvDAL, permissionService, - projectDAL, - kmsService + projectDAL }: TWebhookServiceFactoryDep) => { const createWebhook = async ({ actor, @@ -58,28 +56,33 @@ export const webhookServiceFactory = ({ const env = await projectEnvDAL.findOne({ projectId, slug: environment }); if (!env) throw new BadRequestError({ message: "Env not found" }); - const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({ - projectId, - type: KmsDataKey.SecretManager - }); - - const encryptedSecretKeyWithKms = webhookSecretKey - ? secretManagerEncryptor({ - plainText: Buffer.from(webhookSecretKey) - }).cipherTextBlob - : null; - const encryptedUrl = secretManagerEncryptor({ - plainText: Buffer.from(webhookUrl) - }).cipherTextBlob; - - const webhook = await webhookDAL.create({ - encryptedUrl, - encryptedSecretKeyWithKms, + const insertDoc: TWebhooksInsert = { + url: "", // deprecated - we are moving away from plaintext URLs envId: env.id, isDisabled: false, secretPath: secretPath || "/", type - }); + }; + + if (webhookSecretKey) { + const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookSecretKey); + insertDoc.encryptedSecretKey = ciphertext; + insertDoc.iv = iv; + insertDoc.tag = tag; + insertDoc.algorithm = algorithm; + insertDoc.keyEncoding = encoding; + } + + if (webhookUrl) { + const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookUrl); + insertDoc.urlCipherText = ciphertext; + insertDoc.urlIV = iv; + insertDoc.urlTag = tag; + insertDoc.algorithm = algorithm; + insertDoc.keyEncoding = encoding; + } + + const webhook = await webhookDAL.create(insertDoc); return { ...webhook, projectId, environment: env }; }; @@ -133,18 +136,9 @@ export const webhookServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); let webhookError: string | undefined; - const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ - projectId: project.id, - type: KmsDataKey.SecretManager - }); - const webhookUrl = kmsDataKeyDecryptor({ cipherTextBlob: webhook.encryptedUrl }).toString(); - const webhookSecretKey = webhook.encryptedSecretKeyWithKms - ? kmsDataKeyDecryptor({ cipherTextBlob: webhook.encryptedSecretKeyWithKms }).toString() - : undefined; - try { await triggerWebhookRequest( - { webhookUrl, webhookSecretKey }, + webhook, getWebhookPayload("test", { workspaceName: project.name, workspaceId: webhook.projectId, @@ -183,15 +177,11 @@ export const webhookServiceFactory = ({ ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks); const webhooks = await webhookDAL.findAllWebhooks(projectId, environment, secretPath); - const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.SecretManager, - projectId - }); return webhooks.map((w) => { - const decryptedUrl = kmsDataKeyDecryptor({ cipherTextBlob: w.encryptedUrl }).toString(); + const { url } = decryptWebhookDetails(w); return { ...w, - url: decryptedUrl + url }; }); }; diff --git a/docs/documentation/platform/kms/aws-hsm.mdx b/docs/documentation/platform/kms/aws-hsm.mdx new file mode 100644 index 000000000..e7bd03fd1 --- /dev/null +++ b/docs/documentation/platform/kms/aws-hsm.mdx @@ -0,0 +1,82 @@ +--- +title: "AWS CloudHSM" +description: "Learn how to manage encryption using AWS CloudHSM" +--- + +This guide provides instructions on securing Infisical project secrets using AWS CloudHSM. +Integration with AWS CloudHSM is achieved by configuring it as a custom key store for AWS KMS. +Follow the steps below to set up AWS KMS with AWS CloudHSM as the custom key store. + +## Prepare AWS CloudHSM Cluster + +Before you get started, you'll need to configure a AWS CloudHSM cluster which meets the following criteria: + +- The cluster must be active. +- The cluster must not be associated with any other AWS KMS custom key store. +- The cluster must be configured with private subnets in at least two Availability Zones in the Region. +- The security group for the cluster must include inbound and outbound rules that allow TCP traffic on ports 2223-2225. +- The cluster must contain at least two active HSMs in different Availability Zones. + +For more details on setting up your cluster, refer to the following [AWS documentation](https://docs.aws.amazon.com/kms/latest/developerguide/create-keystore.html#before-keystore). + +## Set Up AWS KMS Custom Key Store + +To set up an AWS KMS custom key store with AWS CloudHSM, you will need the following: + +- The trust anchor certificate of your AWS CloudHSM cluster. +- A `kmsuser` user in the AWS CloudHSM cluster with the crypto-user role. + + + + In the AWS console, head over to `AWS KMS` > `AWS CloudHSM key stores` and click **Create key store**. + + + Input the custom key store name. ![Set key store name](../../../images/platform/kms/aws-hsm/create-key-store-name.png) + + + Select the AWS CloudHSM cluster. You should be able to select the cluster if it meets the required criteria mentioned above. + ![Set key store cluster](../../../images/platform/kms/aws-hsm/create-key-store-cluster.png) + + + Upload your CloudHSM's cluster trust anchor certificate file. + ![Set key store cert](../../../images/platform/kms/aws-hsm/create-key-store-cert.png) + + + Input the password of the `kmsuser` crypto-user in your cluster. + ![Set key store password](../../../images/platform/kms/aws-hsm/create-key-store-password.png) + + + Proceed with creating the AWS CloudHSM key store. + + + +For more details, refer to the following [AWS documentation](https://docs.aws.amazon.com/kms/latest/developerguide/create-keystore.html#create-keystore-console). + +## Create AWS KMS Key +Next, you'll need to create a AWS KMS key where you will set the key store you created previously. + + + + In your AWS console, proceed to `AWS KMS` > `Customer managed keys` and click **Create**. + + + Set Key type to `Symmetric` and Key usage to `Encrypt and decrypt`. + ![Set key options 1](../../../images/platform/kms/aws-hsm/create-kms-key-1.png) + + + In the advanced options, for the Key material origin field, select `AWS CloudHSM key store`. Then, click next. + ![Set key options 2](../../../images/platform/kms/aws-hsm/create-kms-key-2.png) + + + Select the AWS CloudHSM key store you created earlier. + ![Select HSM 1](../../../images/platform/kms/aws-hsm/create-kms-select-hsm.png) + + + Proceed with creating the AWS KMS Key. + + + +## Connect Infisical to AWS KMS Key + +You should now have an AWS KMS that has a custom key store set to AWS CloudHSM. +To secure project resources, you will need to add this AWS KMS to your Infisical organization. To learn how, refer to the documentation [here](./aws-kms). \ No newline at end of file diff --git a/docs/documentation/platform/kms/aws-kms.mdx b/docs/documentation/platform/kms/aws-kms.mdx index 0bf33bb0d..14769f301 100644 --- a/docs/documentation/platform/kms/aws-kms.mdx +++ b/docs/documentation/platform/kms/aws-kms.mdx @@ -1,24 +1,26 @@ --- -title: "AWS Key Management Service (KMS)" +title: "AWS Key Management Service" description: "Learn how to manage encryption using AWS KMS" --- -You can configure your projects to use AWS KMS keys for encryption, enhancing the security and management of your secrets. +To enhance the security of your Infisical projects, you can now encrypt your secrets using an external Key Management Service (KMS). +When external KMS is configured for your project, all encryption and decryption operations will be handled by the chosen KMS. +This guide will walk you through the steps needed to configure external KMS support with AWS KMS. ## Prerequisites -Depending on the AWS Authentication Method you intend to use, you will have to do either of the following: +Before you begin, you'll first need to choose a method of authentication with AWS from below. - + 1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console. ![IAM Role Creation](../../images/integrations/aws/integration-aws-iam-assume-role.png) 2. Select **AWS Account** as the **Trusted Entity Type**. - 3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead. - 4. Optionally, enable **Require external ID** and enter your **project ID** to further enhance security. + 3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If you are self-hosting, provide the AWS account number where Infisical is hosted. + 4. Optionally, enable **Require external ID** and enter your Infisical **project ID** to further enhance security. Use the following custom policy to grant the minimum permissions required by Infisical to integrate with AWS KMS @@ -44,7 +46,7 @@ Depending on the AWS Authentication Method you intend to use, you will have to d - + Navigate to your IAM user and add a policy to grant the following permissions: ```json { @@ -68,10 +70,10 @@ Depending on the AWS Authentication Method you intend to use, you will have to d ## Setup AWS KMS in the Organization Settings -Follow these steps to set up AWS KMS for your organization: +Next, you will need to follow the steps listed below to add AWS KMS for your organization. - + ![Open encryption org settings](../../../images/platform/kms/aws/encryption-org-settings.png) @@ -83,7 +85,8 @@ Follow these steps to set up AWS KMS for your organization: Choose 'AWS KMS' from the list of encryption providers. - Fill in the required details for AWS KMS: + Selecting AWS as the provider will require you input the following fields. + Name for referencing the AWS KMS key within the organization. @@ -96,7 +99,7 @@ Follow these steps to set up AWS KMS for your organization: Authentication mode for AWS, either "AWS Assume Role" or "Access Key". - + ARN of the AWS role to assume for providing Infisical access to the AWS KMS Key (required if Authentication Mode is "AWS Assume Role") @@ -104,11 +107,11 @@ Follow these steps to set up AWS KMS for your organization: Custom identifier for additional validation during role assumption. - + AWS IAM Access Key ID for authentication (required if Authentication Mode is "Access Key"). - + AWS IAM Secret Access Key for authentication (required if Authentication Mode is "Access Key"). @@ -126,14 +129,14 @@ Follow these steps to set up AWS KMS for your organization: -You now have an AWS KMS Key configured at the organization level. You can assign these keys to existing projects via the Project Settings page. +You now have an AWS KMS Key configured at the organization level. You can assign these AWS KMS keys to existing Infisical projects by visiting the 'Project Settings' page. ## Assign AWS KMS Key to an Existing Project -Follow these steps to assign an AWS KMS key to a project: +To assign the AWS KMS key you added to your organization, follow the steps below. - + ![Open encryption project settings](../../../images/platform/kms/aws/encryption-project-settings.png) @@ -143,6 +146,6 @@ Follow these steps to assign an AWS KMS key to a project: Choose the AWS KMS key you configured earlier. - Save the changes to apply the new encryption settings to your project. + Once you have selected the KMS of choice, click save. diff --git a/docs/images/platform/kms/aws-hsm/create-key-store-cert.png b/docs/images/platform/kms/aws-hsm/create-key-store-cert.png new file mode 100644 index 000000000..c07c2a895 Binary files /dev/null and b/docs/images/platform/kms/aws-hsm/create-key-store-cert.png differ diff --git a/docs/images/platform/kms/aws-hsm/create-key-store-cluster.png b/docs/images/platform/kms/aws-hsm/create-key-store-cluster.png new file mode 100644 index 000000000..245b11d98 Binary files /dev/null and b/docs/images/platform/kms/aws-hsm/create-key-store-cluster.png differ diff --git a/docs/images/platform/kms/aws-hsm/create-key-store-name.png b/docs/images/platform/kms/aws-hsm/create-key-store-name.png new file mode 100644 index 000000000..1b47604b8 Binary files /dev/null and b/docs/images/platform/kms/aws-hsm/create-key-store-name.png differ diff --git a/docs/images/platform/kms/aws-hsm/create-key-store-password.png b/docs/images/platform/kms/aws-hsm/create-key-store-password.png new file mode 100644 index 000000000..5ae84394d Binary files /dev/null and b/docs/images/platform/kms/aws-hsm/create-key-store-password.png differ diff --git a/docs/images/platform/kms/aws-hsm/create-kms-key-1.png b/docs/images/platform/kms/aws-hsm/create-kms-key-1.png new file mode 100644 index 000000000..a5bb700c9 Binary files /dev/null and b/docs/images/platform/kms/aws-hsm/create-kms-key-1.png differ diff --git a/docs/images/platform/kms/aws-hsm/create-kms-key-2.png b/docs/images/platform/kms/aws-hsm/create-kms-key-2.png new file mode 100644 index 000000000..78f3926d8 Binary files /dev/null and b/docs/images/platform/kms/aws-hsm/create-kms-key-2.png differ diff --git a/docs/images/platform/kms/aws-hsm/create-kms-select-hsm.png b/docs/images/platform/kms/aws-hsm/create-kms-select-hsm.png new file mode 100644 index 000000000..925bf6928 Binary files /dev/null and b/docs/images/platform/kms/aws-hsm/create-kms-select-hsm.png differ diff --git a/docs/mint.json b/docs/mint.json index 97e4bbf13..537ea55b5 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -158,7 +158,8 @@ "group": "Key Management", "pages": [ "documentation/platform/kms/overview", - "documentation/platform/kms/aws-kms" + "documentation/platform/kms/aws-kms", + "documentation/platform/kms/aws-hsm" ] }, "documentation/platform/secret-sharing" diff --git a/frontend/src/components/v2/UpgradeOverlay/UpgradeOverlay.tsx b/frontend/src/components/v2/UpgradeOverlay/UpgradeOverlay.tsx deleted file mode 100644 index f6d8d71c7..000000000 --- a/frontend/src/components/v2/UpgradeOverlay/UpgradeOverlay.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { useRouter } from "next/router"; - -import { Spinner } from "@app/components/v2"; -import { useWorkspace } from "@app/context"; -import { useToggle } from "@app/hooks"; -import { useGetUpgradeProjectStatus } from "@app/hooks/api/workspace/queries"; -import { ProjectVersion } from "@app/hooks/api/workspace/types"; - -export const UpgradeOverlay = () => { - const router = useRouter(); - const { currentWorkspace } = useWorkspace(); - const [isUpgrading, setIsUpgrading] = useToggle(false); - - const isProjectRoute = router.pathname.includes("/project"); - - const { isLoading: isUpgradeStatusLoading } = useGetUpgradeProjectStatus({ - projectId: currentWorkspace?.id ?? "", - enabled: isProjectRoute && currentWorkspace && currentWorkspace.version === ProjectVersion.V1, - refetchInterval: 5_000, - onSuccess: (data) => { - if (!data) return; - - if (data.status !== "IN_PROGRESS") { - setIsUpgrading.off(); - } else if (data?.status === "IN_PROGRESS") { - setIsUpgrading.on(); - } - } - }); - - // make sure only to display this on /project routes - if (!currentWorkspace || !isProjectRoute) { - return null; - } - - return !isUpgradeStatusLoading && isUpgrading ? ( -
- -
-
Please wait
- Upgrading your project... -
-
- ) : ( -
- ); -}; diff --git a/frontend/src/components/v2/UpgradeOverlay/index.tsx b/frontend/src/components/v2/UpgradeOverlay/index.tsx deleted file mode 100644 index 1a74fb6f1..000000000 --- a/frontend/src/components/v2/UpgradeOverlay/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { UpgradeOverlay } from "./UpgradeOverlay"; diff --git a/frontend/src/components/v2/UpgradeProjectAlert/UpgradeProjectAlert.tsx b/frontend/src/components/v2/UpgradeProjectAlert/UpgradeProjectAlert.tsx deleted file mode 100644 index ebcba1d87..000000000 --- a/frontend/src/components/v2/UpgradeProjectAlert/UpgradeProjectAlert.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { useCallback, useState } from "react"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import { faWarning } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { twMerge } from "tailwind-merge"; - -import { createNotification } from "@app/components/notifications"; -import { useProjectPermission } from "@app/context"; -import { useGetUpgradeProjectStatus, useUpgradeProject } from "@app/hooks/api"; -import { Workspace } from "@app/hooks/api/types"; -import { workspaceKeys } from "@app/hooks/api/workspace/queries"; -import { ProjectVersion } from "@app/hooks/api/workspace/types"; -import { queryClient } from "@app/reactQuery"; - -import { Button } from "../Button"; -import { Tooltip } from "../Tooltip"; - -export type UpgradeProjectAlertProps = { - project: Workspace; - transparent?: boolean; -}; - -export const UpgradeProjectAlert = ({ - project, - transparent -}: UpgradeProjectAlertProps): JSX.Element | null => { - const router = useRouter(); - const { hasProjectRole } = useProjectPermission(); - const upgradeProject = useUpgradeProject(); - const [currentStatus, setCurrentStatus] = useState(null); - const [isUpgrading, setIsUpgrading] = useState(false); - - const isProjectAdmin = hasProjectRole("admin"); - - const { - data: projectStatus, - isLoading: statusIsLoading, - refetch: manualProjectStatusRefetch - } = useGetUpgradeProjectStatus({ - projectId: project.id, - enabled: isProjectAdmin && project.version === ProjectVersion.V1, - refetchInterval: 5_000, - onSuccess: (data) => { - if (!isProjectAdmin) { - return; - } - - if (data && data?.status !== null) { - if (data.status === "IN_PROGRESS") { - setCurrentStatus("Your upgrade is being processed."); - } else if (data.status === "FAILED") { - setCurrentStatus("Upgrade failed, please try again."); - } - } - - if (currentStatus !== null && data?.status === null) { - queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace); - router.reload(); - } - } - }); - - const onUpgradeProject = useCallback(async () => { - if (upgradeProject.isLoading) { - return; - } - setIsUpgrading(true); - const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); - - if (!PRIVATE_KEY) { - createNotification({ - type: "error", - text: "Private key not found" - }); - return; - } - - await upgradeProject.mutateAsync({ - projectId: project.id, - privateKey: PRIVATE_KEY - }); - - manualProjectStatusRefetch(); - - setTimeout(() => setIsUpgrading(false), 5_000); - }, []); - - const isLoading = - isUpgrading || - ((upgradeProject.isLoading || - currentStatus !== null || - (currentStatus === null && statusIsLoading)) && - projectStatus?.status !== "FAILED"); - - if (project.version !== ProjectVersion.V1) return null; - - if (transparent) { - return ( - - ); - } - - return ( -
- -
- Upgrade your project - {isProjectAdmin ? ( - <> -

- Upgrade your project version to continue receiving the latest improvements and - patches. -

- - - Learn more - - - - ) : ( - <> -

- Please ask a project admin to upgrade the project. -
- Upgrading the project version is required to continue receiving the latest - improvements and patches. -

- - - Learn more - - - - )} - {currentStatus &&

Status: {currentStatus}

} -
-
- - - -
-
- ); -}; diff --git a/frontend/src/components/v2/UpgradeProjectAlert/index.tsx b/frontend/src/components/v2/UpgradeProjectAlert/index.tsx deleted file mode 100644 index ab67f86cf..000000000 --- a/frontend/src/components/v2/UpgradeProjectAlert/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { UpgradeProjectAlert } from "./UpgradeProjectAlert"; diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index fc163da13..557b3cd17 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -21,6 +21,7 @@ export enum ProjectPermissionSub { IpAllowList = "ip-allowlist", Workspace = "workspace", Secrets = "secrets", + SecretFolders = "secret-folders", SecretRollback = "secret-rollback", SecretApproval = "secret-approval", SecretRotation = "secret-rotation", diff --git a/frontend/src/hooks/api/secretApprovalRequest/queries.tsx b/frontend/src/hooks/api/secretApprovalRequest/queries.tsx index d990797b6..a15316018 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/queries.tsx +++ b/frontend/src/hooks/api/secretApprovalRequest/queries.tsx @@ -194,7 +194,6 @@ const fetchSecretApprovalRequestDetails = async ({ export const useGetSecretApprovalRequestDetails = ({ id, - decryptKey, options = {} }: TGetSecretApprovalRequestDetails & { options?: Omit< @@ -210,7 +209,7 @@ export const useGetSecretApprovalRequestDetails = ({ useQuery({ queryKey: secretApprovalRequestKeys.detail({ id }), queryFn: () => fetchSecretApprovalRequestDetails({ id }), - enabled: Boolean(id && decryptKey) && (options?.enabled ?? true) + enabled: Boolean(id) && (options?.enabled ?? true) }); const fetchSecretApprovalRequestCount = async ({ workspaceId }: TGetSecretApprovalRequestCount) => { diff --git a/frontend/src/hooks/api/secretApprovalRequest/types.ts b/frontend/src/hooks/api/secretApprovalRequest/types.ts index 9d9e46a7f..c8a685115 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/types.ts +++ b/frontend/src/hooks/api/secretApprovalRequest/types.ts @@ -1,4 +1,3 @@ -import { UserWsKeyPair } from "../keys/types"; import { TSecretApprovalPolicy } from "../secretApproval/types"; import { SecretV3Raw } from "../secrets/types"; import { WsTag } from "../tags/types"; @@ -110,7 +109,6 @@ export type TGetSecretApprovalRequestCount = { export type TGetSecretApprovalRequestDetails = { id: string; - decryptKey: UserWsKeyPair; }; export type TUpdateSecretApprovalReviewStatusDTO = { diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index f5f8b5449..87d4c8458 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -286,12 +286,12 @@ export const AppLayout = ({ children }: LayoutProps) => { // eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected. await new Promise((resolve) => setTimeout(resolve, 2_000)); - createNotification({ text: "Workspace created", type: "success" }); + createNotification({ text: "Project created", type: "success" }); handlePopUpClose("addNewWs"); router.push(`/project/${newProjectId}/secrets/overview`); } catch (err) { console.error(err); - createNotification({ text: "Failed to create workspace", type: "error" }); + createNotification({ text: "Failed to create project", type: "error" }); } }; diff --git a/frontend/src/pages/org/[id]/overview/index.tsx b/frontend/src/pages/org/[id]/overview/index.tsx index 251c36a26..a6256f596 100644 --- a/frontend/src/pages/org/[id]/overview/index.tsx +++ b/frontend/src/pages/org/[id]/overview/index.tsx @@ -565,11 +565,11 @@ const OrganizationPage = withPermission( await new Promise((resolve) => setTimeout(resolve, 2_000)); handlePopUpClose("addNewWs"); - createNotification({ text: "Workspace created", type: "success" }); + createNotification({ text: "Project created", type: "success" }); router.push(`/project/${newProjectId}/secrets/overview`); } catch (err) { console.error(err); - createNotification({ text: "Failed to create workspace", type: "error" }); + createNotification({ text: "Failed to create project", type: "error" }); } }; diff --git a/frontend/src/views/Org/Types/index.ts b/frontend/src/views/Org/Types/index.ts index 76f47c6f2..8fa062aa1 100644 --- a/frontend/src/views/Org/Types/index.ts +++ b/frontend/src/views/Org/Types/index.ts @@ -1,3 +1,3 @@ -import { TabSections, isTabSection } from "./TabSections"; +import { isTabSection,TabSections } from "./TabSections"; -export { TabSections, isTabSection }; +export { isTabSection,TabSections }; diff --git a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserAddToProjectModal.tsx b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserAddToProjectModal.tsx index 2cef4f5f3..713411ec2 100644 --- a/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserAddToProjectModal.tsx +++ b/frontend/src/views/Org/UserPage/components/UserProjectsSection/UserAddToProjectModal.tsx @@ -58,7 +58,7 @@ export const UserAddToProjectModal = ({ membershipId, popUp, handlePopUpToggle } return (workspaces || []).filter( ({ id, orgId: projectOrgId, version }) => - !wsWorkspaceIds.has(id) && projectOrgId === currentOrg?.id && version === ProjectVersion.V2 + !wsWorkspaceIds.has(id) && projectOrgId === currentOrg?.id && version !== ProjectVersion.V1 ); }, [workspaces, projectMemberships]); diff --git a/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx b/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx index cd8137d8b..8a5fe705a 100644 --- a/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx +++ b/frontend/src/views/Project/MembersPage/components/MembersTab/components/AddMemberModal.tsx @@ -74,19 +74,12 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => { decryptKey: wsKey, members: [{ orgMembershipId, userPublicKey: orgUser.user.publicKey }] }); - } else if (currentWorkspace.version === ProjectVersion.V2) { + } else { await addUserToWorkspaceNonE2EE({ projectId: workspaceId, usernames: [orgUser.user.username], orgId }); - } else { - createNotification({ - text: "Failed to add user to project, unknown project type", - type: "error" - }); - - return; } createNotification({ text: "Successfully added user to the project", diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts index 0b534347d..7e27bde6b 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils.ts @@ -36,6 +36,7 @@ export const formSchema = z.object({ permissions: z .object({ secrets: z.record(multiEnvPermissionSchema).optional(), + "secret-folders": generalPermissionSchema.optional(), member: generalPermissionSchema, groups: generalPermissionSchema, identity: generalPermissionSchema, @@ -158,7 +159,7 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => { Object.entries(formVal || {}).forEach(([rule, actions]) => { if (rule === "secrets") { multiEnvForm2Api(permissions, JSON.parse(JSON.stringify(actions || {})), rule); - } else { + } else if (actions) { Object.entries(actions).forEach(([action, isAllowed]) => { if (isAllowed) { permissions.push({ subject: rule, action }); diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionSecretFoldersRow.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionSecretFoldersRow.tsx new file mode 100644 index 000000000..eb0353b73 --- /dev/null +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionSecretFoldersRow.tsx @@ -0,0 +1,71 @@ +import { Control, UseFormSetValue, useWatch } from "react-hook-form"; + +import { Select, SelectItem, Td, Tr } from "@app/components/v2"; +import { ProjectPermissionSub } from "@app/context"; +import { TFormSchema } from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils"; + +type Props = { + isEditable: boolean; + setValue: UseFormSetValue; + control: Control; +}; + +enum Permission { + SameAsSecrets = "same-as-secrets", + ReadOnly = "read-only" +} + +export const RowPermissionSecretFoldersRow = ({ isEditable, setValue, control }: Props) => { + const formName = ProjectPermissionSub.SecretFolders; + const rule = useWatch({ + control, + name: `permissions.${formName}` + }); + + const selectedPermissionCategory = + rule !== undefined ? Permission.ReadOnly : Permission.SameAsSecrets; + + const handlePermissionChange = (val: Permission) => { + if (!val) return; + switch (val) { + case Permission.SameAsSecrets: { + setValue(`permissions.${formName}`, undefined, { shouldDirty: true }); + break; + } + // Read-only + default: + setValue( + `permissions.${formName}`, + { + read: true, + edit: false, + create: false, + delete: false + }, + { + shouldDirty: true + } + ); + break; + } + }; + + return ( + + + Secret Folders + + + + + ); +}; diff --git a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx index 11c47a501..e56c3d44e 100644 --- a/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/views/Project/RolePage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -13,6 +13,7 @@ import { } from "@app/views/Project/RolePage/components/RolePermissionsSection/ProjectRoleModifySection.utils"; import { RolePermissionRow } from "./RolePermissionRow"; +import { RowPermissionSecretFoldersRow } from "./RolePermissionSecretFoldersRow"; import { RowPermissionSecretsRow } from "./RolePermissionSecretsRow"; const SINGLE_PERMISSION_LIST = [ @@ -177,6 +178,11 @@ export const RolePermissionsSection = ({ roleSlug }: Props) => { getValue={getValues} control={control} /> + {SINGLE_PERMISSION_LIST.map((permission) => { return ( { const { user: userSession } = useUser(); - const { data: decryptFileKey } = useGetUserWsKey(workspaceId); const { data: secretApprovalRequestDetails, isSuccess: isSecretApprovalRequestSuccess, isLoading: isSecretApprovalRequestLoading } = useGetSecretApprovalRequestDetails({ - id: approvalRequestId, - decryptKey: decryptFileKey! + id: approvalRequestId }); const { diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx index 855fd393e..705234f63 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx @@ -44,7 +44,12 @@ import { Tooltip, UpgradePlanModal } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useSubscription } from "@app/context"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useSubscription +} from "@app/context"; import { usePopUp } from "@app/hooks"; import { useCreateFolder, useDeleteSecretBatch, useMoveSecrets } from "@app/hooks/api"; import { fetchProjectSecrets } from "@app/hooks/api/secrets/queries"; @@ -121,6 +126,12 @@ export const ActionBar = ({ const { reset: resetSelectedSecret } = useSelectedSecretActions(); const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length); + const { permission } = useProjectPermission(); + + const shouldCheckFolderPermission = permission.rules.some((rule) => + (rule.subject as ProjectPermissionSub[]).includes(ProjectPermissionSub.SecretFolders) + ); + const debouncedOnSearch = debounce(onSearchChange, 500); const handleFolderCreate = async (folderName: string) => { @@ -411,7 +422,12 @@ export const ActionBar = ({
{(isAllowed) => (