1mod 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#[derive(Debug, thiserror::Error)]
50pub enum AuthError {
51 #[error("invalid email or password")]
52 InvalidCredentials,
53 #[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 #[error("{message}")]
72 Refused { code: &'static str, message: String },
73 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
99#[serde(rename_all = "snake_case")]
100pub enum Role {
101 Viewer,
104 Member,
105 Admin,
106 Owner,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
111pub enum Permission {
112 ReadOrg,
115 AdminOrg,
117 ManageMembers,
119 DeleteOrg,
121}
122
123impl Role {
124 pub const ALL: [Role; 4] = [Role::Viewer, Role::Member, Role::Admin, Role::Owner];
125
126 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#[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 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#[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#[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 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#[derive(Debug, Clone)]
228pub struct Accepted {
229 pub user: User,
230 pub membership: Membership,
231 pub created: bool,
233}
234
235#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
237pub struct ApiToken {
238 pub id: i64,
239 pub name: String,
240 pub user_id: i64,
241 pub org: Option<OrgId>,
244 pub created_at: i64,
245 pub last_used: Option<i64>,
246 pub expires_at: Option<i64>,
247 pub scopes: Vec<String>,
250}
251
252#[derive(Debug, Clone)]
253pub struct NewApiToken {
254 pub token: String,
255 pub info: ApiToken,
256}
257
258#[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 #[serde(default)]
270 name: String,
271 #[serde(default, skip_serializing_if = "Vec::is_empty")]
273 scopes: Vec<String>,
274 },
275 Access,
277 Superadmin {
280 source: SuperadminSource,
281 },
282 Workspace {
286 org: OrgId,
287 name: String,
289 },
290 Agent {
294 label: String,
295 },
296}
297
298#[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 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 pub fn can_admin_org(&self, org: &OrgId) -> bool {
326 self.can(org, Permission::AdminOrg)
327 }
328
329 pub fn can_read_org(&self, org: &OrgId) -> bool {
331 self.can(org, Permission::ReadOrg)
332 }
333
334 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 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#[derive(Debug, Clone, Default)]
366pub struct LoginMeta {
367 pub user_agent: Option<String>,
368 pub ip: Option<String>,
369}
370
371#[derive(Debug, Clone)]
373pub struct AuthConfig {
374 pub session_max_age: Duration,
376 pub session_idle: Duration,
378 pub invitation_ttl: Duration,
379 pub reset_ttl: Duration,
380 pub password_cost: PasswordCost,
381 pub login_per_email: Rate,
383 pub attempts_per_ip: Rate,
385 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
404const TOUCH_EVERY: i64 = 60;
406
407pub type Clock = Arc<dyn Fn() -> i64 + Send + Sync>;
408
409pub 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
430pub 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
446pub 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#[derive(Debug, Clone, PartialEq, Eq)]
553pub enum Scope {
554 Read,
556 Deploy,
558 Admin,
560 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn ensure_org(&self, org: &OrgId) -> AuthResult<()> {
1134 ensure_org_tx(&self.db(), org, self.now())
1135 }
1136
1137 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
1737fn 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;