Skip to content

feat(usfm): scope export endpoints to the caller's project/org#201

Closed
henrique221 wants to merge 1 commit into
fix/require-auth-bibles-usfmfrom
fix/198-usfm-project-scoping
Closed

feat(usfm): scope export endpoints to the caller's project/org#201
henrique221 wants to merge 1 commit into
fix/require-auth-bibles-usfmfrom
fix/198-usfm-project-scoping

Conversation

@henrique221

@henrique221 henrique221 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Closes #198.

Stacked on #189 — base is the fix/require-auth-bibles-usfm branch (this builds on the authenticateUser middleware #189 adds to the USFM routes). Retarget to main once #189 merges.

Summary

After #189 the USFM routes require authentication but perform no project/org check — the export pulls per-project translated content (usfm.service.ts getProjectBooks + getBookVerses), so any logged-in user from another org could export a project they don't belong to by guessing a projectUnitId.

  • New requireProjectUnitAccess middleware (usfm-auth.middleware.ts): resolves projectUnitId → project → org and applies ProjectPolicy.read, mirroring translated-verse-auth.middleware.ts. Returns 404 (not 403) on denial to avoid project-unit enumeration, and stashes the resolved project on the context.
  • Applied to the three /project-units/{projectUnitId}/usfm* routes (books, sync export, async export). /jobs and /downloads are async-only — owner-binding is tracked in Async USFM export pipeline hardening (pre-enablement) #196.
  • Each route's 404 is documented as { message } (createMessageObjectSchema), matching what the middleware/global handler returns.
  • Sync handler reads the project name from the context instead of re-querying via getProjectName, removing a redundant query and a duplicate { error } 404.

Behaviour

Caller Before After
Member / same-org manager export export
Logged-in user, different org export 404
Unauthenticated 401 (from #189) 401

Testing note

Mirrors the existing record-level auth middlewares (translated-verse-auth, project-auth), which aren't unit-tested in this repo, so no new test file — the logic is identical to the proven translated-verses read path.

  • npm run typecheck
  • npm run lint
  • npm run test — 80/80 pass

Summary by CodeRabbit

  • Refactor
    • Enhanced access control verification for USFM export operations
    • Streamlined project data retrieval for export endpoints
    • Standardized error response messages for unavailable projects across USFM endpoints

After #189 the USFM routes required authentication but no project/org
check, so any logged-in user could export another organization's
translated content by guessing a projectUnitId.

- Add requireProjectUnitAccess middleware: resolves projectUnitId ->
  project -> org and applies ProjectPolicy.read, mirroring
  translated-verse-auth.middleware.ts. Returns 404 (not 403) on denial to
  avoid enumeration, and stashes the resolved project on the context.
- Apply it to the three /project-units/{projectUnitId}/usfm* routes (books,
  sync export, async export). /jobs and /downloads are async-only; binding
  them to an owner is tracked in #196.
- Document each route's 404 as { message } (createMessageObjectSchema),
  matching what the middleware/global handler returns.
- Sync handler now reads the project name from the context instead of
  re-querying via getProjectName, removing a redundant query and its
  duplicate { error } 404.

Closes #198.
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

A new requireProjectUnitAccess Hono middleware is added that resolves projectUnitId to its owning project, enforces ProjectPolicy.read, and attaches project and projectAuthContext to the context. It is applied to the three project-scoped USFM routes, and the now-redundant in-handler project name lookup is removed.

Changes

USFM project-scoped authorization

Layer / File(s) Summary
requireProjectUnitAccess middleware
src/domains/usfm/usfm-auth.middleware.ts
New middleware factory that parses projectUnitId, resolves unit → project, applies ProjectPolicy.read, returns 404 on any failure (including auth), and sets project and projectAuthContext on the Hono context before calling next().
Route wiring and handler simplification
src/domains/usfm/usfm.route.ts
Imports and adds requireProjectUnitAccess() to the middleware chain for getExportableBooksRoute, exportProjectUSFMRoute, and exportProjectUSFMAsyncRoute; updates NOT_FOUND OpenAPI response schemas to createMessageObjectSchema('Project not found'); replaces usfmService.getProjectName(projectUnitId) in the sync export handler with c.get('project')!.name.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

  • #198 USFM export: add project-scoped authorization — This PR directly implements the requireProjectUnitAccess middleware and applies it to the three /project-units/{projectUnitId}/usfm* routes as specified in that issue.
  • Security: unauthenticated bibles and USFM API domains #194 — The PR completes the planned follow-up item ("USFM project-scoped authorization") identified in #194, which added basic authenticateUser to USFM routes.

Possibly related PRs

Suggested reviewers

  • kaseywright
  • AnuMonachan
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary change: adding project/org-scoped authorization to USFM export endpoints, which matches the core objective of restricting access based on project membership.
Linked Issues check ✅ Passed The PR fully implements the scope specified in #198: adds requireProjectUnitAccess middleware, applies ProjectPolicy.read checks, mirrors translated-verse-auth.middleware.ts pattern, and applies to three specified routes.
Out of Scope Changes check ✅ Passed All changes are directly aligned with #198 scope: middleware creation, route application, schema updates, and context-based optimization. No unrelated changes detected.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/198-usfm-project-scoping

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

@henrique221

Copy link
Copy Markdown
Contributor Author

@CodeRabbit full review

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@henrique221 henrique221 self-assigned this Jun 23, 2026
@henrique221
henrique221 marked this pull request as draft June 23, 2026 18:12
@henrique221

Copy link
Copy Markdown
Contributor Author

⚠️ Merge order / base note

This PR is stacked on #189 — its base is fix/require-auth-bibles-usfm, not main, because the project-scoping builds on the authenticateUser middleware #189 adds to the USFM routes. It can't target main cleanly until #189 lands.

Do not merge before #189 — doing so merges #198 into the #189 branch and bundles the two separately-reviewed changes.

Since this repo squash-merges and #200 has already landed on main touching the same file, the correct flow is:

  1. Merge fix(auth): require authentication on bibles and USFM routes #189main.
  2. Rebase this branch onto main and retarget this PR's base to main.
  3. Merge as an independent PR.

Marked as draft until #189 merges; the rebase + retarget will be handled then. The diff itself (the new requireProjectUnitAccess middleware + route wiring) is ready to review now.

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

🤖 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 `@src/domains/usfm/usfm-auth.middleware.ts`:
- Around line 45-53: The resolveIsProjectMember call is being made
unconditionally, but the ProjectPolicy.read check only uses the isProjectMember
value for TRANSLATOR roles and ignores it for PROJECT_MANAGER roles. To optimize
the hot export path, add a conditional check on user.roleName before calling
resolveIsProjectMember. Only invoke resolveIsProjectMember when the user role is
TRANSLATOR, and for PROJECT_MANAGER roles, skip the database call by either
passing a default value or using a ternary operator to conditionally set
isProjectMember based on the role.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 7b92048d-6981-4ef1-93a8-0f94693d04d7

📥 Commits

Reviewing files that changed from the base of the PR and between 3af9090 and bbbc7dc.

📒 Files selected for processing (2)
  • src/domains/usfm/usfm-auth.middleware.ts
  • src/domains/usfm/usfm.route.ts

Comment on lines +45 to +53
const isProjectMember = await resolveIsProjectMember(
unitResult.data.projectId,
user.id,
user.roleName
);

if (!ProjectPolicy.read(policyUser, projectResult.data, isProjectMember)) {
return c.json({ message: 'Project not found' }, HttpStatusCodes.NOT_FOUND);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find any consumers of projectAuthContext / isProjectMember downstream
rg -nP "projectAuthContext|isProjectMember" -C3 --type=ts -g '!**/*.test.ts'

Repository: eten-tech-foundation/fluent-api

Length of output: 169


🏁 Script executed:

#!/bin/bash
# Search without -P flag for simpler regex
rg "projectAuthContext" --type=ts -g '!**/*.test.ts' -n

Repository: eten-tech-foundation/fluent-api

Length of output: 169


🏁 Script executed:

#!/bin/bash
# Search for isProjectMember usage
rg "isProjectMember" --type=ts -g '!**/*.test.ts' -n

Repository: eten-tech-foundation/fluent-api

Length of output: 169


🏁 Script executed:

#!/bin/bash
# Check the actual middleware file mentioned in the review
cat -n src/domains/usfm/usfm-auth.middleware.ts

Repository: eten-tech-foundation/fluent-api

Length of output: 2808


🏁 Script executed:

#!/bin/bash
# Find and inspect ProjectPolicy implementation
fd -t f "ProjectPolicy" -e ts | head -20

Repository: eten-tech-foundation/fluent-api

Length of output: 169


🏁 Script executed:

#!/bin/bash
# Search for projectAuthContext without glob exclusion
rg "projectAuthContext" --type=ts -n -A 2

Repository: eten-tech-foundation/fluent-api

Length of output: 169


🏁 Script executed:

#!/bin/bash
# Search for isProjectMember without glob exclusion
rg "isProjectMember" --type=ts -n -A 2

Repository: eten-tech-foundation/fluent-api

Length of output: 169


🏁 Script executed:

#!/bin/bash
# Find and inspect ProjectPolicy implementation
fd -t f -e ts | xargs grep -l "class ProjectPolicy\|export.*ProjectPolicy" | head -5

Repository: eten-tech-foundation/fluent-api

Length of output: 208


🏁 Script executed:

#!/bin/bash
# Look for the project policy file directly
fd "project.policy" -e ts

Repository: eten-tech-foundation/fluent-api

Length of output: 208


🏁 Script executed:

#!/bin/bash
# Read the ProjectPolicy file
cat -n src/domains/projects/project.policy.ts

Repository: eten-tech-foundation/fluent-api

Length of output: 2997


🏁 Script executed:

#!/bin/bash
# Try grep instead of rg for finding downstream usage
grep -rn "projectAuthContext" src --include="*.ts" | head -20

Repository: eten-tech-foundation/fluent-api

Length of output: 711


🏁 Script executed:

#!/bin/bash
# Search for handlers that might use the middleware
grep -rn "requireProjectUnitAccess\|projectAuthContext" src --include="*.ts" | head -30

Repository: eten-tech-foundation/fluent-api

Length of output: 1367


🏁 Script executed:

#!/bin/bash
# Search for where handlers actually access/read projectAuthContext
grep -rn "\.get.*projectAuthContext\|get('projectAuthContext')" src --include="*.ts"

Repository: eten-tech-foundation/fluent-api

Length of output: 169


🏁 Script executed:

#!/bin/bash
# Check usfm.route.ts handlers to see if they consume projectAuthContext
cat -n src/domains/usfm/usfm.route.ts

Repository: eten-tech-foundation/fluent-api

Length of output: 18351


🏁 Script executed:

#!/bin/bash
# Check the other middleware files that set projectAuthContext to see if they're used
grep -rn "\.get('projectAuthContext')\|\.get(\"projectAuthContext\")" src --include="*.ts" -B 2 -A 2

Repository: eten-tech-foundation/fluent-api

Length of output: 169


🏁 Script executed:

#!/bin/bash
# Check if there are any queue workers or async handlers that might use projectAuthContext
grep -rn "projectAuthContext" src/lib/queue src/jobs src/workers --include="*.ts" 2>/dev/null || echo "No matches in queue/jobs/workers"

Repository: eten-tech-foundation/fluent-api

Length of output: 202


Short-circuit resolveIsProjectMember for project managers to avoid unnecessary database calls.

ProjectPolicy.read only examines organization for PROJECT_MANAGER and uses isAssignedToProject only for TRANSLATOR. Since managers are likely the primary exporters, this adds an unneeded database round-trip on the hot export path.

The value is stored in projectAuthContext but is never read by any downstream handler, making the optimization straightforward: check the user role before calling resolveIsProjectMember.

🤖 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 `@src/domains/usfm/usfm-auth.middleware.ts` around lines 45 - 53, The
resolveIsProjectMember call is being made unconditionally, but the
ProjectPolicy.read check only uses the isProjectMember value for TRANSLATOR
roles and ignores it for PROJECT_MANAGER roles. To optimize the hot export path,
add a conditional check on user.roleName before calling resolveIsProjectMember.
Only invoke resolveIsProjectMember when the user role is TRANSLATOR, and for
PROJECT_MANAGER roles, skip the database call by either passing a default value
or using a ternary operator to conditionally set isProjectMember based on the
role.

@henrique221 henrique221 added the bug Something isn't working label Jun 25, 2026
@github-actions
github-actions Bot deleted the branch fix/require-auth-bibles-usfm June 26, 2026 21:04
@github-actions github-actions Bot closed this Jun 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant