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:
63
crates/manager-core/src/vars_service.rs
Normal file
63
crates/manager-core/src/vars_service.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
//! `VarsService` — the runtime read path for group-inherited config.
|
||||
//!
|
||||
//! Resolves the calling app's config (own rows + inherited group rows,
|
||||
//! env-filtered, proximity-first; see `config_resolver`) and exposes it to
|
||||
//! scripts as `vars::get(key)` / `vars::all()`. Read-only from scripts;
|
||||
//! writes go through the admin API. Like every SDK service, it derives the
|
||||
//! app from `cx.app_id` — never a script argument — so cross-app isolation
|
||||
//! holds.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use picloud_shared::{SdkCallCx, VarsError, VarsService};
|
||||
use serde_json::Value;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::authz::{self, AuthzRepo, Capability};
|
||||
use crate::config_resolver::{fetch_var_candidates, resolve};
|
||||
|
||||
pub struct VarsServiceImpl {
|
||||
pool: PgPool,
|
||||
authz: Arc<dyn AuthzRepo>,
|
||||
}
|
||||
|
||||
impl VarsServiceImpl {
|
||||
#[must_use]
|
||||
pub fn new(pool: PgPool, authz: Arc<dyn AuthzRepo>) -> Self {
|
||||
Self { pool, authz }
|
||||
}
|
||||
|
||||
/// Authed principals need `AppVarsRead`; anonymous public-HTTP scripts
|
||||
/// (`principal: None`) read freely under script-as-gate semantics.
|
||||
async fn check_read(&self, cx: &SdkCallCx) -> Result<(), VarsError> {
|
||||
if let Some(ref principal) = cx.principal {
|
||||
authz::require(&*self.authz, principal, Capability::AppVarsRead(cx.app_id))
|
||||
.await
|
||||
.map_err(|_| VarsError::Forbidden)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn resolved(&self, cx: &SdkCallCx) -> Result<BTreeMap<String, Value>, VarsError> {
|
||||
let candidates = fetch_var_candidates(&self.pool, cx.app_id)
|
||||
.await
|
||||
.map_err(|e| VarsError::Backend(e.to_string()))?;
|
||||
let (values, _provenance) = resolve(candidates);
|
||||
Ok(values)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl VarsService for VarsServiceImpl {
|
||||
async fn get(&self, cx: &SdkCallCx, key: &str) -> Result<Option<Value>, VarsError> {
|
||||
self.check_read(cx).await?;
|
||||
Ok(self.resolved(cx).await?.remove(key))
|
||||
}
|
||||
|
||||
async fn all(&self, cx: &SdkCallCx) -> Result<BTreeMap<String, Value>, VarsError> {
|
||||
self.check_read(cx).await?;
|
||||
self.resolved(cx).await
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user