Skip to main content

sofie_http/shield/
arguments.rs

1use std::sync::Arc;
2
3use sofie_core::{Error, Result, Timestamp, Uuid};
4
5use super::blocks::{Grade, Key};
6use super::detector::{Message, Signal};
7use super::rule::Rule;
8use super::{RequestShield, Sentinel, effects};
9use crate::throttle::Requester;
10use crate::{Area, RequestContext};
11use sofie_db::Resolve;
12
13/// Without the shield running it counts nothing and locks nothing.
14pub struct SignIns {
15    pub(super) request: Option<RequestShield>,
16}
17
18impl SignIns {
19    /// For this caller: another caller's failures lock it out only past the account-wide count.
20    #[must_use]
21    pub fn locked(&self, account: Uuid) -> bool {
22        self.request.as_ref().is_some_and(|request| {
23            let sentinel = &request.sentinel;
24            let from = Key::AccountFrom(account, request.anonymous());
25            [Key::Account(account), from].into_iter().any(|key| {
26                sentinel
27                    .blocks
28                    .find(key, sentinel.now())
29                    .is_some_and(|block| block.grade == Grade::Lock)
30            })
31        })
32    }
33
34    pub fn failed(&self, account: Option<Uuid>) {
35        let Some(request) = &self.request else {
36            return;
37        };
38        request.count(Rule::SignIn, Key::Caller(request.anonymous()));
39        if let Some(account) = account {
40            let from = Key::AccountFrom(account, request.anonymous());
41            request.count(Rule::Account, from);
42            request.count(Rule::AccountTotal, Key::Account(account));
43        }
44    }
45
46    pub fn signed_up(&self) {
47        if let Some(request) = &self.request {
48            request.count(Rule::SignUp, Key::Caller(request.anonymous()));
49        }
50    }
51}
52
53impl RequestShield {
54    /// By address even when signed in, as whoever signs in next from there is someone else.
55    fn anonymous(&self) -> Requester {
56        Requester::of(None, self.address)
57    }
58
59    fn count(&self, rule: Rule, key: Key) {
60        self.sentinel.post(Message::Signal(Signal {
61            rule,
62            key,
63            address: self.address,
64            area: Some(self.area),
65            at: self.sentinel.now(),
66        }));
67    }
68}
69
70impl<A: Area> Resolve<RequestContext<A>> for SignIns {
71    fn resolve(context: &mut RequestContext<A>) -> impl Future<Output = Result<Self>> + Send {
72        std::future::ready(Ok(Self {
73            request: context.matched.shield.clone(),
74        }))
75    }
76}
77
78pub struct ShieldBlocks {
79    sentinel: Option<Arc<Sentinel>>,
80    keys: Arc<sofie_auth::SigningKeys>,
81}
82
83/// An address is shown by its network only.
84#[derive(Clone, Debug, PartialEq, Eq)]
85pub struct ActiveBlock {
86    pub id: String,
87    pub caller: String,
88    pub rule: &'static str,
89    pub grade: &'static str,
90    pub until: Timestamp,
91}
92
93impl ShieldBlocks {
94    #[must_use]
95    pub fn active(&self) -> Vec<ActiveBlock> {
96        let Some(sentinel) = &self.sentinel else {
97            return Vec::new();
98        };
99        sentinel
100            .blocks
101            .active(sentinel.now())
102            .into_iter()
103            .map(|(key, block)| ActiveBlock {
104                id: key.id(&self.keys),
105                caller: key.shown(),
106                rule: block.rule.name(),
107                grade: block.grade.name(),
108                until: Timestamp::from_microsecond(block.until).unwrap_or(Timestamp::MAX),
109            })
110            .collect()
111    }
112
113    /// Also on every instance when shared; the caller's counts start over.
114    pub async fn lift(&self, id: &str) -> Result<()> {
115        let sentinel = self.sentinel.as_ref().ok_or(Error::NotFound)?;
116        let key = sentinel
117            .blocks
118            .lift(id, &self.keys)
119            .ok_or(Error::NotFound)?;
120        sentinel.post(Message::Lifted(key));
121        tracing::warn!(shield.caller = %key.shown(), "a shield block was lifted");
122        match &sentinel.shared {
123            Some(db) => effects::lift_shared(db, key).await,
124            None => Ok(()),
125        }
126    }
127
128    fn of<A: Area, M>(context: &RequestContext<A, M>) -> Self {
129        Self {
130            sentinel: context
131                .matched
132                .shield
133                .as_ref()
134                .map(|request| Arc::clone(&request.sentinel)),
135            keys: Arc::clone(&context.matched.keys),
136        }
137    }
138}
139
140impl<A: Area> Resolve<RequestContext<A>> for ShieldBlocks {
141    fn resolve(context: &mut RequestContext<A>) -> impl Future<Output = Result<Self>> + Send {
142        std::future::ready(Ok(Self::of(context)))
143    }
144}
145
146/// What a GET may take: lifting a block needs `ShieldBlocks`, in a POST.
147pub struct ActiveBlocks(ShieldBlocks);
148
149impl ActiveBlocks {
150    #[must_use]
151    pub fn list(&self) -> Vec<ActiveBlock> {
152        self.0.active()
153    }
154}
155
156impl<A: Area, M> Resolve<RequestContext<A, M>> for ActiveBlocks {
157    fn resolve(context: &mut RequestContext<A, M>) -> impl Future<Output = Result<Self>> + Send {
158        std::future::ready(Ok(Self(ShieldBlocks::of(context))))
159    }
160}