Add the get_permissions plugin hook (contract 0.10.0)
All checks were successful
CI / backend (push) Successful in 1m20s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 8s

Plugins declare their own RBAC permissions instead of core accumulating
them: 36 permissions moved out of the core catalog into the 9 owning
plugins (core keeps the 19 its own blueprints enforce). The catalog is
resolved dynamically (core + enabled plugins) and feeds the roles grid,
the token scope picker and ceiling, and flask seed permissions;
installing or enabling a plugin seeds its permissions automatically. A
disabled plugin drops out of the assignable catalog while existing role
links keep working. New plugins - bundled or external - now bring their
permissions with zero core edits.

781 tests pass; live-verified with a machines.edit-scoped token.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-12 09:29:55 -04:00
parent 12175169e4
commit 7dfbe7bf8a
22 changed files with 439 additions and 90 deletions

View File

@@ -330,20 +330,27 @@ def create_tool():
...
```
The `measuringtools.*` permissions are seeded exactly the way warranty seeds its
own, by adding them to `Permission.PERMISSIONS` in `shopdb/core/models/user.py`:
The `measuringtools.*` permissions belong to the plugin, not to core. The plugin
declares them from the `get_permissions` hook (contract 0.10.0) so core never edits
its catalog to accommodate a plugin:
```python
# Measuring tools
('measuringtools.view', 'View measuring tools', 'measuringtools'),
('measuringtools.create', 'Create measuring tools', 'measuringtools'),
('measuringtools.edit', 'Edit measuring tools', 'measuringtools'),
('measuringtools.delete', 'Delete measuring tools', 'measuringtools'),
class MeasuringToolsPlugin(BasePlugin):
def get_permissions(self):
return [
('measuringtools.view', 'View measuring tools', 'measuringtools'),
('measuringtools.create', 'Create measuring tools', 'measuringtools'),
('measuringtools.edit', 'Edit measuring tools', 'measuringtools'),
('measuringtools.delete', 'Delete measuring tools', 'measuringtools'),
]
```
`flask seed permissions` is idempotent, so re-running it just adds the four new
rows. The `admin` role bypasses every permission check, so an admin can operate the
plugin before anyone grants the granular permissions.
Installing or enabling the plugin seeds these rows automatically, and
`flask seed permissions` (which now seeds core plus every enabled plugin) is
idempotent, so re-running it just adds any missing rows. The `admin` role bypasses
every permission check, so an admin can operate the plugin before anyone grants the
granular permissions. See `get_permissions` in `docs/PLUGIN-HOOKS.md` for the
disabled-plugin edge case.
**Responses use the framework helpers.** `success_response`, `error_response` (with
`ErrorCodes`), and `paginated_response` produce the standard envelope
@@ -666,7 +673,7 @@ When you build a plugin, confirm all of this before you call it done:
- [ ] Imports only from `shopdb.api` and `shopdb.plugins.base` (contract test green).
- [ ] Blueprint: jwt-optional reads, permission-gated writes; framework response and
pagination helpers; audit logs on writes.
- [ ] Permissions added to `Permission.PERMISSIONS`; `flask seed permissions` run.
- [ ] Permissions declared from the `get_permissions` hook; install/enable (or `flask seed permissions`) seeds them.
- [ ] `on_install` seeds the asset type and any reference data, idempotently.
- [ ] Hooks: navigation, reports, models implemented; config schema and collector
implemented or consciously skipped with a reason.

View File

@@ -9,7 +9,7 @@ The contract is locked in [ADR-001](../docs/adr/ADR-001-asset-as-platform-contra
The framework declares its contract version in `shopdb/__init__.py`:
```python
__contract_version__ = '0.9.0'
__contract_version__ = '0.10.0'
```
Each plugin's `manifest.json` declares the range of contract versions it supports:
@@ -216,6 +216,47 @@ Consumed by `GET /api/reports`, which merges plugin cards after the static core
reports sorted into category groups by the frontend (disabled plugins are
skipped; a broken plugin is isolated in prod, re-raised in dev/test).
### `get_permissions() -> List`
Returns the RBAC permissions this plugin owns. Added in contract 0.10.0. A
plugin declares the permission names its own routes enforce via
`require_permission`, instead of core accumulating every plugin's permissions in
one catalog (plugin-is-the-product).
Each entry is a `(name, description, category)` tuple, matching the core
permission catalog shape (dicts with those keys are also accepted). Names follow
the naming convention (lowercase dotted, e.g. `machines.edit`).
```python
class MachinesPlugin(BasePlugin):
def get_permissions(self):
return [
('machines.view', 'View machines', 'machines'),
('machines.create', 'Create machines', 'machines'),
('machines.edit', 'Edit machines', 'machines'),
('machines.delete', 'Delete machines', 'machines'),
]
```
Consumed by the core helper `full_permission_catalog()` (core permissions plus
every ENABLED plugin's `get_permissions()`), which backs three consumers:
- `flask seed permissions` seeds the full catalog.
- The role-management grid (`GET /api/users/permissions`) lists it, grouped by
category.
- API-token scope validation (`ApiToken.unknown_scope_names`) accepts a plugin
permission as a scope only while that plugin is enabled.
Plugin install and enable also seed the plugin's own permissions idempotently,
so enabling a fresh plugin creates its `Permission` rows without a separate seed
pass.
Disabled-plugin edge case: a disabled plugin is skipped by the catalog, so its
permissions are no longer offered for new scope grants or new role assignments.
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_settings_cards() -> List[Dict]`
Returns settings-catalog card definitions. Added in contract 0.7.0 (ADR-010).

View File

@@ -123,6 +123,7 @@ Override hooks on the plugin class as needed. See [PLUGIN-HOOKS.md](PLUGIN-HOOKS
| `get_dashboard_widgets` | Plugin's dashboard widget appears on the home page |
| `get_reports` | Plugin's report cards appear on the Reports hub |
| `get_settings_cards` | Plugin's card joins the settings rail + landing (no `settingsNav.js` edit) |
| `get_permissions` | Plugin's RBAC permissions join the catalog, seeding, role grid, and token scopes |
| `get_asset_panels` | Plugin panel renders on matching asset-detail pages |
| `get_map_overlays` | Plugin decorates shop-floor map markers + adds a legend entry |
| `get_asset_presentation` | Plugin declares its asset type's search icon + detail route |