feat(v1.1.7-secrets): secrets SDK + table + admin API + dashboard
Encrypted per-app secrets, reachable from scripts as
secrets::{get,set,delete,list}(name) and managed from the dashboard
Secrets tab. Values are AES-256-GCM-sealed with the process master key
(picloud_shared::crypto) before they touch Postgres; the repo only ever
sees ciphertext + nonce. JSON round-trip preserves Rhai types.
- migration 0023_secrets.sql (PRIMARY KEY (app_id, name)).
- SecretsService trait (picloud-shared) + SecretsServiceImpl + repo
(manager-core), wired into the Services bundle and Rhai engine.
- Capability::AppSecretsRead/Write (→ script:read / script:write); no
new Scope variants (seven-scope commitment).
- Admin API GET/POST/DELETE /apps/{id}/secrets (list returns names +
updated_at, never values).
- build_app now takes a MasterKey, sourced from PICLOUD_SECRET_KEY in
main.rs; test callers pass a fixed test key.
- 64 KB value cap (PICLOUD_SECRET_MAX_VALUE_BYTES); no ServiceEvent
emission (secret writes don't fire triggers, by design).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -89,6 +89,14 @@ pub enum Capability {
|
||||
/// (v1.1.5). Maps to `script:write` on API keys (a publish is a
|
||||
/// write that fans out to subscribers). Granted to `editor`+.
|
||||
AppPubsubPublish(AppId),
|
||||
/// Read a decrypted secret from this app's secrets store (v1.1.7).
|
||||
/// Same trust shape as KV/docs/files read — granted to `viewer`+,
|
||||
/// maps to `script:read` on API keys. Honors the seven-scope
|
||||
/// commitment.
|
||||
AppSecretsRead(AppId),
|
||||
/// 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),
|
||||
/// Create / list / delete triggers for this app (v1.1.1). Maps to
|
||||
/// `app:admin` on API keys — triggers are app-configuration acts
|
||||
/// rather than data-plane access. Granted to `app_admin`+.
|
||||
@@ -128,6 +136,8 @@ impl Capability {
|
||||
| Self::AppFilesRead(id)
|
||||
| Self::AppFilesWrite(id)
|
||||
| Self::AppPubsubPublish(id)
|
||||
| Self::AppSecretsRead(id)
|
||||
| Self::AppSecretsWrite(id)
|
||||
| Self::AppManageTriggers(id)
|
||||
| Self::AppDeadLetterManage(id)
|
||||
| Self::AppTopicManage(id) => Some(id),
|
||||
@@ -148,13 +158,15 @@ impl Capability {
|
||||
Self::AppRead(_)
|
||||
| Self::AppKvRead(_)
|
||||
| Self::AppDocsRead(_)
|
||||
| Self::AppFilesRead(_) => Scope::ScriptRead,
|
||||
| Self::AppFilesRead(_)
|
||||
| Self::AppSecretsRead(_) => Scope::ScriptRead,
|
||||
Self::AppWriteScript(_)
|
||||
| Self::AppKvWrite(_)
|
||||
| Self::AppDocsWrite(_)
|
||||
| Self::AppHttpRequest(_)
|
||||
| Self::AppFilesWrite(_)
|
||||
| Self::AppPubsubPublish(_) => Scope::ScriptWrite,
|
||||
| Self::AppPubsubPublish(_)
|
||||
| Self::AppSecretsWrite(_) => Scope::ScriptWrite,
|
||||
Self::AppWriteRoute(_) => Scope::RouteWrite,
|
||||
Self::AppManageDomains(_) => Scope::DomainManage,
|
||||
Self::AppAdmin(_)
|
||||
@@ -305,6 +317,7 @@ const fn role_satisfies(role: AppRole, cap: Capability) -> bool {
|
||||
| Capability::AppKvRead(_)
|
||||
| Capability::AppDocsRead(_)
|
||||
| Capability::AppFilesRead(_)
|
||||
| Capability::AppSecretsRead(_)
|
||||
);
|
||||
let in_editor = in_viewer
|
||||
|| matches!(
|
||||
@@ -316,6 +329,7 @@ const fn role_satisfies(role: AppRole, cap: Capability) -> bool {
|
||||
| Capability::AppHttpRequest(_)
|
||||
| Capability::AppFilesWrite(_)
|
||||
| Capability::AppPubsubPublish(_)
|
||||
| Capability::AppSecretsWrite(_)
|
||||
);
|
||||
let in_app_admin = in_editor
|
||||
|| matches!(
|
||||
|
||||
Reference in New Issue
Block a user