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