gunnar-grpc 3.0.0

The tonic service implementations and the event bus behind gunnar's gRPC control plane.
Documentation
//! **The "encrypted" repository flag is the ACCOUNT admin's decision**
//! (encrypted repositories, Level 2, 2026-10-05).
//!
//! On, the server refuses any push that would put plaintext on it (the
//! `encrypted` gate). Off, it takes plaintext again. Either way it changes what
//! the operator can be trusted with for every member of the account, so it
//! takes `Admin` over the NAMESPACE — the rule `Repos.SetVisibility` keeps —
//! and an `Admin` grant on the one repository does not reach it.

use std::sync::Arc;

use gunnar_grpc::{
    Caller, ControlPlane, EventBus, MemoryControlPlane, MirrorService, ReposService, ServerConfig,
    ServiceContext,
};
use gunnar_proto::mirror_server::Mirror as _;
use gunnar_proto::repos_server::Repos as _;
use gunnar_proto::*;
use tonic::{Code, Request};

const ACCOUNT: &str = "acme";
const REPO: &str = "vault";

struct Rig {
    plane: Arc<MemoryControlPlane>,
    repos: ReposService,
    bus: Arc<EventBus>,
}

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::with_repo(ACCOUNT, REPO));
        plane
            .add_principal(ACCOUNT, ACCOUNT, PrincipalKind::User)
            .expect("the account's owner");
        let control: Arc<dyn ControlPlane> = plane.clone();
        let bus = Arc::new(EventBus::new(16));
        let ctx = ServiceContext::new(control, Arc::clone(&bus), ServerConfig::default());
        Rig {
            repos: ReposService::new(ctx),
            plane,
            bus,
        }
    }

    fn principal(&self, id: &str, scope_repo: &str, level: GrantLevel) -> Caller {
        self.plane
            .add_principal(id, id, PrincipalKind::User)
            .expect("principal");
        self.plane
            .grant(id, ACCOUNT, scope_repo, level)
            .expect("grant");
        caller(id)
    }

    async fn set(&self, who: &Caller, encrypted: bool) -> Result<Repo, tonic::Status> {
        let mut request = Request::new(SetEncryptedRequest {
            path: Some(RepoPath::new(ACCOUNT, REPO)),
            encrypted,
        });
        request.extensions_mut().insert(who.clone());
        self.repos
            .set_encrypted(request)
            .await
            .map(|r| r.into_inner().repo.expect("a repo comes back"))
    }

    fn encrypted(&self) -> bool {
        self.plane
            .get_repo(&RepoPath::new(ACCOUNT, REPO))
            .expect("the repo")
            .encrypted
    }
}

#[tokio::test]
async fn a_new_repository_is_not_encrypted() {
    assert!(!Rig::new().encrypted());
}

#[tokio::test]
async fn the_account_admin_turns_encryption_on_and_off() {
    let rig = Rig::new();
    let on = rig
        .set(&caller(ACCOUNT), true)
        .await
        .expect("the owner turns it on");
    assert!(on.encrypted);
    assert!(rig.encrypted());

    let carol = rig.principal("carol", "", GrantLevel::Admin);
    let off = rig
        .set(&carol, false)
        .await
        .expect("a namespace admin turns it off");
    assert!(!off.encrypted);
    assert!(!rig.encrypted());
}

#[tokio::test]
async fn a_repository_admin_or_a_writer_may_not() {
    let rig = Rig::new();
    let bob = rig.principal("bob", REPO, GrantLevel::Admin);
    let refused = rig
        .set(&bob, true)
        .await
        .expect_err("a repo admin is not the account admin");
    assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
    assert!(
        refused.message().contains("account admin"),
        "the refusal says whose decision it is: {}",
        refused.message()
    );
    let dave = rig.principal("dave", "", GrantLevel::Write);
    assert_eq!(
        rig.set(&dave, true)
            .await
            .expect_err("write is not admin")
            .code(),
        Code::PermissionDenied
    );
    assert!(!rig.encrypted(), "a refusal wrote nothing");
}

// ── a mirror never lands plaintext in an encrypted repository ────────────────
//
// Wiring tracer B 2026-10-08: a mirror's fetched pack is ingested with no
// encrypted gate (`PushIngest::new` sets `encrypted: None`), so a repository
// admin who points an encrypted repository at a plaintext upstream lands
// plaintext on main, framed for the twin. The door refuses it in both
// directions: no mirror INTO an encrypted repository, and no encryption ON a
// mirror (whose content is an upstream's, never sealed by the members).

fn mirrors(rig: &Rig) -> MirrorService {
    let control: Arc<dyn ControlPlane> = rig.plane.clone();
    MirrorService::new(ServiceContext::new(
        control,
        Arc::new(EventBus::new(16)),
        ServerConfig::default(),
    ))
}

async fn add_mirror(
    svc: &MirrorService,
    who: &Caller,
    adopt: bool,
) -> Result<MirrorConfig, tonic::Status> {
    let mut request = Request::new(AddMirrorRequest {
        repo: Some(RepoPath::new(ACCOUNT, REPO)),
        remote_url: "https://example.org/upstream.git".into(),
        prune: false,
        create_repo: false,
        adopt,
    });
    request.extensions_mut().insert(who.clone());
    svc.add(request)
        .await
        .map(|r| r.into_inner().mirror.expect("a mirror comes back"))
}

async fn sync_mirror(
    svc: &MirrorService,
    who: &Caller,
) -> Result<SyncMirrorResponse, tonic::Status> {
    let mut request = Request::new(SyncMirrorRequest {
        repo: Some(RepoPath::new(ACCOUNT, REPO)),
    });
    request.extensions_mut().insert(who.clone());
    svc.sync(request).await.map(|r| r.into_inner())
}

#[tokio::test]
async fn an_encrypted_repository_is_never_made_a_mirror() {
    let rig = Rig::new();
    let svc = mirrors(&rig);
    rig.set(&caller(ACCOUNT), true)
        .await
        .expect("the owner turns it on");
    let bob = rig.principal("bob", REPO, GrantLevel::Admin);
    for (who, adopt) in [(caller(ACCOUNT), false), (bob.clone(), false), (bob, true)] {
        let refused = add_mirror(&svc, &who, adopt)
            .await
            .expect_err("a mirror's pack is an upstream's plaintext");
        assert_eq!(refused.code(), Code::FailedPrecondition, "{refused:?}");
        assert!(
            refused.message().contains("encrypted") && !refused.message().contains("Level 1"),
            "the refusal says why, in plain words: {}",
            refused.message()
        );
    }
    assert!(
        rig.plane
            .mirror_status(None, Some(&RepoPath::new(ACCOUNT, REPO)))
            .expect("status")
            .is_empty(),
        "no mirror was registered"
    );
}

#[tokio::test]
async fn a_mirror_is_never_turned_encrypted() {
    let rig = Rig::new();
    let svc = mirrors(&rig);
    add_mirror(&svc, &caller(ACCOUNT), false)
        .await
        .expect("a plain repository may be a mirror");
    let refused = rig
        .set(&caller(ACCOUNT), true)
        .await
        .expect_err("a mirror's content is the upstream's, never sealed by its members");
    assert_eq!(refused.code(), Code::FailedPrecondition, "{refused:?}");
    assert!(
        refused.message().contains("mirror"),
        "{}",
        refused.message()
    );
    assert!(!rig.encrypted(), "a refusal wrote nothing");
    // Turning it OFF is never refused: it takes nothing away from the members.
    rig.set(&caller(ACCOUNT), false)
        .await
        .expect("off is always allowed");
}

#[tokio::test]
async fn a_mirror_made_encrypted_before_the_rule_is_never_synced() {
    let rig = Rig::new();
    let svc = mirrors(&rig);
    add_mirror(&svc, &caller(ACCOUNT), false)
        .await
        .expect("registered");
    // A repository that became encrypted before the door refused it (or by a
    // path that skips the door): the sync is refused at the door, by name.
    rig.plane
        .set_encrypted(&RepoPath::new(ACCOUNT, REPO), true)
        .expect("the flag, set below the door");
    let refused = sync_mirror(&svc, &caller(ACCOUNT))
        .await
        .expect_err("an encrypted repository is never fetched into");
    assert_eq!(refused.code(), Code::FailedPrecondition, "{refused:?}");
    assert!(
        refused.message().contains("encrypted"),
        "{}",
        refused.message()
    );
}

/// ★ **Turning encryption on or off is an event** (wiring tracer B
/// 2026-10-08), beside the account's audit row: a webhook or a watcher hears
/// it as it hears a visibility change. A set that changes nothing is no event.
#[tokio::test]
async fn turning_encryption_on_or_off_is_an_event() {
    use tokio_stream::StreamExt as _;
    let rig = Rig::new();
    let mut stream = rig.bus.subscribe(gunnar_grpc::EventFilter::default());
    rig.set(&caller(ACCOUNT), true).await.expect("on");
    rig.set(&caller(ACCOUNT), true)
        .await
        .expect("on again: no change");
    rig.set(&caller(ACCOUNT), false).await.expect("off");
    let mut seen = Vec::new();
    while let Ok(Some(Ok(event))) =
        tokio::time::timeout(std::time::Duration::from_millis(500), stream.next()).await
    {
        if let Some(event::Payload::EncryptedChanged(e)) = &event.payload {
            assert_eq!(event.kind, EventKind::EncryptedChanged as i32);
            assert_eq!(e.repo, Some(RepoPath::new(ACCOUNT, REPO)));
            assert_eq!(e.principal_id, ACCOUNT);
            seen.push((e.from, e.to));
        }
    }
    assert_eq!(
        seen,
        vec![(false, true), (true, false)],
        "on and off, each once"
    );
}