fix(manager-core): F-S-012 add AppInvoke capability + gate invoke() / invoke_async()
invoke_service::resolve and enqueue_async performed no authz check — no AppInvoke capability existed. Same-app isolation was preserved (cross-app guards work), but within one app an anonymous public-HTTP script could trigger any other script (e.g. an admin-only worker that hits secrets/files/external HTTP). Worse: invoke_async runs the callee with principal: None, so the callee could hold capabilities the original public caller shouldn't. - Add Capability::AppInvoke(AppId). app_id() / scope_for_capability (script:write) / role_satisfies (editor+) are all updated. - InvokeServiceImpl gains an optional `authz: Option<Arc<dyn AuthzRepo>>` + a `with_authz` builder. When set, resolve() runs script_gate on AppInvoke before doing the cross-app id check. - picloud/src/lib.rs wires it: `InvokeServiceImpl::new(...).with_authz(...)`. - Anonymous callers (cx.principal == None) continue to skip the check via script_gate, preserving the public-HTTP convention. Existing 5 invoke_service unit tests still pass (the tests use the authz-less constructor, so the gate is a no-op there). AUDIT.md anchor: F-S-012. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -137,6 +137,15 @@ pub enum Capability {
|
||||
/// seven-scope commitment); the additional gate vs `Write` lives in
|
||||
/// the per-app role chain (`app_admin`+ only).
|
||||
AppUsersAdmin(AppId),
|
||||
/// F-S-012 (v1.1.9+): `invoke()` / `invoke_async()` synchronously
|
||||
/// trigger another script in the same app. Same-app isolation is
|
||||
/// already enforced (cross-app calls are rejected), but within one
|
||||
/// app an anonymous public-HTTP script could otherwise trigger any
|
||||
/// other script — including ones that hold capabilities the
|
||||
/// original caller shouldn't. Gate authenticated callers on
|
||||
/// AppInvoke; anonymous callers continue to skip the check under
|
||||
/// the script-as-gate convention.
|
||||
AppInvoke(AppId),
|
||||
}
|
||||
|
||||
impl Capability {
|
||||
@@ -171,7 +180,8 @@ impl Capability {
|
||||
| Self::AppTopicManage(id)
|
||||
| Self::AppUsersRead(id)
|
||||
| Self::AppUsersWrite(id)
|
||||
| Self::AppUsersAdmin(id) => Some(id),
|
||||
| Self::AppUsersAdmin(id)
|
||||
| Self::AppInvoke(id) => Some(id),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +212,8 @@ impl Capability {
|
||||
| Self::AppSecretsWrite(_)
|
||||
| Self::AppEmailSend(_)
|
||||
| Self::AppUsersWrite(_)
|
||||
| Self::AppUsersAdmin(_) => Scope::ScriptWrite,
|
||||
| Self::AppUsersAdmin(_)
|
||||
| Self::AppInvoke(_) => Scope::ScriptWrite,
|
||||
Self::AppWriteRoute(_) => Scope::RouteWrite,
|
||||
Self::AppManageDomains(_) => Scope::DomainManage,
|
||||
Self::AppAdmin(_)
|
||||
@@ -401,6 +412,7 @@ const fn role_satisfies(role: AppRole, cap: Capability) -> bool {
|
||||
| Capability::AppSecretsWrite(_)
|
||||
| Capability::AppEmailSend(_)
|
||||
| Capability::AppUsersWrite(_)
|
||||
| Capability::AppInvoke(_)
|
||||
);
|
||||
let in_app_admin = in_editor
|
||||
|| matches!(
|
||||
|
||||
Reference in New Issue
Block a user