Skip to main content

harn_cli/package/
credential_environment.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3
4use super::{ConnectorCredentialEnvironmentManifest, ProviderSetupManifest};
5
6const MAX_ENTRIES: usize = 16;
7const MAX_NAMES_PER_SECRET: usize = 8;
8const MAX_NAME_BYTES: usize = 128;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum CredentialEnvironmentIssue {
12    TooManyEntries,
13    SecretNotRequired {
14        secret: String,
15    },
16    DuplicateSecret {
17        secret: String,
18    },
19    NoNames {
20        secret: String,
21    },
22    TooManyNames {
23        secret: String,
24    },
25    InvalidName {
26        name: String,
27    },
28    DuplicateName {
29        secret: String,
30        name: String,
31    },
32    NameAssignedToSeveralSecrets {
33        name: String,
34        first_secret: String,
35        second_secret: String,
36    },
37}
38
39impl fmt::Display for CredentialEnvironmentIssue {
40    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            Self::TooManyEntries => write!(formatter, "accepts at most {MAX_ENTRIES} entries"),
43            Self::SecretNotRequired { secret } => {
44                write!(
45                    formatter,
46                    "secret '{secret}' must also appear in required_secrets"
47                )
48            }
49            Self::DuplicateSecret { secret } => write!(formatter, "repeats secret '{secret}'"),
50            Self::NoNames { secret } => {
51                write!(
52                    formatter,
53                    "secret '{secret}' must declare at least one environment name"
54                )
55            }
56            Self::TooManyNames { secret } => write!(
57                formatter,
58                "secret '{secret}' accepts at most {MAX_NAMES_PER_SECRET} environment names"
59            ),
60            Self::InvalidName { name } => write!(
61                formatter,
62                "name '{name}' must use uppercase letters, digits, and underscores"
63            ),
64            Self::DuplicateName { secret, name } => {
65                write!(
66                    formatter,
67                    "secret '{secret}' repeats environment name '{name}'"
68                )
69            }
70            Self::NameAssignedToSeveralSecrets {
71                name,
72                first_secret,
73                second_secret,
74            } => write!(
75                formatter,
76                "name '{name}' is assigned to both '{first_secret}' and '{second_secret}'"
77            ),
78        }
79    }
80}
81
82pub fn credential_environment_issues(
83    setup: &ProviderSetupManifest,
84) -> Vec<CredentialEnvironmentIssue> {
85    let mut issues = Vec::new();
86    if setup.credential_environment.len() > MAX_ENTRIES {
87        issues.push(CredentialEnvironmentIssue::TooManyEntries);
88    }
89    let required = setup.required_secrets.iter().collect::<BTreeSet<_>>();
90    let mut declared_secrets = BTreeSet::new();
91    let mut environment_owners = BTreeMap::new();
92    for source in &setup.credential_environment {
93        if !required.contains(&source.secret) {
94            issues.push(CredentialEnvironmentIssue::SecretNotRequired {
95                secret: source.secret.clone(),
96            });
97        }
98        if !declared_secrets.insert(source.secret.as_str()) {
99            issues.push(CredentialEnvironmentIssue::DuplicateSecret {
100                secret: source.secret.clone(),
101            });
102        }
103        if source.environment_names.is_empty() {
104            issues.push(CredentialEnvironmentIssue::NoNames {
105                secret: source.secret.clone(),
106            });
107        }
108        if source.environment_names.len() > MAX_NAMES_PER_SECRET {
109            issues.push(CredentialEnvironmentIssue::TooManyNames {
110                secret: source.secret.clone(),
111            });
112        }
113        let mut declared_names = BTreeSet::new();
114        for name in &source.environment_names {
115            if !environment_name_is_valid(name) {
116                issues.push(CredentialEnvironmentIssue::InvalidName { name: name.clone() });
117            }
118            if !declared_names.insert(name.as_str()) {
119                issues.push(CredentialEnvironmentIssue::DuplicateName {
120                    secret: source.secret.clone(),
121                    name: name.clone(),
122                });
123            }
124            if let Some(first_secret) =
125                environment_owners.insert(name.as_str(), source.secret.as_str())
126            {
127                if first_secret != source.secret {
128                    issues.push(CredentialEnvironmentIssue::NameAssignedToSeveralSecrets {
129                        name: name.clone(),
130                        first_secret: first_secret.to_string(),
131                        second_secret: source.secret.clone(),
132                    });
133                }
134            }
135        }
136    }
137    issues
138}
139
140pub fn available_process_credential_environment_name<'a>(
141    sources: &'a [ConnectorCredentialEnvironmentManifest],
142    secret: &str,
143) -> Option<&'a str> {
144    available_credential_environment_name(sources, secret, |name| {
145        std::env::var(name)
146            .ok()
147            .is_some_and(|value| !value.trim().is_empty())
148    })
149}
150
151pub fn available_credential_environment_name<'a>(
152    sources: &'a [ConnectorCredentialEnvironmentManifest],
153    secret: &str,
154    mut is_present: impl FnMut(&str) -> bool,
155) -> Option<&'a str> {
156    sources
157        .iter()
158        .filter(|source| source.secret == secret)
159        .flat_map(|source| source.environment_names.iter())
160        .find(|name| is_present(name))
161        .map(String::as_str)
162}
163
164pub fn credential_environment_names(setup: &ProviderSetupManifest) -> Vec<String> {
165    let mut names = setup
166        .credential_environment
167        .iter()
168        .flat_map(|source| source.environment_names.iter().cloned())
169        .collect::<Vec<_>>();
170    names.sort();
171    names.dedup();
172    names
173}
174
175fn environment_name_is_valid(name: &str) -> bool {
176    !name.is_empty()
177        && name.len() <= MAX_NAME_BYTES
178        && name
179            .bytes()
180            .all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn lookup_uses_only_declared_nonempty_sources() {
189        let sources = [ConnectorCredentialEnvironmentManifest {
190            secret: "duffel/test-access-token".to_string(),
191            environment_names: vec!["DUFFEL_TEST_KEY".to_string(), "DUFFEL_BACKUP".to_string()],
192        }];
193        let found =
194            available_credential_environment_name(&sources, "duffel/test-access-token", |name| {
195                name == "DUFFEL_BACKUP"
196            });
197        assert_eq!(found, Some("DUFFEL_BACKUP"));
198        assert_eq!(
199            available_credential_environment_name(&sources, "duffel/live-token", |_| true),
200            None
201        );
202    }
203
204    #[test]
205    fn manifest_shape_decodes_logical_secret_environment_sources() {
206        let setup: ProviderSetupManifest = toml::from_str(
207            r#"
208auth_type = "api-key"
209required_secrets = ["duffel/test-access-token"]
210credential_environment = [
211  { secret = "duffel/test-access-token", environment_names = ["DUFFEL_TEST_KEY"] },
212]
213"#,
214        )
215        .expect("setup manifest");
216        assert_eq!(
217            setup.credential_environment,
218            [ConnectorCredentialEnvironmentManifest {
219                secret: "duffel/test-access-token".to_string(),
220                environment_names: vec!["DUFFEL_TEST_KEY".to_string()],
221            }]
222        );
223    }
224}