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