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 pub async fn resolve(&self) -> crate::Result<String> {
156 match self {
157 EnvValue::String(s) => Ok(s.clone()),
158 EnvValue::Int(i) => Ok(i.to_string()),
159 EnvValue::Bool(b) => Ok(b.to_string()),
160 EnvValue::Secret(s) => s.resolve().await,
161 EnvValue::WithPolicies(var) => match &var.value {
162 EnvValueSimple::String(s) => Ok(s.clone()),
163 EnvValueSimple::Int(i) => Ok(i.to_string()),
164 EnvValueSimple::Bool(b) => Ok(b.to_string()),
165 EnvValueSimple::Secret(s) => s.resolve().await,
166 },
167 }
168 }
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, Default)]
173pub struct Environment {
174 #[serde(flatten)]
176 pub vars: HashMap<String, String>,
177}
178
179impl Environment {
180 pub fn new() -> Self {
182 Self::default()
183 }
184
185 pub fn from_map(vars: HashMap<String, String>) -> Self {
187 Self { vars }
188 }
189
190 pub fn get(&self, key: &str) -> Option<&str> {
192 self.vars.get(key).map(|s| s.as_str())
193 }
194
195 pub fn set(&mut self, key: String, value: String) {
197 self.vars.insert(key, value);
198 }
199
200 pub fn contains(&self, key: &str) -> bool {
202 self.vars.contains_key(key)
203 }
204
205 pub fn to_env_vec(&self) -> Vec<String> {
207 self.vars
208 .iter()
209 .map(|(k, v)| format!("{}={}", k, v))
210 .collect()
211 }
212
213 pub fn merge_with_system(&self) -> HashMap<String, String> {
216 let mut merged: HashMap<String, String> = env::vars().collect();
217
218 for (key, value) in &self.vars {
220 merged.insert(key.clone(), value.clone());
221 }
222
223 merged
224 }
225
226 pub fn to_full_env_vec(&self) -> Vec<String> {
228 self.merge_with_system()
229 .iter()
230 .map(|(k, v)| format!("{}={}", k, v))
231 .collect()
232 }
233
234 pub fn len(&self) -> usize {
236 self.vars.len()
237 }
238
239 pub fn is_empty(&self) -> bool {
241 self.vars.is_empty()
242 }
243
244 pub fn resolve_command(&self, command: &str) -> String {
252 if command.starts_with('/') {
254 tracing::debug!(command = %command, "Command is already absolute path");
255 return command.to_string();
256 }
257
258 let path_value = self
260 .vars
261 .get("PATH")
262 .cloned()
263 .or_else(|| env::var("PATH").ok())
264 .unwrap_or_default();
265
266 tracing::debug!(
267 command = %command,
268 env_has_path = self.vars.contains_key("PATH"),
269 path_len = path_value.len(),
270 "Resolving command in PATH"
271 );
272
273 for dir in path_value.split(':') {
275 if dir.is_empty() {
276 continue;
277 }
278 let candidate = std::path::Path::new(dir).join(command);
279 if candidate.is_file() {
280 #[cfg(unix)]
282 {
283 use std::os::unix::fs::PermissionsExt;
284 if let Ok(metadata) = std::fs::metadata(&candidate) {
285 let permissions = metadata.permissions();
286 if permissions.mode() & 0o111 != 0 {
287 tracing::debug!(
288 command = %command,
289 resolved = %candidate.display(),
290 "Command resolved to path"
291 );
292 return candidate.to_string_lossy().to_string();
293 }
294 }
295 }
296 #[cfg(not(unix))]
297 {
298 tracing::debug!(
299 command = %command,
300 resolved = %candidate.display(),
301 "Command resolved to path"
302 );
303 return candidate.to_string_lossy().to_string();
304 }
305 }
306 }
307
308 tracing::warn!(
310 command = %command,
311 env_path_set = self.vars.contains_key("PATH"),
312 "Command not found in PATH, returning original"
313 );
314 command.to_string()
315 }
316
317 pub fn iter(&self) -> impl Iterator<Item = (&String, &String)> {
319 self.vars.iter()
320 }
321
322 pub fn build_for_task(
324 task_name: &str,
325 env_vars: &HashMap<String, EnvValue>,
326 ) -> HashMap<String, String> {
327 env_vars
328 .iter()
329 .filter(|(_, value)| value.is_accessible_by_task(task_name))
330 .map(|(key, value)| (key.clone(), value.to_string_value()))
331 .collect()
332 }
333
334 pub async fn resolve_for_task(
336 task_name: &str,
337 env_vars: &HashMap<String, EnvValue>,
338 ) -> crate::Result<HashMap<String, String>> {
339 let mut resolved = HashMap::new();
340 for (key, value) in env_vars {
341 if value.is_accessible_by_task(task_name) {
342 resolved.insert(key.clone(), value.resolve().await?);
343 }
344 }
345 Ok(resolved)
346 }
347
348 pub fn build_for_exec(
350 command: &str,
351 env_vars: &HashMap<String, EnvValue>,
352 ) -> HashMap<String, String> {
353 env_vars
354 .iter()
355 .filter(|(_, value)| value.is_accessible_by_exec(command))
356 .map(|(key, value)| (key.clone(), value.to_string_value()))
357 .collect()
358 }
359
360 pub async fn resolve_for_exec(
362 command: &str,
363 env_vars: &HashMap<String, EnvValue>,
364 ) -> crate::Result<HashMap<String, String>> {
365 let mut resolved = HashMap::new();
366 for (key, value) in env_vars {
367 if value.is_accessible_by_exec(command) {
368 resolved.insert(key.clone(), value.resolve().await?);
369 }
370 }
371 Ok(resolved)
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 #[test]
380 fn test_environment_basics() {
381 let mut env = Environment::new();
382 assert!(env.is_empty());
383
384 env.set("FOO".to_string(), "bar".to_string());
385 assert_eq!(env.len(), 1);
386 assert!(env.contains("FOO"));
387 assert_eq!(env.get("FOO"), Some("bar"));
388 assert!(!env.contains("BAR"));
389 }
390
391 #[test]
392 fn test_environment_from_map() {
393 let mut vars = HashMap::new();
394 vars.insert("KEY1".to_string(), "value1".to_string());
395 vars.insert("KEY2".to_string(), "value2".to_string());
396
397 let env = Environment::from_map(vars);
398 assert_eq!(env.len(), 2);
399 assert_eq!(env.get("KEY1"), Some("value1"));
400 assert_eq!(env.get("KEY2"), Some("value2"));
401 }
402
403 #[test]
404 fn test_environment_to_vec() {
405 let mut env = Environment::new();
406 env.set("VAR1".to_string(), "val1".to_string());
407 env.set("VAR2".to_string(), "val2".to_string());
408
409 let vec = env.to_env_vec();
410 assert_eq!(vec.len(), 2);
411 assert!(vec.contains(&"VAR1=val1".to_string()));
412 assert!(vec.contains(&"VAR2=val2".to_string()));
413 }
414
415 #[test]
416 fn test_environment_merge_with_system() {
417 let mut env = Environment::new();
418 env.set("PATH".to_string(), "/custom/path".to_string());
419 env.set("CUSTOM_VAR".to_string(), "custom_value".to_string());
420
421 let merged = env.merge_with_system();
422
423 assert_eq!(merged.get("PATH"), Some(&"/custom/path".to_string()));
425 assert_eq!(merged.get("CUSTOM_VAR"), Some(&"custom_value".to_string()));
426
427 assert!(merged.len() >= 2);
430 }
431
432 #[test]
433 fn test_environment_iteration() {
434 let mut env = Environment::new();
435 env.set("A".to_string(), "1".to_string());
436 env.set("B".to_string(), "2".to_string());
437
438 let mut count = 0;
439 for (key, value) in env.iter() {
440 assert!(key == "A" || key == "B");
441 assert!(value == "1" || value == "2");
442 count += 1;
443 }
444 assert_eq!(count, 2);
445 }
446
447 #[test]
448 fn test_env_value_types() {
449 let str_val = EnvValue::String("test".to_string());
450 let int_val = EnvValue::Int(42);
451 let bool_val = EnvValue::Bool(true);
452
453 assert_eq!(str_val, EnvValue::String("test".to_string()));
454 assert_eq!(int_val, EnvValue::Int(42));
455 assert_eq!(bool_val, EnvValue::Bool(true));
456 }
457
458 #[test]
459 fn test_policy_task_access() {
460 let simple_var = EnvValue::String("simple".to_string());
462 assert!(simple_var.is_accessible_by_task("any_task"));
463
464 let no_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
466 value: EnvValueSimple::String("value".to_string()),
467 policies: None,
468 });
469 assert!(no_policy_var.is_accessible_by_task("any_task"));
470
471 let empty_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
473 value: EnvValueSimple::String("value".to_string()),
474 policies: Some(vec![]),
475 });
476 assert!(empty_policy_var.is_accessible_by_task("any_task"));
477
478 let restricted_var = EnvValue::WithPolicies(EnvVarWithPolicies {
480 value: EnvValueSimple::String("secret".to_string()),
481 policies: Some(vec![Policy {
482 allow_tasks: Some(vec!["deploy".to_string(), "release".to_string()]),
483 allow_exec: None,
484 }]),
485 });
486 assert!(restricted_var.is_accessible_by_task("deploy"));
487 assert!(restricted_var.is_accessible_by_task("release"));
488 assert!(!restricted_var.is_accessible_by_task("test"));
489 assert!(!restricted_var.is_accessible_by_task("build"));
490 }
491
492 #[test]
493 fn test_policy_exec_access() {
494 let simple_var = EnvValue::String("simple".to_string());
496 assert!(simple_var.is_accessible_by_exec("bash"));
497
498 let restricted_var = EnvValue::WithPolicies(EnvVarWithPolicies {
500 value: EnvValueSimple::String("secret".to_string()),
501 policies: Some(vec![Policy {
502 allow_tasks: None,
503 allow_exec: Some(vec!["kubectl".to_string(), "terraform".to_string()]),
504 }]),
505 });
506 assert!(restricted_var.is_accessible_by_exec("kubectl"));
507 assert!(restricted_var.is_accessible_by_exec("terraform"));
508 assert!(!restricted_var.is_accessible_by_exec("bash"));
509 assert!(!restricted_var.is_accessible_by_exec("sh"));
510 }
511
512 #[test]
513 fn test_multiple_policies() {
514 let multi_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
516 value: EnvValueSimple::String("value".to_string()),
517 policies: Some(vec![
518 Policy {
519 allow_tasks: Some(vec!["task1".to_string()]),
520 allow_exec: None,
521 },
522 Policy {
523 allow_tasks: Some(vec!["task2".to_string()]),
524 allow_exec: Some(vec!["kubectl".to_string()]),
525 },
526 ]),
527 });
528
529 assert!(multi_policy_var.is_accessible_by_task("task1"));
531 assert!(multi_policy_var.is_accessible_by_task("task2"));
532 assert!(!multi_policy_var.is_accessible_by_task("task3"));
533
534 assert!(multi_policy_var.is_accessible_by_exec("kubectl"));
536 assert!(!multi_policy_var.is_accessible_by_exec("bash"));
537 }
538
539 #[test]
540 fn test_to_string_value() {
541 assert_eq!(
542 EnvValue::String("test".to_string()).to_string_value(),
543 "test"
544 );
545 assert_eq!(EnvValue::Int(42).to_string_value(), "42");
546 assert_eq!(EnvValue::Bool(true).to_string_value(), "true");
547 assert_eq!(EnvValue::Bool(false).to_string_value(), "false");
548
549 let with_policies = EnvValue::WithPolicies(EnvVarWithPolicies {
550 value: EnvValueSimple::String("policy_value".to_string()),
551 policies: Some(vec![]),
552 });
553 assert_eq!(with_policies.to_string_value(), "policy_value");
554 }
555
556 #[test]
557 fn test_build_for_task() {
558 let mut env_vars = HashMap::new();
559
560 env_vars.insert(
562 "PUBLIC".to_string(),
563 EnvValue::String("public_value".to_string()),
564 );
565
566 env_vars.insert(
568 "SECRET".to_string(),
569 EnvValue::WithPolicies(EnvVarWithPolicies {
570 value: EnvValueSimple::String("secret_value".to_string()),
571 policies: Some(vec![Policy {
572 allow_tasks: Some(vec!["deploy".to_string()]),
573 allow_exec: None,
574 }]),
575 }),
576 );
577
578 let deploy_env = Environment::build_for_task("deploy", &env_vars);
580 assert_eq!(deploy_env.len(), 2);
581 assert_eq!(deploy_env.get("PUBLIC"), Some(&"public_value".to_string()));
582 assert_eq!(deploy_env.get("SECRET"), Some(&"secret_value".to_string()));
583
584 let test_env = Environment::build_for_task("test", &env_vars);
586 assert_eq!(test_env.len(), 1);
587 assert_eq!(test_env.get("PUBLIC"), Some(&"public_value".to_string()));
588 assert_eq!(test_env.get("SECRET"), None);
589 }
590
591 #[test]
592 fn test_build_for_exec() {
593 let mut env_vars = HashMap::new();
594
595 env_vars.insert(
597 "PUBLIC".to_string(),
598 EnvValue::String("public_value".to_string()),
599 );
600
601 env_vars.insert(
603 "SECRET".to_string(),
604 EnvValue::WithPolicies(EnvVarWithPolicies {
605 value: EnvValueSimple::String("secret_value".to_string()),
606 policies: Some(vec![Policy {
607 allow_tasks: None,
608 allow_exec: Some(vec!["kubectl".to_string()]),
609 }]),
610 }),
611 );
612
613 let kubectl_env = Environment::build_for_exec("kubectl", &env_vars);
615 assert_eq!(kubectl_env.len(), 2);
616 assert_eq!(kubectl_env.get("PUBLIC"), Some(&"public_value".to_string()));
617 assert_eq!(kubectl_env.get("SECRET"), Some(&"secret_value".to_string()));
618
619 let bash_env = Environment::build_for_exec("bash", &env_vars);
621 assert_eq!(bash_env.len(), 1);
622 assert_eq!(bash_env.get("PUBLIC"), Some(&"public_value".to_string()));
623 assert_eq!(bash_env.get("SECRET"), None);
624 }
625
626 #[test]
627 fn test_env_for_environment() {
628 let mut base = HashMap::new();
629 base.insert("BASE_VAR".to_string(), EnvValue::String("base".to_string()));
630 base.insert(
631 "OVERRIDE_ME".to_string(),
632 EnvValue::String("original".to_string()),
633 );
634
635 let mut dev_env = HashMap::new();
636 dev_env.insert(
637 "OVERRIDE_ME".to_string(),
638 EnvValue::String("dev".to_string()),
639 );
640 dev_env.insert(
641 "DEV_VAR".to_string(),
642 EnvValue::String("development".to_string()),
643 );
644
645 let mut environments = HashMap::new();
646 environments.insert("development".to_string(), dev_env);
647
648 let env = Env {
649 base,
650 environment: Some(environments),
651 };
652
653 let dev_vars = env.for_environment("development");
654 assert_eq!(
655 dev_vars.get("BASE_VAR"),
656 Some(&EnvValue::String("base".to_string()))
657 );
658 assert_eq!(
659 dev_vars.get("OVERRIDE_ME"),
660 Some(&EnvValue::String("dev".to_string()))
661 );
662 assert_eq!(
663 dev_vars.get("DEV_VAR"),
664 Some(&EnvValue::String("development".to_string()))
665 );
666 }
667}