Skip to content

Release 0.31.0 - #230

Merged
fernandorocagonzalez merged 2 commits into
mainfrom
release/0.31.0
Sep 17, 2026
Merged

fernandorocagonzalez merged 2 commits into
mainfrom
release/0.31.0

Conversation

@fernandorocagonzalez

Copy link
Copy Markdown
Contributor

A browsable API, modelled on AWX's /api/v2/. The same routes answer with a
page when the caller sends Accept: text/html, and every response names the
routes next to it so the API can be followed rather than memorised.

What it adds

  • GET /api/v1/ — the index: every collection this key may read, as paths
  • related on the metadata responses, so a consumer can follow the graph in
    curl | jq with no HTML anywhere
  • GET /api/v1/sources/{id} — a source or view as a resource. The id answered
    only DELETE, so the obvious click from the list landed on a 405
  • An HTML layer by content negotiation: indented JSON, clickable routes,
    breadcrumbs, the route's own documentation, the Allow header, six themes
  • A login form, browser sessions and action buttons (Sync now, Evict cache)

Off by default

server.ui.enabled gates all of it, and it is false unless asked for. With
the flag unset the router is exactly the one that shipped in 0.30.0: the
middlewares are not layered and the login routes are not registered. A
machine-to-machine deployment gains only the JSON half.

The handlers were not touched

A middleware renders what they already return, and the help panel, the Allow
line and the action buttons are all read from the OpenAPI document in memory —
the same text that feeds Swagger. So a route added later is browsable and
self-documenting the day it exists, with no second place to register it.

Breaking, both on GET /api/v1/sources

  • The list is every configured source, not only the cached ones. A source
    that never completed a sync did not appear at all, and that is precisely the
    one an operator goes looking for. It can now contain an id whose /dataset
    answers 404 — ?cached=true restores the previous membership exactly.
  • age_seconds and total_hosts are null when an id holds no data, beside a
    new cached boolean. A zero there read as "synced just now, and empty".

Safety

The pages render data that came from connectors, so it is all treated as
hostile. An Html newtype that can only be built by escaping (a &str cannot
reach the page without going through it), linkification restricted to our own
route templates, and a CSP of default-src 'none' with frame-ancestors 'none'
— that last one is what keeps another site from framing a page and collecting a
click on its Evict button with the victim's own CSRF token inside.

Writes from a browser carry a CSRF token; requests presenting an API key
directly do not need one and are unchanged. The _method=DELETE override is
honoured only for a request that carries a session cookie, so a cross-site form
cannot reach a DELETE route. A configuration reload that revokes a key now ends
that key's browser sessions in the same breath.

Sessions are a handle on an existing API key, validated by the same
constant-time compare a header goes through — there is no second credential
store, so the UI inherits the permission model: a restricted key browsing the
console sees exactly the sources its token allows.

Verification

661 tests, clippy -D warnings and cargo fmt --check clean. Reviewed for
correctness, for regressions against existing JSON consumers, and adversarially
for security; every finding from those passes is either fixed here or noted in
docs/browsable-api.md.

A browsable API, modelled on AWX's /api/v2/: the same routes answer with a
page when the caller sends Accept: text/html, and every response names the
routes next to it so the API can be followed rather than memorised.

Off by default behind server.ui.enabled. With the flag unset the router is
exactly the one that shipped in 0.30.0 — no login form, no cookie credential,
no HTML renderer — so a machine-to-machine deployment gains nothing it did
not ask for.

The handlers were not touched. A middleware renders what they already return,
and the help panel, the Allow header and the action buttons are all read from
the OpenAPI document in memory, so a route added later is browsable and
self-documenting the day it exists.

Two breaking changes on GET /api/v1/sources, both documented in the CHANGELOG:
the list is now every configured source rather than only the cached ones, and
age_seconds/total_hosts are null when an id holds no data. ?cached=true
restores the previous membership for a consumer that wants it.
The advisory (TLS 1.3 handshake messages accepted across encryption level
boundaries) landed three days ago and already had main failing audit and deny.
Lockfile only: rustls reaches the build through reqwest's rustls-tls and
through russh, so it covers federated reads and the SSH connector.
@fernandorocagonzalez
fernandorocagonzalez merged commit def0ee5 into main Sep 17, 2026
6 checks passed
@fernandorocagonzalez
fernandorocagonzalez deleted the release/0.31.0 branch September 17, 2026 21:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants