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