pub struct Control { /* private fields */ }Expand description
The identity table and the repository catalog, together.
Cheap to clone: both halves are handles onto the same stored documents.
Implementations§
Source§impl Control
impl Control
Sourcepub fn new(auth: AuthTable, catalog: RepoCatalog) -> Self
pub fn new(auth: AuthTable, catalog: RepoCatalog) -> Self
Wrap an already-open table and catalog. Writable (a primary) until
Control::with_write_gate shares a standby’s freeze flag in.
Sourcepub fn with_encryption_keys(self, keys: EncryptionKeys) -> Self
pub fn with_encryption_keys(self, keys: EncryptionKeys) -> Self
The store of people’s encryption keys this door reads and writes.
Sourcepub fn encryption_keys(&self) -> Option<&EncryptionKeys>
pub fn encryption_keys(&self) -> Option<&EncryptionKeys>
People’s encryption keys, when this door holds them.
Sourcepub fn with_enterprise(
self,
names: NameFilterStore,
audit: Arc<AccountAuditLog>,
) -> Self
pub fn with_enterprise( self, names: NameFilterStore, audit: Arc<AccountAuditLog>, ) -> Self
The name filter and the accounts’ audit logs (2026-10-05): a repository a first push creates is named here too, and its creation is in its account’s log.
Sourcepub fn encryption_key_event(
&self,
principal: &Principal,
kind: &str,
detail: &str,
)
pub fn encryption_key_event( &self, principal: &Principal, kind: &str, detail: &str, )
An event about a person’s own encryption key (RA4-3): written into
the account the person belongs to, the same kinds the gRPC door writes
(encryption-key.created, .method-added, .method-removed).
Sourcepub fn push_create_name_refusal(
&self,
principal: &Principal,
path: &RepoRef,
) -> Option<String>
pub fn push_create_name_refusal( &self, principal: &Principal, path: &RepoRef, ) -> Option<String>
The name filter at the push door: the sentence a first push to
path is refused with, or None. A blocked name is noted for the
account admin; a borderline one is held for them (and refused until
they allow it). Reserved names are a person’s and an account’s, never
a repository’s.
Sourcepub fn with_write_gate(self, writes_frozen: Arc<AtomicBool>) -> Self
pub fn with_write_gate(self, writes_frozen: Arc<AtomicBool>) -> Self
Share the standby write-gate in. The Arc the server’s replication
slot writes on a role change; reading it is how the front door learns,
per push, that this box has become a read-only standby.
Sourcepub fn with_browse_flag(self, browse: Arc<AtomicBool>) -> Self
pub fn with_browse_flag(self, browse: Arc<AtomicBool>) -> Self
Share the browse flag in — ServerStores::browse(), the same Arc
the replication slot flips.
Sourcepub fn with_anonymous_policy(self, policy: AnonymousPolicy) -> Self
pub fn with_anonymous_policy(self, policy: AnonymousPolicy) -> Self
Set the anonymous policy. Refuse is overridden by the browse flag:
see Control::refuses_anonymous.
Sourcepub fn with_twin_flag(self, twin: Arc<AtomicBool>) -> Self
pub fn with_twin_flag(self, twin: Arc<AtomicBool>) -> Self
Share the twin flag in — ServerStores::twin_flag(), the same Arc
Bootstrap.BecomeTwin flips while the server runs.
Sourcepub fn is_twin(&self) -> bool
pub fn is_twin(&self) -> bool
Is this box a twin right now? A live read: the answer changes under
a running server the moment Bootstrap.BecomeTwin returns.
Sourcepub fn refuses_anonymous(&self) -> bool
pub fn refuses_anonymous(&self) -> bool
Is an anonymous request refused here? Refuse on a box that is
not a browse replica. A browse replica serves anonymous reads whatever
the knob says — it is the box the knob points AT, and a deployment that
flipped a primary into a browse box by SetRole must not carry the
primary’s refusal along.
Sourcepub fn browse_host(&self) -> Option<&str>
pub fn browse_host(&self) -> Option<&str>
The browse host the refusal names, when the operator gave one.
Sourcepub fn with_plan_gate(self, gate: Arc<dyn PlanGate>) -> Self
pub fn with_plan_gate(self, gate: Arc<dyn PlanGate>) -> Self
Append a gate to the chain. Order is the call order: the server
hands the disk-headroom gate in first, then (metered) the entitlement
gate, so a full box is told it is full before it is told about its
plan. From here on every push asks each link in
Dispatch::authorise_service, and the first Refuse is the answer.
Sourcepub fn plan_gates(&self) -> &[Arc<dyn PlanGate>]
pub fn plan_gates(&self) -> &[Arc<dyn PlanGate>]
The chain, in the order it is asked. Empty when nothing was wired.
Sourcepub fn open(store: Arc<dyn ControlStore>) -> Result<Self, Error>
pub fn open(store: Arc<dyn ControlStore>) -> Result<Self, Error>
Open both over the server’s control store (the certificate-identity plan).
Sourcepub fn writes_frozen(&self) -> bool
pub fn writes_frozen(&self) -> bool
Would a client write be refused right now? true on a standby.
The replication push from a primary is a PrincipalKind::Machine
identity and is NOT a client write — a standby exists to receive it —
so the freeze is scoped to non-Machine principals by
Control::write_frozen_for, which is what the dispatch actually calls.
Sourcepub fn write_frozen_for(&self, principal: &Principal) -> bool
pub fn write_frozen_for(&self, principal: &Principal) -> bool
Must this principal’s write be refused because this box is a standby?
true only when the box is frozen AND the writer is not the replication
source. A Machine identity is how a primary authenticates its
replication push, and a standby is a box that exists precisely to accept
that — freezing it would stop the very copy the standby is for. A human
(User) or a Deploy key pushing directly to a standby is the write
that must be refused: it would land here and never reach the primary.
Sourcepub fn catalog(&self) -> &RepoCatalog
pub fn catalog(&self) -> &RepoCatalog
The repository catalog.
Sourcepub fn principal_for_key(
&self,
fingerprint: &Fingerprint,
) -> Result<Option<Principal>, Error>
pub fn principal_for_key( &self, fingerprint: &Fingerprint, ) -> Result<Option<Principal>, Error>
Who is this? The principal that owns fingerprint, or None to
refuse the authentication.
Err is a storage fault and is not the same as Ok(None). The caller
must refuse the key either way, but it must say which happened, because
“nobody holds this key” and “the identity table could not be read” call
for entirely different operator action.
Sourcepub fn resolve(&self, path: &RepoRef) -> Result<Option<Repo>, Error>
pub fn resolve(&self, path: &RepoRef) -> Result<Option<Repo>, Error>
Does it exist? The repository at path, or None.
None here is only ever “no such repository”. It must not be conflated
with “no permission” at this layer: the server merges the two into one
client-visible message on purpose, so a client cannot use the difference
to enumerate private repositories, but they arrive separately so the
server log can tell them apart.
Sourcepub fn access_for(
&self,
principal: &Principal,
repo: &Repo,
) -> Result<Option<Level>, Error>
pub fn access_for( &self, principal: &Principal, repo: &Repo, ) -> Result<Option<Level>, Error>
May they? The highest level principal holds on repo, or None
for no access at all.
Derived by asking AuthTable::may from the top down, so this is the
ACL’s answer and not a second reading of the grant table. A namespace
grant matches its namespace exactly: team does not reach
team/sub. That is what closes the escalation the permissive path
sandbox would otherwise carry, and it is pinned by
gunnar-core’s tests/acl.rs.
Sourcepub fn push_create_spec(
&self,
principal: &Principal,
path: &RepoRef,
) -> Result<Option<NewRepo>, Error>
pub fn push_create_spec( &self, principal: &Principal, path: &RepoRef, ) -> Result<Option<NewRepo>, Error>
The first-repository door, the checks: may principal create
path by pushing to it? Some(spec) is what would be created —
nothing has been written; None the dispatch MUST render as its
ordinary not-found, so the door is invisible to everyone it does not
admit.
§Why this exists (the appliance brick, 2026-08-25)
On an installed appliance the bootstrap admin from --admin-key
authenticates and can then do nothing: the exec surface is three git
verbs, all of which want a repository; the catalog is empty; gRPC —
the only surface with Repos.Create — is off by design;
and there is no shell by construction. Nobody could ever create the
first namespace. a8ba0665 fixed “authorizes nobody”; this fixes
“can do nothing”.
§It asks gRPC’s questions, verbatim (one implementation)
This is deliberately NOT a new permission vocabulary. The two predicates are exactly the ones the gRPC control plane gates creation on, so SSH and gRPC can never disagree about who may create:
- a namespace that does not exist needs what
Repos.CreateNamespaceneeds:AuthTable::may_serveratLevel::Admin— creating a tenant is a server-wide act; - the repository needs what
Repos.Createneeds:AuthTable::may_namespaceatLevel::Admin— namespace-scoped, because the repository does not exist yet, so there is nothing narrower to hold a grant on.
So the door admits exactly who the RPC would admit: the gunnar-admin
anywhere, an account owner inside their own (existing) account
namespace, a namespace-admin inside that namespace exactly (no
inheritance into team/sub). The anonymous principal is refused by
construction inside both predicates.
§Why the catalog’s “never conjure a namespace” rule bends here
RepoCatalog::create refuses a repository whose namespace does not
exist, “because then a typo in a push URL would silently become a
tenant”. That rule is about unprivileged writers. Here the namespace
is only ever conjured by a principal holding Server/Admin — whose
grant already covers every namespace on the box, so no tenant boundary
is crossed; the blast radius of the admin’s typo is a private, empty
repository on the admin’s own server.
§What it would create
NewRepo::new and nothing else: sha256, private, default
branch. The one writer of defaults — a door that chose its own
defaults would be the second answer one implementation forbids.
§Why the write is NOT here (the ghost repository, 2026-08-30)
This used to create the catalog row at exec time, before the client
had seen the advertisement. A sha1 working tree pushing to the sha256
default reads object-format=sha256 in that advertisement, prints
“the receiving end does not support this repository’s hash
algorithm” and hangs up having sent no command — and the empty
repository it never pushed to was there afterwards: repo get
succeeded, ls-remote answered (the site test, two FAIL rows).
The server cannot know the client’s hash before advertising, so the
only order that leaves nothing behind on a refusal is: advertise the
prospect, read the commands, check the hash, then write. The
write is Self::admit_push_create, called by the transport once
there is a push to admit; the catalog is untouched until then.
Sourcepub fn admit_push_create(
&self,
principal: &Principal,
path: &RepoRef,
) -> Result<Option<(Repo, bool)>, Error>
pub fn admit_push_create( &self, principal: &Principal, path: &RepoRef, ) -> Result<Option<(Repo, bool)>, Error>
The first-repository door, the write: the namespace, conjured if
it does not exist, and the catalog row — for a push that has sent its
commands and passed the hash check. Re-asks
Self::push_create_spec’s questions, so a grant revoked between the
advertisement and the pack is still a refusal; Ok(None) is that
refusal.
The on-disk stores are NOT created here (this crate has no
filesystem): the transport creates them through ServerStores, and it
is the transport that makes the pair transactional — a store that
cannot be made rolls this row back, see ServerStores::create_repo.
§Races
Two admins pushing the same new name race on the catalog’s
compare-and-swap. The loser of either CAS finds the thing it was
creating already there and proceeds — the repository exists, which is
what it wanted. Err is only ever a storage fault, and the caller
must report it as one (crate::server::DispatchError::Store), never
as not-found.
Returns the repository and whether this call conjured the namespace, so a caller that fails afterwards can take the namespace back with the row and leave no trace at all.
Sourcepub fn retract_push_create(
&self,
path: &RepoRef,
namespace_conjured: bool,
) -> Result<(), Error>
pub fn retract_push_create( &self, path: &RepoRef, namespace_conjured: bool, ) -> Result<(), Error>
Undo Self::admit_push_create after the store could not be made:
the row goes, and the namespace too if that call conjured it. Nothing
is left for repo get to find.