mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 12:27:28 +00:00
feat: added logic for webhook and dynamic secret to use the kms encryption
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { SecretType, TableName } from "../schemas";
|
import { SecretEncryptionAlgo, SecretKeyEncoding, SecretType, TableName } from "../schemas";
|
||||||
import { createJunctionTable, createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
import { createJunctionTable, createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
@@ -143,6 +143,32 @@ export async function up(knex: Knex): Promise<void> {
|
|||||||
t.foreign("rotationId").references("id").inTable(TableName.SecretRotation).onDelete("CASCADE");
|
t.foreign("rotationId").references("id").inTable(TableName.SecretRotation).onDelete("CASCADE");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
await knex.schema.alterTable(TableName.Webhook, (t) => {
|
||||||
|
if (!hasEncryptedWebhookSecretKey) t.binary("encryptedSecretKeyWithKms");
|
||||||
|
if (!hasEncryptedWebhookUrl) t.binary("encryptedUrl");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
|
||||||
|
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");
|
||||||
|
const hasEncryptedConfig = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedConfig");
|
||||||
|
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
|
||||||
|
if (hasInputIV) t.string("inputIV").alter();
|
||||||
|
if (hasInputCipherText) t.text("inputCiphertext").alter();
|
||||||
|
if (hasInputTag) t.string("inputTag").alter();
|
||||||
|
if (hasAlgorithm) t.string("algorithm").defaultTo(SecretEncryptionAlgo.AES_256_GCM).alter();
|
||||||
|
if (hasKeyEncoding) t.string("keyEncoding").defaultTo(SecretKeyEncoding.UTF8).alter();
|
||||||
|
if (!hasEncryptedConfig) t.binary("encryptedConfig");
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(knex: Knex): Promise<void> {
|
export async function down(knex: Knex): Promise<void> {
|
||||||
@@ -177,4 +203,19 @@ export async function down(knex: Knex): Promise<void> {
|
|||||||
if (hasEncryptedAwsIamAssumRole) t.dropColumn("encryptedAwsAssumeIamRoleArn");
|
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");
|
||||||
|
await knex.schema.alterTable(TableName.Webhook, (t) => {
|
||||||
|
if (hasEncryptedWebhookSecretKey) t.dropColumn("encryptedSecretKeyWithKms");
|
||||||
|
if (hasEncryptedWebhookUrl) t.dropColumn("encryptedUrl");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await knex.schema.hasTable(TableName.DynamicSecret)) {
|
||||||
|
const hasEncryptedConfig = await knex.schema.hasColumn(TableName.DynamicSecret, "encryptedConfig");
|
||||||
|
await knex.schema.alterTable(TableName.DynamicSecret, (t) => {
|
||||||
|
if (hasEncryptedConfig) t.dropColumn("encryptedConfig");
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
import { TImmutableDBKeys } from "./models";
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
export const DynamicSecretsSchema = z.object({
|
export const DynamicSecretsSchema = z.object({
|
||||||
@@ -14,16 +16,17 @@ export const DynamicSecretsSchema = z.object({
|
|||||||
type: z.string(),
|
type: z.string(),
|
||||||
defaultTTL: z.string(),
|
defaultTTL: z.string(),
|
||||||
maxTTL: z.string().nullable().optional(),
|
maxTTL: z.string().nullable().optional(),
|
||||||
inputIV: z.string(),
|
inputIV: z.string().nullable().optional(),
|
||||||
inputCiphertext: z.string(),
|
inputCiphertext: z.string().nullable().optional(),
|
||||||
inputTag: z.string(),
|
inputTag: z.string().nullable().optional(),
|
||||||
algorithm: z.string().default("aes-256-gcm"),
|
algorithm: z.string().default("aes-256-gcm").nullable().optional(),
|
||||||
keyEncoding: z.string().default("utf8"),
|
keyEncoding: z.string().default("utf8").nullable().optional(),
|
||||||
folderId: z.string().uuid(),
|
folderId: z.string().uuid(),
|
||||||
status: z.string().nullable().optional(),
|
status: z.string().nullable().optional(),
|
||||||
statusDetails: z.string().nullable().optional(),
|
statusDetails: z.string().nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
encryptedConfig: zodBuffer.nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
|
export type TDynamicSecrets = z.infer<typeof DynamicSecretsSchema>;
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export const IntegrationAuthsSchema = z.object({
|
|||||||
awsAssumeIamRoleArnCipherText: z.string().nullable().optional(),
|
awsAssumeIamRoleArnCipherText: z.string().nullable().optional(),
|
||||||
awsAssumeIamRoleArnIV: z.string().nullable().optional(),
|
awsAssumeIamRoleArnIV: z.string().nullable().optional(),
|
||||||
awsAssumeIamRoleArnTag: z.string().nullable().optional(),
|
awsAssumeIamRoleArnTag: z.string().nullable().optional(),
|
||||||
encryptedAwsIamAssumRole: zodBuffer.nullable().optional(),
|
|
||||||
encryptedAccess: zodBuffer.nullable().optional(),
|
encryptedAccess: zodBuffer.nullable().optional(),
|
||||||
encryptedAccessId: zodBuffer.nullable().optional(),
|
encryptedAccessId: zodBuffer.nullable().optional(),
|
||||||
encryptedRefresh: zodBuffer.nullable().optional(),
|
encryptedRefresh: zodBuffer.nullable().optional(),
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { zodBuffer } from "@app/lib/zod";
|
||||||
|
|
||||||
import { TImmutableDBKeys } from "./models";
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
export const WebhooksSchema = z.object({
|
export const WebhooksSchema = z.object({
|
||||||
@@ -25,7 +27,9 @@ export const WebhooksSchema = z.object({
|
|||||||
urlCipherText: z.string().nullable().optional(),
|
urlCipherText: z.string().nullable().optional(),
|
||||||
urlIV: z.string().nullable().optional(),
|
urlIV: z.string().nullable().optional(),
|
||||||
urlTag: z.string().nullable().optional(),
|
urlTag: z.string().nullable().optional(),
|
||||||
type: z.string().default("general").nullable().optional()
|
type: z.string().default("general").nullable().optional(),
|
||||||
|
encryptedSecretKeyWithKms: zodBuffer.nullable().optional(),
|
||||||
|
encryptedUrl: zodBuffer.nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TWebhooks = z.infer<typeof WebhooksSchema>;
|
export type TWebhooks = z.infer<typeof WebhooksSchema>;
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
|
|||||||
db.ref("folderId").withSchema(TableName.DynamicSecret).as("dynFolderId"),
|
db.ref("folderId").withSchema(TableName.DynamicSecret).as("dynFolderId"),
|
||||||
db.ref("status").withSchema(TableName.DynamicSecret).as("dynStatus"),
|
db.ref("status").withSchema(TableName.DynamicSecret).as("dynStatus"),
|
||||||
db.ref("statusDetails").withSchema(TableName.DynamicSecret).as("dynStatusDetails"),
|
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("createdAt").withSchema(TableName.DynamicSecret).as("dynCreatedAt"),
|
||||||
db.ref("updatedAt").withSchema(TableName.DynamicSecret).as("dynUpdatedAt")
|
db.ref("updatedAt").withSchema(TableName.DynamicSecret).as("dynUpdatedAt")
|
||||||
);
|
);
|
||||||
@@ -71,7 +72,8 @@ export const dynamicSecretLeaseDALFactory = (db: TDbClient) => {
|
|||||||
status: doc.dynStatus,
|
status: doc.dynStatus,
|
||||||
statusDetails: doc.dynStatusDetails,
|
statusDetails: doc.dynStatusDetails,
|
||||||
createdAt: doc.dynCreatedAt,
|
createdAt: doc.dynCreatedAt,
|
||||||
updatedAt: doc.dynUpdatedAt
|
updatedAt: doc.dynUpdatedAt,
|
||||||
|
encryptedConfig: doc.dynEncryptedConfig
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||||
import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue";
|
import { DisableRotationErrors } from "@app/ee/services/secret-rotation/secret-rotation-queue";
|
||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
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 { 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 { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
|
||||||
import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
|
import { DynamicSecretStatus } from "../dynamic-secret/dynamic-secret-types";
|
||||||
@@ -14,6 +18,8 @@ type TDynamicSecretLeaseQueueServiceFactoryDep = {
|
|||||||
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">;
|
dynamicSecretLeaseDAL: Pick<TDynamicSecretLeaseDALFactory, "findById" | "deleteById" | "find" | "updateById">;
|
||||||
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">;
|
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findById" | "deleteById" | "updateById">;
|
||||||
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
|
dynamicSecretProviders: Record<DynamicSecretProviders, TDynamicProviderFns>;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
folderDAL: Pick<TSecretFolderDALFactory, "findById">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDynamicSecretLeaseQueueServiceFactory = ReturnType<typeof dynamicSecretLeaseQueueServiceFactory>;
|
export type TDynamicSecretLeaseQueueServiceFactory = ReturnType<typeof dynamicSecretLeaseQueueServiceFactory>;
|
||||||
@@ -22,7 +28,9 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
|||||||
queueService,
|
queueService,
|
||||||
dynamicSecretDAL,
|
dynamicSecretDAL,
|
||||||
dynamicSecretProviders,
|
dynamicSecretProviders,
|
||||||
dynamicSecretLeaseDAL
|
dynamicSecretLeaseDAL,
|
||||||
|
kmsService,
|
||||||
|
folderDAL
|
||||||
}: TDynamicSecretLeaseQueueServiceFactoryDep) => {
|
}: TDynamicSecretLeaseQueueServiceFactoryDep) => {
|
||||||
const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
|
const pruneDynamicSecret = async (dynamicSecretCfgId: string) => {
|
||||||
await queueService.queue(
|
await queueService.queue(
|
||||||
@@ -77,15 +85,38 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
|||||||
if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
|
if (!dynamicSecretLease) throw new DisableRotationErrors({ message: "Dynamic secret lease not found" });
|
||||||
|
|
||||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const folder = await folderDAL.findById(dynamicSecretCfg.folderId);
|
||||||
const decryptedStoredInput = JSON.parse(
|
if (!folder) throw new DisableRotationErrors({ message: "Folder not found" });
|
||||||
infisicalSymmetricDecrypt({
|
const { projectId } = folder;
|
||||||
|
|
||||||
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
let dynamicSecretInputConfig = "";
|
||||||
|
if (
|
||||||
|
dynamicSecretCfg.keyEncoding &&
|
||||||
|
dynamicSecretCfg.inputCiphertext &&
|
||||||
|
dynamicSecretCfg.inputTag &&
|
||||||
|
dynamicSecretCfg.inputIV
|
||||||
|
) {
|
||||||
|
dynamicSecretInputConfig = infisicalSymmetricDecrypt({
|
||||||
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||||
ciphertext: dynamicSecretCfg.inputCiphertext,
|
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||||
tag: dynamicSecretCfg.inputTag,
|
tag: dynamicSecretCfg.inputTag,
|
||||||
iv: dynamicSecretCfg.inputIV
|
iv: dynamicSecretCfg.inputIV
|
||||||
})
|
});
|
||||||
) as object;
|
} else if (dynamicSecretCfg.encryptedConfig) {
|
||||||
|
dynamicSecretInputConfig = secretManagerDecryptor({
|
||||||
|
cipherTextBlob: dynamicSecretCfg.encryptedConfig
|
||||||
|
}).toString();
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({ message: "Missing secret input config" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
|
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
|
||||||
|
|
||||||
await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId);
|
await selectedProvider.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId);
|
||||||
await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id);
|
await dynamicSecretLeaseDAL.deleteById(dynamicSecretLease.id);
|
||||||
@@ -100,17 +131,39 @@ export const dynamicSecretLeaseQueueServiceFactory = ({
|
|||||||
if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting)
|
if ((dynamicSecretCfg.status as DynamicSecretStatus) !== DynamicSecretStatus.Deleting)
|
||||||
throw new DisableRotationErrors({ message: "Document not deleted" });
|
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 });
|
const dynamicSecretLeases = await dynamicSecretLeaseDAL.find({ dynamicSecretId: dynamicSecretCfgId });
|
||||||
if (dynamicSecretLeases.length) {
|
if (dynamicSecretLeases.length) {
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const decryptedStoredInput = JSON.parse(
|
let dynamicSecretInputConfig = "";
|
||||||
infisicalSymmetricDecrypt({
|
|
||||||
|
if (
|
||||||
|
dynamicSecretCfg.keyEncoding &&
|
||||||
|
dynamicSecretCfg.inputCiphertext &&
|
||||||
|
dynamicSecretCfg.inputTag &&
|
||||||
|
dynamicSecretCfg.inputIV
|
||||||
|
) {
|
||||||
|
dynamicSecretInputConfig = infisicalSymmetricDecrypt({
|
||||||
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||||
ciphertext: dynamicSecretCfg.inputCiphertext,
|
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||||
tag: dynamicSecretCfg.inputTag,
|
tag: dynamicSecretCfg.inputTag,
|
||||||
iv: dynamicSecretCfg.inputIV
|
iv: dynamicSecretCfg.inputIV
|
||||||
})
|
});
|
||||||
) as object;
|
} else if (dynamicSecretCfg.encryptedConfig) {
|
||||||
|
dynamicSecretInputConfig = secretManagerDecryptor({
|
||||||
|
cipherTextBlob: dynamicSecretCfg.encryptedConfig
|
||||||
|
}).toString();
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({ message: "Missing secret input config" });
|
||||||
|
}
|
||||||
|
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
|
||||||
|
|
||||||
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
|
await Promise.all(dynamicSecretLeases.map(({ id }) => unsetLeaseRevocation(id)));
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
|
||||||
import { SecretKeyEncoding } from "@app/db/schemas";
|
import { SecretKeyEncoding, TDynamicSecrets } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
@@ -9,6 +9,8 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
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 { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
|
|
||||||
@@ -34,6 +36,7 @@ type TDynamicSecretLeaseServiceFactoryDep = {
|
|||||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>;
|
export type TDynamicSecretLeaseServiceFactory = ReturnType<typeof dynamicSecretLeaseServiceFactory>;
|
||||||
@@ -46,8 +49,32 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
permissionService,
|
permissionService,
|
||||||
dynamicSecretQueueService,
|
dynamicSecretQueueService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
licenseService
|
licenseService,
|
||||||
|
kmsService
|
||||||
}: TDynamicSecretLeaseServiceFactoryDep) => {
|
}: TDynamicSecretLeaseServiceFactoryDep) => {
|
||||||
|
const $getDynamicSecretInputConfig = (dynamicSecretCfg: TDynamicSecrets, decryptValue: (arg: Buffer) => string) => {
|
||||||
|
if (
|
||||||
|
dynamicSecretCfg.keyEncoding &&
|
||||||
|
dynamicSecretCfg.inputCiphertext &&
|
||||||
|
dynamicSecretCfg.inputTag &&
|
||||||
|
dynamicSecretCfg.inputIV
|
||||||
|
) {
|
||||||
|
return JSON.parse(
|
||||||
|
infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||||
|
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||||
|
tag: dynamicSecretCfg.inputTag,
|
||||||
|
iv: dynamicSecretCfg.inputIV
|
||||||
|
})
|
||||||
|
) as object;
|
||||||
|
}
|
||||||
|
if (dynamicSecretCfg.encryptedConfig) {
|
||||||
|
return JSON.parse(decryptValue(dynamicSecretCfg.encryptedConfig)) as object;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestError({ message: "Missing secret input config" });
|
||||||
|
};
|
||||||
|
|
||||||
const create = async ({
|
const create = async ({
|
||||||
environmentSlug,
|
environmentSlug,
|
||||||
path,
|
path,
|
||||||
@@ -94,14 +121,13 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` });
|
throw new BadRequestError({ message: `Max lease limit reached. Limit: ${appCfg.MAX_LEASE_LIMIT}` });
|
||||||
|
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const decryptedStoredInput = JSON.parse(
|
const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
infisicalSymmetricDecrypt({
|
type: KmsDataKey.SecretManager,
|
||||||
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
projectId
|
||||||
ciphertext: dynamicSecretCfg.inputCiphertext,
|
});
|
||||||
tag: dynamicSecretCfg.inputTag,
|
const decryptedStoredInput = $getDynamicSecretInputConfig(dynamicSecretCfg, (value) =>
|
||||||
iv: dynamicSecretCfg.inputIV
|
kmsDecryptor({ cipherTextBlob: value }).toString()
|
||||||
})
|
);
|
||||||
) as object;
|
|
||||||
|
|
||||||
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
|
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
|
||||||
const { maxTTL } = dynamicSecretCfg;
|
const { maxTTL } = dynamicSecretCfg;
|
||||||
@@ -164,14 +190,13 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
|
|
||||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const decryptedStoredInput = JSON.parse(
|
const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
infisicalSymmetricDecrypt({
|
type: KmsDataKey.SecretManager,
|
||||||
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
projectId
|
||||||
ciphertext: dynamicSecretCfg.inputCiphertext,
|
});
|
||||||
tag: dynamicSecretCfg.inputTag,
|
const decryptedStoredInput = $getDynamicSecretInputConfig(dynamicSecretCfg, (value) =>
|
||||||
iv: dynamicSecretCfg.inputIV
|
kmsDecryptor({ cipherTextBlob: value }).toString()
|
||||||
})
|
);
|
||||||
) as object;
|
|
||||||
|
|
||||||
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
|
const selectedTTL = ttl ?? dynamicSecretCfg.defaultTTL;
|
||||||
const { maxTTL } = dynamicSecretCfg;
|
const { maxTTL } = dynamicSecretCfg;
|
||||||
@@ -231,14 +256,13 @@ export const dynamicSecretLeaseServiceFactory = ({
|
|||||||
|
|
||||||
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
const dynamicSecretCfg = dynamicSecretLease.dynamicSecret;
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const decryptedStoredInput = JSON.parse(
|
const { decryptor: kmsDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
infisicalSymmetricDecrypt({
|
type: KmsDataKey.SecretManager,
|
||||||
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
projectId
|
||||||
ciphertext: dynamicSecretCfg.inputCiphertext,
|
});
|
||||||
tag: dynamicSecretCfg.inputTag,
|
const decryptedStoredInput = $getDynamicSecretInputConfig(dynamicSecretCfg, (value) =>
|
||||||
iv: dynamicSecretCfg.inputIV
|
kmsDecryptor({ cipherTextBlob: value }).toString()
|
||||||
})
|
);
|
||||||
) as object;
|
|
||||||
|
|
||||||
const revokeResponse = await selectedProvider
|
const revokeResponse = await selectedProvider
|
||||||
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId)
|
.revoke(decryptedStoredInput, dynamicSecretLease.externalEntityId)
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import { SecretKeyEncoding } from "@app/db/schemas";
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
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 { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
|
|
||||||
@@ -34,6 +36,7 @@ type TDynamicSecretServiceFactoryDep = {
|
|||||||
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
export type TDynamicSecretServiceFactory = ReturnType<typeof dynamicSecretServiceFactory>;
|
||||||
@@ -46,7 +49,8 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
dynamicSecretProviders,
|
dynamicSecretProviders,
|
||||||
permissionService,
|
permissionService,
|
||||||
dynamicSecretQueueService,
|
dynamicSecretQueueService,
|
||||||
projectDAL
|
projectDAL,
|
||||||
|
kmsService
|
||||||
}: TDynamicSecretServiceFactoryDep) => {
|
}: TDynamicSecretServiceFactoryDep) => {
|
||||||
const create = async ({
|
const create = async ({
|
||||||
path,
|
path,
|
||||||
@@ -96,16 +100,16 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
|
|
||||||
const isConnected = await selectedProvider.validateConnection(provider.inputs);
|
const isConnected = await selectedProvider.validateConnection(provider.inputs);
|
||||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||||
|
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs));
|
const encryptedConfig = secretManagerEncryptor({ plainText: Buffer.from(JSON.stringify(inputs)) }).cipherTextBlob;
|
||||||
const dynamicSecretCfg = await dynamicSecretDAL.create({
|
const dynamicSecretCfg = await dynamicSecretDAL.create({
|
||||||
type: provider.type,
|
type: provider.type,
|
||||||
version: 1,
|
version: 1,
|
||||||
inputIV: encryptedInput.iv,
|
encryptedConfig,
|
||||||
inputTag: encryptedInput.tag,
|
|
||||||
inputCiphertext: encryptedInput.ciphertext,
|
|
||||||
algorithm: encryptedInput.algorithm,
|
|
||||||
keyEncoding: encryptedInput.encoding,
|
|
||||||
maxTTL,
|
maxTTL,
|
||||||
defaultTTL,
|
defaultTTL,
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
@@ -164,28 +168,51 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
|
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let dynamicSecretInputConfig = "";
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const decryptedStoredInput = JSON.parse(
|
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
|
||||||
infisicalSymmetricDecrypt({
|
await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
dynamicSecretCfg.keyEncoding &&
|
||||||
|
dynamicSecretCfg.inputCiphertext &&
|
||||||
|
dynamicSecretCfg.inputTag &&
|
||||||
|
dynamicSecretCfg.inputIV
|
||||||
|
) {
|
||||||
|
dynamicSecretInputConfig = infisicalSymmetricDecrypt({
|
||||||
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||||
ciphertext: dynamicSecretCfg.inputCiphertext,
|
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||||
tag: dynamicSecretCfg.inputTag,
|
tag: dynamicSecretCfg.inputTag,
|
||||||
iv: dynamicSecretCfg.inputIV
|
iv: dynamicSecretCfg.inputIV
|
||||||
})
|
});
|
||||||
) as object;
|
} else if (dynamicSecretCfg.encryptedConfig) {
|
||||||
|
dynamicSecretInputConfig = secretManagerDecryptor({
|
||||||
|
cipherTextBlob: dynamicSecretCfg.encryptedConfig
|
||||||
|
}).toString();
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({ message: "Missing secret input config" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
|
||||||
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
|
const newInput = { ...decryptedStoredInput, ...(inputs || {}) };
|
||||||
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
|
const updatedInput = await selectedProvider.validateProviderInputs(newInput);
|
||||||
|
|
||||||
const isConnected = await selectedProvider.validateConnection(newInput);
|
const isConnected = await selectedProvider.validateConnection(newInput);
|
||||||
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
|
||||||
|
|
||||||
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(updatedInput));
|
const encryptedConfig = secretManagerEncryptor({
|
||||||
|
plainText: Buffer.from(JSON.stringify(updatedInput))
|
||||||
|
}).cipherTextBlob;
|
||||||
|
|
||||||
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
|
const updatedDynamicCfg = await dynamicSecretDAL.updateById(dynamicSecretCfg.id, {
|
||||||
inputIV: encryptedInput.iv,
|
encryptedConfig,
|
||||||
inputTag: encryptedInput.tag,
|
inputIV: null,
|
||||||
inputCiphertext: encryptedInput.ciphertext,
|
inputTag: null,
|
||||||
algorithm: encryptedInput.algorithm,
|
keyEncoding: null,
|
||||||
keyEncoding: encryptedInput.encoding,
|
algorithm: null,
|
||||||
|
inputCiphertext: null,
|
||||||
maxTTL,
|
maxTTL,
|
||||||
defaultTTL,
|
defaultTTL,
|
||||||
name: newName ?? name,
|
name: newName ?? name,
|
||||||
@@ -286,14 +313,33 @@ export const dynamicSecretServiceFactory = ({
|
|||||||
|
|
||||||
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
const dynamicSecretCfg = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
|
||||||
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
|
if (!dynamicSecretCfg) throw new BadRequestError({ message: "Dynamic secret not found" });
|
||||||
const decryptedStoredInput = JSON.parse(
|
const { decryptor: secretManagerDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
infisicalSymmetricDecrypt({
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
let dynamicSecretInputConfig = "";
|
||||||
|
if (
|
||||||
|
dynamicSecretCfg.keyEncoding &&
|
||||||
|
dynamicSecretCfg.inputCiphertext &&
|
||||||
|
dynamicSecretCfg.inputTag &&
|
||||||
|
dynamicSecretCfg.inputIV
|
||||||
|
) {
|
||||||
|
dynamicSecretInputConfig = infisicalSymmetricDecrypt({
|
||||||
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
keyEncoding: dynamicSecretCfg.keyEncoding as SecretKeyEncoding,
|
||||||
ciphertext: dynamicSecretCfg.inputCiphertext,
|
ciphertext: dynamicSecretCfg.inputCiphertext,
|
||||||
tag: dynamicSecretCfg.inputTag,
|
tag: dynamicSecretCfg.inputTag,
|
||||||
iv: dynamicSecretCfg.inputIV
|
iv: dynamicSecretCfg.inputIV
|
||||||
})
|
});
|
||||||
) as object;
|
} else if (dynamicSecretCfg.encryptedConfig) {
|
||||||
|
dynamicSecretInputConfig = secretManagerDecryptor({
|
||||||
|
cipherTextBlob: dynamicSecretCfg.encryptedConfig
|
||||||
|
}).toString();
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({ message: "Missing secret input config" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptedStoredInput = JSON.parse(dynamicSecretInputConfig) as object;
|
||||||
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
const selectedProvider = dynamicSecretProviders[dynamicSecretCfg.type as DynamicSecretProviders];
|
||||||
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
|
const providerInputs = (await selectedProvider.validateProviderInputs(decryptedStoredInput)) as object;
|
||||||
return { ...dynamicSecretCfg, inputs: providerInputs };
|
return { ...dynamicSecretCfg, inputs: providerInputs };
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ export const queueServiceFactory = (redisUrl: string) => {
|
|||||||
const job = await q.getJob(jobId);
|
const job = await q.getJob(jobId);
|
||||||
if (!job) return true;
|
if (!job) return true;
|
||||||
if (!job.repeatJobKey) return true;
|
if (!job.repeatJobKey) return true;
|
||||||
|
await job.remove();
|
||||||
return q.removeRepeatableByKey(job.repeatJobKey);
|
return q.removeRepeatableByKey(job.repeatJobKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -677,7 +677,8 @@ export const registerRoutes = async (
|
|||||||
permissionService,
|
permissionService,
|
||||||
webhookDAL,
|
webhookDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
projectDAL
|
projectDAL,
|
||||||
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService });
|
const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService });
|
||||||
@@ -724,7 +725,8 @@ export const registerRoutes = async (
|
|||||||
integrationAuthDAL,
|
integrationAuthDAL,
|
||||||
snapshotDAL,
|
snapshotDAL,
|
||||||
snapshotSecretV2BridgeDAL,
|
snapshotSecretV2BridgeDAL,
|
||||||
secretApprovalRequestDAL
|
secretApprovalRequestDAL,
|
||||||
|
dynamicSecretDAL
|
||||||
});
|
});
|
||||||
const secretImportService = secretImportServiceFactory({
|
const secretImportService = secretImportServiceFactory({
|
||||||
licenseService,
|
licenseService,
|
||||||
@@ -987,7 +989,9 @@ export const registerRoutes = async (
|
|||||||
queueService,
|
queueService,
|
||||||
dynamicSecretLeaseDAL,
|
dynamicSecretLeaseDAL,
|
||||||
dynamicSecretProviders,
|
dynamicSecretProviders,
|
||||||
dynamicSecretDAL
|
dynamicSecretDAL,
|
||||||
|
kmsService,
|
||||||
|
folderDAL
|
||||||
});
|
});
|
||||||
const dynamicSecretService = dynamicSecretServiceFactory({
|
const dynamicSecretService = dynamicSecretServiceFactory({
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@@ -997,7 +1001,8 @@ export const registerRoutes = async (
|
|||||||
dynamicSecretProviders,
|
dynamicSecretProviders,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService
|
licenseService,
|
||||||
|
kmsService
|
||||||
});
|
});
|
||||||
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
|
const dynamicSecretLeaseService = dynamicSecretLeaseServiceFactory({
|
||||||
projectDAL,
|
projectDAL,
|
||||||
@@ -1007,7 +1012,8 @@ export const registerRoutes = async (
|
|||||||
dynamicSecretLeaseDAL,
|
dynamicSecretLeaseDAL,
|
||||||
dynamicSecretProviders,
|
dynamicSecretProviders,
|
||||||
folderDAL,
|
folderDAL,
|
||||||
licenseService
|
licenseService,
|
||||||
|
kmsService
|
||||||
});
|
});
|
||||||
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
|
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
|
||||||
auditLogDAL,
|
auditLogDAL,
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ export const integrationAuthServiceFactory = ({
|
|||||||
const awsAssumeIamRoleArnEncrypted = secretManagerEncryptor({
|
const awsAssumeIamRoleArnEncrypted = secretManagerEncryptor({
|
||||||
plainText: Buffer.from(awsAssumeIamRoleArn)
|
plainText: Buffer.from(awsAssumeIamRoleArn)
|
||||||
}).cipherTextBlob;
|
}).cipherTextBlob;
|
||||||
updateDoc.encryptedAwsIamAssumRole = awsAssumeIamRoleArnEncrypted;
|
updateDoc.encryptedAwsAssumeIamRoleArn = awsAssumeIamRoleArnEncrypted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
import { ProjectUpgradeStatus, ProjectVersion, TSecretSnapshotSecretsV2, TSecretVersionsV2 } from "@app/db/schemas";
|
import {
|
||||||
|
ProjectUpgradeStatus,
|
||||||
|
ProjectVersion,
|
||||||
|
SecretKeyEncoding,
|
||||||
|
TSecretSnapshotSecretsV2,
|
||||||
|
TSecretVersionsV2
|
||||||
|
} from "@app/db/schemas";
|
||||||
|
import { TDynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal";
|
||||||
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
||||||
import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
|
import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
|
||||||
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
|
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
|
||||||
import { TSnapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal";
|
import { TSnapshotSecretV2DALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-v2-dal";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
|
||||||
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
import { daysToMillisecond, secondsToMillis } from "@app/lib/dates";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { groupBy, isSamePath, unique } from "@app/lib/fn";
|
import { groupBy, isSamePath, unique } from "@app/lib/fn";
|
||||||
@@ -60,7 +68,7 @@ type TSecretQueueFactoryDep = {
|
|||||||
folderDAL: TSecretFolderDALFactory;
|
folderDAL: TSecretFolderDALFactory;
|
||||||
secretDAL: TSecretDALFactory;
|
secretDAL: TSecretDALFactory;
|
||||||
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
|
||||||
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
|
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "find" | "transaction" | "update" | "bulkUpdate" | "upsert">;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "find">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "find">;
|
||||||
projectDAL: TProjectDALFactory;
|
projectDAL: TProjectDALFactory;
|
||||||
projectBotDAL: TProjectBotDALFactory;
|
projectBotDAL: TProjectBotDALFactory;
|
||||||
@@ -79,6 +87,7 @@ type TSecretQueueFactoryDep = {
|
|||||||
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "deleteByProjectId">;
|
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "deleteByProjectId">;
|
||||||
snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">;
|
snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">;
|
||||||
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany">;
|
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany">;
|
||||||
|
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "find" | "upsert">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TGetSecrets = {
|
export type TGetSecrets = {
|
||||||
@@ -122,7 +131,8 @@ export const secretQueueFactory = ({
|
|||||||
secretRotationDAL,
|
secretRotationDAL,
|
||||||
snapshotDAL,
|
snapshotDAL,
|
||||||
snapshotSecretV2BridgeDAL,
|
snapshotSecretV2BridgeDAL,
|
||||||
secretApprovalRequestDAL
|
secretApprovalRequestDAL,
|
||||||
|
dynamicSecretDAL
|
||||||
}: TSecretQueueFactoryDep) => {
|
}: TSecretQueueFactoryDep) => {
|
||||||
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
|
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
@@ -881,6 +891,37 @@ export const secretQueueFactory = ({
|
|||||||
await secretV2BridgeDAL.upsertSecretReferences(secretReferences, tx);
|
await secretV2BridgeDAL.upsertSecretReferences(secretReferences, tx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dynamicSecrets = await dynamicSecretDAL.find({ folderId }, { tx });
|
||||||
|
if (dynamicSecrets.length) {
|
||||||
|
await dynamicSecretDAL.upsert(
|
||||||
|
dynamicSecrets.map((el) => {
|
||||||
|
let { encryptedConfig } = el;
|
||||||
|
if (!encryptedConfig) {
|
||||||
|
if (el.keyEncoding && el.inputCiphertext && el.inputTag && el.inputIV) {
|
||||||
|
const decryptedConfig = infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: el.keyEncoding as SecretKeyEncoding,
|
||||||
|
ciphertext: el.inputCiphertext,
|
||||||
|
tag: el.inputTag,
|
||||||
|
iv: el.inputIV
|
||||||
|
});
|
||||||
|
encryptedConfig = secretManagerEncryptor({ plainText: Buffer.from(decryptedConfig) }).cipherTextBlob;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...el,
|
||||||
|
encryptedConfig,
|
||||||
|
keyEncoding: null,
|
||||||
|
inputCiphertext: null,
|
||||||
|
inputTag: null,
|
||||||
|
inputIV: null,
|
||||||
|
algorithm: null
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
"id",
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const SNAPSHOT_BATCH_SIZE = 15;
|
const SNAPSHOT_BATCH_SIZE = 15;
|
||||||
const snapshots = await snapshotDAL.findNSecretV1SnapshotByFolderId(folderId, SNAPSHOT_BATCH_SIZE, tx);
|
const snapshots = await snapshotDAL.findNSecretV1SnapshotByFolderId(folderId, SNAPSHOT_BATCH_SIZE, tx);
|
||||||
const projectV3SecretVersionsGroupById: Record<string, TSecretVersionsV2> = {};
|
const projectV3SecretVersionsGroupById: Record<string, TSecretVersionsV2> = {};
|
||||||
@@ -1110,6 +1151,71 @@ export const secretQueueFactory = ({
|
|||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* webhooks
|
||||||
|
* */
|
||||||
|
const projectV1Webhooks = await webhookDAL.find({ projectId }, tx);
|
||||||
|
if (projectV1Webhooks.length) {
|
||||||
|
await webhookDAL.upsert(
|
||||||
|
projectV1Webhooks.map((el) => {
|
||||||
|
let { encryptedSecretKeyWithKms, encryptedUrl } = el;
|
||||||
|
if (!encryptedSecretKeyWithKms) {
|
||||||
|
if (el.encryptedSecretKey && el.iv && el.tag) {
|
||||||
|
const webhookSecretKey = infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: el.keyEncoding as SecretKeyEncoding,
|
||||||
|
ciphertext: el.encryptedSecretKey,
|
||||||
|
iv: el.iv,
|
||||||
|
tag: el.tag
|
||||||
|
});
|
||||||
|
encryptedSecretKeyWithKms = secretManagerEncryptor({
|
||||||
|
plainText: Buffer.from(webhookSecretKey)
|
||||||
|
}).cipherTextBlob;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!encryptedUrl) {
|
||||||
|
if (el.urlTag && el.urlCipherText && el.urlIV) {
|
||||||
|
const webhookUrl = infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: el.keyEncoding as SecretKeyEncoding,
|
||||||
|
ciphertext: el.urlCipherText,
|
||||||
|
iv: el.urlIV,
|
||||||
|
tag: el.urlTag
|
||||||
|
});
|
||||||
|
encryptedUrl = secretManagerEncryptor({
|
||||||
|
plainText: Buffer.from(webhookUrl)
|
||||||
|
}).cipherTextBlob;
|
||||||
|
} else {
|
||||||
|
encryptedUrl = secretManagerEncryptor({
|
||||||
|
plainText: Buffer.from(el.url)
|
||||||
|
}).cipherTextBlob;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: el.id,
|
||||||
|
url: el.url,
|
||||||
|
envId: el.envId,
|
||||||
|
type: el.type,
|
||||||
|
isDisabled: el.isDisabled,
|
||||||
|
lastStatus: el.lastStatus,
|
||||||
|
secretPath: el.secretPath,
|
||||||
|
lastRunErrorMessage: el.lastRunErrorMessage,
|
||||||
|
encryptedSecretKeyWithKms,
|
||||||
|
encryptedUrl,
|
||||||
|
urlCipherText: null,
|
||||||
|
urlIV: null,
|
||||||
|
urlTag: null,
|
||||||
|
encryptedSecretKey: null,
|
||||||
|
iv: null,
|
||||||
|
tag: null,
|
||||||
|
keyEncoding: null,
|
||||||
|
algorithm: null
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
"id",
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* approvals: we will delete all approvals this is because some secret versions may not be added yet
|
* approvals: we will delete all approvals this is because some secret versions may not be added yet
|
||||||
* Thus doesn't make sense for rest to be there
|
* Thus doesn't make sense for rest to be there
|
||||||
@@ -1193,7 +1299,7 @@ export const secretQueueFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
queueService.start(QueueName.SecretWebhook, async (job) => {
|
queueService.start(QueueName.SecretWebhook, async (job) => {
|
||||||
await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL });
|
await fnTriggerWebhook({ ...job.data, projectEnvDAL, webhookDAL, projectDAL, kmsService });
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const webhookDALFactory = (db: TDbClient) => {
|
|||||||
.select(tx.ref("projectId").withSchema(TableName.Environment))
|
.select(tx.ref("projectId").withSchema(TableName.Environment))
|
||||||
.select(selectAllTableCols(TableName.Webhook));
|
.select(selectAllTableCols(TableName.Webhook));
|
||||||
|
|
||||||
const find = async (filter: Partial<TWebhooks>, tx?: Knex) => {
|
const find = async (filter: Partial<TWebhooks & { projectId: string }>, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const docs = await webhookFindQuery(tx || db.replicaNode(), filter);
|
const docs = await webhookFindQuery(tx || db.replicaNode(), filter);
|
||||||
return docs.map(({ envId, envSlug, envName, ...el }) => ({
|
return docs.map(({ envId, envSlug, envName, ...el }) => ({
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import crypto from "node:crypto";
|
|||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import picomatch from "picomatch";
|
import picomatch from "picomatch";
|
||||||
|
|
||||||
import { SecretKeyEncoding, TWebhooks } from "@app/db/schemas";
|
import { SecretKeyEncoding } from "@app/db/schemas";
|
||||||
import { request } from "@app/lib/config/request";
|
import { request } from "@app/lib/config/request";
|
||||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
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 { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TWebhookDALFactory } from "./webhook-dal";
|
import { TWebhookDALFactory } from "./webhook-dal";
|
||||||
@@ -16,40 +18,12 @@ import { WebhookType } from "./webhook-types";
|
|||||||
|
|
||||||
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
|
const WEBHOOK_TRIGGER_TIMEOUT = 15 * 1000;
|
||||||
|
|
||||||
export const decryptWebhookDetails = (webhook: TWebhooks) => {
|
export const triggerWebhookRequest = async (
|
||||||
const { keyEncoding, iv, encryptedSecretKey, tag, urlCipherText, urlIV, urlTag, url } = webhook;
|
{ webhookSecretKey: secretKey, webhookUrl: url }: { webhookSecretKey?: string; webhookUrl: string },
|
||||||
|
data: Record<string, unknown>
|
||||||
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<string, unknown>) => {
|
|
||||||
const headers: Record<string, string> = {};
|
const headers: Record<string, string> = {};
|
||||||
const payload = { ...data, timestamp: Date.now() };
|
const payload = { ...data, timestamp: Date.now() };
|
||||||
const { secretKey, url } = decryptWebhookDetails(webhook);
|
|
||||||
|
|
||||||
if (secretKey) {
|
if (secretKey) {
|
||||||
const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex");
|
const webhookSign = crypto.createHmac("sha256", secretKey).update(JSON.stringify(payload)).digest("hex");
|
||||||
@@ -124,6 +98,7 @@ export type TFnTriggerWebhookDTO = {
|
|||||||
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
|
webhookDAL: Pick<TWebhookDALFactory, "findAllWebhooks" | "transaction" | "update" | "bulkUpdate">;
|
||||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
};
|
};
|
||||||
|
|
||||||
// this is reusable function
|
// this is reusable function
|
||||||
@@ -134,7 +109,8 @@ export const fnTriggerWebhook = async ({
|
|||||||
projectId,
|
projectId,
|
||||||
webhookDAL,
|
webhookDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
projectDAL
|
projectDAL,
|
||||||
|
kmsService
|
||||||
}: TFnTriggerWebhookDTO) => {
|
}: TFnTriggerWebhookDTO) => {
|
||||||
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
|
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment);
|
||||||
const toBeTriggeredHooks = webhooks.filter(
|
const toBeTriggeredHooks = webhooks.filter(
|
||||||
@@ -144,10 +120,38 @@ export const fnTriggerWebhook = async ({
|
|||||||
if (!toBeTriggeredHooks.length) return;
|
if (!toBeTriggeredHooks.length) return;
|
||||||
logger.info("Secret webhook job started", { environment, secretPath, projectId });
|
logger.info("Secret webhook job started", { environment, secretPath, projectId });
|
||||||
const project = await projectDAL.findById(projectId);
|
const project = await projectDAL.findById(projectId);
|
||||||
|
const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
projectId,
|
||||||
|
type: KmsDataKey.SecretManager
|
||||||
|
});
|
||||||
|
|
||||||
const webhooksTriggered = await Promise.allSettled(
|
const webhooksTriggered = await Promise.allSettled(
|
||||||
toBeTriggeredHooks.map((hook) =>
|
toBeTriggeredHooks.map((hook) => {
|
||||||
triggerWebhookRequest(
|
let webhookUrl = hook.url;
|
||||||
hook,
|
let webhookSecretKey;
|
||||||
|
if (hook.urlTag && hook.urlCipherText && hook.urlIV) {
|
||||||
|
webhookUrl = infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: hook.keyEncoding as SecretKeyEncoding,
|
||||||
|
ciphertext: hook.urlCipherText,
|
||||||
|
iv: hook.urlIV,
|
||||||
|
tag: hook.urlTag
|
||||||
|
});
|
||||||
|
} else if (hook.encryptedUrl) {
|
||||||
|
webhookUrl = kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedUrl }).toString();
|
||||||
|
}
|
||||||
|
if (hook.encryptedSecretKey && hook.iv && hook.tag) {
|
||||||
|
webhookSecretKey = infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: hook.keyEncoding as SecretKeyEncoding,
|
||||||
|
ciphertext: hook.encryptedSecretKey,
|
||||||
|
iv: hook.iv,
|
||||||
|
tag: hook.tag
|
||||||
|
});
|
||||||
|
} else if (hook.encryptedSecretKeyWithKms) {
|
||||||
|
webhookSecretKey = kmsDataKeyDecryptor({ cipherTextBlob: hook.encryptedSecretKeyWithKms }).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return triggerWebhookRequest(
|
||||||
|
{ webhookUrl, webhookSecretKey },
|
||||||
getWebhookPayload("secrets.modified", {
|
getWebhookPayload("secrets.modified", {
|
||||||
workspaceName: project.name,
|
workspaceName: project.name,
|
||||||
workspaceId: projectId,
|
workspaceId: projectId,
|
||||||
@@ -155,8 +159,8 @@ export const fnTriggerWebhook = async ({
|
|||||||
secretPath,
|
secretPath,
|
||||||
type: hook.type
|
type: hook.type
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
)
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// filter hooks by status
|
// filter hooks by status
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
import { TWebhooksInsert } from "@app/db/schemas";
|
import { SecretKeyEncoding, TWebhooksInsert } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
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 { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
import { TWebhookDALFactory } from "./webhook-dal";
|
import { TWebhookDALFactory } from "./webhook-dal";
|
||||||
import { decryptWebhookDetails, getWebhookPayload, triggerWebhookRequest } from "./webhook-fns";
|
import { getWebhookPayload, triggerWebhookRequest } from "./webhook-fns";
|
||||||
import {
|
import {
|
||||||
TCreateWebhookDTO,
|
TCreateWebhookDTO,
|
||||||
TDeleteWebhookDTO,
|
TDeleteWebhookDTO,
|
||||||
@@ -23,6 +25,7 @@ type TWebhookServiceFactoryDep = {
|
|||||||
projectEnvDAL: TProjectEnvDALFactory;
|
projectEnvDAL: TProjectEnvDALFactory;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TWebhookServiceFactory = ReturnType<typeof webhookServiceFactory>;
|
export type TWebhookServiceFactory = ReturnType<typeof webhookServiceFactory>;
|
||||||
@@ -31,7 +34,8 @@ export const webhookServiceFactory = ({
|
|||||||
webhookDAL,
|
webhookDAL,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
projectDAL
|
projectDAL,
|
||||||
|
kmsService
|
||||||
}: TWebhookServiceFactoryDep) => {
|
}: TWebhookServiceFactoryDep) => {
|
||||||
const createWebhook = async ({
|
const createWebhook = async ({
|
||||||
actor,
|
actor,
|
||||||
@@ -64,22 +68,23 @@ export const webhookServiceFactory = ({
|
|||||||
type
|
type
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
projectId,
|
||||||
|
type: KmsDataKey.SecretManager
|
||||||
|
});
|
||||||
if (webhookSecretKey) {
|
if (webhookSecretKey) {
|
||||||
const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookSecretKey);
|
const encryptedSecretKeyWithKms = secretManagerEncryptor({
|
||||||
insertDoc.encryptedSecretKey = ciphertext;
|
plainText: Buffer.from(webhookSecretKey)
|
||||||
insertDoc.iv = iv;
|
}).cipherTextBlob;
|
||||||
insertDoc.tag = tag;
|
insertDoc.encryptedSecretKeyWithKms = encryptedSecretKeyWithKms;
|
||||||
insertDoc.algorithm = algorithm;
|
|
||||||
insertDoc.keyEncoding = encoding;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (webhookUrl) {
|
if (webhookUrl) {
|
||||||
const { ciphertext, iv, tag, algorithm, encoding } = infisicalSymmetricEncypt(webhookUrl);
|
const encryptedUrl = secretManagerEncryptor({
|
||||||
insertDoc.urlCipherText = ciphertext;
|
plainText: Buffer.from(webhookUrl)
|
||||||
insertDoc.urlIV = iv;
|
}).cipherTextBlob;
|
||||||
insertDoc.urlTag = tag;
|
|
||||||
insertDoc.algorithm = algorithm;
|
insertDoc.encryptedUrl = encryptedUrl;
|
||||||
insertDoc.keyEncoding = encoding;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const webhook = await webhookDAL.create(insertDoc);
|
const webhook = await webhookDAL.create(insertDoc);
|
||||||
@@ -136,9 +141,35 @@ export const webhookServiceFactory = ({
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
||||||
let webhookError: string | undefined;
|
let webhookError: string | undefined;
|
||||||
|
const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
projectId: project.id,
|
||||||
|
type: KmsDataKey.SecretManager
|
||||||
|
});
|
||||||
|
let webhookUrl = webhook.url;
|
||||||
|
let webhookSecretKey;
|
||||||
|
if (webhook.urlTag && webhook.urlCipherText && webhook.urlIV) {
|
||||||
|
webhookUrl = infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: webhook.keyEncoding as SecretKeyEncoding,
|
||||||
|
ciphertext: webhook.urlCipherText,
|
||||||
|
iv: webhook.urlIV,
|
||||||
|
tag: webhook.urlTag
|
||||||
|
});
|
||||||
|
} else if (webhook.encryptedUrl) {
|
||||||
|
webhookUrl = kmsDataKeyDecryptor({ cipherTextBlob: webhook.encryptedUrl }).toString();
|
||||||
|
}
|
||||||
|
if (webhook.encryptedSecretKey && webhook.iv && webhook.tag) {
|
||||||
|
webhookSecretKey = infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: webhook.keyEncoding as SecretKeyEncoding,
|
||||||
|
ciphertext: webhook.encryptedSecretKey,
|
||||||
|
iv: webhook.iv,
|
||||||
|
tag: webhook.tag
|
||||||
|
});
|
||||||
|
} else if (webhook.encryptedSecretKeyWithKms) {
|
||||||
|
webhookSecretKey = kmsDataKeyDecryptor({ cipherTextBlob: webhook.encryptedSecretKeyWithKms }).toString();
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await triggerWebhookRequest(
|
await triggerWebhookRequest(
|
||||||
webhook,
|
{ webhookUrl, webhookSecretKey },
|
||||||
getWebhookPayload("test", {
|
getWebhookPayload("test", {
|
||||||
workspaceName: project.name,
|
workspaceName: project.name,
|
||||||
workspaceId: webhook.projectId,
|
workspaceId: webhook.projectId,
|
||||||
@@ -177,11 +208,25 @@ export const webhookServiceFactory = ({
|
|||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
||||||
|
|
||||||
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment, secretPath);
|
const webhooks = await webhookDAL.findAllWebhooks(projectId, environment, secretPath);
|
||||||
|
const { decryptor: kmsDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.SecretManager,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
return webhooks.map((w) => {
|
return webhooks.map((w) => {
|
||||||
const { url } = decryptWebhookDetails(w);
|
let decryptedUrl = w.url;
|
||||||
|
if (w.urlTag && w.urlCipherText && w.urlIV) {
|
||||||
|
decryptedUrl = infisicalSymmetricDecrypt({
|
||||||
|
keyEncoding: w.keyEncoding as SecretKeyEncoding,
|
||||||
|
ciphertext: w.urlCipherText,
|
||||||
|
iv: w.urlIV,
|
||||||
|
tag: w.urlTag
|
||||||
|
});
|
||||||
|
} else if (w.encryptedUrl) {
|
||||||
|
decryptedUrl = kmsDataKeyDecryptor({ cipherTextBlob: w.encryptedUrl }).toString();
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...w,
|
...w,
|
||||||
url
|
url: decryptedUrl
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user