From 27ac81ef5cc7b78f63f4c0f2c2330c152b99a79c Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Wed, 5 Nov 2025 16:55:41 -0800 Subject: [PATCH] fix: handle group memberships in secret versions history and fix secret versions query to always return all versions --- backend/src/server/routes/sanitizedSchemas.ts | 3 +- .../secret-v2-bridge/secret-v2-bridge-fns.ts | 4 +- .../secret-v2-bridge/secret-version-dal.ts | 23 ++++---- frontend/src/hooks/api/secrets/types.ts | 1 + .../GroupMembersSection/GroupMembersTable.tsx | 19 ++++++- .../route-secret-manager.tsx | 7 +++ .../SecretListView/SecretVersionItem.tsx | 53 ++++++++++++++++--- 7 files changed, 90 insertions(+), 20 deletions(-) diff --git a/backend/src/server/routes/sanitizedSchemas.ts b/backend/src/server/routes/sanitizedSchemas.ts index 47fbb0e07..17cfcb5ce 100644 --- a/backend/src/server/routes/sanitizedSchemas.ts +++ b/backend/src/server/routes/sanitizedSchemas.ts @@ -141,7 +141,8 @@ export const secretRawSchema = z.object({ actorId: z.string().nullable().optional(), actorType: z.string().nullable().optional(), name: z.string().nullable().optional(), - membershipId: z.string().nullable().optional() + membershipId: z.string().nullable().optional(), + groupId: 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 6b284ddee..01a7f6210 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 @@ -793,6 +793,7 @@ export const reshapeBridgeSecret = ( userActorId?: string | null; identityActorId?: string | null; membershipId?: string | null; + groupId?: string | null; actorType?: string | null; tags?: { id: string; @@ -823,7 +824,8 @@ export const reshapeBridgeSecret = ( actorType: secret.actorType, actorId: secret.userActorId || secret.identityActorId, name: secret.identityActorName || secret.userActorName, - membershipId: secret.membershipId + membershipId: secret.membershipId, + groupId: secret.groupId } : undefined, tags: secret.tags, 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 7d25dac86..33ac43d09 100644 --- a/backend/src/services/secret-v2-bridge/secret-version-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-version-dal.ts @@ -182,7 +182,6 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { const findVersionsBySecretIdWithActors = async ({ secretId, - projectId, secretVersions, findOpt = {}, tx @@ -196,11 +195,19 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { try { const { offset, limit, sort = [["createdAt", "desc"]] } = findOpt; const query = (tx || db.replicaNode())(TableName.SecretVersionV2) + .leftJoin(TableName.SecretFolder, `${TableName.SecretFolder}.id`, `${TableName.SecretVersionV2}.folderId`) + .leftJoin(TableName.Environment, `${TableName.Environment}.id`, `${TableName.SecretFolder}.envId`) .leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretVersionV2}.userActorId`) + .leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`) .leftJoin(TableName.Membership, (qb) => { void qb - .on(`${TableName.Membership}.actorUserId`, `${TableName.SecretVersionV2}.userActorId`) - .andOn(`${TableName.Membership}.scope`, db.raw("?", [AccessScope.Project])); + .on(`${TableName.Membership}.scope`, db.raw("?", [AccessScope.Project])) + .andOn(`${TableName.Membership}.scopeProjectId`, `${TableName.Environment}.projectId`) + .andOn((sqb) => { + void sqb + .on(`${TableName.Membership}.actorUserId`, `${TableName.SecretVersionV2}.userActorId`) + .orOn(`${TableName.Membership}.actorGroupId`, `${TableName.UserGroupMembership}.groupId`); + }); }) .leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`) .leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`) @@ -216,12 +223,6 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { ) .where((qb) => { void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId); - void qb.where(`${TableName.Membership}.scopeProjectId`, projectId); - if (secretVersions?.length) void qb.whereIn(`${TableName.SecretVersionV2}.version`, secretVersions); - }) - .orWhere((qb) => { - void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId); - void qb.whereNull(`${TableName.Membership}.scopeProjectId`); if (secretVersions?.length) void qb.whereIn(`${TableName.SecretVersionV2}.version`, secretVersions); }) .select( @@ -229,6 +230,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { db.ref("username").withSchema(TableName.Users).as("userActorName"), db.ref("name").withSchema(TableName.Identity).as("identityActorName"), db.ref("id").withSchema(TableName.Membership).as("membershipId"), + db.ref("actorGroupId").withSchema(TableName.Membership).as("groupId"), db.ref("id").withSchema(TableName.SecretTag).as("tagId"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug") @@ -256,7 +258,8 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { ...SecretVersionsV2Schema.parse(el), userActorName: el.userActorName, identityActorName: el.identityActorName, - membershipId: el.membershipId + membershipId: el.membershipId, + groupId: el.groupId }), childrenMapper: [ { diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 4f912fad2..f4bf2da98 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -109,6 +109,7 @@ export type SecretVersions = { actorType?: string | null; name?: string | null; membershipId?: string | null; + groupId?: string | null; } | null; }; diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx index 1df989602..7ced08534 100644 --- a/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx +++ b/frontend/src/pages/project/GroupDetailsByIDPage/components/GroupMembersSection/GroupMembersTable.tsx @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { useEffect, useMemo } from "react"; import { faArrowDown, faArrowUp, @@ -7,6 +7,7 @@ import { faSearch } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate, useSearch } from "@tanstack/react-router"; import { createNotification } from "@app/components/notifications"; import { @@ -48,6 +49,7 @@ enum GroupMembersOrderBy { } export const GroupMembersTable = ({ groupMembership }: Props) => { + const navigate = useNavigate(); const { search, setSearch, @@ -62,6 +64,21 @@ export const GroupMembersTable = ({ groupMembership }: Props) => { initPerPage: getUserTablePreference("projectGroupMembersTable", PreferenceKey.PerPage, 20) }); + // this handles links from secret versions when the actor is in a group membership + const { username, ...restSearch } = useSearch({ + strict: false + }); + useEffect(() => { + if (username) { + setSearch(username); + navigate({ + to: ".", + replace: true, + search: restSearch + }); + } + }, [username]); + const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["assumePrivileges"] as const); const handlePerPageChange = (newPerPage: number) => { diff --git a/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-manager.tsx b/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-manager.tsx index 01349a83b..c7c1e6e85 100644 --- a/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-manager.tsx +++ b/frontend/src/pages/project/GroupDetailsByIDPage/route-secret-manager.tsx @@ -1,4 +1,6 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router"; +import { zodValidator } from "@tanstack/zod-adapter"; +import { z } from "zod"; import { ProjectAccessControlTabs } from "@app/types/project"; @@ -8,6 +10,11 @@ export const Route = createFileRoute( "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/groups/$groupId" )({ component: GroupDetailsByIDPage, + validateSearch: zodValidator( + z.object({ + username: z.string().optional().catch(undefined) + }) + ), beforeLoad: ({ context, params }) => { return { breadcrumbs: [ diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretVersionItem.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretVersionItem.tsx index a2c25b13a..b6b7175c6 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretVersionItem.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretVersionItem.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { faEye } from "@fortawesome/free-regular-svg-icons"; import { faArrowRotateRight, + faBan, faDesktop, faEyeSlash, faServer, @@ -68,10 +69,14 @@ export const SecretVersionItem = ({ const getLinkToModifyHistoryEntity = ( actorId: string, actorType: string, - membershipId: string | null = "" + membershipId: string | null = "", + groupId: string | null = "", + actorName: string | null = "" ) => { switch (actorType) { case ActorType.USER: + if (groupId) + return `/projects/secret-management/${currentProject.id}/groups/${groupId}?username=${actorName}`; return `/projects/secret-management/${currentProject.id}/members/${membershipId}`; case ActorType.IDENTITY: return `/projects/secret-management/${currentProject.id}/identities/${actorId}`; @@ -83,10 +88,26 @@ export const SecretVersionItem = ({ const onModifyHistoryClick = ( actorId: string | undefined | null, actorType: string | undefined | null, - membershipId: string | undefined | null + membershipId: string | undefined | null, + groupId: string | undefined | null, + actorName: string | undefined | null ) => { + if (!membershipId) { + createNotification({ + type: "info", + text: `This ${actorType === ActorType.USER ? "user" : "identity"} is no longer a member of this project.` + }); + return; + } + if (actorType && actorId && actorType !== ActorType.PLATFORM) { - const redirectLink = getLinkToModifyHistoryEntity(actorId, actorType, membershipId); + const redirectLink = getLinkToModifyHistoryEntity( + actorId, + actorType, + membershipId, + groupId, + actorName + ); if (redirectLink) { navigate({ to: redirectLink }); } @@ -157,15 +178,33 @@ export const SecretVersionItem = ({
Modified by: - + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
- onModifyHistoryClick(actor.actorId, actor.actorType, actor.membershipId) + onClick={ + actor.membershipId + ? () => + onModifyHistoryClick( + actor.actorId, + actor.actorType, + actor.membershipId, + actor.groupId, + actor.name + ) + : undefined } - className="cursor-pointer" + className={actor.membershipId ? "cursor-pointer" : undefined} > + {!actor.membershipId && ( + + )}