Skip to main content

isb_server/auth/
mod.rs

1//! Identity for `isb serve`: users, org memberships and roles, browser
2//! sessions, invitations, API tokens and password resets, in SQLite at
3//! `<state>/isb.db`.
4//!
5//! - Orgs are the trust boundary. A user is a member of an org with a
6//!   [`Role`]; what a role may do is the table in [`Role::permissions`], so
7//!   roles can be refined without touching the callers. A platform admin
8//!   spans orgs.
9//! - Every bearer secret (session, API token, invitation, reset) is 32 random
10//!   bytes shown once; only its SHA-256 is stored ([`secret`]).
11//! - Passwords are argon2id. Login failures never say which half was wrong,
12//!   cost the same either way, and are rate-limited per email and per IP.
13//! - The HTTP endpoints are in [`http`]; the CLI uses this API directly on the
14//!   same file (SQLite in WAL mode handles the daemon and the CLI at once).
15//!
16//! External sign-in ([`oauth`]) attaches rows to `user_identities`; passkeys
17//! ([`webauthn`]) live in `passkeys`; [`external`] manages both.
18
19mod actors;
20pub mod agent_identities;
21pub mod cbor;
22pub mod db;
23pub mod external;
24pub mod http;
25pub mod limit;
26pub mod oauth;
27pub mod oidc;
28pub mod ops;
29pub mod secret;
30pub mod ssh_keys;
31pub mod superadmin;
32pub mod webauthn;
33
34use std::path::{Path, PathBuf};
35use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
36use std::time::Duration;
37
38use rusqlite::{Connection, OptionalExtension, Row, TransactionBehavior, params};
39use serde::{Deserialize, Serialize};
40
41use crate::org::OrgId;
42pub use actors::WORKSPACE_ACTOR;
43use limit::{Rate, RateLimiter};
44use secret::{PasswordCost, TokenKind};
45pub use superadmin::{Superadmin, SuperadminSource, SuperadminToken};
46
47/// What the identity store can fail with. [`AuthError::InvalidCredentials`]
48/// is deliberately vague: it is the answer to every failed login.
49#[derive(Debug, thiserror::Error)]
50pub enum AuthError {
51    #[error("invalid email or password")]
52    InvalidCredentials,
53    /// A presented invitation or reset token that is unknown, used or expired.
54    #[error("{0} is invalid or has expired")]
55    InvalidToken(&'static str),
56    #[error("too many attempts; try again in {retry_after}s")]
57    RateLimited { retry_after: u64 },
58    #[error("{0}")]
59    Forbidden(String),
60    #[error("{0} not found")]
61    NotFound(String),
62    #[error("{0}")]
63    Conflict(String),
64    #[error("{0}")]
65    Invalid(String),
66    #[error("{0}")]
67    Internal(String),
68    /// An external sign-in that proved who the user is, but may not go on
69    /// (no verified email, no invitation, a disabled account). `code` is
70    /// stable, for the login page.
71    #[error("{message}")]
72    Refused { code: &'static str, message: String },
73    /// A passkey assertion or registration that did not verify.
74    #[error("passkey rejected: {0}")]
75    PasskeyRejected(String),
76    #[error("identity database: {0}")]
77    Db(#[from] rusqlite::Error),
78}
79
80impl AuthError {
81    pub(crate) fn io(step: String, e: std::io::Error) -> Self {
82        AuthError::Internal(format!("{step}: {e}"))
83    }
84}
85
86impl From<AuthError> for crate::Error {
87    fn from(e: AuthError) -> Self {
88        match e {
89            AuthError::NotFound(s) => crate::Error::NotFound(s),
90            e => crate::Error::Invalid(e.to_string()),
91        }
92    }
93}
94
95pub type AuthResult<T> = std::result::Result<T, AuthError>;
96
97/// A role in an org. Ordered by reach: an actor may grant roles up to its own.
98#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
99#[serde(rename_all = "snake_case")]
100pub enum Role {
101    /// Reads the org: lists and inspects, no secret values, no deploys, no
102    /// exec or terminal.
103    Viewer,
104    Member,
105    Admin,
106    Owner,
107}
108
109/// Something a role allows within its org.
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
111pub enum Permission {
112    /// List and inspect what is in the org (read-only tools), never secret
113    /// values.
114    ReadOrg,
115    /// Apps, stacks, sandboxes, secrets (read included), deploys, exec.
116    AdminOrg,
117    /// Add, change and remove members; invitations; every token in the org.
118    ManageMembers,
119    /// Delete the org.
120    DeleteOrg,
121}
122
123impl Role {
124    pub const ALL: [Role; 4] = [Role::Viewer, Role::Member, Role::Admin, Role::Owner];
125
126    /// What each role may do. The org is the boundary, so every member
127    /// administers what is in it; a viewer only reads.
128    pub fn permissions(self) -> &'static [Permission] {
129        use Permission::*;
130        match self {
131            Role::Viewer => &[ReadOrg],
132            Role::Member => &[ReadOrg, AdminOrg],
133            Role::Admin => &[ReadOrg, AdminOrg, ManageMembers],
134            Role::Owner => &[ReadOrg, AdminOrg, ManageMembers, DeleteOrg],
135        }
136    }
137
138    pub fn can(self, p: Permission) -> bool {
139        self.permissions().contains(&p)
140    }
141
142    pub fn as_str(self) -> &'static str {
143        match self {
144            Role::Viewer => "viewer",
145            Role::Member => "member",
146            Role::Admin => "admin",
147            Role::Owner => "owner",
148        }
149    }
150
151    pub fn parse(s: &str) -> AuthResult<Role> {
152        Role::ALL
153            .into_iter()
154            .find(|r| r.as_str() == s)
155            .ok_or_else(|| {
156                AuthError::Invalid(format!("role {s:?}: owner, admin, member or viewer"))
157            })
158    }
159}
160
161impl std::fmt::Display for Role {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        f.write_str(self.as_str())
164    }
165}
166
167/// A user, without the password hash (which never leaves the store).
168#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
169pub struct User {
170    pub id: i64,
171    pub email: String,
172    pub name: String,
173    pub platform_admin: bool,
174    pub created_at: i64,
175    pub disabled: bool,
176    /// False for a user who signs in only through an external identity.
177    pub has_password: bool,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
181pub struct Membership {
182    pub org: OrgId,
183    pub role: Role,
184}
185
186/// A browser session. `expires_at` is the absolute limit; it also ends when
187/// unused for the configured idle time ([`Session::idle_expires_at`]).
188#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
189pub struct Session {
190    pub id: i64,
191    pub user_id: i64,
192    pub created_at: i64,
193    pub last_seen: i64,
194    pub expires_at: i64,
195    pub idle_expires_at: i64,
196    pub user_agent: Option<String>,
197    pub ip: Option<String>,
198}
199
200/// A session just created: the token is in hand only now.
201#[derive(Debug, Clone)]
202pub struct NewSession {
203    pub token: String,
204    pub session: Session,
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
208pub struct Invitation {
209    pub id: i64,
210    pub org: OrgId,
211    pub email: String,
212    pub role: Role,
213    /// `None` when made by the local CLI (or the inviter was deleted).
214    pub invited_by: Option<i64>,
215    pub created_at: i64,
216    pub expires_at: i64,
217    pub accepted_at: Option<i64>,
218}
219
220#[derive(Debug, Clone)]
221pub struct NewInvitation {
222    pub token: String,
223    pub invitation: Invitation,
224}
225
226/// What accepting an invitation did.
227#[derive(Debug, Clone)]
228pub struct Accepted {
229    pub user: User,
230    pub membership: Membership,
231    /// True when the invitation created the account.
232    pub created: bool,
233}
234
235/// An API token's metadata.
236#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
237pub struct ApiToken {
238    pub id: i64,
239    pub name: String,
240    pub user_id: i64,
241    /// `None`: a platform token, acting with all of its (platform admin)
242    /// owner's access. `Some`: confined to that org.
243    pub org: Option<OrgId>,
244    pub created_at: i64,
245    pub last_used: Option<i64>,
246    pub expires_at: Option<i64>,
247    /// Restrictions on top of the role ([`Scope`]); empty: the role's
248    /// whole reach.
249    pub scopes: Vec<String>,
250}
251
252#[derive(Debug, Clone)]
253pub struct NewApiToken {
254    pub token: String,
255    pub info: ApiToken,
256}
257
258/// How a principal authenticated.
259#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
260#[serde(tag = "kind", rename_all = "snake_case")]
261pub enum PrincipalKind {
262    Session {
263        id: i64,
264    },
265    ApiToken {
266        id: i64,
267        org: Option<OrgId>,
268        /// The token's name, for audit rows.
269        #[serde(default)]
270        name: String,
271        /// Empty: the role's whole reach.
272        #[serde(default, skip_serializing_if = "Vec::is_empty")]
273        scopes: Vec<String>,
274    },
275    /// A Cloudflare Access identity whose email is this user's.
276    Access,
277    /// A superadmin ([`superadmin`]): a superadmin token or a tailnet
278    /// identity, acting as this (possibly synthetic) user.
279    Superadmin {
280        source: SuperadminSource,
281    },
282    /// An org's workspace (docs/concepts/workspaces.md): its token (`isb_ws_...`),
283    /// held by the agents that live in it. Nobody's account: a synthetic
284    /// principal confined to `org` with the role the workspace was given.
285    Workspace {
286        org: OrgId,
287        /// The workspace's name in its org.
288        name: String,
289    },
290    /// A tailnet or Cloudflare Access caller an org mapped to a role
291    /// ([`agent_identities`]): `label` is `tailnet:<login or node>` or
292    /// `access:<name>`. Nobody's account, confined to the orgs that mapped it.
293    Agent {
294        label: String,
295    },
296}
297
298/// An authenticated caller: who, how, and what they may reach. For an
299/// org-scoped token, `orgs` is that one org and `platform_admin` is false
300/// whatever the user is.
301#[derive(Debug, Clone, PartialEq, Eq)]
302pub struct Principal {
303    pub user: User,
304    pub kind: PrincipalKind,
305    pub orgs: Vec<(OrgId, Role)>,
306    pub platform_admin: bool,
307    /// Set on an org-bound endpoint for a superadmin or platform admin: an admin of this org only.
308    pub downscoped: Option<OrgId>,
309}
310
311impl Principal {
312    pub fn is_platform_admin(&self) -> bool {
313        self.platform_admin
314    }
315
316    pub fn role_in(&self, org: &OrgId) -> Option<Role> {
317        self.orgs.iter().find(|(o, _)| o == org).map(|(_, r)| *r)
318    }
319
320    fn can(&self, org: &OrgId, p: Permission) -> bool {
321        self.platform_admin || self.role_in(org).is_some_and(|r| r.can(p))
322    }
323
324    /// Apps, stacks, sandboxes and secrets in `org`.
325    pub fn can_admin_org(&self, org: &OrgId) -> bool {
326        self.can(org, Permission::AdminOrg)
327    }
328
329    /// Read-only tools in `org`.
330    pub fn can_read_org(&self, org: &OrgId) -> bool {
331        self.can(org, Permission::ReadOrg)
332    }
333
334    /// Members, invitations and every token in `org`.
335    pub fn can_manage_members(&self, org: &OrgId) -> bool {
336        self.can(org, Permission::ManageMembers)
337    }
338
339    pub fn can_delete_org(&self, org: &OrgId) -> bool {
340        self.can(org, Permission::DeleteOrg)
341    }
342
343    /// The highest role this principal may hand out in `org`.
344    pub fn max_grant(&self, org: &OrgId) -> Option<Role> {
345        if self.platform_admin {
346            return Some(Role::Owner);
347        }
348        self.role_in(org)
349            .filter(|r| r.can(Permission::ManageMembers))
350    }
351
352    pub fn session_id(&self) -> Option<i64> {
353        match self.kind {
354            PrincipalKind::Session { id } => Some(id),
355            PrincipalKind::ApiToken { .. }
356            | PrincipalKind::Access
357            | PrincipalKind::Superadmin { .. }
358            | PrincipalKind::Agent { .. }
359            | PrincipalKind::Workspace { .. } => None,
360        }
361    }
362}
363
364/// Where a login came from, kept on the session for the user to review.
365#[derive(Debug, Clone, Default)]
366pub struct LoginMeta {
367    pub user_agent: Option<String>,
368    pub ip: Option<String>,
369}
370
371/// Lifetimes, cost and rate limits.
372#[derive(Debug, Clone)]
373pub struct AuthConfig {
374    /// A session ends this long after login, used or not.
375    pub session_max_age: Duration,
376    /// A session ends after this long unused.
377    pub session_idle: Duration,
378    pub invitation_ttl: Duration,
379    pub reset_ttl: Duration,
380    pub password_cost: PasswordCost,
381    /// Login attempts per email.
382    pub login_per_email: Rate,
383    /// Unauthenticated attempts (login, setup, invitations, resets) per IP.
384    pub attempts_per_ip: Rate,
385    /// Password reset requests per email.
386    pub resets_per_email: Rate,
387}
388
389impl Default for AuthConfig {
390    fn default() -> Self {
391        AuthConfig {
392            session_max_age: Duration::from_secs(30 * 86400),
393            session_idle: Duration::from_secs(7 * 86400),
394            invitation_ttl: Duration::from_secs(7 * 86400),
395            reset_ttl: Duration::from_secs(3600),
396            password_cost: PasswordCost::default(),
397            login_per_email: Rate::new(5, 60),
398            attempts_per_ip: Rate::new(20, 6),
399            resets_per_email: Rate::new(3, 900),
400        }
401    }
402}
403
404/// `last_seen`/`last_used` are written at most this often per session or token.
405const TOUCH_EVERY: i64 = 60;
406
407pub type Clock = Arc<dyn Fn() -> i64 + Send + Sync>;
408
409/// The identity store. Cheap to share behind an `Arc`; one connection behind
410/// a mutex (requests are short, and argon2 runs outside the lock).
411pub struct AuthStore {
412    conn: Mutex<Connection>,
413    path: Option<PathBuf>,
414    cfg: AuthConfig,
415    clock: Clock,
416    per_email: RateLimiter,
417    per_ip: RateLimiter,
418    resets: RateLimiter,
419    dummy: OnceLock<String>,
420}
421
422impl std::fmt::Debug for AuthStore {
423    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
424        f.debug_struct("AuthStore")
425            .field("path", &self.path)
426            .finish()
427    }
428}
429
430/// The identity database under a daemon state directory.
431pub fn db_path(state_dir: &Path) -> PathBuf {
432    state_dir.join("isb.db")
433}
434
435fn unix_now() -> i64 {
436    std::time::SystemTime::now()
437        .duration_since(std::time::UNIX_EPOCH)
438        .map(|d| d.as_secs() as i64)
439        .unwrap_or(0)
440}
441
442fn secs(d: Duration) -> i64 {
443    d.as_secs().min(i64::MAX as u64 / 2) as i64
444}
445
446/// Trimmed, lowercased, and plausibly an address. Not RFC 5322: the address
447/// is a login name and an invitation target, not something isb parses.
448pub fn normalize_email(email: &str) -> AuthResult<String> {
449    let e = email.trim().to_lowercase();
450    let ok = e.len() <= 254
451        && e.split_once('@').is_some_and(|(l, d)| {
452            !l.is_empty() && !d.is_empty() && !d.contains('@') && !d.starts_with('.')
453        })
454        && !e.chars().any(|c| c.is_whitespace() || c.is_control());
455    if ok {
456        Ok(e)
457    } else {
458        Err(AuthError::Invalid(format!(
459            "{email:?} is not an email address"
460        )))
461    }
462}
463
464pub(crate) fn clean_name(name: &str) -> AuthResult<String> {
465    let n = name.trim();
466    if n.chars().count() > 100 || n.chars().any(char::is_control) {
467        return Err(AuthError::Invalid(
468            "name: at most 100 characters, no control characters".into(),
469        ));
470    }
471    Ok(n.to_string())
472}
473
474fn clip(s: Option<String>, n: usize) -> Option<String> {
475    s.map(|s| s.chars().filter(|c| !c.is_control()).take(n).collect())
476}
477
478pub(crate) const USER_COLS: &str = "u.id, u.email, u.name, u.platform_admin, u.created_at, u.disabled, u.password_hash IS NOT NULL";
479
480pub(crate) fn user_row(r: &Row, at: usize) -> rusqlite::Result<User> {
481    Ok(User {
482        id: r.get(at)?,
483        email: r.get(at + 1)?,
484        name: r.get(at + 2)?,
485        platform_admin: r.get(at + 3)?,
486        created_at: r.get(at + 4)?,
487        disabled: r.get(at + 5)?,
488        has_password: r.get(at + 6)?,
489    })
490}
491
492pub(crate) fn org_col(r: &Row, i: usize) -> rusqlite::Result<OrgId> {
493    let s: String = r.get(i)?;
494    OrgId::new(s).map_err(|e| {
495        rusqlite::Error::FromSqlConversionFailure(i, rusqlite::types::Type::Text, Box::new(e))
496    })
497}
498
499fn opt_org_col(r: &Row, i: usize) -> rusqlite::Result<Option<OrgId>> {
500    match r.get::<_, Option<String>>(i)? {
501        None => Ok(None),
502        Some(_) => org_col(r, i).map(Some),
503    }
504}
505
506pub(crate) fn role_col(r: &Row, i: usize) -> rusqlite::Result<Role> {
507    let s: String = r.get(i)?;
508    Role::parse(&s).map_err(|e| {
509        rusqlite::Error::FromSqlConversionFailure(i, rusqlite::types::Type::Text, Box::new(e))
510    })
511}
512
513const INVITATION_COLS: &str =
514    "id, org, email, role, invited_by, created_at, expires_at, accepted_at";
515
516fn invitation_row(r: &Row) -> rusqlite::Result<Invitation> {
517    Ok(Invitation {
518        id: r.get(0)?,
519        org: org_col(r, 1)?,
520        email: r.get(2)?,
521        role: role_col(r, 3)?,
522        invited_by: r.get(4)?,
523        created_at: r.get(5)?,
524        expires_at: r.get(6)?,
525        accepted_at: r.get(7)?,
526    })
527}
528
529const TOKEN_COLS: &str = "id, name, user_id, org, created_at, last_used, expires_at, scopes";
530
531fn token_row(r: &Row) -> rusqlite::Result<ApiToken> {
532    Ok(ApiToken {
533        id: r.get(0)?,
534        name: r.get(1)?,
535        user_id: r.get(2)?,
536        org: opt_org_col(r, 3)?,
537        created_at: r.get(4)?,
538        last_used: r.get(5)?,
539        expires_at: r.get(6)?,
540        scopes: scopes_col(r.get(7)?),
541    })
542}
543
544fn scopes_col(v: Option<String>) -> Vec<String> {
545    v.and_then(|s| serde_json::from_str(&s).ok())
546        .unwrap_or_default()
547}
548
549/// What an API token may do on top of its role. A token with no scopes has
550/// the role's whole reach (agents administer their org by default); scopes
551/// only ever narrow it.
552#[derive(Debug, Clone, PartialEq, Eq)]
553pub enum Scope {
554    /// Read-only tools, never secret values.
555    Read,
556    /// `read`, plus deploys, redeploys, rollbacks, scaling and builds.
557    Deploy,
558    /// Everything the role allows, including tokens and members.
559    Admin,
560    /// Tools whose name matches a glob: `tool:app_*`.
561    Tools(String),
562}
563
564impl Scope {
565    pub fn parse(s: &str) -> AuthResult<Scope> {
566        let s = s.trim();
567        match s {
568            "read" => Ok(Scope::Read),
569            "deploy" => Ok(Scope::Deploy),
570            "admin" => Ok(Scope::Admin),
571            _ => match s.strip_prefix("tool:") {
572                Some(g)
573                    if !g.is_empty()
574                        && g.len() <= 128
575                        && g.bytes()
576                            .all(|b| b.is_ascii_alphanumeric() || b"_.-*?[]!^".contains(&b)) =>
577                {
578                    Ok(Scope::Tools(g.to_string()))
579                }
580                _ => Err(AuthError::Invalid(format!(
581                    "scope {s:?}: read, deploy, admin or tool:GLOB"
582                ))),
583            },
584        }
585    }
586
587    pub fn as_string(&self) -> String {
588        match self {
589            Scope::Read => "read".into(),
590            Scope::Deploy => "deploy".into(),
591            Scope::Admin => "admin".into(),
592            Scope::Tools(g) => format!("tool:{g}"),
593        }
594    }
595
596    /// Check a list, normalized and without duplicates.
597    pub fn normalize(v: &[String]) -> AuthResult<Vec<String>> {
598        if v.len() > 32 {
599            return Err(AuthError::Invalid("at most 32 scopes".into()));
600        }
601        let mut out: Vec<String> = Vec::new();
602        for s in v {
603            let s = Scope::parse(s)?.as_string();
604            if !out.contains(&s) {
605                out.push(s);
606            }
607        }
608        Ok(out)
609    }
610}
611
612impl AuthStore {
613    /// Open (creating and migrating) the database at `path`, default config.
614    pub fn open(path: impl AsRef<Path>) -> AuthResult<AuthStore> {
615        Self::open_with(path, AuthConfig::default())
616    }
617
618    pub fn open_with(path: impl AsRef<Path>, cfg: AuthConfig) -> AuthResult<AuthStore> {
619        let path = path.as_ref();
620        let conn = db::open(path)?;
621        Ok(Self::build(conn, Some(path.to_path_buf()), cfg))
622    }
623
624    /// A throwaway in-memory store (tests, previews).
625    pub fn in_memory(cfg: AuthConfig) -> AuthResult<AuthStore> {
626        Ok(Self::build(db::open_in_memory()?, None, cfg))
627    }
628
629    fn build(conn: Connection, path: Option<PathBuf>, cfg: AuthConfig) -> AuthStore {
630        AuthStore {
631            conn: Mutex::new(conn),
632            path,
633            per_email: RateLimiter::new(cfg.login_per_email),
634            per_ip: RateLimiter::new(cfg.attempts_per_ip),
635            resets: RateLimiter::new(cfg.resets_per_email),
636            cfg,
637            clock: Arc::new(unix_now),
638            dummy: OnceLock::new(),
639        }
640    }
641
642    /// Replace the clock (unix seconds), for tests of expiry.
643    pub fn with_clock(mut self, clock: Clock) -> Self {
644        self.clock = clock;
645        self
646    }
647
648    pub fn config(&self) -> &AuthConfig {
649        &self.cfg
650    }
651
652    pub fn path(&self) -> Option<&Path> {
653        self.path.as_deref()
654    }
655
656    pub fn now(&self) -> i64 {
657        (self.clock)()
658    }
659
660    pub(crate) fn db(&self) -> MutexGuard<'_, Connection> {
661        self.conn.lock().unwrap_or_else(|e| e.into_inner())
662    }
663
664    fn hash_pw(&self, pw: &str) -> AuthResult<String> {
665        secret::check_password_policy(pw)?;
666        secret::hash_password(pw, self.cfg.password_cost)
667    }
668
669    fn rate(&self, l: &RateLimiter, key: &str) -> AuthResult<()> {
670        l.take(key, self.now() as f64)
671            .map_err(|retry_after| AuthError::RateLimited { retry_after })
672    }
673
674    /// Count one unauthenticated attempt from `ip` (login, setup, invitation
675    /// acceptance, password resets).
676    pub fn limit_ip(&self, ip: Option<&str>) -> AuthResult<()> {
677        match ip {
678            Some(ip) => self.rate(&self.per_ip, ip),
679            None => Ok(()),
680        }
681    }
682
683    // ---- users ----
684
685    /// True until the first user exists.
686    pub fn setup_needed(&self) -> AuthResult<bool> {
687        let n: i64 = self
688            .db()
689            .query_row("SELECT COUNT(*) FROM users", [], |r| r.get(0))?;
690        Ok(n == 0)
691    }
692
693    /// Create the first user: a platform admin and owner of the `default`
694    /// org. Refused once any user exists.
695    pub fn create_first_admin(&self, email: &str, name: &str, password: &str) -> AuthResult<User> {
696        let email = normalize_email(email)?;
697        let name = clean_name(name)?;
698        let hash = self.hash_pw(password)?;
699        let now = self.now();
700        let mut db = self.db();
701        let tx = db.transaction_with_behavior(TransactionBehavior::Immediate)?;
702        let n: i64 = tx.query_row("SELECT COUNT(*) FROM users", [], |r| r.get(0))?;
703        if n > 0 {
704            return Err(AuthError::Conflict("setup is already done".into()));
705        }
706        tx.execute(
707            "INSERT INTO users (email, name, password_hash, platform_admin, created_at)
708             VALUES (?1, ?2, ?3, 1, ?4)",
709            params![email, name, hash, now],
710        )?;
711        let id = tx.last_insert_rowid();
712        let org = OrgId::default_org();
713        ensure_org_tx(&tx, &org, now)?;
714        tx.execute(
715            "INSERT INTO memberships (user_id, org, role, created_at) VALUES (?1, ?2, 'owner', ?3)",
716            params![id, org.as_str(), now],
717        )?;
718        tx.commit()?;
719        drop(db);
720        self.user(id)
721    }
722
723    /// Create a user. `password` may be `None` for an account that will sign
724    /// in through an external identity or reset its password.
725    pub fn create_user(
726        &self,
727        email: &str,
728        name: &str,
729        password: Option<&str>,
730        platform_admin: bool,
731    ) -> AuthResult<User> {
732        let email = normalize_email(email)?;
733        let name = clean_name(name)?;
734        let hash = password.map(|p| self.hash_pw(p)).transpose()?;
735        let now = self.now();
736        let db = self.db();
737        let r = db.execute(
738            "INSERT INTO users (email, name, password_hash, platform_admin, created_at)
739             VALUES (?1, ?2, ?3, ?4, ?5)",
740            params![email, name, hash, platform_admin, now],
741        );
742        match r {
743            Ok(_) => {
744                let id = db.last_insert_rowid();
745                drop(db);
746                self.user(id)
747            }
748            Err(rusqlite::Error::SqliteFailure(e, _))
749                if e.code == rusqlite::ErrorCode::ConstraintViolation =>
750            {
751                Err(AuthError::Conflict(format!(
752                    "a user with email {email} exists"
753                )))
754            }
755            Err(e) => Err(e.into()),
756        }
757    }
758
759    pub fn user(&self, id: i64) -> AuthResult<User> {
760        self.db()
761            .query_row(
762                &format!("SELECT {USER_COLS} FROM users u WHERE u.id = ?1"),
763                [id],
764                |r| user_row(r, 0),
765            )
766            .optional()?
767            .ok_or_else(|| AuthError::NotFound(format!("user {id}")))
768    }
769
770    pub fn user_by_email(&self, email: &str) -> AuthResult<Option<User>> {
771        let email = normalize_email(email)?;
772        Ok(self
773            .db()
774            .query_row(
775                &format!("SELECT {USER_COLS} FROM users u WHERE u.email = ?1"),
776                [email],
777                |r| user_row(r, 0),
778            )
779            .optional()?)
780    }
781
782    pub fn list_users(&self) -> AuthResult<Vec<User>> {
783        let db = self.db();
784        let mut st = db.prepare(&format!("SELECT {USER_COLS} FROM users u ORDER BY u.id"))?;
785        let rows = st.query_map([], |r| user_row(r, 0))?;
786        Ok(rows.collect::<rusqlite::Result<_>>()?)
787    }
788
789    /// Disable (or re-enable) a user. Disabling ends their sessions; their
790    /// tokens stop working while disabled.
791    pub fn set_disabled(&self, user_id: i64, disabled: bool) -> AuthResult<()> {
792        let db = self.db();
793        let n = db.execute(
794            "UPDATE users SET disabled = ?2 WHERE id = ?1",
795            params![user_id, disabled],
796        )?;
797        if n == 0 {
798            return Err(AuthError::NotFound(format!("user {user_id}")));
799        }
800        if disabled {
801            db.execute("DELETE FROM sessions WHERE user_id = ?1", [user_id])?;
802        }
803        Ok(())
804    }
805
806    /// When a user last did anything: the latest of their sessions' last use
807    /// and their tokens' last use (`None`: never, or nothing left to tell).
808    pub fn last_active(&self, user_id: i64) -> AuthResult<Option<i64>> {
809        Ok(self.db().query_row(
810            "SELECT MAX(t) FROM (
811                 SELECT MAX(last_seen) AS t FROM sessions WHERE user_id = ?1
812                 UNION ALL
813                 SELECT MAX(last_used) FROM api_tokens WHERE user_id = ?1)",
814            [user_id],
815            |r| r.get(0),
816        )?)
817    }
818
819    /// Enabled platform admins other than `except`.
820    pub fn other_platform_admins(&self, except: i64) -> AuthResult<i64> {
821        Ok(self.db().query_row(
822            "SELECT COUNT(*) FROM users WHERE platform_admin = 1 AND disabled = 0 AND id != ?1",
823            [except],
824            |r| r.get(0),
825        )?)
826    }
827
828    pub fn set_platform_admin(&self, user_id: i64, admin: bool) -> AuthResult<()> {
829        let n = self.db().execute(
830            "UPDATE users SET platform_admin = ?2 WHERE id = ?1",
831            params![user_id, admin],
832        )?;
833        if n == 0 {
834            return Err(AuthError::NotFound(format!("user {user_id}")));
835        }
836        Ok(())
837    }
838
839    /// Set a password without the old one (the local CLI, an admin). Ends
840    /// every session of the user.
841    pub fn set_password(&self, user_id: i64, password: &str) -> AuthResult<()> {
842        let hash = self.hash_pw(password)?;
843        let db = self.db();
844        let n = db.execute(
845            "UPDATE users SET password_hash = ?2 WHERE id = ?1",
846            params![user_id, hash],
847        )?;
848        if n == 0 {
849            return Err(AuthError::NotFound(format!("user {user_id}")));
850        }
851        db.execute("DELETE FROM sessions WHERE user_id = ?1", [user_id])?;
852        Ok(())
853    }
854
855    /// Change a password, proving the current one. Ends every other session
856    /// (`keep` is the caller's own).
857    pub fn change_password(
858        &self,
859        user_id: i64,
860        current: &str,
861        new: &str,
862        keep: Option<i64>,
863    ) -> AuthResult<()> {
864        let stored: Option<String> = self
865            .db()
866            .query_row(
867                "SELECT password_hash FROM users WHERE id = ?1",
868                [user_id],
869                |r| r.get(0),
870            )
871            .optional()?
872            .flatten();
873        let ok = match &stored {
874            Some(h) => secret::verify_password(current, h),
875            None => {
876                secret::verify_password(current, self.dummy());
877                false
878            }
879        };
880        if !ok {
881            return Err(AuthError::Forbidden("current password is wrong".into()));
882        }
883        let hash = self.hash_pw(new)?;
884        let db = self.db();
885        db.execute(
886            "UPDATE users SET password_hash = ?2 WHERE id = ?1",
887            params![user_id, hash],
888        )?;
889        db.execute(
890            "DELETE FROM sessions WHERE user_id = ?1 AND id IS NOT ?2",
891            params![user_id, keep],
892        )?;
893        Ok(())
894    }
895
896    fn dummy(&self) -> &str {
897        secret::dummy_hash(&self.dummy, self.cfg.password_cost)
898    }
899
900    // ---- sessions ----
901
902    /// Check an email and password and start a session. Every failure (no
903    /// such user, wrong password, disabled, no password set) is
904    /// [`AuthError::InvalidCredentials`] and costs one argon2 verification.
905    pub fn login(&self, email: &str, password: &str, meta: LoginMeta) -> AuthResult<NewSession> {
906        let key = email.trim().to_lowercase();
907        self.limit_ip(meta.ip.as_deref())?;
908        self.rate(&self.per_email, &key)?;
909        let found: Option<(i64, Option<String>, bool)> = self
910            .db()
911            .query_row(
912                "SELECT id, password_hash, disabled FROM users WHERE email = ?1",
913                [&key],
914                |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
915            )
916            .optional()?;
917        let user_id = match found {
918            Some((id, Some(h), disabled)) => {
919                let ok = secret::verify_password(password, &h);
920                (ok && !disabled).then_some(id)
921            }
922            _ => {
923                secret::verify_password(password, self.dummy());
924                None
925            }
926        };
927        let user_id = user_id.ok_or(AuthError::InvalidCredentials)?;
928        self.prune()?;
929        self.start_session(user_id, meta)
930    }
931
932    /// Start a session for a user already proven by other means (an
933    /// accepted invitation, a password reset, an external identity).
934    pub fn start_session(&self, user_id: i64, meta: LoginMeta) -> AuthResult<NewSession> {
935        let (token, hash) = secret::new_token(TokenKind::Session)?;
936        let now = self.now();
937        let expires = now + secs(self.cfg.session_max_age);
938        let (ua, ip) = (clip(meta.user_agent, 256), clip(meta.ip, 64));
939        let db = self.db();
940        db.execute(
941            "INSERT INTO sessions (token_hash, user_id, created_at, last_seen, expires_at, user_agent, ip)
942             VALUES (?1, ?2, ?3, ?3, ?4, ?5, ?6)",
943            params![hash, user_id, now, expires, ua, ip],
944        )?;
945        let id = db.last_insert_rowid();
946        Ok(NewSession {
947            token,
948            session: Session {
949                id,
950                user_id,
951                created_at: now,
952                last_seen: now,
953                expires_at: expires,
954                idle_expires_at: self.idle_end(now, expires),
955                user_agent: ua,
956                ip,
957            },
958        })
959    }
960
961    fn idle_end(&self, last_seen: i64, expires: i64) -> i64 {
962        (last_seen + secs(self.cfg.session_idle)).min(expires)
963    }
964
965    fn session_row(&self, r: &Row, at: usize) -> rusqlite::Result<Session> {
966        let last_seen: i64 = r.get(at + 3)?;
967        let expires: i64 = r.get(at + 4)?;
968        Ok(Session {
969            id: r.get(at)?,
970            user_id: r.get(at + 1)?,
971            created_at: r.get(at + 2)?,
972            last_seen,
973            expires_at: expires,
974            idle_expires_at: self.idle_end(last_seen, expires),
975            user_agent: r.get(at + 5)?,
976            ip: r.get(at + 6)?,
977        })
978    }
979
980    /// The live session for `token`, sliding its idle expiry. An expired one
981    /// is deleted; a disabled user has none.
982    pub fn session(&self, token: &str) -> AuthResult<Option<(User, Session)>> {
983        if !secret::well_formed(token, TokenKind::Session) {
984            return Ok(None);
985        }
986        let hash = secret::hash_token(token);
987        let now = self.now();
988        let db = self.db();
989        let found = db
990            .query_row(
991                &format!(
992                    "SELECT s.token_hash, s.id, s.user_id, s.created_at, s.last_seen, s.expires_at,
993                            s.user_agent, s.ip, {USER_COLS}
994                     FROM sessions s JOIN users u ON u.id = s.user_id WHERE s.token_hash = ?1"
995                ),
996                [&hash],
997                |r| {
998                    Ok((
999                        r.get::<_, Vec<u8>>(0)?,
1000                        self.session_row(r, 1)?,
1001                        user_row(r, 8)?,
1002                    ))
1003                },
1004            )
1005            .optional()?;
1006        let Some((stored, mut s, user)) = found else {
1007            return Ok(None);
1008        };
1009        if !secret::ct_eq(&stored, &hash) {
1010            return Ok(None);
1011        }
1012        if now >= s.expires_at || now >= s.idle_expires_at {
1013            db.execute("DELETE FROM sessions WHERE id = ?1", [s.id])?;
1014            return Ok(None);
1015        }
1016        if user.disabled {
1017            return Ok(None);
1018        }
1019        if now - s.last_seen >= TOUCH_EVERY {
1020            db.execute(
1021                "UPDATE sessions SET last_seen = ?2 WHERE id = ?1",
1022                params![s.id, now],
1023            )?;
1024            s.last_seen = now;
1025            s.idle_expires_at = self.idle_end(now, s.expires_at);
1026        }
1027        Ok(Some((user, s)))
1028    }
1029
1030    /// The principal for a Cloudflare Access identity: the enabled user with
1031    /// that email, with all their memberships.
1032    pub fn principal_for_email(&self, email: &str) -> AuthResult<Option<Principal>> {
1033        let Some(user) = self.user_by_email(email)? else {
1034            return Ok(None);
1035        };
1036        if user.disabled {
1037            return Ok(None);
1038        }
1039        let orgs = self
1040            .memberships(user.id)?
1041            .into_iter()
1042            .map(|m| (m.org, m.role))
1043            .collect();
1044        Ok(Some(Principal {
1045            platform_admin: user.platform_admin,
1046            user,
1047            kind: PrincipalKind::Access,
1048            orgs,
1049            downscoped: None,
1050        }))
1051    }
1052
1053    /// The principal behind a session token.
1054    pub fn authenticate_session(&self, token: &str) -> AuthResult<Option<Principal>> {
1055        let Some((user, s)) = self.session(token)? else {
1056            return Ok(None);
1057        };
1058        let orgs = self
1059            .memberships(user.id)?
1060            .into_iter()
1061            .map(|m| (m.org, m.role))
1062            .collect();
1063        Ok(Some(Principal {
1064            platform_admin: user.platform_admin,
1065            user,
1066            kind: PrincipalKind::Session { id: s.id },
1067            orgs,
1068            downscoped: None,
1069        }))
1070    }
1071
1072    /// End the session holding `token`. True if there was one.
1073    pub fn logout(&self, token: &str) -> AuthResult<bool> {
1074        if !secret::well_formed(token, TokenKind::Session) {
1075            return Ok(false);
1076        }
1077        let n = self.db().execute(
1078            "DELETE FROM sessions WHERE token_hash = ?1",
1079            [secret::hash_token(token)],
1080        )?;
1081        Ok(n > 0)
1082    }
1083
1084    /// A user's live sessions, newest first.
1085    pub fn list_sessions(&self, user_id: i64) -> AuthResult<Vec<Session>> {
1086        let now = self.now();
1087        let db = self.db();
1088        let mut st = db.prepare(
1089            "SELECT id, user_id, created_at, last_seen, expires_at, user_agent, ip
1090             FROM sessions WHERE user_id = ?1 ORDER BY id DESC",
1091        )?;
1092        let rows = st.query_map([user_id], |r| self.session_row(r, 0))?;
1093        let all: Vec<Session> = rows.collect::<rusqlite::Result<_>>()?;
1094        Ok(all
1095            .into_iter()
1096            .filter(|s| now < s.expires_at && now < s.idle_expires_at)
1097            .collect())
1098    }
1099
1100    /// End one of a user's sessions. True if it existed.
1101    pub fn revoke_session(&self, user_id: i64, session_id: i64) -> AuthResult<bool> {
1102        let n = self.db().execute(
1103            "DELETE FROM sessions WHERE id = ?1 AND user_id = ?2",
1104            params![session_id, user_id],
1105        )?;
1106        Ok(n > 0)
1107    }
1108
1109    /// End all of a user's sessions but `keep`. Returns how many ended.
1110    pub fn revoke_sessions(&self, user_id: i64, keep: Option<i64>) -> AuthResult<usize> {
1111        Ok(self.db().execute(
1112            "DELETE FROM sessions WHERE user_id = ?1 AND id IS NOT ?2",
1113            params![user_id, keep],
1114        )?)
1115    }
1116
1117    /// Delete expired sessions, invitations and resets.
1118    pub fn prune(&self) -> AuthResult<()> {
1119        let now = self.now();
1120        let idle = secs(self.cfg.session_idle);
1121        self.db().execute_batch(&format!(
1122            "DELETE FROM sessions WHERE expires_at <= {now} OR last_seen + {idle} <= {now};
1123             DELETE FROM invitations WHERE accepted_at IS NULL AND expires_at <= {now};
1124             DELETE FROM password_resets WHERE expires_at <= {now} OR used_at IS NOT NULL;"
1125        ))?;
1126        Ok(())
1127    }
1128
1129    // ---- orgs and memberships ----
1130
1131    /// Record an org (idempotent). The org's runtime is not this module's;
1132    /// this row anchors memberships, invitations and tokens.
1133    pub fn ensure_org(&self, org: &OrgId) -> AuthResult<()> {
1134        ensure_org_tx(&self.db(), org, self.now())
1135    }
1136
1137    /// Forget an org: its memberships, invitations and tokens go with it.
1138    pub fn delete_org(&self, org: &OrgId) -> AuthResult<bool> {
1139        let n = self
1140            .db()
1141            .execute("DELETE FROM orgs WHERE name = ?1", [org.as_str()])?;
1142        Ok(n > 0)
1143    }
1144
1145    pub fn list_orgs(&self) -> AuthResult<Vec<OrgId>> {
1146        let db = self.db();
1147        let mut st = db.prepare("SELECT name FROM orgs ORDER BY name")?;
1148        let rows = st.query_map([], |r| org_col(r, 0))?;
1149        Ok(rows.collect::<rusqlite::Result<_>>()?)
1150    }
1151
1152    pub fn memberships(&self, user_id: i64) -> AuthResult<Vec<Membership>> {
1153        let db = self.db();
1154        let mut st =
1155            db.prepare("SELECT org, role FROM memberships WHERE user_id = ?1 ORDER BY org")?;
1156        let rows = st.query_map([user_id], |r| {
1157            Ok(Membership {
1158                org: org_col(r, 0)?,
1159                role: role_col(r, 1)?,
1160            })
1161        })?;
1162        Ok(rows.collect::<rusqlite::Result<_>>()?)
1163    }
1164
1165    pub fn list_members(&self, org: &OrgId) -> AuthResult<Vec<(User, Role)>> {
1166        let db = self.db();
1167        let mut st = db.prepare(&format!(
1168            "SELECT {USER_COLS}, m.role FROM memberships m JOIN users u ON u.id = m.user_id
1169             WHERE m.org = ?1 ORDER BY u.email"
1170        ))?;
1171        let rows = st.query_map([org.as_str()], |r| Ok((user_row(r, 0)?, role_col(r, 7)?)))?;
1172        Ok(rows.collect::<rusqlite::Result<_>>()?)
1173    }
1174
1175    /// Add `user` to `org` with `role`, or change their role. Refuses to
1176    /// demote the org's last owner.
1177    pub fn set_member(&self, org: &OrgId, user_id: i64, role: Role) -> AuthResult<()> {
1178        let now = self.now();
1179        let mut db = self.db();
1180        let tx = db.transaction_with_behavior(TransactionBehavior::Immediate)?;
1181        ensure_org_tx(&tx, org, now)?;
1182        if role != Role::Owner {
1183            refuse_last_owner(&tx, org, user_id, "demote")?;
1184        }
1185        tx.execute(
1186            "INSERT INTO memberships (user_id, org, role, created_at) VALUES (?1, ?2, ?3, ?4)
1187             ON CONFLICT (user_id, org) DO UPDATE SET role = excluded.role",
1188            params![user_id, org.as_str(), role.as_str(), now],
1189        )
1190        .map_err(|e| match e {
1191            rusqlite::Error::SqliteFailure(f, _)
1192                if f.code == rusqlite::ErrorCode::ConstraintViolation =>
1193            {
1194                AuthError::NotFound(format!("user {user_id}"))
1195            }
1196            e => e.into(),
1197        })?;
1198        tx.commit()?;
1199        Ok(())
1200    }
1201
1202    /// Remove `user` from `org`, and their tokens confined to it. Refuses to
1203    /// remove the last owner. True if they were a member.
1204    pub fn remove_member(&self, org: &OrgId, user_id: i64) -> AuthResult<bool> {
1205        let mut db = self.db();
1206        let tx = db.transaction_with_behavior(TransactionBehavior::Immediate)?;
1207        refuse_last_owner(&tx, org, user_id, "remove")?;
1208        let n = tx.execute(
1209            "DELETE FROM memberships WHERE org = ?1 AND user_id = ?2",
1210            params![org.as_str(), user_id],
1211        )?;
1212        tx.execute(
1213            "DELETE FROM api_tokens WHERE org = ?1 AND user_id = ?2",
1214            params![org.as_str(), user_id],
1215        )?;
1216        tx.commit()?;
1217        Ok(n > 0)
1218    }
1219
1220    // ---- invitations ----
1221
1222    /// Invite `email` to `org` as `role`. Replaces any pending invitation for
1223    /// the same address and org. Authorization is the caller's
1224    /// ([`Principal::max_grant`]); `invited_by` is `None` for the local CLI.
1225    pub fn create_invitation(
1226        &self,
1227        invited_by: Option<i64>,
1228        org: &OrgId,
1229        email: &str,
1230        role: Role,
1231    ) -> AuthResult<NewInvitation> {
1232        let email = normalize_email(email)?;
1233        let (token, hash) = secret::new_token(TokenKind::Invitation)?;
1234        let now = self.now();
1235        let expires = now + secs(self.cfg.invitation_ttl);
1236        let mut db = self.db();
1237        let tx = db.transaction_with_behavior(TransactionBehavior::Immediate)?;
1238        ensure_org_tx(&tx, org, now)?;
1239        tx.execute(
1240            "DELETE FROM invitations WHERE org = ?1 AND email = ?2 AND accepted_at IS NULL",
1241            params![org.as_str(), email],
1242        )?;
1243        tx.execute(
1244            "INSERT INTO invitations (token_hash, org, email, role, invited_by, created_at, expires_at)
1245             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
1246            params![hash, org.as_str(), email, role.as_str(), invited_by, now, expires],
1247        )?;
1248        let id = tx.last_insert_rowid();
1249        tx.commit()?;
1250        Ok(NewInvitation {
1251            token,
1252            invitation: Invitation {
1253                id,
1254                org: org.clone(),
1255                email,
1256                role,
1257                invited_by,
1258                created_at: now,
1259                expires_at: expires,
1260                accepted_at: None,
1261            },
1262        })
1263    }
1264
1265    /// Pending (unaccepted, unexpired) invitations to `org`.
1266    pub fn list_invitations(&self, org: &OrgId) -> AuthResult<Vec<Invitation>> {
1267        let db = self.db();
1268        let mut st = db.prepare(&format!(
1269            "SELECT {INVITATION_COLS} FROM invitations
1270             WHERE org = ?1 AND accepted_at IS NULL AND expires_at > ?2 ORDER BY id"
1271        ))?;
1272        let rows = st.query_map(params![org.as_str(), self.now()], invitation_row)?;
1273        Ok(rows.collect::<rusqlite::Result<_>>()?)
1274    }
1275
1276    /// Withdraw a pending invitation. True if there was one.
1277    pub fn revoke_invitation(&self, org: &OrgId, id: i64) -> AuthResult<bool> {
1278        let n = self.db().execute(
1279            "DELETE FROM invitations WHERE id = ?1 AND org = ?2 AND accepted_at IS NULL",
1280            params![id, org.as_str()],
1281        )?;
1282        Ok(n > 0)
1283    }
1284
1285    /// The pending invitation for `token`, if it is valid.
1286    pub fn invitation(&self, token: &str) -> AuthResult<Option<Invitation>> {
1287        if !secret::well_formed(token, TokenKind::Invitation) {
1288            return Ok(None);
1289        }
1290        let hash = secret::hash_token(token);
1291        let found = self
1292            .db()
1293            .query_row(
1294                &format!(
1295                    "SELECT token_hash, {INVITATION_COLS} FROM invitations WHERE token_hash = ?1"
1296                ),
1297                [&hash],
1298                |r| {
1299                    let stored: Vec<u8> = r.get(0)?;
1300                    let inv = Invitation {
1301                        id: r.get(1)?,
1302                        org: org_col(r, 2)?,
1303                        email: r.get(3)?,
1304                        role: role_col(r, 4)?,
1305                        invited_by: r.get(5)?,
1306                        created_at: r.get(6)?,
1307                        expires_at: r.get(7)?,
1308                        accepted_at: r.get(8)?,
1309                    };
1310                    Ok((stored, inv))
1311                },
1312            )
1313            .optional()?;
1314        Ok(found.and_then(|(stored, inv)| {
1315            (secret::ct_eq(&stored, &hash)
1316                && inv.accepted_at.is_none()
1317                && self.now() < inv.expires_at)
1318                .then_some(inv)
1319        }))
1320    }
1321
1322    /// Accept an invitation without a session. A new address gets an account
1323    /// with `name` and `password`; an existing account must prove `password`
1324    /// (the invitation alone does not let anyone in as someone else).
1325    pub fn accept_invitation(
1326        &self,
1327        token: &str,
1328        name: &str,
1329        password: &str,
1330    ) -> AuthResult<Accepted> {
1331        let inv = self
1332            .invitation(token)?
1333            .ok_or(AuthError::InvalidToken("invitation"))?;
1334        let existing: Option<(i64, Option<String>, bool)> = self
1335            .db()
1336            .query_row(
1337                "SELECT id, password_hash, disabled FROM users WHERE email = ?1",
1338                [&inv.email],
1339                |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
1340            )
1341            .optional()?;
1342        let new_user = match &existing {
1343            Some((_, Some(h), disabled)) => {
1344                if !secret::verify_password(password, h) || *disabled {
1345                    return Err(AuthError::InvalidCredentials);
1346                }
1347                None
1348            }
1349            Some((_, None, _)) => {
1350                secret::verify_password(password, self.dummy());
1351                return Err(AuthError::InvalidCredentials);
1352            }
1353            None => Some((clean_name(name)?, self.hash_pw(password)?)),
1354        };
1355        self.finish_accept(&inv, existing.map(|e| e.0), new_user)
1356    }
1357
1358    /// Accept an invitation as the signed-in user, whose email must match.
1359    pub fn accept_invitation_as(&self, token: &str, user_id: i64) -> AuthResult<Accepted> {
1360        let inv = self
1361            .invitation(token)?
1362            .ok_or(AuthError::InvalidToken("invitation"))?;
1363        let user = self.user(user_id)?;
1364        if user.email != inv.email {
1365            return Err(AuthError::Forbidden(format!(
1366                "this invitation is for {}, and you are signed in as {}",
1367                inv.email, user.email
1368            )));
1369        }
1370        self.finish_accept(&inv, Some(user_id), None)
1371    }
1372
1373    fn finish_accept(
1374        &self,
1375        inv: &Invitation,
1376        user_id: Option<i64>,
1377        new_user: Option<(String, String)>,
1378    ) -> AuthResult<Accepted> {
1379        let now = self.now();
1380        let mut db = self.db();
1381        let tx = db.transaction_with_behavior(TransactionBehavior::Immediate)?;
1382        // Single use: whoever marks it first wins.
1383        let n = tx.execute(
1384            "UPDATE invitations SET accepted_at = ?2 WHERE id = ?1 AND accepted_at IS NULL",
1385            params![inv.id, now],
1386        )?;
1387        if n == 0 {
1388            return Err(AuthError::InvalidToken("invitation"));
1389        }
1390        let (uid, created) = match (user_id, new_user) {
1391            (Some(id), _) => (id, false),
1392            (None, Some((name, hash))) => {
1393                tx.execute(
1394                    "INSERT INTO users (email, name, password_hash, created_at) VALUES (?1, ?2, ?3, ?4)",
1395                    params![inv.email, name, hash, now],
1396                )
1397                .map_err(|e| match e {
1398                    rusqlite::Error::SqliteFailure(f, _)
1399                        if f.code == rusqlite::ErrorCode::ConstraintViolation =>
1400                    {
1401                        AuthError::Conflict(format!("a user with email {} exists", inv.email))
1402                    }
1403                    e => e.into(),
1404                })?;
1405                (tx.last_insert_rowid(), true)
1406            }
1407            (None, None) => return Err(AuthError::Internal("accept: no user".into())),
1408        };
1409        tx.execute(
1410            "UPDATE invitations SET accepted_by = ?2 WHERE id = ?1",
1411            params![inv.id, uid],
1412        )?;
1413        // Never lower an existing role by accepting a lesser invitation.
1414        let current: Option<Role> = tx
1415            .query_row(
1416                "SELECT role FROM memberships WHERE user_id = ?1 AND org = ?2",
1417                params![uid, inv.org.as_str()],
1418                |r| role_col(r, 0),
1419            )
1420            .optional()?;
1421        let role = current.map_or(inv.role, |c| c.max(inv.role));
1422        tx.execute(
1423            "INSERT INTO memberships (user_id, org, role, created_at) VALUES (?1, ?2, ?3, ?4)
1424             ON CONFLICT (user_id, org) DO UPDATE SET role = excluded.role",
1425            params![uid, inv.org.as_str(), role.as_str(), now],
1426        )?;
1427        tx.commit()?;
1428        drop(db);
1429        Ok(Accepted {
1430            user: self.user(uid)?,
1431            membership: Membership {
1432                org: inv.org.clone(),
1433                role,
1434            },
1435            created,
1436        })
1437    }
1438
1439    // ---- API tokens ----
1440
1441    /// Make an API token for `user_id`. Confined to `org` when given (the
1442    /// user must belong to it, or be a platform admin); a platform token
1443    /// (`org: None`) is for platform admins only. `expires: None` never expires.
1444    pub fn create_api_token(
1445        &self,
1446        user_id: i64,
1447        org: Option<&OrgId>,
1448        name: &str,
1449        expires: Option<Duration>,
1450    ) -> AuthResult<NewApiToken> {
1451        self.create_api_token_scoped(user_id, org, name, expires, &[])
1452    }
1453
1454    /// [`Self::create_api_token`], narrowed to `scopes` ([`Scope`]).
1455    pub fn create_api_token_scoped(
1456        &self,
1457        user_id: i64,
1458        org: Option<&OrgId>,
1459        name: &str,
1460        expires: Option<Duration>,
1461        scopes: &[String],
1462    ) -> AuthResult<NewApiToken> {
1463        let scopes = Scope::normalize(scopes)?;
1464        let name = name.trim();
1465        if name.is_empty() || name.chars().count() > 100 || name.chars().any(char::is_control) {
1466            return Err(AuthError::Invalid(
1467                "token name: 1 to 100 characters, no control characters".into(),
1468            ));
1469        }
1470        let user = self.user(user_id)?;
1471        if user.disabled {
1472            return Err(AuthError::Forbidden(format!(
1473                "user {} is disabled",
1474                user.email
1475            )));
1476        }
1477        match org {
1478            None if !user.platform_admin => {
1479                return Err(AuthError::Forbidden(
1480                    "only a platform admin can make a token without an org".into(),
1481                ));
1482            }
1483            Some(o) if !user.platform_admin && self.role_of(user_id, o)?.is_none() => {
1484                return Err(AuthError::Forbidden(format!(
1485                    "{} is not a member of org {o}",
1486                    user.email
1487                )));
1488            }
1489            _ => {}
1490        }
1491        let (token, hash) = secret::new_token(TokenKind::Api)?;
1492        let now = self.now();
1493        let expires_at = expires.map(|d| now + secs(d));
1494        let db = self.db();
1495        if let Some(o) = org {
1496            ensure_org_tx(&db, o, now)?;
1497        }
1498        db.execute(
1499            "INSERT INTO api_tokens (token_hash, name, user_id, org, created_at, expires_at, scopes)
1500             VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
1501            params![
1502                hash,
1503                name,
1504                user_id,
1505                org.map(OrgId::as_str),
1506                now,
1507                expires_at,
1508                (!scopes.is_empty()).then(|| serde_json::to_string(&scopes).unwrap_or_default())
1509            ],
1510        )?;
1511        Ok(NewApiToken {
1512            token,
1513            info: ApiToken {
1514                id: db.last_insert_rowid(),
1515                name: name.to_string(),
1516                user_id,
1517                org: org.cloned(),
1518                created_at: now,
1519                last_used: None,
1520                expires_at,
1521                scopes,
1522            },
1523        })
1524    }
1525
1526    fn role_of(&self, user_id: i64, org: &OrgId) -> AuthResult<Option<Role>> {
1527        Ok(self
1528            .db()
1529            .query_row(
1530                "SELECT role FROM memberships WHERE user_id = ?1 AND org = ?2",
1531                params![user_id, org.as_str()],
1532                |r| role_col(r, 0),
1533            )
1534            .optional()?)
1535    }
1536
1537    /// The principal behind an API token. Expired tokens, disabled users, and
1538    /// org tokens whose user has left the org authenticate nobody.
1539    pub fn authenticate_token(&self, token: &str) -> AuthResult<Option<Principal>> {
1540        if !secret::well_formed(token, TokenKind::Api) {
1541            return Ok(None);
1542        }
1543        let hash = secret::hash_token(token);
1544        let now = self.now();
1545        let found = self
1546            .db()
1547            .query_row(
1548                &format!(
1549                    "SELECT t.token_hash, t.id, t.org, t.expires_at, t.last_used, t.name, t.scopes,
1550                     {USER_COLS} FROM api_tokens t JOIN users u ON u.id = t.user_id
1551                     WHERE t.token_hash = ?1"
1552                ),
1553                [&hash],
1554                |r| {
1555                    Ok((
1556                        r.get::<_, Vec<u8>>(0)?,
1557                        r.get::<_, i64>(1)?,
1558                        opt_org_col(r, 2)?,
1559                        r.get::<_, Option<i64>>(3)?,
1560                        r.get::<_, Option<i64>>(4)?,
1561                        r.get::<_, String>(5)?,
1562                        scopes_col(r.get(6)?),
1563                        user_row(r, 7)?,
1564                    ))
1565                },
1566            )
1567            .optional()?;
1568        let Some((stored, id, org, expires, last_used, name, scopes, user)) = found else {
1569            return Ok(None);
1570        };
1571        if !secret::ct_eq(&stored, &hash) || expires.is_some_and(|e| now >= e) || user.disabled {
1572            return Ok(None);
1573        }
1574        let (orgs, platform_admin) = match &org {
1575            Some(o) => {
1576                let role = match self.role_of(user.id, o)? {
1577                    Some(r) => r,
1578                    // A platform admin reaches every org; confined here.
1579                    None if user.platform_admin => Role::Owner,
1580                    None => return Ok(None),
1581                };
1582                (vec![(o.clone(), role)], false)
1583            }
1584            None if user.platform_admin => (
1585                self.memberships(user.id)?
1586                    .into_iter()
1587                    .map(|m| (m.org, m.role))
1588                    .collect(),
1589                true,
1590            ),
1591            // A platform token whose user is no longer a platform admin.
1592            None => return Ok(None),
1593        };
1594        if last_used.is_none_or(|l| now - l >= TOUCH_EVERY) {
1595            self.db().execute(
1596                "UPDATE api_tokens SET last_used = ?2 WHERE id = ?1",
1597                params![id, now],
1598            )?;
1599        }
1600        Ok(Some(Principal {
1601            user,
1602            kind: PrincipalKind::ApiToken {
1603                id,
1604                org,
1605                name,
1606                scopes,
1607            },
1608            orgs,
1609            platform_admin,
1610            downscoped: None,
1611        }))
1612    }
1613
1614    pub fn api_token(&self, id: i64) -> AuthResult<ApiToken> {
1615        self.db()
1616            .query_row(
1617                &format!("SELECT {TOKEN_COLS} FROM api_tokens WHERE id = ?1"),
1618                [id],
1619                token_row,
1620            )
1621            .optional()?
1622            .ok_or_else(|| AuthError::NotFound(format!("token {id}")))
1623    }
1624
1625    /// A user's tokens.
1626    pub fn list_api_tokens(&self, user_id: i64) -> AuthResult<Vec<ApiToken>> {
1627        let db = self.db();
1628        let mut st = db.prepare(&format!(
1629            "SELECT {TOKEN_COLS} FROM api_tokens WHERE user_id = ?1 ORDER BY id"
1630        ))?;
1631        let rows = st.query_map([user_id], token_row)?;
1632        Ok(rows.collect::<rusqlite::Result<_>>()?)
1633    }
1634
1635    /// Every token confined to `org`, whoever made it.
1636    pub fn list_org_api_tokens(&self, org: &OrgId) -> AuthResult<Vec<ApiToken>> {
1637        let db = self.db();
1638        let mut st = db.prepare(&format!(
1639            "SELECT {TOKEN_COLS} FROM api_tokens WHERE org = ?1 ORDER BY id"
1640        ))?;
1641        let rows = st.query_map([org.as_str()], token_row)?;
1642        Ok(rows.collect::<rusqlite::Result<_>>()?)
1643    }
1644
1645    /// Every token (the local CLI).
1646    pub fn list_all_api_tokens(&self) -> AuthResult<Vec<ApiToken>> {
1647        let db = self.db();
1648        let mut st = db.prepare(&format!("SELECT {TOKEN_COLS} FROM api_tokens ORDER BY id"))?;
1649        let rows = st.query_map([], token_row)?;
1650        Ok(rows.collect::<rusqlite::Result<_>>()?)
1651    }
1652
1653    /// Delete a token. True if it existed. Authorization is the caller's.
1654    pub fn revoke_api_token(&self, id: i64) -> AuthResult<bool> {
1655        let n = self
1656            .db()
1657            .execute("DELETE FROM api_tokens WHERE id = ?1", [id])?;
1658        Ok(n > 0)
1659    }
1660
1661    // ---- password resets ----
1662
1663    /// A one-hour reset token for `email`, or `None` when there is no such
1664    /// (enabled) user. The caller delivers it, and must answer the same way
1665    /// either way so the endpoint does not reveal who has an account.
1666    pub fn request_password_reset(&self, email: &str) -> AuthResult<Option<String>> {
1667        let key = email.trim().to_lowercase();
1668        self.rate(&self.resets, &key)?;
1669        let Some(user) = self.user_by_email(&key).ok().flatten() else {
1670            return Ok(None);
1671        };
1672        if user.disabled {
1673            return Ok(None);
1674        }
1675        let (token, hash) = secret::new_token(TokenKind::PasswordReset)?;
1676        let now = self.now();
1677        let db = self.db();
1678        // Only the newest link works.
1679        db.execute("DELETE FROM password_resets WHERE user_id = ?1", [user.id])?;
1680        db.execute(
1681            "INSERT INTO password_resets (token_hash, user_id, created_at, expires_at)
1682             VALUES (?1, ?2, ?3, ?4)",
1683            params![hash, user.id, now, now + secs(self.cfg.reset_ttl)],
1684        )?;
1685        Ok(Some(token))
1686    }
1687
1688    /// Set a new password with a reset token. Single use; ends every session.
1689    pub fn reset_password(&self, token: &str, password: &str) -> AuthResult<User> {
1690        if !secret::well_formed(token, TokenKind::PasswordReset) {
1691            return Err(AuthError::InvalidToken("reset link"));
1692        }
1693        let hash_pw = self.hash_pw(password)?;
1694        let hash = secret::hash_token(token);
1695        let now = self.now();
1696        let mut db = self.db();
1697        let tx = db.transaction_with_behavior(TransactionBehavior::Immediate)?;
1698        // (hash, id, user, expires, used)
1699        type ResetRow = (Vec<u8>, i64, i64, i64, Option<i64>);
1700        let found: Option<ResetRow> = tx
1701            .query_row(
1702                "SELECT token_hash, id, user_id, expires_at, used_at FROM password_resets
1703                 WHERE token_hash = ?1",
1704                [&hash],
1705                |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?)),
1706            )
1707            .optional()?;
1708        let Some((stored, id, user_id, expires, used)) = found else {
1709            return Err(AuthError::InvalidToken("reset link"));
1710        };
1711        if !secret::ct_eq(&stored, &hash) || used.is_some() || now >= expires {
1712            return Err(AuthError::InvalidToken("reset link"));
1713        }
1714        tx.execute(
1715            "UPDATE password_resets SET used_at = ?2 WHERE id = ?1",
1716            params![id, now],
1717        )?;
1718        tx.execute(
1719            "UPDATE users SET password_hash = ?2 WHERE id = ?1",
1720            params![user_id, hash_pw],
1721        )?;
1722        tx.execute("DELETE FROM sessions WHERE user_id = ?1", [user_id])?;
1723        tx.commit()?;
1724        drop(db);
1725        self.user(user_id)
1726    }
1727}
1728
1729pub(crate) fn ensure_org_tx(conn: &Connection, org: &OrgId, now: i64) -> AuthResult<()> {
1730    conn.execute(
1731        "INSERT INTO orgs (name, created_at) VALUES (?1, ?2) ON CONFLICT (name) DO NOTHING",
1732        params![org.as_str(), now],
1733    )?;
1734    Ok(())
1735}
1736
1737/// An org keeps at least one owner while it has any.
1738fn refuse_last_owner(conn: &Connection, org: &OrgId, user_id: i64, verb: &str) -> AuthResult<()> {
1739    let is_owner: bool = conn
1740        .query_row(
1741            "SELECT role = 'owner' FROM memberships WHERE org = ?1 AND user_id = ?2",
1742            params![org.as_str(), user_id],
1743            |r| r.get(0),
1744        )
1745        .optional()?
1746        .unwrap_or(false);
1747    if !is_owner {
1748        return Ok(());
1749    }
1750    let owners: i64 = conn.query_row(
1751        "SELECT COUNT(*) FROM memberships WHERE org = ?1 AND role = 'owner'",
1752        [org.as_str()],
1753        |r| r.get(0),
1754    )?;
1755    if owners <= 1 {
1756        return Err(AuthError::Conflict(format!(
1757            "cannot {verb} the last owner of org {org}; make someone else owner first"
1758        )));
1759    }
1760    Ok(())
1761}
1762
1763#[cfg(test)]
1764mod tests;