1use std::fmt;
10
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
15#[non_exhaustive]
16pub enum Right {
17 #[serde(rename = "git.ns.admin")]
19 NsAdmin,
20 #[serde(rename = "git.repo.create")]
22 RepoCreate,
23 #[serde(rename = "git.repo.own")]
25 RepoOwn,
26 #[serde(rename = "git.repo.maintain")]
28 RepoMaintain,
29 #[serde(rename = "git.commit.sign")]
32 CommitSign,
33}
34
35impl Right {
36 pub const ALL: [Right; 5] = [
38 Right::NsAdmin,
39 Right::RepoCreate,
40 Right::RepoOwn,
41 Right::RepoMaintain,
42 Right::CommitSign,
43 ];
44
45 pub fn action(self) -> &'static str {
47 match self {
48 Right::NsAdmin => "git.ns.admin",
49 Right::RepoCreate => "git.repo.create",
50 Right::RepoOwn => "git.repo.own",
51 Right::RepoMaintain => "git.repo.maintain",
52 Right::CommitSign => "git.commit.sign",
53 }
54 }
55
56 pub fn from_action(action: &str) -> Option<Right> {
59 Right::ALL.into_iter().find(|r| r.action() == action)
60 }
61
62 fn bit(self) -> u8 {
63 1 << (self as u8)
64 }
65}
66
67impl fmt::Display for Right {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 f.write_str(self.action())
70 }
71}
72
73#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
80pub struct EffectiveRights(u8);
81
82impl EffectiveRights {
83 pub const NONE: EffectiveRights = EffectiveRights(0);
85
86 pub fn from_granted(granted: impl IntoIterator<Item = Right>) -> Self {
88 let mut rights = EffectiveRights::NONE;
89 for right in granted {
90 rights.insert(right);
91 }
92 rights
93 }
94
95 pub fn insert(&mut self, right: Right) {
97 self.0 |= right.bit();
98 match right {
99 Right::NsAdmin => {
100 self.insert(Right::RepoCreate);
101 self.insert(Right::RepoOwn);
102 }
103 Right::RepoOwn => self.insert(Right::RepoMaintain),
104 Right::RepoMaintain => self.insert(Right::CommitSign),
105 Right::RepoCreate | Right::CommitSign => {}
106 }
107 }
108
109 pub fn holds(self, right: Right) -> bool {
111 self.0 & right.bit() != 0
112 }
113
114 pub fn is_empty(self) -> bool {
116 self.0 == 0
117 }
118
119 pub fn iter(self) -> impl Iterator<Item = Right> {
121 Right::ALL.into_iter().filter(move |r| self.holds(*r))
122 }
123
124 pub fn repo_tier(self) -> Option<Right> {
127 [Right::RepoOwn, Right::RepoMaintain, Right::CommitSign]
128 .into_iter()
129 .find(|r| self.holds(*r))
130 }
131}
132
133impl Serialize for EffectiveRights {
134 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
135 s.collect_seq(self.iter())
136 }
137}
138
139impl<'de> Deserialize<'de> for EffectiveRights {
140 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
141 Ok(EffectiveRights::from_granted(Vec::<Right>::deserialize(d)?))
142 }
143}
144
145#[derive(
152 Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
153)]
154#[serde(rename_all = "lowercase")]
155#[non_exhaustive]
156pub enum ForgeRole {
157 #[default]
159 None,
160 Read,
162 Triage,
164 Write,
166 Maintain,
168 Admin,
170}
171
172impl fmt::Display for ForgeRole {
173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 f.write_str(match self {
175 ForgeRole::None => "none",
176 ForgeRole::Read => "read",
177 ForgeRole::Triage => "triage",
178 ForgeRole::Write => "write",
179 ForgeRole::Maintain => "maintain",
180 ForgeRole::Admin => "admin",
181 })
182 }
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
192#[serde(rename_all = "camelCase")]
193#[non_exhaustive]
194pub struct RoleMap {
195 pub own: ForgeRole,
197 pub maintain: ForgeRole,
199 pub commit: ForgeRole,
203}
204
205impl Default for RoleMap {
206 fn default() -> Self {
207 RoleMap {
208 own: ForgeRole::Admin,
209 maintain: ForgeRole::Maintain,
210 commit: ForgeRole::None,
211 }
212 }
213}
214
215impl RoleMap {
216 pub fn with_committer_write() -> Self {
219 RoleMap {
220 commit: ForgeRole::Write,
221 ..RoleMap::default()
222 }
223 }
224
225 pub fn requested(&self, rights: EffectiveRights) -> ForgeRole {
227 match rights.repo_tier() {
228 Some(Right::RepoOwn) => self.own,
229 Some(Right::RepoMaintain) => self.maintain,
230 Some(Right::CommitSign) => self.commit,
231 _ => ForgeRole::None,
232 }
233 }
234}
235
236pub fn collapse_to_ladder(requested: ForgeRole, ladder: &[ForgeRole]) -> ForgeRole {
243 ladder
244 .iter()
245 .copied()
246 .filter(|level| *level <= requested)
247 .max()
248 .unwrap_or(ForgeRole::None)
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn implication_closes_own_to_commit_and_admin_to_own() {
257 let own = EffectiveRights::from_granted([Right::RepoOwn]);
258 assert!(own.holds(Right::RepoMaintain) && own.holds(Right::CommitSign));
259 assert!(!own.holds(Right::NsAdmin) && !own.holds(Right::RepoCreate));
260
261 let admin = EffectiveRights::from_granted([Right::NsAdmin]);
262 assert_eq!(admin.iter().count(), 5);
263
264 let commit = EffectiveRights::from_granted([Right::CommitSign]);
265 assert_eq!(commit.iter().collect::<Vec<_>>(), vec![Right::CommitSign]);
266 assert_eq!(commit.repo_tier(), Some(Right::CommitSign));
267
268 let create = EffectiveRights::from_granted([Right::RepoCreate]);
269 assert_eq!(create.repo_tier(), None);
270 }
271
272 #[test]
273 fn actions_round_trip() {
274 for r in Right::ALL {
275 assert_eq!(Right::from_action(r.action()), Some(r));
276 }
277 assert_eq!(Right::from_action("vtc.member"), None);
278 let json =
279 serde_json::to_string(&EffectiveRights::from_granted([Right::RepoMaintain])).unwrap();
280 assert_eq!(json, r#"["git.repo.maintain","git.commit.sign"]"#);
281 }
282
283 #[test]
284 fn default_map_matches_the_org_projection() {
285 let map = RoleMap::default();
286 let r = |x| EffectiveRights::from_granted([x]);
287 assert_eq!(map.requested(r(Right::NsAdmin)), ForgeRole::Admin);
288 assert_eq!(map.requested(r(Right::RepoOwn)), ForgeRole::Admin);
289 assert_eq!(map.requested(r(Right::RepoMaintain)), ForgeRole::Maintain);
290 assert_eq!(map.requested(r(Right::CommitSign)), ForgeRole::None);
291 assert_eq!(
292 RoleMap::with_committer_write().requested(r(Right::CommitSign)),
293 ForgeRole::Write
294 );
295 assert_eq!(map.requested(EffectiveRights::NONE), ForgeRole::None);
296 }
297
298 #[test]
299 fn collapsing_rounds_down_never_up() {
300 let forgejo = [ForgeRole::Read, ForgeRole::Write, ForgeRole::Admin];
301 assert_eq!(
302 collapse_to_ladder(ForgeRole::Maintain, &forgejo),
303 ForgeRole::Write
304 );
305 assert_eq!(
306 collapse_to_ladder(ForgeRole::Admin, &forgejo),
307 ForgeRole::Admin
308 );
309 assert_eq!(
310 collapse_to_ladder(ForgeRole::Triage, &forgejo),
311 ForgeRole::Read
312 );
313
314 let personal = [ForgeRole::Write];
315 assert_eq!(
316 collapse_to_ladder(ForgeRole::Admin, &personal),
317 ForgeRole::Write
318 );
319 assert_eq!(
320 collapse_to_ladder(ForgeRole::Read, &personal),
321 ForgeRole::None
322 );
323 assert_eq!(
324 collapse_to_ladder(ForgeRole::None, &personal),
325 ForgeRole::None
326 );
327 }
328}