Uh oh!
There was an error while loading. Please reload this page.
[Feature] : API ENDPOINTS PR 9: Files, Uploads and Acc Management endpoints - #1170
Open
pulk17 wants to merge 13 commits into
Open
[Feature] : API ENDPOINTS PR 9: Files, Uploads and Acc Management endpoints#1170pulk17 wants to merge 13 commits into
pulk17 wants to merge 13 commits into
Conversation
pulk17
requested review from
canihavesomecoffee and thealphadollar
as code ownersAugust 11, 2026 12:00
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 freeto 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.



[FEATURE]
In raising this pull request, I confirm the following (please check boxes):
My familiarity with the project is as follows:
PR 9: Files, Uploads and Account Management
Not stacked. PR 8 (#1160) has merged, so this branches straight off
master.PR 8 closed the gaps in reading and administering the platform. This closes the
last ones: getting files out, getting samples in, and letting somebody
manage their own account. With this merged, a client can drive every part of the
platform a signed-in person can reach on the classic pages.
Thirty operations. One new route module,
routes/uploads.py, for the uploadqueue; everything else extends the module that already owns its prefix.
What this adds
Downloads — the classic site's file links, minus the file bytes
GET/samples/{id}/downloadruns:readGET/samples/{id}/media-info/downloadruns:readGET/samples/{id}/extra-files/{extra_id}/downloadruns:readGET/regression-tests/{id}/outputs/{output_id}/downloadruns:readGET/regression-tests/{id}/outputs/{output_id}/variants/{variant_id}/downloadruns:readThese return a signed URL and a
storage_status, not the file. Samples run togigabytes and the API should not be a transfer proxy for them; this is the shape
PR 7 established for run artifacts. When the only copy is on the platform's own
disk there is no URL to sign, so
download_urlisnulland the status says sorather than the request failing.
Baseline variants — a test can legitimately produce different bytes on a
different platform or CCExtractor build
POST/regression-tests/{id}/outputs/{output_id}/variantsruns:writeDELETE/regression-tests/{id}/outputs/{output_id}/variants/{variant_id}runs:writeRecording a hash here makes those runs pass without overwriting the baseline
everyone else is compared against, which is what promoting to baseline would do.
Sample editing
GET/tagsruns:readPOST/tagsruns:writePATCH/samples/{id}runs:writeDELETE/samples/{id}runs:writeDELETE/samples/{id}/extra-files/{extra_id}runs:writeUploads — the queue a file lands in before it is described
POST/samples/uploadruns:writeGET/queued-samplesruns:readGET/queued-samples/{id}runs:readPOST/queued-samples/{id}/finalizeruns:writePOST/queued-samples/{id}/linkruns:writeDELETE/queued-samples/{id}runs:writeAccounts — self-service, so these are the platform's own users rather than CI
POST/auth/signupPOST/auth/password-resetPOST/auth/password-reset/completePATCH/auth/meGET/auth/me/ftp-credentialsruns:write, own accountGET/auth/me/githubDELETE/auth/me/githubGET/users/{id}tokens:managePOST/users/{id}/password-resetPOST/users/{id}/deactivateRuns and platform
POST/runs/{id}/restartruns:writeGET/system/aboutsystem:readGET /runsalso gains a?ccx_version=filter, which resolves a release to thecommit it was cut from so runs can be selected by version rather than by sha.
What deliberately stayed on the classic pages
Two things reach into the flow rather than sitting inside the API, and in both
cases a second implementation felt worse than the hop:
Creating an account.
/auth/signupmails a signed link that lands on theexisting completion page, which is where the row is actually written. Two places
able to mint accounts is a surface worth not having.
Trading a GitHub code for a token.
GET /auth/me/githubreports whether thecaller is connected and hands back the URL to start a connection;
DELETEforgets the platform's copy. The exchange itself stays on
/github_callback,which already holds the client secret and the redirect registered with GitHub.
The response never contains the stored token, and the authorize URL carries only
the client id and the scope, both public.
Worth stating plainly since it surprised me while writing this: GitHub here is
account linking, not sign-in.
github_callbackoperates on an alreadysigned-in user, and the only place a
Userrow is created iscomplete_signup.Every account therefore has a password, so
POST /auth/tokenscan authenticateall of them.
Password reset does not have to hop.
/auth/password-reset/completealreadytakes the signed link's three values, so the only thing tying reset to these
pages was the URL in the email. A new optional
CONSOLE_URLsetting points thelink at a web console instead when one is deployed. Left empty, which is every
existing install, nothing changes.
Scopes
No new ones. The self-service routes carry no
require_scope—PATCH /auth/me, both/auth/me/githubroutes, and the two/users/{id}self-service actions. Closing your own account or asking for your own reset link
cannot depend on
tokens:manage, because no role below admin may ever hold thatscope; gating them on it makes them unreachable for exactly the people who need
them. Ownership is checked in the body instead.
/auth/me/ftp-credentialsis the exception and sits behindruns:write. Ithands back a working credential for the ingest server, and FTP is simply another
way to upload a sample, so a token narrowed to reading has no business fetching
one. Every role holds
runs:write, so this narrows tokens without narrowingpeople.
The remaining consequence, which I would rather flag than leave to be found: a
deliberately narrow token can still edit its own owner's profile. If you would
prefer it could not, the same
runs:writetreatment applied to the otherself-service mutations is a one-line change each.
Behaviour worth reviewing
reason for asking, so they are rate-limited per client IP rather than per
token: five requests per fifteen minutes.
token. A bearer token proves the request came from a signed-in session, not
that the sender knows the account's own credentials, so a leaked token cannot
quietly become a stolen account.
Name and email are replaced with a placeholder and the password randomised; the
row stays so the samples and runs it owns keep an author. Scrambling the
password only stops new tokens being minted, so the ones already issued are
revoked here too, otherwise an account deactivated for abusing the platform
would keep API access for up to thirty days. Tokens belonging to the caller are
untouched unless it is the same account, so an admin doing this to somebody
else keeps working.
the name, the mime type libmagic reads out of the file, and the extension that
mime type implies. The third catches a banned format renamed to slip past the
first.
name. Two clients sending
sample.tsat the same moment would otherwise writeinto one file and hash something that is neither, and an endpoint invites that
concurrency in a way the browser form never did. It also means no
caller-supplied text reaches the staging path at all.
duplicate check and the name the sample is stored under. Reading a
multi-gigabyte upload again to compute it would double the cost of every upload.
/queued-samples/{id}/linkattaches the upload to an existing sample as anextra file. The classic
link_id_confirmdescribes this but its body neverdoes it, so this implements the described behaviour rather than the no-op.
order finalize uses, so a failed commit cannot leave a sample row pointing at
media that is already gone.
TestResultFile,TestResultand
TestProgress— so a re-run replaces the old outcome instead of appendinga second one beside it. The row is locked the way cancel locks it, so two
restarts arriving together do not both go clearing the same results.
_get_outputfilters on the test id and the output id together, so these URLs cannot be used
to walk another test's baselines by guessing.
the baseline's extension to name a file under
TestResults, so anything thatcould climb out of that directory is rejected at validation. Content hashes are
hex, so this costs nothing real.
defined: the bounds come from
MIN_PWD_LEN/MAX_PWD_LENin config, andreading those during class definition would import the app mid-blueprint-setup.
The classic forms read the same two keys, so the rules cannot drift apart.
One file outside the API
tests/test_auth/test_controllers.pyis the only file here that is notmod_api, the spec, ortests/api.test_github_redirectsets a GitHub tokenon the user, which sends
manage()throughgithub_token_validity()— a livePOSTto api.github.com. That failed this PR's build on a TLS interceptionunrelated to any change in it, and CI has no
GITHUB_CLIENT_IDto ask with inany case, so the request went to
/applications//token. It is now mocked thesame way
test_github_token_validityalready is, fifteen lines below.Contract
openapi-ci-api.yamlgrows from 40 paths / 49 operations to 63 / 79, followingthe conventions PR 6 established:
additionalProperties: falseon requestbodies, with length and range limits.
Spec and implementation were cross-checked in both directions — every documented
operation is implemented and every implemented operation is documented, 79 for
79, with no dangling
$refs.Testing
83 new tests, 330 across the API suite, all passing. Beyond the success paths
they cover 400/401/403/404/409 for each endpoint, and specifically:
rather than at its expiry
named after what the caller sent
...is refused rather than resolving to the staging directoryPATCH /auth/meis refused withoutcurrent_passwordwhen the change touchesthe email or the password, and 409s on an email already taken
the unknown-address path that returns before anything is sent