harn_cli/package/
credential_environment.rs1use 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}