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 #[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 #[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}