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");
}