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:
parent
8a10c3cacc
commit
36e1643470
2 changed files with 44 additions and 10 deletions
33
README.md
33
README.md
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue