cc_toolgate/commands/tools/
cargo.rs1use super::super::CommandSpec;
7use crate::config::CargoConfig;
8use crate::eval::{CommandContext, Decision, RuleMatch};
9use std::collections::HashMap;
10
11pub struct CargoSpec {
19 safe_subcommands: Vec<String>,
21 allowed_with_config: Vec<String>,
23 config_env: HashMap<String, String>,
25}
26
27impl CargoSpec {
28 pub fn from_config(config: &CargoConfig) -> Self {
30 Self {
31 safe_subcommands: config.safe_subcommands.clone(),
32 allowed_with_config: config.allowed_with_config.clone(),
33 config_env: config.config_env.clone(),
34 }
35 }
36
37 fn subcommand<'a>(ctx: &'a CommandContext) -> Option<&'a str> {
40 let mut iter = ctx.words.iter();
41 for word in iter.by_ref() {
42 if word == "cargo" {
43 return iter.find(|w| !w.starts_with('-')).map(|s| s.as_str());
44 }
45 }
46 None
47 }
48
49 fn env_keys_display(&self) -> String {
51 let mut keys: Vec<&str> = self.config_env.keys().map(|k| k.as_str()).collect();
52 keys.sort();
53 keys.join(", ")
54 }
55}
56
57impl CommandSpec for CargoSpec {
58 fn evaluate(&self, ctx: &CommandContext) -> RuleMatch {
59 let sub_str = Self::subcommand(ctx).unwrap_or("?");
60
61 if self.safe_subcommands.iter().any(|s| s == sub_str) {
62 if let Some(ref r) = ctx.redirection {
63 return RuleMatch {
64 decision: Decision::Ask,
65 reason: format!("cargo {sub_str} with {}", r.description),
66 };
67 }
68 return RuleMatch {
69 decision: Decision::Allow,
70 reason: format!("cargo {sub_str}"),
71 };
72 }
73
74 if self.allowed_with_config.iter().any(|s| s == sub_str) {
76 if !self.config_env.is_empty() && ctx.env_satisfies(&self.config_env) {
77 if let Some(ref r) = ctx.redirection {
78 return RuleMatch {
79 decision: Decision::Ask,
80 reason: format!("cargo {sub_str} with {}", r.description),
81 };
82 }
83 return RuleMatch {
84 decision: Decision::Allow,
85 reason: format!("cargo {sub_str} with {}", self.env_keys_display()),
86 };
87 }
88 return RuleMatch {
89 decision: Decision::Ask,
90 reason: format!("cargo {sub_str} requires confirmation"),
91 };
92 }
93
94 if ctx.has_any_flag(&["--version", "-V"]) {
96 return RuleMatch {
97 decision: Decision::Allow,
98 reason: "cargo --version".into(),
99 };
100 }
101
102 RuleMatch {
103 decision: Decision::Ask,
104 reason: format!("cargo {sub_str} requires confirmation"),
105 }
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::config::Config;
113
114 fn spec() -> CargoSpec {
115 CargoSpec::from_config(&Config::default_config().cargo)
116 }
117
118 fn eval(cmd: &str) -> Decision {
119 let s = spec();
120 let ctx = CommandContext::from_command(cmd);
121 s.evaluate(&ctx).decision
122 }
123
124 #[test]
125 fn allow_build() {
126 assert_eq!(eval("cargo build --release"), Decision::Allow);
127 }
128
129 #[test]
130 fn allow_test() {
131 assert_eq!(eval("cargo test"), Decision::Allow);
132 }
133
134 #[test]
135 fn allow_clippy() {
136 assert_eq!(eval("cargo clippy"), Decision::Allow);
137 }
138
139 #[test]
140 fn allow_version() {
141 assert_eq!(eval("cargo --version"), Decision::Allow);
142 }
143
144 #[test]
145 fn allow_version_short() {
146 assert_eq!(eval("cargo -V"), Decision::Allow);
147 }
148
149 #[test]
150 fn ask_install() {
151 assert_eq!(eval("cargo install ripgrep"), Decision::Ask);
152 }
153
154 #[test]
155 fn ask_publish() {
156 assert_eq!(eval("cargo publish"), Decision::Ask);
157 }
158
159 #[test]
160 fn redir_build() {
161 assert_eq!(eval("cargo build --release > /tmp/log"), Decision::Ask);
162 }
163
164 fn spec_with_env_gate() -> CargoSpec {
167 CargoSpec::from_config(&CargoConfig {
168 safe_subcommands: vec!["build".into(), "check".into(), "test".into()],
169 allowed_with_config: vec!["install".into(), "publish".into()],
170 config_env: HashMap::from([("CARGO_INSTALL_ROOT".into(), "/tmp/bin".into())]),
171 })
172 }
173
174 fn eval_with_env_gate(cmd: &str) -> Decision {
175 let s = spec_with_env_gate();
176 let ctx = CommandContext::from_command(cmd);
177 s.evaluate(&ctx).decision
178 }
179
180 #[test]
181 fn env_gate_install_with_matching_value() {
182 assert_eq!(
183 eval_with_env_gate("CARGO_INSTALL_ROOT=/tmp/bin cargo install ripgrep"),
184 Decision::Allow
185 );
186 }
187
188 #[test]
189 fn env_gate_install_with_wrong_value() {
190 assert_eq!(
191 eval_with_env_gate("CARGO_INSTALL_ROOT=/usr/local cargo install ripgrep"),
192 Decision::Ask
193 );
194 }
195
196 #[test]
197 fn env_gate_install_no_config() {
198 assert_eq!(eval_with_env_gate("cargo install ripgrep"), Decision::Ask);
199 }
200
201 #[test]
202 fn env_gate_publish_with_config() {
203 assert_eq!(
204 eval_with_env_gate("CARGO_INSTALL_ROOT=/tmp/bin cargo publish"),
205 Decision::Allow
206 );
207 }
208
209 #[test]
210 fn env_gate_build_still_safe_no_env() {
211 assert_eq!(eval_with_env_gate("cargo build"), Decision::Allow);
213 }
214}