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 {
held: u8,
projectable: u8,
}
impl EffectiveRights {
pub const NONE: EffectiveRights = EffectiveRights {
held: 0,
projectable: 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) {
let closure = Self::closure(right);
self.held |= closure;
match right {
Right::NsAdmin | Right::RepoCreate => {}
Right::RepoOwn | Right::RepoMaintain | Right::CommitSign => {
self.projectable |= closure;
}
}
}
fn closure(right: Right) -> u8 {
right.bit()
| match right {
Right::NsAdmin => Self::closure(Right::RepoCreate) | Self::closure(Right::RepoOwn),
Right::RepoOwn => Self::closure(Right::RepoMaintain),
Right::RepoMaintain => Self::closure(Right::CommitSign),
Right::RepoCreate | Right::CommitSign => 0,
}
}
pub fn holds(self, right: Right) -> bool {
self.held & right.bit() != 0
}
pub fn is_empty(self) -> bool {
self.held == 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> {
Self::tier(self.held)
}
pub fn forge_tier(self) -> Option<Right> {
Self::tier(self.projectable)
}
pub fn granted(self) -> Vec<Right> {
let mut out = Vec::new();
if self.holds(Right::NsAdmin) {
out.push(Right::NsAdmin);
} else if self.holds(Right::RepoCreate) {
out.push(Right::RepoCreate);
}
out.extend(self.forge_tier());
out
}
fn tier(bits: u8) -> Option<Right> {
[Right::RepoOwn, Right::RepoMaintain, Right::CommitSign]
.into_iter()
.find(|r| bits & r.bit() != 0)
}
}
impl Serialize for EffectiveRights {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_seq(self.granted())
}
}
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", try_from = "RawRoleMap")]
pub struct RoleMap {
own: ForgeRole,
maintain: ForgeRole,
commit: ForgeRole,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct RawRoleMap {
own: ForgeRole,
maintain: ForgeRole,
commit: ForgeRole,
}
impl TryFrom<RawRoleMap> for RoleMap {
type Error = String;
fn try_from(r: RawRoleMap) -> Result<Self, String> {
RoleMap::new(r.own, r.maintain, r.commit)
}
}
impl Default for RoleMap {
fn default() -> Self {
RoleMap {
own: ForgeRole::Admin,
maintain: ForgeRole::Maintain,
commit: ForgeRole::None,
}
}
}
impl RoleMap {
pub const MAX_COMMIT: ForgeRole = ForgeRole::Write;
pub fn new(own: ForgeRole, maintain: ForgeRole, commit: ForgeRole) -> Result<Self, String> {
for (tier, role) in [("a maintainer", maintain), ("a committer", commit)] {
if role >= ForgeRole::Admin {
return Err(format!(
"{tier} may not get `{role}`: only an owner (`own`) may map to the forge's \
administrator role, since maintain and commit are rights their holder may \
grant themselves"
));
}
}
if maintain > own {
return Err(format!(
"a maintainer (`{maintain}`) may not get more than an owner (`{own}`)"
));
}
if commit > maintain {
return Err(format!(
"a committer (`{commit}`) may not get more than a maintainer (`{maintain}`)"
));
}
if commit > Self::MAX_COMMIT {
return Err(format!(
"a committer may get at most `{}`, not `{commit}`: the check decides whose \
commits land, and merging is a maintainer's",
Self::MAX_COMMIT
));
}
Ok(RoleMap {
own,
maintain,
commit,
})
}
pub fn own(&self) -> ForgeRole {
self.own
}
pub fn maintain(&self) -> ForgeRole {
self.maintain
}
pub fn commit(&self) -> ForgeRole {
self.commit
}
pub fn with_committer_write() -> Self {
RoleMap {
commit: ForgeRole::Write,
..RoleMap::default()
}
}
pub fn requested(&self, rights: EffectiveRights) -> ForgeRole {
match rights.forge_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);
assert_eq!(admin.repo_tier(), Some(Right::RepoOwn));
assert_eq!(admin.forge_tier(), None, "ns.admin never projects");
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"]"#);
}
#[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::None);
assert_eq!(map.requested(r(Right::RepoCreate)), ForgeRole::None);
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 a_namespace_admin_gets_no_forge_role_under_any_map() {
let admin = EffectiveRights::from_granted([Right::NsAdmin]);
let everything =
RoleMap::new(ForgeRole::Admin, ForgeRole::Maintain, ForgeRole::Write).unwrap();
for map in [
RoleMap::default(),
RoleMap::with_committer_write(),
everything,
] {
assert_eq!(map.requested(admin), ForgeRole::None);
}
let both = EffectiveRights::from_granted([Right::NsAdmin, Right::RepoMaintain]);
assert!(both.holds(Right::RepoOwn));
assert_eq!(RoleMap::default().requested(both), ForgeRole::Maintain);
}
#[test]
fn serde_round_trips_are_the_identity() {
use Right::*;
let cases: &[(&[Right], &str)] = &[
(&[], "[]"),
(&[NsAdmin], r#"["git.ns.admin"]"#),
(&[NsAdmin, RepoOwn], r#"["git.ns.admin","git.repo.own"]"#),
(
&[NsAdmin, RepoMaintain],
r#"["git.ns.admin","git.repo.maintain"]"#,
),
(&[NsAdmin, RepoCreate], r#"["git.ns.admin"]"#),
(
&[RepoCreate, CommitSign],
r#"["git.repo.create","git.commit.sign"]"#,
),
(&[RepoOwn, RepoMaintain, CommitSign], r#"["git.repo.own"]"#),
];
for (granted, want) in cases {
let x = EffectiveRights::from_granted(granted.iter().copied());
let json = serde_json::to_string(&x).unwrap();
assert_eq!(json, *want, "{granted:?}");
let back: EffectiveRights = serde_json::from_str(&json).unwrap();
assert_eq!(back, x, "{granted:?}");
assert_eq!(back.forge_tier(), x.forge_tier(), "{granted:?}");
assert_eq!(EffectiveRights::from_granted(x.granted()), x);
}
let admin: EffectiveRights = serde_json::from_str(
&serde_json::to_string(&EffectiveRights::from_granted([NsAdmin])).unwrap(),
)
.unwrap();
assert_eq!(RoleMap::default().requested(admin), ForgeRole::None);
assert_ne!(
EffectiveRights::from_granted([NsAdmin]),
EffectiveRights::from_granted([NsAdmin, RepoOwn])
);
assert_eq!(
EffectiveRights::from_granted([RepoOwn]),
EffectiveRights::from_granted([RepoOwn, CommitSign])
);
}
#[test]
fn a_role_map_is_ordered_and_committers_stop_at_write() {
use ForgeRole::*;
assert!(RoleMap::new(Admin, Maintain, Write).is_ok());
assert!(RoleMap::new(Admin, Write, Write).is_ok());
assert!(RoleMap::new(Write, Write, None).is_ok());
assert!(RoleMap::new(Maintain, Write, None).is_ok());
assert!(RoleMap::new(Write, Maintain, None).is_err());
assert!(RoleMap::new(Admin, Write, Maintain).is_err());
let ok: RoleMap =
serde_json::from_str(r#"{"own":"admin","maintain":"write","commit":"write"}"#).unwrap();
assert_eq!(ok.maintain(), Write);
for bad in [
r#"{"own":"write","maintain":"maintain","commit":"none"}"#,
r#"{"own":"admin","maintain":"write","commit":"maintain"}"#,
r#"{"own":"admin","maintain":"maintain","commit":"none","nsAdmin":"admin"}"#,
] {
assert!(serde_json::from_str::<RoleMap>(bad).is_err(), "{bad}");
}
}
#[test]
fn only_an_owner_may_map_to_admin() {
use ForgeRole::*;
for (own, maintain, commit) in [
(Admin, Admin, None),
(Admin, Admin, Write),
(Admin, Admin, Admin),
(Admin, Maintain, Admin),
(Admin, Write, Admin),
] {
let err = RoleMap::new(own, maintain, commit).unwrap_err();
assert!(
err.contains("only an owner"),
"{own}/{maintain}/{commit}: {err}"
);
}
for bad in [
r#"{"own":"admin","maintain":"admin","commit":"none"}"#,
r#"{"own":"admin","maintain":"admin","commit":"write"}"#,
r#"{"own":"admin","maintain":"maintain","commit":"admin"}"#,
] {
let err = serde_json::from_str::<RoleMap>(bad)
.unwrap_err()
.to_string();
assert!(err.contains("only an owner"), "{bad}: {err}");
}
for map in [RoleMap::default(), RoleMap::with_committer_write()] {
assert!(map.maintain() < Admin && map.commit() < Admin);
}
assert_eq!(RoleMap::default().own(), Admin);
}
#[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
);
}
}