crates/kernel/ents-gate/src/policy.rs
policy.rshistorycomment on this file
| 1 | //! Policy loading: the member set, read from `refs/meta/member/*` |
| 2 | //! through the read half of the ref store (`gate.policy-as-state`). |
| 3 | //! |
| 4 | //! The gate consults no state outside `refs/meta/*`: the member set, |
| 5 | //! each member's current state, and the epoch (`crate::config`) are all |
| 6 | //! repository state, so any frontend with a clone evaluates the actual |
| 7 | //! policy offline, staleness bounded only by the age of its last fetch. |
| 8 | |
| 9 | use ents_model::{Member, MemberId}; |
| 10 | use gix_hash::ObjectId; |
| 11 | use gix_object::Find; |
| 12 | use gix_ref_store::RefStoreRead; |
| 13 | |
| 14 | use crate::error::{Error, Result}; |
| 15 | use crate::object::expect_commit; |
| 16 | |
| 17 | /// One enrolled member: its id (from the refname) and its ref's tip. |
| 18 | #[derive(Debug, Clone)] |
| 19 | pub(crate) struct Enrolled { |
| 20 | /// The member id, i.e. the `<id>` of `refs/meta/member/<id>`. |
| 21 | pub id: MemberId, |
| 22 | /// The member ref's current tip commit. |
| 23 | pub tip: ObjectId, |
| 24 | } |
| 25 | |
| 26 | /// Every ref under `refs/meta/member/`, in store order. |
| 27 | // @relation(gate.policy-as-state, scope=function) |
| 28 | pub(crate) fn members(refs: &dyn RefStoreRead) -> Result<Vec<Enrolled>> { |
| 29 | let mut out = Vec::new(); |
| 30 | for entry in refs.iter_prefix("refs/meta/member/")? { |
| 31 | let (name, tip) = entry?; |
| 32 | let path = name.as_bstr().to_string(); |
| 33 | let id = path |
| 34 | .strip_prefix("refs/meta/member/") |
| 35 | .unwrap_or(&path) |
| 36 | .to_owned(); |
| 37 | out.push(Enrolled { |
| 38 | id: MemberId::new(id), |
| 39 | tip, |
| 40 | }); |
| 41 | } |
| 42 | Ok(out) |
| 43 | } |
| 44 | |
| 45 | /// The member entity currently in force: the typed tree behind `tip`, |
| 46 | /// the member ref's tip as read in the same verification snapshot. |
| 47 | /// |
| 48 | /// Admission consults only this current entity |
| 49 | /// (`model.member-revocation`): a revoked key's new pushes are refused |
| 50 | /// from the moment the revocation lands, regardless of any committer |
| 51 | /// timestamp the pushed commit claims — a backdated commit changes |
| 52 | /// nothing, because no commit-supplied time participates in the |
| 53 | /// judgment. Refs accepted before a revocation stay valid because |
| 54 | /// acceptance is never re-judged; reconstructing what a past acceptance |
| 55 | /// saw is an audit function over the deployment's out-of-scope op log, |
| 56 | /// not a gate path. |
| 57 | // @relation(model.member-revocation, gate.policy-as-state, scope=function) |
| 58 | pub(crate) fn member_current(objects: &dyn Find, tip: ObjectId) -> Result<Member> { |
| 59 | let commit = expect_commit(objects, tip)?; |
| 60 | facet_git_tree::deserialize(&commit.tree, objects) |
| 61 | .map_err(|source| Error::Entity { oid: tip, source }) |
| 62 | } |