OXYGENHUB

Coverage / crates/hub/src/auth/roles.rs 90.0%

72 of 80 lines run, 11 of 24 functions · run #536 on main · run missed

1
//! Roles on repositories and organisations (hub spec §5.2, §5.5). The hub's
2
//! roles decide who may reach a repository and attempt a push; the
3
//! repository's trust policy decides which keys may move which references.
4
5
use crate::db::{self, accounts::Account, repos::RepoRow, Pool};
6
use hub_api::Visibility;
7
8
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
9
#[repr(u8)]
10
pub enum Role {
11
    Read = 1,
12
    Triage = 2,
13
    Write = 3,
14
    Maintain = 4,
15
    Admin = 5,
16
}
17
18
impl Role {
19109×
    pub fn parse(s: &str) -> Option<Role> {
20109×
        Some(match s {
21109×
            "read" => Role::Read,
22108×
            "triage" => Role::Triage,
23108×
            "write" => Role::Write,
2464×
            "maintain" => Role::Maintain,
250×
            "admin" => Role::Admin,
260×
            _ => return None,
27
        })
28109×
    }
29
3015×
    pub fn as_str(self) -> &'static str {
3115×
        match self {
320×
            Role::Read => "read",
330×
            Role::Triage => "triage",
3412×
            Role::Write => "write",
353×
            Role::Maintain => "maintain",
360×
            Role::Admin => "admin",
37
        }
3815×
    }
39
40
    pub const ALL: [Role; 5] = [Role::Read, Role::Triage, Role::Write, Role::Maintain, Role::Admin];
41
}
42
43
/// What a request wants to do with a repository.
44
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45
pub enum Access {
46
    Read,
47
    Write,
48
    Maintain,
49
    Admin,
50
}
51
52
impl Access {
533836×
    fn needs(self) -> Role {
543836×
        match self {
552160×
            Access::Read => Role::Read,
561039×
            Access::Write => Role::Write,
57591×
            Access::Maintain => Role::Maintain,
5846×
            Access::Admin => Role::Admin,
59
        }
603836×
    }
61
}
62
63
/// The role `account` (or nobody) holds on `repo`, from ownership,
64
/// organisation membership, collaborator entries and visibility.
651636×
pub async fn role_for(pool: &Pool, account: Option<&Account>, repo: &RepoRow) -> sqlx::Result<Option<Role>> {
66
    // A drop (#279): anyone with its address reads it, a secret one too
67
    // (secret only keeps it off lists); only its owner writes. A site
68
    // administrator may delete it (`drops::may_delete`), nothing more.
691636×
    if repo.is_drop() {
7024×
        return Ok(match account {
7154×
            Some(a) if !a.blocked() && a.username.eq_ignore_ascii_case(&repo.owner) => Some(Role::Admin),
7224×
            Some(a) if a.blocked() => None,
7352×
            _ => Some(Role::Read),
74
        });
751554×
    }
761554×
    let visibility = repo.visibility();
771554×
    let Some(account) = account else {
78480×
        return Ok(if visibility == Visibility::Public { Some(Role::Read) } else { None });
79
    };
801074×
    if account.blocked() {
810×
        return Ok(None);
821074×
    }
831074×
    if account.is_admin || account.username.eq_ignore_ascii_case(&repo.owner) {
84894×
        return Ok(Some(Role::Admin));
85180×
    }
86180×
    let mut best: Option<Role> = None;
87180×
    if let Some(org) = db::accounts::org_by_name(pool, &repo.owner).await? {
883×
        match db::accounts::org_role(pool, org.id, account.id).await?.as_deref() {
891×
            Some("owner") => return Ok(Some(Role::Admin)),
901×
            Some(_) => best = Role::parse(&org.default_role),
912×
            None => {}
92
        }
93177×
    }
94180×
    if let Some(r) = db::accounts::collaborator_role(pool, repo.id, account.id).await?.as_deref().and_then(Role::parse) {
9541×
        best = Some(best.map_or(r, |b| b.max(r)));
96139×
    }
97180×
    if best.is_none() && visibility != Visibility::Private {
98132×
        best = Some(Role::Read);
99132×
    }
100180×
    Ok(best)
1011636×
}
102
103
/// Whether `role` permits `access`.
1043856×
pub fn allows(role: Option<Role>, access: Access) -> bool {
1053856×
    role.is_some_and(|r| r >= access.needs())
1063856×
}
107
108
/// Whether an account may create repositories under `owner`.
10940×
pub async fn may_create_under(pool: &Pool, account: &Account, owner: &str) -> sqlx::Result<bool> {
11040×
    if account.blocked() {
1110×
        return Ok(false);
11240×
    }
11340×
    if account.username.eq_ignore_ascii_case(owner) || account.is_admin {
11437×
        return Ok(true);
1153×
    }
1163×
    match db::accounts::org_by_name(pool, owner).await? {
1171×
        Some(org) => Ok(db::accounts::org_role(pool, org.id, account.id).await?.is_some()),
1182×
        None => Ok(false),
119
    }
12040×
}
121
122
/// Whether an account administers an organisation.
12341×
pub async fn is_org_owner(pool: &Pool, account: &Account, org_id: i64) -> sqlx::Result<bool> {
12441×
    if account.is_admin {
12534×
        return Ok(true);
1267×
    }
1277×
    Ok(db::accounts::org_role(pool, org_id, account.id).await?.as_deref() == Some("owner"))
12841×
}