Add programmatic cross-origin API for external web apps#409
Open
krokicki wants to merge 6 commits into
Open
Conversation
Lets another same-site web app (e.g. ai-cryoet.int.janelia.org) read and
write files through Fileglancer as the logged-in user, with no login of its
own. The SameSite=Lax session cookie is attached automatically on same-site
cross-subdomain requests, so the cookie is the API credential; a popup/iframe
handshake handles the login-when-needed case.
Backend:
- api_allowed_origins setting: the cross-site boundary. get_current_user now
rejects (403) any Origin that is neither same-origin nor allowlisted, closing
a latent hole where the wildcard CORS + Lax cookie let any same-site page ride
a logged-in session. Anonymous /files/ data links are unaffected.
- GET /api/auth/allowed-origins so the connect popup can self-check its origin.
- PUT /api/content/{fsp}?subpath=... streams the request body to disk as the
user (new write_file worker action + _fd_mode so the passed fd is writable).
Works under NFS root squash: the fd is opened by the setuid'd worker, so
permission is checked at open() time, not on the parent's writes.
Frontend:
- /connect-complete bare route: posts { authenticated, username } to the
requesting origin (validated against the allowlist) via postMessage, then
closes (popup) or reports back (silent iframe) so the SDK can escalate.
Client:
- clients/js @fileglancer/client: credentialed requests, popup/iframe connect
handshake, retry-on-401, typed list/read/write/mkdir/rename/delete.
Docs & tests:
- docs/ProgrammaticAPI.md, config template entry, SDK README.
- Unit + integration tests for origin enforcement; write-endpoint tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GET/HEAD /api/content now emit a strong ETag (mtime+size) and Last-Modified.
PUT /api/content honors If-Match (incl. "*") and If-Unmodified-Since,
returning 412 without altering the file on mismatch. The write_file worker
opens write-only WITHOUT truncate, validates the precondition against the
real fd, and ftruncates only once it passes — so a failed check leaves the
file untouched and the check is race-tight rather than check-then-clobber.
- make_etag / parse_http_date_to_epoch helpers (utils).
- open_file returns last_modified so GET can build the ETag consistently
with HEAD and the PUT precondition check (all from the same stat fields).
- SDK: writeFile(..., {ifMatch, ifUnmodifiedSince}) + ConflictError (412);
reads already expose the ETag on the returned Response.
- Docs: concurrency sections in ProgrammaticAPI.md and the SDK README.
ETag ceiling: mtime+size, so two same-size writes within the filesystem's
mtime resolution can share an ETag — fine for interactive save-conflict
detection, not locking.
Also drops 3 stray assertions in test_put_file_content_path_traversal_blocked
that only passed because the traversal error text contains "directory".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Guards the streaming write path against an unbounded upload filling the disk. Two layers: reject up front when a declared Content-Length exceeds the limit (so the file is never opened/truncated for an obviously-too-large upload), and re-check byte count inside the streaming loop for chunked or dishonest clients, raising 413 and terminating. Default limit 50 GiB; 0 disables. Cleanup simplified to a single finally (closes the handle on success, 413, disconnect, or write error); real write errors now flow through the app's global exception handler instead of a local logger.exception. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Remove Help & Documentation box, center title and login box, update subtitle. Hide navbar chrome and halve spacing when login is shown in the external-app connect popup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this adds
A programmatic HTTP API that lets another same-site web app (e.g.
ai-cryoet.int.janelia.org) read and write files through Fileglancer as the logged-in user, without implementing its own login. When an operation needs authentication, a small Fileglancer popup runs the normal Okta/simple login and closes itself; if the user already has a session it's instant (or invisible via a hidden iframe).The motivating flow: a user clicks "Save" in the other app → the app calls Fileglancer's API → if there's no session yet, a login popup appears → the file is written.
How it works
Full write-up is in
docs/ProgrammaticAPI.md(architecture, config, endpoint list, connect flow, optimistic concurrency, security) andclients/js/README.md(SDK usage). The short version:Because the apps share a registrable domain (
janelia.org), the browser attaches Fileglancer'sSameSite=Laxsession cookie to cross-subdomain requests automatically — so the cookie is the API credential and the other app handles no tokens. Access is gated server-side by an explicit origin allowlist rather than by the cookie alone.What's included
Backend
api_allowed_originssetting — the cross-site boundary.get_current_usernow rejects (403) any request whoseOriginis neither same-origin nor allowlisted. This also closes a latent exposure where wildcard CORS + theSameSite=Laxcookie let any*.janelia.orgpage ride a logged-in session; the anonymous/files/{sharing_key}data links are unaffected.GET /api/auth/allowed-originsso the connect popup can self-check its origin.PUT /api/content/{fsp}?subpath=...streams the request body to disk as the user (newwrite_fileworker action + an_fd_modeflag so the passed fd is writable). Works under NFS root squash because the fd is opened by the setuid'd worker, so permission is checked atopen(), not on the parent's writes. This is the first content-write path in the API — previously only empty files could be created.GET/HEAD /api/contentemit a strongETag(from mtime+size) andLast-Modified;PUThonorsIf-Match(incl.*) andIf-Unmodified-Since, returning412without altering the file on mismatch (the worker validates against the opened fd before truncating, so it's race-tight).max_upload_size_bytes(default 50 GiB,0disables) — rejects up front on an over-limitContent-Lengthand re-checks byte count mid-stream for chunked clients, returning413.Frontend
/connect-completebare route that posts{ authenticated, username }to the requesting origin (validated against the allowlist) viapostMessage, then closes (popup) or reports back (silent hidden iframe) so the client escalates to a visible popup only when login is actually required.Client SDK
clients/js—@fileglancer/client: credentialed requests, the popup/iframe connect handshake, retry-on-401, typedlist/read/write/mkdir/rename/delete,writeFile(..., { ifMatch, ifUnmodifiedSince })for optimistic concurrency, andAuthRequiredError/ForbiddenError/ConflictError.Docs & tests
docs/ProgrammaticAPI.md, config-template entries, and the SDK README.tests/test_api_auth.py); write, optimistic-concurrency, and upload-limit tests (tests/test_endpoints.py).Deployment / config
Same-origin (the Fileglancer UI) is always allowed and need not be listed.
Testing
pixi run -e test test-backend— 754 passed.pixi run node-checkclean for the new files; ESLint/Prettier applied.tsc --noEmitclean.Not included / follow-ups
clients/jsthat typechecks on its own; consuming apps build it locally or vendorsrc/index.tsfor now.SameSite=Laxand the wildcard CORS are intentionally unchanged — the approach depends on same-site deployment. If an integrator ever lands offjanelia.org, the SDK's privaterequest()method is the single seam where a bearer-token transport would slot in.@StephanPreibisch @JaneliaSciComp/fileglancer