diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a8c36f6..e24d97de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/CONTRACT-STABILITY.md b/docs/CONTRACT-STABILITY.md index 3e31faa8..3ed4d4f2 100644 --- a/docs/CONTRACT-STABILITY.md +++ b/docs/CONTRACT-STABILITY.md @@ -40,6 +40,7 @@ Recorded in the comment block in `shopdb/__init__.py`: | 0.18.0 | Added `DISPLAY_ROLES`, `DISPLAY_ROLE_PATHS` and `normalize_display_role`, and finally exported `DashboardDefault`, which 0.17.0 imported but left out of `__all__`. The role vocabulary became the kiosk's own - `Dashboard`, `Lobby`, `3DPrintRoom` - so a plugin holding its own copy of that map reads core's instead of drifting from it | additive surface (minor) | | 0.19.0 | **BREAKING.** `get_dashboard_widgets` returns DATA AND SHAPE, not a component name. The old shape (`name` + `component` + `size`) named a Vue component per widget, which cannot survive a lean build - a plugin's component may never be staged into the frontend bundle (ADR-013) - and five plugins were declaring widgets that pointed at components nobody had written. A card now declares `id` / `title` / `endpoint` / `render` / `severity` / `permission` / `empty` / `position`; see PLUGIN-HOOKS.md. Also added `send_upload` so a plugin serving user-supplied bytes gets the headers that keep an SVG from running as script | **contract change (minor, pre-1.0)** | | 0.20.0 | The `Asset` model gained `levelid` (which floor plan its `mapx`/`mapy` are pixels of) and `mapverifiedat`; `Location` gained `levelid` too, and `resolve_asset_position` now returns the `levelid` belonging to whichever source supplied the coordinates. A plugin that writes a map position MUST write the level with it - a position without one renders as "level unknown" rather than being drawn on the default drawing, because a marker on the wrong floor plan looks correct (ADR-017) | additive surface (minor) | +| 0.21.0 | Added the `get_guest_grantable_permissions` hook: a plugin names the WRITE permissions a site may grant to the seeded `guest` role, which is who anyone without a token now holds. Reads (`.view`/`.read`) are grantable without a plugin saying anything; anything else is refused. Also exposed `require_any_permission` on `shopdb.api` for a read two different rights should reach | additive optional hook + surface (minor) | The source comment block documents 0.3.0, 0.4.0, 0.6.0, 0.7.0, 0.9.0, 0.10.0, 0.11.0, 0.16.0, 0.18.0 and 0.19.0; 0.12.0 through 0.15.0 and 0.17.0 are recorded diff --git a/docs/PLUGIN-HOOKS.md b/docs/PLUGIN-HOOKS.md index e8850fe7..ae4573de 100644 --- a/docs/PLUGIN-HOOKS.md +++ b/docs/PLUGIN-HOOKS.md @@ -298,6 +298,40 @@ The `Permission` ROWS already in the database are NOT deleted, so roles that already reference them keep working until an admin edits the role. A broken plugin is isolated in prod and re-raised in dev/test. +### `get_guest_grantable_permissions() -> List[str]` + +Names the WRITE permissions this plugin allows a site to grant to the `guest` +role. Added in contract 0.21.0. + +Anyone with no account holds the `guest` role. It is seeded with nothing, and a +site opens a door by granting it a permission in the role grid - which replaced +the per-feature booleans (`securesend_allow_guests`, one per kiosk) that used to +answer the same question one feature at a time. + +Any `.view` or `.read` permission is grantable without a plugin saying anything. +This hook is only for the exception: a write that is genuinely meant to work for +somebody who never signs in. Naming one here does not open it - it makes the +checkbox available, and a site still has to tick it. + +```python +class ToolsPlugin(BasePlugin): + def get_guest_grantable_permissions(self): + # Sending a password to a vendor: the recipient has no account here, + # and often neither does the sender. The unguessable token is the + # authorisation and the link is one-view. + return ['tools.secrets'] +``` + +The bar for naming one: could this action be attributed to nobody, forever, and +still be defensible in the audit log? A guest write records no creator - the +trail says when and from where, never who. Return `[]` (the default) and none of +this plugin's writes can ever be made anonymous; anything not named is refused +by the API when an admin tries to grant it, not merely warned about. + +Consumed by `guest_grantable_permissions()`, which backs the role API's guard on +the guest role and the greyed-out checkboxes in the role grid. A plugin may only +bless permissions that already exist in the catalog, so a typo widens nothing. + ### `get_settings_cards() -> List[Dict]` Returns settings-catalog card definitions. Added in contract 0.7.0 (ADR-010). diff --git a/docs/PROJECT-MAP.md b/docs/PROJECT-MAP.md index a69853e0..89d046ef 100644 --- a/docs/PROJECT-MAP.md +++ b/docs/PROJECT-MAP.md @@ -12,7 +12,7 @@ never by editing this file. | series | value | governed by | |---|---|---| | product `__version__` | `0.12.0` | ADR-007 | -| plugin contract `__contract_version__` | `0.20.0` | ADR-002 | +| plugin contract `__contract_version__` | `0.21.0` | ADR-002 | They move independently. A contract bump is not a release. @@ -88,6 +88,6 @@ Manifest-less directories under `plugins/` are core frontend surface and always ## Size -- test functions defined: **1396** (parametrised cases collect higher) +- test functions defined: **1413** (parametrised cases collect higher) - documented API paths: **305** (`docs/openapi.json`, regenerate with `scripts/gen_openapi.py`) diff --git a/frontend/src/views/AppLayout.vue b/frontend/src/views/AppLayout.vue index 2d37253a..8ea01593 100644 --- a/frontend/src/views/AppLayout.vue +++ b/frontend/src/views/AppLayout.vue @@ -11,7 +11,9 @@ -