diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index f4b1bcc35..efc1cb865 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -1039,9 +1039,7 @@ export const registerRoutes = async ( secretApprovalRequestSecretDAL, kmsService, snapshotService, - resourceMetadataDAL, - userDAL, - identityDAL + resourceMetadataDAL }); const secretApprovalRequestService = secretApprovalRequestServiceFactory({ diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 3009993c0..a6cc8bae0 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -116,7 +116,8 @@ export const secretRawSchema = z.object({ .object({ actorId: z.string().nullable().optional(), actorType: z.string().nullable().optional(), - name: z.string().nullable().optional() + name: z.string().nullable().optional(), + membershipId: z.string().nullable().optional() }) .optional() .nullable() 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 8e7eab514..751235cde 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 @@ -633,11 +633,12 @@ export const reshapeBridgeSecret = ( secret: Omit & { value: string; comment: string; - actor?: { - actorType?: string; - actorId?: string; - name?: string; - }; + userActorName?: string | null; + identityActorName?: string | null; + userActorId?: string | null; + identityActorId?: string | null; + membershipId?: string | null; + actorType?: string | null; tags?: { id: string; slug: string; @@ -658,7 +659,14 @@ export const reshapeBridgeSecret = ( _id: secret.id, id: secret.id, user: secret.userId, - actor: secret.actor, + actor: secret.actorType + ? { + actorType: secret.actorType, + actorId: secret.userActorId || secret.identityActorId, + name: secret.identityActorName || secret.userActorName, + membershipId: secret.membershipId + } + : undefined, 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 5162c3488..9063da5c4 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 @@ -23,7 +23,6 @@ import { logger } from "@app/lib/logger"; import { alphaNumericNanoId } from "@app/lib/nanoid"; import { ActorType } from "../auth/auth-type"; -import { TIdentityDALFactory } from "../identity/identity-dal"; import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; @@ -33,7 +32,6 @@ import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { fnSecretsV2FromImports } from "../secret-import/secret-import-fns"; import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal"; -import { TUserDALFactory } from "../user/user-dal"; import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; import { expandSecretReferencesFactory, @@ -87,8 +85,6 @@ type TSecretV2BridgeServiceFactoryDep = { >; snapshotService: Pick; resourceMetadataDAL: Pick; - userDAL: Pick; - identityDAL: Pick; }; export type TSecretV2BridgeServiceFactory = ReturnType; @@ -111,9 +107,7 @@ export const secretV2BridgeServiceFactory = ({ secretApprovalRequestDAL, secretApprovalRequestSecretDAL, kmsService, - resourceMetadataDAL, - userDAL, - identityDAL + resourceMetadataDAL }: TSecretV2BridgeServiceFactoryDep) => { const $validateSecretReferences = async ( projectId: string, @@ -1715,36 +1709,15 @@ export const secretV2BridgeServiceFactory = ({ type: KmsDataKey.SecretManager, projectId: folder.projectId }); - const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] }); - - 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); + const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors(secretId, folder.projectId, { + offset, + limit, + sort: [["createdAt", "desc"]] + }); 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() : "" }); diff --git a/backend/src/services/secret-v2-bridge/secret-version-dal.ts b/backend/src/services/secret-v2-bridge/secret-version-dal.ts index 7772b8518..59bc4c6ff 100644 --- a/backend/src/services/secret-v2-bridge/secret-version-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-version-dal.ts @@ -1,9 +1,10 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { Knex } from "knex"; import { TDbClient } from "@app/db"; import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas"; import { BadRequestError, DatabaseError } from "@app/lib/errors"; -import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex"; import { logger } from "@app/lib/logger"; import { QueueName } from "@app/queue"; @@ -119,11 +120,60 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { logger.info(`${QueueName.DailyResourceCleanUp}: pruning secret version v2 completed`); }; + const findVersionsBySecretIdWithActors = async ( + secretId: string, + projectId: string, + { offset, limit, sort = [["createdAt", "desc"]] }: TFindOpt = {}, + tx?: Knex + ) => { + try { + const query = (tx || db)(TableName.SecretVersionV2) + .where(`${TableName.SecretVersionV2}.secretId`, secretId) + .leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretVersionV2}.userActorId`) + .leftJoin( + TableName.ProjectMembership, + `${TableName.ProjectMembership}.userId`, + `${TableName.SecretVersionV2}.userActorId` + ) + .leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`) + .select( + selectAllTableCols(TableName.SecretVersionV2), + `${TableName.Users}.username as userActorName`, + `${TableName.Identity}.name as identityActorName`, + `${TableName.ProjectMembership}.id as membershipId` + ); + + if (limit) void query.limit(limit); + if (offset) void query.offset(offset); + if (sort) { + void query.orderBy( + sort.map(([column, order, nulls]) => ({ + column: `${TableName.SecretVersionV2}.${column as string}`, + order, + nulls + })) + ); + } + + const docs: Array< + TSecretVersionsV2 & { + userActorName: string | undefined | null; + identityActorName: string | undefined | null; + membershipId: string | undefined | null; + } + > = await query; + return docs; + } catch (error) { + throw new DatabaseError({ error, name: "FindVersionsBySecretIdWithActors" }); + } + }; + return { ...secretVersionV2Orm, pruneExcessVersions, findLatestVersionMany, bulkUpdate, - findLatestVersionByFolderId + findLatestVersionByFolderId, + findVersionsBySecretIdWithActors }; }; diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 9fbef1488..9e0a82dda 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -105,6 +105,7 @@ export type SecretVersions = { actorId?: string | null; actorType?: string | null; name?: string | null; + membershipId?: string | null; } | null; }; 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 1f8907385..d78815222 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx @@ -5,14 +5,14 @@ import { faArrowRotateRight, faCheckCircle, faClock, + faDesktop, faEyeSlash, faPlus, + faServer, faShare, faTag, faTrash, - faUser, - faDesktop, - faServer + faUser } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -48,11 +48,11 @@ import { useWorkspace } from "@app/context"; import { usePopUp, useToggle } from "@app/hooks"; -import { useGetSecretVersion, useGetWorkspaceUsers } from "@app/hooks/api"; +import { useGetSecretVersion } from "@app/hooks/api"; +import { ActorType } from "@app/hooks/api/auditLogs/enums"; 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 { CreateReminderForm } from "./CreateReminderForm"; import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils"; @@ -125,7 +125,6 @@ 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, @@ -225,7 +224,10 @@ export const SecretDetailSidebar = ({ } }; - const getModifiedByName = (userType: string | undefined | null, userName: string | null | undefined) => { + const getModifiedByName = ( + userType: string | undefined | null, + userName: string | null | undefined + ) => { switch (userType) { case ActorType.PLATFORM: return "System-generated"; @@ -234,15 +236,14 @@ export const SecretDetailSidebar = ({ } }; - const getUserMembershipId = (actorId: string) => { - const foundMember = members.find((member) => member.user?.id === actorId); - return foundMember?.id || null; - }; - - const getLinkToModifyHistoryEntity = (actorId: string, actorType: string) => { + const getLinkToModifyHistoryEntity = ( + actorId: string, + actorType: string, + membershipId: string | null = "" + ) => { switch (actorType) { case ActorType.USER: - return `/${ProjectType.SecretManager}/${currentWorkspace.id}/members/${getUserMembershipId(actorId)}`; + return `/${ProjectType.SecretManager}/${currentWorkspace.id}/members/${membershipId}`; case ActorType.IDENTITY: return `/${ProjectType.SecretManager}/${currentWorkspace.id}/identities/${actorId}`; default: @@ -250,9 +251,13 @@ export const SecretDetailSidebar = ({ } }; - const onModifyHistoryClick = (actorId: string | undefined | null, actorType: string | undefined | null) => { + const onModifyHistoryClick = ( + actorId: string | undefined | null, + actorType: string | undefined | null, + membershipId: string | undefined | null + ) => { if (actorType && actorId && actorType !== ActorType.PLATFORM) { - const redirectLink = getLinkToModifyHistoryEntity(actorId, actorType); + const redirectLink = getLinkToModifyHistoryEntity(actorId, actorType, membershipId); if (redirectLink) { navigate({ to: redirectLink }); } @@ -700,7 +705,11 @@ export const SecretDetailSidebar = ({ {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
- onModifyHistoryClick(actor.actorId, actor.actorType) + onModifyHistoryClick( + actor.actorId, + actor.actorType, + actor.membershipId + ) } className="cursor-pointer" > @@ -792,7 +801,9 @@ export const SecretDetailSidebar = ({ type="button" className="ml-1 cursor-pointer" onClick={(e) => { - e.currentTarget.closest(".group")?.classList.add("show-value"); + e.currentTarget + .closest(".group") + ?.classList.add("show-value"); }} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") {