use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Right {
#[serde(rename = "git.ns.admin")]
NsAdmin,
#[serde(rename = "git.repo.create")]
RepoCreate,
#[serde(rename = "git.repo.own")]
RepoOwn,
#[serde(rename = "git.repo.maintain")]
RepoMaintain,
#[serde(rename = "git.commit.sign")]
CommitSign,
}
impl Right {
pub const ALL: [Right; 5] = [
Right::NsAdmin,
Right::RepoCreate,
Right::RepoOwn,
Right::RepoMaintain,
Right::CommitSign,
];
pub fn action(self) -> &'static str {
match self {
Right::NsAdmin => "git.ns.admin",
Right::RepoCreate => "git.repo.create",
Right::RepoOwn => "git.repo.own",
Right::RepoMaintain => "git.repo.maintain",
Right::CommitSign => "git.commit.sign",
}
}
pub fn from_action(action: &str) -> Option<Right> {
Right::ALL.into_iter().find(|r| r.action() == action)
}
fn bit(self) -> u8 {
1 << (self as u8)
}
}
impl fmt::Display for Right {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.action())
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct EffectiveRights(u8);
impl EffectiveRights {
pub const NONE: EffectiveRights = EffectiveRights(0);
pub fn from_granted(granted: impl IntoIterator<Item = Right>) -> Self {
let mut rights = EffectiveRights::NONE;
for right in granted {
rights.insert(right);
}
rights
}
pub fn insert(&mut self, right: Right) {
self.0 |= right.bit();
match right {
Right::NsAdmin => {
self.insert(Right::RepoCreate);
self.insert(Right::RepoOwn);
}
Right::RepoOwn => self.insert(Right::RepoMaintain),
Right::RepoMaintain => self.insert(Right::CommitSign),
Right::RepoCreate | Right::CommitSign => {}
}
}
pub fn holds(self, right: Right) -> bool {
self.0 & right.bit() != 0
}
pub fn is_empty(self) -> bool {
self.0 == 0
}
pub fn iter(self) -> impl Iterator<Item = Right> {
Right::ALL.into_iter().filter(move |r| self.holds(*r))
}
pub fn repo_tier(self) -> Option<Right> {
[Right::RepoOwn, Right::RepoMaintain, Right::CommitSign]
.into_iter()
.find(|r| self.holds(*r))
}
}
impl Serialize for EffectiveRights {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_seq(self.iter())
}
}
impl<'de> Deserialize<'de> for EffectiveRights {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
Ok(EffectiveRights::from_granted(Vec::<Right>::deserialize(d)?))
}
}
#[derive(
Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum ForgeRole {
#[default]
None,
Read,
Triage,
Write,
Maintain,
Admin,
}
impl fmt::Display for ForgeRole {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
ForgeRole::None => "none",
ForgeRole::Read => "read",
ForgeRole::Triage => "triage",
ForgeRole::Write => "write",
ForgeRole::Maintain => "maintain",
ForgeRole::Admin => "admin",
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct RoleMap {
pub own: ForgeRole,
pub maintain: ForgeRole,
pub commit: ForgeRole,
}
impl Default for RoleMap {
fn default() -> Self {
RoleMap {
own: ForgeRole::Admin,
maintain: ForgeRole::Maintain,
commit: ForgeRole::None,
}
}
}
impl RoleMap {
pub fn with_committer_write() -> Self {
RoleMap {
commit: ForgeRole::Write,
..RoleMap::default()
}
}
pub fn requested(&self, rights: EffectiveRights) -> ForgeRole {
match rights.repo_tier() {
Some(Right::RepoOwn) => self.own,
Some(Right::RepoMaintain) => self.maintain,
Some(Right::CommitSign) => self.commit,
_ => ForgeRole::None,
}
}
}
pub fn collapse_to_ladder(requested: ForgeRole, ladder: &[ForgeRole]) -> ForgeRole {
ladder
.iter()
.copied()
.filter(|level| *level <= requested)
.max()
.unwrap_or(ForgeRole::None)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn implication_closes_own_to_commit_and_admin_to_own() {
let own = EffectiveRights::from_granted([Right::RepoOwn]);
assert!(own.holds(Right::RepoMaintain) && own.holds(Right::CommitSign));
assert!(!own.holds(Right::NsAdmin) && !own.holds(Right::RepoCreate));
let admin = EffectiveRights::from_granted([Right::NsAdmin]);
assert_eq!(admin.iter().count(), 5);
let commit = EffectiveRights::from_granted([Right::CommitSign]);
assert_eq!(commit.iter().collect::<Vec<_>>(), vec![Right::CommitSign]);
assert_eq!(commit.repo_tier(), Some(Right::CommitSign));
let create = EffectiveRights::from_granted([Right::RepoCreate]);
assert_eq!(create.repo_tier(), None);
}
#[test]
fn actions_round_trip() {
for r in Right::ALL {
assert_eq!(Right::from_action(r.action()), Some(r));
}
assert_eq!(Right::from_action("vtc.member"), None);
let json =
serde_json::to_string(&EffectiveRights::from_granted([Right::RepoMaintain])).unwrap();
assert_eq!(json, r#"["git.repo.maintain","git.commit.sign"]"#);
}
#[test]
fn default_map_matches_the_org_projection() {
let map = RoleMap::default();
let r = |x| EffectiveRights::from_granted([x]);
assert_eq!(map.requested(r(Right::NsAdmin)), ForgeRole::Admin);
assert_eq!(map.requested(r(Right::RepoOwn)), ForgeRole::Admin);
assert_eq!(map.requested(r(Right::RepoMaintain)), ForgeRole::Maintain);
assert_eq!(map.requested(r(Right::CommitSign)), ForgeRole::None);
assert_eq!(
RoleMap::with_committer_write().requested(r(Right::CommitSign)),
ForgeRole::Write
);
assert_eq!(map.requested(EffectiveRights::NONE), ForgeRole::None);
}
#[test]
fn collapsing_rounds_down_never_up() {
let forgejo = [ForgeRole::Read, ForgeRole::Write, ForgeRole::Admin];
assert_eq!(
collapse_to_ladder(ForgeRole::Maintain, &forgejo),
ForgeRole::Write
);
assert_eq!(
collapse_to_ladder(ForgeRole::Admin, &forgejo),
ForgeRole::Admin
);
assert_eq!(
collapse_to_ladder(ForgeRole::Triage, &forgejo),
ForgeRole::Read
);
let personal = [ForgeRole::Write];
assert_eq!(
collapse_to_ladder(ForgeRole::Admin, &personal),
ForgeRole::Write
);
assert_eq!(
collapse_to_ladder(ForgeRole::Read, &personal),
ForgeRole::None
);
assert_eq!(
collapse_to_ladder(ForgeRole::None, &personal),
ForgeRole::None
);
}
}