feat(v1.1.8): UsersService trait + impl — CRUD + login/verify/logout

UsersService trait in shared::users + Postgres-backed impl in
manager-core::users_service. Wired into the Services bundle (new
`users` field) and the picloud binary's startup.

Commit 4 ships:

  * CRUD: create / get / find_by_email / update / delete / list
    (cursor-paged on created_at). create validates email shape,
    8-char password minimum, optional display_name; throws
    DuplicateEmail on (app_id, lower(email)) collision.
  * Auth: login (timing-flat — runs verify_password against the
    shared dummy Argon2id PHC even on email miss, so the
    bad-email and good-email branches share wall-clock cost);
    verify (sliding TTL bump capped at absolute_expires_at);
    logout (revoke by token).
  * verify_session_for_realtime — non-SDK method taking app_id
    explicitly; used by the F3 realtime auth_mode='session' path
    in a later commit.
  * Cross-app isolation everywhere: cx.app_id (or explicit app_id)
    is the only source of truth; a logout for a foreign-app token
    is a silent no-op rather than a probe of session existence.

Email-tied flows (verification, password reset, invitations), roles,
and admin-mediated helpers are stubbed UsersError::Backend and ship
in later commits in this series.

Config (UsersServiceConfig) sources from env with documented
defaults:
  PICLOUD_APP_USER_SESSION_TTL_HOURS         (24)
  PICLOUD_APP_USER_SESSION_ABSOLUTE_HOURS    (720 = 30d)
  PICLOUD_APP_USER_VERIFICATION_TTL_HOURS    (48)
  PICLOUD_APP_USER_PASSWORD_RESET_TTL_HOURS  (1)
  PICLOUD_APP_USER_INVITATION_TTL_DAYS       (7)

Reserved a TIMING_FLAT_DUMMY_HASH constant on manager-core::auth so
the dummy PHC is one shared definition.

Added AppUserId + InvitationId id types in picloud-shared.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-06 11:57:06 +02:00
parent 7a44cbf5a4
commit c6bf8d3de5
6 changed files with 1198 additions and 10 deletions

View File

@@ -21,14 +21,15 @@ use picloud_manager_core::{
FilesServiceImpl, FsFilesRepo, HttpConfig, HttpServiceImpl, KvServiceImpl, OutboxEventEmitter,
OutboxRepo, PostgresAbandonedRepo, PostgresAdminSessionRepository, PostgresAdminUserRepository,
PostgresApiKeyRepository, PostgresAppDomainRepository, PostgresAppMembersRepository,
PostgresAppRepository, PostgresAppSecretsRepo, 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,
PostgresAppRepository, PostgresAppSecretsRepo, PostgresAppUserRepository,
PostgresAppUserSessionRepository, 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};
@@ -40,7 +41,7 @@ use picloud_shared::{
DeadLetterService, DocsService, EmailService, ExecutionLogSink, FilesService, HttpService,
InboxResolver, KvService, MasterKey, OutboxWriter, PubsubService, RealtimeAuthority,
RealtimeBroadcaster, ScriptValidator, SecretsService, ServiceEventEmitter, Services,
API_VERSION, PRODUCT_VERSION, SDK_VERSION, WIRE_VERSION,
UsersService, API_VERSION, PRODUCT_VERSION, SDK_VERSION, WIRE_VERSION,
};
use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
@@ -244,6 +245,18 @@ pub async fn build_app(
// 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()));
// 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.
let app_users_repo = Arc::new(PostgresAppUserRepository::new(pool.clone()));
let app_user_sessions_repo = Arc::new(PostgresAppUserSessionRepository::new(pool.clone()));
let users: Arc<dyn UsersService> = Arc::new(UsersServiceImpl::new(
app_users_repo.clone(),
app_user_sessions_repo.clone(),
authz.clone(),
events.clone(),
UsersServiceConfig::from_env(),
));
let services = Services::new(
kv,
docs,
@@ -255,6 +268,7 @@ pub async fn build_app(
pubsub,
secrets,
email,
users,
);
let engine = Arc::new(Engine::new(Limits::default(), services));