git-ents.gitmain
⌘K
foforge
state.rs154 lines · 6.7 KB · rusthistorycomment on this file
1//! The web frontend's own handle onto the four composition-root seams
2//! (`roots.composition`), generic over only the object store: `refs` and
3//! `events` are already used as trait objects everywhere in this codebase
4//! (`git_ents::root::LocalRoot` passes `&root.refs` where `&dyn RefStore`
5//! is expected; `ents_forge::comment::add` takes `events: &dyn
6//! ents_receive::EventSink` directly), so [`AppState`] holds them boxed
7//! rather than introducing a type parameter this crate has no other use
8//! for. The object store stays a type parameter `O` because every mutation
9//! path (`ents_receive::propose_entity`) takes it as `&(impl
10//! gix_object::Find + gix_object::Write)`, generic, never `dyn` --
11//! matching that established shape rather than inventing a private
12//! object-store trait (`arch.no-object-store-trait`).
13//!
14//! `objects` is held behind a [`std::sync::Mutex`] rather than bare `O`:
15//! axum requires its `State` to be `Sync` (so it can be shared across
16//! however many worker tasks accept connections), but neither this
17//! crate's real composition-root object store nor its test fixture
18//! (`ents_testutil::ObjectStore`, used by every test in this crate) is
19//! `Sync` on its own -- the fixture's internal `RefCell` makes that
20//! concrete, but the same caution applies to any future object-store
21//! implementation this crate is handed, since nothing about
22//! `gix_object::Find`/`Write` requires an implementation to be safe for
23//! concurrent access. Serializing access behind one mutex is the right
24//! choice for this crate regardless: a web admin UI's request volume is
25//! not a throughput target `roots.adoc` names anywhere.
26
27use std::path::PathBuf;
28use std::sync::Mutex;
29
30use ents_receive::{EventSink, Mode};
31use gix_ref_store::RefStore;
32
33use crate::auth::ChallengeStore;
34use crate::identity::SigningIdentity;
35use crate::session::SessionStore;
36
37/// Who may mutate through this deployment's web UI — injected by the
38/// composition root, exactly as the signing identity is
39/// (`roots.web-agnostic`): this crate branches on the injected policy's
40/// value, never on where it is running (`arch.no-hosted-branch`'s
41/// spirit).
42// @relation(roots.web-signin, scope=type)
43pub enum AccessPolicy {
44 /// Every session mutates as the injected identity — the local root
45 /// (`roots.local`), where the operator's own key *is* the identity
46 /// and no sign-in surface exists (`roots.web-signin`).
47 Trusted,
48 /// Anonymous sessions browse read-only; a mutation requires a
49 /// session signed in as an enrolled, active member
50 /// (`roots.web-signin`) — the hosted root.
51 SignInRequired(Realm),
52}
53
54/// What a sign-in-required deployment knows about itself: the canonical
55/// external host bound into every challenge payload
56/// ([`crate::auth::challenge_payload`]), and the outstanding challenges.
57pub struct Realm {
58 /// The host a member addresses this deployment as, e.g.
59 /// `git.ents.cloud` — a signature is bound to it, so one minted for
60 /// this realm verifies nowhere else (`roots.web-signin`).
61 pub host: String,
62 /// Outstanding sign-in challenges, memory-only like the sessions.
63 pub challenges: ChallengeStore,
64}
65
66/// Everything a page handler needs: the four composition-root seams, the
67/// gate policy in force, the repository's working-tree path (comment
68/// anchoring resolves paths against it), and the in-memory session store
69/// (`roots.web-session`).
70///
71/// Built once per `ents_web::serve`/`ents_web::router` call, by whichever
72/// composition root is wiring this crate in -- never constructed inside a
73/// page handler itself (`roots.config-isolation`'s spirit: every seam
74/// arrives already chosen).
75pub struct AppState<O> {
76 /// The ref store, as the same trait-object shape every mutation
77 /// primitive in this codebase already takes it.
78 pub refs: Box<dyn RefStore>,
79 /// The object store, mutex-serialized (see this module's own doc for
80 /// why). A type parameter, not `dyn`, so every existing
81 /// `propose_entity`/`comment::add`/`toolchain::import` call compiles
82 /// unchanged against a lock guard's deref.
83 objects: Mutex<O>,
84 /// The event sink obligations are enqueued to on a push
85 /// (`receive.event-sink`) -- a local deployment injects a null sink
86 /// (`roots.local`), matching `git ents`'s own CLI commands.
87 pub events: Box<dyn EventSink>,
88 /// The gate policy this deployment runs under (`roots.local`:
89 /// advisory; a future hosted `ents-web` wiring: mandatory).
90 pub mode: Mode,
91 /// The signing identity every mutation page signs on behalf of
92 /// (`roots.web-signing`, `roots.web-agnostic`).
93 pub identity: Box<dyn SigningIdentity>,
94 /// The repository's own path, for anchoring operations
95 /// (`ents_forge::comment`) that need to open the working tree
96 /// directly.
97 pub path: PathBuf,
98 /// In-memory web sessions (`roots.web-session`).
99 pub sessions: SessionStore,
100 /// Who may mutate here (`roots.web-signin`): [`AccessPolicy::Trusted`]
101 /// unless the composition root said otherwise via
102 /// [`AppState::with_access`].
103 pub access: AccessPolicy,
104}
105
106impl<O> AppState<O> {
107 /// Build a state from already-wired seams -- the one constructor every
108 /// composition root calls, and the only place a fresh
109 /// [`SessionStore`] is created.
110 pub fn new(
111 refs: Box<dyn RefStore>,
112 objects: O,
113 events: Box<dyn EventSink>,
114 mode: Mode,
115 identity: Box<dyn SigningIdentity>,
116 path: PathBuf,
117 ) -> Self {
118 Self {
119 refs,
120 objects: Mutex::new(objects),
121 events,
122 mode,
123 identity,
124 path,
125 sessions: SessionStore::default(),
126 access: AccessPolicy::Trusted,
127 }
128 }
129
130 /// Replace the default [`AccessPolicy::Trusted`] — the hosted
131 /// composition root's one extra wiring step
132 /// (`roots.single-node-hosted`, `roots.web-signin`). A consuming
133 /// builder rather than a constructor parameter so every existing
134 /// `new` caller (every `Trusted` deployment and test fixture) stays
135 /// untouched.
136 #[must_use]
137 pub fn with_access(mut self, access: AccessPolicy) -> Self {
138 self.access = access;
139 self
140 }
141
142 /// Lock the object store for the duration of one request.
143 ///
144 /// Poisoning recovers rather than propagating (mirrors
145 /// `SessionStore`'s identical reasoning): an earlier request
146 /// panicking mid-write already unwound that request's own response;
147 /// refusing every subsequent request forever would be strictly worse
148 /// than reusing the store as-is.
149 pub fn objects(&self) -> std::sync::MutexGuard<'_, O> {
150 self.objects
151 .lock()
152 .unwrap_or_else(std::sync::PoisonError::into_inner)
153 }
154}