1use std::path::Path;
2
3use base64::Engine;
4use rusqlite::{OptionalExtension, TransactionBehavior, params};
5use serde::{Deserialize, Serialize};
6use sha2::{Digest, Sha256};
7use tokio_rusqlite::Connection;
8
9use crate::broker::now;
10
11const TOKEN_SECRET_BYTES: usize = 32;
13
14pub const DATABASE: &str = "review-store.sqlite";
15const SCHEMA_VERSION: u32 = 3;
16pub const MAX_ENTRIES: i64 = 100_000;
19pub const MAX_BYTES: i64 = 4 * 1024 * 1024 * 1024;
21
22#[derive(
23 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, clap::ValueEnum,
24)]
25#[serde(rename_all = "lowercase")]
26pub enum Scope {
27 Read,
29 Publish,
31 Human,
33}
34
35impl Scope {
36 pub fn name(self) -> &'static str {
37 match self {
38 Self::Read => "read",
39 Self::Publish => "publish",
40 Self::Human => "human",
41 }
42 }
43}
44
45#[derive(Debug, Clone, Serialize)]
46#[serde(rename_all = "camelCase")]
47pub struct Token {
48 pub name: crate::types::TokenName,
49 pub scopes: Vec<Scope>,
50 pub created_at: crate::types::Timestamp,
51 pub revoked_at: Option<crate::types::Timestamp>,
52}
53
54#[derive(Debug, Serialize)]
55#[serde(rename_all = "camelCase")]
56pub struct Revocation {
57 pub name: crate::types::TokenName,
58 pub revoked_at: crate::types::Timestamp,
59 pub purged_entries: usize,
60}
61
62#[derive(Debug, thiserror::Error)]
63enum Error {
64 #[error(transparent)]
65 Sql(#[from] rusqlite::Error),
66 #[error(transparent)]
67 Json(#[from] serde_json::Error),
68 #[error("{0}")]
69 Invalid(String),
70}
71
72#[derive(Clone)]
73pub struct Store {
74 connection: Connection,
75}
76
77fn digest(token: &str) -> crate::types::Sha256Digest {
78 Sha256::digest(token.as_bytes())
79 .iter()
80 .map(|byte| format!("{byte:02x}"))
81 .collect::<String>()
82 .parse()
83 .expect("SHA-256 token digest")
84}
85
86fn reuse_keys(db: &rusqlite::Transaction<'_>) -> Result<(), Error> {
90 db.execute_batch("DROP TABLE IF EXISTS entries;")?;
91 Ok(())
92}
93
94impl Store {
95 pub async fn open(state: &Path) -> Result<Self, String> {
97 let state = crate::store::state_dir(Some(state))?;
98 crate::platform::create_private_dir_all(&state).map_err(|e| e.to_string())?;
99 for suffix in ["", "-wal", "-shm"] {
100 let path = state.join(format!("{DATABASE}{suffix}"));
101 match crate::platform::path_kind(&path) {
102 Ok(crate::platform::FileKind::File) => {}
103 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
104 Ok(_) => {
105 return Err(format!(
106 "Review store files must be regular files: {}",
107 crate::platform::path_text(&path)
108 ));
109 }
110 Err(error) => return Err(error.to_string()),
111 }
112 }
113 let connection = Connection::open(state.join(DATABASE))
114 .await
115 .map_err(|e| e.to_string())?;
116 connection
117 .call(|db| -> Result<(), Error> {
118 db.busy_timeout(crate::store::SQLITE_BUSY_TIMEOUT)?;
119 let version: u32 = db.pragma_query_value(None, "user_version", |row| row.get(0))?;
120 if version > SCHEMA_VERSION {
121 return Err(Error::Invalid(format!(
122 "Unsupported review store schema version: {version}"
123 )));
124 }
125 db.pragma_update(None, "journal_mode", "WAL")?;
126 let transaction = db.transaction_with_behavior(TransactionBehavior::Immediate)?;
127 if matches!(
129 transaction
130 .pragma_query_value(None, "user_version", |row| row.get::<_, u32>(0))?,
131 1 | 2
132 ) {
133 reuse_keys(&transaction)?;
134 }
135 transaction.execute_batch(
136 "CREATE TABLE IF NOT EXISTS tokens(name TEXT PRIMARY KEY, hash TEXT NOT NULL UNIQUE, scopes TEXT NOT NULL, created_at TEXT NOT NULL, revoked_at TEXT);
137 CREATE TABLE IF NOT EXISTS entries(key TEXT NOT NULL, execution_id TEXT NOT NULL, publisher TEXT NOT NULL, completed_at TEXT NOT NULL, bytes INTEGER NOT NULL, last_used TEXT NOT NULL, data TEXT NOT NULL, UNIQUE(key, execution_id));
138 CREATE INDEX IF NOT EXISTS entries_key ON entries(key, completed_at);
139 CREATE INDEX IF NOT EXISTS entries_lru ON entries(last_used, key);
140 CREATE INDEX IF NOT EXISTS entries_publisher ON entries(publisher);",
141 )?;
142 transaction.pragma_update(None, "user_version", SCHEMA_VERSION)?;
143 transaction.commit()?;
144 Ok(())
145 })
146 .await
147 .map_err(|e| e.to_string())?;
148 Ok(Self { connection })
149 }
150
151 pub async fn add_token(
153 &self,
154 name: &crate::types::TokenName,
155 scopes: &[Scope],
156 ) -> Result<String, String> {
157 let mut scopes = scopes.to_vec();
158 scopes.sort();
159 scopes.dedup();
160 if scopes.is_empty() {
161 return Err("A token needs at least one scope.".into());
162 }
163 let mut secret = [0; TOKEN_SECRET_BYTES];
164 getrandom::fill(&mut secret).map_err(|_| "Cannot obtain secure randomness.".to_owned())?;
165 let token = format!(
166 "azt_{}",
167 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(secret)
168 );
169 let (name, hash, scopes) = (
170 name.clone(),
171 digest(&token),
172 serde_json::to_string(&scopes).map_err(|e| e.to_string())?,
173 );
174 self.connection
175 .call(move |db| -> Result<(), Error> {
176 let inserted = db.execute(
177 "INSERT INTO tokens(name,hash,scopes,created_at) VALUES (?,?,?,?) ON CONFLICT(name) DO NOTHING",
178 params![name, hash, scopes, now()],
179 )?;
180 if inserted == 0 {
181 return Err(Error::Invalid(format!(
182 "Token name {name} is already used; names of revoked tokens are kept."
183 )));
184 }
185 Ok(())
186 })
187 .await
188 .map_err(|e| e.to_string())?;
189 Ok(token)
190 }
191
192 pub async fn tokens(&self) -> Result<Vec<Token>, String> {
193 self.connection
194 .call(|db| -> Result<_, Error> {
195 let mut statement = db.prepare(
196 "SELECT name,scopes,created_at,revoked_at FROM tokens ORDER BY name",
197 )?;
198 let rows = statement
199 .query_map([], |row| {
200 Ok((
201 row.get::<_, crate::types::TokenName>(0)?,
202 row.get::<_, String>(1)?,
203 row.get(2)?,
204 row.get(3)?,
205 ))
206 })?
207 .map(|row| {
208 let (name, scopes, created_at, revoked_at) = row?;
209 Ok(Token {
210 name,
211 scopes: serde_json::from_str(&scopes)?,
212 created_at,
213 revoked_at,
214 })
215 })
216 .collect::<Result<_, Error>>()?;
217 Ok(rows)
218 })
219 .await
220 .map_err(|e| e.to_string())
221 }
222
223 pub async fn revoke(
225 &self,
226 name: &crate::types::TokenName,
227 purge: bool,
228 ) -> Result<Revocation, String> {
229 let name = name.clone();
230 self.connection
231 .call(move |db| -> Result<_, Error> {
232 let transaction = db.transaction_with_behavior(TransactionBehavior::Immediate)?;
233 transaction.execute(
234 "UPDATE tokens SET revoked_at=? WHERE name=? AND revoked_at IS NULL",
235 params![now(), name],
236 )?;
237 let revoked_at: crate::types::Timestamp = transaction
238 .query_row(
239 "SELECT revoked_at FROM tokens WHERE name=?",
240 [&name],
241 |row| row.get(0),
242 )
243 .optional()?
244 .ok_or_else(|| Error::Invalid(format!("Unknown token {name}.")))?;
245 let purged_entries = if purge {
246 transaction.execute("DELETE FROM entries WHERE publisher=?", [&name])?
247 } else {
248 0
249 };
250 transaction.commit()?;
251 Ok(Revocation {
252 name,
253 revoked_at,
254 purged_entries,
255 })
256 })
257 .await
258 .map_err(|e| e.to_string())
259 }
260
261 pub async fn remove(&self, key: &crate::types::ReuseKey) -> Result<bool, String> {
263 let key = key.clone();
264 self.connection
265 .call(move |db| -> Result<_, Error> {
266 Ok(db.execute("DELETE FROM entries WHERE key=?", [key])? != 0)
267 })
268 .await
269 .map_err(|e| e.to_string())
270 }
271
272 pub(super) async fn authenticate(
274 &self,
275 token: &str,
276 ) -> Result<Option<(crate::types::TokenName, Vec<Scope>)>, String> {
277 let hash = digest(token);
278 self.connection
279 .call(move |db| -> Result<_, Error> {
280 let row: Option<(crate::types::TokenName, String)> = db
281 .query_row(
282 "SELECT name,scopes FROM tokens WHERE hash=? AND revoked_at IS NULL",
283 [hash],
284 |row| Ok((row.get(0)?, row.get(1)?)),
285 )
286 .optional()?;
287 row.map(|(name, scopes)| Ok((name, serde_json::from_str(&scopes)?)))
288 .transpose()
289 })
290 .await
291 .map_err(|e| e.to_string())
292 }
293
294 pub(super) async fn lookup(
296 &self,
297 keys: Vec<crate::types::ReuseKey>,
298 ) -> Result<Vec<String>, String> {
299 self.connection
300 .call(move |db| -> Result<_, Error> {
301 let transaction = db.transaction_with_behavior(TransactionBehavior::Immediate)?;
302 let used = now();
303 let mut found = Vec::new();
304 for key in keys {
305 let data: Option<String> = transaction
306 .query_row(
307 "UPDATE entries SET last_used=? WHERE rowid=(SELECT rowid FROM entries WHERE key=? ORDER BY completed_at DESC, rowid DESC LIMIT 1) RETURNING data",
308 params![used, key],
309 |row| row.get(0),
310 )
311 .optional()?;
312 found.extend(data);
313 }
314 transaction.commit()?;
315 Ok(found)
316 })
317 .await
318 .map_err(|e| e.to_string())
319 }
320
321 pub(super) async fn insert(
323 &self,
324 key: &crate::types::ReuseKey,
325 execution_id: &crate::types::ExecutionId,
326 publisher: &crate::types::TokenName,
327 completed_at: crate::types::Timestamp,
328 data: String,
329 ) -> Result<bool, String> {
330 let record = (
331 key.clone(),
332 execution_id.clone(),
333 publisher.clone(),
334 completed_at,
335 );
336 self.connection
337 .call(move |db| -> Result<_, Error> {
338 let (key, execution_id, publisher, completed_at) = record;
339 let transaction = db.transaction_with_behavior(TransactionBehavior::Immediate)?;
340 let used = now();
341 let created = transaction.execute(
342 "INSERT INTO entries(key,execution_id,publisher,completed_at,bytes,last_used,data) VALUES (?,?,?,?,?,?,?) ON CONFLICT DO NOTHING",
343 params![
344 key,
345 execution_id,
346 publisher,
347 completed_at,
348 data.len() as i64,
349 used,
350 data
351 ],
352 )? == 1;
353 loop {
355 let (count, bytes): (i64, i64) = transaction.query_row(
356 "SELECT count(*),coalesce(sum(bytes),0) FROM entries",
357 [],
358 |row| Ok((row.get(0)?, row.get(1)?)),
359 )?;
360 if count <= MAX_ENTRIES && bytes <= MAX_BYTES {
361 break;
362 }
363 if transaction.execute(
364 "DELETE FROM entries WHERE rowid=(SELECT rowid FROM entries WHERE NOT (key=?1 AND execution_id=?2) ORDER BY last_used,key,completed_at LIMIT 1)",
365 params![key, execution_id],
366 )? == 0
367 {
368 break;
369 }
370 }
371 transaction.commit()?;
372 Ok(created)
373 })
374 .await
375 .map_err(|e| e.to_string())
376 }
377}