use gunnar_proto::policy_server::Policy;
use gunnar_proto::{
DeleteProtectionRequest, DeleteProtectionResponse, GateInfo, GetProtectionRequest,
GetProtectionResponse, GetRefNamePolicyRequest, GetRefNamePolicyResponse, ListGatesRequest,
ListGatesResponse, SetProtectionRequest, SetProtectionResponse, SetRefNamePolicyRequest,
SetRefNamePolicyResponse,
};
use tonic::{Request, Response, Status};
use crate::auth::Access;
use crate::service::PolicyService;
use crate::status::{opt_str, repo_path, require};
use gunnar_proto::GrantLevel;
#[tonic::async_trait]
impl Policy for PolicyService {
async fn get_protection(
&self,
request: Request<GetProtectionRequest>,
) -> Result<Response<GetProtectionResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let request = request.into_inner();
let path = repo_path(request.repo, "repo")?;
self.ctx.require(
&caller,
Access::repo(&path),
GrantLevel::Read,
"Policy.GetProtection",
)?;
let protections = self
.ctx
.control
.get_protection(&path, opt_str(&request.ref_pattern))?;
Ok(Response::new(GetProtectionResponse { protections }))
}
async fn set_protection(
&self,
request: Request<SetProtectionRequest>,
) -> Result<Response<SetProtectionResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let request = request.into_inner();
let path = repo_path(request.repo, "repo")?;
self.ctx.require(
&caller,
Access::repo(&path),
GrantLevel::Admin,
"Policy.SetProtection",
)?;
let protection = require(request.protection, "protection")?;
let stored = self.ctx.control.set_protection(&path, protection)?;
self.ctx.account_event(
self.ctx.account_of_namespace(&path.namespace),
crate::enterprise::AccountAuditEvent::by(
&caller,
"protection.set",
format!("{}/{}", path.namespace, path.repo),
)
.detail(format!("{:?}", stored.ref_pattern)),
);
Ok(Response::new(SetProtectionResponse {
protection: Some(stored),
}))
}
async fn delete_protection(
&self,
request: Request<DeleteProtectionRequest>,
) -> Result<Response<DeleteProtectionResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let request = request.into_inner();
let path = repo_path(request.repo, "repo")?;
self.ctx.require(
&caller,
Access::repo(&path),
GrantLevel::Admin,
"Policy.DeleteProtection",
)?;
if request.ref_pattern.is_empty() {
return Err(Status::invalid_argument("`ref_pattern` is required"));
}
self.ctx
.control
.delete_protection(&path, &request.ref_pattern)?;
self.ctx.account_event(
self.ctx.account_of_namespace(&path.namespace),
crate::enterprise::AccountAuditEvent::by(
&caller,
"protection.deleted",
format!("{}/{}", path.namespace, path.repo),
)
.detail(format!("{:?}", request.ref_pattern)),
);
Ok(Response::new(DeleteProtectionResponse {}))
}
async fn get_ref_name_policy(
&self,
request: Request<GetRefNamePolicyRequest>,
) -> Result<Response<GetRefNamePolicyResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let path = repo_path(request.into_inner().repo, "repo")?;
self.ctx.require(
&caller,
Access::repo(&path),
GrantLevel::Read,
"Policy.GetRefNamePolicy",
)?;
let policy = self.ctx.control.get_ref_name_policy(&path)?;
Ok(Response::new(GetRefNamePolicyResponse {
policy: Some(policy),
}))
}
async fn set_ref_name_policy(
&self,
request: Request<SetRefNamePolicyRequest>,
) -> Result<Response<SetRefNamePolicyResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let request = request.into_inner();
let path = repo_path(request.repo, "repo")?;
self.ctx.require(
&caller,
Access::repo(&path),
GrantLevel::Admin,
"Policy.SetRefNamePolicy",
)?;
let policy = require(request.policy, "policy")?;
let stored = self.ctx.control.set_ref_name_policy(&path, policy)?;
self.ctx.account_event(
self.ctx.account_of_namespace(&path.namespace),
crate::enterprise::AccountAuditEvent::by(
&caller,
"protection.ref_names",
format!("{}/{}", path.namespace, path.repo),
),
);
Ok(Response::new(SetRefNamePolicyResponse {
policy: Some(stored),
}))
}
async fn list_gates(
&self,
request: Request<ListGatesRequest>,
) -> Result<Response<ListGatesResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let path = repo_path(request.into_inner().repo, "repo")?;
self.ctx.require(
&caller,
Access::repo(&path),
GrantLevel::Read,
"Policy.ListGates",
)?;
self.ctx.control.get_repo(&path)?;
let protections = self.ctx.control.get_protection(&path, None)?;
let ref_policy = self.ctx.control.get_ref_name_policy(&path)?;
let ref_policy_configured =
!ref_policy.allow_patterns.is_empty() || !ref_policy.deny_patterns.is_empty();
Ok(Response::new(ListGatesResponse {
gates: vec![
GateInfo {
name: "branch-protection".into(),
description: "no force-push, no delete, required signature, allowed \
principals, per ref pattern"
.into(),
enabled: !protections.is_empty(),
},
GateInfo {
name: "ref-name-policy".into(),
description: "a pattern allowlist/denylist over ref names".into(),
enabled: ref_policy_configured,
},
GateInfo {
name: "signed-commits".into(),
description: "verify gpg.format=ssh signatures against the pusher's \
registered keys"
.into(),
enabled: protections.iter().any(|p| p.require_signature),
},
GateInfo {
name: "size-limits".into(),
description: "reject an oversized blob or an oversized push before it lands"
.into(),
enabled: protections
.iter()
.any(|p| p.max_object_bytes > 0 || p.max_objects_per_push > 0),
},
],
}))
}
}