Decide guest grants explicitly instead of by permission name

guest_grantable_permissions() opened any permission whose name ended
'.view' or '.read' to the guest role, on the reasoning that reading is
the safe default. That is true of a permission gating a list of asset
numbers and false of one gating bytes, and a suffix cannot tell them
apart. Twenty-four of eighty-five qualified and none had been decided:

  applications.view  reached the SYSTEM-context PowerShell bodies and
                     the installer binaries pushed to bays
  backups.view       returned the full payload that the separate
                     backups.download permission exists to protect
  assets.view        gated a global search that queries the staff
                     directory, USB history and network inventory with
                     no per-domain check of its own
  audit.view         usernames, addresses, the before/after of every
  settings.view      change, and every setting row - both admin
                     category, opened by naming alone
  users.view         gates nothing at all; list_users is
                     require_role('admin')

Every entry is now declared, by CORE_GUEST_GRANTABLE or by the plugin
that owns the permission, having read what its routes actually return.
Adding one is a code change, which is the point: it puts the decision
in a diff rather than in a suffix.

The two routes serving script bodies and installer packages move to a
new applications.download permission, which is not grantable, so a site
can still open the catalogue metadata to its floor without the bytes
going with it.

The declared set preserves the grants this site had already chosen:
applications.view, computers.view, endpoints.view, reports.view,
tools.secrets, warranty.view.

Two known gaps are recorded on the hooks that expose them rather than
silently carried: the computer serializer publishes every column plus
resolved rdp:// and vnc:// links, and the per-asset endpoint panels are
keyed on a small integer so walking ids reconstructs much of the roster
that endpoints.fleet exists to protect. Neither is widened here.

A shipped test was granting guests audit.view and passing.

Scope: product
(cherry picked from commit 347402896c)
This commit is contained in:
cproudlock
2026-08-31 07:01:52 -04:00
parent 5f27f81e88
commit 85e8a7b877
10 changed files with 192 additions and 41 deletions

View File

@@ -107,17 +107,24 @@ class BasePlugin(ABC):
def get_guest_grantable_permissions(self) -> List[str]:
"""Permissions this plugin allows a site to grant to the GUEST role.
A guest is anyone with no account. Any `.view` or `.read` permission is
grantable without asking a plugin - reading is the safe default. This
hook is for the exception: a WRITE that is genuinely meant to be usable
by somebody who never signs in.
A guest is anyone with no account. EVERY permission of this plugin's
that a site may grant one has to be named here, reads included.
Reads used to be automatic: anything ending `.view` or `.read` was
grantable. That treats a name as a decision, and it went wrong in the
obvious way - permissions that gated installer bytes, backup payloads
and a search across the staff directory were all anonymous-grantable
because of how they were spelled. So the plugin that owns a permission
now says, having looked at what its routes return.
Naming one here does not open it. It makes the checkbox available in
the role grid; a site still has to tick it. Return [] (the default) and
none of this plugin's writes can ever be made anonymous.
nothing of this plugin's can be made anonymous.
The bar: could this action be attributed to nobody, forever, and still
be defensible in the audit log? If not, leave it out.
The bar for a read: could an anonymous visitor hold this, forever, and
would the site be content with everything the routes return - every
column of every serializer, not just the ones the UI happens to show?
For a write, add that the act can never be attributed to anybody.
"""
return []