docs: correct how the employee payslip surface is gated

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jy52j9GRZ6XKa1Zt21LLj
This commit is contained in:
Padreug 2026-08-31 16:16:19 +02:00
commit 36e1643470
2 changed files with 44 additions and 10 deletions

View file

@ -25,12 +25,33 @@ workspace `CLAUDE.md`.)
Add `payroll` to `LNBITS_ADMIN_EXTENSIONS` so the extension is hidden from Add `payroll` to `LNBITS_ADMIN_EXTENSIONS` so the extension is hidden from
ordinary users in the UI as well. ordinary users in the UI as well.
The exception is the employee-facing surface — `/api/v1/my/payouts`, **Do not** add `payroll` to `LNBITS_ADMIN_EXTENSIONS` — see below.
`/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 The employee-facing surface — `/api/v1/my/payouts`, `/my/payouts.csv`,
paid into that wallet, and nothing else. It stays reachable regardless of `/my/contracts` — is gated on a wallet **invoice key** instead. Whoever
`LNBITS_ADMIN_EXTENSIONS`, which governs UI visibility rather than route holds the read key for a wallet can see what payroll paid into that wallet,
mounting. 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 ## Development

View file

@ -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 names exactly one wallet, so there is no lookup that could widen the result
to a sibling wallet the key does not cover. to a sibling wallet the key does not cover.
Note this stays reachable even with `payroll` in `LNBITS_ADMIN_EXTENSIONS`. **The employee must also have `payroll` enabled on their account.** LNbits
That flag governs who sees the extension in the UI; the API routes are gates extension routes per user, not just the UI — `_check_user_access`
mounted instance-wide, and these three are gated by the wallet key they runs `check_user_extension_access` (`lnbits/decorators.py:420`) on every
require rather than by UI visibility. 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.