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