| 1 | //! The HTTP API: the store's operations, with the actor taken from the request's token. |
| 2 | //! |
| 3 | //! `Authorization: Bearer <token>`. A person's token can do anything that person can; an |
| 4 | //! agent's token acts as that agent, so the model refuses it the decisions a person must make |
| 5 | //! in person (landing, declining, groups, tokens). Public projects can be read without a token. |
| 6 | |
| 7 | use crate::http::{Request, Response}; |
| 8 | use crate::json::{self, obj, Value}; |
| 9 | use crate::model::{Actor, ChangeState, Step}; |
| 10 | use crate::objects::{Id, Objects}; |
| 11 | use crate::store::{self, Content, Store}; |
| 12 | use crate::{hex, wire}; |
| 13 | use std::sync::Mutex; |
| 14 | |
| 15 | pub struct Api { |
| 16 | store: Mutex<Store>, |
| 17 | /// Object reads and writes don't need the store's lock. |
| 18 | objects: Objects, |
| 19 | } |
| 20 | |
| 21 | type Out = Result<Response, Response>; |
| 22 | |
| 23 | fn err(status: u16, msg: impl AsRef<str>) -> Response { |
| 24 | Response::error(status, msg.as_ref()) |
| 25 | } |
| 26 | |
| 27 | fn from_store(e: store::Error) -> Response { |
| 28 | match e { |
| 29 | store::Error::Refused(m) => err(400, m), |
| 30 | store::Error::Behind { .. } => err(409, e.to_string()), |
| 31 | other => err(500, other.to_string()), |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | fn body(req: &Request) -> Result<Value, Response> { |
| 36 | if req.body.is_empty() { |
| 37 | return Ok(Value::Object(Vec::new())); |
| 38 | } |
| 39 | let text = std::str::from_utf8(&req.body).map_err(|_| err(400, "body isn't UTF-8"))?; |
| 40 | json::parse(text).map_err(|e| err(400, e.to_string())) |
| 41 | } |
| 42 | |
| 43 | fn field<'a>(v: &'a Value, k: &str) -> Result<&'a str, Response> { |
| 44 | v.get(k).and_then(Value::as_str).ok_or_else(|| err(400, format!("missing {k}"))) |
| 45 | } |
| 46 | |
| 47 | fn num(v: &Value, k: &str) -> Result<u64, Response> { |
| 48 | v.get(k).and_then(Value::as_i64).filter(|n| *n >= 0).map(|n| n as u64).ok_or_else(|| err(400, format!("missing {k}"))) |
| 49 | } |
| 50 | |
| 51 | fn parse_id(h: &str) -> Result<Id, Response> { |
| 52 | hex::decode(h).and_then(|b| b.try_into().ok()).ok_or_else(|| err(400, format!("{h:?} isn't an id"))) |
| 53 | } |
| 54 | |
| 55 | fn ids(v: &Value, k: &str) -> Result<Vec<Id>, Response> { |
| 56 | v.get(k).and_then(Value::as_array).ok_or_else(|| err(400, format!("missing {k}")))?.iter().map(|x| parse_id(x.as_str().unwrap_or(""))).collect() |
| 57 | } |
| 58 | |
| 59 | fn ok(v: Value) -> Out { |
| 60 | Ok(Response::json(200, &v)) |
| 61 | } |
| 62 | |
| 63 | impl Api { |
| 64 | pub fn new(store: Store) -> std::io::Result<Api> { |
| 65 | let objects = Objects::open(&store.dir().join("objects")).map_err(|e| std::io::Error::other(e.to_string()))?; |
| 66 | Ok(Api { store: Mutex::new(store), objects }) |
| 67 | } |
| 68 | |
| 69 | /// The store, even if an earlier request panicked while holding it. Store writes are |
| 70 | /// all-or-nothing, so a panic can't leave it half-changed. |
| 71 | fn store(&self) -> std::sync::MutexGuard<'_, Store> { |
| 72 | self.store.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) |
| 73 | } |
| 74 | |
| 75 | pub fn handle(&self, req: Request) -> Response { |
| 76 | match self.route(&req) { |
| 77 | Ok(r) | Err(r) => r, |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | fn actor(&self, req: &Request) -> Result<Option<Actor>, Response> { |
| 82 | let Some(h) = req.header("authorization") else { return Ok(None) }; |
| 83 | let token = h.strip_prefix("Bearer ").ok_or_else(|| err(401, "use Authorization: Bearer <token>"))?; |
| 84 | let store = self.store(); |
| 85 | store.authenticate(token.trim()).map(Some).ok_or_else(|| err(401, "unknown or revoked token")) |
| 86 | } |
| 87 | |
| 88 | fn required(&self, req: &Request) -> Result<Actor, Response> { |
| 89 | self.actor(req)?.ok_or_else(|| err(401, "sign in: send a token")) |
| 90 | } |
| 91 | |
| 92 | fn route(&self, req: &Request) -> Out { |
| 93 | let segs: Vec<&str> = req.path.trim_matches('/').split('/').collect(); |
| 94 | let m = req.method.as_str(); |
| 95 | match (m, segs.as_slice()) { |
| 96 | ("GET", ["api", "health"]) => ok(obj([("ok", true.into())])), |
| 97 | ("GET", ["api", "whoami"]) => { |
| 98 | let a = self.required(req)?; |
| 99 | let store = self.store(); |
| 100 | let agent_name = a.agent.as_ref().and_then(|id| store.state().agent(id)).map(|g| g.name.clone()); |
| 101 | ok(obj([("person", a.person.clone().into()), ("agent", a.agent.clone().into()), ("agent_name", agent_name.into())])) |
| 102 | } |
| 103 | |
| 104 | // ── Objects ── |
| 105 | ("POST", ["api", "objects", "missing"]) => { |
| 106 | self.required(req)?; |
| 107 | let wanted = ids(&body(req)?, "ids")?; |
| 108 | let missing: Vec<Value> = wanted.iter().filter(|i| !self.objects.has(i)).map(|i| Value::Str(hex::encode(i))).collect(); |
| 109 | ok(obj([("missing", Value::Array(missing))])) |
| 110 | } |
| 111 | ("POST", ["api", "objects"]) => { |
| 112 | self.required(req)?; |
| 113 | let mut stored = 0u64; |
| 114 | for raw in wire::unframe_objects(&req.body).map_err(|e| err(400, e))? { |
| 115 | self.objects.put_raw(raw).map_err(|e| err(400, e.to_string()))?; |
| 116 | stored += 1; |
| 117 | } |
| 118 | ok(obj([("stored", stored.into())])) |
| 119 | } |
| 120 | ("POST", ["api", "objects", "list"]) => { |
| 121 | let root = parse_id(field(&body(req)?, "root")?)?; |
| 122 | let all = self.objects.ids_under(&root).map_err(|e| err(404, e.to_string()))?; |
| 123 | ok(obj([("ids", Value::Array(all.iter().map(|i| Value::Str(hex::encode(i))).collect()))])) |
| 124 | } |
| 125 | ("POST", ["api", "objects", "fetch"]) => { |
| 126 | let wanted = ids(&body(req)?, "ids")?; |
| 127 | let raws = wanted.iter().map(|i| self.objects.get_raw(i)).collect::<Result<Vec<_>, _>>().map_err(|e| err(404, e.to_string()))?; |
| 128 | Ok(Response::bytes(wire::frame_objects(&raws))) |
| 129 | } |
| 130 | |
| 131 | // ── People, agents, tokens, groups ── |
| 132 | ("POST", ["api", "agents"]) => { |
| 133 | let a = self.required(req)?; |
| 134 | if a.agent.is_some() { |
| 135 | return Err(err(403, "agents are registered in person")); |
| 136 | } |
| 137 | let name = field(&body(req)?, "name")?.to_string(); |
| 138 | let id = self.store().add_agent(&a.person, &name).map_err(from_store)?; |
| 139 | ok(obj([("id", id.into())])) |
| 140 | } |
| 141 | ("POST", ["api", "tokens"]) => { |
| 142 | let a = self.required(req)?; |
| 143 | if a.agent.is_some() { |
| 144 | return Err(err(403, "tokens are issued in person")); |
| 145 | } |
| 146 | let b = body(req)?; |
| 147 | let agent = b.get("agent").and_then(Value::as_str).map(str::to_string); |
| 148 | let token = self.store().issue_token(&a.person, agent.as_deref()).map_err(from_store)?; |
| 149 | ok(obj([("token", token.into())])) |
| 150 | } |
| 151 | ("POST", ["api", "groups"]) => { |
| 152 | let a = self.required(req)?; |
| 153 | let name = field(&body(req)?, "name")?.to_string(); |
| 154 | self.store().create_group(&a, &name).map_err(from_store)?; |
| 155 | ok(obj([("name", name.into())])) |
| 156 | } |
| 157 | ("POST", ["api", "groups", group, "members"]) => { |
| 158 | let a = self.required(req)?; |
| 159 | let person = field(&body(req)?, "person")?.to_string(); |
| 160 | self.store().add_member(&a, group, &person).map_err(from_store)?; |
| 161 | ok(obj([("added", person.into())])) |
| 162 | } |
| 163 | |
| 164 | // ── Projects ── |
| 165 | ("POST", ["api", "projects"]) => { |
| 166 | let a = self.required(req)?; |
| 167 | let b = body(req)?; |
| 168 | let owner = b.get("owner").and_then(Value::as_str).unwrap_or(&a.person).to_string(); |
| 169 | let name = field(&b, "name")?.to_string(); |
| 170 | let public = !matches!(b.get("public"), Some(Value::Bool(false))); |
| 171 | self.store().create_project(&a, &owner, &name, public).map_err(from_store)?; |
| 172 | ok(obj([("owner", owner.into()), ("name", name.into())])) |
| 173 | } |
| 174 | (_, ["api", "projects", owner, name, rest @ ..]) => self.project_route(req, m, owner, name, rest), |
| 175 | _ => Err(err(404, "no such endpoint")), |
| 176 | } |
| 177 | } |
| 178 | |
| 179 | fn project_route(&self, req: &Request, m: &str, owner: &str, name: &str, rest: &[&str]) -> Out { |
| 180 | let actor = self.actor(req)?; |
| 181 | let mut store = self.store(); |
| 182 | { |
| 183 | let p = store.project(owner, name).map_err(|_| err(404, format!("no project {owner}/{name}")))?; |
| 184 | let can_read = p.public || actor.as_ref().is_some_and(|a| store.state().owns(&p.owner, &a.person)); |
| 185 | if !can_read { |
| 186 | return Err(err(404, format!("no project {owner}/{name}"))); |
| 187 | } |
| 188 | } |
| 189 | if m == "GET" && rest.is_empty() { |
| 190 | let p = store.project(owner, name).unwrap(); |
| 191 | return ok(wire::project_json(store.state(), p)); |
| 192 | } |
| 193 | let a = actor.ok_or_else(|| err(401, "sign in: send a token"))?; |
| 194 | let b = body(req)?; |
| 195 | match (m, rest) { |
| 196 | ("POST", ["points"]) => { |
| 197 | let snapshot = parse_id(field(&b, "snapshot")?)?; |
| 198 | let message = field(&b, "message")?.to_string(); |
| 199 | let n = store.record_point(&a, owner, name, &snapshot, &message, None).map_err(from_store)?; |
| 200 | ok(obj([("n", n.into())])) |
| 201 | } |
| 202 | ("POST", ["move"]) => { |
| 203 | let to = field(&b, "to")?.to_string(); |
| 204 | store.move_project(&a, owner, name, &to).map_err(from_store)?; |
| 205 | ok(obj([("owner", to.into())])) |
| 206 | } |
| 207 | ("POST", ["changes"]) => { |
| 208 | let intent = field(&b, "intent")?.to_string(); |
| 209 | let n = store.open_change(&a, owner, name, &intent).map_err(from_store)?; |
| 210 | ok(obj([("number", n.into())])) |
| 211 | } |
| 212 | ("POST", ["changes", c, sub]) => { |
| 213 | let c: u64 = c.parse().map_err(|_| err(400, "bad change number"))?; |
| 214 | match *sub { |
| 215 | "revisions" => { |
| 216 | let base = num(&b, "base")?; |
| 217 | let steps = b |
| 218 | .get("steps") |
| 219 | .and_then(Value::as_array) |
| 220 | .ok_or_else(|| err(400, "missing steps"))? |
| 221 | .iter() |
| 222 | .map(|st| Ok(Step { id: field(st, "id")?.to_string(), snapshot: parse_id(field(st, "snapshot")?)?, message: field(st, "message")?.to_string() })) |
| 223 | .collect::<Result<Vec<_>, Response>>()?; |
| 224 | let note = b.get("note").and_then(Value::as_str); |
| 225 | let n = store.revise(&a, owner, name, c, base, &steps, note).map_err(from_store)?; |
| 226 | ok(obj([("number", (n as u64).into())])) |
| 227 | } |
| 228 | "state" => { |
| 229 | let to = ChangeState::parse(field(&b, "state")?).ok_or_else(|| err(400, "bad state"))?; |
| 230 | store.set_state(&a, owner, name, c, to).map_err(from_store)?; |
| 231 | ok(obj([("state", to.name().into())])) |
| 232 | } |
| 233 | "land" => { |
| 234 | let (first, last) = store.land(&a, owner, name, c).map_err(from_store)?; |
| 235 | ok(obj([("first", first.into()), ("last", last.into())])) |
| 236 | } |
| 237 | "messages" => { |
| 238 | store.say(&a, owner, name, c, field(&b, "body")?).map_err(from_store)?; |
| 239 | ok(obj([("ok", true.into())])) |
| 240 | } |
| 241 | "evidence" => { |
| 242 | let revision = num(&b, "revision")? as u32; |
| 243 | let content = match (b.get("blob").and_then(Value::as_str), b.get("url").and_then(Value::as_str)) { |
| 244 | (Some(blob), None) => Content::Blob(parse_id(blob)?), |
| 245 | (None, Some(url)) => Content::Url(url), |
| 246 | _ => return Err(err(400, "evidence is a blob or a url")), |
| 247 | }; |
| 248 | store.attach(&a, owner, name, c, revision, field(&b, "title")?, field(&b, "type")?, content).map_err(from_store)?; |
| 249 | ok(obj([("ok", true.into())])) |
| 250 | } |
| 251 | _ => Err(err(404, "no such endpoint")), |
| 252 | } |
| 253 | } |
| 254 | _ => Err(err(404, "no such endpoint")), |
| 255 | } |
| 256 | } |
| 257 | } |