crates/gunnar-grpc/src/enterprise.rs
//! **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)",
)
}