Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

crates/gunnar-server/src/control_plane/enterprise_plane.rs

//! **The owner's enterprise list on the real stores** (2026-10-05):
//! `gunnar_grpc::enterprise::EnterprisePlane` over `gunnar_core`'s account
//! audit logs, its name filter and the identity table's shield and looks.

use gunnar_core::account_audit::{
    to_csv, to_json, AccountEntry, AccountEvent, AuditQuery, Outcome,
};
use gunnar_core::name_filter::{NameKind, NameReview, Verdict};
use gunnar_core::{OperatorAccess, OperatorAccessState};
use gunnar_grpc::enterprise::{
    AccountAuditEvent, AccountAuditQuery, EnterprisePlane, NameVerdict, OperatorAccessDecision,
};
use gunnar_grpc::{ControlError, ControlResult};

use super::{internal, CoreControlPlane};

fn entry_out(e: AccountEntry) -> gunnar_proto::AuditEntry {
    gunnar_proto::AuditEntry {
        seq: e.seq,
        at_unix_ms: e.at_unix_ms,
        kind: e.kind,
        actor: e.actor,
        actor_role: e.actor_role,
        target: e.target,
        detail: e.detail,
        outcome: e.outcome,
        prev: e.prev,
        hash: e.hash,
    }
}

fn query_in(q: &AccountAuditQuery) -> AuditQuery {
    AuditQuery {
        kinds: q.kinds.clone(),
        actor: q.actor.clone(),
        since_unix_ms: q.since_unix_ms,
        until_unix_ms: q.until_unix_ms,
        text: q.text.clone(),
        before_seq: q.before_seq,
        limit: q.limit as usize,
    }
}

fn look_out(row: OperatorAccess) -> gunnar_proto::OperatorAccess {
    let state = match row.state {
        OperatorAccessState::Pending => gunnar_proto::OperatorAccessState::Pending,
        OperatorAccessState::Approved => gunnar_proto::OperatorAccessState::Approved,
        OperatorAccessState::Refused => gunnar_proto::OperatorAccessState::Refused,
        OperatorAccessState::Revoked => gunnar_proto::OperatorAccessState::Revoked,
        OperatorAccessState::Expired => gunnar_proto::OperatorAccessState::Expired,
    };
    gunnar_proto::OperatorAccess {
        request_id: row.id,
        code: row.code,
        account: row.account,
        operator: row.operator,
        reason: row.reason,
        minutes: row.minutes,
        requested_at_unix_ms: row.requested_at_unix_ms,
        request_expires_at_unix_ms: row.request_expires_at_unix_ms,
        state: state as i32,
        granted_until_unix_ms: row.granted_until_unix_ms,
        decided_by: row.decided_by,
        decided_at_unix_ms: row.decided_at_unix_ms,
    }
}

fn review_out(r: NameReview) -> gunnar_proto::NameReview {
    gunnar_proto::NameReview {
        id: r.id,
        account: r.account,
        kind: r.kind,
        name: r.name,
        actor: r.actor,
        verdict: r.verdict,
        state: r.state.as_str().to_owned(),
        at_unix_ms: r.at_unix_ms,
        decided_by: r.decided_by,
        decided_at_unix_ms: r.decided_at_unix_ms,
    }
}

fn kind_in(kind: &str) -> ControlResult<NameKind> {
    NameKind::parse(kind).ok_or_else(|| {
        ControlError::InvalidArgument(format!(
            "{kind:?} is not a kind of name: account, member, namespace or repo"
        ))
    })
}

fn lists_out(l: gunnar_core::name_filter::NameLists) -> gunnar_proto::NameLists {
    gunnar_proto::NameLists {
        blocked: l.blocked,
        borderline: l.borderline,
        reserved: l.reserved,
        reserved_prefixes: l.reserved_prefixes,
        allowed: l.allowed,
    }
}

impl CoreControlPlane {
    /// What `actor` is to `account` (`AuthTable::role_in_account`); a machine
    /// that authenticated as one is named so even where the table says less.
    fn role_in(&self, actor: &str, machine: bool, account: &str) -> &'static str {
        match self.stores.auth().role_in_account(actor, account) {
            Ok("outsider" | "operator") if machine => "machine",
            Ok(role) => role,
            Err(_) => "unknown",
        }
    }
}

impl EnterprisePlane for CoreControlPlane {
    fn account_of_namespace(&self, namespace: &str) -> ControlResult<Option<String>> {
        self.stores
            .auth()
            .account_of_namespace(namespace)
            .map_err(internal)
    }

    fn account_of_principal(&self, principal_id: &str) -> ControlResult<Option<String>> {
        self.stores
            .auth()
            .account_of_principal(principal_id)
            .map_err(internal)
    }

    fn is_account_admin(&self, principal_id: &str, account: &str) -> ControlResult<bool> {
        self.stores
            .auth()
            .is_account_admin(principal_id, account)
            .map_err(internal)
    }

    fn shielded(&self, principal_id: &str, namespace: &str) -> ControlResult<bool> {
        self.stores
            .auth()
            .shielded(principal_id, namespace)
            .map_err(internal)
    }

    fn account_audit_record(&self, account: &str, event: &AccountAuditEvent) -> ControlResult<()> {
        let role = self.role_in(&event.actor, event.actor_machine, account);
        self.stores
            .account_audit()
            .append(
                account,
                AccountEvent {
                    kind: &event.kind,
                    actor: &event.actor,
                    actor_role: role,
                    target: &event.target,
                    detail: &event.detail,
                    outcome: if event.refused {
                        Outcome::Refused
                    } else {
                        Outcome::Ok
                    },
                },
                gunnar_core::now_ms(),
            )
            .map(|_| ())
            .map_err(internal)
    }

    fn account_audit_log(
        &self,
        account: &str,
        query: &AccountAuditQuery,
    ) -> ControlResult<gunnar_proto::AuditLogResponse> {
        let log = self.stores.account_audit();
        let page = log.query(account, &query_in(query)).map_err(internal)?;
        // The chain is checked on the FIRST page: every entry hashed once,
        // what an admin sees beside the newest entries.
        let chain = if query.before_seq.is_none() {
            let report = log.verify(account).map_err(internal)?;
            Some(gunnar_proto::AuditChain {
                holds: report.holds,
                entries: report.entries,
                tip_hash: report.tip_hash,
                anchor_seq: report.anchor_seq,
                broken_at_seq: report.broken_at_seq,
                what: report.what,
            })
        } else {
            None
        };
        let (written, _) = log.head(account).map_err(internal)?;
        Ok(gunnar_proto::AuditLogResponse {
            account: account.to_owned(),
            entries: page.entries.into_iter().map(entry_out).collect(),
            next_before_seq: page.next_before_seq,
            chain,
            retention_days: log.retention_days(),
            written,
        })
    }

    fn account_audit_export(
        &self,
        account: &str,
        query: &AccountAuditQuery,
        format: gunnar_proto::AuditExportFormat,
    ) -> ControlResult<gunnar_proto::ExportAuditLogResponse> {
        let entries = self
            .stores
            .account_audit()
            .all(account, &query_in(query))
            .map_err(internal)?;
        let day = gunnar_core::iso8601_utc(gunnar_core::now_ms());
        let day = &day[..10];
        let (body, content_type, ext) = match format {
            gunnar_proto::AuditExportFormat::Json => {
                (to_json(&entries), "application/json", "json")
            }
            _ => (to_csv(&entries), "text/csv; charset=utf-8", "csv"),
        };
        Ok(gunnar_proto::ExportAuditLogResponse {
            body: body.into_bytes(),
            content_type: content_type.to_owned(),
            file_name: format!("audit-{account}-{day}.{ext}"),
            entries: entries.len() as u64,
        })
    }

    fn aggregates(&self) -> ControlResult<gunnar_proto::AggregatesResponse> {
        let auth = self.stores.auth();
        let mut out = gunnar_proto::AggregatesResponse {
            at_unix_ms: gunnar_core::now_ms(),
            ..Default::default()
        };
        let mut accounts = std::collections::BTreeSet::new();
        let mut employers = std::collections::BTreeSet::new();
        for p in auth.principals().map_err(internal)? {
            if p.kind != gunnar_core::PrincipalKind::User {
                continue;
            }
            match auth.employer_of(&p.id).map_err(internal)? {
                Some(employer) => {
                    out.members += 1;
                    employers.insert(employer);
                }
                // A person who is an account — not the box's own first
                // identity, whose namespace is the estate's.
                None if auth
                    .account_of_principal(&p.id)
                    .map_err(internal)?
                    .as_deref()
                    == Some(p.id.as_str()) =>
                {
                    accounts.insert(p.id);
                }
                None => {}
            }
        }
        out.accounts = accounts.len() as u64;
        out.enterprise_accounts = employers.iter().filter(|a| accounts.contains(*a)).count() as u64;
        // A CLAN's namespace is inside the accounts too (clans + the
        // enterprise list, 2026-10-05): its repositories are its members',
        // never the operator's. A clan is no account of its own in the count.
        let in_account = |ns: &str| {
            let first = ns.split('/').next().unwrap_or("");
            accounts.contains(first) || auth.is_clan(first).unwrap_or(false)
        };
        // ★ **The WHOLE estate** (round 5.3; the owner on test2 2026-10-08:
        // "Repositories 0 (0 public) · Storage used 0 B · Room held —" over
        // the seven repositories, ~205 MB, the release functest seeded into
        // the operator's `nordisk`). Repositories, the public ones among them,
        // the stored bytes and the room held are every namespace's — the
        // operator's own forge included — as numbers, never names.
        // `operator_repos` is the operator's SHARE of `repos`: what is not
        // inside an account.
        for repo in self.stores.catalog().list(None).map_err(internal)? {
            out.repos += 1;
            if repo.visibility == gunnar_core::Visibility::Public {
                out.public_repos += 1;
            }
            if !in_account(&repo.namespace) {
                out.operator_repos += 1;
            }
        }
        if let Ok(rows) = gunnar_grpc::ControlPlane::usage(self, None) {
            out.stored_bytes = rows
                .iter()
                .map(|row| row.pack_bytes.saturating_add(row.lfs_bytes))
                .sum();
        }
        #[cfg(feature = "metered")]
        if let Ok(facts) = gunnar_grpc::ControlPlane::entitlement_list(self) {
            for fact in &facts {
                out.sold_bytes = out
                    .sold_bytes
                    .saturating_add(fact.caps.get("pack_bytes").copied().unwrap_or(0));
                // Accounts holding a paid plan are ACCOUNTS: the operator's
                // forge is none.
                let state =
                    gunnar_proto::EntitlementState::try_from(fact.state).unwrap_or_default();
                if in_account(&fact.namespace)
                    && matches!(
                        state,
                        gunnar_proto::EntitlementState::Paid
                            | gunnar_proto::EntitlementState::Grace
                    )
                {
                    out.paid_accounts += 1;
                }
            }
        }
        for look in auth.operator_access(None).map_err(internal)? {
            match look.state {
                OperatorAccessState::Approved => out.looks_live += 1,
                OperatorAccessState::Pending => out.looks_pending += 1,
                _ => {}
            }
        }
        let alarms = gunnar_grpc::ControlPlane::health_alarms(self);
        out.health_alarms = alarms.len() as u32;
        out.health = if alarms.is_empty() {
            "ok".to_owned()
        } else {
            "alarm".to_owned()
        };
        Ok(out)
    }

    fn operator_access_request(
        &self,
        account: &str,
        operator: &str,
        reason: &str,
        minutes: u32,
    ) -> ControlResult<gunnar_proto::OperatorAccess> {
        let row = self
            .stores
            .auth()
            .request_operator_access(account, operator, reason, minutes, gunnar_core::now_ms())
            .map_err(internal)?;
        Ok(look_out(row))
    }

    fn operator_access_list(
        &self,
        account: Option<&str>,
    ) -> ControlResult<Vec<gunnar_proto::OperatorAccess>> {
        Ok(self
            .stores
            .auth()
            .operator_access(account)
            .map_err(internal)?
            .into_iter()
            .map(look_out)
            .collect())
    }

    fn operator_access_get(
        &self,
        request_id: &str,
    ) -> ControlResult<Option<gunnar_proto::OperatorAccess>> {
        Ok(self
            .stores
            .auth()
            .operator_access_by_id(request_id)
            .map_err(internal)?
            .map(look_out))
    }

    fn operator_access_decide(
        &self,
        decision: &OperatorAccessDecision,
    ) -> ControlResult<gunnar_proto::OperatorAccess> {
        let step_up = decision
            .step_up
            .as_ref()
            .map(|(id, count)| (gunnar_grpc::webauthn::b64url(id), *count));
        let row = self
            .stores
            .auth()
            .decide_operator_access_stepped(
                &decision.request_id,
                &decision.approver,
                decision.approve,
                decision.minutes,
                step_up.as_ref().map(|(id, count)| (id.as_str(), *count)),
                gunnar_core::now_ms(),
            )
            .map_err(internal)?;
        Ok(look_out(row))
    }

    fn operator_access_revoke(
        &self,
        request_id: &str,
        by: &str,
    ) -> ControlResult<gunnar_proto::OperatorAccess> {
        let row = self
            .stores
            .auth()
            .revoke_operator_access(request_id, by, gunnar_core::now_ms())
            .map_err(internal)?;
        Ok(look_out(row))
    }

    fn name_check(&self, kind: &str, name: &str, account: &str) -> ControlResult<NameVerdict> {
        let kind = kind_in(kind)?;
        let verdict = self
            .stores
            .names()
            .check(kind, name, account)
            .map_err(internal)?;
        Ok(NameVerdict {
            verdict: verdict.as_str().to_owned(),
            sentence: if verdict == Verdict::Clean {
                String::new()
            } else {
                verdict.sentence(kind, name)
            },
        })
    }

    fn name_review_add(
        &self,
        kind: &str,
        name: &str,
        account: &str,
        actor: &str,
        hold: bool,
    ) -> ControlResult<gunnar_proto::NameReview> {
        let kind = kind_in(kind)?;
        let names = self.stores.names();
        let verdict = names.check(kind, name, account).map_err(internal)?;
        let now = gunnar_core::now_ms();
        let row = if hold {
            names.hold(kind, name, account, actor, &verdict, now)
        } else {
            names.note(kind, name, account, actor, &verdict, now)
        }
        .map_err(internal)?;
        Ok(review_out(row))
    }

    fn name_lists(&self) -> ControlResult<gunnar_proto::NameLists> {
        Ok(lists_out(self.stores.names().lists().map_err(internal)?))
    }

    fn set_name_lists(
        &self,
        lists: gunnar_proto::NameLists,
    ) -> ControlResult<gunnar_proto::NameLists> {
        let stored = self
            .stores
            .names()
            .set_lists(gunnar_core::name_filter::NameLists {
                blocked: lists.blocked,
                borderline: lists.borderline,
                reserved: lists.reserved,
                reserved_prefixes: lists.reserved_prefixes,
                allowed: lists.allowed,
            })
            .map_err(internal)?;
        Ok(lists_out(stored))
    }

    fn name_reviews(&self, account: &str) -> ControlResult<Vec<gunnar_proto::NameReview>> {
        Ok(self
            .stores
            .names()
            .reviews(account)
            .map_err(internal)?
            .into_iter()
            .map(review_out)
            .collect())
    }

    fn name_review_decide(
        &self,
        account: &str,
        id: &str,
        allow: bool,
        by: &str,
    ) -> ControlResult<gunnar_proto::NameReview> {
        let row = self
            .stores
            .names()
            .decide(id, account, allow, by, gunnar_core::now_ms())
            .map_err(internal)?;
        Ok(review_out(row))
    }
}