diff --git a/.planning/notes/2026-10-22-external-user-plugin-escalations.md b/.planning/notes/2026-10-22-external-user-plugin-escalations.md index d88297b1..496fdc37 100644 --- a/.planning/notes/2026-10-22-external-user-plugin-escalations.md +++ b/.planning/notes/2026-10-22-external-user-plugin-escalations.md @@ -26,7 +26,7 @@ Reviewed 2026-10-22. The list is `golem15.user` (sm-user-plugin), not cabana adm | 5 | G7 Users form has no role field | Decline | — | — | declined | n/a | | 6 | G8 API token guard ignores `tokens_valid_after` | Decline | — | — | declined | n/a | | 7 | Cookie option on `/login` | Feature | [261022-wah](../quick/261022-wah-opt-in-httponly-session-cookie-on-user-p/) · [todo](../todos/pending/2026-10-22-user-login-opt-in-session-cookie.md) | strnsccy | done (unreleased) | `846087d`, `8bdf2c7` → v0.5.0 | -| 8 | Password-routes 404 switch | Feature | [2026-10-22-user-password-routes-404-switch.md](../todos/pending/2026-10-22-user-password-routes-404-switch.md) | h7yrafgd | open | | +| 8 | Password-routes 404 switch | Feature | [261022-x7a](../quick/261022-x7a-user-plugin-golem15-user-password-public/) · [todo](../todos/pending/2026-10-22-user-password-routes-404-switch.md) | h7yrafgd | done (unreleased) | `3828a65` → v0.5.0 | | 9 | Lockout keyed on `RemoteAddr` | Fix | [261022-vfz](../quick/261022-vfz-user-plugin-login-throttle-via-surf-clie/) · [todo](../todos/pending/2026-10-22-user-login-lockout-and-same-second-cutoff.md) | sniewiif | done (unreleased) | `b6f8055` → v0.5.0 | | 10 | Same-second token refusal | Fix | [261022-vfz](../quick/261022-vfz-user-plugin-login-throttle-via-surf-clie/) · [todo](../todos/pending/2026-10-22-user-login-lockout-and-same-second-cutoff.md) | sniewiif | done (unreleased) | `a484d0e` → v0.5.0 | @@ -48,7 +48,7 @@ Status values: `open` · `done` · `declined`. Shipped: local commit now; replac /msd-quick --validate User-plugin golem15.user.password.public_routes (default true). When false, do not register POST /login /forgot-password /reset-password so they 404 like unknown paths. Leave /register and the rest. No framework wrap, no custom 404. Base 8bdf2c7. See .planning/todos/pending/2026-10-22-user-password-routes-404-switch.md and .planning/notes/2026-10-22-external-user-plugin-escalations.md ``` -Suggested order if you care about risk: G4 (serve blacklist), then G3, G2, 9+10, then the two features. Bugs and item 7 are done locally; item 8 (password-routes 404) is the last of the six. Do not overlap on the same plugin files. Do not send the reply until v0.5.0 is tagged. +Suggested order if you care about risk: G4 (serve blacklist), then G3, G2, 9+10, then the two features. All six are done locally; item 8 (password-routes 404) was the last. Do not overlap on the same plugin files. Do not send the reply until v0.5.0 is tagged. ## Verdicts (kept for the sessions) @@ -59,7 +59,7 @@ Suggested order if you care about risk: G4 (serve blacklist), then G3, G2, 9+10, - **G7.** Stock form omission was a 12.1 decision. Stale `organisation_role` is already cleared (`ClearOnChange`, v0.1.8). Apps extend the form. - **G8.** Personal tokens die via `revoked_at`. `RevokeUserAccess` already sets that on deactivate/ban. Do not teach the API-token guard a JWT cutoff. Password reset killing API tokens would be a separate, explicit revoke. - **7.** Done locally (`846087d`, `8bdf2c7`). `846087d`: a `/refresh` carried by the `auth.cookie` cookie (`token` by default, no bearer) rewrites it and answers `{}`, whatever `token_cookie` says; bearer and other-cookie refreshes stay body-only; refusals write no cookie. `8bdf2c7`: `golem15.user.jwt.token_cookie` (default off) makes /login, /register (auto sign-in) and /activate-by-code set the HttpOnly `auth.cookie` cookie via `bouncer.LoadCookieConfig` and omit body `token`; off keeps today's bodies. SameSite=Lax, no CSRF; API tokens untouched. -- **8.** Plugin config; no framework wrap. Next: `golem15.user.password.public_routes` (default true). When false, skip registering `/login`, `/forgot-password`, `/reset-password`. `/register` stays (`allow_registration` already gates it). +- **8.** Done locally (`3828a65`). Plugin config; no framework wrap. `golem15.user.password.public_routes` (default true); false leaves `POST /login`, `/forgot-password` and `/reset-password` unregistered, so they get the router's own 404 like any unknown path (a `GET` too, no 405); no custom handler, no `pact`/`surf` change. `/register` stays (`allow_registration` gates it, `golem15.user.turnstile` still guards it), and so do `/activate*`, `/fetch`, `/refresh`, `/logout`, `/change-password` and `/tokens`. Read when the routes are built (a restart applies it); a value other than true or false fails boot. - **9.** Done locally (`b6f8055`). /login keys the throttle on surf.ClientIP with http.trusted_proxies (the /register expression); RemoteIP stays the RemoteAddr helper; without trusted proxies X-Forwarded-For is ignored. - **10.** Done locally (`a484d0e`). /reset-password and the admin password set truncate the cutoff to whole seconds like RevokeUserAccess; change-password and bouncer unchanged. @@ -97,7 +97,7 @@ Ships in SummerCMS v0.5.0 (sm-user-plugin 846087d and 8bdf2c7, tagged with that Password-routes switch Agreed, in the user plugin, not as a framework “wrap another plugin’s routes” seam. A config key will stop registering /login, /forgot-password and /reset-password so they 404. -TODO: ships in SummerCMS v0.5.0 — config key . +Ships in SummerCMS v0.5.0 (sm-user-plugin 3828a65, tagged with that wave): set golem15.user.password.public_routes: false (default true; SUMMER_GOLEM15__USER__PASSWORD__PUBLIC_ROUTES=false in the environment) and POST /login, POST /forgot-password and POST /reset-password are not registered, so they answer the same 404 as any unknown path, whatever the method. POST /register stays (allow_registration still decides whether it takes accounts, and golem15.user.turnstile still guards it), as do /activate, /activate-by-code, /fetch, /refresh, /logout, /update, /change-password and /tokens. With the key true or unset nothing changes. The key is read when the routes are built, so a change needs a restart; a value other than true or false stops boot with an error naming the key. Login lockout keyed on RemoteAddr Agreed. /login used RemoteAddr; /register already uses the trusted-proxy client address. Behind a reverse proxy five failures locked everyone. diff --git a/.planning/todos/pending/2026-10-22-user-password-routes-404-switch.md b/.planning/todos/pending/2026-10-22-user-password-routes-404-switch.md index 7fc62220..0d3eeade 100644 --- a/.planning/todos/pending/2026-10-22-user-password-routes-404-switch.md +++ b/.planning/todos/pending/2026-10-22-user-password-routes-404-switch.md @@ -29,6 +29,10 @@ Leave registered: `/register` (`allow_registration` already refuses at the handl Base is sm-user-plugin `8bdf2c7` (item 7). Hold push/tag for SummerCMS v0.5.0. After the commit, fill item 8 in `.planning/notes/2026-10-22-external-user-plugin-escalations.md` like the earlier rows (`done (unreleased)`, ships in v0.5.0, name the key). Leave the todo in pending with an Outcome. Do not send the reply; the hold stands until the rest of the v0.5.0 wave (keyboard shortcuts, MCP / 19.7) is tagged. +## Outcome (2026-10-23) + +Quick `261022-x7a`. One commit on sm-user-plugin master, `3828a65` (base `8bdf2c7`). `routes.go` reads `golem15.user.password.public_routes` through `publicPasswordRoutes` (unset, empty or true register every route; a bool or the string true or false, any case; anything else fails `Routes` naming the key) and, when false, skips only `POST /login`, `/forgot-password` and `/reset-password`; the turnstile guard stays on `/register`, the default route table is unchanged, and there is no `pact`/`surf`/controller change. `TestPasswordPublicRoutes` (`-short`, route-table recording) and `TestPasswordPublicRoutesAnswer404` (Docker: the dropped routes answer exactly like an unknown path, the rest keep working) cover it. README and `config.yaml` updated. Reply note item 8 filled. Not tagged, not pushed: ships with the v0.5.0 wave. + ## Invoke ```