1use std::{
5 sync::Arc,
6 time::{Duration, SystemTime},
7};
8
9use jiff::Timestamp;
10use topcoat::{
11 context::Cx,
12 session::{self, TokenHash, TokenStore, cookie::CookieTokenStore},
13};
14
15use super::{
16 DynAuthenticator, PanelUser, SignedIn, infrastructure_failure,
17 token::{CREDENTIALS, REVOCATION, TokenPurpose},
18};
19use crate::{
20 TenantId,
21 auth::realm::{Realm, current_realm, realms},
22};
23
24pub const SESSION_LIFETIME: Duration = Duration::from_hours(24 * 7);
26
27#[doc(hidden)]
29pub const SESSION_COOKIE_PREFIX: &str = "__Host-tablo_session_";
30
31#[must_use]
37pub fn session_cookie(panel: &str) -> String {
38 format!(
39 "__Host-{}",
40 store_name(&crate::naming::normalize_prefix(panel))
41 )
42}
43
44fn store_name(prefix: &str) -> String {
48 use std::fmt::Write as _;
49
50 let mut name = SESSION_COOKIE_PREFIX
51 .trim_start_matches("__Host-")
52 .to_owned();
53 for byte in prefix.trim_start_matches('/').bytes() {
54 if byte.is_ascii_alphanumeric() || byte == b'-' {
55 name.push(char::from(byte));
56 } else {
57 write!(name, "_{byte:02x}").expect("writing to a String cannot fail");
58 }
59 }
60 name
61}
62
63fn token_store(panel: &Realm) -> CookieTokenStore {
65 CookieTokenStore::new().name(store_name(&panel.prefix))
66}
67
68fn session_panel(cx: &Cx) -> topcoat::Result<&Arc<Realm>> {
70 current_realm(cx).ok_or_else(|| {
71 crate::error::declaration("a panel session changes only under a panel".to_owned())
72 })
73}
74
75#[derive(Debug, Clone, toasty::Model)]
96pub struct AuthSession {
97 #[key]
99 pub token_hash: String,
100 #[index]
102 pub user_id: String,
103 pub panel: String,
106 pub tenant: Option<TenantId>,
108 #[index]
110 pub expires_at: Timestamp,
111 pub created_at: Timestamp,
112 pub impersonating: Option<String>,
115 pub impersonating_until: Option<Timestamp>,
117}
118
119pub(super) async fn start(cx: &Cx) -> topcoat::Result<session::Session> {
121 let token = session::Token::random();
122 token_store(session_panel(cx)?)
123 .write(cx, token.clone(), SESSION_LIFETIME)
124 .await?;
125 Ok(session::Session {
126 token_hash: token.hash(),
127 expires_at: SystemTime::now() + SESSION_LIFETIME,
128 })
129}
130
131pub(super) async fn stop(cx: &Cx) -> topcoat::Result<Option<TokenHash>> {
133 let panel = session_panel(cx)?;
134 let hash = token_hash(cx, panel).await?;
135 token_store(panel).delete(cx).await?;
136 Ok(hash)
137}
138
139async fn token_hash(cx: &Cx, panel: &Realm) -> topcoat::Result<Option<TokenHash>> {
142 let token = token_store(panel).read(cx).await?;
143 Ok(token.map(|token| token.hash()))
144}
145
146pub(super) fn token_key(hash: &TokenHash) -> String {
147 use std::fmt::Write as _;
148
149 let mut key = String::with_capacity(64);
150 for byte in hash.iter() {
151 write!(key, "{byte:02x}").expect("writing to a String cannot fail");
152 }
153 key
154}
155
156pub(super) async fn record(
157 cx: &Cx,
158 session: &session::Session,
159 user: &dyn PanelUser,
160 panel: &Realm,
161) -> topcoat::Result<()> {
162 let expires_at = Timestamp::try_from(session.expires_at).map_err(topcoat::Error::from)?;
163 insert(
164 &mut crate::db::db(cx)?,
165 &session.token_hash,
166 user.user_id(),
167 panel.prefix.clone(),
168 None,
169 expires_at,
170 )
171 .await
172 .map_err(infrastructure_failure)
173}
174
175pub(super) fn credential_panels(
179 cx: &Cx,
180 user_type: std::any::TypeId,
181 own: &Arc<Realm>,
182) -> Vec<Arc<Realm>> {
183 let realms = realms(cx);
184 if realms.is_empty() {
185 return vec![Arc::clone(own)];
186 }
187 realms
188 .into_iter()
189 .filter(|realm| {
190 realm
191 .auth
192 .authenticator()
193 .is_some_and(|auth| auth.user_type() == user_type)
194 })
195 .collect()
196}
197
198pub(super) async fn end_credentials(
202 ex: &mut dyn toasty::Executor,
203 user_id: &str,
204 panels: &[Arc<Realm>],
205 purposes: &[TokenPurpose],
206) -> topcoat::Result<()> {
207 let prefixes: Vec<String> = panels.iter().map(|panel| panel.prefix.clone()).collect();
208 AuthSession::filter(
209 AuthSession::fields()
210 .user_id()
211 .eq(user_id.to_string())
212 .and(AuthSession::fields().panel().in_list(prefixes)),
213 )
214 .delete()
215 .exec(&mut *ex)
216 .await
217 .map_err(infrastructure_failure)?;
218 let issuing: Vec<String> = panels
219 .iter()
220 .filter(|panel| panel.auth.issues_tokens())
221 .map(|panel| panel.prefix.clone())
222 .collect();
223 if issuing.is_empty() {
224 return Ok(());
226 }
227 super::token::end_all(ex, user_id, Some(issuing), purposes).await
228}
229
230pub(super) async fn replace_sessions(
235 cx: &Cx,
236 ex: &mut dyn toasty::Executor,
237 signed: &SignedIn,
238 user_type: std::any::TypeId,
239 session: &session::Session,
240) -> topcoat::Result<()> {
241 let panels = credential_panels(cx, user_type, &signed.realm);
242 let user_id = signed.user.user_id();
243 end_credentials(&mut *ex, &user_id, &panels, CREDENTIALS).await?;
244 let expires_at = Timestamp::try_from(session.expires_at).map_err(topcoat::Error::from)?;
245 insert(
246 ex,
247 &session.token_hash,
248 user_id,
249 signed.realm.prefix.clone(),
250 signed.tenant,
251 expires_at,
252 )
253 .await
254 .map_err(infrastructure_failure)
255}
256
257#[doc(hidden)]
263pub async fn mint_session(
264 ex: &mut dyn toasty::Executor,
265 panel: &str,
266 user: &dyn PanelUser,
267) -> topcoat::Result<String> {
268 let token = session::Token::random();
269 let expires_at = Timestamp::try_from(std::time::SystemTime::now() + SESSION_LIFETIME)
270 .map_err(topcoat::Error::from)?;
271 insert(
272 ex,
273 &token.hash(),
274 user.user_id(),
275 crate::naming::normalize_prefix(panel),
276 None,
277 expires_at,
278 )
279 .await?;
280 Ok(token.encode())
281}
282
283async fn insert(
284 ex: &mut dyn toasty::Executor,
285 hash: &TokenHash,
286 user_id: String,
287 panel: String,
288 tenant: Option<TenantId>,
289 expires_at: Timestamp,
290) -> toasty::Result<()> {
291 toasty::create!(AuthSession {
292 token_hash: token_key(hash),
293 user_id,
294 panel,
295 tenant,
296 expires_at,
297 created_at: Timestamp::now(),
298 impersonating: None,
299 impersonating_until: None,
300 })
301 .exec(ex)
302 .await?;
303 Ok(())
304}
305
306pub(super) async fn select_tenant(cx: &Cx, tenant: TenantId) -> topcoat::Result<()> {
307 let Some(hash) = token_hash(cx, session_panel(cx)?).await? else {
308 return Err(topcoat::router::error::forbidden().into());
309 };
310 let mut db = crate::db::db(cx)?;
311 AuthSession::filter(AuthSession::fields().token_hash().eq(token_key(&hash)))
312 .update()
313 .tenant(Some(tenant))
314 .exec(&mut db)
315 .await
316 .map_err(infrastructure_failure)?;
317 Ok(())
318}
319
320pub(super) async fn impersonate(
323 cx: &Cx,
324 ex: &mut dyn toasty::Executor,
325 target: Option<(String, Timestamp)>,
326) -> topcoat::Result<()> {
327 let Some(hash) = token_hash(cx, session_panel(cx)?).await? else {
328 return Err(topcoat::router::error::forbidden().into());
329 };
330 set_impersonation(ex, &token_key(&hash), target).await
331}
332
333pub(super) async fn set_impersonation(
335 ex: &mut dyn toasty::Executor,
336 key: &str,
337 target: Option<(String, Timestamp)>,
338) -> topcoat::Result<()> {
339 let (user, until) = target.unzip();
340 AuthSession::filter(AuthSession::fields().token_hash().eq(key.to_owned()))
341 .update()
342 .impersonating(user)
343 .impersonating_until(until)
344 .tenant(None)
345 .exec(ex)
346 .await
347 .map_err(infrastructure_failure)?;
348 Ok(())
349}
350
351pub(super) async fn delete_session(cx: &Cx, hash: &TokenHash) -> topcoat::Result<()> {
352 delete_session_row(cx, &token_key(hash)).await
353}
354
355pub(super) async fn start_once(
359 cx: &Cx,
360 started: &mut Option<session::Session>,
361) -> topcoat::Result<session::Session> {
362 if let Some(session) = started {
363 return Ok(session.clone());
364 }
365 let session = start(cx).await?;
366 *started = Some(session.clone());
367 Ok(session)
368}
369
370pub(super) async fn fresh_session(cx: &Cx) -> topcoat::Result<session::Session> {
377 let panel = session_panel(cx)?;
378 if let Some(hash) = token_hash(cx, panel)
379 .await
380 .map_err(infrastructure_failure)?
381 {
382 delete_session(cx, &hash).await?;
383 }
384 start(cx).await.map_err(infrastructure_failure)
385}
386
387async fn delete_session_row(cx: &Cx, key: &str) -> topcoat::Result<()> {
388 let mut db = crate::db::db(cx)?;
389 AuthSession::filter(AuthSession::fields().token_hash().eq(key.to_string()))
390 .delete()
391 .exec(&mut db)
392 .await
393 .map_err(infrastructure_failure)?;
394 Ok(())
395}
396
397pub async fn revoke_sessions_for_user(cx: &Cx, user_id: &str) -> topcoat::Result<()> {
406 let mut db = crate::db::db(cx)?;
407 if let Some(panel) = current_realm(cx) {
408 let panels = match panel.auth.authenticator() {
409 Some(auth) => credential_panels(cx, auth.user_type(), panel),
410 None => vec![Arc::clone(panel)],
411 };
412 end_credentials(&mut db, user_id, &panels, REVOCATION).await?;
413 wake_bells(
414 cx,
415 user_id,
416 panels.iter().map(|panel| panel.prefix.as_str()),
417 );
418 return Ok(());
419 }
420 AuthSession::filter(AuthSession::fields().user_id().eq(user_id.to_string()))
421 .delete()
422 .exec(&mut db)
423 .await
424 .map_err(infrastructure_failure)?;
425 let realms = realms(cx);
426 if realms.iter().any(|realm| realm.auth.issues_tokens()) {
427 super::token::end_all(&mut db, user_id, None, REVOCATION).await?;
428 }
429 wake_bells(
430 cx,
431 user_id,
432 realms.iter().map(|realm| realm.prefix.as_str()),
433 );
434 Ok(())
435}
436
437fn wake_bells<'a>(cx: &Cx, user_id: &str, panels: impl Iterator<Item = &'a str>) {
439 let bus = crate::bus::bus(cx);
440 for panel in panels {
441 bus.wake(panel, user_id);
442 }
443}
444
445pub(super) const SESSION_SWEEP_BATCH: usize = 500;
448
449pub(super) async fn sweep_expired_sessions(cx: &Cx) -> topcoat::Result<()> {
451 let now = Timestamp::now();
452 let mut db = crate::db::db(cx)?;
453 let expired: Vec<String> = AuthSession::filter(AuthSession::fields().expires_at().le(now))
454 .limit(SESSION_SWEEP_BATCH)
455 .exec(&mut db)
456 .await
457 .map_err(infrastructure_failure)?
458 .into_iter()
459 .map(|row| row.token_hash)
460 .collect();
461 if expired.is_empty() {
462 return Ok(());
463 }
464 AuthSession::filter(
466 AuthSession::fields()
467 .token_hash()
468 .in_list(expired)
469 .and(AuthSession::fields().expires_at().le(now)),
470 )
471 .delete()
472 .exec(&mut db)
473 .await
474 .map_err(infrastructure_failure)?;
475 Ok(())
476}
477
478pub(super) async fn session_row(cx: &Cx, panel: &Realm) -> topcoat::Result<Option<AuthSession>> {
482 let Some(hash) = token_hash(cx, panel).await? else {
483 return Ok(None);
484 };
485 let key = token_key(&hash);
486 let mut db = crate::db::db(cx)?;
487 let row = AuthSession::filter(AuthSession::fields().token_hash().eq(key))
488 .first()
489 .exec(&mut db)
490 .await
491 .map_err(infrastructure_failure)?;
492 let Some(row) = row else {
493 return Ok(None);
494 };
495 if row.expires_at <= Timestamp::now() {
496 delete_session_row(cx, &row.token_hash).await?;
497 return Ok(None);
498 }
499 Ok((row.panel == panel.prefix).then_some(row))
500}
501
502pub(super) async fn session_user(
509 cx: &Cx,
510 row: AuthSession,
511 panel: &Arc<Realm>,
512 authenticator: &dyn DynAuthenticator,
513) -> topcoat::Result<Option<SignedIn>> {
514 match authenticator
515 .find_by_id(cx, &row.user_id)
516 .await
517 .map_err(infrastructure_failure)?
518 {
519 Some(user) if row.impersonating.is_some() => {
520 super::impersonation::resolve(cx, row, user, panel, authenticator)
521 .await
522 .map(Some)
523 }
524 Some(user) => Ok(Some(SignedIn {
525 user,
526 realm: Arc::clone(panel),
527 tenant: row.tenant,
528 impersonator: None,
529 })),
530 None => {
531 if panel.auth.external().is_none() {
532 delete_session_row(cx, &row.token_hash).await?;
533 }
534 Ok(None)
535 }
536 }
537}
538
539pub(super) async fn resolve(
541 cx: &Cx,
542 panel: &Arc<Realm>,
543 authenticator: &dyn DynAuthenticator,
544) -> topcoat::Result<Option<SignedIn>> {
545 match session_row(cx, panel).await? {
546 Some(row) => session_user(cx, row, panel, authenticator).await,
547 None => Ok(None),
548 }
549}