gunnar-grpc 3.0.0

The tonic service implementations and the event bus behind gunnar's gRPC control plane.
Documentation
//! `Policy`, the pre-receive gate's configuration.
//!
//! This service only *stores* the rules. `gunnar-policy` runs them between
//! "pack is indexed" and "refs are applied", and a rejection there becomes a
//! `PushRejected` event (the event stream). Measure, never assert applies
//! hardest to the gates themselves: every gate must be seen to REJECT a crafted
//! bad push before it is trusted to allow a good one. That test belongs with the
//! gate, not here.

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")?;
        // Turning the pre-receive gate off for a ref is an ACL act, not a
        // push: Admin, never Write.
        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",
        )?;
        // The repo must exist; otherwise this would answer for a repo that
        // does not, which is the shape of a lie a UI would render.
        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();

        // The v1 gate set of the pre-receive gate. `enabled` reflects whether the repo actually
        // has configuration for that gate. A gate with nothing configured
        // cannot reject anything, and saying otherwise would misreport the
        // server's real behaviour.
        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),
                },
            ],
        }))
    }
}