use gunnar_proto::forge_server::Forge;
use gunnar_proto::{
AppendEventRequest, AppendEventResponse, GetIssueRequest, GetIssueResponse, GetReviewRequest,
GetReviewResponse, GrantLevel, ListIssuesRequest, ListIssuesResponse, ListReviewsRequest,
ListReviewsResponse,
};
use tonic::{Request, Response, Status};
use crate::auth::Access;
use crate::service::ForgeService;
use crate::status::require;
fn scope(repo: &str) -> Result<Access, Status> {
let Some((namespace, name)) = repo.split_once('/') else {
return Err(Status::invalid_argument(format!(
"`repo` must be `<namespace>/<repo>`, got {repo:?}"
)));
};
if namespace.is_empty() || name.is_empty() || name.contains('/') {
return Err(Status::invalid_argument(format!(
"`repo` must be `<namespace>/<repo>`, got {repo:?}"
)));
}
Ok(Access::Repo(gunnar_proto::RepoPath {
namespace: namespace.to_owned(),
repo: name.to_owned(),
}))
}
#[tonic::async_trait]
impl Forge for ForgeService {
async fn append(
&self,
request: Request<AppendEventRequest>,
) -> Result<Response<AppendEventResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let session = request
.extensions()
.get::<crate::auth::PersonSession>()
.copied();
let request = request.into_inner();
let signed = require(request.event, "event")?;
let event = signed
.event
.as_ref()
.ok_or_else(|| Status::invalid_argument("`event.event` is required"))?;
let access = scope(&event.repo)?;
self.ctx
.require(&caller, access, GrantLevel::Write, "Forge.Append")?;
if !caller.principal_id.is_empty() && event.actor != caller.principal_id {
return Err(Status::permission_denied(format!(
"this event is attributed to {:?} and the caller is {:?}; a forge event is \
evidence about who did something, so it may not be filed in another \
principal's name",
event.actor, caller.principal_id
)));
}
if signed.attested.is_some() {
return Err(Status::invalid_argument(
"this event carries an attestation, and an attestation is gunnar's own word: only \
gunnar makes one, for a person's session that stepped up",
));
}
if signed.key_fingerprint.is_empty() && signed.signature.is_empty() {
let Some(session) = session else {
return Err(Status::invalid_argument(
"this event is unsigned: sign it with your own registered key, or send it \
from your own passkey session after a fresh touch, which gunnar attests",
));
};
let repo = event.repo.clone();
let pull = event.pull;
if event.actor.is_empty() || event.actor != caller.principal_id {
return Err(Status::permission_denied(format!(
"gunnar attests an event only for the session's own person ({:?}), and this \
one is attributed to {:?}",
caller.principal_id, event.actor
)));
}
if !session.stepped_up {
tracing::warn!(
target: crate::auth::AUDIT_TARGET,
event = "forge.step_up_refused",
principal = %caller.principal_id,
repo = %repo,
pull,
"a forge event came from no stepped-up session"
);
return Err(Status::permission_denied(
"[stepup] Forge.Append needs a fresh touch of your own passkey (user-verified, \
within the last 10 minutes)",
));
}
let at = session
.stepped_up_at_unix_ms
.unwrap_or_else(crate::bus::now_unix_ms);
let (index, words) = self.ctx.control.forge_append_attested(signed, at)?;
tracing::info!(
target: crate::auth::AUDIT_TARGET,
event = "forge.attested",
principal = %caller.principal_id,
repo = %repo,
pull,
index,
attestation = %words,
"gunnar attested a person's forge event for their stepped-up session"
);
let namespace = repo.split_once('/').map(|(ns, _)| ns).unwrap_or_default();
self.ctx.account_event(
self.ctx.account_of_namespace(namespace),
crate::enterprise::AccountAuditEvent {
kind: "forge.attested".to_owned(),
actor: caller.principal_id.clone(),
actor_machine: false,
target: format!("{repo}#{pull}"),
detail: words,
refused: false,
},
);
return Ok(Response::new(AppendEventResponse { index }));
}
let index = self.ctx.control.forge_append(signed)?;
Ok(Response::new(AppendEventResponse { index }))
}
async fn get_review(
&self,
request: Request<GetReviewRequest>,
) -> Result<Response<GetReviewResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let request = request.into_inner();
let access = scope(&request.repo)?;
self.ctx
.require(&caller, access, GrantLevel::Read, "Forge.GetReview")?;
let (state, events) = self.ctx.control.forge_review(&request.repo, request.pull)?;
Ok(Response::new(GetReviewResponse {
state: Some(state),
events,
}))
}
async fn list_reviews(
&self,
request: Request<ListReviewsRequest>,
) -> Result<Response<ListReviewsResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let request = request.into_inner();
let access = scope(&request.repo)?;
self.ctx
.require(&caller, access, GrantLevel::Read, "Forge.ListReviews")?;
let reviews = self.ctx.control.forge_reviews(&request.repo)?;
Ok(Response::new(ListReviewsResponse { reviews }))
}
async fn get_issue(
&self,
request: Request<GetIssueRequest>,
) -> Result<Response<GetIssueResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let request = request.into_inner();
let access = scope(&request.repo)?;
self.ctx
.require(&caller, access, GrantLevel::Read, "Forge.GetIssue")?;
let (state, events) = self
.ctx
.control
.forge_issue(&request.repo, request.number)?
.ok_or_else(|| {
Status::not_found(format!(
"no issue #{} in {}: nobody has opened one",
request.number, request.repo
))
})?;
Ok(Response::new(GetIssueResponse {
state: Some(state),
events,
}))
}
async fn list_issues(
&self,
request: Request<ListIssuesRequest>,
) -> Result<Response<ListIssuesResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let request = request.into_inner();
let access = scope(&request.repo)?;
self.ctx
.require(&caller, access, GrantLevel::Read, "Forge.ListIssues")?;
let issues = self.ctx.control.forge_issues(&request.repo)?;
Ok(Response::new(ListIssuesResponse { issues }))
}
}