Merge pull request #4817 from Infisical/fix-handle-group-memberships-for-secret-versions

fix(secret-versions): handle group memberships in secret versions history
This commit is contained in:
Scott Wilson
2025-11-06 12:40:10 -08:00
committed by GitHub
7 changed files with 94 additions and 21 deletions
@@ -141,7 +141,8 @@ export const secretRawSchema = z.object({
actorId: z.string().nullable().optional(), actorId: z.string().nullable().optional(),
actorType: z.string().nullable().optional(), actorType: z.string().nullable().optional(),
name: 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() .optional()
.nullable(), .nullable(),
@@ -793,6 +793,7 @@ export const reshapeBridgeSecret = (
userActorId?: string | null; userActorId?: string | null;
identityActorId?: string | null; identityActorId?: string | null;
membershipId?: string | null; membershipId?: string | null;
groupId?: string | null;
actorType?: string | null; actorType?: string | null;
tags?: { tags?: {
id: string; id: string;
@@ -823,7 +824,8 @@ export const reshapeBridgeSecret = (
actorType: secret.actorType, actorType: secret.actorType,
actorId: secret.userActorId || secret.identityActorId, actorId: secret.userActorId || secret.identityActorId,
name: secret.identityActorName || secret.userActorName, name: secret.identityActorName || secret.userActorName,
membershipId: secret.membershipId membershipId: secret.membershipId,
groupId: secret.groupId
} }
: undefined, : undefined,
tags: secret.tags, tags: secret.tags,
@@ -182,7 +182,6 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
const findVersionsBySecretIdWithActors = async ({ const findVersionsBySecretIdWithActors = async ({
secretId, secretId,
projectId,
secretVersions, secretVersions,
findOpt = {}, findOpt = {},
tx tx
@@ -196,13 +195,22 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
try { try {
const { offset, limit, sort = [["createdAt", "desc"]] } = findOpt; const { offset, limit, sort = [["createdAt", "desc"]] } = findOpt;
const query = (tx || db.replicaNode())(TableName.SecretVersionV2) 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.Users, `${TableName.Users}.id`, `${TableName.SecretVersionV2}.userActorId`)
.leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`)
.leftJoin(TableName.UserGroupMembership, `${TableName.UserGroupMembership}.userId`, `${TableName.Users}.id`)
.leftJoin(TableName.Membership, (qb) => { .leftJoin(TableName.Membership, (qb) => {
void qb void qb
.on(`${TableName.Membership}.actorUserId`, `${TableName.SecretVersionV2}.userActorId`) .on(`${TableName.Membership}.scope`, db.raw("?", [AccessScope.Project]))
.andOn(`${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}.actorIdentityId`, `${TableName.SecretVersionV2}.identityActorId`)
.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`) .leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`)
.leftJoin( .leftJoin(
TableName.SecretVersionV2Tag, TableName.SecretVersionV2Tag,
@@ -216,12 +224,6 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
) )
.where((qb) => { .where((qb) => {
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId); 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); if (secretVersions?.length) void qb.whereIn(`${TableName.SecretVersionV2}.version`, secretVersions);
}) })
.select( .select(
@@ -229,6 +231,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
db.ref("username").withSchema(TableName.Users).as("userActorName"), db.ref("username").withSchema(TableName.Users).as("userActorName"),
db.ref("name").withSchema(TableName.Identity).as("identityActorName"), db.ref("name").withSchema(TableName.Identity).as("identityActorName"),
db.ref("id").withSchema(TableName.Membership).as("membershipId"), 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("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"), db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug") db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
@@ -256,7 +259,8 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
...SecretVersionsV2Schema.parse(el), ...SecretVersionsV2Schema.parse(el),
userActorName: el.userActorName, userActorName: el.userActorName,
identityActorName: el.identityActorName, identityActorName: el.identityActorName,
membershipId: el.membershipId membershipId: el.membershipId,
groupId: el.groupId
}), }),
childrenMapper: [ childrenMapper: [
{ {
+1
View File
@@ -109,6 +109,7 @@ export type SecretVersions = {
actorType?: string | null; actorType?: string | null;
name?: string | null; name?: string | null;
membershipId?: string | null; membershipId?: string | null;
groupId?: string | null;
} | null; } | null;
}; };
@@ -1,4 +1,4 @@
import { useMemo } from "react"; import { useEffect, useMemo } from "react";
import { import {
faArrowDown, faArrowDown,
faArrowUp, faArrowUp,
@@ -7,6 +7,7 @@ import {
faSearch faSearch
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { import {
@@ -48,6 +49,7 @@ enum GroupMembersOrderBy {
} }
export const GroupMembersTable = ({ groupMembership }: Props) => { export const GroupMembersTable = ({ groupMembership }: Props) => {
const navigate = useNavigate();
const { const {
search, search,
setSearch, setSearch,
@@ -62,6 +64,21 @@ export const GroupMembersTable = ({ groupMembership }: Props) => {
initPerPage: getUserTablePreference("projectGroupMembersTable", PreferenceKey.PerPage, 20) 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 { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["assumePrivileges"] as const);
const handlePerPageChange = (newPerPage: number) => { const handlePerPageChange = (newPerPage: number) => {
@@ -1,4 +1,6 @@
import { createFileRoute, linkOptions } from "@tanstack/react-router"; import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { ProjectAccessControlTabs } from "@app/types/project"; 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" "/_authenticate/_inject-org-details/_org-layout/projects/secret-management/$projectId/_secret-manager-layout/groups/$groupId"
)({ )({
component: GroupDetailsByIDPage, component: GroupDetailsByIDPage,
validateSearch: zodValidator(
z.object({
username: z.string().optional().catch(undefined)
})
),
beforeLoad: ({ context, params }) => { beforeLoad: ({ context, params }) => {
return { return {
breadcrumbs: [ breadcrumbs: [
@@ -2,6 +2,7 @@ import { useState } from "react";
import { faEye } from "@fortawesome/free-regular-svg-icons"; import { faEye } from "@fortawesome/free-regular-svg-icons";
import { import {
faArrowRotateRight, faArrowRotateRight,
faBan,
faDesktop, faDesktop,
faEyeSlash, faEyeSlash,
faServer, faServer,
@@ -68,10 +69,14 @@ export const SecretVersionItem = ({
const getLinkToModifyHistoryEntity = ( const getLinkToModifyHistoryEntity = (
actorId: string, actorId: string,
actorType: string, actorType: string,
membershipId: string | null = "" membershipId: string | null = "",
groupId: string | null = "",
actorName: string | null = ""
) => { ) => {
switch (actorType) { switch (actorType) {
case ActorType.USER: case ActorType.USER:
if (groupId)
return `/projects/secret-management/${currentProject.id}/groups/${groupId}?username=${actorName}`;
return `/projects/secret-management/${currentProject.id}/members/${membershipId}`; return `/projects/secret-management/${currentProject.id}/members/${membershipId}`;
case ActorType.IDENTITY: case ActorType.IDENTITY:
return `/projects/secret-management/${currentProject.id}/identities/${actorId}`; return `/projects/secret-management/${currentProject.id}/identities/${actorId}`;
@@ -83,10 +88,26 @@ export const SecretVersionItem = ({
const onModifyHistoryClick = ( const onModifyHistoryClick = (
actorId: string | undefined | null, actorId: string | undefined | null,
actorType: 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) { if (actorType && actorId && actorType !== ActorType.PLATFORM) {
const redirectLink = getLinkToModifyHistoryEntity(actorId, actorType, membershipId); const redirectLink = getLinkToModifyHistoryEntity(
actorId,
actorType,
membershipId,
groupId,
actorName
);
if (redirectLink) { if (redirectLink) {
navigate({ to: redirectLink }); navigate({ to: redirectLink });
} }
@@ -157,15 +178,35 @@ export const SecretVersionItem = ({
<div className="flex flex-row"> <div className="flex flex-row">
<div className="flex w-fit flex-row text-sm"> <div className="flex w-fit flex-row text-sm">
Modified by: Modified by:
<Tooltip content={getModifiedByName(actor.actorType, actor.name)}> <Tooltip
className="z-[100] max-w-sm"
content={
getModifiedByName(actor.actorType, actor.name) +
(!actor.membershipId && actor.actorId ? " (Removed from project)" : "")
}
>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div <div
onClick={() => onClick={
onModifyHistoryClick(actor.actorId, actor.actorType, actor.membershipId) actor.membershipId
? () =>
onModifyHistoryClick(
actor.actorId,
actor.actorType,
actor.membershipId,
actor.groupId,
actor.name
)
: undefined
} }
className="cursor-pointer" className={actor.membershipId ? "cursor-pointer" : undefined}
> >
<FontAwesomeIcon icon={getModifiedByIcon(actor.actorType)} className="ml-2" /> <FontAwesomeIcon icon={getModifiedByIcon(actor.actorType)} className="ml-2" />
{!actor.membershipId &&
actor.actorType &&
[ActorType.USER, ActorType.IDENTITY].includes(
actor.actorType as ActorType
) && <FontAwesomeIcon className="ml-1 text-mineshaft-400" icon={faBan} />}
</div> </div>
</Tooltip> </Tooltip>
</div> </div>