1use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::env;
10
11#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
13pub struct Policy {
14 #[serde(skip_serializing_if = "Option::is_none", rename = "allowTasks")]
16 pub allow_tasks: Option<Vec<String>>,
17
18 #[serde(skip_serializing_if = "Option::is_none", rename = "allowExec")]
20 pub allow_exec: Option<Vec<String>>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
25pub struct EnvVarWithPolicies {
26 pub value: EnvValueSimple,
28
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub policies: Option<Vec<Policy>>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
36#[serde(untagged)]
37pub enum EnvValueSimple {
38 String(String),
39 Int(i64),
40 Bool(bool),
41 Secret(crate::secrets::Secret),
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
47#[serde(untagged)]
48pub enum EnvValue {
49 WithPolicies(EnvVarWithPolicies),
51 String(String),
53 Int(i64),
54 Bool(bool),
55 Secret(crate::secrets::Secret),
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
61pub struct Env {
62 #[serde(flatten)]
65 pub base: HashMap<String, EnvValue>,
66
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub environment: Option<HashMap<String, HashMap<String, EnvValue>>>,
70}
71
72impl Env {
73 pub fn for_environment(&self, env_name: &str) -> HashMap<String, EnvValue> {
75 let mut result = self.base.clone();
76
77 if let Some(environments) = &self.environment
78 && let Some(env_overrides) = environments.get(env_name)
79 {
80 result.extend(env_overrides.clone());
81 }
82
83 result
84 }
85}
86
87impl EnvValue {
88 pub fn is_accessible_by_task(&self, task_name: &str) -> bool {
90 match self {
91 EnvValue::String(_) | EnvValue::Int(_) | EnvValue::Bool(_) | EnvValue::Secret(_) => {
93 true
94 }
95
96 EnvValue::WithPolicies(var) => match &var.policies {
98 None => true, Some(policies) if policies.is_empty() => true, Some(policies) => {
101 policies.iter().any(|policy| {
103 policy
104 .allow_tasks
105 .as_ref()
106 .is_some_and(|tasks| tasks.iter().any(|t| t == task_name))
107 })
108 }
109 },
110 }
111 }
112
113 pub fn is_accessible_by_exec(&self, command: &str) -> bool {
115 match self {
116 EnvValue::String(_) | EnvValue::Int(_) | EnvValue::Bool(_) | EnvValue::Secret(_) => {
118 true
119 }
120
121 EnvValue::WithPolicies(var) => match &var.policies {
123 None => true, Some(policies) if policies.is_empty() => true, Some(policies) => {
126 policies.iter().any(|policy| {
128 policy
129 .allow_exec
130 .as_ref()
131 .is_some_and(|execs| execs.iter().any(|e| e == command))
132 })
133 }
134 },
135 }
136 }
137
138 pub fn to_string_value(&self) -> String {
140 match self {
141 EnvValue::String(s) => s.clone(),
142 EnvValue::Int(i) => i.to_string(),
143 EnvValue::Bool(b) => b.to_string(),
144 EnvValue::Secret(_) => "[SECRET]".to_string(), EnvValue::WithPolicies(var) => match &var.value {
146 EnvValueSimple::String(s) => s.clone(),
147 EnvValueSimple::Int(i) => i.to_string(),
148 EnvValueSimple::Bool(b) => b.to_string(),
149 EnvValueSimple::Secret(_) => "[SECRET]".to_string(),
150 },
151 }
152 }
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize, Default)]
157pub struct Environment {
158 #[serde(flatten)]
160 pub vars: HashMap<String, String>,
161}
162
163impl Environment {
164 pub fn new() -> Self {
166 Self::default()
167 }
168
169 pub fn from_map(vars: HashMap<String, String>) -> Self {
171 Self { vars }
172 }
173
174 pub fn get(&self, key: &str) -> Option<&str> {
176 self.vars.get(key).map(|s| s.as_str())
177 }
178
179 pub fn set(&mut self, key: String, value: String) {
181 self.vars.insert(key, value);
182 }
183
184 pub fn contains(&self, key: &str) -> bool {
186 self.vars.contains_key(key)
187 }
188
189 pub fn to_env_vec(&self) -> Vec<String> {
191 self.vars
192 .iter()
193 .map(|(k, v)| format!("{}={}", k, v))
194 .collect()
195 }
196
197 pub fn merge_with_system(&self) -> HashMap<String, String> {
200 let mut merged: HashMap<String, String> = env::vars().collect();
201
202 for (key, value) in &self.vars {
204 merged.insert(key.clone(), value.clone());
205 }
206
207 merged
208 }
209
210 pub fn to_full_env_vec(&self) -> Vec<String> {
212 self.merge_with_system()
213 .iter()
214 .map(|(k, v)| format!("{}={}", k, v))
215 .collect()
216 }
217
218 pub fn len(&self) -> usize {
220 self.vars.len()
221 }
222
223 pub fn is_empty(&self) -> bool {
225 self.vars.is_empty()
226 }
227
228 pub fn iter(&self) -> impl Iterator<Item = (&String, &String)> {
230 self.vars.iter()
231 }
232
233 pub fn build_for_task(
235 task_name: &str,
236 env_vars: &HashMap<String, EnvValue>,
237 ) -> HashMap<String, String> {
238 env_vars
239 .iter()
240 .filter(|(_, value)| value.is_accessible_by_task(task_name))
241 .map(|(key, value)| (key.clone(), value.to_string_value()))
242 .collect()
243 }
244
245 pub fn build_for_exec(
247 command: &str,
248 env_vars: &HashMap<String, EnvValue>,
249 ) -> HashMap<String, String> {
250 env_vars
251 .iter()
252 .filter(|(_, value)| value.is_accessible_by_exec(command))
253 .map(|(key, value)| (key.clone(), value.to_string_value()))
254 .collect()
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn test_environment_basics() {
264 let mut env = Environment::new();
265 assert!(env.is_empty());
266
267 env.set("FOO".to_string(), "bar".to_string());
268 assert_eq!(env.len(), 1);
269 assert!(env.contains("FOO"));
270 assert_eq!(env.get("FOO"), Some("bar"));
271 assert!(!env.contains("BAR"));
272 }
273
274 #[test]
275 fn test_environment_from_map() {
276 let mut vars = HashMap::new();
277 vars.insert("KEY1".to_string(), "value1".to_string());
278 vars.insert("KEY2".to_string(), "value2".to_string());
279
280 let env = Environment::from_map(vars);
281 assert_eq!(env.len(), 2);
282 assert_eq!(env.get("KEY1"), Some("value1"));
283 assert_eq!(env.get("KEY2"), Some("value2"));
284 }
285
286 #[test]
287 fn test_environment_to_vec() {
288 let mut env = Environment::new();
289 env.set("VAR1".to_string(), "val1".to_string());
290 env.set("VAR2".to_string(), "val2".to_string());
291
292 let vec = env.to_env_vec();
293 assert_eq!(vec.len(), 2);
294 assert!(vec.contains(&"VAR1=val1".to_string()));
295 assert!(vec.contains(&"VAR2=val2".to_string()));
296 }
297
298 #[test]
299 fn test_environment_merge_with_system() {
300 let mut env = Environment::new();
301 env.set("PATH".to_string(), "/custom/path".to_string());
302 env.set("CUSTOM_VAR".to_string(), "custom_value".to_string());
303
304 let merged = env.merge_with_system();
305
306 assert_eq!(merged.get("PATH"), Some(&"/custom/path".to_string()));
308 assert_eq!(merged.get("CUSTOM_VAR"), Some(&"custom_value".to_string()));
309
310 assert!(merged.len() >= 2);
313 }
314
315 #[test]
316 fn test_environment_iteration() {
317 let mut env = Environment::new();
318 env.set("A".to_string(), "1".to_string());
319 env.set("B".to_string(), "2".to_string());
320
321 let mut count = 0;
322 for (key, value) in env.iter() {
323 assert!(key == "A" || key == "B");
324 assert!(value == "1" || value == "2");
325 count += 1;
326 }
327 assert_eq!(count, 2);
328 }
329
330 #[test]
331 fn test_env_value_types() {
332 let str_val = EnvValue::String("test".to_string());
333 let int_val = EnvValue::Int(42);
334 let bool_val = EnvValue::Bool(true);
335
336 assert_eq!(str_val, EnvValue::String("test".to_string()));
337 assert_eq!(int_val, EnvValue::Int(42));
338 assert_eq!(bool_val, EnvValue::Bool(true));
339 }
340
341 #[test]
342 fn test_policy_task_access() {
343 let simple_var = EnvValue::String("simple".to_string());
345 assert!(simple_var.is_accessible_by_task("any_task"));
346
347 let no_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
349 value: EnvValueSimple::String("value".to_string()),
350 policies: None,
351 });
352 assert!(no_policy_var.is_accessible_by_task("any_task"));
353
354 let empty_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
356 value: EnvValueSimple::String("value".to_string()),
357 policies: Some(vec![]),
358 });
359 assert!(empty_policy_var.is_accessible_by_task("any_task"));
360
361 let restricted_var = EnvValue::WithPolicies(EnvVarWithPolicies {
363 value: EnvValueSimple::String("secret".to_string()),
364 policies: Some(vec![Policy {
365 allow_tasks: Some(vec!["deploy".to_string(), "release".to_string()]),
366 allow_exec: None,
367 }]),
368 });
369 assert!(restricted_var.is_accessible_by_task("deploy"));
370 assert!(restricted_var.is_accessible_by_task("release"));
371 assert!(!restricted_var.is_accessible_by_task("test"));
372 assert!(!restricted_var.is_accessible_by_task("build"));
373 }
374
375 #[test]
376 fn test_policy_exec_access() {
377 let simple_var = EnvValue::String("simple".to_string());
379 assert!(simple_var.is_accessible_by_exec("bash"));
380
381 let restricted_var = EnvValue::WithPolicies(EnvVarWithPolicies {
383 value: EnvValueSimple::String("secret".to_string()),
384 policies: Some(vec![Policy {
385 allow_tasks: None,
386 allow_exec: Some(vec!["kubectl".to_string(), "terraform".to_string()]),
387 }]),
388 });
389 assert!(restricted_var.is_accessible_by_exec("kubectl"));
390 assert!(restricted_var.is_accessible_by_exec("terraform"));
391 assert!(!restricted_var.is_accessible_by_exec("bash"));
392 assert!(!restricted_var.is_accessible_by_exec("sh"));
393 }
394
395 #[test]
396 fn test_multiple_policies() {
397 let multi_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
399 value: EnvValueSimple::String("value".to_string()),
400 policies: Some(vec![
401 Policy {
402 allow_tasks: Some(vec!["task1".to_string()]),
403 allow_exec: None,
404 },
405 Policy {
406 allow_tasks: Some(vec!["task2".to_string()]),
407 allow_exec: Some(vec!["kubectl".to_string()]),
408 },
409 ]),
410 });
411
412 assert!(multi_policy_var.is_accessible_by_task("task1"));
414 assert!(multi_policy_var.is_accessible_by_task("task2"));
415 assert!(!multi_policy_var.is_accessible_by_task("task3"));
416
417 assert!(multi_policy_var.is_accessible_by_exec("kubectl"));
419 assert!(!multi_policy_var.is_accessible_by_exec("bash"));
420 }
421
422 #[test]
423 fn test_to_string_value() {
424 assert_eq!(
425 EnvValue::String("test".to_string()).to_string_value(),
426 "test"
427 );
428 assert_eq!(EnvValue::Int(42).to_string_value(), "42");
429 assert_eq!(EnvValue::Bool(true).to_string_value(), "true");
430 assert_eq!(EnvValue::Bool(false).to_string_value(), "false");
431
432 let with_policies = EnvValue::WithPolicies(EnvVarWithPolicies {
433 value: EnvValueSimple::String("policy_value".to_string()),
434 policies: Some(vec![]),
435 });
436 assert_eq!(with_policies.to_string_value(), "policy_value");
437 }
438
439 #[test]
440 fn test_build_for_task() {
441 let mut env_vars = HashMap::new();
442
443 env_vars.insert(
445 "PUBLIC".to_string(),
446 EnvValue::String("public_value".to_string()),
447 );
448
449 env_vars.insert(
451 "SECRET".to_string(),
452 EnvValue::WithPolicies(EnvVarWithPolicies {
453 value: EnvValueSimple::String("secret_value".to_string()),
454 policies: Some(vec![Policy {
455 allow_tasks: Some(vec!["deploy".to_string()]),
456 allow_exec: None,
457 }]),
458 }),
459 );
460
461 let deploy_env = Environment::build_for_task("deploy", &env_vars);
463 assert_eq!(deploy_env.len(), 2);
464 assert_eq!(deploy_env.get("PUBLIC"), Some(&"public_value".to_string()));
465 assert_eq!(deploy_env.get("SECRET"), Some(&"secret_value".to_string()));
466
467 let test_env = Environment::build_for_task("test", &env_vars);
469 assert_eq!(test_env.len(), 1);
470 assert_eq!(test_env.get("PUBLIC"), Some(&"public_value".to_string()));
471 assert_eq!(test_env.get("SECRET"), None);
472 }
473
474 #[test]
475 fn test_build_for_exec() {
476 let mut env_vars = HashMap::new();
477
478 env_vars.insert(
480 "PUBLIC".to_string(),
481 EnvValue::String("public_value".to_string()),
482 );
483
484 env_vars.insert(
486 "SECRET".to_string(),
487 EnvValue::WithPolicies(EnvVarWithPolicies {
488 value: EnvValueSimple::String("secret_value".to_string()),
489 policies: Some(vec![Policy {
490 allow_tasks: None,
491 allow_exec: Some(vec!["kubectl".to_string()]),
492 }]),
493 }),
494 );
495
496 let kubectl_env = Environment::build_for_exec("kubectl", &env_vars);
498 assert_eq!(kubectl_env.len(), 2);
499 assert_eq!(kubectl_env.get("PUBLIC"), Some(&"public_value".to_string()));
500 assert_eq!(kubectl_env.get("SECRET"), Some(&"secret_value".to_string()));
501
502 let bash_env = Environment::build_for_exec("bash", &env_vars);
504 assert_eq!(bash_env.len(), 1);
505 assert_eq!(bash_env.get("PUBLIC"), Some(&"public_value".to_string()));
506 assert_eq!(bash_env.get("SECRET"), None);
507 }
508
509 #[test]
510 fn test_env_for_environment() {
511 let mut base = HashMap::new();
512 base.insert("BASE_VAR".to_string(), EnvValue::String("base".to_string()));
513 base.insert(
514 "OVERRIDE_ME".to_string(),
515 EnvValue::String("original".to_string()),
516 );
517
518 let mut dev_env = HashMap::new();
519 dev_env.insert(
520 "OVERRIDE_ME".to_string(),
521 EnvValue::String("dev".to_string()),
522 );
523 dev_env.insert(
524 "DEV_VAR".to_string(),
525 EnvValue::String("development".to_string()),
526 );
527
528 let mut environments = HashMap::new();
529 environments.insert("development".to_string(), dev_env);
530
531 let env = Env {
532 base,
533 environment: Some(environments),
534 };
535
536 let dev_vars = env.for_environment("development");
537 assert_eq!(
538 dev_vars.get("BASE_VAR"),
539 Some(&EnvValue::String("base".to_string()))
540 );
541 assert_eq!(
542 dev_vars.get("OVERRIDE_ME"),
543 Some(&EnvValue::String("dev".to_string()))
544 );
545 assert_eq!(
546 dev_vars.get("DEV_VAR"),
547 Some(&EnvValue::String("development".to_string()))
548 );
549 }
550}