feat(scripts): group-owned script admin API + pic scripts … --group
Phase 4-lite C2. Lets a group own scripts (templates inherited by descendant
apps), with the create/list/manage surface — but not yet the inherited
resolution (binding + runtime), which is C3.
Capabilities: add `GroupScriptsRead` (viewer+ on the group → script:read) and
`GroupScriptsWrite` (editor+ → script:write), mirroring the group-vars tier and
resolved through the same group-ancestor walk.
API:
* New `group_scripts_api`: `POST/GET /groups/{id}/scripts` — create a
group-owned endpoint script and list a group's own (non-inherited) rows.
Phase 4-lite is endpoint-only and self-contained: `kind=module` and any
`import` are rejected (group modules + the lexical resolver are Phase 4b).
Owner resolved first (slug-or-uuid); capability bound to the resolved id.
* The by-id `/scripts/{id}` get/update/delete/logs handlers are now
owner-polymorphic via a `script_cap` helper: app-owned scripts gate on
`App*` exactly as before; group-owned on `GroupScripts*`. This is what
makes `deploy --group` idempotent (update reuses the by-id PUT) and lets a
group script be deleted by id. (C1 had these fail closed for groups.)
Repo: `list_for_group(group_id)` (the group's own rows).
CLI: `pic scripts ls --group <g>`, `pic scripts deploy --group <g>` (create or
update by name; `--app`/`--group` are mutually exclusive, exactly one
required). `pic scripts delete <id>` already works for group scripts via the
owner-polymorphic by-id route.
Live-validated against the dev DB: create → update (v2 via the by-id PUT) →
list → delete, plus module rejection and the polymorphic row shape
(`app_id NULL`, `group_id` set). Group scripts still can't be routed/triggered
or invoked — that lands in C3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -135,6 +135,13 @@ pub enum Capability {
|
||||
/// Write (set/delete) a group-owned secret (Phase 3). editor+ on the
|
||||
/// group.
|
||||
GroupSecretsWrite(GroupId),
|
||||
/// Read (get / list) group-owned scripts (Phase 4). Resolved via the
|
||||
/// group ancestor walk; viewer+ on the group. Maps to `script:read`.
|
||||
GroupScriptsRead(GroupId),
|
||||
/// Create / update / delete a group-owned script (Phase 4). editor+ on
|
||||
/// the group; maps to `script:write` — the same tier as `AppWriteScript`
|
||||
/// for an app, lifted to the group owner.
|
||||
GroupScriptsWrite(GroupId),
|
||||
/// Send an outbound email from a script in this app (v1.1.7). Maps
|
||||
/// to `script:write` on API keys (sending mail is an outbound
|
||||
/// side-effect like an HTTP request). Granted to `editor`+.
|
||||
@@ -199,7 +206,9 @@ impl Capability {
|
||||
| Self::GroupVarsRead(_)
|
||||
| Self::GroupVarsWrite(_)
|
||||
| Self::GroupSecretsRead(_)
|
||||
| Self::GroupSecretsWrite(_) => None,
|
||||
| Self::GroupSecretsWrite(_)
|
||||
| Self::GroupScriptsRead(_)
|
||||
| Self::GroupScriptsWrite(_) => None,
|
||||
Self::AppRead(id)
|
||||
| Self::AppWriteScript(id)
|
||||
| Self::AppWriteRoute(id)
|
||||
@@ -250,7 +259,8 @@ impl Capability {
|
||||
| Self::AppUsersRead(_)
|
||||
| Self::AppVarsRead(_)
|
||||
| Self::GroupRead(_)
|
||||
| Self::GroupVarsRead(_) => Scope::ScriptRead,
|
||||
| Self::GroupVarsRead(_)
|
||||
| Self::GroupScriptsRead(_) => Scope::ScriptRead,
|
||||
Self::AppWriteScript(_)
|
||||
| Self::AppKvWrite(_)
|
||||
| Self::AppDocsWrite(_)
|
||||
@@ -268,6 +278,7 @@ impl Capability {
|
||||
// not app:admin. Only the VALUE READ (GroupSecretsRead) sits at
|
||||
// the admin tier below.
|
||||
| Self::GroupSecretsWrite(_)
|
||||
| Self::GroupScriptsWrite(_)
|
||||
| Self::AppInvoke(_) => Scope::ScriptWrite,
|
||||
Self::AppWriteRoute(_) => Scope::RouteWrite,
|
||||
Self::AppManageDomains(_) => Scope::DomainManage,
|
||||
@@ -453,7 +464,9 @@ async fn role_grants(
|
||||
| Capability::GroupVarsRead(g)
|
||||
| Capability::GroupVarsWrite(g)
|
||||
| Capability::GroupSecretsRead(g)
|
||||
| Capability::GroupSecretsWrite(g) => {
|
||||
| Capability::GroupSecretsWrite(g)
|
||||
| Capability::GroupScriptsRead(g)
|
||||
| Capability::GroupScriptsWrite(g) => {
|
||||
group_member_grants(repo, principal.user_id, cap, g).await
|
||||
}
|
||||
// Creating a root-level group is an instance act — members
|
||||
@@ -513,12 +526,15 @@ async fn group_member_grants(
|
||||
/// viewer→read, editor→write, group_admin(=AppAdmin)→admin.
|
||||
const fn group_role_satisfies(role: AppRole, cap: Capability) -> bool {
|
||||
match cap {
|
||||
// viewer+ reads group metadata and config vars.
|
||||
Capability::GroupRead(_) | Capability::GroupVarsRead(_) => true,
|
||||
// editor+ writes config vars/secrets.
|
||||
// viewer+ reads group metadata, config vars, and scripts.
|
||||
Capability::GroupRead(_)
|
||||
| Capability::GroupVarsRead(_)
|
||||
| Capability::GroupScriptsRead(_) => true,
|
||||
// editor+ writes config vars/secrets/scripts.
|
||||
Capability::GroupWrite(_)
|
||||
| Capability::GroupVarsWrite(_)
|
||||
| Capability::GroupSecretsWrite(_) => {
|
||||
| Capability::GroupSecretsWrite(_)
|
||||
| Capability::GroupScriptsWrite(_) => {
|
||||
matches!(role, AppRole::Editor | AppRole::AppAdmin)
|
||||
}
|
||||
// group_admin manages the group + reads secret VALUES (the
|
||||
|
||||
Reference in New Issue
Block a user