zuka
zuka/src/core/mod.rs

worklyn / zukapublic

Agent-first git hosting. One Rust binary: git over HTTP and SSH, a REST API, MCP, CI, and multi-tenant isolation.

Get a copy: git clone https://zuka.worklyn.com/worklyn/zuka.git
zuka/src/core/mod.rs
RSmod.rs3.7 KBDownload
1// Domain operations shared by every facade.
2//
3// REST, MCP and SSH translate wire formats; the decisions live here and in `git/`.
4// Anything a facade would otherwise implement twice belongs in this module.
5
6pub mod repo;
7
8use crate::account::Identity;
9use crate::error::{Error, Result};
10use crate::git::validate::Name;
11use crate::http::AppState;
12use crate::store::RepoRecord;
13use std::path::PathBuf;
14
15/// What a caller intends to do with a repository.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum Access {
18 Read,
19 Write,
20 Admin,
21}
22
23impl AppState {
24 /// Resolve a repository for a caller: check access, confirm it is ready, and
25 /// return its record and path.
26 ///
27 /// This sequence appeared seven times before it lived here — in both facades,
28 /// the git transport, the SSH session and three separate handlers — with three
29 /// of them quietly differing. `Write` additionally enforces the disk reserve and
30 /// the storage quota, so no write path can forget them.
31 pub fn open_repo(
32 &self,
33 identity: &Identity,
34 account: &Name,
35 repo: &Name,
36 access: Access,
37 ) -> Result<(RepoRecord, PathBuf)> {
38 match access {
39 Access::Read => identity.require_read(account, repo)?,
40 Access::Write => identity.require_write(account, repo)?,
41 Access::Admin => identity.require_repo_admin(account, repo)?,
42 }
43
44 let record = self
45 .meta
46 .get_ready_repo(account, repo)?
47 .ok_or(Error::NotFound("repository"))?;
48
49 if access == Access::Write {
50 self.ensure_space()?;
51 self.check_write_quota(account, repo)?;
52 }
53
54 Ok((record, self.git.repo_path(account, repo)?))
55 }
56
57 /// Resolve a repository for a caller who presented no credential.
58 ///
59 /// The single place anonymous access is granted. Every other path in the service
60 /// requires an `Identity`, so if this function is wrong, exactly one thing is
61 /// wrong — and if a handler forgets to call it, the failure is a 401, not a leak.
62 ///
63 /// Read only, and deliberately not expressed as a variant of `open_repo`: giving
64 /// `Access::Write` an anonymous caller must not be something a handler can ask
65 /// for by passing the wrong argument. There is no anonymous write to refuse
66 /// because there is no way to spell it.
67 ///
68 /// A private repository is reported absent rather than forbidden, matching
69 /// `Identity::check` — otherwise the status code tells a stranger which private
70 /// names exist.
71 pub fn open_public_repo(&self, account: &Name, repo: &Name) -> Result<(RepoRecord, PathBuf)> {
72 let record = self
73 .meta
74 .get_ready_repo(account, repo)?
75 .ok_or(Error::NotFound("repository"))?;
76
77 if !record.visibility.is_public() {
78 return Err(Error::NotFound("repository"));
79 }
80
81 Ok((record, self.git.repo_path(account, repo)?))
82 }
83
84 /// Resolve a repository for a caller who may or may not have authenticated.
85 ///
86 /// Read paths that are reachable by both — the browser views and `git clone` —
87 /// go through here so the fallback order is stated once: a presented credential
88 /// is always honoured, and absence of one falls back to the public rule. Doing
89 /// it the other way round would let a public repository mask a token's
90 /// confinement.
91 pub fn open_repo_for_reading(
92 &self,
93 identity: Option<&Identity>,
94 account: &Name,
95 repo: &Name,
96 ) -> Result<(RepoRecord, PathBuf)> {
97 match identity {
98 Some(identity) => self.open_repo(identity, account, repo, Access::Read),
99 None => self.open_public_repo(account, repo),
100 }
101 }
102}