ravenrs_sheet/engine/macros/
policy.rs1use serde_json::Value as Json;
8
9use crate::engine::Trust;
10
11#[derive(Clone, Debug, PartialEq, Eq)]
13pub enum Policy {
14 Allow,
15 Ask(String),
18 NeedsPermission(String),
21 Deny(String),
23}
24
25const DENIED_PREFIXES: &[(&str, &str)] = &[
27 ("file.", "macros don't open, save, close or create workbooks"),
28 ("app.", "macros don't change the app"),
29 ("window.", "macros don't switch windows"),
30 ("automate.", "macros don't record or run scripts or other macros"),
31 ("macro.", "macros don't record or run other macros"),
32];
33
34const DENIED: &[(&str, &str)] = &[
36 ("edit.undo", "a macro is undone as one step after it runs"),
37 ("edit.redo", "a macro is undone as one step after it runs"),
38 ("view.newWindow", "macros don't open, hide or show windows"),
39 ("view.hideWindow", "macros don't open, hide or show windows"),
40 ("view.unhideWindow", "macros don't open, hide or show windows"),
41 ("control.press", "macros don't press form controls"),
42];
43
44const ALLOWED: &[&str] = &["file.properties"];
46
47const READS_FILES: &[&str] = &["data.getData", "data.fromTextCsv", "insert.picture", "pageLayout.background"];
49
50const EXPORTS: &[&str] = &["file.exportPdf", "file.exportCsv", "file.exportHtml", "file.print", "file.printPreview", "file.saveBytes"];
52
53pub fn writes_file(id: &str, params: &Json) -> bool {
55 EXPORTS.contains(&id) && params.get("path").is_some_and(|p| !p.is_null())
56}
57
58pub fn macro_policy(id: &str, params: &Json, trust: Trust) -> Policy {
64 let has_path = params.get("path").is_some_and(|p| !p.is_null());
65 let base = if EXPORTS.contains(&id) {
66 if has_path { Policy::NeedsPermission("writes a file".into()) } else { Policy::Allow }
67 } else if ALLOWED.contains(&id) {
68 Policy::Allow
69 } else if let Some((_, why)) = DENIED.iter().find(|(d, _)| *d == id) {
70 Policy::Deny((*why).into())
71 } else if let Some((_, why)) = DENIED_PREFIXES.iter().find(|(p, _)| id.starts_with(p)) {
72 Policy::Deny((*why).into())
73 } else if READS_FILES.contains(&id) && has_path {
74 Policy::NeedsPermission("reads a file".into())
75 } else {
76 Policy::Allow
77 };
78 match base {
79 Policy::Allow if !trust.allows_code() => Policy::Ask("came with a workbook you opened".into()),
80 other => other,
81 }
82}
83
84pub fn previewable(id: &str) -> bool {
87 id != "data.solver"
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use serde_json::json;
94
95 #[test]
96 fn the_deny_and_permission_lists() {
97 let none = json!({});
98 let path = json!({"path": "/tmp/x"});
99 for id in [
100 "file.saveAs",
101 "file.save",
102 "file.open",
103 "file.close",
104 "file.new",
105 "app.commands",
106 "window.activate",
107 "automate.runScript",
108 "automate.recordActions",
109 "macro.run",
110 "macro.record",
111 "edit.undo",
112 "edit.redo",
113 "view.newWindow",
114 "view.hideWindow",
115 "view.unhideWindow",
116 "control.press",
117 ] {
118 assert!(matches!(macro_policy(id, &none, Trust::Local), Policy::Deny(_)), "{id}");
119 assert!(matches!(macro_policy(id, &path, Trust::FromFile), Policy::Deny(_)), "{id}");
120 }
121 for id in ["data.getData", "data.fromTextCsv", "insert.picture", "pageLayout.background", "file.exportPdf", "file.print", "file.exportCsv"] {
122 assert!(matches!(macro_policy(id, &path, Trust::Local), Policy::NeedsPermission(_)), "{id}");
123 assert!(matches!(macro_policy(id, &path, Trust::FromFile), Policy::NeedsPermission(_)), "{id}");
124 }
125 for id in ["data.getData", "insert.picture", "file.exportPdf", "file.saveBytes", "file.printPreview"] {
127 assert_eq!(macro_policy(id, &none, Trust::Local), Policy::Allow, "{id}");
128 assert_eq!(macro_policy(id, &json!({"path": null}), Trust::Local), Policy::Allow, "{id}");
129 }
130 for path in [json!(""), json!(" "), json!({"$ref": "Input"}), json!("{{Input}}.csv"), json!(1)] {
132 for id in ["file.exportCsv", "data.fromTextCsv"] {
133 let p = json!({ "path": path });
134 assert!(matches!(macro_policy(id, &p, Trust::Local), Policy::NeedsPermission(_)), "{id} {p}");
135 }
136 }
137 for id in ["cell.set", "home.bold", "view.macros", "edit.copy", "file.properties"] {
138 assert_eq!(macro_policy(id, &none, Trust::Local), Policy::Allow, "{id}");
139 assert!(matches!(macro_policy(id, &none, Trust::FromFile), Policy::Ask(_)), "{id}");
140 }
141 assert!(!previewable("data.solver") && previewable("cell.set"));
142 }
143
144 #[test]
145 fn every_listed_command_exists() {
146 for id in ALLOWED.iter().chain(READS_FILES).chain(EXPORTS).chain(DENIED.iter().map(|(id, _)| id)).chain(&["data.solver"]) {
147 assert!(crate::engine::find_command(id).is_some(), "{id}");
148 }
149 }
150}