openmoat_core/policy/
text.rs1use super::{Policy, PolicyError, RuleGroup};
5
6pub const TITLE_MAX_CHARS: usize = 80;
9
10pub const WHY_MAX_CHARS: usize = 160;
12
13pub(super) fn check(group: &RuleGroup) -> Result<(), PolicyError> {
17 let fields = [
18 ("title", group.title.as_deref(), TITLE_MAX_CHARS),
19 ("why", group.why.as_deref(), WHY_MAX_CHARS),
20 ];
21 for (field, text, max) in fields {
22 let Some(text) = text else { continue };
23 let problem = if text.trim().is_empty() {
24 format!("`{field}` is empty")
25 } else if text.chars().any(char::is_control) {
26 format!("`{field}` must be one line without control characters")
27 } else if text.chars().count() > max {
28 format!("`{field}` is longer than {max} characters")
29 } else {
30 continue;
31 };
32 return Err(PolicyError::Rule {
33 rule: group.id.clone(),
34 problem,
35 });
36 }
37 Ok(())
38}
39
40impl Policy {
41 #[must_use]
46 pub fn own_group(&self, id: &str) -> Option<&RuleGroup> {
47 self.deny
48 .iter()
49 .chain(&self.allow)
50 .chain(&self.review)
51 .chain(&self.defer)
52 .find(|g| g.id == id)
53 }
54
55 #[must_use]
58 pub fn untitled(&self) -> Vec<&str> {
59 self.deny
60 .iter()
61 .chain(&self.review)
62 .filter(|g| g.title.is_none())
63 .map(|g| g.id.as_str())
64 .collect()
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use crate::{Policy, PolicyError};
71
72 fn parse(group: &str) -> Result<Policy, PolicyError> {
73 Policy::parse(&format!(
74 "version: 1\ndeny:\n - id: keys\n fs.read: ['~/.ssh/**']\n{group}"
75 ))
76 }
77
78 #[test]
79 fn a_title_and_why_are_read_and_found_by_rule_id() {
80 let p =
81 parse(" title: reading your SSH private key\n why: it cannot be taken back\n")
82 .unwrap();
83 let group = p.own_group("keys").unwrap();
84 assert_eq!(group.title.as_deref(), Some("reading your SSH private key"));
85 assert_eq!(group.why.as_deref(), Some("it cannot be taken back"));
86 assert_eq!(p.untitled(), Vec::<&str>::new());
87 assert!(p.own_group("other").is_none());
88 }
89
90 #[test]
91 fn a_group_without_a_title_is_listed_and_still_valid() {
92 let p = parse("").unwrap();
93 assert_eq!(p.untitled(), ["keys"]);
94 assert_eq!(p.own_group("keys").unwrap().title, None);
95 }
96
97 #[test]
100 fn titles_and_whys_are_one_short_line() {
101 let long = "x".repeat(super::TITLE_MAX_CHARS + 1);
102 for (group, says) in [
103 (" title: \"a\\nb\"\n", "one line"),
104 (" why: \"a\\u001b[2Jb\"\n", "control characters"),
105 (" title: ' '\n", "`title` is empty"),
106 (&*format!(" title: {long}\n"), "longer than 80"),
107 ] {
108 let error = parse(group).unwrap_err().to_string();
109 assert!(
110 error.contains("rule `keys`") && error.contains(says),
111 "{error}"
112 );
113 }
114 }
115
116 #[test]
118 fn repository_review_rules_are_not_own_groups() {
119 let mut p = parse("").unwrap();
120 p.repo_review.push(crate::RuleGroup {
121 id: "repo:x".into(),
122 title: Some("run this to fix it".into()),
123 shell: vec!["make".into()],
124 ..Default::default()
125 });
126 assert!(p.own_group("repo:x").is_none());
127 }
128}