tablo_core/auth/
session.rs1use std::{sync::Arc, time::Duration};
5
6use jiff::Timestamp;
7use topcoat::{
8 context::Cx,
9 session::{self, TokenHash},
10};
11use uuid::Uuid;
12
13use super::{DynAuthenticator, PanelUser, SignedIn, infrastructure_failure};
14use crate::panel::state::{PanelState, current};
15
16pub const SESSION_LIFETIME: Duration = Duration::from_hours(24 * 7);
18
19#[derive(Debug, Clone, toasty::Model)]
23pub struct AuthSession {
24 #[key]
26 pub token_hash: String,
27 #[index]
29 pub user_id: String,
30 pub panel: String,
33 pub tenant: Option<Uuid>,
35 #[index]
37 pub expires_at: Timestamp,
38 pub created_at: Timestamp,
39}
40
41pub(super) fn token_key(hash: &TokenHash) -> String {
42 use std::fmt::Write as _;
43
44 let mut key = String::with_capacity(64);
45 for byte in hash.iter() {
46 write!(key, "{byte:02x}").expect("writing to a String cannot fail");
47 }
48 key
49}
50
51pub(super) async fn record(
52 cx: &Cx,
53 session: &session::Session,
54 user: &dyn PanelUser,
55 panel: &PanelState,
56) -> topcoat::Result<()> {
57 let mut db = crate::db::db(cx);
58 toasty::create!(AuthSession {
59 token_hash: token_key(&session.token_hash),
60 user_id: user.user_id(),
61 panel: panel.prefix.clone(),
62 tenant: None,
63 expires_at: Timestamp::try_from(session.expires_at).map_err(topcoat::Error::from)?,
64 created_at: Timestamp::now(),
65 })
66 .exec(&mut db)
67 .await
68 .map_err(infrastructure_failure)?;
69 Ok(())
70}
71
72pub(super) async fn select_tenant(cx: &Cx, tenant: Uuid) -> topcoat::Result<()> {
73 let Some(hash) = session::token_hash(cx).await? else {
74 return Err(topcoat::router::error::forbidden().into());
75 };
76 let mut db = crate::db::db(cx);
77 AuthSession::filter(AuthSession::fields().token_hash().eq(token_key(&hash)))
78 .update()
79 .tenant(Some(tenant))
80 .exec(&mut db)
81 .await
82 .map_err(infrastructure_failure)?;
83 Ok(())
84}
85
86pub(super) async fn delete_session(cx: &Cx, hash: &TokenHash) -> topcoat::Result<()> {
87 delete_session_row(cx, &token_key(hash)).await
88}
89
90async fn delete_session_row(cx: &Cx, key: &str) -> topcoat::Result<()> {
91 let mut db = crate::db::db(cx);
92 AuthSession::filter(AuthSession::fields().token_hash().eq(key.to_string()))
93 .delete()
94 .exec(&mut db)
95 .await
96 .map_err(infrastructure_failure)?;
97 Ok(())
98}
99
100pub async fn revoke_sessions_for_user(cx: &Cx, user_id: &str) -> topcoat::Result<()> {
107 let mut db = crate::db::db(cx);
108 let user = AuthSession::fields().user_id().eq(user_id.to_string());
109 let sessions = match current(cx) {
110 Some(panel) => {
111 AuthSession::filter(user.and(AuthSession::fields().panel().eq(panel.prefix.clone())))
112 }
113 None => AuthSession::filter(user),
114 };
115 sessions
116 .delete()
117 .exec(&mut db)
118 .await
119 .map_err(infrastructure_failure)?;
120 Ok(())
121}
122
123pub(super) const SESSION_SWEEP_BATCH: usize = 500;
126
127pub(super) async fn sweep_expired_sessions(cx: &Cx) -> topcoat::Result<()> {
129 let now = Timestamp::now();
130 let mut db = crate::db::db(cx);
131 let expired: Vec<String> = AuthSession::filter(AuthSession::fields().expires_at().le(now))
132 .limit(SESSION_SWEEP_BATCH)
133 .exec(&mut db)
134 .await
135 .map_err(infrastructure_failure)?
136 .into_iter()
137 .map(|row| row.token_hash)
138 .collect();
139 if expired.is_empty() {
140 return Ok(());
141 }
142 AuthSession::filter(
144 AuthSession::fields()
145 .token_hash()
146 .in_list(expired)
147 .and(AuthSession::fields().expires_at().le(now)),
148 )
149 .delete()
150 .exec(&mut db)
151 .await
152 .map_err(infrastructure_failure)?;
153 Ok(())
154}
155
156pub(super) async fn session_row(cx: &Cx) -> topcoat::Result<Option<AuthSession>> {
159 let Some(hash) = session::token_hash(cx).await? else {
160 return Ok(None);
161 };
162 let key = token_key(&hash);
163 let mut db = crate::db::db(cx);
164 let row = AuthSession::filter(AuthSession::fields().token_hash().eq(key))
165 .first()
166 .exec(&mut db)
167 .await
168 .map_err(infrastructure_failure)?;
169 let Some(row) = row else {
170 return Ok(None);
171 };
172 if row.expires_at <= Timestamp::now() {
173 delete_session_row(cx, &row.token_hash).await?;
174 return Ok(None);
175 }
176 Ok(Some(row))
177}
178
179pub(super) async fn session_user(
182 cx: &Cx,
183 row: AuthSession,
184 panel: &Arc<PanelState>,
185 authenticator: &dyn DynAuthenticator,
186) -> topcoat::Result<Option<SignedIn>> {
187 match authenticator
188 .find_by_id(cx, &row.user_id)
189 .await
190 .map_err(infrastructure_failure)?
191 {
192 Some(user) => Ok(Some(SignedIn {
193 user,
194 panel: Arc::clone(panel),
195 tenant: row.tenant,
196 })),
197 None => {
198 delete_session_row(cx, &row.token_hash).await?;
199 Ok(None)
200 }
201 }
202}
203
204pub(super) async fn resolve(
207 cx: &Cx,
208 panel: &Arc<PanelState>,
209 authenticator: &dyn DynAuthenticator,
210) -> topcoat::Result<Option<SignedIn>> {
211 match session_row(cx).await? {
212 Some(row) if row.panel == panel.prefix => session_user(cx, row, panel, authenticator).await,
213 _ => Ok(None),
214 }
215}