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 d65545226..815e255e9 100644 --- a/backend/src/ee/services/secret-replication/secret-replication-service.ts +++ b/backend/src/ee/services/secret-replication/secret-replication-service.ts @@ -14,6 +14,7 @@ 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"; @@ -154,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]; @@ -272,6 +275,7 @@ export const secretReplicationServiceFactory = ({ ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : undefined })); + const sourceSecrets = $getReplicatedSecretsV2(sourceDecryptedLocalSecrets, sourceImportedSecrets); const sourceSecretsGroupByKey = groupBy(sourceSecrets, (i) => i.key); @@ -426,6 +430,7 @@ export const secretReplicationServiceFactory = ({ encryptedValue: doc.encryptedValue, encryptedComment: doc.encryptedComment, skipMultilineEncoding: doc.skipMultilineEncoding, + secretMetadata: doc.secretMetadata, references: doc.secretValue ? getAllSecretReferences(doc.secretValue).nestedReferences : [] }; }) @@ -454,6 +459,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 : [] } }; diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index 6cd0c32ab..374df8d1d 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -222,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() diff --git a/backend/src/services/resource-metadata/resource-metadata-schema.ts b/backend/src/services/resource-metadata/resource-metadata-schema.ts index f8a8b08fd..10641e869 100644 --- a/backend/src/services/resource-metadata/resource-metadata-schema.ts +++ b/backend/src/services/resource-metadata/resource-metadata-schema.ts @@ -3,7 +3,7 @@ import z from "zod"; export const ResourceMetadataSchema = z .object({ key: z.string().trim().min(1), - value: z.string().trim().min(1) + value: z.string().trim().default("") }) .array(); 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-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index b2db2712d..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 + }) } ] }); 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 f58ff9d34..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 @@ -144,7 +144,7 @@ export const secretV2BridgeServiceFactory = ({ }, { operator: "eq", - field: "key", + field: `${TableName.SecretV2}.key` as "key", value: el.secretKey } ] @@ -1127,7 +1127,7 @@ export const secretV2BridgeServiceFactory = ({ value: [ { operator: "eq", - field: "key", + field: `${TableName.SecretV2}.key` as "key", value: el.secretKey }, { @@ -1199,6 +1199,7 @@ export const secretV2BridgeServiceFactory = ({ key: el.secretKey, tagIds: el.tagIds, references, + secretMetadata: el.secretMetadata, type: SecretType.Shared }; }), @@ -1271,7 +1272,7 @@ export const secretV2BridgeServiceFactory = ({ value: [ { operator: "eq", - field: "key", + field: `${TableName.SecretV2}.key` as "key", value: el.secretKey }, { @@ -1336,7 +1337,7 @@ export const secretV2BridgeServiceFactory = ({ value: [ { operator: "eq", - field: "key", + field: `${TableName.SecretV2}.key` as "key", value: el.secretKey }, { @@ -1482,7 +1483,7 @@ export const secretV2BridgeServiceFactory = ({ value: [ { operator: "eq", - field: "key", + field: `${TableName.SecretV2}.key` as "key", value: el.secretKey }, { @@ -1854,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 : [] }; }) @@ -1881,6 +1883,7 @@ export const secretV2BridgeServiceFactory = ({ encryptedComment: doc.encryptedComment, skipMultilineEncoding: doc.skipMultilineEncoding, reminderNote: doc.reminderNote, + secretMetadata: doc.secretMetadata, reminderRepeatDays: doc.reminderRepeatDays, ...(doc.encryptedValue ? { 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 64332cec2..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 @@ -98,6 +98,7 @@ export type TCreateManySecretDTO = Omit & { secretComment?: string; skipMultilineEncoding?: boolean; tagIds?: string[]; + secretMetadata?: ResourceMetadataDTO; metadata?: { source?: string; }; diff --git a/frontend/src/hooks/api/secrets/mutations.tsx b/frontend/src/hooks/api/secrets/mutations.tsx index 7bc3849be..cc154304a 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 a803ff50b..e527fbfb6 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/views/SecretMainPage/components/SecretListView/SecretDetailSidebar.tsx b/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetailSidebar.tsx index 6aa7fd4e1..ae90b4b5d 100644 --- a/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/views/SecretMainPage/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/views/SecretMainPage/components/SecretListView/SecretListView.utils.ts b/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.utils.ts index 5c4d59fff..762469bf8 100644 --- a/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.utils.ts +++ b/frontend/src/views/SecretMainPage/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(),