OXYGENHUB
◆ main oxygenhub / crates / hub / src / auth / roles.rs
4.2 KiB · Rust · last changed Drops: per-user gists, each a small hidden Oxygen repository (#279) 15 days ago source blame history raw
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 {
19
    pub fn parse(s: &str) -> Option<Role> {
20
        Some(match s {
21
            "read" => Role::Read,
22
            "triage" => Role::Triage,
23
            "write" => Role::Write,
24
            "maintain" => Role::Maintain,
25
            "admin" => Role::Admin,
26
            _ => return None,
27
        })
28
    }
29
30
    pub fn as_str(self) -> &'static str {
31
        match self {
32
            Role::Read => "read",
33
            Role::Triage => "triage",
34
            Role::Write => "write",
35
            Role::Maintain => "maintain",
36
            Role::Admin => "admin",
37
        }
38
    }
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 {
53
    fn needs(self) -> Role {
54
        match self {
55
            Access::Read => Role::Read,
56
            Access::Write => Role::Write,
57
            Access::Maintain => Role::Maintain,
58
            Access::Admin => Role::Admin,
59
        }
60
    }
61
}
62
63
/// The role `account` (or nobody) holds on `repo`, from ownership,
64
/// organisation membership, collaborator entries and visibility.
65
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.
69
    if repo.is_drop() {
70
        return Ok(match account {
71
            Some(a) if !a.blocked() && a.username.eq_ignore_ascii_case(&repo.owner) => Some(Role::Admin),
72
            Some(a) if a.blocked() => None,
73
            _ => Some(Role::Read),
74
        });
75
    }
76
    let visibility = repo.visibility();
77
    let Some(account) = account else {
78
        return Ok(if visibility == Visibility::Public { Some(Role::Read) } else { None });
79
    };
80
    if account.blocked() {
81
        return Ok(None);
82
    }
83
    if account.is_admin || account.username.eq_ignore_ascii_case(&repo.owner) {
84
        return Ok(Some(Role::Admin));
85
    }
86
    let mut best: Option<Role> = None;
87
    if let Some(org) = db::accounts::org_by_name(pool, &repo.owner).await? {
88
        match db::accounts::org_role(pool, org.id, account.id).await?.as_deref() {
89
            Some("owner") => return Ok(Some(Role::Admin)),
90
            Some(_) => best = Role::parse(&org.default_role),
91
            None => {}
92
        }
93
    }
94
    if let Some(r) = db::accounts::collaborator_role(pool, repo.id, account.id).await?.as_deref().and_then(Role::parse) {
95
        best = Some(best.map_or(r, |b| b.max(r)));
96
    }
97
    if best.is_none() && visibility != Visibility::Private {
98
        best = Some(Role::Read);
99
    }
100
    Ok(best)
101
}
102
103
/// Whether `role` permits `access`.
104
pub fn allows(role: Option<Role>, access: Access) -> bool {
105
    role.is_some_and(|r| r >= access.needs())
106
}
107
108
/// Whether an account may create repositories under `owner`.
109
pub async fn may_create_under(pool: &Pool, account: &Account, owner: &str) -> sqlx::Result<bool> {
110
    if account.blocked() {
111
        return Ok(false);
112
    }
113
    if account.username.eq_ignore_ascii_case(owner) || account.is_admin {
114
        return Ok(true);
115
    }
116
    match db::accounts::org_by_name(pool, owner).await? {
117
        Some(org) => Ok(db::accounts::org_role(pool, org.id, account.id).await?.is_some()),
118
        None => Ok(false),
119
    }
120
}
121
122
/// Whether an account administers an organisation.
123
pub async fn is_org_owner(pool: &Pool, account: &Account, org_id: i64) -> sqlx::Result<bool> {
124
    if account.is_admin {
125
        return Ok(true);
126
    }
127
    Ok(db::accounts::org_role(pool, org_id, account.id).await?.as_deref() == Some("owner"))
128
}