diff --git a/backend/src/db/migrations/20250305131152_add-actor-id-to-secret-versions-v2.ts b/backend/src/db/migrations/20250305131152_add-actor-id-to-secret-versions-v2.ts new file mode 100644 index 000000000..e4340a436 --- /dev/null +++ b/backend/src/db/migrations/20250305131152_add-actor-id-to-secret-versions-v2.ts @@ -0,0 +1,52 @@ +import { Knex } from "knex"; + +import { TableName } from "@app/db/schemas"; + +export async function up(knex: Knex): Promise { + const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId"); + const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId"); + const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType"); + + if (!hasSecretVersionV2UserActorId) { + await knex.schema.alterTable(TableName.SecretVersionV2, (t) => { + t.uuid("userActorId"); + t.foreign("userActorId").references("id").inTable(TableName.Users); + }); + } + + if (!hasSecretVersionV2IdentityActorId) { + await knex.schema.alterTable(TableName.SecretVersionV2, (t) => { + t.uuid("identityActorId"); + t.foreign("identityActorId").references("id").inTable(TableName.Identity); + }); + } + if (!hasSecretVersionV2ActorType) { + await knex.schema.alterTable(TableName.SecretVersionV2, (t) => { + t.string("actorType"); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasSecretVersionV2UserActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "userActorId"); + const hasSecretVersionV2IdentityActorId = await knex.schema.hasColumn(TableName.SecretVersionV2, "identityActorId"); + const hasSecretVersionV2ActorType = await knex.schema.hasColumn(TableName.SecretVersionV2, "actorType"); + + if (hasSecretVersionV2UserActorId) { + await knex.schema.alterTable(TableName.SecretVersionV2, (t) => { + t.dropColumn("userActorId"); + }); + } + + if (!hasSecretVersionV2IdentityActorId) { + await knex.schema.alterTable(TableName.SecretVersionV2, (t) => { + t.dropColumn("identityActorId"); + }); + } + + if (!hasSecretVersionV2ActorType) { + await knex.schema.alterTable(TableName.SecretVersionV2, (t) => { + t.dropColumn("actorType"); + }); + } +} diff --git a/backend/src/db/schemas/secret-versions-v2.ts b/backend/src/db/schemas/secret-versions-v2.ts index 160ed1c14..593a46b06 100644 --- a/backend/src/db/schemas/secret-versions-v2.ts +++ b/backend/src/db/schemas/secret-versions-v2.ts @@ -25,7 +25,10 @@ export const SecretVersionsV2Schema = z.object({ folderId: z.string().uuid(), userId: z.string().uuid().nullable().optional(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + userActorId: z.string().uuid().nullable().optional(), + identityActorId: z.string().uuid().nullable().optional(), + actorType: z.string().nullable().optional() }); export type TSecretVersionsV2 = z.infer; 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 17eecf508..7f8d266c9 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 @@ -757,7 +757,11 @@ export const secretApprovalRequestServiceFactory = ({ secretDAL, secretVersionDAL, secretTagDAL, - secretVersionTagDAL + secretVersionTagDAL, + actor: { + type: actor, + actorId + } }) : []; const updatedSecrets = secretUpdationCommits.length @@ -803,7 +807,11 @@ export const secretApprovalRequestServiceFactory = ({ secretDAL, secretVersionDAL, secretTagDAL, - secretVersionTagDAL + secretVersionTagDAL, + actor: { + type: actor, + actorId + } }) : []; const deletedSecret = secretDeletionCommits.length 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 3c25db98c..c9b0a3532 100644 --- a/backend/src/ee/services/secret-replication/secret-replication-service.ts +++ b/backend/src/ee/services/secret-replication/secret-replication-service.ts @@ -710,6 +710,10 @@ export const secretReplicationServiceFactory = ({ tx, secretTagDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, inputSecrets: locallyCreatedSecrets.map((doc) => { return { keyEncoding: doc.keyEncoding, @@ -741,6 +745,10 @@ export const secretReplicationServiceFactory = ({ tx, secretTagDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, inputSecrets: locallyUpdatedSecrets.map((doc) => { return { filter: { diff --git a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts index 1c34f6b3d..06ad0cba5 100644 --- a/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts +++ b/backend/src/ee/services/secret-snapshot/secret-snapshot-service.ts @@ -370,7 +370,20 @@ export const secretSnapshotServiceFactory = ({ const secrets = await secretV2BridgeDAL.insertMany( rollbackSnaps.flatMap(({ secretVersions, folderId }) => secretVersions.map( - ({ latestSecretVersion, version, updatedAt, createdAt, secretId, envId, id, tags, ...el }) => ({ + ({ + latestSecretVersion, + version, + updatedAt, + createdAt, + secretId, + envId, + id, + tags, + userActorId, + identityActorId, + actorType, + ...el + }) => ({ ...el, id: secretId, version: deletedTopLevelSecsGroupById[secretId] ? latestSecretVersion + 1 : latestSecretVersion, diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index efc1cb865..f4b1bcc35 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1039,7 +1039,9 @@ export const registerRoutes = async ( secretApprovalRequestSecretDAL, kmsService, snapshotService, - resourceMetadataDAL + resourceMetadataDAL, + userDAL, + identityDAL }); const secretApprovalRequestService = secretApprovalRequestServiceFactory({ diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 4d645ac4b..16a7396cd 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -111,7 +111,15 @@ export const secretRawSchema = z.object({ secretReminderRepeatDays: z.number().nullable().optional(), skipMultilineEncoding: z.boolean().default(false).nullable().optional(), createdAt: z.date(), - updatedAt: z.date() + updatedAt: z.date(), + actor: z + .object({ + actorId: z.string().nullable(), + actorType: z.string().nullable(), + name: z.string().nullable().optional() + }) + .optional() + .nullable() }); export const ProjectPermissionSchema = z.object({ diff --git a/backend/src/services/external-migration/external-migration-fns.ts b/backend/src/services/external-migration/external-migration-fns.ts index 744678792..f4a54f0db 100644 --- a/backend/src/services/external-migration/external-migration-fns.ts +++ b/backend/src/services/external-migration/external-migration-fns.ts @@ -772,6 +772,10 @@ export const importDataIntoInfisicalFn = async ({ secretVersionDAL, secretTagDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, tx }); } 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 cc40b0f26..046b23dec 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 @@ -10,6 +10,7 @@ import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-sche import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types"; +import { ActorType } from "../auth/auth-type"; const INTERPOLATION_SYNTAX_REG = /\${([a-zA-Z0-9-_.]+)}/g; // akhilmhdh: JS regex with global save state in .test @@ -62,6 +63,7 @@ export const fnSecretBulkInsert = async ({ resourceMetadataDAL, secretTagDAL, secretVersionTagDAL, + actor, tx }: TFnSecretBulkInsert) => { const sanitizedInputSecrets = inputSecrets.map( @@ -90,6 +92,9 @@ export const fnSecretBulkInsert = async ({ }) ); + const userActorId = actor && actor.type === ActorType.USER ? actor.actorId : undefined; + const identityActorId = actor && actor.type !== ActorType.USER ? actor.actorId : undefined; + const newSecrets = await secretDAL.insertMany( sanitizedInputSecrets.map((el) => ({ ...el, folderId })), tx @@ -106,6 +111,9 @@ export const fnSecretBulkInsert = async ({ sanitizedInputSecrets.map((el) => ({ ...el, folderId, + userActorId, + identityActorId, + actorType: actor?.type, secretId: newSecretGroupedByKeyName[el.key][0].id })), tx @@ -157,8 +165,12 @@ export const fnSecretBulkUpdate = async ({ secretVersionDAL, secretTagDAL, secretVersionTagDAL, - resourceMetadataDAL + resourceMetadataDAL, + actor }: TFnSecretBulkUpdate) => { + const userActorId = actor && actor?.type === ActorType.USER ? actor?.actorId : undefined; + const identityActorId = actor && actor?.type !== ActorType.USER ? actor?.actorId : undefined; + const sanitizedInputSecrets = inputSecrets.map( ({ filter, @@ -216,7 +228,10 @@ export const fnSecretBulkUpdate = async ({ encryptedValue, reminderRepeatDays, folderId, - secretId + secretId, + userActorId, + identityActorId, + actorType: actor?.type }) ), tx @@ -616,6 +631,11 @@ export const reshapeBridgeSecret = ( secret: Omit & { value: string; comment: string; + actor?: { + actorType?: string; + actorId?: string; + name?: string; + }; tags?: { id: string; slug: string; @@ -636,6 +656,7 @@ export const reshapeBridgeSecret = ( _id: secret.id, id: secret.id, user: secret.userId, + actor: secret.actor, tags: secret.tags, skipMultilineEncoding: secret.skipMultilineEncoding, secretReminderRepeatDays: secret.reminderRepeatDays, 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 0ffb0ea4c..dd0b2d9ec 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 @@ -62,6 +62,8 @@ import { } from "./secret-v2-bridge-types"; import { TSecretVersionV2DALFactory } from "./secret-version-dal"; import { TSecretVersionV2TagDALFactory } from "./secret-version-tag-dal"; +import { TUserDALFactory } from "../user/user-dal"; +import { TIdentityDALFactory } from "../identity/identity-dal"; type TSecretV2BridgeServiceFactoryDep = { secretDAL: TSecretV2BridgeDALFactory; @@ -85,6 +87,8 @@ type TSecretV2BridgeServiceFactoryDep = { >; snapshotService: Pick; resourceMetadataDAL: Pick; + userDAL: Pick; + identityDAL: Pick; }; export type TSecretV2BridgeServiceFactory = ReturnType; @@ -107,7 +111,9 @@ export const secretV2BridgeServiceFactory = ({ secretApprovalRequestDAL, secretApprovalRequestSecretDAL, kmsService, - resourceMetadataDAL + resourceMetadataDAL, + userDAL, + identityDAL }: TSecretV2BridgeServiceFactoryDep) => { const $validateSecretReferences = async ( projectId: string, @@ -301,6 +307,10 @@ export const secretV2BridgeServiceFactory = ({ secretVersionDAL, secretTagDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, tx }) ); @@ -483,6 +493,10 @@ export const secretV2BridgeServiceFactory = ({ secretVersionDAL, secretTagDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, tx }) ); @@ -1230,6 +1244,10 @@ export const secretV2BridgeServiceFactory = ({ secretVersionDAL, secretTagDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, tx }) ); @@ -1490,6 +1508,10 @@ export const secretV2BridgeServiceFactory = ({ secretVersionDAL, secretTagDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, resourceMetadataDAL }); updatedSecrets.push(...bulkUpdatedSecrets.map((el) => ({ ...el, secretPath: folder.path }))); @@ -1522,6 +1544,10 @@ export const secretV2BridgeServiceFactory = ({ secretVersionDAL, secretTagDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, tx }); updatedSecrets.push(...bulkInsertedSecrets.map((el) => ({ ...el, secretPath: folder.path }))); @@ -1690,13 +1716,39 @@ export const secretV2BridgeServiceFactory = ({ projectId: folder.projectId }); const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] }); - return secretVersions.map((el) => - reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", { + + const userIds = Array.from( + new Set(secretVersions.map((version) => version.userActorId).filter(Boolean)) + ) as string[]; + + const users = userIds.length > 0 ? await userDAL.find({ $in: { id: userIds } }) : []; + const usersById = groupBy(users, (user) => user.id); + + const identitiesIds = Array.from( + new Set(secretVersions.map((version) => version.identityActorId).filter(Boolean)) + ) as string[]; + const identities = identitiesIds.length > 0 ? await identityDAL.find({ $in: { id: identitiesIds } }) : []; + const identitiesById = groupBy(identities, (identity) => identity.id); + + return secretVersions.map((el) => { + let entityId; + let actorName; + if (el.userActorId) { + actorName = usersById[el.userActorId]?.[0]?.username; + entityId = el.userActorId; + } else if (el.identityActorId) { + actorName = identitiesById[el.identityActorId]?.[0]?.name; + entityId = el.identityActorId; + } + const actorEntity = el.actorType ? { actorType: el.actorType, actorId: entityId, name: actorName } : undefined; + + return reshapeBridgeSecret(folder.projectId, folder.environment.envSlug, "/", { ...el, + actor: actorEntity, value: el.encryptedValue ? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() : "", comment: el.encryptedComment ? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() : "" - }) - ); + }); + }); }; // this is a backfilling API for secret references @@ -1956,6 +2008,10 @@ export const secretV2BridgeServiceFactory = ({ secretTagDAL, resourceMetadataDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, inputSecrets: locallyCreatedSecrets.map((doc) => { return { type: doc.type, @@ -1982,6 +2038,10 @@ export const secretV2BridgeServiceFactory = ({ tx, secretTagDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, inputSecrets: locallyUpdatedSecrets.map((doc) => { return { filter: { 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 ad8264e81..22956463d 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 @@ -168,6 +168,10 @@ export type TFnSecretBulkInsert = { secretVersionDAL: Pick; secretTagDAL: Pick; secretVersionTagDAL: Pick; + actor?: { + type: string; + actorId: string; + }; }; type TRequireReferenceIfValue = @@ -192,6 +196,10 @@ export type TFnSecretBulkUpdate = { secretVersionDAL: Pick; secretTagDAL: Pick; secretVersionTagDAL: Pick; + actor?: { + type: string; + actorId: string; + }; tx?: Knex; }; diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 93f68e813..49640ac2a 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -284,6 +284,10 @@ export const secretServiceFactory = ({ secretVersionDAL, secretTagDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, tx }) ); @@ -429,6 +433,10 @@ export const secretServiceFactory = ({ secretVersionDAL, secretTagDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, tx }) ); @@ -822,6 +830,10 @@ export const secretServiceFactory = ({ secretVersionDAL, secretTagDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, tx }) ); @@ -931,7 +943,11 @@ export const secretServiceFactory = ({ secretDAL, secretVersionDAL, secretTagDAL, - secretVersionTagDAL + secretVersionTagDAL, + actor: { + type: actor, + actorId + } }) ); @@ -2404,6 +2420,10 @@ export const secretServiceFactory = ({ secretVersionDAL, secretTagDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, tx }) ); @@ -2514,6 +2534,10 @@ export const secretServiceFactory = ({ secretVersionDAL, secretTagDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, tx }) ); @@ -2848,6 +2872,10 @@ export const secretServiceFactory = ({ secretDAL, tx, secretTagDAL, + actor: { + type: actor, + actorId + }, secretVersionTagDAL, inputSecrets: locallyCreatedSecrets.map((doc) => { return { @@ -2879,6 +2907,10 @@ export const secretServiceFactory = ({ tx, secretTagDAL, secretVersionTagDAL, + actor: { + type: actor, + actorId + }, inputSecrets: locallyUpdatedSecrets.map((doc) => { return { filter: { diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index 158605276..242296c50 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -322,6 +322,10 @@ export type TFnSecretBulkInsert = { secretVersionDAL: Pick; secretTagDAL: Pick; secretVersionTagDAL: Pick; + actor?: { + type?: string; + actorId: string; + }; }; export type TFnSecretBulkUpdate = { @@ -336,6 +340,10 @@ export type TFnSecretBulkUpdate = { secretTagDAL: Pick; secretVersionTagDAL: Pick; tx?: Knex; + actor?: { + type?: string; + actorId: string; + }; }; export type TAttachSecretTagsDTO = { diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 92dc220b8..92e671c8b 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -101,6 +101,11 @@ export type SecretVersions = { skipMultilineEncoding?: boolean; createdAt: string; updatedAt: string; + actor?: { + actorId?: string, + actorType: string, + name?: string + }; }; // dto 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 168cd6f43..04f343427 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -9,11 +9,14 @@ import { faPlus, faShare, faTag, - faTrash + faTrash, + faUser, + faDesktop, + faServer } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Link } from "@tanstack/react-router"; +import { Link, useNavigate } from "@tanstack/react-router"; import { format } from "date-fns"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; @@ -49,6 +52,8 @@ import { useGetSecretVersion } from "@app/hooks/api"; import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries"; import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types"; import { ProjectType } from "@app/hooks/api/workspace/types"; +import { ActorType } from "@app/hooks/api/auditLogs/enums"; +import { useGetWorkspaceUsers } from "@app/hooks/api"; import { CreateReminderForm } from "./CreateReminderForm"; import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils"; @@ -120,7 +125,9 @@ export const SecretDetailSidebar = ({ {} ); const selectTagSlugs = selectedTags.map((i) => i.slug); - + const navigate = useNavigate(); + const { data: members = [] } = useGetWorkspaceUsers(currentWorkspace.id); + const cannotEditSecret = permission.cannot( ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { @@ -201,6 +208,41 @@ export const SecretDetailSidebar = ({ const secretReminderRepeatDays = watch("reminderRepeatDays"); const secretReminderNote = watch("reminderNote"); + const getModifiedByIcon = (userType: string) => { + switch (userType) { + case ActorType.USER: + return faUser; + case ActorType.IDENTITY: + return faDesktop; + default: + return faServer; + } + } + + const getUserMembershipId = (actorId: string) => { + return members.filter((member) => member.user?.id === actorId)?.[0].id || null; + } + + const getLinkToModifyHistoryEntity = (actorId: string, actorType: string) => { + switch(actorType) { + case ActorType.USER: + return `/${ProjectType.SecretManager}/${currentWorkspace.id}/members/${getUserMembershipId(actorId)}`; + case ActorType.IDENTITY: + return `/${ProjectType.SecretManager}/${currentWorkspace.id}/identities/${actorId}`; + default: + return null; + } + } + + const onModifyHistoryClick = (actorId: string | undefined, actorType: string) => { + if (actorId && actorType !== ActorType.PLATFORM) { + const redirectLink = getLinkToModifyHistoryEntity(actorId, actorType); + if (redirectLink) { + navigate({ to: redirectLink }); + } + } + } + return ( <>
Version History
- {secretVersion?.map(({ createdAt, secretValue, version, id }) => ( + {secretVersion?.map(({ createdAt, secretValue, version, id, actor }) => (
@@ -633,36 +675,29 @@ export const SecretDetailSidebar = ({
-
-
- Value: -
-
-
- - + + }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.stopPropagation(); + e.currentTarget + .closest(".group") + ?.classList.remove("show-value"); + } + }} + > + + +
+ + {secretValue?.replace(/./g, "*")} + +
- - {secretValue?.replace(/./g, "*")} - -