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}