Skip to main content

Control

Struct Control 

Source
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

Source

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.

Source

pub fn with_encryption_keys(self, keys: EncryptionKeys) -> Self

The store of people’s encryption keys this door reads and writes.

Source

pub fn encryption_keys(&self) -> Option<&EncryptionKeys>

People’s encryption keys, when this door holds them.

Source

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.

Source

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).

Source

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.

Source

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.

Source

pub fn with_browse_flag(self, browse: Arc<AtomicBool>) -> Self

Share the browse flag in — ServerStores::browse(), the same Arc the replication slot flips.

Source

pub fn with_anonymous_policy(self, policy: AnonymousPolicy) -> Self

Set the anonymous policy. Refuse is overridden by the browse flag: see Control::refuses_anonymous.

Source

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.

Source

pub fn is_browse(&self) -> bool

Is this box a browse replica right now?

Source

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.

Source

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.

Source

pub fn browse_host(&self) -> Option<&str>

The browse host the refusal names, when the operator gave one.

Source

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.

Source

pub fn plan_gates(&self) -> &[Arc<dyn PlanGate>]

The chain, in the order it is asked. Empty when nothing was wired.

Source

pub fn open(store: Arc<dyn ControlStore>) -> Result<Self, Error>

Open both over the server’s control store (the certificate-identity plan).

Source

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.

Source

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.

Source

pub fn auth(&self) -> &AuthTable

The identity table.

Source

pub fn catalog(&self) -> &RepoCatalog

The repository catalog.

Source

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.

Source

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.

Source

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.

Source

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.CreateNamespace needs: AuthTable::may_server at Level::Admin — creating a tenant is a server-wide act;
  • the repository needs what Repos.Create needs: AuthTable::may_namespace at Level::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.

Source

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.

Source

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.

Trait Implementations§

Source§

impl Clone for Control

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Control

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

By hand rather than derived, because the gates are dyn trait objects with no Debug of their own — and what an operator wants to see of them is which are wired and in what order, not their insides.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<'a, T, E> AsTaggedExplicit<'a, E> for T
where T: 'a,

Source§

fn explicit(self, class: Class, tag: u32) -> TaggedParser<'a, Explicit, Self, E>

Source§

impl<'a, T, E> AsTaggedImplicit<'a, E> for T
where T: 'a,

Source§

fn implicit( self, class: Class, constructed: bool, tag: u32, ) -> TaggedParser<'a, Implicit, Self, E>

Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more