Repository navigation
fix(uploads): verify processor file hash - #1219
Conversation
…gets Page targets delegate to the existing createUploadServiceToken with parentId === pageId to preserve channel-route permission behavior byte-for-byte. Conversation targets validate participant1 OR participant2 of the DM, then mint a session bound to resourceType: 'conversation' with no driveId — DM files have no drive. This is the first deliverable of PR 3 (epic item 7). Subsequent commits add the processAttachmentUpload pipeline, generalize the processor upload route, and rewrite the channel route as a thin wrapper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d DMs Owns formData parse, memory check, quota, semaphore acquire/release, token mint, processor forward, file row insert, target-specific linkage (filePages or fileConversations), storage usage, audit, and activity log. Returns the same JSON shape across page and conversation targets so the client uploader does not have to branch. Persistence is isolated behind an attachment-upload-repository seam so route tests can assert payloads without touching ORM chains (per unit-test-rubric §4). logFileActivity now accepts driveId: string | null since DM uploads have no drive — the underlying logActivity already accepted nullable driveId. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ngle upload Generalize the resource-binding gate to allow conversation tokens (DM uploads). Conversation tokens have no driveId — DM files live outside any drive — so they must reject any driveId in the request body (defense-in-depth against forged or misrouted tokens). Conversation uploads do not have a Page row, so the ingest-file worker (which calls setPageProcessing(fileId)) is not queued for them. Image optimization for DM uploads is a follow-up worth flagging once the rest of the DM surface lands. Existing channel upload happy path is unchanged. The error string for a missing binding broadens from "page resource binding" to "valid resource binding" because both kinds are now accepted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ssAttachmentUpload The channel-upload pipeline (memory check, quota, semaphore, token mint, processor forward, file insert, linkage, audit, activity log) now lives in @pagespace/lib/services/attachment-upload so it is shared with DM uploads in PR 4. The route keeps only the channel-specific concerns: page-type validation and canUserEditPage permission gate. Ends up around 60 lines (down from 266). Public response shape is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three behavior fixes for the channel upload route after the PR 3 thin-wrapper
rewrite:
1. Wrap auth + page lookup + permission check in try/catch so unexpected
failures (DB outage, etc.) return the structured `{ error }` JSON contract
instead of bubbling as Next.js framework HTML errors. Restores the
error-response shape the previous route guaranteed.
2. Emit `auditRequest({ eventType: 'authz.access.denied', ... })` on the
403 permission-denied path. Gives SIEM the canonical signal (matches the
pattern audit-coverage gate enforces) and keeps the static
security-audit-coverage.test.ts scan finding the `auditRequest(` literal
in the route file.
3. Restore byte-for-byte parity with master in the attachment-upload pipeline:
- drop the new `pageId` field from the `updateStorageUsage` storage event
- second `auditRequest` `resourceId` reverts to pageId/conversationId
(was contentHash); drop the synthetic `targetType` detail.
Test deltas:
- route.test.ts: assert authz.access.denied audit on 403; new test for the
500 JSON contract when the page lookup throws.
- process-attachment-upload.test.ts: unchanged — assertions did not depend
on the dropped fields.
Closes Codex P2 review thread (wrapper-stage error handling) and the
security-audit-coverage CI failure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Require processAttachmentUpload callers to pass a validated EnforcedAuthContext instead of a raw user id. Recompute SHA-256 and validate processor-reported hash and size before persisting file metadata, linkages, or storage accounting. Document DM attachment scanning depth and cross-context content-addressed dedup assumptions.
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThis PR implements file upload integrity validation by computing SHA-256 hashes of received bytes, validating processor responses against those hashes before persisting files, and refactoring the upload pipeline to use enforced authentication contexts instead of raw user IDs. Marketing documentation, test coverage, and task specifications are updated accordingly. ChangesFile Upload Integrity & Auth Context
Sequence DiagramsequenceDiagram
actor Client
participant Route as Channel Upload<br/>Route
participant Service as Upload<br/>Service
participant Processor as External<br/>Processor
participant Storage as File<br/>Storage
Client->>Route: POST with file + auth
Route->>Route: authenticateWithEnforcedContext
Route->>Service: processAttachmentUpload(request, authContext)
Service->>Service: computeFileSha256(file)<br/>→ expectedHash
Service->>Processor: POST file bytes
Processor-->>Service: { contentHash, size, ... }
alt Processor Response Valid
Service->>Service: validateProcessorResult<br/>(response, expected)
Service->>Storage: persist file
Service->>Storage: link to target
Service-->>Route: { fileId, contentHash, ... }
Route-->>Client: 200 OK
else Hash or Size Mismatch
Service->>Service: release semaphore slot
Service->>Route: 502 error<br/>(integrity failure)
Route-->>Client: 502 Bad Gateway
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Review rate limit: 0/1 reviews remaining, refill in 32 minutes and 8 seconds.Comment |
Update the DM upload route added on master to pass the validated auth context into processAttachmentUpload. Keep the wrapper tests aligned with the hardened shared upload API.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
apps/web/src/app/api/channels/[pageId]/upload/__tests__/route.test.ts (1)
63-65: ⚡ Quick winUse a real
EnforcedAuthContextin test fixtures to prevent contract drift.The current plain-object
ctxkeeps tests passing even if the route/service contract later relies on actualEnforcedAuthContextsemantics beyonduserId.Suggested test-fixture tightening
+import { EnforcedAuthContext } from '@pagespace/lib/permissions/enforced-context'; +import type { SessionClaims } from '@pagespace/lib/auth/session-service'; ... function makeAuthSuccess(userId = 'user-1') { - return { ctx: { userId } }; + const claims: SessionClaims = { + sessionId: `session-${userId}`, + userId, + userRole: 'user', + tokenVersion: 1, + adminRoleVersion: 1, + type: 'user', + scopes: [], + expiresAt: new Date(Date.now() + 60_000), + }; + return { ctx: EnforcedAuthContext.fromSession(claims) }; } ... - authContext: { userId: 'user-1' }, + authContext: expect.objectContaining({ userId: 'user-1' }),Also applies to: 94-95
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/app/api/channels/`[pageId]/upload/__tests__/route.test.ts around lines 63 - 65, Replace the plain-object fixture returned by makeAuthSuccess with a real EnforcedAuthContext instance to avoid contract drift: import or construct the EnforcedAuthContext type/object used by the route (e.g., the EnforcedAuthContext exported from your auth module) and have makeAuthSuccess return { ctx: <EnforcedAuthContext with userId set> } (or call the helper that builds one) instead of a raw object; also update the other test fixtures in this file that mirror lines 94–95 to use the same EnforcedAuthContext-based construction so tests exercise the real auth contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@tasks/dm-file-attachments.md`:
- Line 52: The docs still show processAttachmentUpload({ request, target }) but
the implementation now requires a validated auth context; update the earlier
function signature text to reflect this by changing the documented signature to
processAttachmentUpload({ request, target, authContext }) and note that
authContext must be an enforced/validated auth context (not a raw
caller-provided userId); update any nearby explanatory text to state that
authContext is required and validated before calling processAttachmentUpload
(reference: processAttachmentUpload).
---
Nitpick comments:
In `@apps/web/src/app/api/channels/`[pageId]/upload/__tests__/route.test.ts:
- Around line 63-65: Replace the plain-object fixture returned by
makeAuthSuccess with a real EnforcedAuthContext instance to avoid contract
drift: import or construct the EnforcedAuthContext type/object used by the route
(e.g., the EnforcedAuthContext exported from your auth module) and have
makeAuthSuccess return { ctx: <EnforcedAuthContext with userId set> } (or call
the helper that builds one) instead of a raw object; also update the other test
fixtures in this file that mirror lines 94–95 to use the same
EnforcedAuthContext-based construction so tests exercise the real auth contract.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: ceba5870-bacf-4743-9e99-9e34705ed7cc
📒 Files selected for processing (6)
apps/marketing/src/app/docs/security/zero-trust/page.tsxapps/web/src/app/api/channels/[pageId]/upload/__tests__/route.test.tsapps/web/src/app/api/channels/[pageId]/upload/route.tspackages/lib/src/services/__tests__/process-attachment-upload.test.tspackages/lib/src/services/attachment-upload.tstasks/dm-file-attachments.md
Update the DM attachment task signature to require authContext and construct EnforcedAuthContext instances in upload wrapper tests.
* feat(lib): add createAttachmentUploadServiceToken for polymorphic targets
Page targets delegate to the existing createUploadServiceToken with parentId === pageId
to preserve channel-route permission behavior byte-for-byte. Conversation targets
validate participant1 OR participant2 of the DM, then mint a session bound to
resourceType: 'conversation' with no driveId — DM files have no drive.
This is the first deliverable of PR 3 (epic item 7). Subsequent commits add the
processAttachmentUpload pipeline, generalize the processor upload route, and
rewrite the channel route as a thin wrapper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(lib): add processAttachmentUpload pipeline shared by channels and DMs
Owns formData parse, memory check, quota, semaphore acquire/release, token mint,
processor forward, file row insert, target-specific linkage (filePages or
fileConversations), storage usage, audit, and activity log. Returns the same
JSON shape across page and conversation targets so the client uploader does not
have to branch.
Persistence is isolated behind an attachment-upload-repository seam so route
tests can assert payloads without touching ORM chains (per unit-test-rubric §4).
logFileActivity now accepts driveId: string | null since DM uploads have no
drive — the underlying logActivity already accepted nullable driveId.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(processor): accept page or conversation resource bindings on /single upload
Generalize the resource-binding gate to allow conversation tokens (DM uploads).
Conversation tokens have no driveId — DM files live outside any drive — so they
must reject any driveId in the request body (defense-in-depth against forged
or misrouted tokens).
Conversation uploads do not have a Page row, so the ingest-file worker (which
calls setPageProcessing(fileId)) is not queued for them. Image optimization for
DM uploads is a follow-up worth flagging once the rest of the DM surface lands.
Existing channel upload happy path is unchanged. The error string for a missing
binding broadens from "page resource binding" to "valid resource binding"
because both kinds are now accepted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(channels): rewrite upload route as a thin wrapper over processAttachmentUpload
The channel-upload pipeline (memory check, quota, semaphore, token mint, processor
forward, file insert, linkage, audit, activity log) now lives in
@pagespace/lib/services/attachment-upload so it is shared with DM uploads in PR 4.
The route keeps only the channel-specific concerns: page-type validation and
canUserEditPage permission gate.
Ends up around 60 lines (down from 266). Public response shape is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(channels-upload): wrapper-stage try/catch + audit signal parity
Three behavior fixes for the channel upload route after the PR 3 thin-wrapper
rewrite:
1. Wrap auth + page lookup + permission check in try/catch so unexpected
failures (DB outage, etc.) return the structured `{ error }` JSON contract
instead of bubbling as Next.js framework HTML errors. Restores the
error-response shape the previous route guaranteed.
2. Emit `auditRequest({ eventType: 'authz.access.denied', ... })` on the
403 permission-denied path. Gives SIEM the canonical signal (matches the
pattern audit-coverage gate enforces) and keeps the static
security-audit-coverage.test.ts scan finding the `auditRequest(` literal
in the route file.
3. Restore byte-for-byte parity with master in the attachment-upload pipeline:
- drop the new `pageId` field from the `updateStorageUsage` storage event
- second `auditRequest` `resourceId` reverts to pageId/conversationId
(was contentHash); drop the synthetic `targetType` detail.
Test deltas:
- route.test.ts: assert authz.access.denied audit on 403; new test for the
500 JSON contract when the page lookup throws.
- process-attachment-upload.test.ts: unchanged — assertions did not depend
on the dropped fields.
Closes Codex P2 review thread (wrapper-stage error handling) and the
security-audit-coverage CI failure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(uploads): verify processor file hash
Require processAttachmentUpload callers to pass a validated EnforcedAuthContext instead of a raw user id.
Recompute SHA-256 and validate processor-reported hash and size before persisting file metadata, linkages, or storage accounting.
Document DM attachment scanning depth and cross-context content-addressed dedup assumptions.
* fix(dm): use enforced upload auth context
Update the DM upload route added on master to pass the validated auth context into processAttachmentUpload.
Keep the wrapper tests aligned with the hardened shared upload API.
* fix(uploads): align auth context docs tests
Update the DM attachment task signature to require authContext and construct EnforcedAuthContext instances in upload wrapper tests.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
EnforcedAuthContextinstead of a raw user idcontentHashorsizebefore persistence/linkage/accountingmasterRoot Cause
processAttachmentUploadpreviously trusted the processor-returnedcontentHashwhen writing thefilesrow and target linkage. A compromised processor could return another tenant's existing content hash and cause the uploader to receive a fresh authorized linkage to unrelated data.Validation
pnpm ipnpm --filter @pagespace/lib buildpnpm --filter web exec vitest run 'src/app/api/messages/[conversationId]/upload/__tests__/route.test.ts'pnpm --filter web exec vitest run 'src/app/api/channels/[pageId]/upload/__tests__/route.test.ts'pnpm --filter web typecheckpnpm --filter @pagespace/lib exec vitest run src/services/__tests__/process-attachment-upload.test.tspnpm --filter web lint(passes with existing unrelatedQuickCreatePalette.tsxhook dependency warning)git diff --check