tablo_core/auth/throttle/
shared.rs1use std::{fmt::Write as _, net::IpAddr};
5
6use jiff::{SignedDuration, Timestamp};
7use sha2::{Digest, Sha256};
8use topcoat::context::Cx;
9
10use super::Limit;
11use crate::auth::{infrastructure_failure, realm::panel_prefix, token::run_serializable};
12
13#[derive(Debug, Clone, toasty::Model)]
16pub struct AuthThrottle {
17 #[key]
20 pub key: String,
21 #[index]
23 pub scope: String,
24 pub attempts: i64,
25 pub since: Timestamp,
27}
28
29pub(super) struct Slot {
31 scope: String,
32 key: String,
33}
34
35impl Slot {
36 pub(super) fn of(cx: &Cx, scope: &str, login: &str, client: Option<IpAddr>) -> Self {
38 let scope = format!("{}:{scope}", panel_prefix(cx));
39 let client = client.map(|client| client.to_string()).unwrap_or_default();
40 let mut hash = Sha256::new();
41 for part in [
42 scope.as_str(),
43 login.trim().to_lowercase().as_str(),
44 &client,
45 ] {
46 hash.update(part.len().to_be_bytes());
48 hash.update(part.as_bytes());
49 }
50 let mut key = String::with_capacity(64);
51 for byte in hash.finalize() {
52 write!(key, "{byte:02x}").expect("writing to a String cannot fail");
53 }
54 Self { scope, key }
55 }
56}
57
58pub(super) async fn attempt(cx: &Cx, limit: Limit, slot: &Slot) -> topcoat::Result<bool> {
60 let now = Timestamp::now();
61 let began = now
62 .checked_sub(SignedDuration::try_from(limit.window).map_err(topcoat::Error::from)?)
63 .map_err(topcoat::Error::from)?;
64 let fresh = run_serializable!(cx, |tx| async {
65 let row = AuthThrottle::filter(AuthThrottle::fields().key().eq(slot.key.clone()))
66 .first()
67 .exec(&mut *tx)
68 .await
69 .map_err(infrastructure_failure)?;
70 match row {
71 Some(row) if row.since > began => {
72 if row.attempts >= i64::from(limit.attempts) {
73 return Ok(None);
74 }
75 AuthThrottle::filter(AuthThrottle::fields().key().eq(slot.key.clone()))
76 .update()
77 .attempts(row.attempts + 1)
78 .exec(&mut *tx)
79 .await
80 .map_err(infrastructure_failure)?;
81 Ok(Some(false))
82 }
83 stale => {
84 if stale.is_some() {
85 AuthThrottle::filter(AuthThrottle::fields().key().eq(slot.key.clone()))
86 .delete()
87 .exec(&mut *tx)
88 .await
89 .map_err(infrastructure_failure)?;
90 }
91 toasty::create!(AuthThrottle {
92 key: slot.key.clone(),
93 scope: slot.scope.clone(),
94 attempts: 1,
95 since: now,
96 })
97 .exec(&mut *tx)
98 .await
99 .map_err(infrastructure_failure)?;
100 Ok(Some(true))
101 }
102 }
103 })?;
104 let Some(fresh) = fresh else {
105 return Ok(false);
106 };
107 if fresh {
108 prune(cx, &slot.scope, began).await?;
110 }
111 Ok(true)
112}
113
114pub(super) async fn clear(cx: &Cx, slot: &Slot) -> topcoat::Result<()> {
116 let mut db = crate::db::db(cx)?;
117 AuthThrottle::filter(AuthThrottle::fields().key().eq(slot.key.clone()))
118 .delete()
119 .exec(&mut db)
120 .await
121 .map_err(infrastructure_failure)
122}
123
124async fn prune(cx: &Cx, scope: &str, began: Timestamp) -> topcoat::Result<()> {
127 let mut db = crate::db::db(cx)?;
128 AuthThrottle::filter(
129 AuthThrottle::fields()
130 .scope()
131 .eq(scope.to_owned())
132 .and(AuthThrottle::fields().since().le(began)),
133 )
134 .delete()
135 .exec(&mut db)
136 .await
137 .map_err(infrastructure_failure)
138}