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

nordisk/gunnar

crates/gunnar-grpc/tests/reserved_names.rs

//! **The console's own names are not a person's** (owner 2026-10-04, the thin
//! layer, Stage L).
//!
//! `console-*`, `gunnar-ui` and `uitest-console` are names a console signs its
//! own calls under. Until this file only the console refused them — and only
//! at the INVITE, after `Accounts.EnrollEmployee` had already made the
//! principal — so an account admin could leave a `console-<x>` person at
//! gunnar, the name the console's reviewer provisioning then adopted and
//! granted server-wide Write. gunnar now refuses them itself, as an id or as
//! a name: always for an employee, and for `Access.AddPrincipal` unless the
//! principal is a MACHINE (the console's own principal, made by a server
//! admin with `gunnar-ui-server enroll`).

use std::sync::Arc;

use gunnar_grpc::{
    AccessService, AccountsService, Caller, ControlPlane, EventBus, MemoryControlPlane,
    ServerConfig, ServiceContext,
};
use gunnar_proto::access_server::Access as _;
use gunnar_proto::accounts_server::Accounts as _;
use gunnar_proto::*;
use tonic::{Code, Request};

const ACCOUNT: &str = "acme";

struct Rig {
    plane: Arc<MemoryControlPlane>,
    ctx: ServiceContext,
}

fn caller(id: &str) -> Caller {
    Caller {
        principal_id: id.to_owned(),
        fingerprint: format!("SHA256:{id}"),
        machine: false,
        authenticated: true,
    }
}

impl Rig {
    fn new() -> Rig {
        let plane = Arc::new(MemoryControlPlane::new());
        plane
            .create_namespace(ACCOUNT, "")
            .expect("the account's namespace");
        plane
            .add_principal(ACCOUNT, ACCOUNT, PrincipalKind::User)
            .expect("the account's owner");
        plane
            .add_principal("boss", "boss", PrincipalKind::User)
            .expect("the box's administrator");
        plane.grant_server("boss", GrantLevel::Admin);
        let control: Arc<dyn ControlPlane> = plane.clone();
        let ctx = ServiceContext::new(
            control,
            Arc::new(EventBus::new(16)),
            ServerConfig::default(),
        );
        Rig { plane, ctx }
    }

    fn with<T>(who: &str, body: T) -> Request<T> {
        let mut request = Request::new(body);
        request.extensions_mut().insert(caller(who));
        request
    }

    async fn add(
        &self,
        id: &str,
        name: &str,
        kind: PrincipalKind,
    ) -> Result<Principal, tonic::Status> {
        AccessService::new(self.ctx.clone())
            .add_principal(Rig::with(
                "boss",
                AddPrincipalRequest {
                    name: name.into(),
                    kind: kind as i32,
                    id: id.into(),
                },
            ))
            .await
            .map(|r| r.into_inner().principal.expect("a principal"))
    }

    async fn enroll(&self, id: &str, name: &str) -> Result<Principal, tonic::Status> {
        AccountsService::new(self.ctx.clone())
            .enroll_employee(Rig::with(
                ACCOUNT,
                EnrollEmployeeRequest {
                    account: ACCOUNT.into(),
                    id: id.into(),
                    name: name.into(),
                    public_key: String::new(),
                    ..Default::default()
                },
            ))
            .await
            .map(|r| r.into_inner().principal.expect("a principal"))
    }

    fn exists(&self, id: &str) -> bool {
        self.plane
            .list_principals()
            .expect("list")
            .iter()
            .any(|p| p.id == id)
    }
}

/// ★ **An account admin may not enroll an employee under a console's name**
/// — as the id or as the name — and nothing is made.
#[tokio::test]
async fn an_employee_may_not_take_a_consoles_name() {
    let rig = Rig::new();
    for (id, name) in [
        ("console-x", "console-x"),
        ("bob", "console-bob"),
        ("console-y", "yvonne"),
        ("gunnar-ui", "gunnar-ui"),
        ("uitest-console", "uitest-console"),
        ("Console-Z", "Console-Z"),
    ] {
        let refused = rig
            .enroll(id, name)
            .await
            .expect_err("a console's name is not an employee's");
        assert_eq!(
            refused.code(),
            Code::InvalidArgument,
            "{id}/{name}: {refused:?}"
        );
        assert!(
            refused.message().contains("reserved"),
            "the refusal names the rule: {refused:?}"
        );
        assert!(!rig.exists(id), "{id} was made anyway");
    }
    rig.enroll("bob", "bob")
        .await
        .expect("an ordinary name is still enrolled");
}

/// ★ **`Access.AddPrincipal` refuses a console's name for a person, a group
/// or a deploy key — and lets a server admin make the console's own MACHINE
/// principal under it.**
#[tokio::test]
async fn a_consoles_name_is_a_machines_only() {
    let rig = Rig::new();
    for kind in [
        PrincipalKind::User,
        PrincipalKind::Group,
        PrincipalKind::Deploy,
    ] {
        let refused = rig
            .add("console-kim", "console-kim", kind)
            .await
            .expect_err("a console's name is not a person's");
        assert_eq!(
            refused.code(),
            Code::InvalidArgument,
            "{kind:?}: {refused:?}"
        );
    }
    let refused = rig
        .add("kim", "gunnar-ui", PrincipalKind::User)
        .await
        .expect_err("the NAME is reserved too");
    assert_eq!(refused.code(), Code::InvalidArgument, "{refused:?}");
    assert!(!rig.exists("console-kim") && !rig.exists("kim"));

    rig.add("gunnar-ui", "gunnar-ui", PrincipalKind::Machine)
        .await
        .expect("the console's own machine principal");
    rig.add("uitest-console", "uitest-console", PrincipalKind::Machine)
        .await
        .expect("the test rig's console");
    rig.add("kim", "kim", PrincipalKind::User)
        .await
        .expect("an ordinary person");
}

/// A rename is the same door: a person may not be renamed into a console's
/// name, and a machine may.
#[tokio::test]
async fn a_person_may_not_be_renamed_into_a_consoles_name() {
    let rig = Rig::new();
    rig.add("kim", "kim", PrincipalKind::User)
        .await
        .expect("kim");
    rig.add("ci", "ci", PrincipalKind::Machine)
        .await
        .expect("ci");
    let rename = |id: &str, name: &str| {
        Rig::with(
            "boss",
            RenamePrincipalRequest {
                principal_id: id.into(),
                name: name.into(),
            },
        )
    };
    let access = AccessService::new(rig.ctx.clone());
    let refused = access
        .rename_principal(rename("kim", "console-kim"))
        .await
        .expect_err("a person is not renamed into a console's name");
    assert_eq!(refused.code(), Code::InvalidArgument, "{refused:?}");
    access
        .rename_principal(rename("ci", "console-ci"))
        .await
        .expect("a machine may carry one");
}