feat(secrets): group-secrets admin API, masked read, config/effective

Adds the Phase-3 admin surface on top of the group-secrets storage:

* `secrets_api` gains group routes under `/groups/{id}/secrets`
  (set/list/delete, env-scoped) gated `GroupSecretsWrite` (editor+), plus
  the ONE plaintext endpoint `GET /groups/{id}/secrets/{name}/value` gated
  `GroupSecretsRead` (group_admin only). That is the masked-secret
  boundary: a descendant app's dev sees a group secret EXISTS and consumes
  it at runtime via `secrets::get`, but only a reader at the OWNING group
  gets the value. App secrets stay env-agnostic (a stray `env` is rejected).
  The owner is resolved first, then the capability binds to the resolved
  id — never a path param.

* `config_api`: `GET /apps/{id}/config/effective` (gated `AppVarsRead`)
  returns the resolved view a dev would get — every inherited var with its
  value + provenance, and every inherited secret MASKED (name/owner/scope,
  never the value). Backed by a new `fetch_effective_secret_meta`
  (DISTINCT-ON nearest-wins, same ordering as the per-name resolver).

* authz: `GroupSecretsWrite` moves from `app:admin` to `script:write`
  scope so its API-key scope matches its editor role tier (closing the
  latent scope/role mismatch the checkpoint review flagged); the value
  read `GroupSecretsRead` stays at `app:admin`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-24 22:00:32 +02:00
parent e6b4792389
commit c914758c09
6 changed files with 505 additions and 72 deletions

View File

@@ -31,6 +31,7 @@ pub mod auth_api;
pub mod auth_bootstrap;
pub mod auth_middleware;
pub mod authz;
pub mod config_api;
pub mod config_resolver;
pub mod cron_scheduler;
pub mod dead_letter_repo;
@@ -150,6 +151,7 @@ pub use auth_middleware::{
API_KEY_PREFIX, API_KEY_PREFIX_LEN,
};
pub use authz::{can, require, AuthzDenied, AuthzError, AuthzRepo, Capability, Decision};
pub use config_api::{config_router, ConfigApiError, ConfigApiState};
pub use cron_scheduler::spawn_cron_scheduler;
pub use dead_letter_repo::{
DeadLetterRepo, DeadLetterRepoError, DeadLetterRow, NewDeadLetter, PostgresDeadLetterRepo,