From 29af5308f3d6a791e15d16074e7eba40302439cc Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Thu, 28 May 2026 15:22:20 +0800 Subject: [PATCH] [FE-3493] fix(studio): respect role impersonation when copying truncated rows (#46442) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copy/export of selected rows in the Table Editor refetches full values for cells truncated in the grid (via `getCellValue`), but that refetch was bypassing role impersonation. The main grid query respects the impersonated role; the truncated-cell hydration didn't, so the copy could fetch as the service role even when "View as " was active – an inconsistency, since the UI still indicates the impersonated role is in effect. Threads `roleImpersonationState` through `hydrateTruncatedRows` → `getCellValue`, and wraps the SQL in `wrapWithRoleImpersonation` (matching how `getTableRows` does it). Addresses FE-3493. **Changed:** - `getCellValue` accepts an optional `roleImpersonationState` and wraps its SQL with `wrapWithRoleImpersonation` + flags `isRoleImpersonationEnabled` on `executeSql` - `hydrateTruncatedRows` threads `roleImpersonationState` through to `getCellValue` - `Header.tsx`'s `onCopyRows` passes the in-scope `roleImpersonationState` into `hydrateTruncatedRows` ## To test 1. Open the Table Editor on a table with a row containing a large/truncated string value and a primary key 2. Enable role impersonation → "View as role" → pick any role with read access to the table 3. Select the row, then `Copy → Copy as JSON` (also try CSV / SQL) 4. The copy should succeed and contain the full (non-truncated) value 5. Inspect the SQL request – it should now be wrapped with the impersonation context, matching how the main grid query is wrapped Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com> --- .../components/grid/components/header/Header.tsx | 1 + .../grid/components/header/Header.utils.ts | 4 ++++ .../data/table-rows/get-cell-value-mutation.ts | 16 ++++++++++++++-- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/studio/components/grid/components/header/Header.tsx b/apps/studio/components/grid/components/header/Header.tsx index dc4c830bf04..efaf1c26acc 100644 --- a/apps/studio/components/grid/components/header/Header.tsx +++ b/apps/studio/components/grid/components/header/Header.tsx @@ -235,6 +235,7 @@ const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => { table: snap.table, projectRef: project.ref, connectionString: project.connectionString ?? null, + roleImpersonationState: roleImpersonationState as RoleImpersonationState, }) if (hydrated.status !== 'ok') { throw new Error('Failed to fetch full values for truncated cells') diff --git a/apps/studio/components/grid/components/header/Header.utils.ts b/apps/studio/components/grid/components/header/Header.utils.ts index f9dbee6a3ee..7c2c7fb1dec 100644 --- a/apps/studio/components/grid/components/header/Header.utils.ts +++ b/apps/studio/components/grid/components/header/Header.utils.ts @@ -3,6 +3,7 @@ import Papa from 'papaparse' import type { SupaTable } from '@/components/grid/types' import { isValueTruncated } from '@/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils' import { getCellValue } from '@/data/table-rows/get-cell-value-mutation' +import type { RoleImpersonationState } from '@/lib/role-impersonation' export const formatRowsForCSV = ({ rows, columns }: { rows: any[]; columns: string[] }) => { const formattedRows = rows.map((row) => { @@ -34,11 +35,13 @@ export const hydrateTruncatedRows = async ({ table, projectRef, connectionString, + roleImpersonationState, }: { rows: Record[] table: SupaTable projectRef: string connectionString: string | null + roleImpersonationState?: RoleImpersonationState }): Promise => { const jobs: { rowIdx: number; column: string }[] = [] rows.forEach((row, rowIdx) => { @@ -73,6 +76,7 @@ export const hydrateTruncatedRows = async ({ table: { schema: table.schema ?? 'public', name: table.name }, column, pkMatch, + roleImpersonationState, }) }) ) diff --git a/apps/studio/data/table-rows/get-cell-value-mutation.ts b/apps/studio/data/table-rows/get-cell-value-mutation.ts index b7987745b34..7fa7a637acb 100644 --- a/apps/studio/data/table-rows/get-cell-value-mutation.ts +++ b/apps/studio/data/table-rows/get-cell-value-mutation.ts @@ -4,6 +4,8 @@ import { useMutation } from '@tanstack/react-query' import { toast } from 'sonner' import { executeSql } from '@/data/sql/execute-sql-query' +import { RoleImpersonationState, wrapWithRoleImpersonation } from '@/lib/role-impersonation' +import { isRoleImpersonationEnabled } from '@/state/role-impersonation-state' import type { ResponseError, UseCustomMutationOptions } from '@/types' export type GetCellValueVariables = { @@ -12,6 +14,7 @@ export type GetCellValueVariables = { table: { schema: string; name: string } column: string pkMatch: { [key: string]: any } + roleImpersonationState?: RoleImpersonationState } export function getCellValueSql({ @@ -32,9 +35,18 @@ export async function getCellValue({ table, column, pkMatch, + roleImpersonationState, }: GetCellValueVariables) { - const sql = getCellValueSql({ table, column, pkMatch }) - const { result } = await executeSql({ projectRef, connectionString, sql }) + const sql = wrapWithRoleImpersonation( + getCellValueSql({ table, column, pkMatch }), + roleImpersonationState + ) + const { result } = await executeSql({ + projectRef, + connectionString, + sql, + isRoleImpersonationEnabled: isRoleImpersonationEnabled(roleImpersonationState?.role), + }) return result?.[0][column] }