gunnar-grpc 2.0.0

The tonic service implementations and the event bus behind gunnar's gRPC control plane.
Documentation
//! **The owner's enterprise list, at the seam** (2026-10-05): the account
//! admin's audit log, the operator's aggregates and the look the account admin
//! grants, the name filter. The types a handler hands the plane; the plane
//! (`gunnar-server`'s `CoreControlPlane`, over `gunnar-core`) decides and
//! stores. `crate::service::enterprise` is the service.

/// **One event for an account's audit log**, as a handler hands it over. The
/// plane resolves what the actor is TO the account (admin, member, operator,
/// machine, outsider) itself: a handler that said it would be a second source
/// of truth about membership.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AccountAuditEvent {
    /// `family.verb`: `login`, `repo.created`, `visibility.changed` …
    pub kind: String,
    /// The principal that did it; empty for the system.
    pub actor: String,
    /// Whether the actor authenticated as a machine.
    pub actor_machine: bool,
    /// What it was done to.
    pub target: String,
    /// One sentence more, or empty.
    pub detail: String,
    /// Asked for and refused.
    pub refused: bool,
}

impl AccountAuditEvent {
    /// An event `kind` on `target`, by `caller`, done.
    pub fn by(caller: &crate::auth::Caller, kind: &str, target: impl Into<String>) -> Self {
        AccountAuditEvent {
            kind: kind.to_owned(),
            actor: caller.principal_id.clone(),
            actor_machine: caller.machine,
            target: target.into(),
            detail: String::new(),
            refused: false,
        }
    }

    /// With one sentence more.
    pub fn detail(mut self, detail: impl Into<String>) -> Self {
        self.detail = detail.into();
        self
    }

    /// Refused rather than done.
    pub fn refused(mut self) -> Self {
        self.refused = true;
        self
    }
}

/// **What an account admin asks of the log.**
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AccountAuditQuery {
    pub kinds: Vec<String>,
    pub actor: String,
    pub since_unix_ms: i64,
    pub until_unix_ms: i64,
    pub text: String,
    /// Only entries older than this seq.
    pub before_seq: Option<u64>,
    /// 0 = 50.
    pub limit: u32,
}

impl AccountAuditQuery {
    /// From the wire's filter.
    pub fn from_filter(filter: Option<&gunnar_proto::AuditFilter>, before_seq: Option<u64>, limit: u32) -> Self {
        let filter = filter.cloned().unwrap_or_default();
        AccountAuditQuery {
            kinds: filter.kinds.into_iter().map(|k| k.trim().to_owned()).filter(|k| !k.is_empty()).collect(),
            actor: filter.actor.trim().to_owned(),
            since_unix_ms: filter.since_unix_ms,
            until_unix_ms: filter.until_unix_ms,
            text: filter.text.trim().to_owned(),
            before_seq,
            limit,
        }
    }
}

/// **What the name filter says**, as the plane answers it.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct NameVerdict {
    /// `clean`, `reserved`, `blocked`, `borderline`.
    pub verdict: String,
    /// The sentence a refused person reads; empty when clean.
    pub sentence: String,
}

impl NameVerdict {
    /// Nothing listed in it.
    pub fn clean() -> Self {
        NameVerdict {
            verdict: "clean".to_owned(),
            sentence: String::new(),
        }
    }

    /// The filter has nothing to say.
    pub fn is_clean(&self) -> bool {
        self.verdict == "clean"
    }

    /// Held for the account admin.
    pub fn is_borderline(&self) -> bool {
        self.verdict == "borderline"
    }

    /// Reserved for gunnar's machines, brands and staff.
    pub fn is_reserved(&self) -> bool {
        self.verdict == "reserved"
    }
}

/// **The account admin's decision on an operator's look**, verified by the
/// service (the approver is an admin of the account; for an approval, a fresh
/// step-up of their own passkey) and committed by the plane in one update.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct OperatorAccessDecision {
    pub request_id: String,
    pub approver: String,
    pub approve: bool,
    /// The admin's length, minutes; `None` = what the operator asked for.
    pub minutes: Option<u32>,
    /// The step-up's passkey and the counter it presented, stored back in
    /// the same update.
    pub step_up: Option<(Vec<u8>, u32)>,
}

use crate::seam::ControlResult;

/// **The enterprise half of the storage seam.** A plane with gunnar's stores
/// behind it answers [`crate::seam::ControlPlane::enterprise`] with one of
/// these; a bare plane answers `None`.
pub trait EnterprisePlane: Send + Sync {
    /// The account `namespace` belongs to: a person's, read off its first
    /// component. `None` for a namespace no person owns.
    fn account_of_namespace(&self, namespace: &str) -> ControlResult<Option<String>>;

    /// The account `principal_id` belongs to: its employer, or its own.
    fn account_of_principal(&self, principal_id: &str) -> ControlResult<Option<String>>;

    /// **Is `principal_id` an admin of `account`?** Its own person, or one
    /// its admin made Admin over the namespace — never a server-scope grant.
    fn is_account_admin(&self, principal_id: &str, account: &str) -> ControlResult<bool>;

    /// **Is `namespace` closed to `principal_id`'s server-scope grants?** A
    /// customer's account the principal is not inside and holds no live look
    /// into. What a server-wide listing asks per row.
    fn shielded(&self, principal_id: &str, namespace: &str) -> ControlResult<bool>;

    /// **Append one event to `account`'s audit log**; the plane says what
    /// the actor is to the account.
    fn account_audit_record(&self, account: &str, event: &AccountAuditEvent) -> ControlResult<()>;

    /// One page of `account`'s log, its chain's state with it.
    fn account_audit_log(&self, account: &str, query: &AccountAuditQuery) -> ControlResult<gunnar_proto::AuditLogResponse>;

    /// Every entry the query matches, as a file.
    fn account_audit_export(
        &self,
        account: &str,
        query: &AccountAuditQuery,
        format: gunnar_proto::AuditExportFormat,
    ) -> ControlResult<gunnar_proto::ExportAuditLogResponse>;

    /// **The operator's totals**: no name of anything inside an account.
    fn aggregates(&self) -> ControlResult<gunnar_proto::AggregatesResponse>;

    /// The operator asks to look inside `account`.
    fn operator_access_request(
        &self,
        account: &str,
        operator: &str,
        reason: &str,
        minutes: u32,
    ) -> ControlResult<gunnar_proto::OperatorAccess>;

    /// The looks into `account` (`None`: every account), newest first.
    fn operator_access_list(&self, account: Option<&str>) -> ControlResult<Vec<gunnar_proto::OperatorAccess>>;

    /// One look, by its request id.
    fn operator_access_get(&self, request_id: &str) -> ControlResult<Option<gunnar_proto::OperatorAccess>>;

    /// The account admin's decision, committed with the step-up's counter.
    fn operator_access_decide(&self, decision: &OperatorAccessDecision) -> ControlResult<gunnar_proto::OperatorAccess>;

    /// End a look now.
    fn operator_access_revoke(&self, request_id: &str, by: &str) -> ControlResult<gunnar_proto::OperatorAccess>;

    /// **What the name filter says** about `name` as a `kind` (`account`,
    /// `member`, `namespace`, `repo`) in `account` (empty: none).
    fn name_check(&self, kind: &str, name: &str, account: &str) -> ControlResult<NameVerdict>;

    /// Hold a borderline name for `account`'s admin (`hold`), or note a
    /// refused one for them (`!hold`).
    fn name_review_add(
        &self,
        kind: &str,
        name: &str,
        account: &str,
        actor: &str,
        hold: bool,
    ) -> ControlResult<gunnar_proto::NameReview>;

    /// The lists in force.
    fn name_lists(&self) -> ControlResult<gunnar_proto::NameLists>;

    /// Replace the lists.
    fn set_name_lists(&self, lists: gunnar_proto::NameLists) -> ControlResult<gunnar_proto::NameLists>;

    /// `account`'s queue, newest first.
    fn name_reviews(&self, account: &str) -> ControlResult<Vec<gunnar_proto::NameReview>>;

    /// The account admin decides a held name.
    fn name_review_decide(&self, account: &str, id: &str, allow: bool, by: &str) -> ControlResult<gunnar_proto::NameReview>;
}

/// The refusal every `Enterprise` verb answers on a plane with no stores.
pub fn no_plane() -> crate::seam::ControlError {
    crate::seam::ControlError::Unimplemented("the enterprise list needs gunnar's stores (this plane has none)")
}