1use std::collections::hash_map::DefaultHasher;
16use std::collections::HashSet;
17use std::hash::Hash;
18use std::hash::Hasher;
19use std::path::Path;
20use std::path::PathBuf;
21
22use cheetah_string::CheetahString;
23use rocketmq_runtime::BlockingExecutor;
24use tokio::fs;
25
26use crate::acl::validator::validate_acl_config;
27use crate::migration::alc::acl_config::AclConfig;
28use crate::migration::alc::plain_access_config::PlainAccessConfig;
29use crate::migration::alc::plain_access_data::DataVersion;
30use crate::migration::alc::plain_access_data::PlainAccessData;
31use crate::runtime_bridge::AuthBlockingExecutor;
32use crate::AuthFailureKind;
33use crate::AuthOperation;
34use crate::AuthServiceError;
35use crate::AuthServiceResult;
36
37#[derive(Clone, Debug)]
38pub struct FileAclConfigLoader {
39 roots: Vec<PathBuf>,
40 blocking: AuthBlockingExecutor,
41}
42
43#[derive(Clone, Debug)]
44pub struct FileAclConfigStore {
45 file: PathBuf,
46}
47
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub struct AclConfigFingerprint(u64);
50
51impl FileAclConfigLoader {
52 pub fn new(root: impl Into<PathBuf>, blocking: BlockingExecutor) -> Self {
53 Self {
54 roots: vec![root.into()],
55 blocking: AuthBlockingExecutor::new(blocking),
56 }
57 }
58
59 pub async fn load_with_fingerprint(&self) -> AuthServiceResult<(AclConfig, AclConfigFingerprint)> {
60 let files = self.discover_files().await?;
61 let (config, fingerprint) = load_files(files).await?;
62 validate_acl_config(&config)?;
63 Ok((config, fingerprint))
64 }
65
66 pub async fn discover_files(&self) -> AuthServiceResult<Vec<PathBuf>> {
67 let roots = self.roots.clone();
68 self.blocking
69 .spawn_io("auth.acl.discover_files", move || discover_acl_files(&roots))
70 .await
71 .map_err(|source| {
72 AuthServiceError::with_source(AuthOperation::ReadMetadata, AuthFailureKind::Unavailable, source)
73 })?
74 }
75}
76
77impl FileAclConfigStore {
78 pub fn new(file: impl Into<PathBuf>) -> Self {
79 Self { file: file.into() }
80 }
81
82 pub async fn update_global_white_remote_addresses<I, S>(&self, addresses: I) -> AuthServiceResult<()>
83 where
84 I: IntoIterator<Item = S>,
85 S: AsRef<str>,
86 {
87 let addresses: Vec<_> = addresses
88 .into_iter()
89 .map(|address| address.as_ref().trim().to_owned())
90 .filter(|address| !address.is_empty())
91 .map(CheetahString::from)
92 .collect();
93 if addresses.is_empty() {
94 return Err(AuthServiceError::new(
95 AuthOperation::ManageMetadata,
96 AuthFailureKind::InvalidInput,
97 ));
98 }
99
100 let mut data = self.load_plain_access_data().await?;
101 data.set_global_white_remote_addresses(addresses);
102 bump_data_version(&mut data);
103 self.write_plain_access_data(&data).await
104 }
105
106 async fn load_plain_access_data(&self) -> AuthServiceResult<PlainAccessData> {
107 let bytes = match fs::read(&self.file).await {
108 Ok(bytes) => bytes,
109 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(PlainAccessData::default()),
110 Err(error) => return Err(AuthServiceError::storage_read_failed(error)),
111 };
112
113 if bytes.iter().all(u8::is_ascii_whitespace) {
114 return Ok(PlainAccessData::default());
115 }
116
117 serde_yaml::from_slice(&bytes).map_err(|source| {
118 AuthServiceError::with_source(AuthOperation::DecodeMetadata, AuthFailureKind::InvalidData, source)
119 })
120 }
121
122 async fn write_plain_access_data(&self, data: &PlainAccessData) -> AuthServiceResult<()> {
123 let content = serde_yaml::to_string(data).map_err(|source| {
124 AuthServiceError::with_source(AuthOperation::EncodeMetadata, AuthFailureKind::InvalidData, source)
125 })?;
126
127 if let Some(parent) = self.file.parent() {
128 fs::create_dir_all(parent)
129 .await
130 .map_err(AuthServiceError::storage_write_failed)?;
131 }
132
133 if fs::metadata(&self.file).await.is_ok() {
134 let backup = backup_file_path(&self.file);
135 fs::copy(&self.file, &backup)
136 .await
137 .map_err(AuthServiceError::storage_write_failed)?;
138 }
139
140 let temp_file = temp_file_path(&self.file);
141 fs::write(&temp_file, content)
142 .await
143 .map_err(AuthServiceError::storage_write_failed)?;
144
145 match fs::rename(&temp_file, &self.file).await {
146 Ok(()) => Ok(()),
147 Err(_rename_error) => {
148 fs::copy(&temp_file, &self.file)
149 .await
150 .map_err(AuthServiceError::storage_write_failed)?;
151 let _ = fs::remove_file(&temp_file).await;
152 Ok(())
153 }
154 }
155 }
156}
157
158async fn load_files(files: Vec<PathBuf>) -> AuthServiceResult<(AclConfig, AclConfigFingerprint)> {
159 let mut accounts = Vec::new();
160 let mut global_white_addrs = Vec::new();
161 let mut seen_access_keys = HashSet::new();
162 let mut hasher = DefaultHasher::new();
163
164 for file in files {
165 file.hash(&mut hasher);
166 let bytes = fs::read(&file).await.map_err(AuthServiceError::storage_read_failed)?;
167 bytes.hash(&mut hasher);
168 if bytes.iter().all(u8::is_ascii_whitespace) {
169 continue;
170 }
171
172 let data: PlainAccessData = serde_yaml::from_slice(&bytes).map_err(|source| {
173 AuthServiceError::with_source(AuthOperation::DecodeMetadata, AuthFailureKind::InvalidData, source)
174 })?;
175
176 global_white_addrs.extend(data.global_white_remote_addresses().iter().cloned());
177 extend_accounts(&mut accounts, &mut seen_access_keys, data.accounts());
178 }
179
180 let mut config = AclConfig::new();
181 if !global_white_addrs.is_empty() {
182 config.set_global_white_addrs(global_white_addrs);
183 }
184 if !accounts.is_empty() {
185 config.set_plain_access_configs(accounts);
186 }
187 Ok((config, AclConfigFingerprint(hasher.finish())))
188}
189
190fn extend_accounts(
191 accounts: &mut Vec<PlainAccessConfig>,
192 seen_access_keys: &mut HashSet<String>,
193 incoming: &[PlainAccessConfig],
194) {
195 for account in incoming {
196 let Some(access_key) = account.access_key() else {
197 continue;
198 };
199 if seen_access_keys.insert(access_key.to_string()) {
200 accounts.push(account.clone());
201 }
202 }
203}
204
205fn discover_acl_files(roots: &[PathBuf]) -> AuthServiceResult<Vec<PathBuf>> {
206 let mut files = Vec::new();
207 for root in roots {
208 collect_acl_files(root, &mut files)?;
209 }
210 Ok(files)
211}
212
213fn collect_acl_files(path: &Path, files: &mut Vec<PathBuf>) -> AuthServiceResult<()> {
214 let metadata = std::fs::metadata(path).map_err(AuthServiceError::storage_read_failed)?;
215
216 if metadata.is_file() {
217 if is_acl_yaml(path) {
218 files.push(path.to_path_buf());
219 }
220 return Ok(());
221 }
222
223 if metadata.is_dir() {
224 let mut entries = Vec::new();
225 for entry in std::fs::read_dir(path).map_err(AuthServiceError::storage_read_failed)? {
226 let entry = entry.map_err(AuthServiceError::storage_read_failed)?;
227 entries.push(entry.path());
228 }
229 entries.sort();
230 for entry in entries {
231 collect_acl_files(&entry, files)?;
232 }
233 }
234
235 Ok(())
236}
237
238fn is_acl_yaml(path: &Path) -> bool {
239 let Some(extension) = path.extension().and_then(|value| value.to_str()) else {
240 return false;
241 };
242 if !extension.eq_ignore_ascii_case("yml") && !extension.eq_ignore_ascii_case("yaml") {
243 return false;
244 }
245 path.file_name()
246 .and_then(|value| value.to_str())
247 .is_none_or(|file_name| !file_name.eq_ignore_ascii_case("tools.yml"))
248}
249
250fn backup_file_path(path: &Path) -> PathBuf {
251 let mut backup = path.as_os_str().to_os_string();
252 backup.push(".bak");
253 PathBuf::from(backup)
254}
255
256fn temp_file_path(path: &Path) -> PathBuf {
257 let file_name = path
258 .file_name()
259 .and_then(|value| value.to_str())
260 .unwrap_or("plain_acl.yml");
261 let nanos = std::time::SystemTime::now()
262 .duration_since(std::time::UNIX_EPOCH)
263 .map(|duration| duration.as_nanos())
264 .unwrap_or_default();
265 path.with_file_name(format!(".{file_name}.{}.tmp", nanos))
266}
267
268fn bump_data_version(data: &mut PlainAccessData) {
269 let counter = data
270 .latest_version()
271 .map_or(1, |version| version.counter.saturating_add(1));
272 data.set_data_version(vec![DataVersion {
273 timestamp: current_time_millis(),
274 counter,
275 }]);
276}
277
278fn current_time_millis() -> u64 {
279 std::time::SystemTime::now()
280 .duration_since(std::time::UNIX_EPOCH)
281 .map_or(0, |duration| u64::try_from(duration.as_millis()).unwrap_or(u64::MAX))
282}
283
284#[cfg(test)]
285mod tests {
286 use std::fs;
287
288 use cheetah_string::CheetahString;
289 use rocketmq_runtime::RuntimeContext;
290 use tempfile::TempDir;
291
292 use super::*;
293
294 fn metadata_blocking() -> BlockingExecutor {
295 RuntimeContext::from_current("auth-acl-loader-test")
296 .service_context("auth-acl-loader")
297 .metadata_io()
298 .clone()
299 }
300
301 #[tokio::test]
302 async fn discovers_yaml_files_recursively_and_skips_tools_file() {
303 let temp = TempDir::new().unwrap();
304 let root = temp.path();
305 let nested = root.join("nested");
306 fs::create_dir_all(&nested).unwrap();
307 fs::write(root.join("a.yml"), "").unwrap();
308 fs::write(root.join("tools.yml"), "").unwrap();
309 fs::write(nested.join("b.yaml"), "").unwrap();
310 fs::write(nested.join("ignored.txt"), "").unwrap();
311
312 let files = FileAclConfigLoader::new(root, metadata_blocking())
313 .discover_files()
314 .await
315 .unwrap();
316
317 assert_eq!(files.len(), 2);
318 assert!(files.iter().any(|path| path.ends_with("a.yml")));
319 assert!(files.iter().any(|path| path.ends_with("b.yaml")));
320 assert!(!files.iter().any(|path| path.ends_with("tools.yml")));
321 }
322
323 #[tokio::test]
324 async fn loads_java_acl_files_and_keeps_first_duplicate_access_key() {
325 let temp = TempDir::new().unwrap();
326 let first = temp.path().join("first.yml");
327 let second = temp.path().join("second.yml");
328 fs::write(
329 &first,
330 r#"
331globalWhiteRemoteAddresses:
332 - 10.0.0.1
333accounts:
334 - accessKey: ak
335 secretKey: first-secret
336 admin: true
337"#,
338 )
339 .unwrap();
340 fs::write(
341 &second,
342 r#"
343globalWhiteRemoteAddresses:
344 - 10.0.0.2
345accounts:
346 - accessKey: ak
347 secretKey: second-secret
348 - accessKey: bk
349 secretKey: bk-secret
350"#,
351 )
352 .unwrap();
353
354 let (config, _) = FileAclConfigLoader::new(temp.path(), metadata_blocking())
355 .load_with_fingerprint()
356 .await
357 .unwrap();
358
359 let white_addrs = config.global_white_addrs().unwrap();
360 assert_eq!(
361 white_addrs,
362 &[CheetahString::from("10.0.0.1"), CheetahString::from("10.0.0.2")]
363 );
364
365 let accounts = config.plain_access_configs().unwrap();
366 assert_eq!(accounts.len(), 2);
367 assert_eq!(accounts[0].access_key().unwrap().as_str(), "ak");
368 assert_eq!(accounts[0].secret_key().unwrap().as_str(), "first-secret");
369 assert_eq!(accounts[1].access_key().unwrap().as_str(), "bk");
370 }
371
372 #[tokio::test]
373 async fn empty_files_load_as_empty_config() {
374 let temp = TempDir::new().unwrap();
375 let file = temp.path().join("plain_acl.yml");
376 fs::write(&file, " \n\t").unwrap();
377
378 let (config, _) = FileAclConfigLoader::new(file, metadata_blocking())
379 .load_with_fingerprint()
380 .await
381 .unwrap();
382
383 assert!(config.global_white_addrs().is_none());
384 assert!(config.plain_access_configs().is_none());
385 }
386
387 #[tokio::test]
388 async fn fingerprint_changes_when_acl_file_content_changes() {
389 let temp = TempDir::new().unwrap();
390 let file = temp.path().join("plain_acl.yml");
391 fs::write(
392 &file,
393 r#"
394accounts:
395 - accessKey: ak
396 secretKey: first
397"#,
398 )
399 .unwrap();
400
401 let loader = FileAclConfigLoader::new(&file, metadata_blocking());
402 let (_, first_fingerprint) = loader.load_with_fingerprint().await.unwrap();
403 let (_, same_fingerprint) = loader.load_with_fingerprint().await.unwrap();
404 assert_eq!(first_fingerprint, same_fingerprint);
405
406 fs::write(
407 &file,
408 r#"
409accounts:
410 - accessKey: ak
411 secretKey: second
412"#,
413 )
414 .unwrap();
415
416 let (_, updated_fingerprint) = loader.load_with_fingerprint().await.unwrap();
417 assert_ne!(first_fingerprint, updated_fingerprint);
418 }
419
420 #[tokio::test]
421 async fn rejects_acl_file_with_missing_secret_key() {
422 let temp = TempDir::new().unwrap();
423 let file = temp.path().join("plain_acl.yml");
424 fs::write(
425 &file,
426 r#"
427accounts:
428 - accessKey: ak
429"#,
430 )
431 .unwrap();
432
433 let error = FileAclConfigLoader::new(file, metadata_blocking())
434 .load_with_fingerprint()
435 .await
436 .unwrap_err();
437
438 assert_eq!(error.kind(), AuthFailureKind::InvalidConfiguration);
439 assert!(!error.to_string().contains("secret="));
440 }
441
442 #[tokio::test]
443 async fn store_updates_global_white_remote_addresses_and_preserves_accounts() {
444 let temp = TempDir::new().unwrap();
445 let file = temp.path().join("plain_acl.yml");
446 fs::write(
447 &file,
448 r#"
449globalWhiteRemoteAddresses:
450 - 10.10.*.*
451accounts:
452 - accessKey: ak
453 secretKey: sk
454 defaultTopicPerm: PUB
455"#,
456 )
457 .unwrap();
458
459 FileAclConfigStore::new(&file)
460 .update_global_white_remote_addresses(["172.16.*.*", "192.168.0.*"])
461 .await
462 .unwrap();
463
464 let content = fs::read_to_string(&file).unwrap();
465 let data: PlainAccessData = serde_yaml::from_str(&content).unwrap();
466 assert_eq!(
467 data.global_white_remote_addresses(),
468 &[
469 CheetahString::from_static_str("172.16.*.*"),
470 CheetahString::from_static_str("192.168.0.*")
471 ]
472 );
473 assert_eq!(data.accounts().len(), 1);
474 assert_eq!(data.accounts()[0].access_key().unwrap().as_str(), "ak");
475 assert_eq!(data.accounts()[0].secret_key().unwrap().as_str(), "sk");
476 assert!(content.contains("globalWhiteRemoteAddresses"));
477 assert!(content.contains("accessKey"));
478 }
479
480 #[tokio::test]
481 async fn store_creates_missing_acl_file_for_global_white_remote_addresses() {
482 let temp = TempDir::new().unwrap();
483 let file = temp.path().join("conf").join("plain_acl.yml");
484
485 FileAclConfigStore::new(&file)
486 .update_global_white_remote_addresses(["10.10.*.*"])
487 .await
488 .unwrap();
489
490 let data: PlainAccessData = serde_yaml::from_str(&fs::read_to_string(&file).unwrap()).unwrap();
491 assert_eq!(
492 data.global_white_remote_addresses(),
493 &[CheetahString::from_static_str("10.10.*.*")]
494 );
495 assert!(data.accounts().is_empty());
496 }
497
498 #[tokio::test]
499 async fn store_bumps_data_version_when_global_white_remote_addresses_change() {
500 let temp = TempDir::new().unwrap();
501 let file = temp.path().join("plain_acl.yml");
502 fs::write(
503 &file,
504 r#"
505globalWhiteRemoteAddresses:
506 - 10.10.*.*
507dataVersion:
508 - timestamp: 100
509 counter: 7
510accounts:
511 - accessKey: ak
512 secretKey: sk
513"#,
514 )
515 .unwrap();
516
517 FileAclConfigStore::new(&file)
518 .update_global_white_remote_addresses(["172.16.*.*"])
519 .await
520 .unwrap();
521
522 let data: PlainAccessData = serde_yaml::from_str(&fs::read_to_string(&file).unwrap()).unwrap();
523 let version = data.latest_version().unwrap();
524 assert_eq!(version.counter, 8);
525 assert!(version.timestamp > 100);
526 }
527
528 #[tokio::test]
529 async fn store_creates_data_version_when_missing() {
530 let temp = TempDir::new().unwrap();
531 let file = temp.path().join("plain_acl.yml");
532
533 FileAclConfigStore::new(&file)
534 .update_global_white_remote_addresses(["10.10.*.*"])
535 .await
536 .unwrap();
537
538 let data: PlainAccessData = serde_yaml::from_str(&fs::read_to_string(&file).unwrap()).unwrap();
539 let version = data.latest_version().unwrap();
540 assert_eq!(version.counter, 1);
541 assert!(version.timestamp > 0);
542 }
543}