Skip to main content

didbot_serve/
lookups.rs

1//! Reading the records a write's policies declared they read, before the
2//! write is judged.
3//!
4//! The gate is judged inside a synchronous section, so nothing under it can
5//! await and nothing under it may reach the network. The read is therefore
6//! split in two, in the order a write meets them:
7//!
8//! 1. [`Reader::prefetch`], from the route that accepted the write, while
9//!    there is still a runtime. It works out which records the applicable
10//!    policies want for this write and fills `didbot-lookup`'s cache with
11//!    what it does not already hold, inside that crate's own deadlines.
12//! 2. [`Reader::answers`], from `didbot-pds`'s write path, which is
13//!    `didbot_pds::policy::WriteLookups`. It reads the same cache, does no
14//!    I/O, and hands the answers to the gate as ordinary subject fields.
15//!
16//! Both halves work out what a write wants from the same code, so a record
17//! the first half reads is the record the second half looks for.
18
19use std::sync::{Arc, RwLock};
20
21use didbot_lookup::{Answer, ForeignRecords, RecordRef, MAX_NAMED_ACCOUNTS};
22use didbot_pds::policy::{LookupRefusal, OwnedFound, OwnedLookup, WriteLookups};
23use didbot_pds::Registry;
24use didbot_policy_records::records::{Accounts, Lookup};
25use didbot_policy_records::Lookups;
26use serde_json::Value;
27use tracing::debug;
28
29/// The lookups this deployment's policies declare, swapped each time the
30/// policy set is rebuilt.
31///
32/// It starts holding the built-ins' lookups rather than none. A deployment
33/// that has not polled its operator yet still enforces the floor, and a
34/// floor rule whose records were never read would stand aside on every
35/// write — which is a rule that is off, spelled as a rule that is on.
36pub struct DeclaredLookups(RwLock<Arc<Lookups>>);
37
38impl DeclaredLookups {
39    /// The lookups of the policies compiled into this binary.
40    #[must_use]
41    pub fn new() -> Self {
42        Self(RwLock::new(Arc::new(Lookups::of(
43            &didbot_policy_records::builtin_policies(),
44            &std::collections::HashMap::new(),
45            &std::collections::HashMap::new(),
46        ))))
47    }
48
49    /// Replaces them with the lookups of a newly built set.
50    pub fn publish(&self, lookups: Lookups) {
51        *self.0.write().unwrap_or_else(|p| p.into_inner()) = Arc::new(lookups);
52    }
53
54    /// The lookups in force right now.
55    #[must_use]
56    pub fn current(&self) -> Arc<Lookups> {
57        self.0.read().unwrap_or_else(|p| p.into_inner()).clone()
58    }
59}
60
61impl Default for DeclaredLookups {
62    fn default() -> Self {
63        Self::new()
64    }
65}
66
67/// This deployment's two-phase read of the records its policies declare.
68pub struct Reader {
69    declared: Arc<DeclaredLookups>,
70    foreign: Arc<ForeignRecords>,
71    registry: Arc<dyn Registry>,
72}
73
74impl Reader {
75    /// A reader over this deployment's declared lookups, its one foreign
76    /// record reader, and its registry — which is what says an account is
77    /// this deployment's own.
78    #[must_use]
79    pub fn new(
80        declared: Arc<DeclaredLookups>,
81        foreign: Arc<ForeignRecords>,
82        registry: Arc<dyn Registry>,
83    ) -> Self {
84        Self {
85            declared,
86            foreign,
87            registry,
88        }
89    }
90
91    /// Every record this write's policies want, each once, or the refusal
92    /// of a write naming more accounts than they can all be asked about.
93    ///
94    /// An account this deployment holds is not one of them. Its records are
95    /// this server's own, the reader refuses to fetch them over the
96    /// network, and spending a write's lookup budget on a refusal it can
97    /// predict would leave less of it for the strangers the rule is about.
98    fn wanted(&self, collection: &str, after: Option<&Value>) -> Result<Wanted, LookupRefusal> {
99        let declared = self.declared.current();
100        let for_write = declared.for_write(collection);
101        if for_write.is_empty() {
102            return Ok(Vec::new());
103        }
104        let named: Vec<String> = after
105            .map(didbot_policy_records::named_accounts)
106            .unwrap_or_default()
107            .into_iter()
108            .filter(|did| self.registry.account(did).is_none())
109            .collect();
110        let checked = MAX_NAMED_ACCOUNTS.min(self.foreign.limits().per_write / for_write.len());
111        if named.len() > checked {
112            return Err(LookupRefusal(format!(
113                "this record names more than {checked} accounts, which is more than this server \
114                 checks for a declared AI preference; name fewer"
115            )));
116        }
117        let mut wanted = Vec::new();
118        for lookup in for_write {
119            let accounts: &[String] = match lookup.accounts {
120                Accounts::Named => &named,
121                // A word this build does not know names nobody, so it
122                // reads nothing: see `Accounts`.
123                Accounts::Other(_) => &[],
124            };
125            for account in accounts {
126                let reference = match &lookup.rkey {
127                    Some(rkey) => RecordRef::new(account, &lookup.collection, rkey),
128                    None => RecordRef::collection(account, &lookup.collection),
129                };
130                wanted.push((lookup.clone(), account.clone(), reference));
131            }
132        }
133        Ok(wanted)
134    }
135
136    /// Fills the cache with what this write's policies want and it does not
137    /// already hold. Bounded by `didbot-lookup`'s own deadlines and by how
138    /// many records one write may read. A write the write path will refuse
139    /// for naming too many accounts reads nothing.
140    pub async fn prefetch(&self, collection: &str, after: Option<&Value>) {
141        let Ok(wanted) = self.wanted(collection, after) else {
142            return;
143        };
144        if wanted.is_empty() {
145            return;
146        }
147        let references: Vec<RecordRef> = wanted.into_iter().map(|(_, _, r)| r).collect();
148        self.foreign.prefetch(&references).await;
149    }
150}
151
152/// What one write wants read: the lookup, the account it is about, and the
153/// record to read.
154type Wanted = Vec<(Lookup, String, RecordRef)>;
155
156impl WriteLookups for Reader {
157    fn answers(
158        &self,
159        _account: &str,
160        collection: &str,
161        after: Option<&Value>,
162    ) -> Result<Vec<OwnedLookup>, LookupRefusal> {
163        let handles = after
164            .map(didbot_policy_records::named_handles)
165            .unwrap_or_default();
166        Ok(self
167            .wanted(collection, after)?
168            .into_iter()
169            .map(|(lookup, account, reference)| {
170                let uri = reference.at_uri();
171                let found = match self.foreign.read(&reference) {
172                    Answer::Present(records) => OwnedFound::Records((*records).clone()),
173                    Answer::Absent => OwnedFound::Nothing,
174                    Answer::Unknown(unknown) => OwnedFound::Unknown(unknown.as_str().to_owned()),
175                };
176                debug!(
177                    lookup = %lookup.name,
178                    names = %account,
179                    uri = %uri,
180                    found = match &found {
181                        OwnedFound::Records(_) => "records",
182                        OwnedFound::Nothing => "nothing",
183                        OwnedFound::Unknown(why) => why.as_str(),
184                    },
185                    "read what a policy declared it reads about an account this write names"
186                );
187                let handle = handles
188                    .iter()
189                    .find(|(did, _)| *did == account)
190                    .map(|(_, handle)| handle.clone());
191                OwnedLookup {
192                    name: lookup.name.clone(),
193                    account,
194                    handle,
195                    uri,
196                    found,
197                }
198            })
199            .collect())
200    }
201}