feat(forge): show inline images on GitLab #149

Merged
deadstyle merged 9 commits from feature/143-gitlab-images into develop 2026-10-12 09:40:06 -05:00
Owner

Refs #143 (parent #137)

Summary

Images uploaded to GitLab MRs and issues now draw inline in the Details pane, on public and private projects, as on Forgejo. Images that fail to load or live on another host stay the 🖼 alt link. The GitLab token never leaves the GitLab host on any request.

Decisions

  • Core resolved GitLab's /uploads/<secret>/<file> links against the host root, losing the project. New optional, I/O-free forge.AssetResolver lets an adapter resolve a body link for its repo before core's default; core still applies its credential refusal and same-origin gate to the result. The GitLab resolver handles only the two upload forms; every other link takes core's path.
  • OpenAsset rewrites an on-origin upload URL to GET /api/v4/projects/:id/uploads/:secret/:filename (GitLab 17.4+; older servers 404, so the link stays) and refuses off-origin URLs before any request. Links with ., .. or empty secret/filename segments are refused, so a body someone else wrote can't steer an authenticated request elsewhere on the host.
  • Security fix, client-wide: Go forwards custom headers such as PRIVATE-TOKEN on every redirect hop, including to other hosts. The adapter's HTTP client now drops PRIVATE-TOKEN and Authorization on any hop off the GitLab origin, for every call (API, job logs, images), replacing #139's job-log-only guard. Recorded in ADR 0018.

Test plan

  • go test -race ./...: 15/15 packages ok. Build, govulncheck, install-test and windows vet/build pass.
  • Lint and fmt-check with CI's golangci-lint v2.14.0: 0 issues.
  • Tests written red-first from the AC; 22 mutations, all caught (one hollow test rewritten). Redirect tests prove the token is absent on an off-origin hop for API calls, job logs and images, including a scheme change.

🤖 Generated with Claude Code

Refs #143 (parent #137) ## Summary Images uploaded to GitLab MRs and issues now draw inline in the Details pane, on public and private projects, as on Forgejo. Images that fail to load or live on another host stay the `🖼 alt` link. The GitLab token never leaves the GitLab host on any request. ## Decisions - Core resolved GitLab's `/uploads/<secret>/<file>` links against the host root, losing the project. New optional, I/O-free `forge.AssetResolver` lets an adapter resolve a body link for its repo before core's default; core still applies its credential refusal and same-origin gate to the result. The GitLab resolver handles only the two upload forms; every other link takes core's path. - `OpenAsset` rewrites an on-origin upload URL to `GET /api/v4/projects/:id/uploads/:secret/:filename` (GitLab 17.4+; older servers 404, so the link stays) and refuses off-origin URLs before any request. Links with `.`, `..` or empty secret/filename segments are refused, so a body someone else wrote can't steer an authenticated request elsewhere on the host. - **Security fix, client-wide:** Go forwards custom headers such as `PRIVATE-TOKEN` on every redirect hop, including to other hosts. The adapter's HTTP client now drops `PRIVATE-TOKEN` and `Authorization` on any hop off the GitLab origin, for every call (API, job logs, images), replacing #139's job-log-only guard. Recorded in ADR 0018. ## Test plan - `go test -race ./...`: 15/15 packages ok. Build, govulncheck, install-test and windows vet/build pass. - Lint and fmt-check with CI's golangci-lint v2.14.0: 0 issues. - Tests written red-first from the AC; 22 mutations, all caught (one hollow test rewritten). Redirect tests prove the token is absent on an off-origin hop for API calls, job logs and images, including a scheme change. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
newForge wraps the session client with a CheckRedirect that drops
PRIVATE-TOKEN and Authorization on any hop off the web origin, replacing
the JobLog-only guard.

Refs #143

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds forge.AssetResolver so core resolves GitLab's project-relative
/uploads/ links per project, and gitlab.OpenAsset, which fetches uploads
through /api/v4/projects/:id/uploads/:secret/:filename.

Refs #143

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #143

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #143

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The other-scheme redirect case failed the TLS handshake before the hop
server saw a request, so the forge now uses the TLS server's client.
Image always fetches and only PeekImage reads the cache, so the
per-project cache test checks both entries by peeking.

Refs #143

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #143

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A secret or filename of . or .. (raw or percent-encoded) or empty let a
link in someone else's body steer an authenticated GET to another API
path. ResolveAsset now leaves such links to the caller and OpenAsset
refuses them without a request.

Refs #143

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #143

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test(gitlab): make the transport-error leak check reach a URL with a query
All checks were successful
ci / check (pull_request) Successful in 1m9s
0d38b07f14
Refs #143

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
deadstyle merged commit 6726d5bdcc into develop 2026-10-12 09:40:06 -05:00
deadstyle deleted branch feature/143-gitlab-images 2026-10-12 09:40:06 -05:00
Sign in to join this conversation.
No description provided.