feat(v1.1.8): F3 realtime auth_mode = 'session' (migration 0033)

Migration 0033 widens topics.auth_mode CHECK to include 'session'
alongside 'public' and 'token'.

TopicAuthMode enum gains a Session variant (as_str + from_db
extended uniformly).

RealtimeAuthorityImpl now takes Arc<dyn UsersService> as a third
constructor arg. The Session branch of authorize_subscribe
delegates to UsersService::verify_session_for_realtime(app_id,
token):
  * Returns Some(user) → allow. The service bumps the sliding TTL
    on success.
  * Returns None → Unauthorized.
  * Defense-in-depth: even though verify_session_for_realtime
    already enforces cross-app isolation, the branch re-checks
    user.app_id == app_id.

Tests added (4 new cases): valid session token allows; missing
token is Unauthorized; wrong token is Unauthorized; cross-app
session token is Unauthorized. All 12 realtime_authority tests
pass.

Dashboard: TopicAuthMode TypeScript union widened to include
'session'; the topic create + edit forms gain a third radio option
labeled "session — requires a per-app user session minted by
users::login (v1.1.8)".

picloud binary: construction order reshuffled so users is built
before realtime_authority. app_secrets_repo is now .clone()'d into
the pubsub realtime wiring so the original Arc can be re-used by
realtime_authority.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-06 15:00:47 +02:00
parent 3c2c4a3767
commit ff4f443531
6 changed files with 374 additions and 12 deletions

View File

@@ -13,14 +13,16 @@ use picloud_shared::AppId;
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
/// External-subscriber auth gate for a topic. `'public'` + `'token'` in
/// v1.1.6; `'session'` (v1.1.8) and `'script'` (v1.2) extend the DB
/// CHECK constraint and this enum later.
/// External-subscriber auth gate for a topic. v1.1.6 shipped
/// `'public'` + `'token'`; v1.1.8 F3 adds `'session'` (per-app user
/// sessions authorize subscription); `'script'` (v1.2) extends the
/// DB CHECK constraint and this enum later.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TopicAuthMode {
Public,
Token,
Session,
}
impl TopicAuthMode {
@@ -29,6 +31,7 @@ impl TopicAuthMode {
match self {
Self::Public => "public",
Self::Token => "token",
Self::Session => "session",
}
}
@@ -36,6 +39,7 @@ impl TopicAuthMode {
match s {
"public" => Ok(Self::Public),
"token" => Ok(Self::Token),
"session" => Ok(Self::Session),
other => Err(TopicRepoError::Backend(format!(
"unknown auth_mode in DB: {other}"
))),