crates/gunnar-ui-server/src/api/rights.rs
//! **What THIS person may do on one repository** — gunnar's answer, for the
//! pages to draw from (owner order 2026-10-18).
//!
//! The console role is the SERVER-wide ladder, and every ordinary person is a
//! viewer — the owner of a repository included. Whether they may open an
//! issue, merge a pull request or edit branch protection on THIS repository is
//! a per-repository fact that only gunnar holds: its grants (owner, admin or
//! write of the repository or its namespace), its groups, the account gate.
//! This asks `Access.Check` AS THE PERSON at Read, Write and Admin and says
//! what came back. Nothing is decided here; each verb is signed as the same
//! person and gunnar decides it again.
use axum::extract::{Path, State};
use axum::Json;
use serde::Serialize;
use crate::api::{Actor, ApiResult};
use crate::app::AppState;
use crate::auth::middleware::Caller;
use crate::pb;
/// `GET /api/repos/{namespace}/{name}/rights`.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
pub struct Rights {
pub read: bool,
pub write: bool,
pub admin: bool,
}
/// A GET, so a viewer's to ask (`role_floor`); not on the anonymous
/// allow-list, so a visitor is refused before it. A session gunnar cannot act
/// as holds no right: all three false, never an error the page must draw.
pub async fn read(
State(state): State<AppState>,
caller: Option<axum::Extension<Caller>>,
Path((namespace, name)): Path<(String, String)>,
) -> ApiResult<Rights> {
let Ok(actor) = Actor::of(&state, caller.as_ref().map(|c| &c.0)).await else {
return Ok(Json(Rights::default()));
};
let ask = |level| crate::api::may(&actor.gunnar, &namespace, &name, level);
let read = ask(pb::GrantLevel::Read).await?.0;
let write = ask(pb::GrantLevel::Write).await?.0;
let admin = ask(pb::GrantLevel::Admin).await?.0;
Ok(Json(Rights { read, write, admin }))
}