feat: E2E #2 (Stash) gap remediation + S6 hardening
Closes the gaps and the one security finding from the second end-to-end
CLI test (E2E_STASH_REPORT.md), plus the H1 boot-regression found while
re-reviewing those fixes.
Security
- S6: reserved-path validation (`check_reserved`) now case-folds before
comparing, so `/API/v2/x`, `/HEALTHZ`, `/Admin/x` are rejected like
their lowercase forms. Request-time matching stays case-sensitive.
- S10: "public route != public data" callout in sdk-shape.md (script_gate
skips authz when the principal is anonymous).
Observability / features
- G1: trigger executions now write `execution_logs`. Migration 0043 adds
a `source` column (CHECK mirrors ExecutionSource/OutboxSourceKind,
DEFAULT 'http' backfills history); a shared `build_execution_log` helper
in executor-core; dispatcher logging for outbox triggers + queue
consumers (skips sync-HTTP rows the orchestrator already logs). `pic
logs` gains a source column + `--source` filter.
- G5: dev-only in-memory email capture under PICLOUD_DEV_MODE with no SMTP
(email::send succeeds locally), readable at GET /api/v1/admin/dev/emails
(Owner/Admin only; route mounted only in capture mode).
- G6: generalized the Rhai in-place-mutation footgun note (trim/replace/
make_upper/make_lower/crop/truncate/pad return ()).
- G2/G3/G4 (CLI): `pic members`, `pic files`, `pic queues`, read-only
`pic kv` (+ new kv_api.rs); `pic deploy --timeout/--memory/--kind/
--sandbox`; first-class `pic triggers create-{docs,files,pubsub,queue,
email}` wrappers. All new client path segments percent-encoded via seg().
H1 regression fix (found in re-review)
- The S6 change also runs in `compile_routes`, which compiles every stored
route at boot and on each route CRUD. A single stored route the new
validation rejects (creatable while the S6 gap existed) made the whole
compile Err and aborted startup. `compile_routes` is now lenient: it
skips an un-compilable row with a warning instead of bricking boot
(route creation still validates separately). Migration 0044 sweeps
pre-existing reserved-path routes on upgrade (WHERE mirrors
check_reserved exactly). Added regression tests for both.
Verified: cargo fmt, clippy --all-targets --all-features -D warnings, the
schema_snapshot test, and the new S6/lenient-compile unit tests all pass;
boot-resilience and G1/G5 confirmed live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12,26 +12,26 @@ use picloud_executor_core::{Engine, Limits};
|
||||
use picloud_manager_core::{
|
||||
admin_router, admins_router, api_keys_router, app_members_router, apps_api, apps_router,
|
||||
attach_principal_if_present, auth_router, compile_routes, dead_letters_router,
|
||||
email_inbound_router, files_admin_router, migrations, require_authenticated,
|
||||
route_admin_router, secrets_router, topics_router, triggers_router, AbandonedRepo,
|
||||
AdminPrincipalResolver, AdminSessionRepository, AdminState, AdminUserRepository, AdminsState,
|
||||
ApiKeyRepository, ApiKeysState, AppDomainRepository, AppMembersRepository, AppMembersState,
|
||||
AppRepository, AppsState, AuthState, AuthzRepo, DeadLetterRepo, DeadLettersState, Dispatcher,
|
||||
DocsServiceImpl, EmailInboundState, EmailServiceImpl, FilesAdminState, FilesConfig,
|
||||
FilesServiceImpl, FsFilesRepo, HttpConfig, HttpServiceImpl, InboundNonceDedup, KvServiceImpl,
|
||||
OutboxEventEmitter, OutboxRepo, PostgresAbandonedRepo, PostgresAdminSessionRepository,
|
||||
PostgresAdminUserRepository, PostgresApiKeyRepository, PostgresAppDomainRepository,
|
||||
PostgresAppMembersRepository, PostgresAppRepository, PostgresAppSecretsRepo,
|
||||
PostgresAppUserInvitationRepo, PostgresAppUserPasswordResetRepo, PostgresAppUserRepository,
|
||||
PostgresAppUserRoleRepo, PostgresAppUserSessionRepository, PostgresAppUserVerificationRepo,
|
||||
PostgresDeadLetterRepo, PostgresDeadLetterService, PostgresDocsRepo,
|
||||
PostgresExecutionLogRepository, PostgresExecutionLogSink, PostgresKvRepo, PostgresOutboxRepo,
|
||||
PostgresPubsubRepo, PostgresRouteRepository, PostgresScriptRepository, PostgresSecretsRepo,
|
||||
PostgresTopicRepo, PostgresTriggerRepo, PrincipalResolver, PubsubServiceImpl,
|
||||
RealtimeAuthorityImpl, RepoResolver, RouteAdminState, RouteRepository, SandboxCeiling,
|
||||
ScriptRepository, SecretsConfig, SecretsServiceImpl, SecretsState, SubscriberTokenConfig,
|
||||
TopicRepo, TopicsState, TriggerConfig, TriggerRepo, TriggersState, UsersServiceConfig,
|
||||
UsersServiceImpl,
|
||||
dev_emails_router, email_inbound_router, files_admin_router, kv_admin_router, migrations,
|
||||
require_authenticated, route_admin_router, secrets_router, topics_router, triggers_router,
|
||||
AbandonedRepo, AdminPrincipalResolver, AdminSessionRepository, AdminState, AdminUserRepository,
|
||||
AdminsState, ApiKeyRepository, ApiKeysState, AppDomainRepository, AppMembersRepository,
|
||||
AppMembersState, AppRepository, AppsState, AuthState, AuthzRepo, DeadLetterRepo,
|
||||
DeadLettersState, DevEmailState, Dispatcher, DocsServiceImpl, EmailInboundState,
|
||||
EmailServiceImpl, FilesAdminState, FilesConfig, FilesServiceImpl, FsFilesRepo, HttpConfig,
|
||||
HttpServiceImpl, InboundNonceDedup, KvAdminState, KvServiceImpl, OutboxEventEmitter,
|
||||
OutboxRepo, PostgresAbandonedRepo, PostgresAdminSessionRepository, PostgresAdminUserRepository,
|
||||
PostgresApiKeyRepository, PostgresAppDomainRepository, PostgresAppMembersRepository,
|
||||
PostgresAppRepository, PostgresAppSecretsRepo, PostgresAppUserInvitationRepo,
|
||||
PostgresAppUserPasswordResetRepo, PostgresAppUserRepository, PostgresAppUserRoleRepo,
|
||||
PostgresAppUserSessionRepository, PostgresAppUserVerificationRepo, PostgresDeadLetterRepo,
|
||||
PostgresDeadLetterService, PostgresDocsRepo, PostgresExecutionLogRepository,
|
||||
PostgresExecutionLogSink, PostgresKvRepo, PostgresOutboxRepo, PostgresPubsubRepo,
|
||||
PostgresRouteRepository, PostgresScriptRepository, PostgresSecretsRepo, PostgresTopicRepo,
|
||||
PostgresTriggerRepo, PrincipalResolver, PubsubServiceImpl, RealtimeAuthorityImpl, RepoResolver,
|
||||
RouteAdminState, RouteRepository, SandboxCeiling, ScriptRepository, SecretsConfig,
|
||||
SecretsServiceImpl, SecretsState, SubscriberTokenConfig, TopicRepo, TopicsState, TriggerConfig,
|
||||
TriggerRepo, TriggersState, UsersServiceConfig, UsersServiceImpl,
|
||||
};
|
||||
use picloud_orchestrator_core::realtime::DEFAULT_GC_INTERVAL_SECS;
|
||||
use picloud_orchestrator_core::routing::{AppDomainTable, RouteTable};
|
||||
@@ -146,7 +146,7 @@ pub async fn build_app(
|
||||
outbox_repo.clone(),
|
||||
));
|
||||
let kv: Arc<dyn KvService> = Arc::new(KvServiceImpl::with_max_value_bytes(
|
||||
kv_repo,
|
||||
kv_repo.clone(),
|
||||
authz.clone(),
|
||||
events.clone(),
|
||||
picloud_manager_core::kv_service::kv_max_value_bytes_from_env(),
|
||||
@@ -239,8 +239,12 @@ pub async fn build_app(
|
||||
secrets_config,
|
||||
));
|
||||
// v1.1.7 outbound email. Builds a lettre SMTP transport from
|
||||
// PICLOUD_SMTP_* env (disabled mode + warning if unconfigured).
|
||||
let email: Arc<dyn EmailService> = Arc::new(EmailServiceImpl::from_env(authz.clone()));
|
||||
// PICLOUD_SMTP_* env (disabled mode + warning if unconfigured). G5: in
|
||||
// dev mode with no relay, captures mail in memory instead of erroring;
|
||||
// `dev_email_sink` is `Some` then, and we mount the dev inspection
|
||||
// endpoint below.
|
||||
let (email_impl, dev_email_sink) = EmailServiceImpl::from_env_with_dev_capture(authz.clone());
|
||||
let email: Arc<dyn EmailService> = Arc::new(email_impl);
|
||||
// v1.1.8 data-plane user management. Wires Argon2id-hashed user
|
||||
// rows + SHA-256-hashed sliding-window sessions to the Rhai
|
||||
// `users::*` namespace and the admin /apps/{id}/users HTTP surface.
|
||||
@@ -290,8 +294,10 @@ pub async fn build_app(
|
||||
// routes.
|
||||
let route_table = Arc::new(RouteTable::new());
|
||||
let initial = route_repo.list_all().await?;
|
||||
let compiled = compile_routes(&initial)
|
||||
.map_err(|e| anyhow::anyhow!("failed to compile stored routes: {e}"))?;
|
||||
// Lenient: a single un-compilable stored route (e.g. one whose path
|
||||
// became reserved under stricter validation) is skipped-with-warning
|
||||
// inside compile_routes, never aborting boot. (H1)
|
||||
let compiled = compile_routes(&initial);
|
||||
route_table.replace_all(compiled);
|
||||
|
||||
// v1.1.9 function composition. InvokeServiceImpl resolves targets
|
||||
@@ -380,6 +386,7 @@ pub async fn build_app(
|
||||
principals,
|
||||
executor: executor.clone(),
|
||||
gate,
|
||||
log_sink: log_sink.clone(),
|
||||
inbox: inbox_resolver,
|
||||
queue: queue_repo.clone(),
|
||||
config: trigger_config,
|
||||
@@ -544,7 +551,7 @@ pub async fn build_app(
|
||||
// else under /admin gets the require_authenticated layer; capability
|
||||
// checks live in each handler (after the resource is loaded so the
|
||||
// capability binds to the resource's actual app_id).
|
||||
let guarded_admin = Router::new()
|
||||
let mut guarded_admin = Router::new()
|
||||
.merge(admin_router(admin))
|
||||
.merge(route_admin_router(route_admin))
|
||||
.merge(admins_router(admins_state))
|
||||
@@ -565,13 +572,24 @@ pub async fn build_app(
|
||||
},
|
||||
))
|
||||
.merge(files_admin_router(files_admin_state))
|
||||
.merge(kv_admin_router(KvAdminState {
|
||||
kv: kv_repo.clone(),
|
||||
apps: apps_repo.clone(),
|
||||
authz: authz.clone(),
|
||||
}))
|
||||
.merge(topics_router(topics_state))
|
||||
.merge(secrets_router(secrets_state))
|
||||
.merge(dead_letters_router(dead_letters_state))
|
||||
.layer(from_fn_with_state(
|
||||
auth_state.clone(),
|
||||
require_authenticated,
|
||||
));
|
||||
.merge(dead_letters_router(dead_letters_state));
|
||||
// G5: dev-only mail inspection — mounted exactly when the email
|
||||
// service is capturing in memory (dev mode + no relay). Same
|
||||
// `require_authenticated` layer as the rest of /admin applies below.
|
||||
if let Some(sink) = dev_email_sink {
|
||||
guarded_admin = guarded_admin.merge(dev_emails_router(DevEmailState { sink }));
|
||||
}
|
||||
let guarded_admin = guarded_admin.layer(from_fn_with_state(
|
||||
auth_state.clone(),
|
||||
require_authenticated,
|
||||
));
|
||||
|
||||
// Silence "unused import" lint on `apps_api` — we re-export via the
|
||||
// facade above; the bare module path is retained so it's discoverable.
|
||||
|
||||
Reference in New Issue
Block a user