feat(vars): VarsService + vars:: SDK + config capabilities
Scripts can now read group-inherited config via vars::get(key) / vars::all(). - shared: VarsService trait (read-only get/all) + NoopVarsService; added as the 14th field on the Services bundle. - manager-core: VarsServiceImpl resolves the calling app's config via config_resolver (derives app_id from cx.app_id; AppVarsRead-gated for authed principals, script-as-gate for anon). - executor-core: vars:: Rhai bridge (get/all), registered in the SDK. - authz: AppVarsRead/Write, GroupVarsRead/Write, GroupSecretsRead/Write capabilities (group caps resolve via the Phase-2 ancestor walk; the GroupSecretsRead human-read gate is group_admin-only, distinct from an app's runtime injection). - wired VarsServiceImpl into the picloud binary; updated the executor-core test Services::new call sites. 386 manager-core lib tests green; clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -116,6 +116,25 @@ pub enum Capability {
|
||||
/// Write (set/delete) a secret in this app's secrets store (v1.1.7).
|
||||
/// Granted to `editor`+, maps to `script:write` on API keys.
|
||||
AppSecretsWrite(AppId),
|
||||
/// Read this app's resolved config vars (Phase 3). Same trust shape as
|
||||
/// secrets-read — granted to `viewer`+, maps to `script:read`.
|
||||
AppVarsRead(AppId),
|
||||
/// Write (set/delete) an app-owned config var (Phase 3). Granted to
|
||||
/// `editor`+, maps to `script:write`.
|
||||
AppVarsWrite(AppId),
|
||||
/// Read a group's config vars (Phase 3). Resolved via the group
|
||||
/// ancestor walk; viewer+ on the group.
|
||||
GroupVarsRead(GroupId),
|
||||
/// Write (set/delete) a group-owned config var (Phase 3). editor+ on
|
||||
/// the group.
|
||||
GroupVarsWrite(GroupId),
|
||||
/// Read a group-owned secret's VALUE (Phase 3, the human-read gate).
|
||||
/// group_admin on the owning group — distinct from runtime injection,
|
||||
/// which an inheriting app does without this check.
|
||||
GroupSecretsRead(GroupId),
|
||||
/// Write (set/delete) a group-owned secret (Phase 3). editor+ on the
|
||||
/// group.
|
||||
GroupSecretsWrite(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`+.
|
||||
@@ -176,7 +195,11 @@ impl Capability {
|
||||
// app) is denied group management at the binding layer.
|
||||
| Self::GroupRead(_)
|
||||
| Self::GroupWrite(_)
|
||||
| Self::GroupAdmin(_) => None,
|
||||
| Self::GroupAdmin(_)
|
||||
| Self::GroupVarsRead(_)
|
||||
| Self::GroupVarsWrite(_)
|
||||
| Self::GroupSecretsRead(_)
|
||||
| Self::GroupSecretsWrite(_) => None,
|
||||
Self::AppRead(id)
|
||||
| Self::AppWriteScript(id)
|
||||
| Self::AppWriteRoute(id)
|
||||
@@ -194,6 +217,8 @@ impl Capability {
|
||||
| Self::AppQueueEnqueue(id)
|
||||
| Self::AppSecretsRead(id)
|
||||
| Self::AppSecretsWrite(id)
|
||||
| Self::AppVarsRead(id)
|
||||
| Self::AppVarsWrite(id)
|
||||
| Self::AppEmailSend(id)
|
||||
| Self::AppManageTriggers(id)
|
||||
| Self::AppDeadLetterManage(id)
|
||||
@@ -223,7 +248,9 @@ impl Capability {
|
||||
| Self::AppFilesRead(_)
|
||||
| Self::AppSecretsRead(_)
|
||||
| Self::AppUsersRead(_)
|
||||
| Self::GroupRead(_) => Scope::ScriptRead,
|
||||
| Self::AppVarsRead(_)
|
||||
| Self::GroupRead(_)
|
||||
| Self::GroupVarsRead(_) => Scope::ScriptRead,
|
||||
Self::AppWriteScript(_)
|
||||
| Self::AppKvWrite(_)
|
||||
| Self::AppDocsWrite(_)
|
||||
@@ -235,6 +262,7 @@ impl Capability {
|
||||
| Self::AppEmailSend(_)
|
||||
| Self::AppUsersWrite(_)
|
||||
| Self::AppUsersAdmin(_)
|
||||
| Self::AppVarsWrite(_)
|
||||
| Self::AppInvoke(_) => Scope::ScriptWrite,
|
||||
Self::AppWriteRoute(_) => Scope::RouteWrite,
|
||||
Self::AppManageDomains(_) => Scope::DomainManage,
|
||||
@@ -243,7 +271,10 @@ impl Capability {
|
||||
| Self::AppDeadLetterManage(_)
|
||||
| Self::AppTopicManage(_)
|
||||
| Self::GroupWrite(_)
|
||||
| Self::GroupAdmin(_) => Scope::AppAdmin,
|
||||
| Self::GroupAdmin(_)
|
||||
| Self::GroupVarsWrite(_)
|
||||
| Self::GroupSecretsRead(_)
|
||||
| Self::GroupSecretsWrite(_) => Scope::AppAdmin,
|
||||
Self::AppLogRead(_) => Scope::LogRead,
|
||||
}
|
||||
}
|
||||
@@ -412,7 +443,13 @@ async fn role_grants(
|
||||
// walk (a group_admin on an ancestor is implicitly admin of
|
||||
// the descendant group). Routed before member_grants because
|
||||
// group caps carry no app_id.
|
||||
Capability::GroupRead(g) | Capability::GroupWrite(g) | Capability::GroupAdmin(g) => {
|
||||
Capability::GroupRead(g)
|
||||
| Capability::GroupWrite(g)
|
||||
| Capability::GroupAdmin(g)
|
||||
| Capability::GroupVarsRead(g)
|
||||
| Capability::GroupVarsWrite(g)
|
||||
| Capability::GroupSecretsRead(g)
|
||||
| Capability::GroupSecretsWrite(g) => {
|
||||
group_member_grants(repo, principal.user_id, cap, g).await
|
||||
}
|
||||
// Creating a root-level group is an instance act — members
|
||||
@@ -472,9 +509,19 @@ 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 {
|
||||
Capability::GroupRead(_) => true, // any role can read
|
||||
Capability::GroupWrite(_) => matches!(role, AppRole::Editor | AppRole::AppAdmin),
|
||||
Capability::GroupAdmin(_) => matches!(role, AppRole::AppAdmin),
|
||||
// viewer+ reads group metadata and config vars.
|
||||
Capability::GroupRead(_) | Capability::GroupVarsRead(_) => true,
|
||||
// editor+ writes config vars/secrets.
|
||||
Capability::GroupWrite(_)
|
||||
| Capability::GroupVarsWrite(_)
|
||||
| Capability::GroupSecretsWrite(_) => {
|
||||
matches!(role, AppRole::Editor | AppRole::AppAdmin)
|
||||
}
|
||||
// group_admin manages the group + reads secret VALUES (the
|
||||
// human-read gate, distinct from an app's runtime injection).
|
||||
Capability::GroupAdmin(_) | Capability::GroupSecretsRead(_) => {
|
||||
matches!(role, AppRole::AppAdmin)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -493,6 +540,7 @@ const fn role_satisfies(role: AppRole, cap: Capability) -> bool {
|
||||
| Capability::AppFilesRead(_)
|
||||
| Capability::AppSecretsRead(_)
|
||||
| Capability::AppUsersRead(_)
|
||||
| Capability::AppVarsRead(_)
|
||||
);
|
||||
let in_editor = in_viewer
|
||||
|| matches!(
|
||||
@@ -508,6 +556,7 @@ const fn role_satisfies(role: AppRole, cap: Capability) -> bool {
|
||||
| Capability::AppSecretsWrite(_)
|
||||
| Capability::AppEmailSend(_)
|
||||
| Capability::AppUsersWrite(_)
|
||||
| Capability::AppVarsWrite(_)
|
||||
| Capability::AppInvoke(_)
|
||||
);
|
||||
let in_app_admin = in_editor
|
||||
|
||||
Reference in New Issue
Block a user