diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 05dff5a73..aaa2014b8 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -218,6 +218,9 @@ import { TRateLimit, TRateLimitInsert, TRateLimitUpdate, + TResourceMetadata, + TResourceMetadataInsert, + TResourceMetadataUpdate, TSamlConfigs, TSamlConfigsInsert, TSamlConfigsUpdate, @@ -887,6 +890,11 @@ declare module "knex/types/tables" { TProjectSplitBackfillIdsInsert, TProjectSplitBackfillIdsUpdate >; + [TableName.ResourceMetadata]: KnexOriginal.CompositeTableType< + TResourceMetadata, + TResourceMetadataInsert, + TResourceMetadataUpdate + >; [TableName.AppConnection]: KnexOriginal.CompositeTableType< TAppConnections, TAppConnectionsInsert, diff --git a/backend/src/db/migrations/20241218165837_resource-metadata.ts b/backend/src/db/migrations/20241218165837_resource-metadata.ts new file mode 100644 index 000000000..af62f9895 --- /dev/null +++ b/backend/src/db/migrations/20241218165837_resource-metadata.ts @@ -0,0 +1,40 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.ResourceMetadata))) { + await knex.schema.createTable(TableName.ResourceMetadata, (tb) => { + tb.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + tb.string("key").notNullable(); + tb.string("value", 1020).notNullable(); + tb.uuid("orgId").notNullable(); + tb.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + tb.uuid("userId"); + tb.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + tb.uuid("identityId"); + tb.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE"); + tb.uuid("secretId"); + tb.foreign("secretId").references("id").inTable(TableName.SecretV2).onDelete("CASCADE"); + tb.timestamps(true, true, true); + }); + } + + const hasSecretMetadataField = await knex.schema.hasColumn(TableName.SecretApprovalRequestSecretV2, "secretMetadata"); + if (!hasSecretMetadataField) { + await knex.schema.alterTable(TableName.SecretApprovalRequestSecretV2, (t) => { + t.jsonb("secretMetadata"); + }); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.ResourceMetadata); + + const hasSecretMetadataField = await knex.schema.hasColumn(TableName.SecretApprovalRequestSecretV2, "secretMetadata"); + if (hasSecretMetadataField) { + await knex.schema.alterTable(TableName.SecretApprovalRequestSecretV2, (t) => { + t.dropColumn("secretMetadata"); + }); + } +} diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 19f45eb33..9bcfdd49f 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -71,6 +71,7 @@ export * from "./project-user-additional-privilege"; export * from "./project-user-membership-roles"; export * from "./projects"; export * from "./rate-limit"; +export * from "./resource-metadata"; export * from "./saml-configs"; export * from "./scim-tokens"; export * from "./secret-approval-policies"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 620c526a5..dac713cd9 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -80,6 +80,7 @@ export enum TableName { IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege", // used by both identity and users IdentityMetadata = "identity_metadata", + ResourceMetadata = "resource_metadata", ScimToken = "scim_tokens", AccessApprovalPolicy = "access_approval_policies", AccessApprovalPolicyApprover = "access_approval_policies_approvers", diff --git a/backend/src/db/schemas/resource-metadata.ts b/backend/src/db/schemas/resource-metadata.ts new file mode 100644 index 000000000..f496b29db --- /dev/null +++ b/backend/src/db/schemas/resource-metadata.ts @@ -0,0 +1,24 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ResourceMetadataSchema = z.object({ + id: z.string().uuid(), + key: z.string(), + value: z.string(), + orgId: z.string().uuid(), + userId: z.string().uuid().nullable().optional(), + identityId: z.string().uuid().nullable().optional(), + secretId: z.string().uuid().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TResourceMetadata = z.infer; +export type TResourceMetadataInsert = Omit, TImmutableDBKeys>; +export type TResourceMetadataUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/secret-approval-requests-secrets-v2.ts b/backend/src/db/schemas/secret-approval-requests-secrets-v2.ts index ee25ed6ef..298985fed 100644 --- a/backend/src/db/schemas/secret-approval-requests-secrets-v2.ts +++ b/backend/src/db/schemas/secret-approval-requests-secrets-v2.ts @@ -24,7 +24,8 @@ export const SecretApprovalRequestsSecretsV2Schema = z.object({ requestId: z.string().uuid(), op: z.string(), secretId: z.string().uuid().nullable().optional(), - secretVersion: z.string().uuid().nullable().optional() + secretVersion: z.string().uuid().nullable().optional(), + secretMetadata: z.unknown().nullable().optional() }); export type TSecretApprovalRequestsSecretsV2 = z.infer; diff --git a/backend/src/ee/routes/v1/secret-approval-request-router.ts b/backend/src/ee/routes/v1/secret-approval-request-router.ts index e1c56583c..fe4d8ee81 100644 --- a/backend/src/ee/routes/v1/secret-approval-request-router.ts +++ b/backend/src/ee/routes/v1/secret-approval-request-router.ts @@ -12,6 +12,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; +import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; const approvalRequestUser = z.object({ userId: z.string().nullable().optional() }).merge( UsersSchema.pick({ @@ -274,6 +275,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv .extend({ op: z.string(), tags: tagSchema, + secretMetadata: ResourceMetadataSchema.nullish(), secret: z .object({ id: z.string(), @@ -291,7 +293,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv secretKey: z.string(), secretValue: z.string().optional(), secretComment: z.string().optional(), - tags: tagSchema + tags: tagSchema, + secretMetadata: ResourceMetadataSchema.nullish() }) .optional() }) diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts index 77f38dd53..f72977a9b 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-secret-dal.ts @@ -256,6 +256,7 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => { `${TableName.SecretVersionV2Tag}.${TableName.SecretTag}Id`, db.ref("id").withSchema("secVerTag") ) + .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) .select(selectAllTableCols(TableName.SecretApprovalRequestSecretV2)) .select({ secVerTagId: "secVerTag.id", @@ -279,6 +280,11 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => { db.ref("key").withSchema(TableName.SecretVersionV2).as("secVerKey"), db.ref("encryptedValue").withSchema(TableName.SecretVersionV2).as("secVerValue"), db.ref("encryptedComment").withSchema(TableName.SecretVersionV2).as("secVerComment") + ) + .select( + db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") ); const formatedDoc = sqlNestRelationships({ data: doc, @@ -338,9 +344,19 @@ export const secretApprovalRequestSecretDALFactory = (db: TDbClient) => { }) } ] + }, + { + key: "metadataId", + label: "oldSecretMetadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) } ] }); + return formatedDoc?.map(({ secret, secretVersion, ...el }) => ({ ...el, secret: secret?.[0], diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts index 75b664861..76cf35ca0 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-service.ts @@ -22,6 +22,8 @@ import { KmsDataKey } from "@app/services/kms/kms-types"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal"; +import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal"; +import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema"; import { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { decryptSecretWithBot, @@ -91,6 +93,7 @@ type TSecretApprovalRequestServiceFactoryDep = { secretBlindIndexDAL: Pick; snapshotService: Pick; secretVersionDAL: Pick; + resourceMetadataDAL: Pick; secretVersionTagDAL: Pick; smtpService: Pick; userDAL: Pick; @@ -138,7 +141,8 @@ export const secretApprovalRequestServiceFactory = ({ secretVersionV2BridgeDAL, secretVersionTagV2BridgeDAL, licenseService, - projectSlackConfigDAL + projectSlackConfigDAL, + resourceMetadataDAL }: TSecretApprovalRequestServiceFactoryDep) => { const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => { if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" }); @@ -241,6 +245,7 @@ export const secretApprovalRequestServiceFactory = ({ secretKey: el.key, id: el.id, version: el.version, + secretMetadata: el.secretMetadata as ResourceMetadataDTO, secretValue: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", secretComment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() @@ -269,7 +274,8 @@ export const secretApprovalRequestServiceFactory = ({ secretComment: el.secretVersion.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedComment }).toString() : "", - tags: el.secretVersion.tags + tags: el.secretVersion.tags, + secretMetadata: el.oldSecretMetadata as ResourceMetadataDTO } : undefined })); @@ -543,6 +549,7 @@ export const secretApprovalRequestServiceFactory = ({ ? await fnSecretV2BridgeBulkInsert({ tx, folderId, + orgId: actorOrgId, inputSecrets: secretCreationCommits.map((el) => ({ tagIds: el?.tags.map(({ id }) => id), version: 1, @@ -550,6 +557,7 @@ export const secretApprovalRequestServiceFactory = ({ encryptedValue: el.encryptedValue, skipMultilineEncoding: el.skipMultilineEncoding, key: el.key, + secretMetadata: el.secretMetadata as ResourceMetadataDTO, references: el.encryptedValue ? getAllSecretReferencesV2Bridge( secretManagerDecryptor({ @@ -559,6 +567,7 @@ export const secretApprovalRequestServiceFactory = ({ : [], type: SecretType.Shared })), + resourceMetadataDAL, secretDAL: secretV2BridgeDAL, secretVersionDAL: secretVersionV2BridgeDAL, secretTagDAL, @@ -568,6 +577,7 @@ export const secretApprovalRequestServiceFactory = ({ const updatedSecrets = secretUpdationCommits.length ? await fnSecretV2BridgeBulkUpdate({ folderId, + orgId: actorOrgId, tx, inputSecrets: secretUpdationCommits.map((el) => { const encryptedValue = @@ -592,6 +602,7 @@ export const secretApprovalRequestServiceFactory = ({ skipMultilineEncoding: el.skipMultilineEncoding, key: el.key, tags: el?.tags.map(({ id }) => id), + secretMetadata: el.secretMetadata as ResourceMetadataDTO, ...encryptedValue } }; @@ -599,7 +610,8 @@ export const secretApprovalRequestServiceFactory = ({ secretDAL: secretV2BridgeDAL, secretVersionDAL: secretVersionV2BridgeDAL, secretTagDAL, - secretVersionTagDAL: secretVersionTagV2BridgeDAL + secretVersionTagDAL: secretVersionTagV2BridgeDAL, + resourceMetadataDAL }) : []; const deletedSecret = secretDeletionCommits.length @@ -824,6 +836,7 @@ export const secretApprovalRequestServiceFactory = ({ } await secretQueueService.syncSecrets({ projectId, + orgId: actorOrgId, secretPath: folder.path, environmentSlug: folder.environmentSlug, actorId, @@ -1208,6 +1221,7 @@ export const secretApprovalRequestServiceFactory = ({ ), skipMultilineEncoding: createdSecret.skipMultilineEncoding, key: createdSecret.secretKey, + secretMetadata: createdSecret.secretMetadata, type: SecretType.Shared })) ); @@ -1263,12 +1277,14 @@ export const secretApprovalRequestServiceFactory = ({ reminderNote, secretComment, metadata, - skipMultilineEncoding + skipMultilineEncoding, + secretMetadata }) => { const secretId = updatingSecretsGroupByKey[secretKey][0].id; if (tagIds?.length) commitTagIds[secretKey] = tagIds; return { ...latestSecretVersions[secretId], + secretMetadata, key: newSecretName || secretKey, encryptedComment: setKnexStringValue( secretComment, @@ -1370,7 +1386,8 @@ export const secretApprovalRequestServiceFactory = ({ reminderRepeatDays, encryptedValue, secretId, - secretVersion + secretVersion, + secretMetadata }) => ({ version, requestId: doc.id, @@ -1383,7 +1400,8 @@ export const secretApprovalRequestServiceFactory = ({ reminderRepeatDays, reminderNote, encryptedComment, - key + key, + secretMetadata: JSON.stringify(secretMetadata) }) ), tx diff --git a/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts b/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts index 50a70fd60..89af253dd 100644 --- a/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts +++ b/backend/src/ee/services/secret-approval-request/secret-approval-request-types.ts @@ -1,5 +1,6 @@ import { TImmutableDBKeys, TSecretApprovalPolicies, TSecretApprovalRequestsSecrets } from "@app/db/schemas"; import { TProjectPermission } from "@app/lib/types"; +import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema"; import { SecretOperations } from "@app/services/secret/secret-types"; export enum RequestState { @@ -34,6 +35,7 @@ export type TApprovalCreateSecretV2Bridge = { reminderRepeatDays?: number | null; skipMultilineEncoding?: boolean; metadata?: Record; + secretMetadata?: ResourceMetadataDTO; tagIds?: string[]; }; diff --git a/backend/src/ee/services/secret-replication/secret-replication-service.ts b/backend/src/ee/services/secret-replication/secret-replication-service.ts index 81d467bab..3c25db98c 100644 --- a/backend/src/ee/services/secret-replication/secret-replication-service.ts +++ b/backend/src/ee/services/secret-replication/secret-replication-service.ts @@ -13,6 +13,8 @@ import { ActorType } from "@app/services/auth/auth-type"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; +import { TResourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal"; +import { ResourceMetadataDTO } from "@app/services/resource-metadata/resource-metadata-schema"; import { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { fnSecretBulkInsert, fnSecretBulkUpdate } from "@app/services/secret/secret-fns"; import { TSecretQueueFactory, uniqueSecretQueueKey } from "@app/services/secret/secret-queue"; @@ -56,6 +58,7 @@ type TSecretReplicationServiceFactoryDep = { >; secretVersionTagDAL: Pick; secretVersionV2TagBridgeDAL: Pick; + resourceMetadataDAL: Pick; secretQueueService: Pick; queueService: Pick; secretApprovalPolicyService: Pick; @@ -121,7 +124,8 @@ export const secretReplicationServiceFactory = ({ secretVersionV2TagBridgeDAL, secretVersionV2BridgeDAL, secretV2BridgeDAL, - kmsService + kmsService, + resourceMetadataDAL }: TSecretReplicationServiceFactoryDep) => { const $getReplicatedSecrets = ( botKey: string, @@ -151,8 +155,10 @@ export const secretReplicationServiceFactory = ({ }; const $getReplicatedSecretsV2 = ( - localSecrets: (TSecretsV2 & { secretKey: string; secretValue?: string })[], - importedSecrets: { secrets: (TSecretsV2 & { secretKey: string; secretValue?: string })[] }[] + localSecrets: (TSecretsV2 & { secretKey: string; secretValue?: string; secretMetadata?: ResourceMetadataDTO })[], + importedSecrets: { + secrets: (TSecretsV2 & { secretKey: string; secretValue?: string; secretMetadata?: ResourceMetadataDTO })[]; + }[] ) => { const deDupe = new Set(); const secrets = [...localSecrets]; @@ -178,6 +184,7 @@ export const secretReplicationServiceFactory = ({ secretPath, environmentSlug, projectId, + orgId, actorId, actor, pickOnlyImportIds, @@ -222,6 +229,7 @@ export const secretReplicationServiceFactory = ({ .map(({ folderId }) => secretQueueService.replicateSecrets({ projectId, + orgId, secretPath: foldersGroupedById[folderId][0]?.path as string, environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string, actorId, @@ -267,6 +275,7 @@ export const secretReplicationServiceFactory = ({ ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined })); + const sourceSecrets = $getReplicatedSecretsV2(sourceDecryptedLocalSecrets, sourceImportedSecrets); const sourceSecretsGroupByKey = groupBy(sourceSecrets, (i) => i.key); @@ -333,13 +342,29 @@ export const secretReplicationServiceFactory = ({ .map((el) => ({ ...el, operation: SecretOperations.Create })); // rewrite update ops to create const locallyUpdatedSecrets = sourceSecrets - .filter( - ({ key, secretKey, secretValue }) => + .filter(({ key, secretKey, secretValue, secretMetadata }) => { + const sourceSecretMetadataJson = JSON.stringify( + (secretMetadata ?? []).map((entry) => ({ + key: entry.key, + value: entry.value + })) + ); + + const destinationSecretMetadataJson = JSON.stringify( + (destinationLocalSecretsGroupedByKey[key]?.[0]?.secretMetadata ?? []).map((entry) => ({ + key: entry.key, + value: entry.value + })) + ); + + return ( destinationLocalSecretsGroupedByKey[key]?.[0] && // if key or value changed (destinationLocalSecretsGroupedByKey[key]?.[0]?.secretKey !== secretKey || - destinationLocalSecretsGroupedByKey[key]?.[0]?.secretValue !== secretValue) - ) + destinationLocalSecretsGroupedByKey[key]?.[0]?.secretValue !== secretValue || + sourceSecretMetadataJson !== destinationSecretMetadataJson) + ); + }) .map((el) => ({ ...el, operation: SecretOperations.Update })); // rewrite update ops to create const locallyDeletedSecrets = destinationLocalSecrets @@ -387,6 +412,7 @@ export const secretReplicationServiceFactory = ({ op: operation, requestId: approvalRequestDoc.id, metadata: doc.metadata, + secretMetadata: JSON.stringify(doc.secretMetadata), key: doc.key, encryptedValue: doc.encryptedValue, encryptedComment: doc.encryptedComment, @@ -406,10 +432,12 @@ export const secretReplicationServiceFactory = ({ if (locallyCreatedSecrets.length) { await fnSecretV2BridgeBulkInsert({ folderId: destinationReplicationFolderId, + orgId, secretVersionDAL: secretVersionV2BridgeDAL, secretDAL: secretV2BridgeDAL, tx, secretTagDAL, + resourceMetadataDAL, secretVersionTagDAL: secretVersionV2TagBridgeDAL, inputSecrets: locallyCreatedSecrets.map((doc) => { return { @@ -419,6 +447,7 @@ export const secretReplicationServiceFactory = ({ encryptedValue: doc.encryptedValue, encryptedComment: doc.encryptedComment, skipMultilineEncoding: doc.skipMultilineEncoding, + secretMetadata: doc.secretMetadata, references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : [] }; }) @@ -426,10 +455,12 @@ export const secretReplicationServiceFactory = ({ } if (locallyUpdatedSecrets.length) { await fnSecretV2BridgeBulkUpdate({ + orgId, folderId: destinationReplicationFolderId, secretVersionDAL: secretVersionV2BridgeDAL, secretDAL: secretV2BridgeDAL, tx, + resourceMetadataDAL, secretTagDAL, secretVersionTagDAL: secretVersionV2TagBridgeDAL, inputSecrets: locallyUpdatedSecrets.map((doc) => { @@ -445,6 +476,7 @@ export const secretReplicationServiceFactory = ({ encryptedValue: doc.encryptedValue as Buffer, encryptedComment: doc.encryptedComment, skipMultilineEncoding: doc.skipMultilineEncoding, + secretMetadata: doc.secretMetadata, references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : [] } }; @@ -466,6 +498,7 @@ export const secretReplicationServiceFactory = ({ await secretQueueService.syncSecrets({ projectId, + orgId, secretPath: destinationFolder.path, environmentSlug: destinationFolder.environmentSlug, actorId, @@ -751,6 +784,7 @@ export const secretReplicationServiceFactory = ({ await secretQueueService.syncSecrets({ projectId, + orgId, secretPath: destinationFolder.path, environmentSlug: destinationFolder.environmentSlug, actorId, diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 293815d7a..57816dc24 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -1150,7 +1150,8 @@ export const INTEGRATION = { shouldMaskSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Masked'.", shouldProtectSecrets: "Specifies if the secrets synced from Infisical to Gitlab should be marked as 'Protected'.", shouldEnableDelete: "The flag to enable deletion of secrets.", - octopusDeployScopeValues: "Specifies the scope values to set on synced secrets to Octopus Deploy." + octopusDeployScopeValues: "Specifies the scope values to set on synced secrets to Octopus Deploy.", + metadataSyncMode: "The mode for syncing metadata to external system" } }, UPDATE: { diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index eafd3467f..05575a63f 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -181,6 +181,7 @@ import { projectUserMembershipRoleDALFactory } from "@app/services/project-membe import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal"; import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service"; import { dailyResourceCleanUpQueueServiceFactory } from "@app/services/resource-cleanup/resource-cleanup-queue"; +import { resourceMetadataDALFactory } from "@app/services/resource-metadata/resource-metadata-dal"; import { secretDALFactory } from "@app/services/secret/secret-dal"; import { secretQueueFactory } from "@app/services/secret/secret-queue"; import { secretServiceFactory } from "@app/services/secret/secret-service"; @@ -374,6 +375,7 @@ export const registerRoutes = async ( const externalGroupOrgRoleMappingDAL = externalGroupOrgRoleMappingDALFactory(db); const projectTemplateDAL = projectTemplateDALFactory(db); + const resourceMetadataDAL = resourceMetadataDALFactory(db); const permissionService = permissionServiceFactory({ permissionDAL, @@ -854,7 +856,8 @@ export const registerRoutes = async ( secretApprovalRequestDAL, projectKeyDAL, projectUserMembershipRoleDAL, - orgService + orgService, + resourceMetadataDAL }); const projectService = projectServiceFactory({ @@ -980,7 +983,8 @@ export const registerRoutes = async ( secretApprovalPolicyService, secretApprovalRequestSecretDAL, kmsService, - snapshotService + snapshotService, + resourceMetadataDAL }); const secretApprovalRequestService = secretApprovalRequestServiceFactory({ @@ -1007,7 +1011,8 @@ export const registerRoutes = async ( projectEnvDAL, userDAL, licenseService, - projectSlackConfigDAL + projectSlackConfigDAL, + resourceMetadataDAL }); const secretService = secretServiceFactory({ @@ -1086,8 +1091,10 @@ export const registerRoutes = async ( kmsService, secretV2BridgeDAL, secretVersionV2TagBridgeDAL: secretVersionTagV2BridgeDAL, - secretVersionV2BridgeDAL + secretVersionV2BridgeDAL, + resourceMetadataDAL }); + const secretRotationQueue = secretRotationQueueFactory({ telemetryService, secretRotationDAL, @@ -1339,7 +1346,8 @@ export const registerRoutes = async ( folderDAL, secretDAL: secretV2BridgeDAL, queueService, - secretV2BridgeService + secretV2BridgeService, + resourceMetadataDAL }); const migrationService = externalMigrationServiceFactory({ diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index d3975bbc2..372fd04a1 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -17,6 +17,7 @@ import { getUserAgentType } from "@app/server/plugins/audit-log"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { SanitizedDynamicSecretSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { AuthMode } from "@app/services/auth/auth-type"; +import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; @@ -116,6 +117,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { secrets: secretRawSchema .extend({ secretPath: z.string().optional(), + secretMetadata: ResourceMetadataSchema.optional(), tags: SecretTagsSchema.pick({ id: true, slug: true, @@ -408,6 +410,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { secrets: secretRawSchema .extend({ secretPath: z.string().optional(), + secretMetadata: ResourceMetadataSchema.optional(), tags: SecretTagsSchema.pick({ id: true, slug: true, @@ -693,6 +696,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { secrets: secretRawSchema .extend({ secretPath: z.string().optional(), + secretMetadata: ResourceMetadataSchema.optional(), tags: SecretTagsSchema.pick({ id: true, slug: true, @@ -864,6 +868,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { secrets: secretRawSchema .extend({ secretPath: z.string().optional(), + secretMetadata: ResourceMetadataSchema.optional(), tags: SecretTagsSchema.pick({ id: true, slug: true, diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 61981bef5..374df8d1d 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -18,6 +18,7 @@ import { getUserAgentType } from "@app/server/plugins/audit-log"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ActorType, AuthMode } from "@app/services/auth/auth-type"; import { ProjectFilterType } from "@app/services/project/project-types"; +import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; import { SecretOperations, SecretProtectionType } from "@app/services/secret/secret-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; @@ -205,6 +206,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secrets: secretRawSchema .extend({ secretPath: z.string().optional(), + secretMetadata: ResourceMetadataSchema.optional(), tags: SecretTagsSchema.pick({ id: true, slug: true, @@ -220,7 +222,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretPath: z.string(), environment: z.string(), folderId: z.string().optional(), - secrets: secretRawSchema.omit({ createdAt: true, updatedAt: true }).array() + secrets: secretRawSchema + .omit({ createdAt: true, updatedAt: true }) + .extend({ + secretMetadata: ResourceMetadataSchema.optional() + }) + .array() }) .array() .optional() @@ -348,7 +355,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }) .extend({ name: z.string() }) .array() - .optional() + .optional(), + secretMetadata: ResourceMetadataSchema.optional() }) }) } @@ -450,6 +458,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { .transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())) .describe(RAW_SECRETS.CREATE.secretValue), secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment), + secretMetadata: ResourceMetadataSchema.optional(), tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds), skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding), type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.CREATE.type), @@ -484,6 +493,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretValue: req.body.secretValue, skipMultilineEncoding: req.body.skipMultilineEncoding, secretComment: req.body.secretComment, + secretMetadata: req.body.secretMetadata, tagIds: req.body.tagIds, secretReminderNote: req.body.secretReminderNote, secretReminderRepeatDays: req.body.secretReminderRepeatDays @@ -558,6 +568,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.UPDATE.type), tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds), metadata: z.record(z.string()).optional(), + secretMetadata: ResourceMetadataSchema.optional(), secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote), secretReminderRepeatDays: z .number() @@ -595,8 +606,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretReminderNote: req.body.secretReminderNote, metadata: req.body.metadata, newSecretName: req.body.newSecretName, - secretComment: req.body.secretComment + secretComment: req.body.secretComment, + secretMetadata: req.body.secretMetadata }); + if (secretOperation.type === SecretProtectionType.Approval) { return { approval: secretOperation.approval }; } @@ -1850,6 +1863,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { secretComment: z.string().trim().optional().default("").describe(RAW_SECRETS.CREATE.secretComment), skipMultilineEncoding: z.boolean().optional().describe(RAW_SECRETS.CREATE.skipMultilineEncoding), metadata: z.record(z.string()).optional(), + secretMetadata: ResourceMetadataSchema.optional(), tagIds: z.string().array().optional().describe(RAW_SECRETS.CREATE.tagIds) }) .array() @@ -1952,6 +1966,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { newSecretName: z.string().min(1).optional().describe(RAW_SECRETS.UPDATE.newSecretName), tagIds: z.string().array().optional().describe(RAW_SECRETS.UPDATE.tagIds), secretReminderNote: z.string().optional().nullable().describe(RAW_SECRETS.UPDATE.secretReminderNote), + secretMetadata: ResourceMetadataSchema.optional(), secretReminderRepeatDays: z .number() .optional() diff --git a/backend/src/services/external-migration/external-migration-fns.ts b/backend/src/services/external-migration/external-migration-fns.ts index 7ae0d0aad..744678792 100644 --- a/backend/src/services/external-migration/external-migration-fns.ts +++ b/backend/src/services/external-migration/external-migration-fns.ts @@ -16,6 +16,7 @@ import { TProjectDALFactory } from "../project/project-dal"; import { TProjectServiceFactory } from "../project/project-service"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvServiceFactory } from "../project-env/project-env-service"; +import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; @@ -35,6 +36,8 @@ export type TImportDataIntoInfisicalDTO = { secretTagDAL: Pick; secretVersionTagDAL: Pick; + resourceMetadataDAL: Pick; + folderDAL: Pick; projectService: Pick; projectEnvService: Pick; @@ -503,6 +506,7 @@ export const importDataIntoInfisicalFn = async ({ secretTagDAL, secretVersionTagDAL, folderDAL, + resourceMetadataDAL, input: { data, actor, actorId, actorOrgId, actorAuthMethod } }: TImportDataIntoInfisicalDTO) => { // Import data to infisical @@ -762,6 +766,8 @@ export const importDataIntoInfisicalFn = async ({ }; }), folderId: selectedFolder.id, + orgId: actorOrgId, + resourceMetadataDAL, secretDAL, secretVersionDAL, secretTagDAL, diff --git a/backend/src/services/external-migration/external-migration-queue.ts b/backend/src/services/external-migration/external-migration-queue.ts index 3cbe8b616..e3d77e832 100644 --- a/backend/src/services/external-migration/external-migration-queue.ts +++ b/backend/src/services/external-migration/external-migration-queue.ts @@ -8,6 +8,7 @@ import { TProjectDALFactory } from "../project/project-dal"; import { TProjectServiceFactory } from "../project/project-service"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvServiceFactory } from "../project-env/project-env-service"; +import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; @@ -35,6 +36,8 @@ export type TExternalMigrationQueueFactoryDep = { projectService: Pick; projectEnvService: Pick; secretV2BridgeService: Pick; + + resourceMetadataDAL: Pick; }; export type TExternalMigrationQueueFactory = ReturnType; @@ -52,7 +55,8 @@ export const externalMigrationQueueFactory = ({ secretVersionDAL, secretTagDAL, secretVersionTagDAL, - folderDAL + folderDAL, + resourceMetadataDAL }: TExternalMigrationQueueFactoryDep) => { const startImport = async (dto: { actorEmail: string; @@ -109,7 +113,8 @@ export const externalMigrationQueueFactory = ({ kmsService, projectService, projectEnvService, - secretV2BridgeService + secretV2BridgeService, + resourceMetadataDAL }); if (projectsNotImported.length) { diff --git a/backend/src/services/integration-auth/integration-list.ts b/backend/src/services/integration-auth/integration-list.ts index d6da2194d..16a717bd6 100644 --- a/backend/src/services/integration-auth/integration-list.ts +++ b/backend/src/services/integration-auth/integration-list.ts @@ -427,3 +427,8 @@ export const getIntegrationOptions = async () => { return INTEGRATION_OPTIONS; }; + +export enum IntegrationMetadataSyncMode { + CUSTOM = "custom", + SECRET_METADATA = "secret-metadata" +} diff --git a/backend/src/services/integration-auth/integration-sync-secret.ts b/backend/src/services/integration-auth/integration-sync-secret.ts index f3010a7d7..f18ced7e7 100644 --- a/backend/src/services/integration-auth/integration-sync-secret.ts +++ b/backend/src/services/integration-auth/integration-sync-secret.ts @@ -38,6 +38,7 @@ import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/ import { TIntegrationDALFactory } from "../integration/integration-dal"; import { IntegrationMetadataSchema } from "../integration/integration-schema"; +import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { IntegrationAuthMetadataSchema } from "./integration-auth-schema"; import { CircleCiScope, @@ -48,6 +49,7 @@ import { import { IntegrationInitialSyncBehavior, IntegrationMappingBehavior, + IntegrationMetadataSyncMode, Integrations, IntegrationUrls } from "./integration-list"; @@ -1082,14 +1084,14 @@ const syncSecretsAWSSecretManager = async ({ projectId }: { integration: TIntegrations; - secrets: Record; + secrets: Record; accessId: string | null; accessToken: string; awsAssumeRoleArn: string | null; projectId?: string; }) => { const appCfg = getConfig(); - const metadata = z.record(z.any()).parse(integration.metadata || {}); + const metadata = IntegrationMetadataSchema.parse(integration.metadata || {}); if (!accessId && !awsAssumeRoleArn) { throw new Error("AWS access ID/AWS Assume Role is required"); @@ -1137,8 +1139,25 @@ const syncSecretsAWSSecretManager = async ({ const processAwsSecret = async ( secretId: string, - secretValue: Record | string + secretValue: Record | string, + secretMetadata?: ResourceMetadataDTO ) => { + const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined; + const shouldTag = + (secretAWSTag && secretAWSTag.length) || + (metadata.metadataSyncMode === IntegrationMetadataSyncMode.SECRET_METADATA && + metadata.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE); + const tagArray = + (metadata.metadataSyncMode === IntegrationMetadataSyncMode.SECRET_METADATA ? secretMetadata : secretAWSTag) ?? []; + + const integrationTagObj = tagArray.reduce( + (acc, item) => { + acc[item.key] = item.value; + return acc; + }, + {} as Record + ); + try { const awsSecretManagerSecret = await secretsManager.send( new GetSecretValueCommand({ @@ -1174,9 +1193,7 @@ const syncSecretsAWSSecretManager = async ({ } } - const secretAWSTag = metadata.secretAWSTag as { key: string; value: string }[] | undefined; - - if (secretAWSTag && secretAWSTag.length) { + if (shouldTag) { const describedSecret = await secretsManager.send( // requires secretsmanager:DescribeSecret policy new DescribeSecretCommand({ @@ -1186,14 +1203,6 @@ const syncSecretsAWSSecretManager = async ({ if (!describedSecret.Tags) return; - const integrationTagObj = secretAWSTag.reduce( - (acc, item) => { - acc[item.key] = item.value; - return acc; - }, - {} as Record - ); - const awsTagObj = (describedSecret.Tags || []).reduce( (acc, item) => { if (item.Key && item.Value) { @@ -1225,7 +1234,7 @@ const syncSecretsAWSSecretManager = async ({ } }); - secretAWSTag?.forEach((tag) => { + tagArray.forEach((tag) => { if (!(tag.key in awsTagObj)) { // create tag in AWS secret manager tagsToUpdate.push({ @@ -1262,8 +1271,8 @@ const syncSecretsAWSSecretManager = async ({ Name: secretId, SecretString: typeof secretValue === "string" ? secretValue : JSON.stringify(secretValue), ...(metadata.kmsKeyId && { KmsKeyId: metadata.kmsKeyId }), - Tags: metadata.secretAWSTag - ? metadata.secretAWSTag.map((tag: { key: string; value: string }) => ({ + Tags: shouldTag + ? tagArray.map((tag: { key: string; value: string }) => ({ Key: tag.key, Value: tag.value })) @@ -1280,7 +1289,7 @@ const syncSecretsAWSSecretManager = async ({ if (metadata.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE) { for await (const [key, value] of Object.entries(secrets)) { - await processAwsSecret(key, value.value); + await processAwsSecret(key, value.value, value.secretMetadata); } } else { await processAwsSecret(integration.app as string, getSecretKeyValuePair(secrets)); @@ -4440,7 +4449,7 @@ export const syncIntegrationSecrets = async ({ secretPath: string; }; integrationAuth: TIntegrationAuths; - secrets: Record; + secrets: Record; accessId: string | null; awsAssumeRoleArn: string | null; accessToken: string; diff --git a/backend/src/services/integration/integration-schema.ts b/backend/src/services/integration/integration-schema.ts index de4790188..084946c0b 100644 --- a/backend/src/services/integration/integration-schema.ts +++ b/backend/src/services/integration/integration-schema.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { INTEGRATION } from "@app/lib/api-docs"; -import { IntegrationMappingBehavior } from "../integration-auth/integration-list"; +import { IntegrationMappingBehavior, IntegrationMetadataSyncMode } from "../integration-auth/integration-list"; export const IntegrationMetadataSchema = z.object({ initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir), @@ -50,6 +50,11 @@ export const IntegrationMetadataSchema = z.object({ shouldMaskSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldMaskSecrets), shouldProtectSecrets: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldProtectSecrets), + metadataSyncMode: z + .nativeEnum(IntegrationMetadataSyncMode) + .optional() + .describe(INTEGRATION.CREATE.metadata.metadataSyncMode), + octopusDeployScopeValues: z .object({ // in Octopus Deploy Scope Value Format diff --git a/backend/src/services/resource-metadata/resource-metadata-dal.ts b/backend/src/services/resource-metadata/resource-metadata-dal.ts new file mode 100644 index 000000000..b8b7a1541 --- /dev/null +++ b/backend/src/services/resource-metadata/resource-metadata-dal.ts @@ -0,0 +1,11 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TResourceMetadataDALFactory = ReturnType; + +export const resourceMetadataDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ResourceMetadata); + + return orm; +}; diff --git a/backend/src/services/resource-metadata/resource-metadata-schema.ts b/backend/src/services/resource-metadata/resource-metadata-schema.ts new file mode 100644 index 000000000..10641e869 --- /dev/null +++ b/backend/src/services/resource-metadata/resource-metadata-schema.ts @@ -0,0 +1,10 @@ +import z from "zod"; + +export const ResourceMetadataSchema = z + .object({ + key: z.string().trim().min(1), + value: z.string().trim().default("") + }) + .array(); + +export type ResourceMetadataDTO = z.infer; diff --git a/backend/src/services/secret-import/secret-import-fns.ts b/backend/src/services/secret-import/secret-import-fns.ts index d75a25514..d21ad3b9c 100644 --- a/backend/src/services/secret-import/secret-import-fns.ts +++ b/backend/src/services/secret-import/secret-import-fns.ts @@ -1,6 +1,7 @@ import { SecretType, TSecretImports, TSecrets, TSecretsV2 } from "@app/db/schemas"; import { groupBy, unique } from "@app/lib/fn"; +import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { TSecretDALFactory } from "../secret/secret-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; @@ -39,6 +40,7 @@ type TSecretImportSecretsV2 = { // But for somereason ts consider ? and undefined explicit as different just ts things secretValue: string; secretComment: string; + secretMetadata?: ResourceMetadataDTO; })[]; }; diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index bb2c54372..3a62fb48a 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -160,6 +160,7 @@ export const secretImportServiceFactory = ({ if (secImport.isReplication && sourceFolder) { await secretQueueService.replicateSecrets({ secretPath: secImport.importPath, + orgId: actorOrgId, projectId, environmentSlug: importEnv.slug, pickOnlyImportIds: [secImport.id], @@ -169,6 +170,7 @@ export const secretImportServiceFactory = ({ } else { await secretQueueService.syncSecrets({ secretPath, + orgId: actorOrgId, projectId, environmentSlug: environment, actorId, @@ -340,6 +342,7 @@ export const secretImportServiceFactory = ({ await secretQueueService.syncSecrets({ secretPath, + orgId: actorOrgId, projectId, environmentSlug: environment, actor, @@ -415,6 +418,7 @@ export const secretImportServiceFactory = ({ if (membership && sourceFolder) { await secretQueueService.replicateSecrets({ + orgId: actorOrgId, secretPath: secretImportDoc.importPath, projectId, environmentSlug: secretImportDoc.importEnv.slug, diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index 3bdd5783f..64ad77deb 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -78,6 +78,12 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id` ) + .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) + .select( + db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") + ) .select(selectAllTableCols(TableName.SecretV2)) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) @@ -103,6 +109,15 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { slug, name: slug }) + }, + { + key: "metadataId", + label: "secretMetadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) } ] }); @@ -221,7 +236,9 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { const secs = await (tx || db.replicaNode())(TableName.SecretV2) .where({ folderId }) .where((bd) => { - void bd.whereNull("userId").orWhere({ userId: userId || null }); + void bd + .whereNull(`${TableName.SecretV2}.userId`) + .orWhere({ [`${TableName.SecretV2}.userId` as "userId"]: userId || null }); }) .leftJoin( TableName.SecretV2JnTag, @@ -233,10 +250,16 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id` ) + .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) .select(selectAllTableCols(TableName.SecretV2)) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) + .select( + db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") + ) .orderBy("id", "asc"); const data = sqlNestRelationships({ @@ -253,6 +276,15 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { slug, name: slug }) + }, + { + key: "metadataId", + label: "secretMetadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) } ] }); @@ -367,7 +399,9 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { } }) .where((bd) => { - void bd.whereNull(`${TableName.SecretV2}.userId`).orWhere({ userId: userId || null }); + void bd + .whereNull(`${TableName.SecretV2}.userId`) + .orWhere({ [`${TableName.SecretV2}.userId` as "userId"]: userId || null }); }) .leftJoin( TableName.SecretV2JnTag, @@ -379,13 +413,23 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id` ) + .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) .select( selectAllTableCols(TableName.SecretV2), - db.raw(`DENSE_RANK() OVER (ORDER BY "key" ${filters?.orderDirection ?? OrderByDirection.ASC}) as rank`) + db.raw( + `DENSE_RANK() OVER (ORDER BY "${TableName.SecretV2}".key ${ + filters?.orderDirection ?? OrderByDirection.ASC + }) as rank` + ) ) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) + .select( + db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") + ) .where((bd) => { const slugs = filters?.tagSlugs?.filter(Boolean); if (slugs && slugs.length > 0) { @@ -425,6 +469,15 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { slug, name: slug }) + }, + { + key: "metadataId", + label: "secretMetadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) } ] }); @@ -545,10 +598,17 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { `${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id` ) + .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) .select(selectAllTableCols(TableName.SecretV2)) .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) - .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")); + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) + .select( + db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), + db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), + db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") + ); + const docs = sqlNestRelationships({ data: rawDocs, key: "id", @@ -563,6 +623,15 @@ export const secretV2BridgeDALFactory = (db: TDbClient) => { slug, name: slug }) + }, + { + key: "metadataId", + label: "secretMetadata" as const, + mapper: ({ metadataKey, metadataValue, metadataId }) => ({ + id: metadataId, + key: metadataKey, + value: metadataValue + }) } ] }); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts index 79aa22799..cc40b0f26 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-fns.ts @@ -6,6 +6,7 @@ import { groupBy } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; +import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types"; @@ -54,9 +55,11 @@ export const getAllSecretReferences = (maybeSecretReference: string) => { export const fnSecretBulkInsert = async ({ // TODO: Pick types here folderId, + orgId, inputSecrets, secretDAL, secretVersionDAL, + resourceMetadataDAL, secretTagDAL, secretVersionTagDAL, tx @@ -91,6 +94,7 @@ export const fnSecretBulkInsert = async ({ sanitizedInputSecrets.map((el) => ({ ...el, folderId })), tx ); + const newSecretGroupedByKeyName = groupBy(newSecrets, (item) => item.key); const newSecretTags = inputSecrets.flatMap(({ tagIds: secretTags = [], key }) => secretTags.map((tag) => ({ @@ -106,6 +110,7 @@ export const fnSecretBulkInsert = async ({ })), tx ); + await secretDAL.upsertSecretReferences( inputSecrets.map(({ references = [], key }) => ({ secretId: newSecretGroupedByKeyName[key][0].id, @@ -113,6 +118,22 @@ export const fnSecretBulkInsert = async ({ })), tx ); + + await resourceMetadataDAL.insertMany( + inputSecrets.flatMap(({ key: secretKey, secretMetadata }) => { + if (secretMetadata) { + return secretMetadata.map(({ key, value }) => ({ + key, + value, + secretId: newSecretGroupedByKeyName[secretKey][0].id, + orgId + })); + } + return []; + }), + tx + ); + if (newSecretTags.length) { const secTags = await secretTagDAL.saveTagsToSecretV2(newSecretTags, tx); const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId); @@ -120,6 +141,7 @@ export const fnSecretBulkInsert = async ({ [`${TableName.SecretVersionV2}Id` as const]: secVersionsGroupBySecId[secrets_v2Id][0].id, [`${TableName.SecretTag}Id` as const]: secret_tagsId })); + await secretVersionTagDAL.insertMany(newSecretVersionTags, tx); } @@ -130,10 +152,12 @@ export const fnSecretBulkUpdate = async ({ tx, inputSecrets, folderId, + orgId, secretDAL, secretVersionDAL, secretTagDAL, - secretVersionTagDAL + secretVersionTagDAL, + resourceMetadataDAL }: TFnSecretBulkUpdate) => { const sanitizedInputSecrets = inputSecrets.map( ({ @@ -231,6 +255,34 @@ export const fnSecretBulkUpdate = async ({ } } + const inputSecretIdsWithMetadata = inputSecrets + .filter((sec) => Boolean(sec.data.secretMetadata)) + .map((sec) => sec.filter.id); + + await resourceMetadataDAL.delete( + { + $in: { + secretId: inputSecretIdsWithMetadata + } + }, + tx + ); + + await resourceMetadataDAL.insertMany( + inputSecrets.flatMap(({ filter: { id }, data: { secretMetadata } }) => { + if (secretMetadata) { + return secretMetadata.map(({ key, value }) => ({ + key, + value, + secretId: id, + orgId + })); + } + return []; + }), + tx + ); + return newSecrets.map((secret) => ({ ...secret, _id: secret.id })); }; @@ -570,6 +622,7 @@ export const reshapeBridgeSecret = ( color?: string | null; name: string; }[]; + secretMetadata?: ResourceMetadataDTO; } ) => ({ secretKey: secret.key, @@ -588,6 +641,7 @@ export const reshapeBridgeSecret = ( secretReminderRepeatDays: secret.reminderRepeatDays, secretReminderNote: secret.reminderNote, metadata: secret.metadata, + secretMetadata: secret.secretMetadata, createdAt: secret.createdAt, updatedAt: secret.updatedAt }); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index e13a77432..a0f93626d 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -18,6 +18,7 @@ import { ActorType } from "../auth/auth-type"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; +import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; import { TSecretQueueFactory } from "../secret/secret-queue"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; @@ -74,6 +75,7 @@ type TSecretV2BridgeServiceFactoryDep = { "insertV2Bridge" | "insertApprovalSecretV2Tags" >; snapshotService: Pick; + resourceMetadataDAL: Pick; }; export type TSecretV2BridgeServiceFactory = ReturnType; @@ -95,7 +97,8 @@ export const secretV2BridgeServiceFactory = ({ secretApprovalPolicyService, secretApprovalRequestDAL, secretApprovalRequestSecretDAL, - kmsService + kmsService, + resourceMetadataDAL }: TSecretV2BridgeServiceFactoryDep) => { const $validateSecretReferences = async ( projectId: string, @@ -141,7 +144,7 @@ export const secretV2BridgeServiceFactory = ({ }, { operator: "eq", - field: "key", + field: `${TableName.SecretV2}.key` as "key", value: el.secretKey } ] @@ -186,6 +189,7 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, projectId, secretPath, + secretMetadata, ...inputSecret }: TCreateSecretDTO) => { const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( @@ -255,6 +259,7 @@ export const secretV2BridgeServiceFactory = ({ const secret = await secretDAL.transaction((tx) => fnSecretBulkInsert({ folderId, + orgId: actorOrgId, inputSecrets: [ { version: 1, @@ -272,9 +277,11 @@ export const secretV2BridgeServiceFactory = ({ key: secretName, userId: inputSecret.type === SecretType.Personal ? actorId : null, tagIds: inputSecret.tagIds, - references: nestedReferences + references: nestedReferences, + secretMetadata } ], + resourceMetadataDAL, secretDAL, secretVersionDAL, secretTagDAL, @@ -287,6 +294,7 @@ export const secretV2BridgeServiceFactory = ({ await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ secretPath, + orgId: actorOrgId, actorId, actor, projectId, @@ -309,6 +317,7 @@ export const secretV2BridgeServiceFactory = ({ actorAuthMethod, projectId, secretPath, + secretMetadata, ...inputSecret }: TUpdateSecretDTO) => { const { permission, ForbidOnInvalidProjectType } = await permissionService.getProjectPermission( @@ -435,6 +444,8 @@ export const secretV2BridgeServiceFactory = ({ const updatedSecret = await secretDAL.transaction(async (tx) => fnSecretBulkUpdate({ folderId, + orgId: actorOrgId, + resourceMetadataDAL, inputSecrets: [ { filter: { id: secretId }, @@ -448,6 +459,7 @@ export const secretV2BridgeServiceFactory = ({ skipMultilineEncoding: inputSecret.skipMultilineEncoding, key: inputSecret.newSecretName || secretName, tags: inputSecret.tagIds, + secretMetadata, ...encryptedValue } } @@ -475,6 +487,7 @@ export const secretV2BridgeServiceFactory = ({ actorId, actor, projectId, + orgId: actorOrgId, environmentSlug: folder.environment.slug }); } @@ -562,6 +575,7 @@ export const secretV2BridgeServiceFactory = ({ actorId, actor, projectId, + orgId: actorOrgId, environmentSlug: folder.environment.slug }); } @@ -961,8 +975,8 @@ export const secretV2BridgeServiceFactory = ({ ? secretDAL.findOneWithTags({ folderId, type: secretType, - key: secretName, - userId: secretType === SecretType.Personal ? actorId : null + [`${TableName.SecretV2}.key` as "key"]: secretName, + [`${TableName.SecretV2}.userId` as "userId"]: secretType === SecretType.Personal ? actorId : null }) : secretVersionDAL .findOne({ @@ -1113,7 +1127,7 @@ export const secretV2BridgeServiceFactory = ({ value: [ { operator: "eq", - field: "key", + field: `${TableName.SecretV2}.key` as "key", value: el.secretKey }, { @@ -1185,11 +1199,14 @@ export const secretV2BridgeServiceFactory = ({ key: el.secretKey, tagIds: el.tagIds, references, + secretMetadata: el.secretMetadata, type: SecretType.Shared }; }), folderId, + orgId: actorOrgId, secretDAL, + resourceMetadataDAL, secretVersionDAL, secretTagDAL, secretVersionTagDAL, @@ -1203,6 +1220,7 @@ export const secretV2BridgeServiceFactory = ({ actorId, secretPath, projectId, + orgId: actorOrgId, environmentSlug: folder.environment.slug }); @@ -1254,7 +1272,7 @@ export const secretV2BridgeServiceFactory = ({ value: [ { operator: "eq", - field: "key", + field: `${TableName.SecretV2}.key` as "key", value: el.secretKey }, { @@ -1319,7 +1337,7 @@ export const secretV2BridgeServiceFactory = ({ value: [ { operator: "eq", - field: "key", + field: `${TableName.SecretV2}.key` as "key", value: el.secretKey }, { @@ -1371,6 +1389,7 @@ export const secretV2BridgeServiceFactory = ({ const secrets = await secretDAL.transaction(async (tx) => fnSecretBulkUpdate({ folderId, + orgId: actorOrgId, tx, inputSecrets: inputSecrets.map((el) => { const originalSecret = secretsToUpdateInDBGroupedByKey[el.secretKey][0]; @@ -1394,6 +1413,7 @@ export const secretV2BridgeServiceFactory = ({ skipMultilineEncoding: el.skipMultilineEncoding, key: el.newSecretName || el.secretKey, tags: el.tagIds, + secretMetadata: el.secretMetadata, ...encryptedValue } }; @@ -1401,7 +1421,8 @@ export const secretV2BridgeServiceFactory = ({ secretDAL, secretVersionDAL, secretTagDAL, - secretVersionTagDAL + secretVersionTagDAL, + resourceMetadataDAL }) ); await snapshotService.performSnapshot(folderId); @@ -1410,6 +1431,7 @@ export const secretV2BridgeServiceFactory = ({ actorId, secretPath, projectId, + orgId: actorOrgId, environmentSlug: folder.environment.slug }); @@ -1461,7 +1483,7 @@ export const secretV2BridgeServiceFactory = ({ value: [ { operator: "eq", - field: "key", + field: `${TableName.SecretV2}.key` as "key", value: el.secretKey }, { @@ -1512,6 +1534,7 @@ export const secretV2BridgeServiceFactory = ({ actorId, secretPath, projectId, + orgId: actorOrgId, environmentSlug: folder.environment.slug }); @@ -1815,10 +1838,12 @@ export const secretV2BridgeServiceFactory = ({ if (locallyCreatedSecrets.length) { await fnSecretBulkInsert({ folderId: destinationFolder.id, + orgId: actorOrgId, secretVersionDAL, secretDAL, tx, secretTagDAL, + resourceMetadataDAL, secretVersionTagDAL, inputSecrets: locallyCreatedSecrets.map((doc) => { return { @@ -1830,6 +1855,7 @@ export const secretV2BridgeServiceFactory = ({ skipMultilineEncoding: doc.skipMultilineEncoding, reminderNote: doc.reminderNote, reminderRepeatDays: doc.reminderRepeatDays, + secretMetadata: doc.secretMetadata, references: doc.value ? getAllSecretReferences(doc.value).nestedReferences : [] }; }) @@ -1838,6 +1864,8 @@ export const secretV2BridgeServiceFactory = ({ if (locallyUpdatedSecrets.length) { await fnSecretBulkUpdate({ folderId: destinationFolder.id, + orgId: actorOrgId, + resourceMetadataDAL, secretVersionDAL, secretDAL, tx, @@ -1855,6 +1883,7 @@ export const secretV2BridgeServiceFactory = ({ encryptedComment: doc.encryptedComment, skipMultilineEncoding: doc.skipMultilineEncoding, reminderNote: doc.reminderNote, + secretMetadata: doc.secretMetadata, reminderRepeatDays: doc.reminderRepeatDays, ...(doc.encryptedValue ? { @@ -1938,6 +1967,7 @@ export const secretV2BridgeServiceFactory = ({ await snapshotService.performSnapshot(destinationFolder.id); await secretQueueService.syncSecrets({ projectId, + orgId: actorOrgId, secretPath: destinationFolder.path, environmentSlug: destinationFolder.environment.slug, actorId, @@ -1949,6 +1979,7 @@ export const secretV2BridgeServiceFactory = ({ await snapshotService.performSnapshot(sourceFolder.id); await secretQueueService.syncSecrets({ projectId, + orgId: actorOrgId, secretPath: sourceFolder.path, environmentSlug: sourceFolder.environment.slug, actorId, diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index 7216989ff..5e5cc26cd 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -7,6 +7,8 @@ import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; +import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; +import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; import { TSecretVersionV2DALFactory } from "./secret-version-dal"; import { TSecretVersionV2TagDALFactory } from "./secret-version-tag-dal"; @@ -58,6 +60,7 @@ export type TCreateSecretDTO = TProjectPermission & { skipMultilineEncoding?: boolean; secretReminderRepeatDays?: number | null; secretReminderNote?: string | null; + secretMetadata?: ResourceMetadataDTO; }; export type TUpdateSecretDTO = TProjectPermission & { @@ -75,6 +78,7 @@ export type TUpdateSecretDTO = TProjectPermission & { metadata?: { source?: string; }; + secretMetadata?: ResourceMetadataDTO; }; export type TDeleteSecretDTO = TProjectPermission & { @@ -94,6 +98,7 @@ export type TCreateManySecretDTO = Omit & { secretComment?: string; skipMultilineEncoding?: boolean; tagIds?: string[]; + secretMetadata?: ResourceMetadataDTO; metadata?: { source?: string; }; @@ -113,6 +118,7 @@ export type TUpdateManySecretDTO = Omit & { tagIds?: string[]; secretReminderRepeatDays?: number | null; secretReminderNote?: string | null; + secretMetadata?: ResourceMetadataDTO; }[]; }; @@ -136,8 +142,16 @@ export type TSecretReference = { environment: string; secretPath: string; secret export type TFnSecretBulkInsert = { folderId: string; + orgId: string; tx?: Knex; - inputSecrets: Array & { tagIds?: string[]; references: TSecretReference[] }>; + inputSecrets: Array< + Omit & { + tagIds?: string[]; + references: TSecretReference[]; + secretMetadata?: ResourceMetadataDTO; + } + >; + resourceMetadataDAL: Pick; secretDAL: Pick; secretVersionDAL: Pick; secretTagDAL: Pick; @@ -156,10 +170,12 @@ type TRequireReferenceIfValue = export type TFnSecretBulkUpdate = { folderId: string; + orgId: string; inputSecrets: { filter: Partial; - data: TRequireReferenceIfValue & { tags?: string[] }; + data: TRequireReferenceIfValue & { tags?: string[]; secretMetadata?: ResourceMetadataDTO }; }[]; + resourceMetadataDAL: Pick; secretDAL: Pick; secretVersionDAL: Pick; secretTagDAL: Pick; diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 6336c479d..29d381bfa 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -749,7 +749,8 @@ export const createManySecretsRawFnFactory = ({ secretVersionV2BridgeDAL, secretV2BridgeDAL, secretVersionTagV2BridgeDAL, - kmsService + kmsService, + resourceMetadataDAL }: TCreateManySecretsRawFnFactory) => { const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL); const createManySecretsRawFn = async ({ @@ -760,7 +761,7 @@ export const createManySecretsRawFnFactory = ({ userId }: TCreateManySecretsRawFn) => { const { botKey, shouldUseSecretV2Bridge } = await getBotKeyFn(projectId); - + const project = await projectDAL.findById(projectId); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) throw new NotFoundError({ @@ -814,7 +815,9 @@ export const createManySecretsRawFnFactory = ({ tagIds: el.tags })), folderId, + orgId: project.orgId, secretDAL: secretV2BridgeDAL, + resourceMetadataDAL, secretVersionDAL: secretVersionV2BridgeDAL, secretTagDAL, secretVersionTagDAL: secretVersionTagV2BridgeDAL, @@ -909,6 +912,7 @@ export const updateManySecretsRawFnFactory = ({ secretVersionTagV2BridgeDAL, secretVersionV2BridgeDAL, secretV2BridgeDAL, + resourceMetadataDAL, kmsService }: TUpdateManySecretsRawFnFactory) => { const getBotKeyFn = getBotKeyFnFactory(projectBotDAL, projectDAL); @@ -920,6 +924,7 @@ export const updateManySecretsRawFnFactory = ({ userId }: TUpdateManySecretsRawFn): Promise> => { const { botKey, shouldUseSecretV2Bridge } = await getBotKeyFn(projectId); + const project = await projectDAL.findById(projectId); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); if (!folder) @@ -988,11 +993,13 @@ export const updateManySecretsRawFnFactory = ({ const updatedSecrets = await secretDAL.transaction(async (tx) => fnSecretV2BridgeBulkUpdate({ folderId, + orgId: project.orgId, tx, inputSecrets: inputSecrets.map((el) => ({ filter: { id: secretsToUpdateInDBGroupedByKey[el.key][0].id, type: SecretType.Shared }, data: el })), + resourceMetadataDAL, secretDAL: secretV2BridgeDAL, secretVersionDAL: secretVersionV2BridgeDAL, secretTagDAL, diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 9b2f80225..8bb1e03d2 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -47,6 +47,8 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; +import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; +import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns"; @@ -104,6 +106,7 @@ type TSecretQueueFactoryDep = { auditLogService: Pick; orgService: Pick; projectUserMembershipRoleDAL: Pick; + resourceMetadataDAL: Pick; }; export type TGetSecrets = { @@ -120,7 +123,12 @@ export const uniqueSecretQueueKey = (environment: string, secretPath: string) => type TIntegrationSecret = Record< string, - { value: string; comment?: string; skipMultilineEncoding?: boolean | null | undefined } + { + value: string; + comment?: string; + skipMultilineEncoding?: boolean | null | undefined; + secretMetadata?: ResourceMetadataDTO; + } >; // TODO(akhilmhdh): split this into multiple queue @@ -157,7 +165,8 @@ export const secretQueueFactory = ({ auditLogService, orgService, projectUserMembershipRoleDAL, - projectKeyDAL + projectKeyDAL, + resourceMetadataDAL }: TSecretQueueFactoryDep) => { const integrationMeter = opentelemetry.metrics.getMeter("Integrations"); const errorHistogram = integrationMeter.createHistogram("integration_secret_sync_errors", { @@ -306,7 +315,8 @@ export const secretQueueFactory = ({ kmsService, secretVersionV2BridgeDAL, secretV2BridgeDAL, - secretVersionTagV2BridgeDAL + secretVersionTagV2BridgeDAL, + resourceMetadataDAL }); const updateManySecretsRawFn = updateManySecretsRawFnFactory({ @@ -321,7 +331,8 @@ export const secretQueueFactory = ({ kmsService, secretVersionV2BridgeDAL, secretV2BridgeDAL, - secretVersionTagV2BridgeDAL + secretVersionTagV2BridgeDAL, + resourceMetadataDAL }); /** @@ -372,6 +383,7 @@ export const secretQueueFactory = ({ } content[secretKey].skipMultilineEncoding = Boolean(secret.skipMultilineEncoding); + content[secretKey].secretMetadata = secret.secretMetadata; }) ); @@ -397,7 +409,8 @@ export const secretQueueFactory = ({ content[importedSecret.key] = { skipMultilineEncoding: importedSecret.skipMultilineEncoding, comment: importedSecret.secretComment, - value: importedSecret.secretValue || "" + value: importedSecret.secretValue || "", + secretMetadata: importedSecret.secretMetadata }; } } @@ -597,6 +610,7 @@ export const secretQueueFactory = ({ _depth: depth, secretPath, projectId, + orgId, environmentSlug: environment, excludeReplication, actorId, @@ -625,6 +639,7 @@ export const secretQueueFactory = ({ _deDupeReplicationQueue: deDupeReplicationQueue, _depth: depth, projectId, + orgId, secretPath, actorId, actor, @@ -681,6 +696,7 @@ export const secretQueueFactory = ({ if (!folder) { throw new Error("Secret path not found"); } + const project = await projectDAL.findById(projectId); // find all imports made with the given environment and secret path const linkSourceDto = { @@ -715,6 +731,7 @@ export const secretQueueFactory = ({ .map(({ folderId }) => syncSecrets({ projectId, + orgId: project.orgId, secretPath: foldersGroupedById[folderId][0]?.path as string, environmentSlug: foldersGroupedById[folderId][0]?.environmentSlug as string, _deDupeQueue: deDupeQueue, @@ -767,6 +784,7 @@ export const secretQueueFactory = ({ .map((folderId) => syncSecrets({ projectId, + orgId: project.orgId, secretPath: referencedFoldersGroupedById[folderId][0]?.path as string, environmentSlug: referencedFoldersGroupedById[folderId][0]?.environmentSlug as string, _deDupeQueue: deDupeQueue, diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index fbf90a7f8..7fe6f824c 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -288,6 +288,7 @@ export const secretServiceFactory = ({ actorId, actor, projectId, + orgId: actorOrgId, environmentSlug: folder.environment.slug }); } @@ -429,6 +430,7 @@ export const secretServiceFactory = ({ await snapshotService.performSnapshot(folderId); await secretQueueService.syncSecrets({ secretPath: path, + orgId: actorOrgId, actorId, actor, projectId, @@ -526,6 +528,7 @@ export const secretServiceFactory = ({ actorId, actor, projectId, + orgId: actorOrgId, environmentSlug: folder.environment.slug }); } @@ -820,6 +823,7 @@ export const secretServiceFactory = ({ actorId, secretPath: path, projectId, + orgId: actorOrgId, environmentSlug: folder.environment.slug }); @@ -928,6 +932,7 @@ export const secretServiceFactory = ({ actorId, secretPath: path, projectId, + orgId: actorOrgId, environmentSlug: folder.environment.slug }); @@ -1014,6 +1019,7 @@ export const secretServiceFactory = ({ actorId, secretPath: path, projectId, + orgId: actorOrgId, environmentSlug: folder.environment.slug }); @@ -1385,7 +1391,8 @@ export const secretServiceFactory = ({ skipMultilineEncoding, tagIds, secretReminderNote, - secretReminderRepeatDays + secretReminderRepeatDays, + secretMetadata }: TCreateSecretRawDTO) => { const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const policy = @@ -1412,7 +1419,8 @@ export const secretServiceFactory = ({ secretValue, tagIds, reminderNote: secretReminderNote, - reminderRepeatDays: secretReminderRepeatDays + reminderRepeatDays: secretReminderRepeatDays, + secretMetadata } ] } @@ -1435,7 +1443,8 @@ export const secretServiceFactory = ({ tagIds, secretReminderNote, skipMultilineEncoding, - secretReminderRepeatDays + secretReminderRepeatDays, + secretMetadata }); return { secret, type: SecretProtectionType.Direct as const }; } @@ -1525,7 +1534,8 @@ export const secretServiceFactory = ({ secretReminderRepeatDays, metadata, secretComment, - newSecretName + newSecretName, + secretMetadata }: TUpdateSecretRawDTO) => { const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const policy = @@ -1553,7 +1563,8 @@ export const secretServiceFactory = ({ secretValue, tagIds, reminderNote: secretReminderNote, - reminderRepeatDays: secretReminderRepeatDays + reminderRepeatDays: secretReminderRepeatDays, + secretMetadata } ] } @@ -1577,7 +1588,8 @@ export const secretServiceFactory = ({ secretName, newSecretName, metadata, - secretValue + secretValue, + secretMetadata }); return { type: SecretProtectionType.Direct as const, secret }; } @@ -1793,7 +1805,8 @@ export const secretServiceFactory = ({ secretComment: el.secretComment, metadata: el.metadata, skipMultilineEncoding: el.skipMultilineEncoding, - secretKey: el.secretKey + secretKey: el.secretKey, + secretMetadata: el.secretMetadata })) } }); @@ -1919,7 +1932,8 @@ export const secretServiceFactory = ({ secretValue: el.secretValue, secretComment: el.secretComment, skipMultilineEncoding: el.skipMultilineEncoding, - secretKey: el.secretKey + secretKey: el.secretKey, + secretMetadata: el.secretMetadata })) } }); @@ -2262,6 +2276,7 @@ export const secretServiceFactory = ({ await secretQueueService.syncSecrets({ secretPath, projectId: project.id, + orgId: project.orgId, environmentSlug: environment, excludeReplication: true }); @@ -2370,6 +2385,7 @@ export const secretServiceFactory = ({ await secretQueueService.syncSecrets({ secretPath, projectId: project.id, + orgId: project.orgId, environmentSlug: environment, excludeReplication: true }); @@ -2828,6 +2844,7 @@ export const secretServiceFactory = ({ await snapshotService.performSnapshot(destinationFolder.id); await secretQueueService.syncSecrets({ projectId: project.id, + orgId: project.orgId, secretPath: destinationFolder.path, environmentSlug: destinationFolder.environment.slug, actorId, @@ -2839,6 +2856,7 @@ export const secretServiceFactory = ({ await snapshotService.performSnapshot(sourceFolder.id); await secretQueueService.syncSecrets({ projectId: project.id, + orgId: project.orgId, secretPath: sourceFolder.path, environmentSlug: sourceFolder.environment.slug, actorId, diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index ca5c5a74b..0bcf6ed13 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -14,6 +14,8 @@ import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; import { ActorType } from "../auth/auth-type"; import { TKmsServiceFactory } from "../kms/kms-service"; +import { TResourceMetadataDALFactory } from "../resource-metadata/resource-metadata-dal"; +import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal"; @@ -211,6 +213,7 @@ export type TCreateSecretRawDTO = TProjectPermission & { skipMultilineEncoding?: boolean; secretReminderRepeatDays?: number | null; secretReminderNote?: string | null; + secretMetadata?: ResourceMetadataDTO; }; export type TUpdateSecretRawDTO = TProjectPermission & { @@ -228,6 +231,7 @@ export type TUpdateSecretRawDTO = TProjectPermission & { metadata?: { source?: string; }; + secretMetadata?: ResourceMetadataDTO; }; export type TDeleteSecretRawDTO = TProjectPermission & { @@ -248,6 +252,7 @@ export type TCreateManySecretRawDTO = Omit & { secretComment?: string; skipMultilineEncoding?: boolean; tagIds?: string[]; + secretMetadata?: ResourceMetadataDTO; metadata?: { source?: string; }; @@ -266,6 +271,7 @@ export type TUpdateManySecretRawDTO = Omit & { secretComment?: string; skipMultilineEncoding?: boolean; tagIds?: string[]; + secretMetadata?: ResourceMetadataDTO; secretReminderRepeatDays?: number | null; secretReminderNote?: string | null; }[]; @@ -293,7 +299,13 @@ export type TSecretReference = { environment: string; secretPath: string }; export type TFnSecretBulkInsert = { folderId: string; tx?: Knex; - inputSecrets: Array & { tags?: string[]; references?: TSecretReference[] }>; + inputSecrets: Array< + Omit & { + tags?: string[]; + references?: TSecretReference[]; + secretMetadata?: ResourceMetadataDTO; + } + >; secretDAL: Pick; secretVersionDAL: Pick; secretTagDAL: Pick; @@ -389,6 +401,7 @@ export type TCreateManySecretsRawFnFactory = { >; secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; + resourceMetadataDAL: Pick; }; export type TCreateManySecretsRawFn = { @@ -425,6 +438,7 @@ export type TUpdateManySecretsRawFnFactory = { >; secretVersionV2BridgeDAL: Pick; secretVersionTagV2BridgeDAL: Pick; + resourceMetadataDAL: Pick; }; export type TUpdateManySecretsRawFn = { @@ -460,6 +474,7 @@ export type TSyncSecretsDTO = { _depth?: number; secretPath: string; projectId: string; + orgId: string; environmentSlug: string; // cases for just doing sync integration and webhook excludeReplication?: T; diff --git a/docs/images/integrations/aws/integrations-aws-secret-manager-options.png b/docs/images/integrations/aws/integrations-aws-secret-manager-options.png index f8492cdfa..9450df8b7 100644 Binary files a/docs/images/integrations/aws/integrations-aws-secret-manager-options.png and b/docs/images/integrations/aws/integrations-aws-secret-manager-options.png differ diff --git a/docs/integrations/cloud/aws-secret-manager.mdx b/docs/integrations/cloud/aws-secret-manager.mdx index 64df1df32..1f3a0da1f 100644 --- a/docs/integrations/cloud/aws-secret-manager.mdx +++ b/docs/integrations/cloud/aws-secret-manager.mdx @@ -17,6 +17,7 @@ Prerequisites: If your instance is deployed on AWS, the aws-sdk will automatically retrieve the credentials. Ensure that you assign the provided permission policy to your deployed instance, such as ECS or EC2. The following steps are for instances not deployed on AWS + Navigate to [Create IAM User](https://console.aws.amazon.com/iamv2/home#/users/create) in your AWS Console. @@ -40,9 +41,10 @@ The following steps are for instances not deployed on AWS Obtain the AWS access key ID and secret access key for your IAM User by navigating to IAM > Users > [Your User] > Security credentials > Access keys. - ![Access Key Step 1](../../images/integrations/aws/integrations-aws-access-key-1.png) - ![Access Key Step 2](../../images/integrations/aws/integrations-aws-access-key-2.png) - ![Access Key Step 3](../../images/integrations/aws/integrations-aws-access-key-3.png) +![Access Key Step 1](../../images/integrations/aws/integrations-aws-access-key-1.png) +![Access Key Step 2](../../images/integrations/aws/integrations-aws-access-key-2.png) +![Access Key Step 3](../../images/integrations/aws/integrations-aws-access-key-3.png) + 1. Set the access key as **CLIENT_ID_AWS_INTEGRATION**. @@ -59,6 +61,7 @@ The following steps are for instances not deployed on AWS 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. + @@ -89,11 +92,13 @@ The following steps are for instances not deployed on AWS ] } ``` + - - ![Copy IAM Role ARN](../../images/integrations/aws/integration-aws-iam-assume-arn.png) - + + ![Copy IAM Role + ARN](../../images/integrations/aws/integration-aws-iam-assume-arn.png) + 1. Navigate to your project's integrations tab in Infisical. @@ -104,6 +109,7 @@ The following steps are for instances not deployed on AWS ![Select Assume Role](../../images/integrations/aws/integration-aws-iam-assume-select.png) 4. Provide the **AWS IAM Role ARN** obtained from the previous step. + Select how you want to integration to work by specifying a number of parameters: @@ -127,6 +133,12 @@ The following steps are for instances not deployed on AWS Optionally, you can add tags or specify the encryption key of all the secrets created via this integration: + + The sync mode for AWS tags. The supported options are `Secret Metadata` and `Custom`. If `Secret Metadata` is selected, + the metadata of the Infisical secrets are used as tags in AWS. If custom is selected, then the key/value of the **Secret Tag** field is used. `Secret Metadata` mode + is only supported for one-to-one integrations. + + The Key/Value of a tag that will be added to secrets in AWS. Please note that it is possible to add multiple tags via API. diff --git a/frontend/src/hooks/api/integrations/queries.tsx b/frontend/src/hooks/api/integrations/queries.tsx index 2b90d5e9a..a439f8caa 100644 --- a/frontend/src/hooks/api/integrations/queries.tsx +++ b/frontend/src/hooks/api/integrations/queries.tsx @@ -4,7 +4,12 @@ import { createNotification } from "@app/components/notifications"; import { apiRequest } from "@app/config/request"; import { workspaceKeys } from "../workspace"; -import { TCloudIntegration, TIntegrationWithEnv, TOctopusDeployScopeValues } from "./types"; +import { + IntegrationMetadataSyncMode, + TCloudIntegration, + TIntegrationWithEnv, + TOctopusDeployScopeValues +} from "./types"; export const integrationQueryKeys = { getIntegrations: () => ["integrations"] as const, @@ -88,6 +93,7 @@ export const useCreateIntegration = () => { shouldProtectSecrets?: boolean; shouldEnableDelete?: boolean; octopusDeployScopeValues?: TOctopusDeployScopeValues; + metadataSyncMode?: IntegrationMetadataSyncMode; }; }) => { const { diff --git a/frontend/src/hooks/api/integrations/types.ts b/frontend/src/hooks/api/integrations/types.ts index 7054befc7..b7adb0f53 100644 --- a/frontend/src/hooks/api/integrations/types.ts +++ b/frontend/src/hooks/api/integrations/types.ts @@ -62,6 +62,7 @@ export type TIntegration = { octopusDeployScopeValues?: TOctopusDeployScopeValues; awsIamRole?: string; region?: string; + metadataSyncMode?: IntegrationMetadataSyncMode; }; }; @@ -92,3 +93,8 @@ export enum IntegrationMappingBehavior { ONE_TO_ONE = "one-to-one", MANY_TO_ONE = "many-to-one" } + +export enum IntegrationMetadataSyncMode { + CUSTOM = "custom", + SECRET_METADATA = "secret-metadata" +} diff --git a/frontend/src/hooks/api/secretApprovalRequest/types.ts b/frontend/src/hooks/api/secretApprovalRequest/types.ts index c8a685115..a03526dae 100644 --- a/frontend/src/hooks/api/secretApprovalRequest/types.ts +++ b/frontend/src/hooks/api/secretApprovalRequest/types.ts @@ -82,6 +82,7 @@ export type TSecretApprovalRequest = { conflicts: Array<{ secretId: string; op: CommitType.UPDATE }>; commits: ({ // if there is no secret means it was creation + secretMetadata?: { key: string; value: string }[]; secret?: { version: number }; secretVersion: SecretV3Raw; // if there is no new version its for Delete diff --git a/frontend/src/hooks/api/secrets/mutations.tsx b/frontend/src/hooks/api/secrets/mutations.tsx index efdb866cb..68453ecdd 100644 --- a/frontend/src/hooks/api/secrets/mutations.tsx +++ b/frontend/src/hooks/api/secrets/mutations.tsx @@ -84,7 +84,8 @@ export const useUpdateSecretV3 = ({ secretReminderRepeatDays, secretReminderNote, newSecretName, - skipMultilineEncoding + skipMultilineEncoding, + secretMetadata }) => { const { data } = await apiRequest.patch(`/api/v3/secrets/raw/${secretKey}`, { workspaceId, @@ -97,7 +98,8 @@ export const useUpdateSecretV3 = ({ newSecretName, secretComment, tagIds, - secretValue + secretValue, + secretMetadata }); return data; }, diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index 4b95579c2..04610e36b 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -67,7 +67,8 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => { updatedAt: el.updatedAt, version: el.version, skipMultilineEncoding: el.skipMultilineEncoding, - path: el.secretPath + path: el.secretPath, + secretMetadata: el.secretMetadata }; if (el.type === SecretType.Personal) { diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index f4dfb07d1..35fbb1713 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -48,6 +48,7 @@ export type SecretV3RawSanitized = { overrideAction?: string; folderId?: string; skipMultilineEncoding?: boolean; + secretMetadata?: { key: string; value: string }[]; }; export type SecretV3Raw = { @@ -63,6 +64,7 @@ export type SecretV3Raw = { secretComment?: string; secretReminderNote?: string; secretReminderRepeatDays?: number; + secretMetadata?: { key: string; value: string }[]; skipMultilineEncoding?: boolean; metadata?: Record; tags?: WsTag[]; @@ -148,6 +150,7 @@ export type TUpdateSecretsV3DTO = { secretReminderRepeatDays?: number | null; secretReminderNote?: string | null; tagIds?: string[]; + secretMetadata?: { key: string; value: string }[]; }; export type TDeleteSecretsV3DTO = { diff --git a/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/IntegrationSettingsSection.tsx b/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/IntegrationSettingsSection.tsx index 51a87b052..0868dfa56 100644 --- a/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/IntegrationSettingsSection.tsx +++ b/frontend/src/pages/secret-manager/IntegrationsDetailsByIDPage/components/IntegrationSettingsSection.tsx @@ -32,7 +32,8 @@ const metadataMappings: Record { diff --git a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx index b663109a8..22b0e1df7 100644 --- a/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx +++ b/frontend/src/pages/secret-manager/SecretApprovalsPage/components/SecretApprovalRequest/components/SecretApprovalRequestChangeItem.tsx @@ -1,4 +1,4 @@ -import { faExclamationTriangle, faInfo } from "@fortawesome/free-solid-svg-icons"; +import { faExclamationTriangle, faInfo, faKey } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { @@ -18,7 +18,10 @@ import { CommitType, SecretV3Raw, TSecretApprovalSecChange, WsTag } from "@app/h export type Props = { op: CommitType; secretVersion?: SecretV3Raw; - newVersion?: Omit & { tags?: WsTag[] }; + newVersion?: Omit & { + tags?: WsTag[]; + secretMetadata?: { key: string; value: string }[]; + }; presentSecretVersionNumber: number; hasMerged?: boolean; conflicts: Array<{ secretId: string; op: CommitType }>; @@ -80,11 +83,12 @@ export const SecretApprovalRequestChangeItem = ({ - {op === CommitType.UPDATE && - - - + {op === CommitType.UPDATE && + + + + {op === CommitType.UPDATE ? ( @@ -110,6 +114,33 @@ export const SecretApprovalRequestChangeItem = ({ ))} + @@ -132,6 +163,33 @@ export const SecretApprovalRequestChangeItem = ({ ))} + ) : ( @@ -173,6 +231,33 @@ export const SecretApprovalRequestChangeItem = ({ ) )} + )} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx index 92f01f1d0..95a39d416 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -8,7 +8,8 @@ import { faClock, faPlus, faShare, - faTag + faTag, + faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -26,6 +27,7 @@ import { DropdownMenuLabel, DropdownMenuTrigger, FormControl, + FormLabel, IconButton, Input, Switch, @@ -87,11 +89,16 @@ export const SecretDetailSidebar = ({ const { permission } = useProjectPermission(); - const { fields, append, remove } = useFieldArray({ + const tagFields = useFieldArray({ control, name: "tags" }); + const metadataFormFields = useFieldArray({ + control, + name: "secretMetadata" + }); + const secretKey = secret?.key || ""; const selectedTags = watch("tags", []) || []; const selectedTagsGroupById = selectedTags.reduce>( @@ -153,10 +160,10 @@ export const SecretDetailSidebar = ({ if (selectedTagsGroupById?.[tag.id]) { const tagPos = selectedTags.findIndex(({ id }) => id === tag.id); if (tagPos !== -1) { - remove(tagPos); + tagFields.remove(tagPos); } } else { - append(tag); + tagFields.append(tag); } }; @@ -277,9 +284,73 @@ export const SecretDetailSidebar = ({ )} /> )} + +
+ {metadataFormFields.fields.map(({ id: metadataFieldId }, i) => ( +
+
+ {i === 0 && Key} + ( + + + + )} + /> +
+
+ {i === 0 && ( + + )} + ( + + + + )} + /> +
+ metadataFormFields.remove(i)} + > + + +
+ ))} +
+ +
+
+
- {fields.map(({ tagColor, id: formId, slug, id }) => ( + {tagFields.fields.map(({ tagColor, id: formId, slug, id }) => ( = {} ) => { if (operation === "delete") { @@ -115,7 +117,8 @@ export const SecretListView = ({ secretReminderRepeatDays: reminderRepeatDays, secretReminderNote: reminderNote, skipMultilineEncoding, - newSecretName: newKey + newSecretName: newKey, + secretMetadata }); return; } @@ -138,7 +141,10 @@ export const SecretListView = ({ const handleSaveSecret = useCallback( async ( orgSecret: SecretV3RawSanitized, - modSecret: Omit & { tags?: { id: string }[] }, + modSecret: Omit & { + tags?: { id: string }[]; + secretMetadata?: { key: string; value: string }[]; + }, cb?: () => void ) => { const { key: oldKey } = orgSecret; @@ -151,7 +157,8 @@ export const SecretListView = ({ tags, comment, reminderRepeatDays, - reminderNote + reminderNote, + secretMetadata } = modSecret; const hasKeyChanged = oldKey !== key && key; @@ -166,7 +173,8 @@ export const SecretListView = ({ "comment", "skipMultilineEncoding", "reminderRepeatDays", - "reminderNote" + "reminderNote", + "secretMetadata" ] as const ).every((el) => orgSecret[el] === modSecret[el]) && isSameTags; @@ -199,7 +207,8 @@ export const SecretListView = ({ reminderNote, secretId: orgSecret.id, newKey: hasKeyChanged ? key : undefined, - skipMultilineEncoding: modSecret.skipMultilineEncoding + skipMultilineEncoding: modSecret.skipMultilineEncoding, + secretMetadata }); if (cb) cb(); } diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.utils.ts b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.utils.ts index 5c4d59fff..762469bf8 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.utils.ts +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.utils.ts @@ -47,7 +47,13 @@ export const formSchema = z.object({ .nullable() .optional(), reminderNote: z.string().trim().nullable().optional(), - + secretMetadata: z + .object({ + key: z.string().trim().min(1), + value: z.string().trim().default("") + }) + .array() + .optional(), tags: z .object({ id: z.string(), diff --git a/frontend/src/pages/secret-manager/integrations/AwsSecretManagerConfigurePage/AwsSecretManagerConfigurePage.tsx b/frontend/src/pages/secret-manager/integrations/AwsSecretManagerConfigurePage/AwsSecretManagerConfigurePage.tsx index a64d84f17..2a5bdbf70 100644 --- a/frontend/src/pages/secret-manager/integrations/AwsSecretManagerConfigurePage/AwsSecretManagerConfigurePage.tsx +++ b/frontend/src/pages/secret-manager/integrations/AwsSecretManagerConfigurePage/AwsSecretManagerConfigurePage.tsx @@ -34,7 +34,10 @@ import { useWorkspace } from "@app/context"; import { useCreateIntegration } from "@app/hooks/api"; import { useGetIntegrationAuthById } from "@app/hooks/api/integrationAuth"; import { useGetIntegrationAuthAwsKmsKeys } from "@app/hooks/api/integrationAuth/queries"; -import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types"; +import { + IntegrationMappingBehavior, + IntegrationMetadataSyncMode +} from "@app/hooks/api/integrations/types"; enum TabSections { Connection = "connection", @@ -94,6 +97,7 @@ const schema = z mappingBehavior: z.nativeEnum(IntegrationMappingBehavior), kmsKeyId: z.string().optional(), shouldTag: z.boolean().optional(), + metadataSyncMode: z.nativeEnum(IntegrationMetadataSyncMode).optional(), tags: z .object({ key: z.string(), @@ -136,6 +140,7 @@ export const AwsSecretManagerConfigurePage = () => { }); const shouldTagState = watch("shouldTag"); + const selectedMetadataSyncMode = watch("metadataSyncMode"); const selectedSourceEnvironment = watch("sourceEnvironment"); const selectedAWSRegion = watch("awsRegion"); const selectedMappingBehavior = watch("mappingBehavior"); @@ -171,7 +176,8 @@ export const AwsSecretManagerConfigurePage = () => { tags, secretPrefix, kmsKeyId, - mappingBehavior + mappingBehavior, + metadataSyncMode }: TFormSchema) => { try { if (!integrationAuth?.id) return; @@ -186,7 +192,8 @@ export const AwsSecretManagerConfigurePage = () => { metadata: { ...(shouldTag ? { - secretAWSTag: tags + secretAWSTag: tags, + metadataSyncMode } : {}), ...(secretPrefix && { secretPrefix }), @@ -339,7 +346,12 @@ export const AwsSecretManagerConfigurePage = () => { > + )} /> - ( - - - - )} - /> -
- )} - + )} + {shouldTagState && + selectedMetadataSyncMode === IntegrationMetadataSyncMode.CUSTOM && ( +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ )}
} - SecretValueCommentTags} + SecretValueCommentTagsMetadata
+ {secretVersion?.secretMetadata?.length ? ( +
+ {secretVersion.secretMetadata?.map((el) => ( +
+ + +
{el.key}
+
+ +
+ {el.value} +
+
+
+ ))} +
+ ) : ( +

-

+ )} +
NEW + {newVersion?.secretMetadata?.length ? ( +
+ {newVersion.secretMetadata?.map((el) => ( +
+ + +
{el.key}
+
+ +
+ {el.value} +
+
+
+ ))} +
+ ) : ( +

-

+ )} +
+ {newVersion?.secretMetadata?.length ? ( +
+ {newVersion.secretMetadata?.map((el) => ( +
+ + +
{el.key}
+
+ +
+ {el.value} +
+
+
+ ))} +
+ ) : ( +

-

+ )} +