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 fn build_for_exec(
681 command: &str,
682 env_vars: &HashMap<String, EnvValue>,
683 ) -> HashMap<String, String> {
684 env_vars
685 .iter()
686 .filter(|(_, value)| value.is_accessible_by_exec(command))
687 .map(|(key, value)| (key.clone(), value.to_string_value()))
688 .collect()
689 }
690
691 pub async fn resolve_for_exec(
696 command: &str,
697 env_vars: &HashMap<String, EnvValue>,
698 ) -> crate::Result<HashMap<String, String>> {
699 let (resolved, _secrets) = Self::resolve_for_exec_with_secrets(command, env_vars).await?;
700 Ok(resolved)
701 }
702
703 pub async fn resolve_for_exec_with_secrets(
713 command: &str,
714 env_vars: &HashMap<String, EnvValue>,
715 ) -> crate::Result<(HashMap<String, String>, Vec<String>)> {
716 let accessible: Vec<_> = env_vars
717 .iter()
718 .filter(|(_, value)| value.is_accessible_by_exec(command))
719 .collect();
720
721 Self::resolve_filtered_with_secrets(&accessible).await
722 }
723
724 async fn resolve_filtered_with_secrets(
736 accessible: &[(&String, &EnvValue)],
737 ) -> crate::Result<(HashMap<String, String>, Vec<String>)> {
738 let mut resolved = HashMap::new();
739 let mut all_secrets = Vec::new();
740
741 type SecretVarEntry<'a> = (
743 &'a String,
744 &'a EnvValue,
745 Vec<(usize, crate::secrets::Secret)>,
746 );
747 let mut secret_vars: Vec<SecretVarEntry<'_>> = Vec::new();
748
749 for (key, value) in accessible {
750 let collected = value.collect_secrets();
751 if collected.is_empty() {
752 resolved.insert((*key).clone(), value.to_string_value());
754 } else {
755 let owned_secrets: Vec<(usize, crate::secrets::Secret)> = collected
756 .into_iter()
757 .map(|(idx, s)| (idx, s.clone()))
758 .collect();
759 secret_vars.push((key, value, owned_secrets));
760 }
761 }
762
763 if secret_vars.is_empty() {
765 return Ok((resolved, all_secrets));
766 }
767
768 let registry = Arc::new(crate::secrets::create_default_registry()?);
770 let mut join_set = tokio::task::JoinSet::new();
771
772 for (key, _, secrets) in &secret_vars {
773 for (part_idx, secret) in secrets {
774 let key = (*key).clone();
775 let part_idx = *part_idx;
776 let secret = secret.clone();
777 let registry = Arc::clone(®istry);
778 join_set.spawn(async move {
779 let value = secret.resolve_with_registry(®istry).await?;
780 Ok::<_, crate::Error>((key, part_idx, value))
781 });
782 }
783 }
784
785 let mut resolved_by_key: HashMap<String, HashMap<usize, String>> = HashMap::new();
787 while let Some(result) = join_set.join_next().await {
788 let (key, part_idx, value) = result.map_err(|e| {
789 crate::Error::configuration(format!("Secret resolution task panicked: {e}"))
790 })??;
791 resolved_by_key
792 .entry(key)
793 .or_default()
794 .insert(part_idx, value);
795 }
796
797 for (key, value, _) in &secret_vars {
799 let key_resolved = resolved_by_key.get(*key).cloned().unwrap_or_default();
800 let (final_value, mut value_secrets) = value.reassemble_with_resolved(&key_resolved);
801 if !value_secrets.is_empty() {
802 tracing::debug!(
803 key = *key,
804 secret_count = value_secrets.len(),
805 "resolved secrets"
806 );
807 }
808 all_secrets.append(&mut value_secrets);
809 resolved.insert((*key).clone(), final_value);
810 }
811
812 Ok((resolved, all_secrets))
813 }
814}
815
816#[cfg(test)]
817mod tests {
818 use super::*;
819
820 #[test]
821 fn test_environment_basics() {
822 let mut env = Environment::new();
823 assert!(env.is_empty());
824
825 env.set("FOO".to_string(), "bar".to_string());
826 assert_eq!(env.len(), 1);
827 assert!(env.contains("FOO"));
828 assert_eq!(env.get("FOO"), Some("bar"));
829 assert!(!env.contains("BAR"));
830 }
831
832 #[test]
833 fn test_environment_from_map() {
834 let mut vars = HashMap::new();
835 vars.insert("KEY1".to_string(), "value1".to_string());
836 vars.insert("KEY2".to_string(), "value2".to_string());
837
838 let env = Environment::from_map(vars);
839 assert_eq!(env.len(), 2);
840 assert_eq!(env.get("KEY1"), Some("value1"));
841 assert_eq!(env.get("KEY2"), Some("value2"));
842 }
843
844 #[test]
845 fn test_environment_to_vec() {
846 let mut env = Environment::new();
847 env.set("VAR1".to_string(), "val1".to_string());
848 env.set("VAR2".to_string(), "val2".to_string());
849
850 let vec = env.to_env_vec();
851 assert_eq!(vec.len(), 2);
852 assert!(vec.contains(&"VAR1=val1".to_string()));
853 assert!(vec.contains(&"VAR2=val2".to_string()));
854 }
855
856 #[test]
857 fn test_environment_merge_with_system() {
858 let mut env = Environment::new();
859 env.set("PATH".to_string(), "/custom/path".to_string());
860 env.set("CUSTOM_VAR".to_string(), "custom_value".to_string());
861
862 let merged = env.merge_with_system();
863
864 assert_eq!(merged.get("PATH"), Some(&"/custom/path".to_string()));
866 assert_eq!(merged.get("CUSTOM_VAR"), Some(&"custom_value".to_string()));
867
868 assert!(merged.len() >= 2);
871 }
872
873 #[test]
874 fn test_environment_iteration() {
875 let mut env = Environment::new();
876 env.set("A".to_string(), "1".to_string());
877 env.set("B".to_string(), "2".to_string());
878
879 let mut count = 0;
880 for (key, value) in env.iter() {
881 assert!(key == "A" || key == "B");
882 assert!(value == "1" || value == "2");
883 count += 1;
884 }
885 assert_eq!(count, 2);
886 }
887
888 #[test]
889 fn test_env_value_types() {
890 let str_val = EnvValue::String("test".to_string());
891 let int_val = EnvValue::Int(42);
892 let bool_val = EnvValue::Bool(true);
893
894 assert_eq!(str_val, EnvValue::String("test".to_string()));
895 assert_eq!(int_val, EnvValue::Int(42));
896 assert_eq!(bool_val, EnvValue::Bool(true));
897 }
898
899 #[test]
900 fn test_policy_task_access() {
901 let simple_var = EnvValue::String("simple".to_string());
903 assert!(simple_var.is_accessible_by_task("any_task"));
904
905 let no_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
907 value: EnvValueSimple::String("value".to_string()),
908 policies: None,
909 });
910 assert!(no_policy_var.is_accessible_by_task("any_task"));
911
912 let empty_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
914 value: EnvValueSimple::String("value".to_string()),
915 policies: Some(vec![]),
916 });
917 assert!(empty_policy_var.is_accessible_by_task("any_task"));
918
919 let restricted_var = EnvValue::WithPolicies(EnvVarWithPolicies {
921 value: EnvValueSimple::String("secret".to_string()),
922 policies: Some(vec![Policy {
923 allow_tasks: Some(vec!["deploy".to_string(), "release".to_string()]),
924 allow_exec: None,
925 }]),
926 });
927 assert!(restricted_var.is_accessible_by_task("deploy"));
928 assert!(restricted_var.is_accessible_by_task("release"));
929 assert!(!restricted_var.is_accessible_by_task("test"));
930 assert!(!restricted_var.is_accessible_by_task("build"));
931 }
932
933 #[test]
934 fn test_policy_exec_access() {
935 let simple_var = EnvValue::String("simple".to_string());
937 assert!(simple_var.is_accessible_by_exec("bash"));
938
939 let restricted_var = EnvValue::WithPolicies(EnvVarWithPolicies {
941 value: EnvValueSimple::String("secret".to_string()),
942 policies: Some(vec![Policy {
943 allow_tasks: None,
944 allow_exec: Some(vec!["kubectl".to_string(), "terraform".to_string()]),
945 }]),
946 });
947 assert!(restricted_var.is_accessible_by_exec("kubectl"));
948 assert!(restricted_var.is_accessible_by_exec("terraform"));
949 assert!(!restricted_var.is_accessible_by_exec("bash"));
950 assert!(!restricted_var.is_accessible_by_exec("sh"));
951 }
952
953 #[test]
954 fn test_multiple_policies() {
955 let multi_policy_var = EnvValue::WithPolicies(EnvVarWithPolicies {
957 value: EnvValueSimple::String("value".to_string()),
958 policies: Some(vec![
959 Policy {
960 allow_tasks: Some(vec!["task1".to_string()]),
961 allow_exec: None,
962 },
963 Policy {
964 allow_tasks: Some(vec!["task2".to_string()]),
965 allow_exec: Some(vec!["kubectl".to_string()]),
966 },
967 ]),
968 });
969
970 assert!(multi_policy_var.is_accessible_by_task("task1"));
972 assert!(multi_policy_var.is_accessible_by_task("task2"));
973 assert!(!multi_policy_var.is_accessible_by_task("task3"));
974
975 assert!(multi_policy_var.is_accessible_by_exec("kubectl"));
977 assert!(!multi_policy_var.is_accessible_by_exec("bash"));
978 }
979
980 #[test]
981 fn test_to_string_value() {
982 assert_eq!(
983 EnvValue::String("test".to_string()).to_string_value(),
984 "test"
985 );
986 assert_eq!(EnvValue::Int(42).to_string_value(), "42");
987 assert_eq!(EnvValue::Bool(true).to_string_value(), "true");
988 assert_eq!(EnvValue::Bool(false).to_string_value(), "false");
989
990 let with_policies = EnvValue::WithPolicies(EnvVarWithPolicies {
991 value: EnvValueSimple::String("policy_value".to_string()),
992 policies: Some(vec![]),
993 });
994 assert_eq!(with_policies.to_string_value(), "policy_value");
995 }
996
997 #[test]
998 fn test_build_for_task() {
999 let mut env_vars = HashMap::new();
1000
1001 env_vars.insert(
1003 "PUBLIC".to_string(),
1004 EnvValue::String("public_value".to_string()),
1005 );
1006
1007 env_vars.insert(
1009 "SECRET".to_string(),
1010 EnvValue::WithPolicies(EnvVarWithPolicies {
1011 value: EnvValueSimple::String("secret_value".to_string()),
1012 policies: Some(vec![Policy {
1013 allow_tasks: Some(vec!["deploy".to_string()]),
1014 allow_exec: None,
1015 }]),
1016 }),
1017 );
1018
1019 let deploy_env = Environment::build_for_task("deploy", &env_vars);
1021 assert_eq!(deploy_env.len(), 2);
1022 assert_eq!(deploy_env.get("PUBLIC"), Some(&"public_value".to_string()));
1023 assert_eq!(deploy_env.get("SECRET"), Some(&"secret_value".to_string()));
1024
1025 let test_env = Environment::build_for_task("test", &env_vars);
1027 assert_eq!(test_env.len(), 1);
1028 assert_eq!(test_env.get("PUBLIC"), Some(&"public_value".to_string()));
1029 assert_eq!(test_env.get("SECRET"), None);
1030 }
1031
1032 #[test]
1033 fn test_build_for_exec() {
1034 let mut env_vars = HashMap::new();
1035
1036 env_vars.insert(
1038 "PUBLIC".to_string(),
1039 EnvValue::String("public_value".to_string()),
1040 );
1041
1042 env_vars.insert(
1044 "SECRET".to_string(),
1045 EnvValue::WithPolicies(EnvVarWithPolicies {
1046 value: EnvValueSimple::String("secret_value".to_string()),
1047 policies: Some(vec![Policy {
1048 allow_tasks: None,
1049 allow_exec: Some(vec!["kubectl".to_string()]),
1050 }]),
1051 }),
1052 );
1053
1054 let kubectl_env = Environment::build_for_exec("kubectl", &env_vars);
1056 assert_eq!(kubectl_env.len(), 2);
1057 assert_eq!(kubectl_env.get("PUBLIC"), Some(&"public_value".to_string()));
1058 assert_eq!(kubectl_env.get("SECRET"), Some(&"secret_value".to_string()));
1059
1060 let bash_env = Environment::build_for_exec("bash", &env_vars);
1062 assert_eq!(bash_env.len(), 1);
1063 assert_eq!(bash_env.get("PUBLIC"), Some(&"public_value".to_string()));
1064 assert_eq!(bash_env.get("SECRET"), None);
1065 }
1066
1067 #[test]
1068 fn test_env_for_environment() {
1069 let mut base = HashMap::new();
1070 base.insert("BASE_VAR".to_string(), EnvValue::String("base".to_string()));
1071 base.insert(
1072 "OVERRIDE_ME".to_string(),
1073 EnvValue::String("original".to_string()),
1074 );
1075
1076 let mut dev_env = HashMap::new();
1077 dev_env.insert(
1078 "OVERRIDE_ME".to_string(),
1079 EnvValue::String("dev".to_string()),
1080 );
1081 dev_env.insert(
1082 "DEV_VAR".to_string(),
1083 EnvValue::String("development".to_string()),
1084 );
1085
1086 let mut environments = HashMap::new();
1087 environments.insert("development".to_string(), dev_env);
1088
1089 let env = Env {
1090 base,
1091 environment: Some(environments),
1092 };
1093
1094 let dev_vars = env.for_environment("development");
1095 assert_eq!(
1096 dev_vars.get("BASE_VAR"),
1097 Some(&EnvValue::String("base".to_string()))
1098 );
1099 assert_eq!(
1100 dev_vars.get("OVERRIDE_ME"),
1101 Some(&EnvValue::String("dev".to_string()))
1102 );
1103 assert_eq!(
1104 dev_vars.get("DEV_VAR"),
1105 Some(&EnvValue::String("development".to_string()))
1106 );
1107 }
1108
1109 #[test]
1110 fn test_env_deserialize_with_environment_overrides() {
1111 let json = r#"{
1112 "API_URL": "https://api.example.com",
1113 "environment": {
1114 "production": {
1115 "API_URL": "https://api.prod.example.com",
1116 "AUTH_SECRET": {"resolver": "exec", "command": "echo", "args": ["token"]}
1117 }
1118 }
1119 }"#;
1120
1121 let env: Env = serde_json::from_str(json).expect("valid env payload");
1122
1123 assert!(env.base.contains_key("API_URL"));
1124 assert!(!env.base.contains_key("environment"));
1125
1126 let environments = env
1127 .environment
1128 .expect("environment overrides should deserialize");
1129 let production = environments
1130 .get("production")
1131 .expect("production overrides should exist");
1132 assert!(production.contains_key("AUTH_SECRET"));
1133 }
1134
1135 #[tokio::test]
1136 async fn test_resolve_plain_string() {
1137 let env_val = EnvValue::String("plain_value".to_string());
1138 let resolved = env_val.resolve().await.unwrap();
1139 assert_eq!(resolved, "plain_value");
1140 }
1141
1142 #[tokio::test]
1143 async fn test_resolve_int() {
1144 let env_val = EnvValue::Int(42);
1145 let resolved = env_val.resolve().await.unwrap();
1146 assert_eq!(resolved, "42");
1147 }
1148
1149 #[tokio::test]
1150 async fn test_resolve_bool() {
1151 let env_val = EnvValue::Bool(true);
1152 let resolved = env_val.resolve().await.unwrap();
1153 assert_eq!(resolved, "true");
1154 }
1155
1156 #[tokio::test]
1157 async fn test_resolve_with_policies_plain_string() {
1158 let env_val = EnvValue::WithPolicies(EnvVarWithPolicies {
1159 value: EnvValueSimple::String("policy_value".to_string()),
1160 policies: None,
1161 });
1162 let resolved = env_val.resolve().await.unwrap();
1163 assert_eq!(resolved, "policy_value");
1164 }
1165
1166 #[test]
1171 fn test_env_part_literal() {
1172 let part = EnvPart::Literal("hello".to_string());
1173 assert!(!part.is_secret());
1174 }
1175
1176 #[test]
1177 fn test_env_part_secret() {
1178 let secret = crate::secrets::Secret::new("echo".to_string(), vec!["test".to_string()]);
1179 let part = EnvPart::Secret(secret);
1180 assert!(part.is_secret());
1181 }
1182
1183 #[test]
1184 fn test_env_part_deserialization_literal() {
1185 let json = r#""hello""#;
1186 let part: EnvPart = serde_json::from_str(json).unwrap();
1187 assert!(matches!(part, EnvPart::Literal(ref s) if s == "hello"));
1188 assert!(!part.is_secret());
1189 }
1190
1191 #[test]
1192 fn test_env_part_deserialization_secret() {
1193 let json = r#"{"resolver": "exec", "command": "echo", "args": ["test"]}"#;
1194 let part: EnvPart = serde_json::from_str(json).unwrap();
1195 assert!(part.is_secret());
1196 }
1197
1198 #[test]
1199 fn test_env_value_interpolated_deserialization() {
1200 let json =
1201 r#"["prefix-", {"resolver": "exec", "command": "gh", "args": ["auth", "token"]}]"#;
1202 let value: EnvValue = serde_json::from_str(json).unwrap();
1203 assert!(matches!(value, EnvValue::Interpolated(_)));
1204 assert!(value.is_secret());
1205 }
1206
1207 #[test]
1208 fn test_interpolated_is_secret_with_no_secrets() {
1209 let parts = vec![
1210 EnvPart::Literal("hello".to_string()),
1211 EnvPart::Literal("world".to_string()),
1212 ];
1213 let value = EnvValue::Interpolated(parts);
1214 assert!(!value.is_secret());
1215 }
1216
1217 #[test]
1218 fn test_interpolated_is_secret_with_secret() {
1219 let secret = crate::secrets::Secret::new("echo".to_string(), vec![]);
1220 let parts = vec![
1221 EnvPart::Literal("prefix".to_string()),
1222 EnvPart::Secret(secret),
1223 ];
1224 let value = EnvValue::Interpolated(parts);
1225 assert!(value.is_secret());
1226 }
1227
1228 #[test]
1229 fn test_interpolated_to_string_value_redacts_secrets() {
1230 let secret = crate::secrets::Secret::new(
1231 "gh".to_string(),
1232 vec!["auth".to_string(), "token".to_string()],
1233 );
1234 let parts = vec![
1235 EnvPart::Literal("access-tokens = github.com=".to_string()),
1236 EnvPart::Secret(secret),
1237 ];
1238 let value = EnvValue::Interpolated(parts);
1239 assert_eq!(value.to_string_value(), "access-tokens = github.com=*_*");
1240 }
1241
1242 #[test]
1243 fn test_interpolated_to_string_value_no_secrets() {
1244 let parts = vec![
1245 EnvPart::Literal("hello".to_string()),
1246 EnvPart::Literal("-".to_string()),
1247 EnvPart::Literal("world".to_string()),
1248 ];
1249 let value = EnvValue::Interpolated(parts);
1250 assert_eq!(value.to_string_value(), "hello-world");
1251 }
1252
1253 #[tokio::test]
1254 async fn test_resolve_with_secrets_collects_only_secret_parts() {
1255 let parts = vec![
1258 EnvPart::Literal("hello-".to_string()),
1259 EnvPart::Literal("world".to_string()),
1260 ];
1261 let value = EnvValue::Interpolated(parts);
1262 let (resolved, secrets) = value.resolve_with_secrets().await.unwrap();
1263 assert_eq!(resolved, "hello-world");
1264 assert!(secrets.is_empty()); }
1266
1267 #[tokio::test]
1268 async fn test_resolve_interpolated_concatenates_parts() {
1269 let parts = vec![
1270 EnvPart::Literal("a".to_string()),
1271 EnvPart::Literal("b".to_string()),
1272 EnvPart::Literal("c".to_string()),
1273 ];
1274 let value = EnvValue::Interpolated(parts);
1275 let resolved = value.resolve().await.unwrap();
1276 assert_eq!(resolved, "abc");
1277 }
1278
1279 #[test]
1280 fn test_interpolated_with_policies_is_secret() {
1281 let secret = crate::secrets::Secret::new("cmd".to_string(), vec![]);
1282 let parts = vec![
1283 EnvPart::Literal("prefix".to_string()),
1284 EnvPart::Secret(secret),
1285 ];
1286
1287 let value = EnvValue::WithPolicies(EnvVarWithPolicies {
1288 value: EnvValueSimple::Interpolated(parts),
1289 policies: Some(vec![Policy {
1290 allow_tasks: Some(vec!["deploy".to_string()]),
1291 allow_exec: None,
1292 }]),
1293 });
1294
1295 assert!(value.is_secret());
1296 }
1297
1298 #[test]
1299 fn test_interpolated_with_policies_to_string_value() {
1300 let secret = crate::secrets::Secret::new("cmd".to_string(), vec![]);
1301 let parts = vec![
1302 EnvPart::Literal("before-".to_string()),
1303 EnvPart::Secret(secret),
1304 EnvPart::Literal("-after".to_string()),
1305 ];
1306
1307 let value = EnvValue::WithPolicies(EnvVarWithPolicies {
1308 value: EnvValueSimple::Interpolated(parts),
1309 policies: None,
1310 });
1311
1312 assert_eq!(value.to_string_value(), "before-*_*-after");
1313 }
1314
1315 #[test]
1316 fn test_interpolated_accessible_by_task() {
1317 let parts = vec![EnvPart::Literal("value".to_string())];
1318 let value = EnvValue::Interpolated(parts);
1319 assert!(value.is_accessible_by_task("any_task"));
1321 }
1322
1323 #[test]
1324 fn test_extract_static_env_vars_skips_interpolated_secrets() {
1325 let secret = crate::secrets::Secret::new("cmd".to_string(), vec![]);
1327 let parts = vec![
1328 EnvPart::Literal("prefix".to_string()),
1329 EnvPart::Secret(secret),
1330 ];
1331
1332 let mut base = HashMap::new();
1333 base.insert("PLAIN".to_string(), EnvValue::String("value".to_string()));
1334 base.insert(
1335 "INTERPOLATED_SECRET".to_string(),
1336 EnvValue::Interpolated(parts),
1337 );
1338 base.insert(
1339 "INTERPOLATED_PLAIN".to_string(),
1340 EnvValue::Interpolated(vec![
1341 EnvPart::Literal("a".to_string()),
1342 EnvPart::Literal("b".to_string()),
1343 ]),
1344 );
1345
1346 let vars: HashMap<_, _> = base
1348 .iter()
1349 .filter(|(_, v)| !v.is_secret())
1350 .map(|(k, v)| (k.clone(), v.to_string_value()))
1351 .collect();
1352
1353 assert!(vars.contains_key("PLAIN"));
1354 assert!(!vars.contains_key("INTERPOLATED_SECRET"));
1355 assert!(vars.contains_key("INTERPOLATED_PLAIN"));
1356 assert_eq!(vars.get("INTERPOLATED_PLAIN"), Some(&"ab".to_string()));
1357 }
1358
1359 #[test]
1360 fn test_env_value_simple_interpolated_deserialization() {
1361 let json = r#"["a", "b", "c"]"#;
1363 let value: EnvValueSimple = serde_json::from_str(json).unwrap();
1364 assert!(matches!(value, EnvValueSimple::Interpolated(_)));
1365 }
1366
1367 #[test]
1368 fn test_env_value_with_policies_interpolated_deserialization() {
1369 let json = r#"{
1370 "value": ["prefix-", {"resolver": "exec", "command": "gh", "args": ["auth", "token"]}],
1371 "policies": [{"allowTasks": ["deploy"]}]
1372 }"#;
1373 let value: EnvValue = serde_json::from_str(json).unwrap();
1374 assert!(matches!(value, EnvValue::WithPolicies(_)));
1375 assert!(value.is_secret());
1376 }
1377
1378 #[test]
1379 fn test_interpolated_empty_array() {
1380 let parts = vec![];
1381 let value = EnvValue::Interpolated(parts);
1382 assert_eq!(value.to_string_value(), "");
1383 assert!(!value.is_secret());
1384 }
1385
1386 #[tokio::test]
1387 async fn test_resolve_interpolated_with_actual_secret() {
1388 let secret =
1389 crate::secrets::Secret::new("echo".to_string(), vec!["secret_value".to_string()]);
1390 let parts = vec![
1391 EnvPart::Literal("prefix-".to_string()),
1392 EnvPart::Secret(secret),
1393 EnvPart::Literal("-suffix".to_string()),
1394 ];
1395 let value = EnvValue::Interpolated(parts);
1396 let (resolved, secrets) = value.resolve_with_secrets().await.unwrap();
1397
1398 assert!(resolved.contains("prefix-"));
1399 assert!(resolved.contains("secret_value"));
1400 assert!(resolved.contains("-suffix"));
1401 assert_eq!(secrets.len(), 1);
1402 }
1403}