Skip to main content

rustio_admin/auth/
mod.rs

1//! Authentication & authorization.
2//!
3//! Three pieces:
4//! - `users.rs`       — user records, password hashing, login
5//! - `sessions.rs`    — DB-backed sessions with expiry cleanup
6//! - `permissions.rs` — granular permissions + groups
7//!
8//! A user belongs to zero or more groups. Permissions come from two
9//! sources: (a) direct assignments on the user, (b) inherited from
10//! the user's groups. The permission string is
11//! `<app>.<action>_<model>` — e.g. `posts.change_post`.
12
13// public:
14pub mod emergency;
15// public:
16pub mod guards;
17pub(crate) mod mfa;
18mod permissions;
19pub(crate) mod recovery;
20pub(crate) mod recovery_admin;
21mod role;
22// `sessions` is `pub(crate)` so the `__integration` test door at
23// `crate::__integration` can re-export individual `pub(crate)`
24// helpers (specifically `hash_token_for_storage`, used by the R4
25// emergency-access integration suite to verify token-hash format
26// parity with R1's consume path). Public re-exports still flow
27// through `pub use sessions::{...}` below — this visibility bump
28// does not change the external API surface.
29pub(crate) mod sessions;
30mod users;
31
32// public:
33pub use mfa::MfaPolicy;
34pub(crate) use permissions::invalidate_user_cache;
35// public:
36pub use permissions::{
37    add_user_to_group, check_permission, create_group, grant_model_to_default_groups,
38    grant_to_group, grant_to_user, init_permission_tables, permissions_for_user,
39    register_model_permissions, remove_user_from_group, seed_default_groups, Permission,
40    PermissionError, Superuser, DEFAULT_GROUP_NAMES,
41};
42// public:
43pub use recovery::{
44    DefaultPasswordPolicy, DefaultRecoveryPolicy, LoginThrottle, PasswordPolicy,
45    PasswordPolicyError, RecoveryPolicy, SharedPasswordPolicy, SharedRecoveryPolicy,
46};
47// `issue_reset_token` / `consume_reset_token` and the `IssueOutcome` /
48// `ConsumeOutcome` / `MailerEmailStatus` types live in `recovery`
49// (`pub(crate) mod recovery`) so the admin handlers in commit #8+
50// reach them as `crate::auth::recovery::*`. They are intentionally
51// NOT re-exported here — the framework owns the handler shape, and
52// projects compose recovery via the trait surfaces re-exported above.
53// `purge_expired_reset_tokens` (R1 commit #12) is reached the same
54// way from `background::spawn_session_sweeper`.
55// public:
56pub use role::{protected_roles, Role};
57// public:
58pub use sessions::{
59    create_session, current_session_id, delete_session, identity_from_session, init_session_tables,
60    invalidate_sessions, list_active_for_user, logout_session, purge_expired_sessions,
61    session_token_from_cookie, InvalidationOutcome, Session, SessionInvalidationReason,
62    SessionTarget, SessionTrust, SESSION_COOKIE,
63};
64// public:
65#[allow(deprecated)]
66pub use users::would_orphan_developers;
67// public:
68pub use users::{
69    create_user, find_user_by_email, hash_password, init_user_tables, load_user_profile, login,
70    migrate_user_schema, set_password, update_user_role, verdict_for_orphan_role, verify_password,
71    would_orphan_protected, would_orphan_role, Identity, StoredUser, UserProfile,
72};
73
74use crate::error::Result;
75use crate::orm::Db;
76
77// public:
78/// Initialise every auth-related table. Safe to call on every boot.
79///
80/// Fast-path: the full sequence below is idempotent but costs ~13 DDL
81/// round-trips. We stamp the crate version into `rustio_admin_meta` once
82/// it has run, and skip the sequence when the stored stamp already
83/// matches the running version. A fresh database (no stamp) or a
84/// framework upgrade (version changed) runs the full sequence and
85/// re-stamps, so any schema change shipped in a new release self-heals on
86/// the first boot of that release. To force a re-run on the same version,
87/// `TRUNCATE rustio_admin_meta`.
88pub async fn init_tables(db: &Db) -> Result<()> {
89    const STAMP_KEY: &str = "auth_schema_version";
90    let current = env!("CARGO_PKG_VERSION");
91    crate::meta::ensure_table(db).await?;
92    if crate::meta::get(db, STAMP_KEY).await?.as_deref() == Some(current) {
93        return Ok(());
94    }
95
96    init_user_tables(db).await?;
97    migrate_user_schema(db).await?;
98    init_session_tables(db).await?;
99    sessions::migrate_session_schema(db).await?;
100    sessions::migrate_session_lifecycle(db).await?;
101    init_permission_tables(db).await?;
102    // PR 2.2 (0.21.0) — structural permission defaults. Seeds the
103    // three groups (`administrator`, `editor`, `viewer`) on fresh
104    // databases. Guarded: skipped when the project has built its
105    // own group structure. Idempotent on the happy path.
106    // See `docs/design/DESIGN_PERMISSIONS.md`.
107    seed_default_groups(db).await?;
108    // R1 (0.5.0) — self password recovery schema. See
109    // DESIGN_RECOVERY.md §9 for the contract.
110    recovery::migrate_user_recovery_schema(db).await?;
111    recovery::init_recovery_tables(db).await?;
112    // R2 (0.6.0) — organisational recovery schema (lockout columns +
113    // partial index). See DESIGN_R2_ORGANISATIONAL.md §4 for the
114    // contract. Schema is additive and orthogonal to R1's recovery
115    // tables; the runtime that reads these columns lands in later
116    // R2 commits.
117    recovery_admin::migrate_user_lockout_schema(db).await?;
118    // R3 (0.7.0) — TOTP MFA schema (4 additive columns on
119    // rustio_users + new rustio_mfa_backup_codes table + partial
120    // index). See DESIGN_R3_MFA.md §7 for the contract. Schema
121    // is additive and orthogonal to R1 / R2; the encryption
122    // helpers, TOTP RFC 6238 implementation, enrolment /
123    // verification / disable runtime, and login-flow integration
124    // land in later R3 commits.
125    mfa::migrate_user_mfa_schema(db).await?;
126    // R2 (0.6.0) — surfaced by the testcontainers integration suite:
127    // `rustio_admin_actions` was previously created lazily on first
128    // dashboard hit (`handlers::ensure_audit_ready`), so any audit-
129    // emitting path that ran BEFORE someone visited `/admin` would
130    // fail. The R2 audit-heavy paths (auto-throttle, admin reset,
131    // admin lock/unlock/revoke, forced rotation) made this latent
132    // issue routinely reachable. Creating the table eagerly during
133    // boot closes the gap; `ensure_table` is already idempotent so
134    // this is safe to call alongside the lazy path that still
135    // exists in the dashboard handler.
136    crate::admin::audit::ensure_table(db).await?;
137
138    // Stamp the version that just initialised the schema so subsequent
139    // boots on this version take the fast-path above.
140    crate::meta::set(db, STAMP_KEY, current).await?;
141    Ok(())
142}