Skip to content

fix(file-manager): authorize org-level file access by organization_id#4192

Open
gorkem-bwl wants to merge 2 commits into
developfrom
fix/file-manager-org-tenant-check
Open

fix(file-manager): authorize org-level file access by organization_id#4192
gorkem-bwl wants to merge 2 commits into
developfrom
fix/file-manager-org-tenant-check

Conversation

@gorkem-bwl

Copy link
Copy Markdown
Contributor

Summary

Fixes a 403 Forbidden when downloading (or deleting, or viewing metadata for) an organization-level file such as an evidence upload or an AI Trust Center file. The error affected everyone, including org admins.

Root cause

The access check for organization-level files (project_id IS NULL) compared file.org_id against the caller's organization. org_id is a legacy column that is NULL for files created by newer flows (AI Trust Center, evidence uploads), so Number(null) = 0 never matched the real org id and every such request was denied. The canonical tenant column is organization_id, which getFileById already scopes the lookup by.

In the DB, 41 of 42 files have org_id NULL with organization_id set, so the download path was broken for nearly every org-level file.

Changes

  • fileManager.ctrl.ts — all five org-file access checks (downloadFile, removeFile, getFileMetadata, and two others) now compare organization_id instead of the legacy org_id.
  • file.model.ts — add the organization_id column to FileModel and the File interface. It was previously unmapped, so mapToModel silently dropped the real value.
  • file.repository.ts — add organization_id to OrganizationFileMetadata.
  • tests — mock files now carry organization_id; the org-mismatch cases assert on organization_id. 72/72 pass.

Notes

  • Tenant isolation was never weakened: getFileById filters WHERE organization_id = :organizationId, so a cross-org file returns 404 before this check runs. The re-check is belt-and-suspenders.
  • Verified against the live endpoint: the file that previously returned 403 now returns 200 with the correct content type.
  • A separate, pre-existing bug (file content stored as a JSON-serialized buffer, which makes downloads open as "damaged") is being tracked separately and is not addressed here.

Downloading, deleting, or viewing metadata for an organization-level file
(project_id IS NULL) returned 403 Forbidden for everyone, including org admins.

The access check compared file.org_id against the caller's org. org_id is a
legacy column that is NULL for files created by newer flows (AI Trust Center,
evidence uploads), so Number(null) = 0 never matched the real org id and every
such request was denied. The canonical tenant column is organization_id, which
getFileById already scopes the lookup by — so this re-check only needed to read
the right column.

## Changes
- fileManager.ctrl: all five org-file checks (downloadFile, removeFile,
  getFileMetadata, and two more) now compare organization_id, not org_id.
- file.model: add the organization_id column to FileModel and the File
  interface. It was unmapped, so mapToModel silently dropped the real value.
- file.repository: add organization_id to OrganizationFileMetadata.
- tests: mock files now carry organization_id; the org-mismatch cases assert on
  organization_id. 72/72 pass.

## Notes
- Tenant isolation was never actually weakened: getFileById filters
  WHERE organization_id = :organizationId, so a cross-org file returns 404
  before this check. The re-check is belt-and-suspenders.
- A separate, pre-existing bug (file content stored as a JSON-serialized buffer,
  making downloads open as damaged) is tracked separately and not addressed here.
Code-review catch on PR #4192: getFileMetadata regressed because it sources its
row from getFileWithMetadata, whose explicit SELECT list projected f.org_id but
not f.organization_id. The new access check Number(file.organization_id) !== orgId
then read undefined -> NaN, which never equals orgId, so metadata for every
org-level file 403'd — the exact bug this PR set out to fix, reintroduced on the
metadata endpoint. The other handlers were unaffected because they use
getFileById (SELECT *).

- Add f.organization_id to the getFileWithMetadata SELECT list (WHERE already
  scoped by it).
- Add a repository unit test asserting the column is in the SELECT projection
  (scoped to SELECT..FROM, since organization_id also appears in JOIN/WHERE). The
  controller test could not catch this — it mocks the repository, so the real
  SQL projection was never exercised. Verified the guard fails when the column is
  dropped.

@MuhammadKhalilzadeh MuhammadKhalilzadeh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Evidences work fine now, but for AI T0rust Center, we're getting the followings on setting a logo:

2026-06-29T17:41:23.123Z [debug]: 🖼️ Uploading company logo
2026-06-29T17:41:23.171Z [info]: 124, 2026-06-29T17:41:23.171Z, error, failed to upload company logo, uploadCompanyLogo, aiTrustCentre.ctrl.ts
2026-06-29T17:41:40.048Z [info]: 125, 2026-06-29T17:41:40.048Z, processing, uploading company logo, uploadCompanyLogo, aiTrustCentre.ctrl.ts
2026-06-29T17:41:40.049Z [debug]: 🖼️ Uploading company logo
2026-06-29T17:41:40.175Z [info]: 126, 2026-06-29T17:41:40.175Z, error, failed to upload company logo, uploadCompanyLogo, aiTrustCentre.ctrl.ts
2026-06-29T17:41:54.358Z [info]: 127, 2026-06-29T17:41:54.358Z, processing, uploading company logo, uploadCompanyLogo, aiTrustCentre.ctrl.ts
2026-06-29T17:41:54.358Z [debug]: 🖼️ Uploading company logo
2026-06-29T17:41:54.394Z [info]: 128, 2026-06-29T17:41:54.394Z, error, failed to upload company logo, uploadCompanyLogo, aiTrustCentre.ctrl.ts

@MuhammadKhalilzadeh MuhammadKhalilzadeh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

l🐜

@MuhammadKhalilzadeh MuhammadKhalilzadeh added this to the 2.5 milestone Jul 1, 2026
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