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/browse.rs

//! **The browse replica's control plane refuses every mutator**, at the seam
//! every handler passes through — `ServiceContext::
//! require` / `require_repo_or_namespace` — against the in-memory control
//! plane, so the matrix is the gate's and not one handler's.
//!
//! Red-first: with the `refuse_on_browse` call removed from `require`, every
//! `refuses!` line below passes its gate and this file goes red on the first
//! one. The "still answers" lines are the other half: a refusal that also
//! blocked `Mirror.ReplicationStatus` would leave the operator blind on the
//! box they most need to see, and one that blocked the primary's `Repos.Delete`
//! would leave private data on the public-facing replica.

use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

use gunnar_grpc::auth::audited_methods;
use gunnar_grpc::{
    AccessService, Caller, ControlPlane, EventBus, MemoryControlPlane, MirrorService, PullsService,
    ReposService, ServerConfig, ServiceContext, BROWSE_STILL_ANSWERS,
};
use gunnar_proto::access_server::Access as _;
use gunnar_proto::mirror_server::Mirror as _;
use gunnar_proto::pull_requests_server::PullRequests as _;
use gunnar_proto::repos_server::Repos as _;
use gunnar_proto::*;
use tonic::{Code, Request};

const NS: &str = "nordisk";
const REPO: &str = "gunnar";

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

impl Rig {
    fn new() -> Rig {
        let plane = Arc::new(MemoryControlPlane::with_repo(NS, REPO));
        let control: Arc<dyn ControlPlane> = plane.clone();
        let browse = Arc::new(AtomicBool::new(false));
        let ctx = ServiceContext::new(
            control,
            Arc::new(EventBus::new(64)),
            ServerConfig::default(),
        )
        .with_browse_flag(Arc::clone(&browse));
        Rig { plane, ctx, browse }
    }

    /// A server-wide admin, as the operator's key is.
    fn admin(&self, name: &str, kind: PrincipalKind) -> Caller {
        let principal = self
            .plane
            .add_principal(name, name, kind)
            .expect("principal added");
        self.plane.grant_server(&principal.id, GrantLevel::Admin);
        Caller {
            principal_id: principal.id,
            fingerprint: format!("SHA256:{name}"),
            machine: kind == PrincipalKind::Machine,
            authenticated: true,
        }
    }

    fn authed<T>(&self, caller: &Caller, message: T) -> Request<T> {
        let mut request = Request::new(message);
        request.extensions_mut().insert(caller.clone());
        request
    }
}

fn code_of<T>(result: Result<T, tonic::Status>) -> Option<Code> {
    result.err().map(|status| status.code())
}

/// The refusal is `FAILED_PRECONDITION`, names the box, and lifts the instant
/// the flag drops — the same admin key, the same request, both answers.
#[test]
fn a_browse_replica_refuses_a_mutator_by_precondition_and_a_standby_does_not() {
    let rig = Rig::new();
    let admin = rig.admin("operator", PrincipalKind::User);
    let repos = ReposService::new(rig.ctx.clone());
    let rt = tokio::runtime::Builder::new_current_thread()
        .build()
        .expect("runtime");
    let create = || {
        rig.authed(
            &admin,
            CreateRepoRequest {
                path: Some(RepoPath::new(NS, "new")),
                ..Default::default()
            },
        )
    };

    rig.browse.store(true, Ordering::SeqCst);
    let refused = rt
        .block_on(repos.create(create()))
        .expect_err("a browse replica must refuse Repos.Create");
    assert_eq!(refused.code(), Code::FailedPrecondition, "{refused:?}");
    assert!(
        refused.message().contains("browse replica") && refused.message().contains("Repos.Create"),
        "the refusal names the box and the method: {}",
        refused.message()
    );

    rig.browse.store(false, Ordering::SeqCst);
    rt.block_on(repos.create(create()))
        .expect("the same request on a non-browse box passes the gate");
}

/// The matrix: every Admin-level write is refused; the Admin-level reads and
/// `Mirror.SetRole` still answer; `Repos.Delete` and `Repos.Create` answer a
/// MACHINE and refuse a person.
#[test]
fn the_browse_matrix_reads_answer_writes_refuse_and_delete_is_the_machines_alone() {
    let rig = Rig::new();
    let person = rig.admin("operator", PrincipalKind::User);
    let machine = rig.admin("primary", PrincipalKind::Machine);
    rig.browse.store(true, Ordering::SeqCst);
    let repos = ReposService::new(rig.ctx.clone());
    let access = AccessService::new(rig.ctx.clone());
    let mirror = MirrorService::new(rig.ctx.clone());
    let pulls = PullsService::new(rig.ctx.clone());
    let rt = tokio::runtime::Builder::new_current_thread()
        .build()
        .expect("runtime");

    // Writes: refused, whoever asks.
    for caller in [&person, &machine] {
        assert_eq!(
            code_of(rt.block_on(repos.create_namespace(rig.authed(
                caller,
                CreateNamespaceRequest {
                    name: "x".into(),
                    ..Default::default()
                },
            )))),
            Some(Code::FailedPrecondition),
            "Repos.CreateNamespace by {}",
            caller.principal_id
        );
        assert_eq!(
            code_of(rt.block_on(access.add_principal(rig.authed(
                caller,
                AddPrincipalRequest {
                    name: "someone".into(),
                    kind: PrincipalKind::User as i32,
                    id: String::new(),
                },
            )))),
            Some(Code::FailedPrecondition),
            "Access.AddPrincipal by {}",
            caller.principal_id
        );
        assert_eq!(
            code_of(rt.block_on(
                mirror.set_replication(rig.authed(caller, SetReplicationRequest { target: None },))
            )),
            Some(Code::FailedPrecondition),
            "Mirror.SetReplication by {}",
            caller.principal_id
        );
        assert_eq!(
            code_of(rt.block_on(
                mirror.abdicate_replication(rig.authed(caller, AbdicateReplicationRequest {},))
            )),
            Some(Code::FailedPrecondition),
            "Mirror.AbdicateReplication by {}",
            caller.principal_id
        );
        // **GUN-010 says this one is not covered. It is** — and now it is
        // pinned rather than argued. The review read `refuse_on_browse` as a
        // check the merge path skips; the merge path calls
        // `ServiceContext::require` like every other handler, `require` calls
        // `refuse_on_browse` first, `PullRequests.Merge` is not in
        // `BROWSE_STILL_ANSWERS`, and the level is `Write`. So a merge on a
        // browse box is `FAILED_PRECONDITION` before the seam is reached —
        // which is also why the row is here, in the matrix, and not in a test
        // of its own: the property belongs to the gate, not to the verb.
        assert_eq!(
            code_of(rt.block_on(pulls.merge(rig.authed(
                caller,
                MergePullRequestRequest {
                    repo: Some(RepoPath::new(NS, REPO)),
                    number: 1,
                    ..Default::default()
                },
            )))),
            Some(Code::FailedPrecondition),
            "PullRequests.Merge by {}",
            caller.principal_id
        );
    }

    // Reads at Admin level: not refused by the browse gate. (The in-memory
    // plane has no replication, so ReplicationStatus answers Unimplemented —
    // the point is that it is NOT FailedPrecondition.)
    assert_ne!(
        code_of(rt.block_on(
            access.list_principals(rig.authed(&person, ListPrincipalsRequest::default(),))
        )),
        Some(Code::FailedPrecondition),
        "Access.ListPrincipals must still answer on a browse box"
    );
    assert_ne!(
        code_of(rt.block_on(mirror.replication_status(rig.authed(
            &person,
            ReplicationStatusRequest {
                include_refs: false
            },
        )))),
        Some(Code::FailedPrecondition),
        "Mirror.ReplicationStatus must still answer on a browse box"
    );
    // SetRole passes the gate (the control plane decides PRIMARY vs STANDBY).
    assert_ne!(
        code_of(rt.block_on(mirror.set_role(rig.authed(
            &person,
            SetRoleRequest {
                role: ReplicationRole::Standby as i32,
            },
        )))),
        Some(Code::FailedPrecondition),
        "Mirror.SetRole must reach the control plane on a browse box"
    );

    // Repos.Delete: the machine's alone.
    assert_eq!(
        code_of(rt.block_on(repos.delete(rig.authed(
            &person,
            DeleteRepoRequest {
                path: Some(RepoPath::new(NS, REPO)),
                purge: true,
                ..Default::default()
            },
        )))),
        Some(Code::FailedPrecondition),
        "a PERSON's admin key cannot delete on a browse box"
    );
    rt.block_on(repos.delete(rig.authed(
        &machine,
        DeleteRepoRequest {
            path: Some(RepoPath::new(NS, REPO)),
            purge: true,
            ..Default::default()
        },
    )))
    .expect("the primary's MACHINE deletes on a browse box: that is the forget path");

    // Repos.Create: the machine's alone, likewise (2026-09-22) — the forget's
    // other half. A repository the browse box purged comes back by a push,
    // and a push-create is sha1 by construction, so the primary's replicator
    // pre-creates a sha256 repository here in its own format before pushing.
    let create = |caller: &Caller| {
        rig.authed(
            caller,
            CreateRepoRequest {
                path: Some(RepoPath::new(NS, "returned")),
                hash_kind: HashKind::Sha256 as i32,
                visibility: Visibility::Public as i32,
                ..Default::default()
            },
        )
    };
    assert_eq!(
        code_of(rt.block_on(repos.create(create(&person)))),
        Some(Code::FailedPrecondition),
        "a PERSON's admin key cannot create on a browse box"
    );
    rt.block_on(repos.create(create(&machine))).expect(
        "the primary's MACHINE creates on a browse box: that is the pre-create before a push",
    );
}

/// The allowlist names only methods that exist, so a rename cannot silently
/// turn a "still answers" into a refusal.
///
/// **The carve-out this used to carry is gone (2026-09-11, GUN-023 /
/// GUN-024).** On 2026-09-04 three Admin-level READS —
/// `Access.ListGroupMembers`, `Access.ListGroupMembershipEvents`,
/// `Webhooks.ListWebhooks` — were found audited by their handlers
/// (`service/access.rs`, `service/webhook.rs`) and absent from
/// `gunnar_grpc::auth::METHODS`, so the deny-by-default sweep in
/// `tests/auth.rs` never drove them. They were listed here by name with an
/// INVERTED assertion — they had to stay absent — precisely so that the day
/// METHODS was fixed this list would have to shrink rather than quietly become
/// a second census.
///
/// That day came, and the inverted assertion is what announced it: with the
/// census derived from the `.proto` files it turned out to be missing twelve
/// RPCs and not three, all three of those among them. So the list below is
/// gone rather than shorter, the inversion has nothing left to hold, and every
/// method in `BROWSE_STILL_ANSWERS` is now simply required to be IN the census
/// — the one assertion this test always wanted to be.
#[test]
fn every_method_the_browse_replica_still_answers_is_a_real_method() {
    let census: std::collections::BTreeSet<&str> = audited_methods().into_iter().collect();
    for method in BROWSE_STILL_ANSWERS {
        assert!(
            census.contains(method),
            "{method} is in BROWSE_STILL_ANSWERS but no handler audits under that name"
        );
    }
}

/// **T21 (owner ruling 2026-09-22): a TWIN takes its own data-set growth exactly as
/// main does.** A twin is frozen like a browse replica, so without the twin clause
/// `Admin.GrowStart` / `GrowResume` / `GrowStatus` were refused there — which left
/// stop → resize → start as the only way to grow it. They pass the browse gate on a
/// twin and ONLY on a twin: a public browse box still refuses them.
#[test]
fn a_twin_answers_its_own_growth_and_a_browse_replica_still_refuses_it() {
    use gunnar_grpc::AdminService;
    use gunnar_proto::admin_server::Admin as _;
    let rig = Rig::new();
    let admin = rig.admin("monetize", PrincipalKind::Machine);
    let svc = AdminService::new(rig.ctx.clone());
    let rt = tokio::runtime::Builder::new_current_thread()
        .build()
        .expect("runtime");
    let start = || {
        rig.authed(
            &admin,
            GrowStartRequest {
                target_sectors_per_member: 60 << 21,
                go_ahead: b"{}".to_vec(),
                ..Default::default()
            },
        )
    };
    let resume = || rig.authed(&admin, GrowResumeRequest { give_up: false });
    rig.browse.store(true, Ordering::SeqCst);

    let refused = rt
        .block_on(svc.grow_start(start()))
        .expect_err("a browse replica refuses a growth");
    assert_eq!(refused.code(), Code::FailedPrecondition, "{refused:?}");
    assert!(
        refused.message().contains("browse replica"),
        "{}",
        refused.message()
    );
    assert_eq!(
        code_of(rt.block_on(svc.grow_resume(resume()))),
        Some(Code::FailedPrecondition)
    );

    rig.plane.set_twin(true);
    // Past the gate: the in-memory plane has no engine, and says so — which is the
    // HANDLER answering, not the browse refusal.
    let past = rt
        .block_on(svc.grow_start(start()))
        .expect_err("the double has no engine");
    assert_ne!(
        past.code(),
        Code::FailedPrecondition,
        "a twin must not be refused its own growth as a browse box: {past:?}"
    );
    assert!(
        !past.message().contains("browse replica"),
        "{}",
        past.message()
    );
    assert!(
        rt.block_on(svc.grow_status(rig.authed(&admin, GrowStatusRequest {})))
            .is_ok(),
        "a twin answers its growth status"
    );
    assert_ne!(
        code_of(rt.block_on(svc.grow_resume(resume()))),
        Some(Code::FailedPrecondition)
    );
}