1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::env;
9use std::sync::Arc;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14#[serde(untagged)]
15pub enum EnvPart {
16 Secret(crate::secrets::Secret),
18 Literal(String),
20}
21
22impl EnvPart {
23 #[must_use]
25 pub fn is_secret(&self) -> bool {
26 matches!(self, EnvPart::Secret(_))
27 }
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
32pub struct Policy {
33 #[serde(skip_serializing_if = "Option::is_none", rename = "allowTasks")]
35 pub allow_tasks: Option<Vec<String>>,
36
37 #[serde(skip_serializing_if = "Option::is_none", rename = "allowExec")]
39 pub allow_exec: Option<Vec<String>>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
44pub struct EnvVarWithPolicies {
45 pub value: EnvValueSimple,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub policies: Option<Vec<Policy>>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
55#[serde(untagged)]
56pub enum EnvValueSimple {
57 Secret(crate::secrets::Secret),
59 Interpolated(Vec<EnvPart>),
61 String(String),
63 Int(i64),
65 Bool(bool),
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
73#[serde(untagged)]
74pub enum EnvValue {
75 WithPolicies(EnvVarWithPolicies),
78 Secret(crate::secrets::Secret),
80 Interpolated(Vec<EnvPart>),
82 String(String),
84 Int(i64),
85 Bool(bool),
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
91pub struct Env {
92 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub environment: Option<HashMap<String, HashMap<String, EnvValue>>>,
95
96 #[serde(flatten)]
99 pub base: HashMap<String, EnvValue>,
100}
101
102impl Env {
103 pub fn for_environment(&self, env_name: &str) -> HashMap<String, EnvValue> {
105 let mut result = self.base.clone();
106
107 if let Some(environments) = &self.environment
108 && let Some(env_overrides) = environments.get(env_name)
109 {
110 result.extend(env_overrides.clone());
111 }
112
113 result
114 }
115}
116
117impl EnvValue {
118 pub fn is_accessible_by_task(&self, task_name: &str) -> bool {
120 match self {
121 EnvValue::String(_)
123 | EnvValue::Int(_)
124 | EnvValue::Bool(_)
125 | EnvValue::Secret(_)
126 | EnvValue::Interpolated(_) => true,
127
128 EnvValue::WithPolicies(var) => match &var.policies {
130 None => true, Some(policies) if policies.is_empty() => true, Some(policies) => {
133 policies.iter().any(|policy| {
135 policy
136 .allow_tasks
137 .as_ref()
138 .is_some_and(|tasks| tasks.iter().any(|t| t == task_name))
139 })
140 }
141 },
142 }
143 }
144
145 pub fn is_accessible_by_exec(&self, command: &str) -> bool {
147 match self {
148 EnvValue::String(_)
150 | EnvValue::Int(_)
151 | EnvValue::Bool(_)
152 | EnvValue::Secret(_)
153 | EnvValue::Interpolated(_) => true,
154
155 EnvValue::WithPolicies(var) => match &var.policies {
157 None => true, Some(policies) if policies.is_empty() => true, Some(policies) => {
160 policies.iter().any(|policy| {
162 policy
163 .allow_exec
164 .as_ref()
165 .is_some_and(|execs| execs.iter().any(|e| e == command))
166 })
167 }
168 },
169 }
170 }
171
172 pub fn to_string_value(&self) -> String {
175 match self {
176 EnvValue::String(s) => s.clone(),
177 EnvValue::Int(i) => i.to_string(),
178 EnvValue::Bool(b) => b.to_string(),
179 EnvValue::Secret(_) => cuenv_events::REDACTED_PLACEHOLDER.to_string(),
180 EnvValue::Interpolated(parts) => Self::parts_to_string_value(parts),
181 EnvValue::WithPolicies(var) => match &var.value {
182 EnvValueSimple::String(s) => s.clone(),
183 EnvValueSimple::Int(i) => i.to_string(),
184 EnvValueSimple::Bool(b) => b.to_string(),
185 EnvValueSimple::Secret(_) => cuenv_events::REDACTED_PLACEHOLDER.to_string(),
186 EnvValueSimple::Interpolated(parts) => Self::parts_to_string_value(parts),
187 },
188 }
189 }
190
191 fn parts_to_string_value(parts: &[EnvPart]) -> String {
193 parts
194 .iter()
195 .map(|p| match p {
196 EnvPart::Literal(s) => s.clone(),
197 EnvPart::Secret(_) => cuenv_events::REDACTED_PLACEHOLDER.to_string(),
198 })
199 .collect()
200 }
201
202 #[must_use]
204 pub fn is_secret(&self) -> bool {
205 match self {
206 EnvValue::Secret(_) => true,
207 EnvValue::Interpolated(parts) => parts.iter().any(EnvPart::is_secret),
208 EnvValue::WithPolicies(var) => match &var.value {
209 EnvValueSimple::Secret(_) => true,
210 EnvValueSimple::Interpolated(parts) => parts.iter().any(EnvPart::is_secret),
211 _ => false,
212 },
213 _ => false,
214 }
215 }
216
217 pub async fn resolve(&self) -> crate::Result<String> {
222 let (resolved, _) = self.resolve_with_secrets().await?;
223 Ok(resolved)
224 }
225
226 pub async fn resolve_with_secrets(&self) -> crate::Result<(String, Vec<String>)> {
232 match self {
233 EnvValue::String(s) => Ok((s.clone(), vec![])),
234 EnvValue::Int(i) => Ok((i.to_string(), vec![])),
235 EnvValue::Bool(b) => Ok((b.to_string(), vec![])),
236 EnvValue::Secret(s) => {
237 let resolved = s.resolve().await?;
238 Ok((resolved.clone(), vec![resolved]))
239 }
240 EnvValue::Interpolated(parts) => Self::resolve_parts_with_secrets(parts).await,
241 EnvValue::WithPolicies(var) => Self::resolve_simple_with_secrets(&var.value).await,
242 }
243 }
244
245 async fn resolve_parts_with_secrets(parts: &[EnvPart]) -> crate::Result<(String, Vec<String>)> {
247 let mut result = String::new();
248 let mut secrets = Vec::new();
249
250 for part in parts {
251 match part {
252 EnvPart::Literal(s) => result.push_str(s),
253 EnvPart::Secret(s) => {
254 let resolved = s.resolve().await?;
255 result.push_str(&resolved);
256 secrets.push(resolved);
257 }
258 }
259 }
260
261 Ok((result, secrets))
262 }
263
264 async fn resolve_simple_with_secrets(
266 value: &EnvValueSimple,
267 ) -> crate::Result<(String, Vec<String>)> {
268 match value {
269 EnvValueSimple::String(s) => Ok((s.clone(), vec![])),
270 EnvValueSimple::Int(i) => Ok((i.to_string(), vec![])),
271 EnvValueSimple::Bool(b) => Ok((b.to_string(), vec![])),
272 EnvValueSimple::Secret(s) => {
273 let resolved = s.resolve().await?;
274 Ok((resolved.clone(), vec![resolved]))
275 }
276 EnvValueSimple::Interpolated(parts) => Self::resolve_parts_with_secrets(parts).await,
277 }
278 }
279
280 fn collect_secrets(&self) -> Vec<(usize, &crate::secrets::Secret)> {
285 match self {
286 EnvValue::Secret(s) => vec![(0, s)],
287 EnvValue::Interpolated(parts) => Self::collect_secrets_from_parts(parts),
288 EnvValue::WithPolicies(var) => match &var.value {
289 EnvValueSimple::Secret(s) => vec![(0, s)],
290 EnvValueSimple::Interpolated(parts) => Self::collect_secrets_from_parts(parts),
291 _ => vec![],
292 },
293 _ => vec![],
294 }
295 }
296
297 fn collect_secrets_from_parts(parts: &[EnvPart]) -> Vec<(usize, &crate::secrets::Secret)> {
299 parts
300 .iter()
301 .enumerate()
302 .filter_map(|(i, part)| match part {
303 EnvPart::Secret(s) => Some((i, s)),
304 EnvPart::Literal(_) => None,
305 })
306 .collect()
307 }
308
309 fn reassemble_with_resolved(
314 &self,
315 resolved_secrets: &HashMap<usize, String>,
316 ) -> (String, Vec<String>) {
317 match self {
318 EnvValue::String(s) => (s.clone(), vec![]),
319 EnvValue::Int(i) => (i.to_string(), vec![]),
320 EnvValue::Bool(b) => (b.to_string(), vec![]),
321 EnvValue::Secret(_) => {
322 let val = resolved_secrets.get(&0).cloned().unwrap_or_default();
323 (val.clone(), vec![val])
324 }
325 EnvValue::Interpolated(parts) => Self::reassemble_parts(parts, resolved_secrets),
326 EnvValue::WithPolicies(var) => match &var.value {
327 EnvValueSimple::String(s) => (s.clone(), vec![]),
328 EnvValueSimple::Int(i) => (i.to_string(), vec![]),
329 EnvValueSimple::Bool(b) => (b.to_string(), vec![]),
330 EnvValueSimple::Secret(_) => {
331 let val = resolved_secrets.get(&0).cloned().unwrap_or_default();
332 (val.clone(), vec![val])
333 }
334 EnvValueSimple::Interpolated(parts) => {
335 Self::reassemble_parts(parts, resolved_secrets)
336 }
337 },
338 }
339 }
340
341 fn reassemble_parts(
343 parts: &[EnvPart],
344 resolved_secrets: &HashMap<usize, String>,
345 ) -> (String, Vec<String>) {
346 let mut result = String::new();
347 let mut secrets = Vec::new();
348 for (i, part) in parts.iter().enumerate() {
349 match part {
350 EnvPart::Literal(s) => result.push_str(s),
351 EnvPart::Secret(_) => {
352 if let Some(val) = resolved_secrets.get(&i) {
353 result.push_str(val);
354 secrets.push(val.clone());
355 }
356 }
357 }
358 }
359 (result, secrets)
360 }
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize, Default)]
365pub struct Environment {
366 #[serde(flatten)]
368 pub vars: HashMap<String, String>,
369}
370
371impl Environment {
372 pub fn new() -> Self {
374 Self::default()
375 }
376
377 pub fn from_map(vars: HashMap<String, String>) -> Self {
379 Self { vars }
380 }
381
382 pub fn get(&self, key: &str) -> Option<&str> {
384 self.vars.get(key).map(|s| s.as_str())
385 }
386
387 pub fn set(&mut self, key: String, value: String) {
389 self.vars.insert(key, value);
390 }
391
392 pub fn contains(&self, key: &str) -> bool {
394 self.vars.contains_key(key)
395 }
396
397 pub fn to_env_vec(&self) -> Vec<String> {
399 self.vars
400 .iter()
401 .map(|(k, v)| format!("{}={}", k, v))
402 .collect()
403 }
404
405 pub fn merge_with_system(&self) -> HashMap<String, String> {
408 let mut merged: HashMap<String, String> = env::vars().collect();
409
410 for (key, value) in &self.vars {
412 merged.insert(key.clone(), value.clone());
413 }
414
415 merged
416 }
417
418 const HERMETIC_ALLOWED_VARS: &'static [&'static str] = &[
421 "HOME",
422 "USER",
423 "LOGNAME",
424 "SHELL",
425 "TERM",
426 "COLORTERM",
427 "LANG",
428 "LC_ALL",
429 "LC_CTYPE",
430 "LC_MESSAGES",
431 "TMPDIR",
432 "TMP",
433 "TEMP",
434 "XDG_RUNTIME_DIR",
435 "XDG_CONFIG_HOME",
436 "XDG_CACHE_HOME",
437 "XDG_DATA_HOME",
438 ];
439
440 pub fn merge_with_system_hermetic(&self) -> HashMap<String, String> {
452 let mut merged: HashMap<String, String> = HashMap::new();
453
454 for var in Self::HERMETIC_ALLOWED_VARS {
456 if let Ok(value) = env::var(var) {
457 merged.insert((*var).to_string(), value);
458 }
459 }
460
461 for (key, value) in env::vars() {
463 if key.starts_with("LC_") {
464 merged.insert(key, value);
465 }
466 }
467
468 for (key, value) in &self.vars {
470 merged.insert(key.clone(), value.clone());
471 }
472
473 merged
474 }
475
476 pub fn to_full_env_vec(&self) -> Vec<String> {
478 self.merge_with_system()
479 .iter()
480 .map(|(k, v)| format!("{}={}", k, v))
481 .collect()
482 }
483
484 pub fn len(&self) -> usize {
486 self.vars.len()
487 }
488
489 pub fn is_empty(&self) -> bool {
491 self.vars.is_empty()
492 }
493
494 pub fn resolve_command(&self, command: &str) -> String {
502 if command.starts_with('/') {
504 tracing::debug!(command = %command, "Command is already absolute path");
505 return command.to_string();
506 }
507
508 let path_value = self
510 .vars
511 .get("PATH")
512 .cloned()
513 .or_else(|| env::var("PATH").ok())
514 .unwrap_or_default();
515
516 tracing::debug!(
517 command = %command,
518 env_has_path = self.vars.contains_key("PATH"),
519 path_len = path_value.len(),
520 "Resolving command in PATH"
521 );
522
523 for dir in path_value.split(':') {
525 if dir.is_empty() {
526 continue;
527 }
528 let candidate = std::path::Path::new(dir).join(command);
529 if candidate.is_file() {
530 #[cfg(unix)]
532 {
533 use std::os::unix::fs::PermissionsExt;
534 if let Ok(metadata) = std::fs::metadata(&candidate) {
535 let permissions = metadata.permissions();
536 if permissions.mode() & 0o111 != 0 {
537 tracing::debug!(
538 command = %command,
539 resolved = %candidate.display(),
540 "Command resolved to path"
541 );
542 return candidate.to_string_lossy().to_string();
543 }
544 }
545 }
546 #[cfg(not(unix))]
547 {
548 tracing::debug!(
549 command = %command,
550 resolved = %candidate.display(),
551 "Command resolved to path"
552 );
553 return candidate.to_string_lossy().to_string();
554 }
555 }
556 }
557
558 if self.vars.contains_key("PATH")
562 && let Ok(system_path) = env::var("PATH")
563 {
564 tracing::debug!(
565 command = %command,
566 "Command not found in env PATH, trying system PATH"
567 );
568 for dir in system_path.split(':') {
569 if dir.is_empty() {
570 continue;
571 }
572 let candidate = std::path::Path::new(dir).join(command);
573 if candidate.is_file() {
574 #[cfg(unix)]
575 {
576 use std::os::unix::fs::PermissionsExt;
577 if let Ok(metadata) = std::fs::metadata(&candidate) {
578 let permissions = metadata.permissions();
579 if permissions.mode() & 0o111 != 0 {
580 tracing::debug!(
581 command = %command,
582 resolved = %candidate.display(),
583 "Command resolved from system PATH"
584 );
585 return candidate.to_string_lossy().to_string();
586 }
587 }
588 }
589 #[cfg(not(unix))]
590 {
591 tracing::debug!(
592 command = %command,
593 resolved = %candidate.display(),
594 "Command resolved from system PATH"
595 );
596 return candidate.to_string_lossy().to_string();
597 }
598 }
599 }
600 }
601
602 tracing::warn!(
604 command = %command,
605 env_path_set = self.vars.contains_key("PATH"),
606 "Command not found in PATH, returning original"
607 );
608 command.to_string()
609 }
610
611 pub fn iter(&self) -> impl Iterator<Item = (&String, &String)> {
613 self.vars.iter()
614 }
615
616 pub fn build_for_task(
618 task_name: &str,
619 env_vars: &HashMap<String, EnvValue>,
620 ) -> HashMap<String, String> {
621 env_vars
622 .iter()
623 .filter(|(_, value)| value.is_accessible_by_task(task_name))
624 .map(|(key, value)| (key.clone(), value.to_string_value()))
625 .collect()
626 }
627
628 pub async fn resolve_all_with_secrets(
634 env_vars: &HashMap<String, EnvValue>,
635 ) -> crate::Result<(HashMap<String, String>, Vec<String>)> {
636 let all: Vec<_> = env_vars.iter().collect();
637 Self::resolve_filtered_with_secrets(&all).await
638 }
639
640 pub async fn resolve_for_task(
645 task_name: &str,
646 env_vars: &HashMap<String, EnvValue>,
647 ) -> crate::Result<HashMap<String, String>> {
648 let (resolved, _secrets) = Self::resolve_for_task_with_secrets(task_name, env_vars).await?;
649 Ok(resolved)
650 }
651
652 pub async fn resolve_for_task_with_secrets(
662 task_name: &str,
663 env_vars: &HashMap<String, EnvValue>,
664 ) -> crate::Result<(HashMap<String, String>, Vec<String>)> {
665 tracing::debug!(
666 task = task_name,
667 env_count = env_vars.len(),
668 "resolve_for_task_with_secrets"
669 );
670
671 let accessible: Vec<_> = env_vars
672 .iter()
673 .filter(|(_, value)| value.is_accessible_by_task(task_name))
674 .collect();
675
676 Self::resolve_filtered_with_secrets(&accessible).await
677 }
678
679 pub async fn resolve_for_service_with_secrets(
689 service_name: &str,
690 env_vars: &HashMap<String, EnvValue>,
691 ) -> crate::Result<(HashMap<String, String>, Vec<String>)> {
692 tracing::debug!(
693 service = service_name,
694 env_count = env_vars.len(),
695 "resolve_for_service_with_secrets"
696 );
697
698 let accessible: Vec<_> = env_vars
699 .iter()
700 .filter(|(_, value)| value.is_accessible_by_task(service_name))
701 .collect();
702
703 Self::resolve_filtered_with_secrets(&accessible).await
704 }
705
706 pub fn build_for_exec(
708 command: &str,
709 env_vars: &HashMap<String, EnvValue>,
710 ) -> HashMap<String, String> {
711 env_vars
712 .iter()
713 .filter(|(_, value)| value.is_accessible_by_exec(command))
714 .map(|(key, value)| (key.clone(), value.to_string_value()))
715 .collect()
716 }
717
718 pub async fn resolve_for_exec(
723 command: &str,
724 env_vars: &HashMap<String, EnvValue>,
725 ) -> crate::Result<HashMap<String, String>> {
726 let (resolved, _secrets) = Self::resolve_for_exec_with_secrets(command, env_vars).await?;
727 Ok(resolved)
728 }
729
730 pub async fn resolve_for_exec_with_secrets(
740 command: &str,
741 env_vars: &HashMap<String, EnvValue>,
742 ) -> crate::Result<(HashMap<String, String>, Vec<String>)> {
743 let accessible: Vec<_> = env_vars
744 .iter()
745 .filter(|(_, value)| value.is_accessible_by_exec(command))
746 .collect();
747
748 Self::resolve_filtered_with_secrets(&accessible).await
749 }
750
751 async fn resolve_filtered_with_secrets(
763 accessible: &[(&String, &EnvValue)],
764 ) -> crate::Result<(HashMap<String, String>, Vec<String>)> {
765 let mut resolved = HashMap::new();
766 let mut all_secrets = Vec::new();
767
768 type SecretVarEntry<'a> = (
770 &'a String,
771 &'a EnvValue,
772 Vec<(usize, crate::secrets::Secret)>,
773 );
774 let mut secret_vars: Vec<SecretVarEntry<'_>> = Vec::new();
775
776 for (key, value) in accessible {
777 let collected = value.collect_secrets();
778 if collected.is_empty() {
779 resolved.insert((*key).clone(), value.to_string_value());
781 } else {
782 let owned_secrets: Vec<(usize, crate::secrets::Secret)> = collected
783 .into_iter()
784 .map(|(idx, s)| (idx, s.clone()))
785 .collect();
786 secret_vars.push((key, value, owned_secrets));
787 }
788 }
789
790 if secret_vars.is_empty() {
792 return Ok((resolved, all_secrets));
793 }
794
795 let registry = Arc::new(crate::secrets::create_default_registry()?);
797 let mut join_set = tokio::task::JoinSet::new();
798
799 for (key, _, secrets) in &secret_vars {
800 for (part_idx, secret) in secrets {
801 let key = (*key).clone();
802 let part_idx = *part_idx;
803 let secret = secret.clone();
804 let registry = Arc::clone(®istry);
805 join_set.spawn(async move {
806 let value = secret.resolve_with_registry(®istry).await?;
807 Ok::<_, crate::Error>((key, part_idx, value))
808 });
809 }
810 }
811
812 let mut resolved_by_key: HashMap<String, HashMap<usize, String>> = HashMap::new();
814 while let Some(result) = join_set.join_next().await {
815 let (key, part_idx, value) = result.map_err(|e| {
816 crate::Error::configuration(format!("Secret resolution task panicked: {e}"))
817 })??;
818 resolved_by_key
819 .entry(key)
820 .or_default()
821 .insert(part_idx, value);
822 }
823
824 for (key, value, _) in &secret_vars {
826 let key_resolved = resolved_by_key.get(*key).cloned().unwrap_or_default();
827 let (final_value, mut value_secrets) = value.reassemble_with_resolved(&key_resolved);
828 if !value_secrets.is_empty() {
829 tracing::debug!(
830 key = *key,
831 secret_count = value_secrets.len(),
832 "resolved secrets"
833 );
834 }
835 all_secrets.append(&mut value_secrets);
836 resolved.insert((*key).clone(), final_value);
837 }
838
839 Ok((resolved, all_secrets))
840 }
841}
842
843#[cfg(test)]
844mod tests {
845 use super::*;
846
847 #[test]
848 fn test_environment_basics() {
849 let mut env = Environment::new();
850 assert!(env.is_empty());
851
852 env.set("FOO".to_string(), "bar".to_string());
853 assert_eq!(env.len(), 1);
854 assert!(env.contains("FOO"));
855 assert_eq!(env.get("FOO"), Some("bar"));
856 assert!(!env.contains("BAR"));
857 }
858
859 #[test]
860 fn test_environment_from_map() {
861 let mut vars = HashMap::new();
862 vars.insert("KEY1".to_string(), "value1".to_string());
863 vars.insert("KEY2".to_string(), "value2".to_string());
864
865 let env = Environment::from_map(vars);
866 assert_eq!(env.len(), 2);
867 assert_eq!(env.get("KEY1"), Some("value1"));
868 assert_eq!(env.get("KEY2"), Some("value2"));
869 }
870
871 #[test]
872 fn test_environment_to_vec() {
873 let mut env = Environment::new();
874 env.set("VAR1".to_string(), "val1".to_string());
875 env.set("VAR2".to_string(), "val2".to_string());
876
877 let vec = env.to_env_vec();
878 assert_eq!(vec.len(), 2);
879 assert!(vec.contains(&"VAR1=val1".to_string()));
880 assert!(vec.contains(&"VAR2=val2".to_string()));
881 }
882
883 #[test]
884 fn test_environment_merge_with_system() {
885 let mut env = Environment::new();
886 env.set("PATH".to_string(), "/custom/path".to_string());
887 env.set("CUSTOM_VAR".to_string(), "custom_value".to_string());
888
889 let merged = env.merge_with_system();
890
891 assert_eq!(merged.get("PATH"), Some(&"/custom/path".to_string()));
893 assert_eq!(merged.get("CUSTOM_VAR"), Some(&"custom_value".to_string()));
894
895 assert!(merged.len() >= 2);
898 }
899
900 #[test]
901 fn test_environment_iteration() {
902 let mut env = Environment::new();
903 env.set("A".to_string(), "1".to_string());
904 env.set("B".to_string(), "2".to_string());
905
906 let mut count = 0;
907 for (key, value) in env.iter() {
908 assert!(key == "A" || key == "B");
909 assert!(value == "1" || value == "2");
910 count += 1;
911 }
912 assert_eq!(count, 2);
913 }
914
915 #[test]
916 fn test_env_value_types() {
917 let str_val = EnvValue::String("test".to_string());
918 let int_val = EnvValue::Int(42);
919 let bool_val = EnvValue::Bool(true);
920
921 assert_eq!(str_val, EnvValue::String("test".to_string()));
922 assert_eq!(int_val, EnvValue::Int(42));
923 assert_eq!(bool_val, EnvValue::Bool(true));
924 }
925
926 #[test]
927 fn test_policy_task_access() {
928 let simple_var = EnvValue::String("simple".to_string());
930 assert!(simple_var.is_accessible_by_task("any_task"));
931
932 let no_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
934 value: EnvValueSimple::String("value".to_string()),
935 policies: None,
936 });
937 assert!(no_policy_var.is_accessible_by_task("any_task"));
938
939 let empty_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
941 value: EnvValueSimple::String("value".to_string()),
942 policies: Some(vec![]),
943 });
944 assert!(empty_policy_var.is_accessible_by_task("any_task"));
945
946 let restricted_var = EnvValue::WithPolicies(EnvVarWithPolicies {
948 value: EnvValueSimple::String("secret".to_string()),
949 policies: Some(vec![Policy {
950 allow_tasks: Some(vec!["deploy".to_string(), "release".to_string()]),
951 allow_exec: None,
952 }]),
953 });
954 assert!(restricted_var.is_accessible_by_task("deploy"));
955 assert!(restricted_var.is_accessible_by_task("release"));
956 assert!(!restricted_var.is_accessible_by_task("test"));
957 assert!(!restricted_var.is_accessible_by_task("build"));
958 }
959
960 #[test]
961 fn test_policy_exec_access() {
962 let simple_var = EnvValue::String("simple".to_string());
964 assert!(simple_var.is_accessible_by_exec("bash"));
965
966 let restricted_var = EnvValue::WithPolicies(EnvVarWithPolicies {
968 value: EnvValueSimple::String("secret".to_string()),
969 policies: Some(vec![Policy {
970 allow_tasks: None,
971 allow_exec: Some(vec!["kubectl".to_string(), "terraform".to_string()]),
972 }]),
973 });
974 assert!(restricted_var.is_accessible_by_exec("kubectl"));
975 assert!(restricted_var.is_accessible_by_exec("terraform"));
976 assert!(!restricted_var.is_accessible_by_exec("bash"));
977 assert!(!restricted_var.is_accessible_by_exec("sh"));
978 }
979
980 #[test]
981 fn test_multiple_policies() {
982 let multi_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
984 value: EnvValueSimple::String("value".to_string()),
985 policies: Some(vec![
986 Policy {
987 allow_tasks: Some(vec!["task1".to_string()]),
988 allow_exec: None,
989 },
990 Policy {
991 allow_tasks: Some(vec!["task2".to_string()]),
992 allow_exec: Some(vec!["kubectl".to_string()]),
993 },
994 ]),
995 });
996
997 assert!(multi_policy_var.is_accessible_by_task("task1"));
999 assert!(multi_policy_var.is_accessible_by_task("task2"));
1000 assert!(!multi_policy_var.is_accessible_by_task("task3"));
1001
1002 assert!(multi_policy_var.is_accessible_by_exec("kubectl"));
1004 assert!(!multi_policy_var.is_accessible_by_exec("bash"));
1005 }
1006
1007 #[test]
1008 fn test_to_string_value() {
1009 assert_eq!(
1010 EnvValue::String("test".to_string()).to_string_value(),
1011 "test"
1012 );
1013 assert_eq!(EnvValue::Int(42).to_string_value(), "42");
1014 assert_eq!(EnvValue::Bool(true).to_string_value(), "true");
1015 assert_eq!(EnvValue::Bool(false).to_string_value(), "false");
1016
1017 let with_policies = EnvValue::WithPolicies(EnvVarWithPolicies {
1018 value: EnvValueSimple::String("policy_value".to_string()),
1019 policies: Some(vec![]),
1020 });
1021 assert_eq!(with_policies.to_string_value(), "policy_value");
1022 }
1023
1024 #[test]
1025 fn test_build_for_task() {
1026 let mut env_vars = HashMap::new();
1027
1028 env_vars.insert(
1030 "PUBLIC".to_string(),
1031 EnvValue::String("public_value".to_string()),
1032 );
1033
1034 env_vars.insert(
1036 "SECRET".to_string(),
1037 EnvValue::WithPolicies(EnvVarWithPolicies {
1038 value: EnvValueSimple::String("secret_value".to_string()),
1039 policies: Some(vec![Policy {
1040 allow_tasks: Some(vec!["deploy".to_string()]),
1041 allow_exec: None,
1042 }]),
1043 }),
1044 );
1045
1046 let deploy_env = Environment::build_for_task("deploy", &env_vars);
1048 assert_eq!(deploy_env.len(), 2);
1049 assert_eq!(deploy_env.get("PUBLIC"), Some(&"public_value".to_string()));
1050 assert_eq!(deploy_env.get("SECRET"), Some(&"secret_value".to_string()));
1051
1052 let test_env = Environment::build_for_task("test", &env_vars);
1054 assert_eq!(test_env.len(), 1);
1055 assert_eq!(test_env.get("PUBLIC"), Some(&"public_value".to_string()));
1056 assert_eq!(test_env.get("SECRET"), None);
1057 }
1058
1059 #[test]
1060 fn test_build_for_exec() {
1061 let mut env_vars = HashMap::new();
1062
1063 env_vars.insert(
1065 "PUBLIC".to_string(),
1066 EnvValue::String("public_value".to_string()),
1067 );
1068
1069 env_vars.insert(
1071 "SECRET".to_string(),
1072 EnvValue::WithPolicies(EnvVarWithPolicies {
1073 value: EnvValueSimple::String("secret_value".to_string()),
1074 policies: Some(vec![Policy {
1075 allow_tasks: None,
1076 allow_exec: Some(vec!["kubectl".to_string()]),
1077 }]),
1078 }),
1079 );
1080
1081 let kubectl_env = Environment::build_for_exec("kubectl", &env_vars);
1083 assert_eq!(kubectl_env.len(), 2);
1084 assert_eq!(kubectl_env.get("PUBLIC"), Some(&"public_value".to_string()));
1085 assert_eq!(kubectl_env.get("SECRET"), Some(&"secret_value".to_string()));
1086
1087 let bash_env = Environment::build_for_exec("bash", &env_vars);
1089 assert_eq!(bash_env.len(), 1);
1090 assert_eq!(bash_env.get("PUBLIC"), Some(&"public_value".to_string()));
1091 assert_eq!(bash_env.get("SECRET"), None);
1092 }
1093
1094 #[test]
1095 fn test_env_for_environment() {
1096 let mut base = HashMap::new();
1097 base.insert("BASE_VAR".to_string(), EnvValue::String("base".to_string()));
1098 base.insert(
1099 "OVERRIDE_ME".to_string(),
1100 EnvValue::String("original".to_string()),
1101 );
1102
1103 let mut dev_env = HashMap::new();
1104 dev_env.insert(
1105 "OVERRIDE_ME".to_string(),
1106 EnvValue::String("dev".to_string()),
1107 );
1108 dev_env.insert(
1109 "DEV_VAR".to_string(),
1110 EnvValue::String("development".to_string()),
1111 );
1112
1113 let mut environments = HashMap::new();
1114 environments.insert("development".to_string(), dev_env);
1115
1116 let env = Env {
1117 base,
1118 environment: Some(environments),
1119 };
1120
1121 let dev_vars = env.for_environment("development");
1122 assert_eq!(
1123 dev_vars.get("BASE_VAR"),
1124 Some(&EnvValue::String("base".to_string()))
1125 );
1126 assert_eq!(
1127 dev_vars.get("OVERRIDE_ME"),
1128 Some(&EnvValue::String("dev".to_string()))
1129 );
1130 assert_eq!(
1131 dev_vars.get("DEV_VAR"),
1132 Some(&EnvValue::String("development".to_string()))
1133 );
1134 }
1135
1136 #[test]
1137 fn test_env_deserialize_with_environment_overrides() {
1138 let json = r#"{
1139 "API_URL": "https://api.example.com",
1140 "environment": {
1141 "production": {
1142 "API_URL": "https://api.prod.example.com",
1143 "AUTH_SECRET": {"resolver": "exec", "command": "echo", "args": ["token"]}
1144 }
1145 }
1146 }"#;
1147
1148 let env: Env = serde_json::from_str(json).expect("valid env payload");
1149
1150 assert!(env.base.contains_key("API_URL"));
1151 assert!(!env.base.contains_key("environment"));
1152
1153 let environments = env
1154 .environment
1155 .expect("environment overrides should deserialize");
1156 let production = environments
1157 .get("production")
1158 .expect("production overrides should exist");
1159 assert!(production.contains_key("AUTH_SECRET"));
1160 }
1161
1162 #[tokio::test]
1163 async fn test_resolve_plain_string() {
1164 let env_val = EnvValue::String("plain_value".to_string());
1165 let resolved = env_val.resolve().await.unwrap();
1166 assert_eq!(resolved, "plain_value");
1167 }
1168
1169 #[tokio::test]
1170 async fn test_resolve_int() {
1171 let env_val = EnvValue::Int(42);
1172 let resolved = env_val.resolve().await.unwrap();
1173 assert_eq!(resolved, "42");
1174 }
1175
1176 #[tokio::test]
1177 async fn test_resolve_bool() {
1178 let env_val = EnvValue::Bool(true);
1179 let resolved = env_val.resolve().await.unwrap();
1180 assert_eq!(resolved, "true");
1181 }
1182
1183 #[tokio::test]
1184 async fn test_resolve_with_policies_plain_string() {
1185 let env_val = EnvValue::WithPolicies(EnvVarWithPolicies {
1186 value: EnvValueSimple::String("policy_value".to_string()),
1187 policies: None,
1188 });
1189 let resolved = env_val.resolve().await.unwrap();
1190 assert_eq!(resolved, "policy_value");
1191 }
1192
1193 #[test]
1198 fn test_env_part_literal() {
1199 let part = EnvPart::Literal("hello".to_string());
1200 assert!(!part.is_secret());
1201 }
1202
1203 #[test]
1204 fn test_env_part_secret() {
1205 let secret = crate::secrets::Secret::new("echo".to_string(), vec!["test".to_string()]);
1206 let part = EnvPart::Secret(secret);
1207 assert!(part.is_secret());
1208 }
1209
1210 #[test]
1211 fn test_env_part_deserialization_literal() {
1212 let json = r#""hello""#;
1213 let part: EnvPart = serde_json::from_str(json).unwrap();
1214 assert!(matches!(part, EnvPart::Literal(ref s) if s == "hello"));
1215 assert!(!part.is_secret());
1216 }
1217
1218 #[test]
1219 fn test_env_part_deserialization_secret() {
1220 let json = r#"{"resolver": "exec", "command": "echo", "args": ["test"]}"#;
1221 let part: EnvPart = serde_json::from_str(json).unwrap();
1222 assert!(part.is_secret());
1223 }
1224
1225 #[test]
1226 fn test_env_value_interpolated_deserialization() {
1227 let json =
1228 r#"["prefix-", {"resolver": "exec", "command": "gh", "args": ["auth", "token"]}]"#;
1229 let value: EnvValue = serde_json::from_str(json).unwrap();
1230 assert!(matches!(value, EnvValue::Interpolated(_)));
1231 assert!(value.is_secret());
1232 }
1233
1234 #[test]
1235 fn test_interpolated_is_secret_with_no_secrets() {
1236 let parts = vec![
1237 EnvPart::Literal("hello".to_string()),
1238 EnvPart::Literal("world".to_string()),
1239 ];
1240 let value = EnvValue::Interpolated(parts);
1241 assert!(!value.is_secret());
1242 }
1243
1244 #[test]
1245 fn test_interpolated_is_secret_with_secret() {
1246 let secret = crate::secrets::Secret::new("echo".to_string(), vec![]);
1247 let parts = vec![
1248 EnvPart::Literal("prefix".to_string()),
1249 EnvPart::Secret(secret),
1250 ];
1251 let value = EnvValue::Interpolated(parts);
1252 assert!(value.is_secret());
1253 }
1254
1255 #[test]
1256 fn test_interpolated_to_string_value_redacts_secrets() {
1257 let secret = crate::secrets::Secret::new(
1258 "gh".to_string(),
1259 vec!["auth".to_string(), "token".to_string()],
1260 );
1261 let parts = vec![
1262 EnvPart::Literal("access-tokens = github.com=".to_string()),
1263 EnvPart::Secret(secret),
1264 ];
1265 let value = EnvValue::Interpolated(parts);
1266 assert_eq!(value.to_string_value(), "access-tokens = github.com=*_*");
1267 }
1268
1269 #[test]
1270 fn test_interpolated_to_string_value_no_secrets() {
1271 let parts = vec![
1272 EnvPart::Literal("hello".to_string()),
1273 EnvPart::Literal("-".to_string()),
1274 EnvPart::Literal("world".to_string()),
1275 ];
1276 let value = EnvValue::Interpolated(parts);
1277 assert_eq!(value.to_string_value(), "hello-world");
1278 }
1279
1280 #[tokio::test]
1281 async fn test_resolve_with_secrets_collects_only_secret_parts() {
1282 let parts = vec![
1285 EnvPart::Literal("hello-".to_string()),
1286 EnvPart::Literal("world".to_string()),
1287 ];
1288 let value = EnvValue::Interpolated(parts);
1289 let (resolved, secrets) = value.resolve_with_secrets().await.unwrap();
1290 assert_eq!(resolved, "hello-world");
1291 assert!(secrets.is_empty()); }
1293
1294 #[tokio::test]
1295 async fn test_resolve_interpolated_concatenates_parts() {
1296 let parts = vec![
1297 EnvPart::Literal("a".to_string()),
1298 EnvPart::Literal("b".to_string()),
1299 EnvPart::Literal("c".to_string()),
1300 ];
1301 let value = EnvValue::Interpolated(parts);
1302 let resolved = value.resolve().await.unwrap();
1303 assert_eq!(resolved, "abc");
1304 }
1305
1306 #[test]
1307 fn test_interpolated_with_policies_is_secret() {
1308 let secret = crate::secrets::Secret::new("cmd".to_string(), vec![]);
1309 let parts = vec![
1310 EnvPart::Literal("prefix".to_string()),
1311 EnvPart::Secret(secret),
1312 ];
1313
1314 let value = EnvValue::WithPolicies(EnvVarWithPolicies {
1315 value: EnvValueSimple::Interpolated(parts),
1316 policies: Some(vec![Policy {
1317 allow_tasks: Some(vec!["deploy".to_string()]),
1318 allow_exec: None,
1319 }]),
1320 });
1321
1322 assert!(value.is_secret());
1323 }
1324
1325 #[test]
1326 fn test_interpolated_with_policies_to_string_value() {
1327 let secret = crate::secrets::Secret::new("cmd".to_string(), vec![]);
1328 let parts = vec![
1329 EnvPart::Literal("before-".to_string()),
1330 EnvPart::Secret(secret),
1331 EnvPart::Literal("-after".to_string()),
1332 ];
1333
1334 let value = EnvValue::WithPolicies(EnvVarWithPolicies {
1335 value: EnvValueSimple::Interpolated(parts),
1336 policies: None,
1337 });
1338
1339 assert_eq!(value.to_string_value(), "before-*_*-after");
1340 }
1341
1342 #[test]
1343 fn test_interpolated_accessible_by_task() {
1344 let parts = vec![EnvPart::Literal("value".to_string())];
1345 let value = EnvValue::Interpolated(parts);
1346 assert!(value.is_accessible_by_task("any_task"));
1348 }
1349
1350 #[test]
1351 fn test_extract_static_env_vars_skips_interpolated_secrets() {
1352 let secret = crate::secrets::Secret::new("cmd".to_string(), vec![]);
1354 let parts = vec![
1355 EnvPart::Literal("prefix".to_string()),
1356 EnvPart::Secret(secret),
1357 ];
1358
1359 let mut base = HashMap::new();
1360 base.insert("PLAIN".to_string(), EnvValue::String("value".to_string()));
1361 base.insert(
1362 "INTERPOLATED_SECRET".to_string(),
1363 EnvValue::Interpolated(parts),
1364 );
1365 base.insert(
1366 "INTERPOLATED_PLAIN".to_string(),
1367 EnvValue::Interpolated(vec![
1368 EnvPart::Literal("a".to_string()),
1369 EnvPart::Literal("b".to_string()),
1370 ]),
1371 );
1372
1373 let vars: HashMap<_, _> = base
1375 .iter()
1376 .filter(|(_, v)| !v.is_secret())
1377 .map(|(k, v)| (k.clone(), v.to_string_value()))
1378 .collect();
1379
1380 assert!(vars.contains_key("PLAIN"));
1381 assert!(!vars.contains_key("INTERPOLATED_SECRET"));
1382 assert!(vars.contains_key("INTERPOLATED_PLAIN"));
1383 assert_eq!(vars.get("INTERPOLATED_PLAIN"), Some(&"ab".to_string()));
1384 }
1385
1386 #[test]
1387 fn test_env_value_simple_interpolated_deserialization() {
1388 let json = r#"["a", "b", "c"]"#;
1390 let value: EnvValueSimple = serde_json::from_str(json).unwrap();
1391 assert!(matches!(value, EnvValueSimple::Interpolated(_)));
1392 }
1393
1394 #[test]
1395 fn test_env_value_with_policies_interpolated_deserialization() {
1396 let json = r#"{
1397 "value": ["prefix-", {"resolver": "exec", "command": "gh", "args": ["auth", "token"]}],
1398 "policies": [{"allowTasks": ["deploy"]}]
1399 }"#;
1400 let value: EnvValue = serde_json::from_str(json).unwrap();
1401 assert!(matches!(value, EnvValue::WithPolicies(_)));
1402 assert!(value.is_secret());
1403 }
1404
1405 #[test]
1406 fn test_interpolated_empty_array() {
1407 let parts = vec![];
1408 let value = EnvValue::Interpolated(parts);
1409 assert_eq!(value.to_string_value(), "");
1410 assert!(!value.is_secret());
1411 }
1412
1413 #[tokio::test]
1414 async fn test_resolve_interpolated_with_actual_secret() {
1415 let secret =
1416 crate::secrets::Secret::new("echo".to_string(), vec!["secret_value".to_string()]);
1417 let parts = vec![
1418 EnvPart::Literal("prefix-".to_string()),
1419 EnvPart::Secret(secret),
1420 EnvPart::Literal("-suffix".to_string()),
1421 ];
1422 let value = EnvValue::Interpolated(parts);
1423 let (resolved, secrets) = value.resolve_with_secrets().await.unwrap();
1424
1425 assert!(resolved.contains("prefix-"));
1426 assert!(resolved.contains("secret_value"));
1427 assert!(resolved.contains("-suffix"));
1428 assert_eq!(secrets.len(), 1);
1429 }
1430}