fix(files): allow downloading issue attachments - #256
Conversation
The file-serving endpoint only accepted object paths under "uploads/", but attachments are stored as "attachments/<issueId>/<assetId>", so every attachment download was rejected with 400 before any lookup — a core feature was completely broken. ServeFile now serves both "uploads/" and "attachments/" prefixes (still rejecting empty paths and path traversal). The prefix check is extracted into a small isServableObjectPath helper with unit tests. Closes #135 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Strix Security ReviewAll previously reported security findings have been resolved. 1 resolved finding
Review summaryRe-review of the latest attachment download changes. I verified the new attachment authorization path in Updated for Reviewed by Strix |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAttachment file serving now accepts ChangesAttachment download path and authorization flow
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant ServeFile
participant AttachmentService
Client->>ServeFile: GET /api/files/attachments/<issueId>/<assetId>
ServeFile->>ServeFile: validate object path
ServeFile->>ServeFile: parse attachment IDs
ServeFile->>AttachmentService: AuthorizeDownload(issueID, assetID, userID)
AttachmentService-->>ServeFile: allowed / not found / forbidden
ServeFile-->>Client: stream file or return 404/503
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.12.2)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" Comment |
There was a problem hiding this comment.
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 `@apps/api/internal/handler/upload.go`:
- Around line 115-124: The ServeFile flow in UploadHandler only validates the
MinIO path and then streams the object, so attachment URLs can bypass
issue/workspace authorization. Update ServeFile to detect attachment paths,
resolve the attachment record first, and enforce the same access checks used by
the attachment APIs before calling MinIO. Use the existing UploadHandler,
ServeFile, and attachment lookup/authorization helpers to locate the record and
verify the caller is allowed to access that issue/project.
🪄 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 Plus
Run ID: eebd55f6-17bf-4d6d-8581-53f97cbd8936
📒 Files selected for processing (2)
apps/api/internal/handler/upload.goapps/api/internal/handler/upload_path_test.go
Addresses the Strix/CodeRabbit finding on PR #256: serving the attachments/ prefix meant any signed-in user with a leaked object URL could fetch the file, bypassing the issue/workspace authorization the attachment APIs enforce. ServeFile now resolves the attachment record from the object path and requires the caller to be a member of the attachment's workspace before streaming it; missing and forbidden both return 404 so attachment existence isn't leaked. The uploads/ prefix (avatars/covers/logos) is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
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 `@apps/api/internal/service/attachment.go`:
- Around line 92-98: In AuthorizeDownload, stop collapsing all failures from
GetAttachmentByAssetID and IsMember into ErrAttachmentNotFound or
ErrProjectForbidden. Update the attachment lookup and membership check handling
so only the genuine nil/not-found and false/not-member cases return those domain
errors, while any non-nil datastore error is returned directly to the caller to
flow into the 5xx path. Keep the fix localized around
s.is.GetAttachmentByAssetID and s.ws.IsMember.
🪄 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 Plus
Run ID: a2645d0a-5aae-49df-86a1-18363541f0c6
📒 Files selected for processing (5)
apps/api/internal/handler/attachment_authz_test.goapps/api/internal/handler/upload.goapps/api/internal/handler/upload_path_test.goapps/api/internal/router/router.goapps/api/internal/service/attachment.go
CodeRabbit follow-up: AuthorizeDownload collapsed unexpected store errors into not-found/forbidden, hiding real failures behind 404/403. It now returns ErrAttachmentNotFound only for a genuinely missing record and propagates any other datastore error, which ServeFile maps to 500 (missing/forbidden stay 404). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What
Closes #135. Issue/epic attachments could never be downloaded. The file-serving endpoint only accepted object paths under
uploads/, but the attachment flow stores objects asattachments/<issueId>/<assetId>and hands the UI URLs like/api/files/attachments/.... Every attachment download was rejected with 400 before any lookup — a High-severity break of a core feature (uploads appeared to succeed but the file was unretrievable).How
ServeFilenow serves both theuploads/(avatars/covers/logos) andattachments/(issue attachments) prefixes. Empty paths and path traversal (..) are still rejected. The check is pulled into a smallisServableObjectPathhelper so it is unit-testable without MinIO. The route stays behindRequireAuth, so the auth model is unchanged.Testing
New
internal/handler/upload_path_test.gounit-testsisServableObjectPath:uploads/andattachments/paths pass; empty, traversal, and other prefixes are rejected.Verified against a running server (MinIO up, authenticated request):
attachments/<uuid>/<uuid>uploads/<...>secrets/keyattachments/../secrets/keyThe 400 -> 404 flip on the attachments path is the exact fix; disallowed prefixes and traversal still 400.
AI assistance
Produced with the help of Claude Code (Claude Opus 4.8). AI-assisted commits carry a
Co-Authored-Bytrailer.Summary by CodeRabbit
uploads/andattachments/, blocking empty and path-traversal requests.