//! Roles on repositories and organisations (hub spec §5.2, §5.5). The hub's //! roles decide who may reach a repository and attempt a push; the //! repository's trust policy decides which keys may move which references. use crate::db::{self, accounts::Account, repos::RepoRow, Pool}; use hub_api::Visibility; #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] #[repr(u8)] pub enum Role { Read = 1, Triage = 2, Write = 3, Maintain = 4, Admin = 5, } impl Role { pub fn parse(s: &str) -> Option { Some(match s { "read" => Role::Read, "triage" => Role::Triage, "write" => Role::Write, "maintain" => Role::Maintain, "admin" => Role::Admin, _ => return None, }) } pub fn as_str(self) -> &'static str { match self { Role::Read => "read", Role::Triage => "triage", Role::Write => "write", Role::Maintain => "maintain", Role::Admin => "admin", } } pub const ALL: [Role; 5] = [Role::Read, Role::Triage, Role::Write, Role::Maintain, Role::Admin]; } /// What a request wants to do with a repository. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Access { Read, Write, Maintain, Admin, } impl Access { fn needs(self) -> Role { match self { Access::Read => Role::Read, Access::Write => Role::Write, Access::Maintain => Role::Maintain, Access::Admin => Role::Admin, } } } /// The role `account` (or nobody) holds on `repo`, from ownership, /// organisation membership, collaborator entries and visibility. pub async fn role_for(pool: &Pool, account: Option<&Account>, repo: &RepoRow) -> sqlx::Result> { // A drop (#279): anyone with its address reads it, a secret one too // (secret only keeps it off lists); only its owner writes. A site // administrator may delete it (`drops::may_delete`), nothing more. if repo.is_drop() { return Ok(match account { Some(a) if !a.blocked() && a.username.eq_ignore_ascii_case(&repo.owner) => Some(Role::Admin), Some(a) if a.blocked() => None, _ => Some(Role::Read), }); } let visibility = repo.visibility(); let Some(account) = account else { return Ok(if visibility == Visibility::Public { Some(Role::Read) } else { None }); }; if account.blocked() { return Ok(None); } if account.is_admin || account.username.eq_ignore_ascii_case(&repo.owner) { return Ok(Some(Role::Admin)); } let mut best: Option = None; if let Some(org) = db::accounts::org_by_name(pool, &repo.owner).await? { match db::accounts::org_role(pool, org.id, account.id).await?.as_deref() { Some("owner") => return Ok(Some(Role::Admin)), Some(_) => best = Role::parse(&org.default_role), None => {} } } if let Some(r) = db::accounts::collaborator_role(pool, repo.id, account.id).await?.as_deref().and_then(Role::parse) { best = Some(best.map_or(r, |b| b.max(r))); } if best.is_none() && visibility != Visibility::Private { best = Some(Role::Read); } Ok(best) } /// Whether `role` permits `access`. pub fn allows(role: Option, access: Access) -> bool { role.is_some_and(|r| r >= access.needs()) } /// Whether an account may create repositories under `owner`. pub async fn may_create_under(pool: &Pool, account: &Account, owner: &str) -> sqlx::Result { if account.blocked() { return Ok(false); } if account.username.eq_ignore_ascii_case(owner) || account.is_admin { return Ok(true); } match db::accounts::org_by_name(pool, owner).await? { Some(org) => Ok(db::accounts::org_role(pool, org.id, account.id).await?.is_some()), None => Ok(false), } } /// Whether an account administers an organisation. pub async fn is_org_owner(pool: &Pool, account: &Account, org_id: i64) -> sqlx::Result { if account.is_admin { return Ok(true); } Ok(db::accounts::org_role(pool, org_id, account.id).await?.as_deref() == Some("owner")) }