Files
werkator/docs/prs/2026-09-09-PR#43-private-repositories-and-quota-severity.md
2026-09-09 18:49:25 +02:00

174 lines
9.2 KiB
Markdown

> **WARNING:** This document describes only the change applied in this PR.
> It may already be outdated once the next PR is merged.
> Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.
## Related Links
- [PR#42](2026-09-09-PR%2342-sizing-corrections.md) — corrected the sizing numbers; this PR corrects how loudly they are said, and adds the order button this PR now gates.
## The Problem
**A full quota was as red as a full disk.**
The SSD group quota on the production host reached 92 %, so the advice chip turned red and the machine verdict read *overloaded*.
Neither is true in the way the colour claims: a quota is raised by asking a service desk, nothing stops while it is being asked for, and the hardware behind it had well over a terabyte free.
Red belongs to what cannot be asked for.
**The build pages showed every repository's work to everyone.**
The pages are readable without any token — that is deliberate, a build page is meant to be linkable — and they name every branch, every commit, every artifact and every log line of every served repository.
For a repository that is private on the forge, that is more than a passer-by should see, and the instance is on the internet.
**The order button offered to spend money.**
PR#42 put a button on the System page that composes a hardware order; it appeared for anyone who opened the page, and it named the webspace and the machine while doing so.
## Non-Goals
- No login, no sessions, no users. The control token is what Werkator has, and this PR only makes it readable earlier in the request.
- No configuration key for visibility. The forge knows, and a second place to declare it could only drift from the truth.
- Private repositories are not removed from the overview. That a repository exists, that it builds, and that a build failed stays visible; only what names the work goes.
- Nothing changes for a public repository, or for an instance with no forge configured.
## The Scenarios
### Feature: how loudly an advice is shown
#### Scenario#43.01: A quota to raise is yellow, a filling volume is red
So that a colour says how urgent a thing is, not merely what kind of thing it is.
- **Given** a disk whose peak is 92 % of a group quota
- **and** whose physical volume has room to spare
- **Then** the verdict stays `upgrade` and the chip is drawn in the watch colour
- **and** the machine verdict counts it as pressure, not as overload
- **Given** the same fill against the volume itself, or against a quota whose volume is the tighter budget
- **Then** the chip is drawn in the critical colour and the machine verdict counts it as overload
- **because** raising a quota whose backing is what runs out buys nothing
##### Verified by
- [CapacitySizingTest](../../src/test/kotlin/de/hoennig/werkator/metrics/CapacitySizingTest.kt)
- [UiViewsTest](../../src/test/kotlin/de/hoennig/werkator/server/UiViewsTest.kt)
### Feature: private repositories
#### Background
- The pages and the JSON API are readable without a token; only mutating endpoints have ever required one.
- A repository's visibility is the forge's own fact, reachable through the Gitea repository API.
#### Scenario#43.02: What a visitor sees of a private repository
So that a build page can stay public without publishing the work behind it.
- **Given** a repository the forge calls private
- **and** a browser without the control token
- **When** any page or API of that repository is requested
- **Then** its name reads `hidden private repo`, greyed out, and carries no link
- **and** branch, build name and commit read `n/a`
- **and** the artifact key is empty, so no artifact, no log and no permanent branch link is offered
- **and** no link into the forge is rendered
- **and** the redaction happened on the server: the JSON the browser received never held the values
- **And then** the counts, the statuses and the timestamps are untouched, and the shapes are unchanged, so the page renders the repository like any other
- **Given** a public repository
- **Then** nothing changes at all
##### Verified by
- [RedactionTest](../../src/test/kotlin/de/hoennig/werkator/server/RedactionTest.kt)
- [BuildsApiControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/BuildsApiControllerTest.kt)
#### Scenario#43.03: The overview keeps the shape, loses the identity
- **Given** a private repository among the served ones
- **Then** its entry shows `hidden private repo` and is not clickable
- **and** its failed builds are listed as `n/a`, with their timestamps
- **and** its branch count, running and pending counts and watcher problem stay
##### Verified by
- [RedactionTest](../../src/test/kotlin/de/hoennig/werkator/server/RedactionTest.kt)
#### Scenario#43.04: The forge is asked, and the fallbacks are asymmetric
So that nothing has to be configured, and a failure does not publish a repository.
- **Given** a repository whose forge answers `private`
- **Then** it is hidden; answered `public`, it is shown
- **Given** a repository with no forge configured
- **Then** it is public, exactly as before this change — there is nothing to ask and nothing to hide behind
- **Given** a repository whose forge is configured but does not answer
- **Then** the last answer stands, and without one the repository is hidden
- **And** the answer is cached, so the pages' polling never reaches the forge
##### Verified by
- [RepositoryPrivacyTest](../../src/test/kotlin/de/hoennig/werkator/server/RepositoryPrivacyTest.kt)
#### Scenario#43.05: The token in a cookie reveals, the token in a header acts
So that a server-rendered page can decide what to show, without weakening what protects the actions.
- **Given** a browser that has been given the control token
- **Then** it is kept in local storage as before *and* in a cookie
- **and** the cookie makes every page and every API answer unredacted
- **and** restarting or deleting a build still requires the token in the request header
- **Given** a foreign page that makes the browser send the cookie
- **Then** it gains nothing: it can neither read the response across origins nor set the header the actions require
##### Verified by
- [BuildsApiControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/BuildsApiControllerTest.kt)
### Feature: the order button
#### Scenario#43.06: Only the operator may order
- **Given** a browser without the control token
- **Then** `GET /api/system` reports no hosting, so neither the order button nor the webspace and machine names reach it
- **Given** the control token
- **Then** the button appears as in PR#42
##### Verified by
- [SystemApiControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/SystemApiControllerTest.kt)
#### Scenario#43.07: An action nobody may take is shown, but inert
So that the page says what exists without inviting a click that can only fail.
- **Given** a browser without a valid control token
- **Then** the restart, delete and cancel buttons are rendered greyed out and disabled
- **and** their tooltip says why: `Needs the control token`
- **and** this holds for the server-rendered rows and for the rows the script re-renders alike
- **And** the header carries a key button that asks for the token and reloads
- **because** with every action disabled there would otherwise be nothing left to click that asks for it
- **Given** a valid token
- **Then** the buttons behave as before, and the key button offers to forget it again
##### Verified by
- [UiControllerTest](../../src/test/kotlin/de/hoennig/werkator/server/UiControllerTest.kt)
## The Solution
`ResourceAdvice` gains an `askable` flag and, from it, a `severity` in the page's own `ok`/`warn`/`crit` vocabulary.
The chips take their colour from the severity rather than from the verdict, so the text keeps saying *upgrade* while the colour says how loudly; `SystemHealth` applies the same rule to the machine verdict.
A disk is askable when its budget is a quota and the volume behind it is not the tighter budget.
`GiteaClient.readVisibility` asks the forge, and `RepositoryPrivacy` caches the answer per repository with the fallbacks of Scenario#43.04.
`Redaction` is one object holding what a redacted build, branch and overview look like, applied in the controllers before anything is serialized — never in the page.
That is what keeps the frontend unaware: same fields, same types, one row per build, and a first paint that already agrees with the first poll.
The same cookie tells a page whether it may act: `canAct` reaches the templates and, as a `data-` attribute, the script, so the disabled state is decided in one place and by the only party that can tell a valid token from a stale one.
`ViewerIdentity` reads the token from the cookie, from the current request where a controller has many handlers.
The cookie is written by `werkator.js` next to the local-storage copy it has always kept, including once on load for a browser that was given the token before this change.
## Open Questions
- The visibility cache has no invalidation other than its fifteen minutes; making a repository public shows up within that window, not immediately.
- A repository is hidden as a whole. A forge that distinguishes internal from private is treated as private.
## Additional Changes
- Version 1.5.0 and its release-notes entry, since this is what gets deployed.