crates/gunnar-grpc/src/repo_door.rs
//! **A repository's settings, asked over another door** (owner, 2026-10-14).
//!
//! On an installed estate the control plane is bound to the WireGuard overlay,
//! so a person's own machine cannot reach `Repos.Get` / `Repos.SetVisibility`
//! / `Repos.SetDefaultBranch`. The forge's SSH door can: it proves who asks
//! with the person's key and hands the ask here. This module decides NOTHING:
//! it builds the same `tonic::Request` the gRPC door would, with the
//! [`Caller`] the SSH door authenticated attached the way [`crate::auth`]'s
//! gate attaches it, and calls the SAME [`ReposService`] handlers — so the
//! account-admin rule for visibility, the repository-Admin rule for the
//! default branch, the account audit event and the browse replica's notice
//! are the handlers' own, not a copy.
use gunnar_proto::repos_server::Repos;
use gunnar_proto::{
CreateRepoRequest, GetRepoRequest, HashKind, Repo, RepoPath, SetDefaultBranchRequest,
SetVisibilityRequest, Visibility,
};
use tonic::{Request, Status};
use crate::auth::Caller;
use crate::service::{ReposService, ServiceContext};
/// What is asked of one repository.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RepoSetting {
/// `Repos.Get`.
Show,
/// `Repos.SetVisibility`: `true` is public.
Visibility(bool),
/// `Repos.SetDefaultBranch`, the full ref name (`refs/heads/…`).
DefaultBranch(String),
/// `Repos.Create` of an empty repository: `true` is sha256.
Create(bool),
}
/// Ask `Repos` as `principal` (authenticated by `fingerprint`'s key).
pub async fn ask(
ctx: &ServiceContext,
principal: &str,
fingerprint: &str,
namespace: &str,
repo: &str,
setting: RepoSetting,
) -> Result<Repo, Status> {
let caller = Caller {
principal_id: principal.to_owned(),
fingerprint: fingerprint.to_owned(),
// A person's key over the forge's door. The browse replica's single
// machine write never comes this way.
machine: false,
authenticated: true,
};
fn as_caller<T>(caller: &Caller, message: T) -> Request<T> {
let mut request = Request::new(message);
request.extensions_mut().insert(caller.clone());
request
}
let path = Some(RepoPath {
namespace: namespace.to_owned(),
repo: repo.to_owned(),
});
let service = ReposService::new(ctx.clone());
let repo = match setting {
RepoSetting::Show => {
service
.get(as_caller(&caller, GetRepoRequest { path }))
.await?
.into_inner()
.repo
}
RepoSetting::Visibility(public) => {
let visibility = if public {
Visibility::Public
} else {
Visibility::Private
} as i32;
service
.set_visibility(as_caller(
&caller,
SetVisibilityRequest { path, visibility },
))
.await?
.into_inner()
.repo
}
RepoSetting::Create(sha256) => {
let hash_kind = if sha256 {
HashKind::Sha256
} else {
HashKind::Sha1
} as i32;
service
.create(as_caller(
&caller,
CreateRepoRequest {
path,
hash_kind,
..Default::default()
},
))
.await?
.into_inner()
.repo
}
RepoSetting::DefaultBranch(default_branch) => {
service
.set_default_branch(as_caller(
&caller,
SetDefaultBranchRequest {
path,
default_branch,
},
))
.await?
.into_inner()
.repo
}
};
repo.ok_or_else(|| Status::internal("the repository service answered with no repository"))
}
/// The answer as `key value` lines, the shape the SSH verb prints.
pub fn render(repo: &Repo) -> String {
let path = repo
.path
.as_ref()
.map(|p| format!("{}/{}", p.namespace, p.repo))
.unwrap_or_default();
let visibility = if repo.visibility == Visibility::Public as i32 {
"public"
} else {
"private"
};
let format = if repo.hash_kind == HashKind::Sha256 as i32 {
"sha256"
} else {
"sha1"
};
format!(
"repository {path}\nvisibility {visibility}\ndefault-branch {}\nencrypted {}\nobject-format {format}\n",
repo.default_branch,
if repo.encrypted { "yes" } else { "no" }
)
}