1mod actors;
20pub mod agent_identities;
21pub mod cbor;
22pub mod db;
23pub mod dev;
24pub mod edge;
25pub mod external;
26pub mod http;
27pub mod limit;
28pub mod oauth;
29pub mod oidc;
30pub mod ops;
31mod scope;
32pub mod secret;
33mod setup;
34pub mod ssh_keys;
35pub mod superadmin;
36pub mod webauthn;
37
38use std::path::{Path, PathBuf};
39use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
40use std::time::Duration;
41
42use rusqlite::{Connection, OptionalExtension, Row, TransactionBehavior, params};
43use serde::{Deserialize, Serialize};
44
45use crate::org::OrgId;
46pub use actors::WORKSPACE_ACTOR;
47use limit::{Rate, RateLimiter};
48pub use scope::Scope;
49use secret::{PasswordCost, TokenKind};
50pub use superadmin::{Superadmin, SuperadminSource, SuperadminToken};
51
52#[derive(Debug, thiserror::Error)]
55pub enum AuthError {
56 #[error("invalid email or password")]
57 InvalidCredentials,
58 #[error("{0} is invalid or has expired")]
60 InvalidToken(&'static str),
61 #[error("too many attempts; try again in {retry_after}s")]
62 RateLimited { retry_after: u64 },
63 #[error("{0}")]
64 Forbidden(String),
65 #[error("{0} not found")]
66 NotFound(String),
67 #[error("{0}")]
68 Conflict(String),
69 #[error("{0}")]
70 Invalid(String),
71 #[error("{0}")]
72 Internal(String),
73 #[error("{message}")]
77 Refused { code: &'static str, message: String },
78 #[error("passkey rejected: {0}")]
80 PasskeyRejected(String),
81 #[error("identity database: {0}")]
82 Db(#[from] rusqlite::Error),
83}
84
85impl AuthError {
86 pub(crate) fn io(step: String, e: std::io::Error) -> Self {
87 AuthError::Internal(format!("{step}: {e}"))
88 }
89}
90
91impl From<AuthError> for crate::Error {
92 fn from(e: AuthError) -> Self {
93 match e {
94 AuthError::NotFound(s) => crate::Error::NotFound(s),
95 e => crate::Error::Invalid(e.to_string()),
96 }
97 }
98}
99
100pub type AuthResult<T> = std::result::Result<T, AuthError>;
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
104#[serde(rename_all = "snake_case")]
105pub enum Role {
106 Viewer,
109 Member,
110 Admin,
111 Owner,
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
116pub enum Permission {
117 ReadOrg,
120 AdminOrg,
122 ManageMembers,
124 DeleteOrg,
126}
127
128impl Role {
129 pub const ALL: [Role; 4] = [Role::Viewer, Role::Member, Role::Admin, Role::Owner];
130
131 pub fn permissions(self) -> &'static [Permission] {
134 use Permission::*;
135 match self {
136 Role::Viewer => &[ReadOrg],
137 Role::Member => &[ReadOrg, AdminOrg],
138 Role::Admin => &[ReadOrg, AdminOrg, ManageMembers],
139 Role::Owner => &[ReadOrg, AdminOrg, ManageMembers, DeleteOrg],
140 }
141 }
142
143 pub fn can(self, p: Permission) -> bool {
144 self.permissions().contains(&p)
145 }
146
147 pub fn as_str(self) -> &'static str {
148 match self {
149 Role::Viewer => "viewer",
150 Role::Member => "member",
151 Role::Admin => "admin",
152 Role::Owner => "owner",
153 }
154 }
155
156 pub fn parse(s: &str) -> AuthResult<Role> {
157 Role::ALL
158 .into_iter()
159 .find(|r| r.as_str() == s)
160 .ok_or_else(|| {
161 AuthError::Invalid(format!("role {s:?}: owner, admin, member or viewer"))
162 })
163 }
164}
165
166impl std::fmt::Display for Role {
167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 f.write_str(self.as_str())
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
174pub struct User {
175 pub id: i64,
176 pub email: String,
177 pub name: String,
178 pub platform_admin: bool,
179 pub created_at: i64,
180 pub disabled: bool,
181 pub has_password: bool,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
186pub struct Membership {
187 pub org: OrgId,
188 pub role: Role,
189}
190
191#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
194pub struct Session {
195 pub id: i64,
196 pub user_id: i64,
197 pub created_at: i64,
198 pub last_seen: i64,
199 pub expires_at: i64,
200 pub idle_expires_at: i64,
201 pub user_agent: Option<String>,
202 pub ip: Option<String>,
203}
204
205#[derive(Debug, Clone)]
207pub struct NewSession {
208 pub token: String,
209 pub session: Session,
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
213pub struct Invitation {
214 pub id: i64,
215 pub org: OrgId,
216 pub email: String,
217 pub role: Role,
218 pub invited_by: Option<i64>,
220 pub created_at: i64,
221 pub expires_at: i64,
222 pub accepted_at: Option<i64>,
223}
224
225#[derive(Debug, Clone)]
226pub struct NewInvitation {
227 pub token: String,
228 pub invitation: Invitation,
229}
230
231#[derive(Debug, Clone)]
233pub struct Accepted {
234 pub user: User,
235 pub membership: Membership,
236 pub created: bool,
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
242pub struct ApiToken {
243 pub id: i64,
244 pub name: String,
245 pub user_id: i64,
246 pub org: Option<OrgId>,
249 pub created_at: i64,
250 pub last_used: Option<i64>,
251 pub expires_at: Option<i64>,
252 pub scopes: Vec<String>,
255}
256
257#[derive(Debug, Clone)]
258pub struct NewApiToken {
259 pub token: String,
260 pub info: ApiToken,
261}
262
263#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
265#[serde(tag = "kind", rename_all = "snake_case")]
266pub enum PrincipalKind {
267 Session {
268 id: i64,
269 },
270 ApiToken {
271 id: i64,
272 org: Option<OrgId>,
273 #[serde(default)]
275 name: String,
276 #[serde(default, skip_serializing_if = "Vec::is_empty")]
278 scopes: Vec<String>,
279 },
280 Access,
282 Superadmin {
285 source: SuperadminSource,
286 },
287 Workspace {
291 org: OrgId,
292 name: String,
294 },
295 Agent {
299 label: String,
300 },
301}
302
303#[derive(Debug, Clone, PartialEq, Eq)]
307pub struct Principal {
308 pub user: User,
309 pub kind: PrincipalKind,
310 pub orgs: Vec<(OrgId, Role)>,
311 pub platform_admin: bool,
312 pub downscoped: Option<OrgId>,
314}
315
316impl Principal {
317 pub fn is_platform_admin(&self) -> bool {
318 self.platform_admin
319 }
320
321 pub fn role_in(&self, org: &OrgId) -> Option<Role> {
322 self.orgs.iter().find(|(o, _)| o == org).map(|(_, r)| *r)
323 }
324
325 fn can(&self, org: &OrgId, p: Permission) -> bool {
326 self.platform_admin || self.role_in(org).is_some_and(|r| r.can(p))
327 }
328
329 pub fn can_admin_org(&self, org: &OrgId) -> bool {
331 self.can(org, Permission::AdminOrg)
332 }
333
334 pub fn can_read_org(&self, org: &OrgId) -> bool {
336 self.can(org, Permission::ReadOrg)
337 }
338
339 pub fn can_manage_members(&self, org: &OrgId) -> bool {
341 self.can(org, Permission::ManageMembers)
342 }
343
344 pub fn can_delete_org(&self, org: &OrgId) -> bool {
345 self.can(org, Permission::DeleteOrg)
346 }
347
348 pub fn max_grant(&self, org: &OrgId) -> Option<Role> {
350 if self.platform_admin {
351 return Some(Role::Owner);
352 }
353 self.role_in(org)
354 .filter(|r| r.can(Permission::ManageMembers))
355 }
356
357 pub fn session_id(&self) -> Option<i64> {
358 match self.kind {
359 PrincipalKind::Session { id } => Some(id),
360 PrincipalKind::ApiToken { .. }
361 | PrincipalKind::Access
362 | PrincipalKind::Superadmin { .. }
363 | PrincipalKind::Agent { .. }
364 | PrincipalKind::Workspace { .. } => None,
365 }
366 }
367}
368
369#[derive(Debug, Clone, Default)]
371pub struct LoginMeta {
372 pub user_agent: Option<String>,
373 pub ip: Option<String>,
374}
375
376#[derive(Debug, Clone)]
378pub struct AuthConfig {
379 pub session_max_age: Duration,
381 pub session_idle: Duration,
383 pub invitation_ttl: Duration,
384 pub reset_ttl: Duration,
385 pub password_cost: PasswordCost,
386 pub login_per_email: Rate,
388 pub attempts_per_ip: Rate,
390 pub resets_per_email: Rate,
392}
393
394impl Default for AuthConfig {
395 fn default() -> Self {
396 AuthConfig {
397 session_max_age: Duration::from_secs(30 * 86400),
398 session_idle: Duration::from_secs(7 * 86400),
399 invitation_ttl: Duration::from_secs(7 * 86400),
400 reset_ttl: Duration::from_secs(3600),
401 password_cost: PasswordCost::default(),
402 login_per_email: Rate::new(5, 60),
403 attempts_per_ip: Rate::new(20, 6),
404 resets_per_email: Rate::new(3, 900),
405 }
406 }
407}
408
409const TOUCH_EVERY: i64 = 60;
411
412pub type Clock = Arc<dyn Fn() -> i64 + Send + Sync>;
413
414pub struct AuthStore {
417 conn: Mutex<Connection>,
418 path: Option<PathBuf>,
419 cfg: AuthConfig,
420 clock: Clock,
421 per_email: RateLimiter,
422 per_ip: RateLimiter,
423 resets: RateLimiter,
424 dummy: OnceLock<String>,
425}
426
427impl std::fmt::Debug for AuthStore {
428 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
429 f.debug_struct("AuthStore")
430 .field("path", &self.path)
431 .finish()
432 }
433}
434
435pub fn db_path(state_dir: &Path) -> PathBuf {
437 state_dir.join("isb.db")
438}
439
440fn unix_now() -> i64 {
441 std::time::SystemTime::now()
442 .duration_since(std::time::UNIX_EPOCH)
443 .map(|d| d.as_secs() as i64)
444 .unwrap_or(0)
445}
446
447fn secs(d: Duration) -> i64 {
448 d.as_secs().min(i64::MAX as u64 / 2) as i64
449}
450
451pub fn normalize_email(email: &str) -> AuthResult<String> {
454 let e = email.trim().to_lowercase();
455 let ok = e.len() <= 254
456 && e.split_once('@').is_some_and(|(l, d)| {
457 !l.is_empty() && !d.is_empty() && !d.contains('@') && !d.starts_with('.')
458 })
459 && !e.chars().any(|c| c.is_whitespace() || c.is_control());
460 if ok {
461 Ok(e)
462 } else {
463 Err(AuthError::Invalid(format!(
464 "{email:?} is not an email address"
465 )))
466 }
467}
468
469pub(crate) fn clean_name(name: &str) -> AuthResult<String> {
470 let n = name.trim();
471 if n.chars().count() > 100 || n.chars().any(char::is_control) {
472 return Err(AuthError::Invalid(
473 "name: at most 100 characters, no control characters".into(),
474 ));
475 }
476 Ok(n.to_string())
477}
478
479fn clip(s: Option<String>, n: usize) -> Option<String> {
480 s.map(|s| s.chars().filter(|c| !c.is_control()).take(n).collect())
481}
482
483pub(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";
484
485pub(crate) fn user_row(r: &Row, at: usize) -> rusqlite::Result<User> {
486 Ok(User {
487 id: r.get(at)?,
488 email: r.get(at + 1)?,
489 name: r.get(at + 2)?,
490 platform_admin: r.get(at + 3)?,
491 created_at: r.get(at + 4)?,
492 disabled: r.get(at + 5)?,
493 has_password: r.get(at + 6)?,
494 })
495}
496
497pub(crate) fn org_col(r: &Row, i: usize) -> rusqlite::Result<OrgId> {
498 let s: String = r.get(i)?;
499 OrgId::new(s).map_err(|e| {
500 rusqlite::Error::FromSqlConversionFailure(i, rusqlite::types::Type::Text, Box::new(e))
501 })
502}
503
504fn opt_org_col(r: &Row, i: usize) -> rusqlite::Result<Option<OrgId>> {
505 match r.get::<_, Option<String>>(i)? {
506 None => Ok(None),
507 Some(_) => org_col(r, i).map(Some),
508 }
509}
510
511pub(crate) fn role_col(r: &Row, i: usize) -> rusqlite::Result<Role> {
512 let s: String = r.get(i)?;
513 Role::parse(&s).map_err(|e| {
514 rusqlite::Error::FromSqlConversionFailure(i, rusqlite::types::Type::Text, Box::new(e))
515 })
516}
517
518const INVITATION_COLS: &str =
519 "id, org, email, role, invited_by, created_at, expires_at, accepted_at";
520
521fn invitation_row(r: &Row) -> rusqlite::Result<Invitation> {
522 Ok(Invitation {
523 id: r.get(0)?,
524 org: org_col(r, 1)?,
525 email: r.get(2)?,
526 role: role_col(r, 3)?,
527 invited_by: r.get(4)?,
528 created_at: r.get(5)?,
529 expires_at: r.get(6)?,
530 accepted_at: r.get(7)?,
531 })
532}
533
534const TOKEN_COLS: &str = "id, name, user_id, org, created_at, last_used, expires_at, scopes";
535
536fn token_row(r: &Row) -> rusqlite::Result<ApiToken> {
537 Ok(ApiToken {
538 id: r.get(0)?,
539 name: r.get(1)?,
540 user_id: r.get(2)?,
541 org: opt_org_col(r, 3)?,
542 created_at: r.get(4)?,
543 last_used: r.get(5)?,
544 expires_at: r.get(6)?,
545 scopes: scopes_col(r.get(7)?),
546 })
547}
548
549fn scopes_col(v: Option<String>) -> Vec<String> {
550 v.and_then(|s| serde_json::from_str(&s).ok())
551 .unwrap_or_default()
552}
553
554impl AuthStore {
555 pub fn open(path: impl AsRef<Path>) -> AuthResult<AuthStore> {
557 Self::open_with(path, AuthConfig::default())
558 }
559
560 pub fn open_with(path: impl AsRef<Path>, cfg: AuthConfig) -> AuthResult<AuthStore> {
561 let path = path.as_ref();
562 let conn = db::open(path)?;
563 Ok(Self::build(conn, Some(path.to_path_buf()), cfg))
564 }
565
566 pub fn in_memory(cfg: AuthConfig) -> AuthResult<AuthStore> {
568 Ok(Self::build(db::open_in_memory()?, None, cfg))
569 }
570
571 fn build(conn: Connection, path: Option<PathBuf>, cfg: AuthConfig) -> AuthStore {
572 AuthStore {
573 conn: Mutex::new(conn),
574 path,
575 per_email: RateLimiter::new(cfg.login_per_email),
576 per_ip: RateLimiter::new(cfg.attempts_per_ip),
577 resets: RateLimiter::new(cfg.resets_per_email),
578 cfg,
579 clock: Arc::new(unix_now),
580 dummy: OnceLock::new(),
581 }
582 }
583
584 pub fn with_clock(mut self, clock: Clock) -> Self {
586 self.clock = clock;
587 self
588 }
589
590 pub fn config(&self) -> &AuthConfig {
591 &self.cfg
592 }
593
594 pub fn path(&self) -> Option<&Path> {
595 self.path.as_deref()
596 }
597
598 pub fn now(&self) -> i64 {
599 (self.clock)()
600 }
601
602 pub(crate) fn db(&self) -> MutexGuard<'_, Connection> {
603 self.conn.lock().unwrap_or_else(|e| e.into_inner())
604 }
605
606 fn hash_pw(&self, pw: &str) -> AuthResult<String> {
607 secret::check_password_policy(pw)?;
608 secret::hash_password(pw, self.cfg.password_cost)
609 }
610
611 fn rate(&self, l: &RateLimiter, key: &str) -> AuthResult<()> {
612 l.take(key, self.now() as f64)
613 .map_err(|retry_after| AuthError::RateLimited { retry_after })
614 }
615
616 pub fn limit_ip(&self, ip: Option<&str>) -> AuthResult<()> {
619 match ip {
620 Some(ip) => self.rate(&self.per_ip, ip),
621 None => Ok(()),
622 }
623 }
624
625 pub fn create_user(
630 &self,
631 email: &str,
632 name: &str,
633 password: Option<&str>,
634 platform_admin: bool,
635 ) -> AuthResult<User> {
636 let email = normalize_email(email)?;
637 let name = clean_name(name)?;
638 let hash = password.map(|p| self.hash_pw(p)).transpose()?;
639 let now = self.now();
640 let db = self.db();
641 let r = db.execute(
642 "INSERT INTO users (email, name, password_hash, platform_admin, created_at)
643 VALUES (?1, ?2, ?3, ?4, ?5)",
644 params![email, name, hash, platform_admin, now],
645 );
646 match r {
647 Ok(_) => {
648 let id = db.last_insert_rowid();
649 drop(db);
650 self.user(id)
651 }
652 Err(rusqlite::Error::SqliteFailure(e, _))
653 if e.code == rusqlite::ErrorCode::ConstraintViolation =>
654 {
655 Err(AuthError::Conflict(format!(
656 "a user with email {email} exists"
657 )))
658 }
659 Err(e) => Err(e.into()),
660 }
661 }
662
663 pub fn user(&self, id: i64) -> AuthResult<User> {
664 self.db()
665 .query_row(
666 &format!("SELECT {USER_COLS} FROM users u WHERE u.id = ?1"),
667 [id],
668 |r| user_row(r, 0),
669 )
670 .optional()?
671 .ok_or_else(|| AuthError::NotFound(format!("user {id}")))
672 }
673
674 pub fn user_by_email(&self, email: &str) -> AuthResult<Option<User>> {
675 let email = normalize_email(email)?;
676 Ok(self
677 .db()
678 .query_row(
679 &format!("SELECT {USER_COLS} FROM users u WHERE u.email = ?1"),
680 [email],
681 |r| user_row(r, 0),
682 )
683 .optional()?)
684 }
685
686 pub fn list_users(&self) -> AuthResult<Vec<User>> {
687 let db = self.db();
688 let mut st = db.prepare(&format!("SELECT {USER_COLS} FROM users u ORDER BY u.id"))?;
689 let rows = st.query_map([], |r| user_row(r, 0))?;
690 Ok(rows.collect::<rusqlite::Result<_>>()?)
691 }
692
693 pub fn set_disabled(&self, user_id: i64, disabled: bool) -> AuthResult<()> {
696 let db = self.db();
697 let n = db.execute(
698 "UPDATE users SET disabled = ?2 WHERE id = ?1",
699 params![user_id, disabled],
700 )?;
701 if n == 0 {
702 return Err(AuthError::NotFound(format!("user {user_id}")));
703 }
704 if disabled {
705 db.execute("DELETE FROM sessions WHERE user_id = ?1", [user_id])?;
706 }
707 Ok(())
708 }
709
710 pub fn last_active(&self, user_id: i64) -> AuthResult<Option<i64>> {
713 Ok(self.db().query_row(
714 "SELECT MAX(t) FROM (
715 SELECT MAX(last_seen) AS t FROM sessions WHERE user_id = ?1
716 UNION ALL
717 SELECT MAX(last_used) FROM api_tokens WHERE user_id = ?1)",
718 [user_id],
719 |r| r.get(0),
720 )?)
721 }
722
723 pub fn other_platform_admins(&self, except: i64) -> AuthResult<i64> {
725 Ok(self.db().query_row(
726 "SELECT COUNT(*) FROM users WHERE platform_admin = 1 AND disabled = 0 AND id != ?1",
727 [except],
728 |r| r.get(0),
729 )?)
730 }
731
732 pub fn set_platform_admin(&self, user_id: i64, admin: bool) -> AuthResult<()> {
733 let n = self.db().execute(
734 "UPDATE users SET platform_admin = ?2 WHERE id = ?1",
735 params![user_id, admin],
736 )?;
737 if n == 0 {
738 return Err(AuthError::NotFound(format!("user {user_id}")));
739 }
740 Ok(())
741 }
742
743 pub fn set_password(&self, user_id: i64, password: &str) -> AuthResult<()> {
746 let hash = self.hash_pw(password)?;
747 let db = self.db();
748 let n = db.execute(
749 "UPDATE users SET password_hash = ?2 WHERE id = ?1",
750 params![user_id, hash],
751 )?;
752 if n == 0 {
753 return Err(AuthError::NotFound(format!("user {user_id}")));
754 }
755 db.execute("DELETE FROM sessions WHERE user_id = ?1", [user_id])?;
756 Ok(())
757 }
758
759 pub fn change_password(
762 &self,
763 user_id: i64,
764 current: &str,
765 new: &str,
766 keep: Option<i64>,
767 ) -> AuthResult<()> {
768 let stored: Option<String> = self
769 .db()
770 .query_row(
771 "SELECT password_hash FROM users WHERE id = ?1",
772 [user_id],
773 |r| r.get(0),
774 )
775 .optional()?
776 .flatten();
777 let ok = match &stored {
778 Some(h) => secret::verify_password(current, h),
779 None => {
780 secret::verify_password(current, self.dummy());
781 false
782 }
783 };
784 if !ok {
785 return Err(AuthError::Forbidden("current password is wrong".into()));
786 }
787 let hash = self.hash_pw(new)?;
788 let db = self.db();
789 db.execute(
790 "UPDATE users SET password_hash = ?2 WHERE id = ?1",
791 params![user_id, hash],
792 )?;
793 db.execute(
794 "DELETE FROM sessions WHERE user_id = ?1 AND id IS NOT ?2",
795 params![user_id, keep],
796 )?;
797 Ok(())
798 }
799
800 fn dummy(&self) -> &str {
801 secret::dummy_hash(&self.dummy, self.cfg.password_cost)
802 }
803
804 pub fn login(&self, email: &str, password: &str, meta: LoginMeta) -> AuthResult<NewSession> {
810 let key = email.trim().to_lowercase();
811 self.limit_ip(meta.ip.as_deref())?;
812 self.rate(&self.per_email, &key)?;
813 let found: Option<(i64, Option<String>, bool)> = self
814 .db()
815 .query_row(
816 "SELECT id, password_hash, disabled FROM users WHERE email = ?1",
817 [&key],
818 |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
819 )
820 .optional()?;
821 let user_id = match found {
822 Some((id, Some(h), disabled)) => {
823 let ok = secret::verify_password(password, &h);
824 (ok && !disabled).then_some(id)
825 }
826 _ => {
827 secret::verify_password(password, self.dummy());
828 None
829 }
830 };
831 let user_id = user_id.ok_or(AuthError::InvalidCredentials)?;
832 self.prune()?;
833 self.start_session(user_id, meta)
834 }
835
836 pub fn start_session(&self, user_id: i64, meta: LoginMeta) -> AuthResult<NewSession> {
839 let (token, hash) = secret::new_token(TokenKind::Session)?;
840 let now = self.now();
841 let expires = now + secs(self.cfg.session_max_age);
842 let (ua, ip) = (clip(meta.user_agent, 256), clip(meta.ip, 64));
843 let db = self.db();
844 db.execute(
845 "INSERT INTO sessions (token_hash, user_id, created_at, last_seen, expires_at, user_agent, ip)
846 VALUES (?1, ?2, ?3, ?3, ?4, ?5, ?6)",
847 params![hash, user_id, now, expires, ua, ip],
848 )?;
849 let id = db.last_insert_rowid();
850 Ok(NewSession {
851 token,
852 session: Session {
853 id,
854 user_id,
855 created_at: now,
856 last_seen: now,
857 expires_at: expires,
858 idle_expires_at: self.idle_end(now, expires),
859 user_agent: ua,
860 ip,
861 },
862 })
863 }
864
865 fn idle_end(&self, last_seen: i64, expires: i64) -> i64 {
866 (last_seen + secs(self.cfg.session_idle)).min(expires)
867 }
868
869 fn session_row(&self, r: &Row, at: usize) -> rusqlite::Result<Session> {
870 let last_seen: i64 = r.get(at + 3)?;
871 let expires: i64 = r.get(at + 4)?;
872 Ok(Session {
873 id: r.get(at)?,
874 user_id: r.get(at + 1)?,
875 created_at: r.get(at + 2)?,
876 last_seen,
877 expires_at: expires,
878 idle_expires_at: self.idle_end(last_seen, expires),
879 user_agent: r.get(at + 5)?,
880 ip: r.get(at + 6)?,
881 })
882 }
883
884 pub fn session(&self, token: &str) -> AuthResult<Option<(User, Session)>> {
887 if !secret::well_formed(token, TokenKind::Session) {
888 return Ok(None);
889 }
890 let hash = secret::hash_token(token);
891 let now = self.now();
892 let db = self.db();
893 let found = db
894 .query_row(
895 &format!(
896 "SELECT s.token_hash, s.id, s.user_id, s.created_at, s.last_seen, s.expires_at,
897 s.user_agent, s.ip, {USER_COLS}
898 FROM sessions s JOIN users u ON u.id = s.user_id WHERE s.token_hash = ?1"
899 ),
900 [&hash],
901 |r| {
902 Ok((
903 r.get::<_, Vec<u8>>(0)?,
904 self.session_row(r, 1)?,
905 user_row(r, 8)?,
906 ))
907 },
908 )
909 .optional()?;
910 let Some((stored, mut s, user)) = found else {
911 return Ok(None);
912 };
913 if !secret::ct_eq(&stored, &hash) {
914 return Ok(None);
915 }
916 if now >= s.expires_at || now >= s.idle_expires_at {
917 db.execute("DELETE FROM sessions WHERE id = ?1", [s.id])?;
918 return Ok(None);
919 }
920 if user.disabled {
921 return Ok(None);
922 }
923 if now - s.last_seen >= TOUCH_EVERY {
924 db.execute(
925 "UPDATE sessions SET last_seen = ?2 WHERE id = ?1",
926 params![s.id, now],
927 )?;
928 s.last_seen = now;
929 s.idle_expires_at = self.idle_end(now, s.expires_at);
930 }
931 Ok(Some((user, s)))
932 }
933
934 pub fn principal_for_email(&self, email: &str) -> AuthResult<Option<Principal>> {
937 let Some(user) = self.user_by_email(email)? else {
938 return Ok(None);
939 };
940 if user.disabled {
941 return Ok(None);
942 }
943 let orgs = self
944 .memberships(user.id)?
945 .into_iter()
946 .map(|m| (m.org, m.role))
947 .collect();
948 Ok(Some(Principal {
949 platform_admin: user.platform_admin,
950 user,
951 kind: PrincipalKind::Access,
952 orgs,
953 downscoped: None,
954 }))
955 }
956
957 pub fn authenticate_session(&self, token: &str) -> AuthResult<Option<Principal>> {
959 let Some((user, s)) = self.session(token)? else {
960 return Ok(None);
961 };
962 let orgs = self
963 .memberships(user.id)?
964 .into_iter()
965 .map(|m| (m.org, m.role))
966 .collect();
967 Ok(Some(Principal {
968 platform_admin: user.platform_admin,
969 user,
970 kind: PrincipalKind::Session { id: s.id },
971 orgs,
972 downscoped: None,
973 }))
974 }
975
976 pub fn logout(&self, token: &str) -> AuthResult<bool> {
978 if !secret::well_formed(token, TokenKind::Session) {
979 return Ok(false);
980 }
981 let n = self.db().execute(
982 "DELETE FROM sessions WHERE token_hash = ?1",
983 [secret::hash_token(token)],
984 )?;
985 Ok(n > 0)
986 }
987
988 pub fn list_sessions(&self, user_id: i64) -> AuthResult<Vec<Session>> {
990 let now = self.now();
991 let db = self.db();
992 let mut st = db.prepare(
993 "SELECT id, user_id, created_at, last_seen, expires_at, user_agent, ip
994 FROM sessions WHERE user_id = ?1 ORDER BY id DESC",
995 )?;
996 let rows = st.query_map([user_id], |r| self.session_row(r, 0))?;
997 let all: Vec<Session> = rows.collect::<rusqlite::Result<_>>()?;
998 Ok(all
999 .into_iter()
1000 .filter(|s| now < s.expires_at && now < s.idle_expires_at)
1001 .collect())
1002 }
1003
1004 pub fn revoke_session(&self, user_id: i64, session_id: i64) -> AuthResult<bool> {
1006 let n = self.db().execute(
1007 "DELETE FROM sessions WHERE id = ?1 AND user_id = ?2",
1008 params![session_id, user_id],
1009 )?;
1010 Ok(n > 0)
1011 }
1012
1013 pub fn revoke_sessions(&self, user_id: i64, keep: Option<i64>) -> AuthResult<usize> {
1015 Ok(self.db().execute(
1016 "DELETE FROM sessions WHERE user_id = ?1 AND id IS NOT ?2",
1017 params![user_id, keep],
1018 )?)
1019 }
1020
1021 pub fn prune(&self) -> AuthResult<()> {
1023 let now = self.now();
1024 let idle = secs(self.cfg.session_idle);
1025 self.db().execute_batch(&format!(
1026 "DELETE FROM sessions WHERE expires_at <= {now} OR last_seen + {idle} <= {now};
1027 DELETE FROM invitations WHERE accepted_at IS NULL AND expires_at <= {now};
1028 DELETE FROM password_resets WHERE expires_at <= {now} OR used_at IS NOT NULL;"
1029 ))?;
1030 Ok(())
1031 }
1032
1033 pub fn ensure_org(&self, org: &OrgId) -> AuthResult<()> {
1038 ensure_org_tx(&self.db(), org, self.now())
1039 }
1040
1041 pub fn delete_org(&self, org: &OrgId) -> AuthResult<bool> {
1043 let n = self
1044 .db()
1045 .execute("DELETE FROM orgs WHERE name = ?1", [org.as_str()])?;
1046 Ok(n > 0)
1047 }
1048
1049 pub fn list_orgs(&self) -> AuthResult<Vec<OrgId>> {
1050 let db = self.db();
1051 let mut st = db.prepare("SELECT name FROM orgs ORDER BY name")?;
1052 let rows = st.query_map([], |r| org_col(r, 0))?;
1053 Ok(rows.collect::<rusqlite::Result<_>>()?)
1054 }
1055
1056 pub fn memberships(&self, user_id: i64) -> AuthResult<Vec<Membership>> {
1057 let db = self.db();
1058 let mut st =
1059 db.prepare("SELECT org, role FROM memberships WHERE user_id = ?1 ORDER BY org")?;
1060 let rows = st.query_map([user_id], |r| {
1061 Ok(Membership {
1062 org: org_col(r, 0)?,
1063 role: role_col(r, 1)?,
1064 })
1065 })?;
1066 Ok(rows.collect::<rusqlite::Result<_>>()?)
1067 }
1068
1069 pub fn list_members(&self, org: &OrgId) -> AuthResult<Vec<(User, Role)>> {
1070 let db = self.db();
1071 let mut st = db.prepare(&format!(
1072 "SELECT {USER_COLS}, m.role FROM memberships m JOIN users u ON u.id = m.user_id
1073 WHERE m.org = ?1 ORDER BY u.email"
1074 ))?;
1075 let rows = st.query_map([org.as_str()], |r| Ok((user_row(r, 0)?, role_col(r, 7)?)))?;
1076 Ok(rows.collect::<rusqlite::Result<_>>()?)
1077 }
1078
1079 pub fn set_member(&self, org: &OrgId, user_id: i64, role: Role) -> AuthResult<()> {
1082 let now = self.now();
1083 let mut db = self.db();
1084 let tx = db.transaction_with_behavior(TransactionBehavior::Immediate)?;
1085 ensure_org_tx(&tx, org, now)?;
1086 if role != Role::Owner {
1087 refuse_last_owner(&tx, org, user_id, "demote")?;
1088 }
1089 tx.execute(
1090 "INSERT INTO memberships (user_id, org, role, created_at) VALUES (?1, ?2, ?3, ?4)
1091 ON CONFLICT (user_id, org) DO UPDATE SET role = excluded.role",
1092 params![user_id, org.as_str(), role.as_str(), now],
1093 )
1094 .map_err(|e| match e {
1095 rusqlite::Error::SqliteFailure(f, _)
1096 if f.code == rusqlite::ErrorCode::ConstraintViolation =>
1097 {
1098 AuthError::NotFound(format!("user {user_id}"))
1099 }
1100 e => e.into(),
1101 })?;
1102 tx.commit()?;
1103 Ok(())
1104 }
1105
1106 pub fn remove_member(&self, org: &OrgId, user_id: i64) -> AuthResult<bool> {
1109 let mut db = self.db();
1110 let tx = db.transaction_with_behavior(TransactionBehavior::Immediate)?;
1111 refuse_last_owner(&tx, org, user_id, "remove")?;
1112 let n = tx.execute(
1113 "DELETE FROM memberships WHERE org = ?1 AND user_id = ?2",
1114 params![org.as_str(), user_id],
1115 )?;
1116 tx.execute(
1117 "DELETE FROM api_tokens WHERE org = ?1 AND user_id = ?2",
1118 params![org.as_str(), user_id],
1119 )?;
1120 tx.commit()?;
1121 Ok(n > 0)
1122 }
1123
1124 pub fn create_invitation(
1130 &self,
1131 invited_by: Option<i64>,
1132 org: &OrgId,
1133 email: &str,
1134 role: Role,
1135 ) -> AuthResult<NewInvitation> {
1136 let email = normalize_email(email)?;
1137 let (token, hash) = secret::new_token(TokenKind::Invitation)?;
1138 let now = self.now();
1139 let expires = now + secs(self.cfg.invitation_ttl);
1140 let mut db = self.db();
1141 let tx = db.transaction_with_behavior(TransactionBehavior::Immediate)?;
1142 ensure_org_tx(&tx, org, now)?;
1143 tx.execute(
1144 "DELETE FROM invitations WHERE org = ?1 AND email = ?2 AND accepted_at IS NULL",
1145 params![org.as_str(), email],
1146 )?;
1147 tx.execute(
1148 "INSERT INTO invitations (token_hash, org, email, role, invited_by, created_at, expires_at)
1149 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
1150 params![hash, org.as_str(), email, role.as_str(), invited_by, now, expires],
1151 )?;
1152 let id = tx.last_insert_rowid();
1153 tx.commit()?;
1154 Ok(NewInvitation {
1155 token,
1156 invitation: Invitation {
1157 id,
1158 org: org.clone(),
1159 email,
1160 role,
1161 invited_by,
1162 created_at: now,
1163 expires_at: expires,
1164 accepted_at: None,
1165 },
1166 })
1167 }
1168
1169 pub fn list_invitations(&self, org: &OrgId) -> AuthResult<Vec<Invitation>> {
1171 let db = self.db();
1172 let mut st = db.prepare(&format!(
1173 "SELECT {INVITATION_COLS} FROM invitations
1174 WHERE org = ?1 AND accepted_at IS NULL AND expires_at > ?2 ORDER BY id"
1175 ))?;
1176 let rows = st.query_map(params![org.as_str(), self.now()], invitation_row)?;
1177 Ok(rows.collect::<rusqlite::Result<_>>()?)
1178 }
1179
1180 pub fn revoke_invitation(&self, org: &OrgId, id: i64) -> AuthResult<bool> {
1182 let n = self.db().execute(
1183 "DELETE FROM invitations WHERE id = ?1 AND org = ?2 AND accepted_at IS NULL",
1184 params![id, org.as_str()],
1185 )?;
1186 Ok(n > 0)
1187 }
1188
1189 pub fn invitation(&self, token: &str) -> AuthResult<Option<Invitation>> {
1191 if !secret::well_formed(token, TokenKind::Invitation) {
1192 return Ok(None);
1193 }
1194 let hash = secret::hash_token(token);
1195 let found = self
1196 .db()
1197 .query_row(
1198 &format!(
1199 "SELECT token_hash, {INVITATION_COLS} FROM invitations WHERE token_hash = ?1"
1200 ),
1201 [&hash],
1202 |r| {
1203 let stored: Vec<u8> = r.get(0)?;
1204 let inv = Invitation {
1205 id: r.get(1)?,
1206 org: org_col(r, 2)?,
1207 email: r.get(3)?,
1208 role: role_col(r, 4)?,
1209 invited_by: r.get(5)?,
1210 created_at: r.get(6)?,
1211 expires_at: r.get(7)?,
1212 accepted_at: r.get(8)?,
1213 };
1214 Ok((stored, inv))
1215 },
1216 )
1217 .optional()?;
1218 Ok(found.and_then(|(stored, inv)| {
1219 (secret::ct_eq(&stored, &hash)
1220 && inv.accepted_at.is_none()
1221 && self.now() < inv.expires_at)
1222 .then_some(inv)
1223 }))
1224 }
1225
1226 pub fn accept_invitation(
1230 &self,
1231 token: &str,
1232 name: &str,
1233 password: &str,
1234 ) -> AuthResult<Accepted> {
1235 let inv = self
1236 .invitation(token)?
1237 .ok_or(AuthError::InvalidToken("invitation"))?;
1238 let existing: Option<(i64, Option<String>, bool)> = self
1239 .db()
1240 .query_row(
1241 "SELECT id, password_hash, disabled FROM users WHERE email = ?1",
1242 [&inv.email],
1243 |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
1244 )
1245 .optional()?;
1246 let new_user = match &existing {
1247 Some((_, Some(h), disabled)) => {
1248 if !secret::verify_password(password, h) || *disabled {
1249 return Err(AuthError::InvalidCredentials);
1250 }
1251 None
1252 }
1253 Some((_, None, _)) => {
1254 secret::verify_password(password, self.dummy());
1255 return Err(AuthError::InvalidCredentials);
1256 }
1257 None => Some((clean_name(name)?, self.hash_pw(password)?)),
1258 };
1259 self.finish_accept(&inv, existing.map(|e| e.0), new_user)
1260 }
1261
1262 pub fn accept_invitation_as(&self, token: &str, user_id: i64) -> AuthResult<Accepted> {
1264 let inv = self
1265 .invitation(token)?
1266 .ok_or(AuthError::InvalidToken("invitation"))?;
1267 let user = self.user(user_id)?;
1268 if user.email != inv.email {
1269 return Err(AuthError::Forbidden(format!(
1270 "this invitation is for {}, and you are signed in as {}",
1271 inv.email, user.email
1272 )));
1273 }
1274 self.finish_accept(&inv, Some(user_id), None)
1275 }
1276
1277 fn finish_accept(
1278 &self,
1279 inv: &Invitation,
1280 user_id: Option<i64>,
1281 new_user: Option<(String, String)>,
1282 ) -> AuthResult<Accepted> {
1283 let now = self.now();
1284 let mut db = self.db();
1285 let tx = db.transaction_with_behavior(TransactionBehavior::Immediate)?;
1286 let n = tx.execute(
1288 "UPDATE invitations SET accepted_at = ?2 WHERE id = ?1 AND accepted_at IS NULL",
1289 params![inv.id, now],
1290 )?;
1291 if n == 0 {
1292 return Err(AuthError::InvalidToken("invitation"));
1293 }
1294 let (uid, created) = match (user_id, new_user) {
1295 (Some(id), _) => (id, false),
1296 (None, Some((name, hash))) => {
1297 tx.execute(
1298 "INSERT INTO users (email, name, password_hash, created_at) VALUES (?1, ?2, ?3, ?4)",
1299 params![inv.email, name, hash, now],
1300 )
1301 .map_err(|e| match e {
1302 rusqlite::Error::SqliteFailure(f, _)
1303 if f.code == rusqlite::ErrorCode::ConstraintViolation =>
1304 {
1305 AuthError::Conflict(format!("a user with email {} exists", inv.email))
1306 }
1307 e => e.into(),
1308 })?;
1309 (tx.last_insert_rowid(), true)
1310 }
1311 (None, None) => return Err(AuthError::Internal("accept: no user".into())),
1312 };
1313 tx.execute(
1314 "UPDATE invitations SET accepted_by = ?2 WHERE id = ?1",
1315 params![inv.id, uid],
1316 )?;
1317 let current: Option<Role> = tx
1319 .query_row(
1320 "SELECT role FROM memberships WHERE user_id = ?1 AND org = ?2",
1321 params![uid, inv.org.as_str()],
1322 |r| role_col(r, 0),
1323 )
1324 .optional()?;
1325 let role = current.map_or(inv.role, |c| c.max(inv.role));
1326 tx.execute(
1327 "INSERT INTO memberships (user_id, org, role, created_at) VALUES (?1, ?2, ?3, ?4)
1328 ON CONFLICT (user_id, org) DO UPDATE SET role = excluded.role",
1329 params![uid, inv.org.as_str(), role.as_str(), now],
1330 )?;
1331 tx.commit()?;
1332 drop(db);
1333 Ok(Accepted {
1334 user: self.user(uid)?,
1335 membership: Membership {
1336 org: inv.org.clone(),
1337 role,
1338 },
1339 created,
1340 })
1341 }
1342
1343 pub fn create_api_token(
1349 &self,
1350 user_id: i64,
1351 org: Option<&OrgId>,
1352 name: &str,
1353 expires: Option<Duration>,
1354 ) -> AuthResult<NewApiToken> {
1355 self.create_api_token_scoped(user_id, org, name, expires, &[])
1356 }
1357
1358 pub fn create_api_token_scoped(
1360 &self,
1361 user_id: i64,
1362 org: Option<&OrgId>,
1363 name: &str,
1364 expires: Option<Duration>,
1365 scopes: &[String],
1366 ) -> AuthResult<NewApiToken> {
1367 let scopes = Scope::normalize(scopes)?;
1368 let name = name.trim();
1369 if name.is_empty() || name.chars().count() > 100 || name.chars().any(char::is_control) {
1370 return Err(AuthError::Invalid(
1371 "token name: 1 to 100 characters, no control characters".into(),
1372 ));
1373 }
1374 let user = self.user(user_id)?;
1375 if user.disabled {
1376 return Err(AuthError::Forbidden(format!(
1377 "user {} is disabled",
1378 user.email
1379 )));
1380 }
1381 match org {
1382 None if !user.platform_admin => {
1383 return Err(AuthError::Forbidden(
1384 "only a platform admin can make a token without an org".into(),
1385 ));
1386 }
1387 Some(o) if !user.platform_admin && self.role_of(user_id, o)?.is_none() => {
1388 return Err(AuthError::Forbidden(format!(
1389 "{} is not a member of org {o}",
1390 user.email
1391 )));
1392 }
1393 _ => {}
1394 }
1395 let (token, hash) = secret::new_token(TokenKind::Api)?;
1396 let now = self.now();
1397 let expires_at = expires.map(|d| now + secs(d));
1398 let db = self.db();
1399 if let Some(o) = org {
1400 ensure_org_tx(&db, o, now)?;
1401 }
1402 db.execute(
1403 "INSERT INTO api_tokens (token_hash, name, user_id, org, created_at, expires_at, scopes)
1404 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
1405 params![
1406 hash,
1407 name,
1408 user_id,
1409 org.map(OrgId::as_str),
1410 now,
1411 expires_at,
1412 (!scopes.is_empty()).then(|| serde_json::to_string(&scopes).unwrap_or_default())
1413 ],
1414 )?;
1415 Ok(NewApiToken {
1416 token,
1417 info: ApiToken {
1418 id: db.last_insert_rowid(),
1419 name: name.to_string(),
1420 user_id,
1421 org: org.cloned(),
1422 created_at: now,
1423 last_used: None,
1424 expires_at,
1425 scopes,
1426 },
1427 })
1428 }
1429
1430 fn role_of(&self, user_id: i64, org: &OrgId) -> AuthResult<Option<Role>> {
1431 Ok(self
1432 .db()
1433 .query_row(
1434 "SELECT role FROM memberships WHERE user_id = ?1 AND org = ?2",
1435 params![user_id, org.as_str()],
1436 |r| role_col(r, 0),
1437 )
1438 .optional()?)
1439 }
1440
1441 pub fn authenticate_token(&self, token: &str) -> AuthResult<Option<Principal>> {
1444 if !secret::well_formed(token, TokenKind::Api) {
1445 return Ok(None);
1446 }
1447 let hash = secret::hash_token(token);
1448 let now = self.now();
1449 let found = self
1450 .db()
1451 .query_row(
1452 &format!(
1453 "SELECT t.token_hash, t.id, t.org, t.expires_at, t.last_used, t.name, t.scopes,
1454 {USER_COLS} FROM api_tokens t JOIN users u ON u.id = t.user_id
1455 WHERE t.token_hash = ?1"
1456 ),
1457 [&hash],
1458 |r| {
1459 Ok((
1460 r.get::<_, Vec<u8>>(0)?,
1461 r.get::<_, i64>(1)?,
1462 opt_org_col(r, 2)?,
1463 r.get::<_, Option<i64>>(3)?,
1464 r.get::<_, Option<i64>>(4)?,
1465 r.get::<_, String>(5)?,
1466 scopes_col(r.get(6)?),
1467 user_row(r, 7)?,
1468 ))
1469 },
1470 )
1471 .optional()?;
1472 let Some((stored, id, org, expires, last_used, name, scopes, user)) = found else {
1473 return Ok(None);
1474 };
1475 if !secret::ct_eq(&stored, &hash) || expires.is_some_and(|e| now >= e) || user.disabled {
1476 return Ok(None);
1477 }
1478 let (orgs, platform_admin) = match &org {
1479 Some(o) => {
1480 let role = match self.role_of(user.id, o)? {
1481 Some(r) => r,
1482 None if user.platform_admin => Role::Owner,
1484 None => return Ok(None),
1485 };
1486 (vec![(o.clone(), role)], false)
1487 }
1488 None if user.platform_admin => (
1489 self.memberships(user.id)?
1490 .into_iter()
1491 .map(|m| (m.org, m.role))
1492 .collect(),
1493 true,
1494 ),
1495 None => return Ok(None),
1497 };
1498 if last_used.is_none_or(|l| now - l >= TOUCH_EVERY) {
1499 self.db().execute(
1500 "UPDATE api_tokens SET last_used = ?2 WHERE id = ?1",
1501 params![id, now],
1502 )?;
1503 }
1504 Ok(Some(Principal {
1505 user,
1506 kind: PrincipalKind::ApiToken {
1507 id,
1508 org,
1509 name,
1510 scopes,
1511 },
1512 orgs,
1513 platform_admin,
1514 downscoped: None,
1515 }))
1516 }
1517
1518 pub fn api_token(&self, id: i64) -> AuthResult<ApiToken> {
1519 self.db()
1520 .query_row(
1521 &format!("SELECT {TOKEN_COLS} FROM api_tokens WHERE id = ?1"),
1522 [id],
1523 token_row,
1524 )
1525 .optional()?
1526 .ok_or_else(|| AuthError::NotFound(format!("token {id}")))
1527 }
1528
1529 pub fn list_api_tokens(&self, user_id: i64) -> AuthResult<Vec<ApiToken>> {
1531 let db = self.db();
1532 let mut st = db.prepare(&format!(
1533 "SELECT {TOKEN_COLS} FROM api_tokens WHERE user_id = ?1 ORDER BY id"
1534 ))?;
1535 let rows = st.query_map([user_id], token_row)?;
1536 Ok(rows.collect::<rusqlite::Result<_>>()?)
1537 }
1538
1539 pub fn list_org_api_tokens(&self, org: &OrgId) -> AuthResult<Vec<ApiToken>> {
1541 let db = self.db();
1542 let mut st = db.prepare(&format!(
1543 "SELECT {TOKEN_COLS} FROM api_tokens WHERE org = ?1 ORDER BY id"
1544 ))?;
1545 let rows = st.query_map([org.as_str()], token_row)?;
1546 Ok(rows.collect::<rusqlite::Result<_>>()?)
1547 }
1548
1549 pub fn list_all_api_tokens(&self) -> AuthResult<Vec<ApiToken>> {
1551 let db = self.db();
1552 let mut st = db.prepare(&format!("SELECT {TOKEN_COLS} FROM api_tokens ORDER BY id"))?;
1553 let rows = st.query_map([], token_row)?;
1554 Ok(rows.collect::<rusqlite::Result<_>>()?)
1555 }
1556
1557 pub fn revoke_api_token(&self, id: i64) -> AuthResult<bool> {
1559 let n = self
1560 .db()
1561 .execute("DELETE FROM api_tokens WHERE id = ?1", [id])?;
1562 Ok(n > 0)
1563 }
1564
1565 pub fn request_password_reset(&self, email: &str) -> AuthResult<Option<String>> {
1571 let key = email.trim().to_lowercase();
1572 self.rate(&self.resets, &key)?;
1573 let Some(user) = self.user_by_email(&key).ok().flatten() else {
1574 return Ok(None);
1575 };
1576 if user.disabled {
1577 return Ok(None);
1578 }
1579 let (token, hash) = secret::new_token(TokenKind::PasswordReset)?;
1580 let now = self.now();
1581 let db = self.db();
1582 db.execute("DELETE FROM password_resets WHERE user_id = ?1", [user.id])?;
1584 db.execute(
1585 "INSERT INTO password_resets (token_hash, user_id, created_at, expires_at)
1586 VALUES (?1, ?2, ?3, ?4)",
1587 params![hash, user.id, now, now + secs(self.cfg.reset_ttl)],
1588 )?;
1589 Ok(Some(token))
1590 }
1591
1592 pub fn reset_password(&self, token: &str, password: &str) -> AuthResult<User> {
1594 if !secret::well_formed(token, TokenKind::PasswordReset) {
1595 return Err(AuthError::InvalidToken("reset link"));
1596 }
1597 let hash_pw = self.hash_pw(password)?;
1598 let hash = secret::hash_token(token);
1599 let now = self.now();
1600 let mut db = self.db();
1601 let tx = db.transaction_with_behavior(TransactionBehavior::Immediate)?;
1602 type ResetRow = (Vec<u8>, i64, i64, i64, Option<i64>);
1604 let found: Option<ResetRow> = tx
1605 .query_row(
1606 "SELECT token_hash, id, user_id, expires_at, used_at FROM password_resets
1607 WHERE token_hash = ?1",
1608 [&hash],
1609 |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?, r.get(4)?)),
1610 )
1611 .optional()?;
1612 let Some((stored, id, user_id, expires, used)) = found else {
1613 return Err(AuthError::InvalidToken("reset link"));
1614 };
1615 if !secret::ct_eq(&stored, &hash) || used.is_some() || now >= expires {
1616 return Err(AuthError::InvalidToken("reset link"));
1617 }
1618 tx.execute(
1619 "UPDATE password_resets SET used_at = ?2 WHERE id = ?1",
1620 params![id, now],
1621 )?;
1622 tx.execute(
1623 "UPDATE users SET password_hash = ?2 WHERE id = ?1",
1624 params![user_id, hash_pw],
1625 )?;
1626 tx.execute("DELETE FROM sessions WHERE user_id = ?1", [user_id])?;
1627 tx.commit()?;
1628 drop(db);
1629 self.user(user_id)
1630 }
1631}
1632
1633pub(crate) fn ensure_org_tx(conn: &Connection, org: &OrgId, now: i64) -> AuthResult<()> {
1634 conn.execute(
1635 "INSERT INTO orgs (name, created_at) VALUES (?1, ?2) ON CONFLICT (name) DO NOTHING",
1636 params![org.as_str(), now],
1637 )?;
1638 Ok(())
1639}
1640
1641fn refuse_last_owner(conn: &Connection, org: &OrgId, user_id: i64, verb: &str) -> AuthResult<()> {
1643 let is_owner: bool = conn
1644 .query_row(
1645 "SELECT role = 'owner' FROM memberships WHERE org = ?1 AND user_id = ?2",
1646 params![org.as_str(), user_id],
1647 |r| r.get(0),
1648 )
1649 .optional()?
1650 .unwrap_or(false);
1651 if !is_owner {
1652 return Ok(());
1653 }
1654 let owners: i64 = conn.query_row(
1655 "SELECT COUNT(*) FROM memberships WHERE org = ?1 AND role = 'owner'",
1656 [org.as_str()],
1657 |r| r.get(0),
1658 )?;
1659 if owners <= 1 {
1660 return Err(AuthError::Conflict(format!(
1661 "cannot {verb} the last owner of org {org}; make someone else owner first"
1662 )));
1663 }
1664 Ok(())
1665}
1666
1667#[cfg(test)]
1668mod tests;