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

View File

@@ -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(),

View File

@@ -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,

View File

@@ -182,7 +182,6 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
const findVersionsBySecretIdWithActors = async ({
secretId,
projectId,
secretVersions,
findOpt = {},
tx
@@ -196,13 +195,22 @@ 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.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`)
.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}.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.SecretVersionV2Tag,
@@ -216,12 +224,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 +231,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 +259,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: [
{

View File

@@ -109,6 +109,7 @@ export type SecretVersions = {
actorType?: string | null;
name?: string | null;
membershipId?: string | null;
groupId?: string | null;
} | null;
};

View File

@@ -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) => {

View File

@@ -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: [

View File

@@ -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,35 @@ export const SecretVersionItem = ({
<div className="flex flex-row">
<div className="flex w-fit flex-row text-sm">
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 */}
<div
onClick={() =>
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}
>
<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>
</Tooltip>
</div>