Files
PiCloud/crates/shared/src/lib.rs
MechaCat02 d08df88df5 fix(interceptors): harden the §9.4 slice — seal, fail-closed, re-entrancy, shared coverage
An end-to-end audit of the (unmerged) interceptor slice surfaced seven ways a
KV allow/deny guard could be silently defeated or misbehave. Close all of them:

1. Shared-collection bypass — `kv::shared_collection(...).set/delete` skipped
   the hook entirely, so any `(kv, set/delete)` guard was circumvented by
   choosing the shared handle. `GroupKvHandle` now runs the same before-op hook.

2. Seal to the declaring owner — the marker resolved nearest-owner-wins but its
   script name was then re-resolved on the CALLER's chain, so a descendant app
   could shadow a group's mandatory guard with a same-named local script.
   `resolve_before` now returns the script sealed to the owner that declared the
   marker (one LEFT JOIN in manager-core), so the executor never re-resolves by
   name. A descendant can only override with its OWN explicit marker.

3. Re-entrancy — an "allow + audit-log" guard that itself wrote KV re-triggered
   itself to the depth cap and then DENIED the original write (plus ~8x
   resolve/compile/execute amplification). A thread-local guard makes a write
   performed by an interceptor bypass interception.

4. Fail-closed verdict — only `#{ allowed: false }` denied; a bare bool, a
   typo'd key, a non-bool, or a bare unit all ALLOWED. Now allow ONLY on an
   explicit `#{ allowed: true }`; every other shape denies.

5/6. Fail-closed edges — a dangling (missing/disabled) interceptor script and a
   missing engine back-reference now DENY instead of allowing.

7. AppInvoke coupling — resolution no longer routes through `invoke.resolve`, so
   installing a guard no longer denies writes to principals who hold KV-write
   but not AppInvoke.

The seam (`InterceptorService::resolve_before`) now returns an
`InterceptorResolution { None | Dangling | Run(ResolvedScript) }`; the executor
runs the sealed script straight through the shared `run_resolved_blocking` core
and drops its `InvokeService` dependency. executor-core stays Postgres-free.
No migration change (0073 unchanged).

Pinned by three new journeys in tests/interceptors.rs: the seal (a same-named
app script does NOT shadow the group's guard), shared-collection coverage, and
the fail-closed verdict. Full journey suite 157/157.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 21:20:20 +02:00

129 lines
5.0 KiB
Rust

//! Cross-cutting types used by every PiCloud crate.
//!
//! Keep this crate small. If something only one core needs, it belongs in
//! that core's crate. Things here must be genuinely shared (IDs, the Script
//! entity, error roots, transport DTOs).
pub mod app;
pub mod auth;
pub mod crypto;
pub mod dead_letters;
pub mod docs;
pub mod email;
pub mod error;
pub mod events;
pub mod exec_summary;
pub mod execution_log;
pub mod files;
pub mod group;
pub mod group_docs;
pub mod group_files;
pub mod group_kv;
pub mod group_pubsub;
pub mod group_queue;
pub mod http;
pub mod ids;
pub mod inbox;
pub mod interceptor;
pub mod invoke;
pub mod kv;
pub mod log_sink;
pub mod modules;
pub mod outbox_writer;
pub mod project;
pub mod pubsub;
pub mod queue;
pub mod realtime;
pub mod realtime_authority;
pub mod route;
pub mod sandbox;
pub mod script;
pub mod sdk_cx;
pub mod secrets;
pub mod services;
pub mod subscriber_token;
pub mod trigger_event;
pub mod users;
pub mod validator;
pub mod vars;
pub mod version;
pub mod workflow;
/// serde `default` for `enabled`-style boolean fields that should default to
/// `true` when absent from the wire (bool's own `Default` is `false`). Shared
/// by `Script`/`Route`, the apply `Bundle` types, and the CLI manifest.
#[must_use]
pub fn default_true() -> bool {
true
}
pub use app::{App, AppDomain, DomainShape};
pub use auth::{AppRole, InstanceRole, Principal, Scope, UserId};
pub use crypto::{decrypt, encrypt, CryptoError, EncryptResult, MasterKey, MasterKeyError};
pub use dead_letters::{DeadLetterError, DeadLetterId, DeadLetterService, NoopDeadLetterService};
pub use docs::{DocId, DocRow, DocsError, DocsListPage, DocsService, NoopDocsService};
pub use email::{EmailError, EmailService, NoopEmailService, OutboundEmail};
pub use error::Error;
pub use events::{EmitError, NoopEventEmitter, ServiceEvent, ServiceEventEmitter};
pub use exec_summary::ExecResponseSummary;
pub use execution_log::{ExecutionLog, ExecutionSource, ExecutionStatus};
pub use files::{
sanitize_stored_content_type, sanitize_stored_filename,
validate_collection as validate_files_collection, FileMeta, FileUpdate, FilesError,
FilesListPage, FilesService, NewFile, NoopFilesService, SAFE_FILENAME_FALLBACK,
SAFE_RENDER_FALLBACK,
};
pub use group::Group;
pub use group_docs::{GroupDocsError, GroupDocsService, NoopGroupDocsService};
pub use group_files::{GroupFilesError, GroupFilesService, NoopGroupFilesService};
pub use group_kv::{GroupKvError, GroupKvService, NoopGroupKvService};
pub use group_pubsub::{GroupPubsubError, GroupPubsubService, NoopGroupPubsubService};
pub use group_queue::{GroupQueueError, GroupQueueService, NoopGroupQueueService};
pub use http::{HttpError, HttpRequest, HttpResponse, HttpService, NoopHttpService};
pub use ids::{
AdminUserId, ApiKeyId, AppId, AppUserId, ExecutionId, GroupId, InvitationId, ProjectId,
QueueMessageId, RequestId, ScriptId, TriggerId, WorkflowId, WorkflowRunId, WorkflowRunStepId,
};
pub use inbox::{
InboxDeliveryOutcome, InboxFailureKind, InboxResolver, InboxResult, NoopInboxResolver,
};
pub use interceptor::{InterceptorResolution, InterceptorService, NoopInterceptorService};
pub use invoke::{InvokeError, InvokeService, InvokeTarget, NoopInvokeService, ResolvedScript};
pub use kv::{KvError, KvListPage, KvService, NoopKvService};
pub use log_sink::{ExecutionLogSink, LogSinkError};
pub use modules::{
ModuleResolution, ModuleScript, ModuleSource, ModuleSourceError, NoopModuleSource,
};
pub use outbox_writer::{HttpDispatchPayload, NewHttpOutbox, OutboxWriter, OutboxWriterError};
pub use project::Project;
pub use pubsub::{
topic_matches, validate_topic_pattern, NoopPubsubService, PubsubError, PubsubService,
};
pub use queue::{EnqueueOpts, NoopQueueService, QueueError, QueueService};
pub use realtime::{BroadcasterError, NoopRealtimeBroadcaster, RealtimeBroadcaster, RealtimeEvent};
pub use realtime_authority::{DenyAllRealtimeAuthority, RealtimeAuthority, SubscribeDenied};
pub use route::{DispatchMode, HostKind, PathKind, Route};
pub use sandbox::ScriptSandbox;
pub use script::{Script, ScriptKind, ScriptOwner};
pub use sdk_cx::SdkCallCx;
pub use secrets::{
validate_secret_name, NoopSecretsService, SecretsError, SecretsListPage, SecretsService,
SECRET_NAME_MAX_BYTES,
};
pub use services::Services;
pub use trigger_event::{
DeadLetterEventDetail, DocsEventOp, FilesEventOp, KvEventOp, TriggerEvent,
};
pub use users::{
AppUser, CreateUserInput, EmailTemplateOpts, GeneratedAccept, GeneratedSession, Invitation,
InviteOpts, NoopUsersService, UpdateUserInput, UsersError, UsersListOpts, UsersListPage,
UsersService,
};
pub use validator::{ScriptValidator, ValidatedScript, ValidationError};
pub use vars::{NoopVarsService, VarsError, VarsService};
pub use version::{API_VERSION, PRODUCT_VERSION, SDK_VERSION, WIRE_VERSION};
pub use workflow::{
default_base_ms, NoopWorkflowService, OnError, RunStatus, StepStatus, WorkflowBackoff,
WorkflowDefinition, WorkflowError, WorkflowRetry, WorkflowService, WorkflowStepDef,
};