diff --git a/plugins/example/manifest.ts b/plugins/example/manifest.ts index 63aef46b..1ea71cdc 100644 --- a/plugins/example/manifest.ts +++ b/plugins/example/manifest.ts @@ -30,7 +30,6 @@ export const manifest: PluginManifest = { key: 'example', label: 'Example', description: 'The reference plugin', - ownerOnly: false, }, ], }; diff --git a/plugins/offscale/manifest.ts b/plugins/offscale/manifest.ts index a08d3a87..a1c34360 100644 --- a/plugins/offscale/manifest.ts +++ b/plugins/offscale/manifest.ts @@ -22,21 +22,22 @@ export const manifest: PluginManifest = { icon: 'Network', color: '#818cf8', - // One permission gating the whole surface. + // One permission gating the whole surface, grantable per role at read or write like every other. // - // `ownerOnly` because the credential behind it is a Headscale ADMIN api key that can delete every node - // on a tailnet, and there is no read-only version of it. A read grant would still be reading through - // that key; the protection is that non-owners cannot reach the routes at all. + // `[open]` What a member's grant MEANS here is this plugin's own job and is not finished. The queries + // still scope by the caller (`listHeadscaleServers(userId)`), so a granted member would see their own + // empty server list rather than the owner's, and could register a Headscale of their own. The model in + // docs/offscale-plugin.md is one shared resource: read sees what the owner sees, write can change it. + // That is a change inside these queries, not a flag on the manifest. // - // Read/write for members is the model recorded in docs/offscale-plugin.md and deliberately not enabled - // here yet: it needs the queries to resolve to the OWNER's rows rather than the caller's, which is a - // change inside this plugin and not a flag. + // Worth knowing while it is unfinished: the stored credential is a Headscale ADMIN api key that can + // delete every node on a tailnet, and there is no read-only version of it — so `write` here is close to + // full control of the tailnet, which is the owner's decision to make deliberately. permissions: [ { key: 'offscale', label: 'Offscale', description: 'The tailnet: machines, routes, keys and ACLs', - ownerOnly: true, // Two POSTs that are really reads — a reachability probe and a policy DRAFT that never saves. // Without declaring them a read-level account meets a broken feature where a withheld permission // should be. Inert while ownerOnly, and correct the moment that changes. diff --git a/src/servers/plugins/manifest.test.ts b/src/servers/plugins/manifest.test.ts index e380b50e..d1aa44ba 100644 --- a/src/servers/plugins/manifest.test.ts +++ b/src/servers/plugins/manifest.test.ts @@ -9,7 +9,7 @@ const valid = (over: Partial = {}): PluginManifest => ({ summary: 'Your tailnet', icon: 'Network', color: '#818cf8', - permissions: [{ key: 'offscale', label: 'Offscale', description: 'The tailnet', ownerOnly: true }], + permissions: [{ key: 'offscale', label: 'Offscale', description: 'The tailnet' }], ...over, }); diff --git a/src/servers/plugins/manifest.ts b/src/servers/plugins/manifest.ts index f96dc636..a8764d62 100644 --- a/src/servers/plugins/manifest.ts +++ b/src/servers/plugins/manifest.ts @@ -24,14 +24,16 @@ export type PluginPermission = { key: string; label: string; description: string; - /** - * Owner-only, or grantable to members. The whole distinction a plugin needs. - * - * The platform's own `CapabilityKind` has five values because the PLATFORM has five sorts of surface. - * A plugin has two states, so this is a boolean — which also removes the escalation question rather - * than answering it: a plugin cannot claim `core` if `core` is not a word it can say. - */ - ownerOnly?: boolean; + // There is deliberately NO `ownerOnly`, and no kind of any sort. + // + // Every plugin permission is grantable, per role, at read or write — the same configuration as every + // core capability, on the same page, with no special cases to learn. A plugin that wanted to be + // owner-only would be a plugin the owner cannot delegate, and "which members may reach this" is a + // decision that belongs to the person running the server rather than to the person who wrote the code. + // + // Finer visibility — whose rows a member sees, what a read means for this plugin's data — is the + // PLUGIN's business and lives in its own queries. The platform's answer is uniform: read, write, or + // nothing. See docs/offscale-plugin.md. /** * Requests that look like writes and are not — `POST /ssh-test` probes, `POST /policy/assist` proposes * a document and never saves one. Without declaring them, a read-level account meets what reads as a diff --git a/src/servers/plugins/mount.ts b/src/servers/plugins/mount.ts index 7ba5d6bd..48064f85 100644 --- a/src/servers/plugins/mount.ts +++ b/src/servers/plugins/mount.ts @@ -89,9 +89,10 @@ export async function mountablePlugins(snapshot: PluginsSnapshot): Promise