permissions: a plugin can mark part of its surface owner-only
A plugin worth granting to a Member can also hold the credential the rest of it runs on. Jellyfin's /_config stores a server's admin token and drives account provisioning; the rest of the plugin is exactly what a Member should have. The choice was all of it or none of it. `ownerOnly` on a plugin permission takes sub-paths relative to the mount prefix. The permission stays fully grantable — this is not the ownerOnly PERMISSION that manifest.ts refuses, and that refusal still stands: a permission nobody can delegate is a contradiction. This says a few paths administer the plugin rather than use it. Declared by the plugin, enforced by the platform. A plugin could check this itself and the first one would; the tenth would add a route and forget, with no boot failure and no warning — the shape assertPermissionTotality exists to prevent. In the gate it is enforced once, on the door they all pass through, and a route added later by someone who never read the manifest is covered by default. Checked before selfService and before kind. A path that is somehow both self-service and owner-only is a contradiction, and the safe reading has to win. A write grant does not unlock it either — level is not the question this answers. Also forwards X-Officer-Owner and X-Officer-Level to a sidecar whose proxy names its permission key, for the finer decisions the platform cannot make for it: whose rows a read returns, which fields a level may see. Facts, never the role name — a plugin branching on "Super Admin" would give every plugin repository an opinion about the platform's role vocabulary, and renaming a role would change authorization in code the platform does not own. These are safe to trust only because the proxy copies a three-item allowlist off the inbound request; that is now written where it would be changed. The owner's level is reported as write rather than the absent 'none': they hold everything implicitly and appear in no grants map, so the raw lookup inverts the meaning for the one account that can do anything. No plugin declares one yet. Jellyfin cannot until connections stop being per-user — locking /_config today leaves a Member with a plugin that only shows a connect form they may not submit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
# Plugin route authorization
|
||||
|
||||
**Design, not built.** Agreed 2026-08-21, deferred deliberately. Nothing here is implemented.
|
||||
**Platform half built 2026-08-21.** The gate enforces `ownerOnly`, the manifest accepts it, and the proxy
|
||||
forwards `X-Officer-Owner` / `X-Officer-Level`. **No plugin declares it yet** — jellyfin's `/_config` is
|
||||
still reachable by anyone granted the plugin, and cannot be locked until the credential question at the
|
||||
bottom of this file is answered.
|
||||
|
||||
The question: a plugin's **settings** routes should be reachable only by the owner, even when the plugin
|
||||
itself is granted to other accounts. Jellyfin is the case that raised it — connecting a server, storing
|
||||
@@ -79,9 +82,13 @@ forgets one route, and nothing anywhere notices — no boot failure, no warning,
|
||||
So the recommendation is not "platform enforces instead". It is **declare in the manifest, enforce in the
|
||||
proxy, and pass the facts to the sidecar as well**:
|
||||
|
||||
Built as a field on the permission rather than at the manifest root, following `readOnlyWrites` — both
|
||||
are sub-paths written relative to the plugin's prefix with a rule attached, and one precedent is better
|
||||
than two shapes:
|
||||
|
||||
```ts
|
||||
// manifest.ts
|
||||
ownerOnlyPaths: ['/_config'],
|
||||
permissions: [{ key: 'jellyfin', label: 'Jellyfin', description: '…', ownerOnly: ['/_config'] }],
|
||||
```
|
||||
|
||||
- **Declared by the plugin**, because the plugin knows which of its paths are administrative.
|
||||
@@ -121,11 +128,13 @@ guard is a few lines; this is the work.
|
||||
|
||||
## Status
|
||||
|
||||
| | |
|
||||
| ----------- | ---------------------------------------------------------------------------------- |
|
||||
| Agreed | plugin decides what a level means inside its own surface |
|
||||
| Agreed | pass `isOwner` and the effective level; never the role name |
|
||||
| Agreed | fail closed when the headers are absent |
|
||||
| Proposed | `ownerOnlyPaths` in the manifest, enforced by the proxy — pending the owner's call |
|
||||
| Blocked on | credential resolution for non-owners, above |
|
||||
| Not started | all of it |
|
||||
| | |
|
||||
| ----------- | ------------------------------------------------------------------------------------- |
|
||||
| Agreed | plugin decides what a level means inside its own surface |
|
||||
| Agreed | pass `isOwner` and the effective level; never the role name |
|
||||
| Agreed | fail closed when the headers are absent |
|
||||
| Built | `ownerOnly` on a plugin permission, enforced by the gate before self-service and kind |
|
||||
| Built | `X-Officer-Owner` and `X-Officer-Level`, sent when a proxy names its permission key |
|
||||
| Built | manifest validation — a sub-path without a leading slash is a manifest error |
|
||||
| Blocked on | credential resolution for non-owners, above |
|
||||
| Not started | any plugin actually declaring one |
|
||||
|
||||
Reference in New Issue
Block a user