Skip to main content

rns_git/
acl.rs

1use std::collections::{BTreeMap, HashMap};
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::process::Command;
5
6use crate::util::{parse_hex_16, validate_repo_name};
7use crate::{Error, Result};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Operation {
11    Read,
12    Write,
13    Create,
14    Stats,
15    Release,
16    Interact,
17    Propose,
18    Admin,
19}
20
21#[derive(Debug, Clone)]
22pub struct Access {
23    read: Rule,
24    write: Rule,
25    create: Rule,
26    stats: Rule,
27    release: Rule,
28    interact: Rule,
29    propose: Rule,
30    admin: Rule,
31    aliases: BTreeMap<String, [u8; 16]>,
32    blocked: Vec<[u8; 16]>,
33    repositories_dir: PathBuf,
34}
35
36#[derive(Debug, Clone)]
37enum Rule {
38    None,
39    All,
40    Identities(Vec<[u8; 16]>),
41}
42
43impl Access {
44    // Kept as a source-compatible public constructor; internal call sites use named config types.
45    #[allow(clippy::too_many_arguments)]
46    pub fn new(
47        read: &[String],
48        write: &[String],
49        create: &[String],
50        stats: &[String],
51        release: &[String],
52        interact: &[String],
53        admin: &[String],
54        repositories_dir: PathBuf,
55    ) -> Result<Self> {
56        Self::new_with_aliases(
57            read,
58            write,
59            create,
60            stats,
61            release,
62            interact,
63            admin,
64            repositories_dir,
65            BTreeMap::new(),
66        )
67    }
68
69    // Kept as a source-compatible public constructor; changing its shape would be breaking.
70    #[allow(clippy::too_many_arguments)]
71    pub fn new_with_aliases(
72        read: &[String],
73        write: &[String],
74        create: &[String],
75        stats: &[String],
76        release: &[String],
77        interact: &[String],
78        admin: &[String],
79        repositories_dir: PathBuf,
80        aliases: BTreeMap<String, [u8; 16]>,
81    ) -> Result<Self> {
82        Ok(Self {
83            read: Rule::parse(read, &aliases)?,
84            write: Rule::parse(write, &aliases)?,
85            create: Rule::parse(create, &aliases)?,
86            stats: Rule::parse(stats, &aliases)?,
87            release: Rule::parse(release, &aliases)?,
88            interact: Rule::parse(interact, &aliases)?,
89            propose: Rule::None,
90            admin: Rule::parse(admin, &aliases)?,
91            aliases,
92            blocked: Vec::new(),
93            repositories_dir,
94        })
95    }
96
97    pub fn with_propose(mut self, propose: &[String]) -> Result<Self> {
98        self.propose = Rule::parse(propose, &self.aliases)?;
99        Ok(self)
100    }
101
102    pub fn with_blocked_identities(mut self, blocked: Vec<[u8; 16]>) -> Self {
103        self.blocked = blocked;
104        self
105    }
106
107    pub fn allows(
108        &self,
109        op: Operation,
110        repository: &str,
111        identity: Option<&[u8; 16]>,
112    ) -> Result<bool> {
113        validate_repo_name(repository)?;
114        if identity.is_some_and(|identity| self.blocked.iter().any(|blocked| blocked == identity)) {
115            return Ok(false);
116        }
117        if op != Operation::Admin
118            && (self.repo_allowed(Operation::Admin, repository, identity)?
119                || self.admin.allows(identity))
120        {
121            return Ok(true);
122        }
123        if self.repo_allowed(op, repository, identity)? {
124            return Ok(true);
125        }
126        Ok(match op {
127            Operation::Read => self.read.allows(identity),
128            Operation::Write => self.write.allows(identity),
129            Operation::Create => self.create.allows(identity),
130            Operation::Stats => self.stats.allows(identity),
131            Operation::Release => self.release.allows(identity),
132            Operation::Interact => self.interact.allows(identity),
133            Operation::Propose => self.propose.allows(identity),
134            Operation::Admin => self.admin.allows(identity),
135        })
136    }
137
138    fn repo_allowed(
139        &self,
140        op: Operation,
141        repository: &str,
142        identity: Option<&[u8; 16]>,
143    ) -> Result<bool> {
144        for path in self.allowed_files(repository) {
145            if !path.exists() {
146                continue;
147            }
148            let rules = parse_allowed_file_with_aliases(&allowed_input(&path)?, &self.aliases)?;
149            let Some(rule) = rules.get(operation_key(op)) else {
150                continue;
151            };
152            if rule.allows(identity) {
153                return Ok(true);
154            }
155        }
156        Ok(false)
157    }
158
159    fn allowed_files(&self, repository: &str) -> Vec<PathBuf> {
160        let repo = self.repositories_dir.join(repository);
161        let mut out = vec![
162            repo.join(".allowed"),
163            self.repositories_dir.join(format!("{repository}.allowed")),
164        ];
165        if let Some(group) = repository.split('/').next() {
166            out.push(self.repositories_dir.join(group).join("group.allowed"));
167            out.push(self.repositories_dir.join(format!("{group}.allowed")));
168        }
169        out
170    }
171}
172
173pub(crate) fn allowed_input_allows(
174    input: &str,
175    op: Operation,
176    identity: Option<&[u8; 16]>,
177) -> Result<bool> {
178    allowed_input_allows_with_aliases(input, op, identity, &BTreeMap::new())
179}
180
181pub(crate) fn allowed_input_allows_with_aliases(
182    input: &str,
183    op: Operation,
184    identity: Option<&[u8; 16]>,
185    aliases: &BTreeMap<String, [u8; 16]>,
186) -> Result<bool> {
187    Ok(parse_allowed_file_with_aliases(input, aliases)?
188        .get(operation_key(op))
189        .is_some_and(|rule| rule.allows(identity)))
190}
191
192pub(crate) fn validate_allowed_input(input: &str) -> Result<()> {
193    validate_allowed_input_with_aliases(input, &BTreeMap::new())
194}
195
196pub(crate) fn validate_allowed_input_with_aliases(
197    input: &str,
198    aliases: &BTreeMap<String, [u8; 16]>,
199) -> Result<()> {
200    parse_allowed_file_with_aliases(input, aliases).map(|_| ())
201}
202
203fn allowed_input(path: &Path) -> Result<String> {
204    if is_executable_file(path)? {
205        let output = Command::new(path).output()?;
206        if !output.status.success() {
207            return Err(Error::msg(format!(
208                "allowed file {} exited with status {}",
209                path.display(),
210                output.status
211            )));
212        }
213        Ok(String::from_utf8_lossy(&output.stdout).into_owned())
214    } else {
215        Ok(fs::read_to_string(path)?)
216    }
217}
218
219#[cfg(unix)]
220fn is_executable_file(path: &Path) -> Result<bool> {
221    use std::os::unix::fs::PermissionsExt;
222
223    let metadata = fs::metadata(path)?;
224    Ok(metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
225}
226
227#[cfg(not(unix))]
228fn is_executable_file(path: &Path) -> Result<bool> {
229    Ok(fs::metadata(path)?.is_file() && false)
230}
231
232fn operation_key(op: Operation) -> &'static str {
233    match op {
234        Operation::Read => "read",
235        Operation::Write => "write",
236        Operation::Create => "create",
237        Operation::Stats => "stats",
238        Operation::Release => "release",
239        Operation::Interact => "interact",
240        Operation::Propose => "propose",
241        Operation::Admin => "admin",
242    }
243}
244
245impl Rule {
246    fn parse(values: &[String], aliases: &BTreeMap<String, [u8; 16]>) -> Result<Self> {
247        if values.iter().any(|v| v.eq_ignore_ascii_case("all")) {
248            return Ok(Rule::All);
249        }
250        let identities: Vec<[u8; 16]> = values
251            .iter()
252            .filter(|v| !v.eq_ignore_ascii_case("none"))
253            .map(|v| parse_identity_ref(v, aliases))
254            .collect::<Result<_>>()?;
255        if identities.is_empty() {
256            Ok(Rule::None)
257        } else {
258            Ok(Rule::Identities(identities))
259        }
260    }
261
262    fn allows(&self, identity: Option<&[u8; 16]>) -> bool {
263        match self {
264            Rule::None => false,
265            Rule::All => true,
266            Rule::Identities(allowed) => identity.is_some_and(|id| allowed.iter().any(|v| v == id)),
267        }
268    }
269}
270
271fn parse_identity_ref(value: &str, aliases: &BTreeMap<String, [u8; 16]>) -> Result<[u8; 16]> {
272    aliases
273        .get(value.trim())
274        .copied()
275        .map(Ok)
276        .unwrap_or_else(|| parse_hex_16(value))
277}
278
279fn parse_allowed_file_with_aliases(
280    input: &str,
281    aliases: &BTreeMap<String, [u8; 16]>,
282) -> Result<HashMap<String, Rule>> {
283    let mut values: HashMap<String, Vec<String>> = HashMap::new();
284    for raw in input.lines() {
285        let line = raw.split('#').next().unwrap_or("").trim();
286        if line.is_empty() {
287            continue;
288        }
289        let (key, value) = line
290            .split_once('=')
291            .or_else(|| line.split_once(':'))
292            .unwrap_or(("read", line));
293        let raw_key = key.trim().to_ascii_lowercase();
294        let key = match raw_key.as_str() {
295            "r" | "read" => "read",
296            "w" | "write" => "write",
297            "rw" | "readwrite" => "readwrite",
298            "c" | "create" => "create",
299            "s" | "stats" => "stats",
300            "rel" | "release" => "release",
301            "i" | "interact" => "interact",
302            "p" | "propose" => "propose",
303            "adm" | "admin" => "admin",
304            _ => return Err(Error::msg(format!("invalid permission \"{raw_key}\""))),
305        };
306        let targets = value.split(',').map(|v| v.trim().to_string());
307        if key == "readwrite" {
308            values
309                .entry("read".into())
310                .or_default()
311                .extend(targets.clone());
312            values.entry("write".into()).or_default().extend(targets);
313        } else {
314            values.entry(key.into()).or_default().extend(targets);
315        }
316    }
317    values
318        .into_iter()
319        .map(|(key, values)| Ok((key, Rule::parse(&values, aliases)?)))
320        .collect()
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn global_rules_allow_all_or_none() {
329        let access = Access::new(
330            &["all".into()],
331            &["none".into()],
332            &["none".into()],
333            &["all".into()],
334            &["none".into()],
335            &["none".into()],
336            &["none".into()],
337            PathBuf::from("."),
338        )
339        .unwrap();
340        assert!(access.allows(Operation::Read, "group/repo", None).unwrap());
341        assert!(!access.allows(Operation::Write, "group/repo", None).unwrap());
342        assert!(!access
343            .allows(Operation::Create, "group/repo", None)
344            .unwrap());
345        assert!(access.allows(Operation::Stats, "group/repo", None).unwrap());
346        assert!(!access
347            .allows(Operation::Release, "group/repo", None)
348            .unwrap());
349        assert!(!access
350            .allows(Operation::Interact, "group/repo", None)
351            .unwrap());
352        assert!(!access.allows(Operation::Admin, "group/repo", None).unwrap());
353    }
354
355    #[test]
356    fn global_permission_rules_resolve_identity_aliases() {
357        let mut aliases = BTreeMap::new();
358        aliases.insert(
359            "alice".into(),
360            [
361                0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
362                0xee, 0xff,
363            ],
364        );
365        let access = Access::new_with_aliases(
366            &["alice".into()],
367            &["none".into()],
368            &["none".into()],
369            &["none".into()],
370            &["none".into()],
371            &["none".into()],
372            &["none".into()],
373            PathBuf::from("."),
374            aliases,
375        )
376        .unwrap();
377
378        let alice = [
379            0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
380            0xee, 0xff,
381        ];
382        assert!(access
383            .allows(Operation::Read, "group/repo", Some(&alice))
384            .unwrap());
385    }
386
387    #[test]
388    fn blocked_identities_override_global_and_admin_permissions() {
389        let blocked = [
390            0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
391            0xee, 0xff,
392        ];
393        let access = Access::new(
394            &["all".into()],
395            &["all".into()],
396            &["all".into()],
397            &["all".into()],
398            &["all".into()],
399            &["all".into()],
400            &["all".into()],
401            PathBuf::from("."),
402        )
403        .unwrap()
404        .with_blocked_identities(vec![blocked]);
405
406        assert!(!access
407            .allows(Operation::Read, "group/repo", Some(&blocked))
408            .unwrap());
409        assert!(!access
410            .allows(Operation::Admin, "group/repo", Some(&blocked))
411            .unwrap());
412        assert!(access.allows(Operation::Read, "group/repo", None).unwrap());
413    }
414
415    #[test]
416    fn allowed_files_resolve_identity_aliases() {
417        let tmp = tempfile::tempdir().unwrap();
418        let repo = tmp.path().join("group/repo");
419        fs::create_dir_all(&repo).unwrap();
420        fs::write(repo.join(".allowed"), "write = alice\n").unwrap();
421        let mut aliases = BTreeMap::new();
422        aliases.insert(
423            "alice".into(),
424            [
425                0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
426                0xee, 0xff,
427            ],
428        );
429        let access = Access::new_with_aliases(
430            &["none".into()],
431            &["none".into()],
432            &["none".into()],
433            &["none".into()],
434            &["none".into()],
435            &["none".into()],
436            &["none".into()],
437            tmp.path().to_path_buf(),
438            aliases,
439        )
440        .unwrap();
441        let alice = [
442            0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
443            0xee, 0xff,
444        ];
445
446        assert!(access
447            .allows(Operation::Write, "group/repo", Some(&alice))
448            .unwrap());
449    }
450
451    #[test]
452    fn repo_allowed_file_can_grant_write() {
453        let tmp = tempfile::tempdir().unwrap();
454        let repo = tmp.path().join("group/repo");
455        fs::create_dir_all(&repo).unwrap();
456        fs::write(repo.join(".allowed"), "write = all\n").unwrap();
457        let access = Access::new(
458            &["none".into()],
459            &["none".into()],
460            &["none".into()],
461            &["none".into()],
462            &["none".into()],
463            &["none".into()],
464            &["none".into()],
465            tmp.path().into(),
466        )
467        .unwrap();
468        assert!(access.allows(Operation::Write, "group/repo", None).unwrap());
469    }
470
471    #[test]
472    fn repo_sidecar_allowed_file_can_grant_write() {
473        let tmp = tempfile::tempdir().unwrap();
474        fs::create_dir_all(tmp.path().join("group/repo")).unwrap();
475        fs::write(tmp.path().join("group/repo.allowed"), "write = all\n").unwrap();
476        let access = Access::new(
477            &["none".into()],
478            &["none".into()],
479            &["none".into()],
480            &["none".into()],
481            &["none".into()],
482            &["none".into()],
483            &["none".into()],
484            tmp.path().into(),
485        )
486        .unwrap();
487        assert!(access.allows(Operation::Write, "group/repo", None).unwrap());
488    }
489
490    #[test]
491    fn repo_allowed_file_can_grant_stats_with_long_or_short_key() {
492        let tmp = tempfile::tempdir().unwrap();
493        let repo = tmp.path().join("group/repo");
494        fs::create_dir_all(&repo).unwrap();
495        fs::write(repo.join(".allowed"), "stats = all\n").unwrap();
496        let access = Access::new(
497            &["none".into()],
498            &["none".into()],
499            &["none".into()],
500            &["none".into()],
501            &["none".into()],
502            &["none".into()],
503            &["none".into()],
504            tmp.path().into(),
505        )
506        .unwrap();
507        assert!(access.allows(Operation::Stats, "group/repo", None).unwrap());
508
509        fs::write(repo.join(".allowed"), "s = all\n").unwrap();
510        assert!(access.allows(Operation::Stats, "group/repo", None).unwrap());
511    }
512
513    #[test]
514    fn repo_allowed_file_can_grant_create_with_long_or_short_key() {
515        let tmp = tempfile::tempdir().unwrap();
516        let repo = tmp.path().join("group/repo");
517        fs::create_dir_all(&repo).unwrap();
518        fs::write(repo.join(".allowed"), "create = all\n").unwrap();
519        let access = Access::new(
520            &["none".into()],
521            &["none".into()],
522            &["none".into()],
523            &["none".into()],
524            &["none".into()],
525            &["none".into()],
526            &["none".into()],
527            tmp.path().into(),
528        )
529        .unwrap();
530        assert!(access
531            .allows(Operation::Create, "group/repo", None)
532            .unwrap());
533
534        fs::write(repo.join(".allowed"), "c = all\n").unwrap();
535        assert!(access
536            .allows(Operation::Create, "group/repo", None)
537            .unwrap());
538    }
539
540    #[test]
541    fn group_allowed_file_can_grant_create() {
542        let tmp = tempfile::tempdir().unwrap();
543        let group = tmp.path().join("group");
544        fs::create_dir_all(&group).unwrap();
545        fs::write(group.join("group.allowed"), "create = all\n").unwrap();
546        let access = Access::new(
547            &["none".into()],
548            &["none".into()],
549            &["none".into()],
550            &["none".into()],
551            &["none".into()],
552            &["none".into()],
553            &["none".into()],
554            tmp.path().into(),
555        )
556        .unwrap();
557        assert!(access
558            .allows(Operation::Create, "group/repo", None)
559            .unwrap());
560    }
561
562    #[test]
563    fn group_sidecar_allowed_file_can_grant_create() {
564        let tmp = tempfile::tempdir().unwrap();
565        fs::create_dir_all(tmp.path().join("group")).unwrap();
566        fs::write(tmp.path().join("group.allowed"), "create = all\n").unwrap();
567        let access = Access::new(
568            &["none".into()],
569            &["none".into()],
570            &["none".into()],
571            &["none".into()],
572            &["none".into()],
573            &["none".into()],
574            &["none".into()],
575            tmp.path().into(),
576        )
577        .unwrap();
578        assert!(access
579            .allows(Operation::Create, "group/repo", None)
580            .unwrap());
581    }
582
583    #[cfg(unix)]
584    #[test]
585    fn executable_allowed_file_uses_stdout_permissions() {
586        use std::os::unix::fs::PermissionsExt;
587
588        let tmp = tempfile::tempdir().unwrap();
589        let repo = tmp.path().join("group/repo");
590        fs::create_dir_all(&repo).unwrap();
591        let allowed = tmp.path().join("group/repo.allowed");
592        fs::write(&allowed, "#!/bin/sh\nprintf 'stats = all\\n'\n").unwrap();
593        let mut perms = fs::metadata(&allowed).unwrap().permissions();
594        perms.set_mode(0o755);
595        fs::set_permissions(&allowed, perms).unwrap();
596
597        let access = Access::new(
598            &["none".into()],
599            &["none".into()],
600            &["none".into()],
601            &["none".into()],
602            &["none".into()],
603            &["none".into()],
604            &["none".into()],
605            tmp.path().into(),
606        )
607        .unwrap();
608        assert!(access.allows(Operation::Stats, "group/repo", None).unwrap());
609    }
610
611    #[test]
612    fn group_allowed_file_can_grant_stats() {
613        let tmp = tempfile::tempdir().unwrap();
614        let group = tmp.path().join("group");
615        fs::create_dir_all(&group).unwrap();
616        fs::write(group.join("group.allowed"), "s = all\n").unwrap();
617        let access = Access::new(
618            &["none".into()],
619            &["none".into()],
620            &["none".into()],
621            &["none".into()],
622            &["none".into()],
623            &["none".into()],
624            &["none".into()],
625            tmp.path().into(),
626        )
627        .unwrap();
628        assert!(access.allows(Operation::Stats, "group/repo", None).unwrap());
629    }
630
631    #[test]
632    fn repo_allowed_file_can_grant_release_with_long_or_short_key() {
633        let tmp = tempfile::tempdir().unwrap();
634        let repo = tmp.path().join("group/repo");
635        fs::create_dir_all(&repo).unwrap();
636        fs::write(repo.join(".allowed"), "release = all\n").unwrap();
637        let access = Access::new(
638            &["none".into()],
639            &["none".into()],
640            &["none".into()],
641            &["none".into()],
642            &["none".into()],
643            &["none".into()],
644            &["none".into()],
645            tmp.path().into(),
646        )
647        .unwrap();
648        assert!(access
649            .allows(Operation::Release, "group/repo", None)
650            .unwrap());
651
652        fs::write(repo.join(".allowed"), "rel = all\n").unwrap();
653        assert!(access
654            .allows(Operation::Release, "group/repo", None)
655            .unwrap());
656    }
657
658    #[test]
659    fn group_allowed_file_can_grant_release() {
660        let tmp = tempfile::tempdir().unwrap();
661        let group = tmp.path().join("group");
662        fs::create_dir_all(&group).unwrap();
663        fs::write(group.join("group.allowed"), "rel = all\n").unwrap();
664        let access = Access::new(
665            &["none".into()],
666            &["none".into()],
667            &["none".into()],
668            &["none".into()],
669            &["none".into()],
670            &["none".into()],
671            &["none".into()],
672            tmp.path().into(),
673        )
674        .unwrap();
675        assert!(access
676            .allows(Operation::Release, "group/repo", None)
677            .unwrap());
678    }
679
680    #[test]
681    fn repo_allowed_file_can_grant_interact_and_admin_with_long_or_short_key() {
682        let tmp = tempfile::tempdir().unwrap();
683        let repo = tmp.path().join("group/repo");
684        fs::create_dir_all(&repo).unwrap();
685        fs::write(repo.join(".allowed"), "interact = all\nadmin = all\n").unwrap();
686        let access = Access::new(
687            &["none".into()],
688            &["none".into()],
689            &["none".into()],
690            &["none".into()],
691            &["none".into()],
692            &["none".into()],
693            &["none".into()],
694            tmp.path().into(),
695        )
696        .unwrap();
697        assert!(access
698            .allows(Operation::Interact, "group/repo", None)
699            .unwrap());
700        assert!(access.allows(Operation::Admin, "group/repo", None).unwrap());
701
702        fs::write(repo.join(".allowed"), "i = all\nadm = all\n").unwrap();
703        assert!(access
704            .allows(Operation::Interact, "group/repo", None)
705            .unwrap());
706        assert!(access.allows(Operation::Admin, "group/repo", None).unwrap());
707    }
708
709    #[test]
710    fn group_allowed_file_can_grant_interact_and_admin() {
711        let tmp = tempfile::tempdir().unwrap();
712        let group = tmp.path().join("group");
713        fs::create_dir_all(&group).unwrap();
714        fs::write(group.join("group.allowed"), "i = all\nadm = all\n").unwrap();
715        let access = Access::new(
716            &["none".into()],
717            &["none".into()],
718            &["none".into()],
719            &["none".into()],
720            &["none".into()],
721            &["none".into()],
722            &["none".into()],
723            tmp.path().into(),
724        )
725        .unwrap();
726        assert!(access
727            .allows(Operation::Interact, "group/repo", None)
728            .unwrap());
729        assert!(access.allows(Operation::Admin, "group/repo", None).unwrap());
730    }
731
732    #[test]
733    fn admin_identity_satisfies_repository_permission_checks() {
734        let tmp = tempfile::tempdir().unwrap();
735        let admin = [0xA5; 16];
736        let other = [0x5A; 16];
737        let repo = tmp.path().join("group/repo");
738        fs::create_dir_all(&repo).unwrap();
739        fs::write(
740            repo.join(".allowed"),
741            "admin = a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5\n",
742        )
743        .unwrap();
744        let access = Access::new(
745            &["none".into()],
746            &["none".into()],
747            &["none".into()],
748            &["none".into()],
749            &["none".into()],
750            &["none".into()],
751            &["none".into()],
752            tmp.path().into(),
753        )
754        .unwrap();
755
756        for op in [
757            Operation::Read,
758            Operation::Write,
759            Operation::Create,
760            Operation::Stats,
761            Operation::Release,
762            Operation::Interact,
763        ] {
764            assert!(access.allows(op, "group/repo", Some(&admin)).unwrap());
765            assert!(!access.allows(op, "group/repo", Some(&other)).unwrap());
766        }
767    }
768
769    #[test]
770    fn invalid_repository_names_are_rejected_before_acl_files() {
771        let access = Access::new(
772            &["all".into()],
773            &["all".into()],
774            &["all".into()],
775            &["all".into()],
776            &["all".into()],
777            &["all".into()],
778            &["all".into()],
779            PathBuf::from("."),
780        )
781        .unwrap();
782        assert!(access.allows(Operation::Read, "../repo", None).is_err());
783        assert!(access.allows(Operation::Create, "../repo", None).is_err());
784        assert!(access.allows(Operation::Stats, "../repo", None).is_err());
785        assert!(access.allows(Operation::Release, "../repo", None).is_err());
786        assert!(access.allows(Operation::Interact, "../repo", None).is_err());
787        assert!(access.allows(Operation::Admin, "../repo", None).is_err());
788    }
789}