1#![allow(clippy::disallowed_types)] use std::collections::HashMap;
10use std::fmt;
11use std::hash::BuildHasher;
12
13use crate::storage_location::StorageProvider;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum AuthSource {
18 ExplicitStatic,
20 ExplicitToken,
22 EnvironmentStatic,
24 EnvironmentToken,
26 Profile,
28 WebIdentity,
30 WorkloadIdentity,
32 ManagedIdentityOrMetadata,
34 ApplicationDefault,
36 AzureCli,
38 Anonymous,
40 DownstreamDefault,
42 Unknown,
44}
45
46impl fmt::Display for AuthSource {
47 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
48 formatter.write_str(match self {
49 Self::ExplicitStatic => "explicit-static",
50 Self::ExplicitToken => "explicit-token",
51 Self::EnvironmentStatic => "environment-static",
52 Self::EnvironmentToken => "environment-token",
53 Self::Profile => "profile",
54 Self::WebIdentity => "web-identity",
55 Self::WorkloadIdentity => "workload-identity",
56 Self::ManagedIdentityOrMetadata => "managed-identity-or-metadata",
57 Self::ApplicationDefault => "application-default",
58 Self::AzureCli => "azure-cli",
59 Self::Anonymous => "anonymous",
60 Self::DownstreamDefault => "downstream-default",
61 Self::Unknown => "unknown",
62 })
63 }
64}
65
66#[must_use]
72pub fn classify_storage_auth_source<F, S>(
73 provider: StorageProvider,
74 explicit: &HashMap<String, String, S>,
75 env_lookup: &F,
76) -> AuthSource
77where
78 F: Fn(&str) -> Option<String>,
79 S: BuildHasher,
80{
81 let signals = AuthSignals {
82 explicit,
83 env_lookup,
84 };
85 match provider {
86 StorageProvider::AwsS3 => classify_aws(&signals),
87 StorageProvider::AzureAdls => classify_azure(&signals),
88 StorageProvider::Gcs => classify_gcs(&signals),
89 StorageProvider::Local => AuthSource::Anonymous,
90 }
91}
92
93struct AuthSignals<'a, F, S> {
94 explicit: &'a HashMap<String, String, S>,
95 env_lookup: &'a F,
96}
97
98impl<F, S> AuthSignals<'_, F, S>
99where
100 F: Fn(&str) -> Option<String>,
101 S: BuildHasher,
102{
103 fn explicit_has(&self, keys: &[&str]) -> bool {
104 self.explicit.iter().any(|(candidate, value)| {
105 keys.iter().any(|key| candidate.eq_ignore_ascii_case(key)) && !value.trim().is_empty()
106 })
107 }
108
109 fn explicit_true(&self, keys: &[&str]) -> bool {
110 self.explicit.iter().any(|(candidate, value)| {
111 keys.iter().any(|key| candidate.eq_ignore_ascii_case(key))
112 && value.trim().eq_ignore_ascii_case("true")
113 })
114 }
115
116 fn env_has(&self, keys: &[&str]) -> bool {
117 keys.iter()
118 .any(|key| (self.env_lookup)(key).is_some_and(|value| !value.trim().is_empty()))
119 }
120
121 fn env_true(&self, keys: &[&str]) -> bool {
122 keys.iter().any(|key| {
123 (self.env_lookup)(key).is_some_and(|value| value.trim().eq_ignore_ascii_case("true"))
124 })
125 }
126}
127
128fn classify_aws<F, S>(signals: &AuthSignals<'_, F, S>) -> AuthSource
129where
130 F: Fn(&str) -> Option<String>,
131 S: BuildHasher,
132{
133 if signals.explicit_true(&["aws_skip_signature", "skip_signature"]) {
134 AuthSource::Anonymous
135 } else if signals.explicit_has(&["aws_session_token", "aws_token", "session_token", "token"]) {
136 AuthSource::ExplicitToken
137 } else if signals.explicit_has(&["aws_access_key_id", "access_key_id"])
138 || signals.explicit_has(&["aws_secret_access_key", "secret_access_key"])
139 {
140 AuthSource::ExplicitStatic
141 } else if signals.explicit_has(&["aws_profile", "profile"]) {
142 AuthSource::Profile
143 } else if signals.explicit_has(&["aws_web_identity_token_file", "web_identity_token_file"]) {
144 AuthSource::WebIdentity
145 } else if signals.env_has(&["AWS_SESSION_TOKEN"]) {
146 AuthSource::EnvironmentToken
147 } else if signals.env_has(&["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]) {
148 AuthSource::EnvironmentStatic
149 } else if signals.env_has(&["AWS_WEB_IDENTITY_TOKEN_FILE"]) {
150 AuthSource::WebIdentity
151 } else if signals.env_has(&["AWS_PROFILE"]) {
152 AuthSource::Profile
153 } else if signals.env_has(&[
154 "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI",
155 "AWS_CONTAINER_CREDENTIALS_FULL_URI",
156 ]) {
157 AuthSource::ManagedIdentityOrMetadata
158 } else {
159 AuthSource::DownstreamDefault
160 }
161}
162
163fn classify_azure<F, S>(signals: &AuthSignals<'_, F, S>) -> AuthSource
164where
165 F: Fn(&str) -> Option<String>,
166 S: BuildHasher,
167{
168 if signals.explicit_true(&["azure_skip_signature", "skip_signature"])
169 || signals.explicit_true(&["azure_storage_use_emulator", "use_emulator"])
170 {
171 AuthSource::Anonymous
172 } else if signals.explicit_has(&[
173 "azure_storage_account_key",
174 "azure_storage_access_key",
175 "account_key",
176 "azure_storage_client_secret",
177 "azure_client_secret",
178 "client_secret",
179 ]) {
180 AuthSource::ExplicitStatic
181 } else if signals.explicit_has(&[
182 "azure_storage_sas_token",
183 "azure_storage_sas_key",
184 "sas_token",
185 "azure_storage_token",
186 "bearer_token",
187 ]) {
188 AuthSource::ExplicitToken
189 } else if signals.explicit_has(&["azure_federated_token_file", "federated_token_file"]) {
190 AuthSource::WorkloadIdentity
191 } else if signals.explicit_true(&["azure_use_azure_cli", "use_azure_cli"]) {
192 AuthSource::AzureCli
193 } else if signals.env_has(&["AZURE_STORAGE_SAS_TOKEN", "AZURE_STORAGE_TOKEN"]) {
194 AuthSource::EnvironmentToken
195 } else if signals.env_has(&["AZURE_STORAGE_ACCOUNT_KEY", "AZURE_CLIENT_SECRET"]) {
196 AuthSource::EnvironmentStatic
197 } else if signals.env_has(&["AZURE_FEDERATED_TOKEN_FILE"]) {
198 AuthSource::WorkloadIdentity
199 } else if signals.env_true(&["AZURE_USE_AZURE_CLI"]) {
200 AuthSource::AzureCli
201 } else {
202 AuthSource::ManagedIdentityOrMetadata
203 }
204}
205
206fn classify_gcs<F, S>(signals: &AuthSignals<'_, F, S>) -> AuthSource
207where
208 F: Fn(&str) -> Option<String>,
209 S: BuildHasher,
210{
211 if signals.explicit_true(&["google_skip_signature", "skip_signature"])
212 || signals.explicit_true(&["gcs.no-auth"])
213 {
214 AuthSource::Anonymous
215 } else if signals.explicit_has(&[
216 "google_service_account_key",
217 "service_account_key",
218 "google_service_account_path",
219 "service_account_path",
220 ]) {
221 AuthSource::ExplicitStatic
222 } else if signals.explicit_has(&["gcs.token", "google_token"]) {
223 AuthSource::ExplicitToken
224 } else if signals.explicit_has(&["google_application_credentials", "application_credentials"])
225 || signals.env_has(&["GOOGLE_APPLICATION_CREDENTIALS"])
226 {
227 AuthSource::ApplicationDefault
228 } else if signals.env_has(&[
229 "GOOGLE_SERVICE_ACCOUNT_KEY",
230 "SERVICE_ACCOUNT",
231 "GOOGLE_SERVICE_ACCOUNT",
232 "GOOGLE_SERVICE_ACCOUNT_PATH",
233 ]) {
234 AuthSource::EnvironmentStatic
235 } else {
236 AuthSource::ApplicationDefault
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 fn lookup(values: &[(&str, &str)], key: &str) -> Option<String> {
245 values
246 .iter()
247 .find_map(|(candidate, value)| (*candidate == key).then(|| (*value).to_string()))
248 }
249
250 #[test]
251 fn classifies_refreshable_ambient_sources_without_values() {
252 let options = HashMap::new();
253 assert_eq!(
254 classify_storage_auth_source(StorageProvider::AzureAdls, &options, &|key| lookup(
255 &[("AZURE_FEDERATED_TOKEN_FILE", "/token")],
256 key
257 )),
258 AuthSource::WorkloadIdentity
259 );
260 assert_eq!(
261 classify_storage_auth_source(StorageProvider::AwsS3, &options, &|key| lookup(
262 &[("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "/task")],
263 key
264 )),
265 AuthSource::ManagedIdentityOrMetadata
266 );
267 assert_eq!(
268 classify_storage_auth_source(StorageProvider::Gcs, &options, &|_| None),
269 AuthSource::ApplicationDefault
270 );
271 }
272
273 #[test]
274 fn display_is_low_cardinality_and_non_secret() {
275 assert_eq!(AuthSource::WebIdentity.to_string(), "web-identity");
276 assert_eq!(AuthSource::AzureCli.to_string(), "azure-cli");
277 }
278
279 #[test]
280 fn explicit_azure_credentials_take_precedence_over_ambient_cli() {
281 let options = HashMap::from([(
282 "azure_storage_account_key".to_string(),
283 "not-logged".to_string(),
284 )]);
285 assert_eq!(
286 classify_storage_auth_source(StorageProvider::AzureAdls, &options, &|key| lookup(
287 &[("AZURE_USE_AZURE_CLI", "true")],
288 key
289 )),
290 AuthSource::ExplicitStatic
291 );
292 }
293
294 #[test]
295 fn explicit_gcs_token_has_a_non_secret_source_classification() {
296 let options = HashMap::from([("gcs.token".to_string(), "not-logged".to_string())]);
297 assert_eq!(
298 classify_storage_auth_source(StorageProvider::Gcs, &options, &|_| None),
299 AuthSource::ExplicitToken
300 );
301 }
302}