Declare the guest grants that already shipped

Inverting the grantable rule from the .view suffix to explicit declaration
dropped every permission that had qualified by how it was spelled. Two of
them were shipped guest features with grants already in the field.

guides.view: the nav item and list_guides both reason in comments about a
signed-out reader holding it, and the route is written around one - drafts
are filtered out, search matches the published revision so draft wording
cannot leak through a hit, and the author branches cannot open for a caller
with no user id.

tools.view: without it a guest holding tools.secrets has permission to use
Secure Send and no way to reach the page, because tools.view is what puts
the entry in the nav. The tools themselves run in the browser and touch no
data.

The failure was worse than a missing checkbox. update_role refuses the
WHOLE request when any submitted permission is undeclared, and the role
grid resubmits what the role already holds, so a site that granted either
of these under the old rule could no longer save the guest role at all -
including to tick assets.view, which is the grant the shop floor needs to
look up an asset number without signing in.

The test asserts both stay grantable, so narrowing the set again means
looking at what is already granted in the field rather than only at what
the catalogue declares.

Scope: product
This commit is contained in:
cproudlock
2026-08-31 09:56:19 -04:00
parent d9eb7a74db
commit 723a4ff19b
3 changed files with 49 additions and 1 deletions

View File

@@ -74,6 +74,25 @@ class GuidesPlugin(BasePlugin):
('guides.delete', 'Delete guides', 'guides'),
]
def get_guest_grantable_permissions(self) -> List[str]:
"""A procedure is for whoever is standing at the machine.
Guides are written to be followed on the floor, by people who often
have no account and no reason to get one. The nav item and the API
route both already reason about a signed-out reader holding this, so
the grant was intended from the start; it qualified under the old
.view suffix rule and is written down here now that a grant has to be
declared.
What it reaches is bounded by the routes, not by this: list_guides
returns published guides only, matching search against the PUBLISHED
revision so draft wording cannot leak through a hit, and a guest can
never take the author branches because a guest has no user id. Guide
history stays with the editors. Publishing, editing and deleting are
separate permissions and none of them is grantable.
"""
return ['guides.view']
def get_navigation_items(self) -> List[Dict]:
# Position 8, directly under Slides (7), rather than down in the
# Information section with the reference lists. A guide is something you

View File

@@ -85,8 +85,14 @@ class ToolsPlugin(BasePlugin):
A guest-created link records no creator. The audit trail says when and
from where, and never who. That is the cost, and it is why this is the
only write in the product offered to guests at all.
tools.view rides along because tools.secrets without it is a page a
guest holds permission to use and no way to reach: it gates the nav
entry, and the browser tools behind it (codes, conversions) run client
side and touch no data at all. Declared explicitly rather than inferred
from the .view suffix, which is what the grantable rule now asks for.
"""
return ['tools.secrets']
return ['tools.secrets', 'tools.view']
def get_settings_defaults(self) -> List[dict]:
"""The one switch this plugin owns.

View File

@@ -125,6 +125,28 @@ def test_a_view_permission_is_not_grantable_merely_for_being_a_view(app):
assert name not in grantable, name
def test_the_guest_features_that_already_shipped_stay_grantable(app):
"""Both were granted on a live site before a grant had to be declared.
Inverting the rule to explicit declaration dropped every permission that
had qualified by its .view suffix, including two the product ships guest
flows for: guides, whose nav item and list route both reason about a
signed-out reader, and the tools page, without which a guest holding
tools.secrets cannot reach Secure Send at all.
A site that granted them under the old rule was then unable to save the
guest role AT ALL - the API refuses the whole request when any submitted
permission is undeclared, and the grid resubmits what the role already
holds. That deadlock is what this guards against, so anyone narrowing the
grantable set again has to look at what is already granted in the field.
"""
from shopdb.core.models import guest_grantable_permissions
with app.app_context():
grantable = guest_grantable_permissions()
assert 'guides.view' in grantable
assert 'tools.view' in grantable
def test_the_bytes_behind_applications_are_not_grantable(app):
"""applications.view is grantable only because the bytes moved off it."""
from shopdb.core.models import guest_grantable_permissions
@@ -232,3 +254,4 @@ def test_a_bad_token_is_still_refused(client, db, guestrole):
response = client.get('/api/auditlogs',
headers={'Authorization': 'Bearer not.a.token'})
assert response.status_code == 422