Skip to content

feat: implement column-level read permissions for table queries#1843

Merged
Artuomka merged 1 commit into
mainfrom
backend_column_level_permission
Jun 15, 2026
Merged

feat: implement column-level read permissions for table queries#1843
Artuomka merged 1 commit into
mainfrom
backend_column_level_permission

Conversation

@Artuomka

@Artuomka Artuomka commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator
  • Replaced TableReadGuard with QueryTableGuard in relevant controllers to enforce new query permissions.
  • Introduced QueryTableGuard to validate user permissions for querying tables.
  • Updated use cases to filter rows and structures based on readable columns.
  • Added utility functions for filtering columns by read permissions.
  • Enhanced CSV export functionality to respect column-level permissions.
  • Updated tests to cover new permission logic and ensure correct enforcement of column visibility.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added column-level read access control, allowing administrators to restrict which columns users can view within tables.
    • Introduced readableColumns configuration to define permitted columns per user and connection.
    • Columns are now filtered in row data, CSV exports, and table structure views based on user permissions.
  • Tests

    • Added comprehensive test coverage for column-level authorization and permission enforcement.

- Replaced TableReadGuard with QueryTableGuard in relevant controllers to enforce new query permissions.
- Introduced QueryTableGuard to validate user permissions for querying tables.
- Updated use cases to filter rows and structures based on readable columns.
- Added utility functions for filtering columns by read permissions.
- Enhanced CSV export functionality to respect column-level permissions.
- Updated tests to cover new permission logic and ensure correct enforcement of column visibility.
Copilot AI review requested due to automatic review settings June 15, 2026 13:11
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Introduces column-level read authorization into the Cedar policy model by adding a Column entity type and two new actions (table:query, column:read). The existing TableReadGuard is renamed to QueryTableGuard and updated to use table:query. Table row, structure, and CSV responses are now filtered to only include columns the requesting user is permitted to read.

Changes

Column-Level Cedar Authorization

Layer / File(s) Summary
Cedar schema and action-map contracts
backend/src/entities/cedar-authorization/cedar-schema.json, cedar-schema.ts, cedar-action-map.ts, backend/src/entities/permission/permission.interface.ts, ...create-permissions.ds.ts
Adds Column entity (memberOf Table, attrs connectionId/tableName), table:query and column:read actions to the Cedar schema (both JSON and TS forms); adds CedarAction.TableQuery, CedarResourceType.Column, COLUMN_PROBE_ID, optional columnName in CedarValidationRequest, and readableColumns to permission interfaces.
Policy generator and parser
backend/src/entities/cedar-authorization/cedar-policy-generator.ts, cedar-policy-parser.ts
generateCedarPolicyForGroup emits table:query plus either per-column or table-wide column:read permits; parseCedarPolicyToClassicalPermissions recognizes table:query and column:read permits, maintains the readableColumns whitelist, and adds extractColumnResource helper.
Cedar entity builder
backend/src/entities/cedar-authorization/cedar-entity-builder.ts
buildCedarEntities gains an optional columnName parameter and conditionally emits a RocketAdmin::Column entity with a parent link to RocketAdmin::Table.
CedarAuthorizationService routing and validation
backend/src/entities/cedar-authorization/cedar-authorization.service.ts
validate routes table:query (with table:read fallback) and a new column prefix case through the action switch; evaluate passes columnName to buildCedarEntities; validatePolicyReferences enforces foreign-connection checks for RocketAdmin::Column resources.
CedarPermissionsService column evaluation
backend/src/entities/cedar-authorization/cedar-permissions.service.ts
Adds evaluateTableQuery (with legacy table:read fallback), evaluateColumnRead (using COLUMN_PROBE_ID probe for table-wide grants), and public checkColumnRead/getReadableColumns methods; table permission flags now use evaluateTableQuery.
QueryTableGuard and controller wiring
backend/src/guards/query-table.guard.ts, backend/src/entities/table/table.controller.ts, ...table-pure-crud-operations/..., ...table-filters/..., ...personal-table-settings/..., ...visualizations/panel/use-cases/*
TableReadGuard renamed to QueryTableGuard using CedarAction.TableQuery; all read-oriented table endpoints swap to QueryTableGuard; panel use cases switch cedarAuthService.validate to CedarAction.TableQuery.
Column filtering utility and use-case enforcement
backend/src/entities/table/utils/filter-columns-by-read-permission.util.ts, ...use-cases/get-table-rows.use.case.ts, ...get-row-by-primary-key.use.case.ts, ...export-csv-from-table.use.case.ts
New utility exports five column-filtering helpers; getTableRows, getRowByPrimaryKey, and exportCSVFromTable use cases call getReadableColumns and conditionally strip non-readable columns from rows, structure metadata, list fields, and CSV output.
Tests
backend/test/ava-tests/non-saas-tests/non-saas-cedar-entity-builder.test.ts, ...cedar-policy-generator.test.ts, ...cedar-policy-parser.test.ts, ...cedar-save-policy-e2e.test.ts
New and updated AVA unit tests for entity builder, policy generator, and parser; new e2e tests covering column whitelisting, table:query enforcement, legacy table:read alias, per-row/structure/CSV column filtering, and readableColumns persistence round-trip.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant QueryTableGuard
  participant GetTableRowsUseCase
  participant CedarPermissionsService
  participant CedarAuthorizationService

  Client->>QueryTableGuard: GET /table/rows/:connectionId
  QueryTableGuard->>CedarAuthorizationService: validate(table:query, tableName)
  CedarAuthorizationService-->>QueryTableGuard: allowed / denied
  QueryTableGuard-->>Client: 403 if denied

  QueryTableGuard->>GetTableRowsUseCase: execute(rowsInput)
  GetTableRowsUseCase->>CedarPermissionsService: getReadableColumns(user, connectionId, tableName, allCols)
  CedarPermissionsService->>CedarAuthorizationService: validate(column:read, COLUMN_PROBE_ID) [probe]
  alt table-wide grant (probe passes)
    CedarAuthorizationService-->>CedarPermissionsService: allowed → all columns readable
  else per-column
    loop each column
      CedarPermissionsService->>CedarAuthorizationService: validate(column:read, columnName)
    end
  end
  CedarPermissionsService-->>GetTableRowsUseCase: Set<readableColumns>
  GetTableRowsUseCase->>GetTableRowsUseCase: filter rows/structure/list_fields to readableColumns
  GetTableRowsUseCase-->>Client: filtered rowsRO response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • rocket-admin/rocketadmin#1799: This PR adds table:query and column:read to CEDAR_SCHEMA.RocketAdmin.actions, which directly feeds the /permissions/available catalog built from that schema in PR #1799.
  • rocket-admin/rocketadmin#1831: Both PRs modify table-pure-crud-operations.controller.ts — PR #1831 introduced the CRUD endpoints that this PR now switches from TableReadGuard to QueryTableGuard.
  • rocket-admin/rocketadmin#1713: Both extend the same Cedar pipeline files (cedar-action-map.ts, cedar-authorization.service.ts, cedar-entity-builder.ts) by adding a new scoped resource type and a new field on CedarValidationRequest.

Suggested reviewers

  • lyubov-voloshko
  • gugu

Poem

🐇 Hopping through the policy rows,
Each column now gets what it's owed.
table:query opens the gate,
column:read decides your fate!
No peeking at hidden fields, no way —
The rabbit guards Cedar all day. 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Security Check ⚠️ Warning The PR has at least two critical security vulnerabilities: (1) Probe-based shortcut in getReadableColumns can be exploited if a database column is named 'probe' to grant all-column access inste... Fix the probe-based shortcut by removing the early return on COLUMN_PROBE_ID match and checking all columns individually. Also filter metadata fields (primaryColumns, table_settings, etc.) that expose column names when restrictColumns is...
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and concisely summarizes the main objective: implementing column-level read permissions for table queries, which aligns perfectly with the changeset's comprehensive modifications across authorization, entity management, and data access layers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch backend_column_level_permission

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements Cedar-backed column-level read permissions for table querying by introducing table:query (pre-query gate) and column:read (post-query filtering), while preserving legacy table:read as an alias for full read.

Changes:

  • Replaces table read guarding with QueryTableGuard (uses table:query) across table read/query endpoints and visualization query execution.
  • Adds column:read support end-to-end: Cedar schema/entity support, policy generator/parser updates, and row/structure/CSV filtering utilities.
  • Expands non-SaaS AVA test coverage for column visibility, legacy alias behavior, and permissions round-trips.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
backend/test/ava-tests/non-saas-tests/non-saas-cedar-save-policy-e2e.test.ts Adds E2E coverage for column filtering, query denial, legacy alias behavior, CSV export filtering, and permissions persistence.
backend/test/ava-tests/non-saas-tests/non-saas-cedar-policy-parser.test.ts Adds parser tests for table:query + column:read (wildcard and per-column) and generator/parser round-trips.
backend/test/ava-tests/non-saas-tests/non-saas-cedar-policy-generator.test.ts Updates generator expectations for the new alias model (table:query + column:read).
backend/test/ava-tests/non-saas-tests/non-saas-cedar-entity-builder.test.ts Tests new Column entity building and table parent relationship.
backend/src/guards/query-table.guard.ts Renames/repurposes guard to enforce table:query instead of table:read.
backend/src/entities/visualizations/panel/use-cases/test-db-query.use.case.ts Switches referenced table validation from table:read to table:query.
backend/src/entities/visualizations/panel/use-cases/execute-panel.use.case.ts Switches referenced table validation from table:read to table:query.
backend/src/entities/table/utils/filter-columns-by-read-permission.util.ts New utilities to filter rows/structure/column name lists by readable column set.
backend/src/entities/table/use-cases/get-table-rows.use.case.ts Computes readable columns and filters returned rows/metadata accordingly.
backend/src/entities/table/use-cases/get-row-by-primary-key.use.case.ts Applies readable-column filtering to single-row retrieval response.
backend/src/entities/table/use-cases/export-csv-from-table.use.case.ts Filters exported CSV rows to readable columns (array and stream modes).
backend/src/entities/table/table.controller.ts Switches read/structure/row/CSV endpoints to QueryTableGuard.
backend/src/entities/table/table-pure-crud-operations/table-pure-crud-operations.controller.ts Switches CRUD read endpoints to QueryTableGuard.
backend/src/entities/table-settings/personal-table-settings/personal-table-settings.controller.ts Switches settings endpoints to QueryTableGuard.
backend/src/entities/table-filters/table-filters.controller.ts Switches filters read endpoint to QueryTableGuard.
backend/src/entities/permission/permission.interface.ts Adds readableColumns?: string[] to table permission model.
backend/src/entities/permission/application/data-structures/create-permissions.ds.ts Adds DTO validation for readableColumns.
backend/src/entities/cedar-authorization/cedar-schema.ts Adds Column entity type and table:query / column:read actions.
backend/src/entities/cedar-authorization/cedar-schema.json JSON schema mirror update for Column + new actions.
backend/src/entities/cedar-authorization/cedar-policy-parser.ts Parses table:query and column:read into classical permissions + whitelists.
backend/src/entities/cedar-authorization/cedar-policy-generator.ts Generates table:query + wildcard/per-column column:read instead of direct table:read.
backend/src/entities/cedar-authorization/cedar-permissions.service.ts Adds checkColumnRead + getReadableColumns; updates table-read checks to mean query permission.
backend/src/entities/cedar-authorization/cedar-entity-builder.ts Adds Column entity parented by Table to support resource in Table policies.
backend/src/entities/cedar-authorization/cedar-authorization.service.ts Adds Column validation flow and policy reference validation for Column ids.
backend/src/entities/cedar-authorization/cedar-action-map.ts Adds TableQuery / ColumnRead actions, Column resource type, and probe id support.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +363 to +370
if (restrictColumns) {
rowsRO.rows = filterRowsByReadableColumns(rowsRO.rows, readableColumns);
rowsRO.structure = filterStructureByReadableColumns(rowsRO.structure, readableColumns);
rowsRO.list_fields = filterColumnNamesByReadable(rowsRO.list_fields, readableColumns) ?? [];
rowsRO.sortable_by = filterColumnNamesByReadable(rowsRO.sortable_by, readableColumns) ?? [];
rowsRO.columns_view = filterColumnNamesByReadable(rowsRO.columns_view, readableColumns);
rowsRO.table_permissions.readableColumns = Array.from(readableColumns);
}
Comment on lines 193 to 197
readonly_fields: tableSettings?.readonly_fields ? tableSettings.readonly_fields : [],
list_fields: findAvailableFields(builtDAOsTableSettings, tableStructure),
list_fields: listFields,
action_events: customActionEvents.map((event) => buildActionEventDto(event)),
table_actions: customActionEvents.map((el) => buildActionEventDto(el)),
identity_column: tableSettings?.identity_column ? tableSettings.identity_column : null,
Comment on lines 381 to 392
const tableResourceIds = [...cedarPolicy.matchAll(/resource\s*==\s*RocketAdmin::Table::"([^"]+)"/g)].map(
(m) => m[1],
);

for (const tableRef of tableResourceIds) {
if (!tableRef.startsWith(`${connectionId}/`)) {
throw new HttpException(
{ message: Messages.CEDAR_POLICY_REFERENCES_FOREIGN_CONNECTION },
HttpStatus.BAD_REQUEST,
);
}
}
Comment on lines +407 to +411
// The exported file is returned as a binary body (Buffer), not parsed text.
const csvText = (csvResponse.body as Buffer)?.toString() ?? csvResponse.text;
// The CSV header is derived from row keys; the hidden column must not appear at all.
t.true(csvText.includes(allowedColumn));
t.false(csvText.includes(hiddenColumn));
Comment on lines +335 to 336
@UseGuards(QueryTableGuard)
@Get('/table/structure/:connectionId')

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
backend/src/entities/table/utils/filter-columns-by-read-permission.util.ts (1)

5-42: ⚡ Quick win

Use arrow-function exports for these helpers.

This utility introduces multiple function declarations; the project JS/TS style prefers arrow functions for declarations.

As per coding guidelines, "Prefer arrow functions over function declarations".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/entities/table/utils/filter-columns-by-read-permission.util.ts`
around lines 5 - 42, Convert all function declarations in this file to arrow
function exports to align with the project's coding style guidelines.
Specifically, refactor isAllColumnsReadable, filterRowByReadableColumns,
filterRowsByReadableColumns, filterStructureByReadableColumns, and
filterColumnNamesByReadable from traditional function declaration syntax to
const-based arrow function syntax while preserving their return types and
parameters.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/entities/cedar-authorization/cedar-permissions.service.ts`:
- Around line 298-300: The evaluateColumnRead method call at line 298 uses
COLUMN_PROBE_ID as a synthetic probe to determine if a user can read all
columns, but if a real column or policy resource actually uses that same probe
ID value, it can over-grant access. Ensure that COLUMN_PROBE_ID is guaranteed to
be a unique, non-colliding identifier that cannot match any real column name, or
alternatively refactor the authorization logic to avoid this shortcut and
instead enumerate individual column permissions rather than treating a single
probe result as proxy for all columns.

In `@backend/src/entities/table/use-cases/export-csv-from-table.use.case.ts`:
- Around line 132-147: Remove the `as any` type assertion from the csv.stringify
call in the restrictColumns branch where rowsArray is processed. Additionally,
add explicit type annotations to the Transform stream callback function
parameters in the second restrictColumns branch: annotate the `row` parameter as
`Record<string, unknown>` (consistent with how it is used in the
filterRowByReadableColumns call) and annotate the `_encoding` parameter with the
appropriate encoding type from Node.js stream types. This ensures both the
array-based path and the stream-based path maintain proper type safety without
resorting to generic `any` types.

In `@backend/src/entities/table/use-cases/get-table-rows.use.case.ts`:
- Around line 363-370: The code currently filters several column-related fields
when restrictColumns is enabled, but leaves other metadata fields containing
column names unfiltered (such as primaryColumns and column arrays within
table_settings), allowing restricted users to infer hidden columns. Identify all
response fields on the rowsRO object that carry column names (beyond rows,
structure, list_fields, sortable_by, and columns_view), and apply the same
filtering pattern using the existing filter functions like
filterColumnNamesByReadable to those fields as well. Ensure this filtering is
applied consistently in both the multi-row response path shown in the
restrictColumns block and in the single-row use case response path.

---

Nitpick comments:
In `@backend/src/entities/table/utils/filter-columns-by-read-permission.util.ts`:
- Around line 5-42: Convert all function declarations in this file to arrow
function exports to align with the project's coding style guidelines.
Specifically, refactor isAllColumnsReadable, filterRowByReadableColumns,
filterRowsByReadableColumns, filterStructureByReadableColumns, and
filterColumnNamesByReadable from traditional function declaration syntax to
const-based arrow function syntax while preserving their return types and
parameters.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ca1c604c-903b-4de6-9371-c99835ef3666

📥 Commits

Reviewing files that changed from the base of the PR and between 6b5d9ed and 73b8d24.

📒 Files selected for processing (25)
  • backend/src/entities/cedar-authorization/cedar-action-map.ts
  • backend/src/entities/cedar-authorization/cedar-authorization.service.ts
  • backend/src/entities/cedar-authorization/cedar-entity-builder.ts
  • backend/src/entities/cedar-authorization/cedar-permissions.service.ts
  • backend/src/entities/cedar-authorization/cedar-policy-generator.ts
  • backend/src/entities/cedar-authorization/cedar-policy-parser.ts
  • backend/src/entities/cedar-authorization/cedar-schema.json
  • backend/src/entities/cedar-authorization/cedar-schema.ts
  • backend/src/entities/permission/application/data-structures/create-permissions.ds.ts
  • backend/src/entities/permission/permission.interface.ts
  • backend/src/entities/table-filters/table-filters.controller.ts
  • backend/src/entities/table-settings/personal-table-settings/personal-table-settings.controller.ts
  • backend/src/entities/table/table-pure-crud-operations/table-pure-crud-operations.controller.ts
  • backend/src/entities/table/table.controller.ts
  • backend/src/entities/table/use-cases/export-csv-from-table.use.case.ts
  • backend/src/entities/table/use-cases/get-row-by-primary-key.use.case.ts
  • backend/src/entities/table/use-cases/get-table-rows.use.case.ts
  • backend/src/entities/table/utils/filter-columns-by-read-permission.util.ts
  • backend/src/entities/visualizations/panel/use-cases/execute-panel.use.case.ts
  • backend/src/entities/visualizations/panel/use-cases/test-db-query.use.case.ts
  • backend/src/guards/query-table.guard.ts
  • backend/test/ava-tests/non-saas-tests/non-saas-cedar-entity-builder.test.ts
  • backend/test/ava-tests/non-saas-tests/non-saas-cedar-policy-generator.test.ts
  • backend/test/ava-tests/non-saas-tests/non-saas-cedar-policy-parser.test.ts
  • backend/test/ava-tests/non-saas-tests/non-saas-cedar-save-policy-e2e.test.ts

Comment on lines +298 to +300
if (this.evaluateColumnRead(cognitoUserName, connectionId, tableName, COLUMN_PROBE_ID, ctx)) {
return new Set(allColumnNames);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Probe-based full-column shortcut can over-grant access.

Line 298 treats an allow on synthetic __probe__ as “all columns readable”. If a real column (or explicit policy resource) uses that same id, users can be escalated from one-column access to all columns.

Suggested fix
-		if (this.evaluateColumnRead(cognitoUserName, connectionId, tableName, COLUMN_PROBE_ID, ctx)) {
-			return new Set(allColumnNames);
-		}
-
 		const readable = new Set<string>();
 		for (const columnName of allColumnNames) {
 			if (this.evaluateColumnRead(cognitoUserName, connectionId, tableName, columnName, ctx)) {
 				readable.add(columnName);
 			}
 		}
 		return readable;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (this.evaluateColumnRead(cognitoUserName, connectionId, tableName, COLUMN_PROBE_ID, ctx)) {
return new Set(allColumnNames);
}
const readable = new Set<string>();
for (const columnName of allColumnNames) {
if (this.evaluateColumnRead(cognitoUserName, connectionId, tableName, columnName, ctx)) {
readable.add(columnName);
}
}
return readable;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/entities/cedar-authorization/cedar-permissions.service.ts` around
lines 298 - 300, The evaluateColumnRead method call at line 298 uses
COLUMN_PROBE_ID as a synthetic probe to determine if a user can read all
columns, but if a real column or policy resource actually uses that same probe
ID value, it can over-grant access. Ensure that COLUMN_PROBE_ID is guaranteed to
be a unique, non-colliding identifier that cannot match any real column name, or
alternatively refactor the authorization logic to avoid this shortcut and
instead enumerate individual column permissions rather than treating a single
probe result as proxy for all columns.

Comment on lines +132 to 147
const rowsArray = restrictColumns
? (rowsStream as unknown as Array<Record<string, unknown>>).map((row) =>
filterRowByReadableColumns(row, readableColumns),
)
: rowsStream;
return new StreamableFile(csv.stringify(rowsArray as any, { header: true }));
}
if (restrictColumns) {
const columnFilterTransform = new Transform({
objectMode: true,
transform(row, _encoding, callback) {
callback(null, filterRowByReadableColumns(row as Record<string, unknown>, readableColumns));
},
});
return new StreamableFile(rowsStream.pipe(columnFilterTransform).pipe(csv.stringify({ header: true })));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and examine the code at lines 132-147
fd -p "export-csv-from-table.use.case.ts" backend/

Repository: rocket-admin/rocketadmin

Length of output: 139


🏁 Script executed:

# Get the file content to verify the actual code and context
cat -n backend/src/entities/table/use-cases/export-csv-from-table.use.case.ts | sed -n '100,160p'

Repository: rocket-admin/rocketadmin

Length of output: 2760


🏁 Script executed:

# Check the current imports at the top of the file
head -30 backend/src/entities/table/use-cases/export-csv-from-table.use.case.ts

Repository: rocket-admin/rocketadmin

Length of output: 2383


🏁 Script executed:

# Check what types are available from node:stream
node -e "const stream = require('stream'); console.log(Object.getOwnPropertyNames(stream).filter(name => name.includes('Callback')).join('\n'))"

Repository: rocket-admin/rocketadmin

Length of output: 69


🌐 Web query:

node:stream TransformCallback type Node.js TypeScript

💡 Result:

In Node.js, TransformCallback is a TypeScript type alias used within the stream module to define the callback function signature for the _transform and _flush methods [1]. The signature of TransformCallback is defined as: type TransformCallback = (error?: Error | null, data?: any) => void; This callback is used to signal that the transformation of a chunk is complete or that an error has occurred [2]. Key implementation details: - It is typically imported from node:stream [3][4]. - When implementing a custom Transform stream by extending the Transform class, the _transform method accepts this callback as its third argument [1][2]. - You call the callback with an error object (or null) to indicate the status of the operation [2]. You can optionally pass a second argument (data) to push the transformed chunk to the readable side of the stream, which is equivalent to calling this.push(data) followed by callback [5]. Example usage: import { Transform, type TransformCallback } from 'node:stream'; class MyTransform extends Transform { _transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback): void { try { const transformed = chunk.toString.toUpperCase; // Signal completion and optionally pass the data callback(null, transformed); } catch (err) { // Signal error callback(err instanceof Error? err: new Error(String(err))); } } }

Citations:


Tighten types in the new CSV column-restriction path.

The new branch introduces as any and an unannotated Transform callback. Replace these with explicit stream/callback types to keep this path type-safe.

Proposed typed adjustment
-import { Transform } from 'node:stream';
+import { Transform, TransformCallback } from 'node:stream';
...
-				return new StreamableFile(csv.stringify(rowsArray as any, { header: true }));
+				return new StreamableFile(csv.stringify(rowsArray as Array<Record<string, unknown>>, { header: true }));
...
-					transform(row, _encoding, callback) {
+					transform(
+						row: Record<string, unknown>,
+						_encoding: BufferEncoding,
+						callback: TransformCallback,
+					): void {
 						callback(null, filterRowByReadableColumns(row as Record<string, unknown>, readableColumns));
 					},

The coding guidelines require: "Always add type annotations to function parameters and return types in TypeScript" and "Avoid any types - use specific types or generics instead".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const rowsArray = restrictColumns
? (rowsStream as unknown as Array<Record<string, unknown>>).map((row) =>
filterRowByReadableColumns(row, readableColumns),
)
: rowsStream;
return new StreamableFile(csv.stringify(rowsArray as any, { header: true }));
}
if (restrictColumns) {
const columnFilterTransform = new Transform({
objectMode: true,
transform(row, _encoding, callback) {
callback(null, filterRowByReadableColumns(row as Record<string, unknown>, readableColumns));
},
});
return new StreamableFile(rowsStream.pipe(columnFilterTransform).pipe(csv.stringify({ header: true })));
}
const rowsArray = restrictColumns
? (rowsStream as unknown as Array<Record<string, unknown>>).map((row) =>
filterRowByReadableColumns(row, readableColumns),
)
: rowsStream;
return new StreamableFile(csv.stringify(rowsArray as Array<Record<string, unknown>>, { header: true }));
}
if (restrictColumns) {
const columnFilterTransform = new Transform({
objectMode: true,
transform(
row: Record<string, unknown>,
_encoding: BufferEncoding,
callback: TransformCallback,
): void {
callback(null, filterRowByReadableColumns(row as Record<string, unknown>, readableColumns));
},
});
return new StreamableFile(rowsStream.pipe(columnFilterTransform).pipe(csv.stringify({ header: true })));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/entities/table/use-cases/export-csv-from-table.use.case.ts`
around lines 132 - 147, Remove the `as any` type assertion from the
csv.stringify call in the restrictColumns branch where rowsArray is processed.
Additionally, add explicit type annotations to the Transform stream callback
function parameters in the second restrictColumns branch: annotate the `row`
parameter as `Record<string, unknown>` (consistent with how it is used in the
filterRowByReadableColumns call) and annotate the `_encoding` parameter with the
appropriate encoding type from Node.js stream types. This ensures both the
array-based path and the stream-based path maintain proper type safety without
resorting to generic `any` types.

Source: Coding guidelines

Comment on lines +363 to +370
if (restrictColumns) {
rowsRO.rows = filterRowsByReadableColumns(rowsRO.rows, readableColumns);
rowsRO.structure = filterStructureByReadableColumns(rowsRO.structure, readableColumns);
rowsRO.list_fields = filterColumnNamesByReadable(rowsRO.list_fields, readableColumns) ?? [];
rowsRO.sortable_by = filterColumnNamesByReadable(rowsRO.sortable_by, readableColumns) ?? [];
rowsRO.columns_view = filterColumnNamesByReadable(rowsRO.columns_view, readableColumns);
rowsRO.table_permissions.readableColumns = Array.from(readableColumns);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restricted users can still infer hidden columns from unfiltered metadata.

When restriction is enabled, only rows, structure, and selected column lists are filtered. Column-name-bearing metadata such as primaryColumns (and column arrays within table_settings) can still expose non-readable columns. Apply the same readable-column filtering to every response field that carries column names (including the single-row use case response path).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/entities/table/use-cases/get-table-rows.use.case.ts` around lines
363 - 370, The code currently filters several column-related fields when
restrictColumns is enabled, but leaves other metadata fields containing column
names unfiltered (such as primaryColumns and column arrays within
table_settings), allowing restricted users to infer hidden columns. Identify all
response fields on the rowsRO object that carry column names (beyond rows,
structure, list_fields, sortable_by, and columns_view), and apply the same
filtering pattern using the existing filter functions like
filterColumnNamesByReadable to those fields as well. Ensure this filtering is
applied consistently in both the multi-row response path shown in the
restrictColumns block and in the single-row use case response path.

@Artuomka Artuomka merged commit 4433ae3 into main Jun 15, 2026
17 of 18 checks passed
@Artuomka Artuomka deleted the backend_column_level_permission branch June 15, 2026 13:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants