Skip to main content

scone_core/
auth.rs

1//! The single authorization chokepoint (spec invariant I5).
2//!
3//! Every core read/write API requires a [`ScopedSpace`], and this module is
4//! the only place one can be constructed. Access rules live here once, so
5//! surfaces cannot re-implement (and diverge on) them — the failure mode the
6//! predecessor shipped (memory/bugs.md P-1).
7
8use crate::Engine;
9use crate::error::{Result, SconeError};
10
11/// Proof of access to one space. Construction is private to this module.
12#[derive(Debug, Clone)]
13pub struct ScopedSpace {
14    id: i64,
15    name: String,
16}
17
18impl ScopedSpace {
19    pub fn name(&self) -> &str {
20        &self.name
21    }
22
23    pub(crate) fn id(&self) -> i64 {
24        self.id
25    }
26}
27
28/// Resolve a space by name, optionally creating it.
29///
30/// Names are bounded at the surface (memory/bugs.md P-4): 1..=64 chars of
31/// `[a-z0-9-_]`.
32pub fn resolve(engine: &mut Engine, name: &str, create: bool) -> Result<ScopedSpace> {
33    if name.is_empty()
34        || name.len() > 64
35        || !name
36            .chars()
37            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '_')
38    {
39        return Err(SconeError::InvalidInput(format!(
40            "space name must be 1..=64 chars of [a-z0-9-_], got {name:?}"
41        )));
42    }
43    let conn = engine.conn_mut();
44    if create {
45        conn.execute("INSERT OR IGNORE INTO spaces (name) VALUES (?1)", [name])?;
46    }
47    let id = conn
48        .query_row("SELECT id FROM spaces WHERE name = ?1", [name], |r| {
49            r.get(0)
50        })
51        .map_err(|e| match e {
52            rusqlite::Error::QueryReturnedNoRows => SconeError::NotFound(format!("space {name:?}")),
53            other => SconeError::Db(other),
54        })?;
55    Ok(ScopedSpace {
56        id,
57        name: name.to_owned(),
58    })
59}