diff --git a/config/config.exs b/config/config.exs index d0ad034..64f3cd3 100644 --- a/config/config.exs +++ b/config/config.exs @@ -122,7 +122,10 @@ config :logger, :default_formatter, config :phoenix, :json_library, Jason # A BasicHTTP location's token is a bearer capability, so a params log -# names it filtered, beside Phoenix's own default. +# names it filtered, beside Phoenix's own default. Phoenix filters every +# param whose key contains a listed string, so "token" also redacts any +# other token-named param in those logs, the CSRF token (`_csrf_token`) +# among them. config :phoenix, :filter_parameters, ["password", "token"] # OpenTelemetry. `opentelemetry_statifier` brings only the API, so the SDK's diff --git a/docs/guides/basichttp-front.md b/docs/guides/basichttp-front.md index 4bf65c5..b7a9d04 100644 --- a/docs/guides/basichttp-front.md +++ b/docs/guides/basichttp-front.md @@ -20,17 +20,39 @@ which drives the whole of it through the controller. Anyone who holds a location can post events to that execution, and the router authenticates nothing beyond possession of it (ruled by the operator, 2026-09-30). The hold hands its location to the one desk its -request names and to nobody else. No request line or dispatch log carries -it: the endpoint's `Plug.Telemetry` logs no request line under -`/basichttp` (`StatifierExamplesWeb.Endpoint.log_level/1`), the route is -`log: false`, and `:filter_parameters` names `token`. Ecto's query log at -`:debug` prints bound parameters, and the router binds the token to look -a location up and to store it, so a host keeps `:debug` out of -production; at `:info`, the production level here, no query is logged. -A host serves -the base URL over TLS and rotates a location that may have leaked with -`StatifierRouter.BasicHTTP.rotate_location/2`, after which the old one -answers 404. +request names. No request line or dispatch log carries it: the endpoint's +`Plug.Telemetry` logs no request line under `/basichttp` +(`StatifierExamplesWeb.Endpoint.log_level/1`), the route is +`log: false`, and `:filter_parameters` names `token`. + +The location is also stored, and whoever can read where it is stored +holds the capability as surely as the desk does. The router's location +table holds the token, which the front looks the location up by. The +location is part of the execution's persisted state: `_ioprocessors` +names it, and the execution's `position_blob` carries it in the clear, +because `StatifierExamples.Persistence` stores the blob as `:binary`. +And the `StatifierExamples.HoldDesk.DeskPost` job that carries the POST +holds it in its arguments, in a job row this app never prunes (see the +end of "The outbound send runs in the executor"). + +A host keeps `:debug` out of production; at `:info`, the production level +here, no query is logged. `statifier_router` 0.10.0 runs its own three +statements that bind the token with Ecto's `log: false`, so the router's +query log no longer prints it, but other statements still bind it. +`statifier_persistence` writes the position on the create and on every +step, and Ecto's `:debug` query log prints those writes with the blob cut +short by its inspect limit: the token is unreadable in the line, not +absent from its parameters. Ecto's query telemetry event carries every +statement's bound parameters whatever the `log` option says, so a handler +on it receives the position writes and the desk post job's insert, whose +own log line Oban suppresses. The router's +`docs/adr/0002-addressing.md`, the Note of 2026-10-02 on the location +token and the query log, records the router's half of this and the +persisted state. + +A host serves the base URL over TLS and rotates a location that may have +leaked with `StatifierRouter.BasicHTTP.rotate_location/2`, after which +the old one answers 404. ## The pins @@ -135,7 +157,12 @@ database. The job's arguments carry the planned POST as it was planned, body included, so the `reply_to` location the body hands the desk is written to -the jobs table with it, and stays there until the host prunes the job. +the jobs table with it. This app configures no Oban pruner, and Oban +prunes nothing unless one is configured, so the row and the location in +it stay, after the job completes or is cancelled, until a host deletes +them; anyone who can read the jobs table holds the capability (see "A +location is a bearer capability" above). A host that prunes sets Oban's +`:pruner`. ## The front diff --git a/lib/statifier_examples/hold_desk.ex b/lib/statifier_examples/hold_desk.ex index f558cf2..7ce98ae 100644 --- a/lib/statifier_examples/hold_desk.ex +++ b/lib/statifier_examples/hold_desk.ex @@ -16,9 +16,32 @@ defmodule StatifierExamples.HoldDesk do **A location is a bearer capability** (ruled by the operator, 2026-09-30): anyone who holds it can post events to that execution, and the router authenticates nothing beyond possession of it. This module - hands the location to the desk the hold request names and to nobody - else, and never logs it; `StatifierExamplesWeb.BasicHTTPController` - says what keeps it out of the request log. + hands the location to the one desk the hold request names and writes + no log line of its own that carries it; + `StatifierExamplesWeb.BasicHTTPController` says what keeps it out of + the request log. Whoever can read this app's database holds the + capability too: the router's location table holds the token; the + location is part of the execution's persisted state, in + `_ioprocessors` inside the execution's `position_blob`, in the clear + (`StatifierExamples.Persistence` stores the blob as `:binary`); and the + arguments of the `StatifierExamples.HoldDesk.DeskPost` job that carries + the POST hold it, in a job row this app never prunes. + + **The `:debug` limit.** `statifier_router` 0.10.0 runs its own three + statements that bind the token - the front's lookup, the location + insert at create and the rotation upsert - with Ecto's `log: false`, + so the router's query log no longer prints it. The token still reaches + a `:debug` repo through statements that are not the router's. + `statifier_persistence` binds the execution's position on the create + and on every step, and Ecto's query log prints those writes with the + blob cut short by its inspect limit, which makes the token unreadable + in the line but not absent from the parameters. And Ecto's query + telemetry event carries every statement's bound parameters whatever + the `log` option says, so a handler on it receives the position writes + and the desk post job's insert, whose own log line Oban suppresses. So + this app keeps `:debug` out of production, as the router's ADR-0002 + advises in its Note on the location token and the query log, and + rotates a location that may have leaked. `config/0` is a router configuration of its own, separate from `StatifierExamples.RoutedWorkflow.config/0`: only this configuration diff --git a/lib/statifier_examples_web/controllers/basic_http_controller.ex b/lib/statifier_examples_web/controllers/basic_http_controller.ex index a9067b3..a02382a 100644 --- a/lib/statifier_examples_web/controllers/basic_http_controller.ex +++ b/lib/statifier_examples_web/controllers/basic_http_controller.ex @@ -14,9 +14,10 @@ defmodule StatifierExamplesWeb.BasicHTTPController do nothing else. No request line or dispatch log carries it: `StatifierExamplesWeb.Endpoint.log_level/1` skips the request line for `/basichttp`, the route is `log: false`, and `:filter_parameters` names - `token`. At `:debug` Ecto's query log prints bound parameters, and the - router's location lookup binds the token, so a host keeps `:debug` out - of production. + `token`. The router's own lookup no longer prints the token at `:debug` + (`statifier_router` 0.10.0 runs it with Ecto's `log: false`); + `StatifierExamples.HoldDesk` says what still carries the token at that + level, and why a host keeps `:debug` out of production. """ use StatifierExamplesWeb, :controller