Anyone not signed in holds the guest role

Whether somebody with no account may see something was being answered one
feature at a time: a securesend_allow_guests setting, an enable flag per kiosk,
and very nearly a third for guides - each with its own settings page, its own
vocabulary and its own hand-written branch inside a route. Every one of them
answered a permissions question, and this app already administers permissions
per role.

So a caller with no token is not nobody: they hold the seeded `guest` role.
require_permission consults it when there is no identity, which is the entire
mechanism. The role is seeded holding NOTHING, so an upgrade opens no door; a
site opens one by ticking a box in the same grid it uses for every other role,
and closes it the same way. Revocation is read on every request rather than
cached, because taking guest access away is what somebody does the moment they
realise what they opened, and it has to bite immediately.

The redundant @jwt_required() above require_permission is gone from 184 routes.
It was belt-and-braces before; now it is actively wrong, because it refuses a
request with no token before the permission check can have an opinion, so a
grant would appear to do nothing.

What a guest may hold is bounded. Any .view or .read permission is grantable.
A WRITE has to be blessed by whoever owns it, through the new
get_guest_grantable_permissions hook (contract 0.21.0), and Secure Send is the
only case that exists: sending a password to a vendor is the one write this
product means to offer somebody who never signs in. Everything else is refused
by the API and greyed out in the grid rather than warned about - a checkbox
that quietly makes deleting an asset anonymous is not a warning anybody reads
twice. A guest write records no creator: the trail says when and from where,
never who, and that is the reason for the bar.

GE-Enforce and Slides gained read permissions so they can be opened to guests
without handing out the manage right. Their read routes accept either through
require_any_permission, so nothing an existing manager could reach has moved.

The navigation endpoint now returns only what the caller can actually reach.
Without that a signed-out visitor gets a sidebar of the whole app where every
link bounces off the login page, which reads as a broken site rather than as a
closed door. A nav item may name several permissions; holding any one is
enough, which is what GE-Enforce and Slides need now that a viewer and a
manager hold different strings.

Two things this found on the way: current_user is a LocalProxy, so
`current_user is None` is False for a request with nobody behind it - harmless
under a required JWT, an AttributeError the moment verification became
optional. And an image pasted into a guide before its first save was never
attached to it, so it was visible to editors and invisible to every reader;
saving now attaches it, and `flask guides fix-images` repairs what is stored.

The guides guest setting written earlier today is gone. Granting guides.view to
the guest role replaces it, which is the point.
This commit is contained in:
cproudlock
2026-08-27 12:51:58 -04:00
parent 8531559f22
commit 151d7c4f44
44 changed files with 783 additions and 240 deletions

View File

@@ -12,6 +12,34 @@ ADR-007 and ADR-002.
### Added
- **A guest is a role now, not a switch per feature.** Whether somebody with no
account may see something was answered one feature at a time: a
`securesend_allow_guests` setting, an enable flag per kiosk, and very nearly a
third for guides - each with its own settings page and its own hand-written
branch inside a route. It is a permissions question, and this app already
administers permissions per role, so anyone not signed in now holds the
seeded `guest` role. It starts with nothing, so an upgrade opens no door; a
site opens one by ticking a box in the same role grid it uses for everybody
else, and closes it the same way - taking effect on the next request, not the
next restart. `require_permission` consults the role when there is no token,
which is the whole mechanism.
What a guest may be granted is bounded: any `.view` or `.read` permission,
plus writes a plugin has explicitly declared safe to perform without an
account (`get_guest_grantable_permissions`, contract 0.21.0). Secure Send
declares its create permission, because sending a password to a vendor is the
one write the product means to offer somebody who never signs in. Everything
else is refused by the API and greyed out in the grid rather than warned
about. A guest write records no creator - the audit trail can say when and
from where, never who - and that is the reason for the bar.
GE-Enforce and Slides gained read permissions (`geenforce.view`,
`slides.view`) so they can be opened to guests without handing out the manage
right; their read routes accept either, so nothing an existing manager could
reach has moved. The navigation endpoint now returns only what the caller can
actually reach, so a signed-out visitor sees the pages that were opened to
them rather than a sidebar of links that bounce off a login page.
- **USB check-in / check-out audit report.** CMMC removable-media tracking is an
audit obligation and there was no report for it - answering "who had this
drive, and when" meant paging through the check-in/out list by hand. The