feat(scripts): polymorphic script owner (app XOR group) — Phase 4 foundation
Phase 4-lite C1. Make script ownership polymorphic so a script can be owned
by a GROUP (a template inherited by descendant apps) instead of an app —
mirroring vars/secrets (0048/0049), but ON DELETE RESTRICT (code is not data).
Schema (0050): `scripts.group_id` (nullable FK→groups RESTRICT), `app_id` made
nullable, `scripts_owner_exactly_one` CHECK, and the per-app name index split
into two per-owner partial-unique indexes. Existing app-owned rows keep their
exact `(app_id, lower(name))` uniqueness.
Type: `Script.app_id` becomes `Option<AppId>`; add `Script.group_id` +
`ScriptOwner` / `is_owned_by_app()`. `NewScript` gains the same polymorphic
owner. The execution-context app (what a script runs *under*) is supplied by
the invoking route/trigger/caller, never read off the script — group scripts
have no single app.
Behavior is fully preserved for app-owned scripts (the only kind creatable
today): every isolation backstop and authz site now uses `is_owned_by_app`,
which is byte-identical for app owners and **fails closed** for group owners.
A group script therefore can't yet be run, route/trigger-bound, invoked, or
managed via the app-script API — those land in C2 (group-script creation) and
C3 (chain-membership resolution + binding). The `/execute/{id}` bypass 404s a
group script (no app context to run under).
Re-blesses expected_schema.txt; note the golden was last blessed at migration
0044, so this also captures the already-committed 0045–0049 schema.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -289,7 +289,10 @@ async fn validate_trigger_target(
|
||||
.ok_or_else(|| {
|
||||
TriggersApiError::Invalid(format!("script {script_id} not found in this app"))
|
||||
})?;
|
||||
if script.app_id != app_id {
|
||||
// Phase 4: trigger targets are app-owned scripts; a group-owned script
|
||||
// (`app_id: None`) fails closed here until group-script trigger binding
|
||||
// lands in C3.
|
||||
if !script.is_owned_by_app(app_id) {
|
||||
return Err(TriggersApiError::Invalid(format!(
|
||||
"script {script_id} does not belong to this app"
|
||||
)));
|
||||
@@ -1348,7 +1351,8 @@ mod tests {
|
||||
script_id,
|
||||
picloud_shared::Script {
|
||||
id: script_id,
|
||||
app_id,
|
||||
app_id: Some(app_id),
|
||||
group_id: None,
|
||||
name: format!(
|
||||
"{}_{}",
|
||||
match kind {
|
||||
@@ -1393,7 +1397,7 @@ mod tests {
|
||||
.lock()
|
||||
.await
|
||||
.values()
|
||||
.find(|s| s.app_id == app_id && s.name == name)
|
||||
.find(|s| s.app_id == Some(app_id) && s.name == name)
|
||||
.cloned())
|
||||
}
|
||||
async fn list(
|
||||
|
||||
Reference in New Issue
Block a user