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