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 expires_at = Timestamp::try_from(session.expires_at).map_err(topcoat::Error::from)?;
58 insert(
59 &mut crate::db::db(cx),
60 &session.token_hash,
61 user.user_id(),
62 panel.prefix.clone(),
63 expires_at,
64 )
65 .await
66 .map_err(infrastructure_failure)
67}
68
69#[doc(hidden)]
75pub async fn mint_session(
76 ex: &mut dyn toasty::Executor,
77 panel: &str,
78 user: &dyn PanelUser,
79) -> topcoat::Result<String> {
80 let token = session::Token::random();
81 let expires_at = Timestamp::try_from(std::time::SystemTime::now() + SESSION_LIFETIME)
82 .map_err(topcoat::Error::from)?;
83 insert(
84 ex,
85 &token.hash(),
86 user.user_id(),
87 crate::panel::normalize_prefix(panel),
88 expires_at,
89 )
90 .await?;
91 Ok(token.encode())
92}
93
94async fn insert(
95 ex: &mut dyn toasty::Executor,
96 hash: &TokenHash,
97 user_id: String,
98 panel: String,
99 expires_at: Timestamp,
100) -> toasty::Result<()> {
101 toasty::create!(AuthSession {
102 token_hash: token_key(hash),
103 user_id,
104 panel,
105 tenant: None,
106 expires_at,
107 created_at: Timestamp::now(),
108 })
109 .exec(ex)
110 .await?;
111 Ok(())
112}
113
114pub(super) async fn select_tenant(cx: &Cx, tenant: Uuid) -> topcoat::Result<()> {
115 let Some(hash) = session::token_hash(cx).await? else {
116 return Err(topcoat::router::error::forbidden().into());
117 };
118 let mut db = crate::db::db(cx);
119 AuthSession::filter(AuthSession::fields().token_hash().eq(token_key(&hash)))
120 .update()
121 .tenant(Some(tenant))
122 .exec(&mut db)
123 .await
124 .map_err(infrastructure_failure)?;
125 Ok(())
126}
127
128pub(super) async fn delete_session(cx: &Cx, hash: &TokenHash) -> topcoat::Result<()> {
129 delete_session_row(cx, &token_key(hash)).await
130}
131
132async fn delete_session_row(cx: &Cx, key: &str) -> topcoat::Result<()> {
133 let mut db = crate::db::db(cx);
134 AuthSession::filter(AuthSession::fields().token_hash().eq(key.to_string()))
135 .delete()
136 .exec(&mut db)
137 .await
138 .map_err(infrastructure_failure)?;
139 Ok(())
140}
141
142pub async fn revoke_sessions_for_user(cx: &Cx, user_id: &str) -> topcoat::Result<()> {
149 let mut db = crate::db::db(cx);
150 let user = AuthSession::fields().user_id().eq(user_id.to_string());
151 let sessions = match current(cx) {
152 Some(panel) => {
153 AuthSession::filter(user.and(AuthSession::fields().panel().eq(panel.prefix.clone())))
154 }
155 None => AuthSession::filter(user),
156 };
157 sessions
158 .delete()
159 .exec(&mut db)
160 .await
161 .map_err(infrastructure_failure)?;
162 Ok(())
163}
164
165pub(super) const SESSION_SWEEP_BATCH: usize = 500;
168
169pub(super) async fn sweep_expired_sessions(cx: &Cx) -> topcoat::Result<()> {
171 let now = Timestamp::now();
172 let mut db = crate::db::db(cx);
173 let expired: Vec<String> = AuthSession::filter(AuthSession::fields().expires_at().le(now))
174 .limit(SESSION_SWEEP_BATCH)
175 .exec(&mut db)
176 .await
177 .map_err(infrastructure_failure)?
178 .into_iter()
179 .map(|row| row.token_hash)
180 .collect();
181 if expired.is_empty() {
182 return Ok(());
183 }
184 AuthSession::filter(
186 AuthSession::fields()
187 .token_hash()
188 .in_list(expired)
189 .and(AuthSession::fields().expires_at().le(now)),
190 )
191 .delete()
192 .exec(&mut db)
193 .await
194 .map_err(infrastructure_failure)?;
195 Ok(())
196}
197
198pub(super) async fn session_row(cx: &Cx) -> topcoat::Result<Option<AuthSession>> {
201 let Some(hash) = session::token_hash(cx).await? else {
202 return Ok(None);
203 };
204 let key = token_key(&hash);
205 let mut db = crate::db::db(cx);
206 let row = AuthSession::filter(AuthSession::fields().token_hash().eq(key))
207 .first()
208 .exec(&mut db)
209 .await
210 .map_err(infrastructure_failure)?;
211 let Some(row) = row else {
212 return Ok(None);
213 };
214 if row.expires_at <= Timestamp::now() {
215 delete_session_row(cx, &row.token_hash).await?;
216 return Ok(None);
217 }
218 Ok(Some(row))
219}
220
221pub(super) async fn session_user(
224 cx: &Cx,
225 row: AuthSession,
226 panel: &Arc<PanelState>,
227 authenticator: &dyn DynAuthenticator,
228) -> topcoat::Result<Option<SignedIn>> {
229 match authenticator
230 .find_by_id(cx, &row.user_id)
231 .await
232 .map_err(infrastructure_failure)?
233 {
234 Some(user) => Ok(Some(SignedIn {
235 user,
236 panel: Arc::clone(panel),
237 tenant: row.tenant,
238 })),
239 None => {
240 delete_session_row(cx, &row.token_hash).await?;
241 Ok(None)
242 }
243 }
244}
245
246pub(super) async fn resolve(
249 cx: &Cx,
250 panel: &Arc<PanelState>,
251 authenticator: &dyn DynAuthenticator,
252) -> topcoat::Result<Option<SignedIn>> {
253 match session_row(cx).await? {
254 Some(row) if row.panel == panel.prefix => session_user(cx, row, panel, authenticator).await,
255 _ => Ok(None),
256 }
257}