git-ents.gitmain
⌘K
foforge
members.rs176 lines · 5.5 KB · rusthistorycomment on this file
1//! `git ents members`: enroll, remove, revoke, unrevoke, and check members
2//! (`model.member-identity`, `model.member-revocation`).
3
4use ents_model::{Member, MemberId, MemberState, Provenance, namespace};
5use ents_receive::{Identity, propose_delete, propose_entity};
6use gix_object::Find;
7use gix_ref_store::RefStoreRead;
8
9use super::{actor, signer};
10use crate::error::{Error, Result};
11use crate::mutate::outcome_to_result;
12use crate::root::LocalRoot;
13
14/// `git ents members list`: every member ref and its current state.
15///
16/// Takes the ref and object seams rather than a concrete root so the same
17/// read path serves both composition roots — the local CLI and the hosted
18/// web mount both need "who are the members," never a root-specific rescan.
19///
20/// # Errors
21///
22/// Propagates a ref-store or object read failure.
23pub fn list(refs: &impl RefStoreRead, objects: &impl Find) -> Result<Vec<(String, Member)>> {
24 let mut out = Vec::new();
25 for entry in refs.iter_prefix("refs/meta/member/")? {
26 let (name, tip) = entry?;
27 let path = name.as_bstr().to_string();
28 let Some(username) = path.strip_prefix("refs/meta/member/") else {
29 continue;
30 };
31 if let Some(member) = read_member(objects, tip)? {
32 out.push((username.to_owned(), member));
33 }
34 }
35 Ok(out)
36}
37
38/// `git ents members add`: enroll `username` with `pubkey` (or the
39/// signer's own public key), admin-registered.
40///
41/// # Errors
42///
43/// Propagates a signing, serialization, or `receive` failure; see
44/// [`crate::mutate::outcome_to_result`] for how a reached refusal renders.
45pub fn add(
46 root: &LocalRoot,
47 username: &str,
48 pubkey: Option<String>,
49 key: Option<std::path::PathBuf>,
50) -> Result<()> {
51 let signer = signer(root, key)?;
52 let pubkey = pubkey.unwrap_or_else(|| signer.public_openssh());
53 let member = Member::new(MemberId::new(username), pubkey, Provenance::AdminRegistered);
54 let name = namespace::member_ref(&MemberId::new(username))?;
55 let identity = Identity {
56 actor: actor(&signer),
57 author: None,
58 sign: &|payload| signer.sign(payload),
59 };
60 let outcome = propose_entity(
61 &root.refs,
62 &root.objects,
63 &root.events,
64 name,
65 &member,
66 &identity,
67 &format!("Enroll {username}"),
68 root.mode(),
69 )?;
70 outcome_to_result(outcome, None)?;
71 Ok(())
72}
73
74/// `git ents members remove`: delete `username`'s ref entirely.
75///
76/// # Errors
77///
78/// See [`add`].
79pub fn remove(root: &LocalRoot, username: &str, key: Option<std::path::PathBuf>) -> Result<()> {
80 let signer = signer(root, key)?;
81 let name = namespace::member_ref(&MemberId::new(username))?;
82 let outcome = propose_delete(&root.refs, &root.objects, &root.events, name, root.mode())?;
83 let _ = signer; // signing material is not needed for a deletion transition.
84 outcome_to_result(outcome, None)?;
85 Ok(())
86}
87
88/// `git ents members revoke`/`unrevoke`: flip `username`'s
89/// [`MemberState`] without deleting the record (`model.member-revocation`).
90///
91/// # Errors
92///
93/// [`Error::NotFound`] if `username` has no member ref; otherwise see
94/// [`add`].
95pub fn set_revoked(
96 root: &LocalRoot,
97 username: &str,
98 revoked: bool,
99 key: Option<std::path::PathBuf>,
100) -> Result<()> {
101 let signer = signer(root, key)?;
102 let name = namespace::member_ref(&MemberId::new(username))?;
103 let Some(tip) = root.refs.get(name.as_ref())? else {
104 return Err(Error::NotFound {
105 what: format!("member {username}"),
106 });
107 };
108 let mut member = read_member(&root.objects, tip)?.ok_or_else(|| Error::NotFound {
109 what: format!("member {username}"),
110 })?;
111 member.state = if revoked {
112 MemberState::Revoked
113 } else {
114 MemberState::Active
115 };
116 let identity = Identity {
117 actor: actor(&signer),
118 author: None,
119 sign: &|payload| signer.sign(payload),
120 };
121 let verb = if revoked { "Revoke" } else { "Unrevoke" };
122 let outcome = propose_entity(
123 &root.refs,
124 &root.objects,
125 &root.events,
126 name,
127 &member,
128 &identity,
129 &format!("{verb} {username}"),
130 root.mode(),
131 )?;
132 outcome_to_result(outcome, Some(tip))?;
133 Ok(())
134}
135
136/// `git ents members check`: whether `key` (or the resolved signing key)
137/// names an active member, and which username.
138///
139/// # Errors
140///
141/// Propagates a signing-key or ref-store read failure.
142pub fn check(
143 root: &LocalRoot,
144 key: Option<std::path::PathBuf>,
145) -> Result<Option<(String, MemberState)>> {
146 let signer = signer(root, key)?;
147 find_by_key(&root.refs, &root.objects, &signer.public_openssh())
148}
149
150/// Resolve `pubkey` to the enrolled member whose stored key matches it, if
151/// any -- the shared match loop behind [`check`], `git ents serve`'s own
152/// identity-chip label (`crate::commands::serve::build_state`,
153/// `roots.web-signing`), and the hosted mount's server-key enrollment
154/// check: all need "which member owns this key," never a bespoke re-scan
155/// of `list`'s own rows.
156///
157/// # Errors
158///
159/// Propagates a ref-store or object read failure.
160pub fn find_by_key(
161 refs: &impl RefStoreRead,
162 objects: &impl Find,
163 pubkey: &str,
164) -> Result<Option<(String, MemberState)>> {
165 for (username, member) in list(refs, objects)? {
166 if member.key == pubkey {
167 return Ok(Some((username, member.state)));
168 }
169 }
170 Ok(None)
171}
172
173fn read_member(objects: &impl Find, tip: gix_hash::ObjectId) -> Result<Option<Member>> {
174 let tree = crate::commands::commit_tree(objects, tip)?;
175 Ok(facet_git_tree::deserialize::<Member>(&tree, objects).ok())
176}