From 36e16434708a4256cecc474fcdba2c21393e3428 Mon Sep 17 00:00:00 2001 From: Padreug Date: Mon, 31 Aug 2026 16:16:19 +0200 Subject: [PATCH] docs: correct how the employee payslip surface is gated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The README and operations notes claimed the /my/* endpoints "stay reachable regardless of LNBITS_ADMIN_EXTENSIONS" because "the API routes are mounted instance-wide". Both halves are wrong, and the smoke test on bohm caught it: an invoice-key request from employee_1 came back with {"detail": "Extension 'payroll' not enabled."} LNbits gates every extension path per user via check_user_extension_access (lnbits/decorators.py:420), not just the UI listing. Two consequences the docs now state instead of contradicting: - An employee needs `payroll` among their active extensions before an invoice key gets them anywhere. Enable it through LNBITS_USER_DEFAULT_EXTENSIONS or per account in the Admin UI. - `payroll` must stay OUT of LNBITS_ADMIN_EXTENSIONS. That list trips the earlier branch of the same check — "User not authorized for extension" — which a non-admin can never clear, so it would permanently lock employees out of their own payslips. It also buys nothing: check_super_user on the router and user.super_user on the page route are what actually restrict the console, and a non-admin key was verified to get 401 from /contracts and /users. Also notes the wart this exposes: an employee with the extension enabled sees "Payroll" in their menu and gets a 403 page, because the only page route is the operator console. Filed as a follow-up rather than fixed here. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018jy52j9GRZ6XKa1Zt21LLj --- README.md | 33 +++++++++++++++++++++++++++------ docs/operations.md | 21 +++++++++++++++++---- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 6897391..38489c7 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,33 @@ workspace `CLAUDE.md`.) Add `payroll` to `LNBITS_ADMIN_EXTENSIONS` so the extension is hidden from ordinary users in the UI as well. -The exception is the employee-facing surface — `/api/v1/my/payouts`, -`/my/payouts.csv`, `/my/contracts` — which is gated on a wallet **invoice -key** instead. Whoever holds the read key for a wallet can see what payroll -paid into that wallet, and nothing else. It stays reachable regardless of -`LNBITS_ADMIN_EXTENSIONS`, which governs UI visibility rather than route -mounting. +**Do not** add `payroll` to `LNBITS_ADMIN_EXTENSIONS` — see below. + +The employee-facing surface — `/api/v1/my/payouts`, `/my/payouts.csv`, +`/my/contracts` — is gated on a wallet **invoice key** instead. Whoever +holds the read key for a wallet can see what payroll paid into that wallet, +and nothing else. + +### Employees must have the extension enabled + +LNbits gates every extension route *per user*, not just the UI +(`check_user_extension_access`, `lnbits/decorators.py:420`). An employee who +does not have `payroll` among their active extensions gets + +```json +{"detail": "Extension 'payroll' not enabled."} +``` + +from `/my/payouts`, invoice key or not. So enable `payroll` for anyone who +should read their own payslips — via `LNBITS_USER_DEFAULT_EXTENSIONS`, or +per account in the Admin UI. + +That is also why `payroll` must stay **out** of `LNBITS_ADMIN_EXTENSIONS`. +That list triggers the harder branch of the same check — *"User not +authorized for extension"* — which a non-admin can never clear, so it would +make the employee surface permanently unreachable. Restricting the operator +console does not depend on it anyway: every super-user route is gated in +code by `check_super_user`, and the page route checks `user.super_user`. ## Development diff --git a/docs/operations.md b/docs/operations.md index 888c67b..5a9b8fd 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -160,7 +160,20 @@ Scoped to the wallet rather than to the account on purpose: an invoice key names exactly one wallet, so there is no lookup that could widen the result to a sibling wallet the key does not cover. -Note this stays reachable even with `payroll` in `LNBITS_ADMIN_EXTENSIONS`. -That flag governs who sees the extension in the UI; the API routes are -mounted instance-wide, and these three are gated by the wallet key they -require rather than by UI visibility. +**The employee must also have `payroll` enabled on their account.** LNbits +gates extension routes per user, not just the UI — `_check_user_access` +runs `check_user_extension_access` (`lnbits/decorators.py:420`) on every +extension path. Without it the endpoint answers `{"detail": "Extension +'payroll' not enabled."}` however valid the invoice key is. Enable it via +`LNBITS_USER_DEFAULT_EXTENSIONS` or per account in the Admin UI. + +For the same reason `payroll` must stay **out** of +`LNBITS_ADMIN_EXTENSIONS`: that list trips the earlier branch of the same +check, *"User not authorized for extension"*, which a non-admin can never +clear. It would lock employees out of their own payslips permanently, and +it buys nothing — the operator console is already gated in code by +`check_super_user` on the router and `user.super_user` on the page route. + +Known wart: an employee with the extension enabled sees "Payroll" in their +menu, and clicking it hits the operator console route, which 403s. Tracked +as a follow-up — the page should render their payslips instead.