1use std::any::Any;
21use std::collections::HashMap;
22use std::path::Path;
23use std::path::PathBuf;
24use std::sync::atomic::AtomicU64;
25use std::sync::atomic::Ordering;
26use std::sync::Arc;
27use std::sync::RwLock;
28use std::sync::RwLockReadGuard;
29use std::sync::RwLockWriteGuard;
30use std::time::Duration;
31
32use rocketmq_runtime::MetadataDeadline;
33use rocketmq_runtime::MetadataGeneration;
34use rocketmq_runtime::MetadataIoActor;
35use rocketmq_runtime::MonotonicClock;
36use rocketmq_runtime::SystemMonotonicClock;
37use rocketmq_security_api::Action;
38use serde::Deserialize;
39use serde::Serialize;
40use tracing::debug;
41use tracing::warn;
42
43use crate::authentication::enums::subject_type::SubjectType;
44use crate::authentication::model::subject::Subject;
45use crate::authorization::enums::decision::Decision;
46use crate::authorization::enums::policy_type::PolicyType;
47use crate::authorization::metadata_provider::AuthorizationMetadataProvider;
48use crate::authorization::model::acl::Acl;
49use crate::authorization::model::environment::Environment;
50use crate::authorization::model::policy::Policy;
51use crate::authorization::model::policy_entry::PolicyEntry;
52use crate::authorization::model::resource::Resource;
53use crate::config::AuthConfig;
54use crate::metadata_snapshot::classify_snapshot_persistence;
55use crate::metadata_snapshot::ReconciliationGeneration;
56use crate::metadata_snapshot::SnapshotPersistence;
57use crate::runtime_bridge::AuthBlockingExecutor;
58use crate::AuthFailureKind;
59use crate::AuthOperation;
60use crate::AuthServiceError;
61use crate::AuthServiceResult;
62
63pub struct LocalAuthorizationMetadataProvider {
108 storage_path: Option<PathBuf>,
110 storage: Arc<RwLock<HashMap<String, Acl>>>,
111
112 cache: Arc<RwLock<HashMap<String, CachedAcl>>>,
115
116 cache_config: CacheConfig,
118 clock: Arc<dyn MonotonicClock>,
119 storage_generation: AtomicU64,
123
124 reconciliation: ReconciliationGeneration,
126
127 initialized: Arc<RwLock<bool>>,
129 write_lock: Arc<tokio::sync::Mutex<()>>,
130 blocking: AuthBlockingExecutor,
131 metadata_io: Option<MetadataIoActor>,
132 #[cfg(test)]
133 refill_hook: std::sync::Mutex<Option<CacheRefillHook>>,
134 #[cfg(test)]
135 cache_lock_attempt_hook: std::sync::Mutex<Option<Arc<tokio::sync::Notify>>>,
136 #[cfg(test)]
137 canonical_read_count: AtomicU64,
138}
139
140enum CacheCommit {
141 Store { subject_key: String, acl: Acl },
142 Remove { subject_key: String },
143 Clear,
144}
145
146#[cfg(test)]
147struct CacheRefillHook {
148 storage_read: Arc<tokio::sync::Notify>,
149 resume: Arc<tokio::sync::Notify>,
150}
151
152#[derive(Clone, Debug)]
154struct CachedAcl {
155 acl: Option<Acl>,
156 last_accessed: Duration,
158 created_at: Duration,
160}
161
162impl CachedAcl {
163 fn new(acl: Option<Acl>, now: Duration) -> Self {
164 Self {
165 acl,
166 last_accessed: now,
167 created_at: now,
168 }
169 }
170
171 fn requires_reload(&self, now: Duration, config: &CacheConfig) -> bool {
174 let age = now.saturating_sub(self.created_at);
175 age >= config.ttl || age >= config.refresh_interval
176 }
177
178 fn touch(&mut self, now: Duration) {
179 self.last_accessed = now;
180 }
181}
182
183#[derive(Clone, Debug)]
185struct CacheConfig {
186 max_size: usize,
188 ttl: Duration,
190 refresh_interval: Duration,
192}
193
194impl Default for CacheConfig {
195 fn default() -> Self {
196 Self {
197 max_size: 1000,
198 ttl: Duration::from_secs(300), refresh_interval: Duration::from_secs(60), }
201 }
202}
203
204impl CacheConfig {
205 fn from_auth_config(config: &AuthConfig) -> Self {
206 Self {
207 max_size: config.acl_cache_max_num as usize,
208 ttl: Duration::from_secs(config.acl_cache_expired_second as u64),
209 refresh_interval: Duration::from_secs(config.acl_cache_refresh_second as u64),
210 }
211 }
212}
213
214impl LocalAuthorizationMetadataProvider {
215 pub(crate) async fn flush_shared(&self) -> AuthServiceResult<()> {
216 let _writer = self.write_lock.lock().await;
217 let snapshot = self.storage_read()?.clone();
218 match self.persist_storage_snapshot(&snapshot).await.into_error() {
221 Some(error) => Err(error),
222 None => Ok(()),
223 }
224 }
225
226 pub(crate) async fn close_shared(&self) -> AuthServiceResult<()> {
227 let _writer = self.write_lock.lock().await;
228 let mut initialized = self.initialized_write()?;
229 if !*initialized {
230 return Ok(());
231 }
232 self.commit_storage_snapshot(HashMap::new(), CacheCommit::Clear)?;
233 *initialized = false;
234 Ok(())
235 }
236
237 pub fn new() -> Self {
239 Self {
240 storage_path: None,
241 storage: Arc::new(RwLock::new(HashMap::new())),
242 cache: Arc::new(RwLock::new(HashMap::new())),
243 cache_config: CacheConfig::default(),
244 clock: Arc::new(SystemMonotonicClock::new()),
245 storage_generation: AtomicU64::new(0),
246 reconciliation: ReconciliationGeneration::default(),
247 initialized: Arc::new(RwLock::new(false)),
248 write_lock: Arc::new(tokio::sync::Mutex::new(())),
249 blocking: AuthBlockingExecutor::default(),
250 metadata_io: None,
251 #[cfg(test)]
252 refill_hook: std::sync::Mutex::new(None),
253 #[cfg(test)]
254 cache_lock_attempt_hook: std::sync::Mutex::new(None),
255 #[cfg(test)]
256 canonical_read_count: AtomicU64::new(0),
257 }
258 }
259
260 pub fn with_metadata_io(metadata_io: MetadataIoActor) -> Self {
261 Self {
262 metadata_io: Some(metadata_io),
263 ..Self::new()
264 }
265 }
266
267 #[cfg(test)]
268 fn with_clock(clock: Arc<dyn MonotonicClock>) -> Self {
269 Self { clock, ..Self::new() }
270 }
271
272 #[cfg(test)]
273 fn install_refill_hook(&self) -> (Arc<tokio::sync::Notify>, Arc<tokio::sync::Notify>) {
274 let storage_read = Arc::new(tokio::sync::Notify::new());
275 let resume = Arc::new(tokio::sync::Notify::new());
276 *self.refill_hook.lock().unwrap() = Some(CacheRefillHook {
277 storage_read: storage_read.clone(),
278 resume: resume.clone(),
279 });
280 (storage_read, resume)
281 }
282
283 #[cfg(test)]
284 fn install_cache_lock_attempt_hook(&self) -> Arc<tokio::sync::Notify> {
285 let attempted = Arc::new(tokio::sync::Notify::new());
286 *self.cache_lock_attempt_hook.lock().unwrap() = Some(attempted.clone());
287 attempted
288 }
289
290 #[cfg(test)]
291 fn signal_cache_lock_attempt(&self) {
292 if let Some(attempted) = self.cache_lock_attempt_hook.lock().unwrap().take() {
293 attempted.notify_one();
294 }
295 }
296
297 #[cfg(test)]
298 async fn pause_after_storage_read(&self) {
299 let hook = self.refill_hook.lock().unwrap().take();
300 if let Some(hook) = hook {
301 hook.storage_read.notify_one();
302 hook.resume.notified().await;
303 }
304 }
305
306 fn load_from_storage(&self, subject_key: &str) -> AuthServiceResult<Option<Acl>> {
307 #[cfg(test)]
308 self.canonical_read_count.fetch_add(1, Ordering::Relaxed);
309 let storage = self.storage_read()?;
310 Ok(storage.get(subject_key).cloned())
311 }
312
313 async fn persist_storage_snapshot(&self, snapshot: &HashMap<String, Acl>) -> SnapshotPersistence {
319 let Some(path) = &self.storage_path else {
320 return SnapshotPersistence::Durable {
321 generation: MetadataGeneration::new(0),
322 };
323 };
324 let content = match encode_acl_snapshot(snapshot) {
325 Ok(content) => content,
326 Err(error) => return SnapshotPersistence::NotWritten(error),
327 };
328 if let Some(metadata_io) = &self.metadata_io {
329 let persistence = match metadata_io
330 .submit_next_observed(
331 "auth.authorization-acls",
332 path,
333 content,
334 MetadataDeadline::after(Duration::from_secs(5)),
335 )
336 .await
337 {
338 Ok(observation) => classify_snapshot_persistence(observation),
339 Err(error) => SnapshotPersistence::NotWritten(AuthServiceError::metadata_io(error)),
340 };
341 match &persistence {
344 SnapshotPersistence::Durable { generation } => {
345 self.reconciliation.release(*generation);
346 }
347 SnapshotPersistence::Unconfirmed { generation, .. } => {
348 self.reconciliation.record(*generation);
349 }
350 SnapshotPersistence::NotWritten(_) => {}
351 }
352 return persistence;
353 }
354 let path = path.clone();
355 let path_display = path.display().to_string();
356 match self
357 .blocking
358 .spawn_io("auth.authorization.write_acl_snapshot", move || {
359 write_acl_snapshot(&path, &content)
360 })
361 .await
362 {
363 Ok(Ok(())) => SnapshotPersistence::Durable {
364 generation: MetadataGeneration::new(0),
365 },
366 Ok(Err(error)) => SnapshotPersistence::NotWritten(error),
367 Err(error) => {
368 let _ = path_display;
369 SnapshotPersistence::NotWritten(AuthServiceError::storage_write_failed(error))
370 }
371 }
372 }
373
374 pub fn reconciliation_required(&self) -> bool {
376 self.reconciliation.required()
377 }
378
379 pub fn reconciliation_generation(&self) -> Option<MetadataGeneration> {
381 self.reconciliation.generation()
382 }
383
384 fn replace_storage(&self, snapshot: HashMap<String, Acl>) -> AuthServiceResult<()> {
385 self.commit_storage_snapshot(snapshot, CacheCommit::Clear)
386 }
387
388 fn list_from_storage(&self) -> AuthServiceResult<Vec<Acl>> {
389 let storage = self.storage_read()?;
390 Ok(storage.values().cloned().collect())
391 }
392
393 async fn get_cached(&self, subject_key: &str) -> AuthServiceResult<Option<Acl>> {
395 if self.cache_config.max_size == 0 {
396 return self.load_from_storage(subject_key);
397 }
398
399 {
402 #[cfg(test)]
403 self.signal_cache_lock_attempt();
404 let mut cache = self.cache_write()?;
405 let now = self.clock.now();
406 if let Some(cached) = cache.get_mut(subject_key) {
407 if !cached.requires_reload(now, &self.cache_config) {
408 cached.touch(now);
409 debug!("Cache hit for subject: {}", subject_key);
410 return Ok(cached.acl.clone());
411 } else {
412 cache.remove(subject_key);
413 debug!(
414 "Cache entry reached its refresh or expiry bound for subject: {}",
415 subject_key
416 );
417 }
418 }
419 }
420
421 debug!("Cache miss for subject: {}", subject_key);
423 let observed_generation = self.storage_generation.load(Ordering::Acquire);
424 let acl = self.load_from_storage(subject_key)?;
425
426 #[cfg(test)]
427 self.pause_after_storage_read().await;
428
429 {
432 let mut cache = self.cache_write()?;
433 if self.storage_generation.load(Ordering::Acquire) == observed_generation {
434 self.publish_cache_entry(&mut cache, subject_key, acl.clone());
435 return Ok(acl);
436 }
437 }
438
439 let _write_guard = self.write_lock.lock().await;
443 let mut cache = self.cache_write()?;
444 let acl = self.load_from_storage(subject_key)?;
445 self.publish_cache_entry(&mut cache, subject_key, acl.clone());
446
447 Ok(acl)
448 }
449
450 fn commit_storage_snapshot(
455 &self,
456 snapshot: HashMap<String, Acl>,
457 cache_commit: CacheCommit,
458 ) -> AuthServiceResult<()> {
459 let mut cache = self.cache_write()?;
460 {
461 let mut storage = self.storage_write()?;
462 *storage = snapshot;
463 self.storage_generation.fetch_add(1, Ordering::Release);
464 }
465
466 match cache_commit {
467 CacheCommit::Store { subject_key, acl } => {
468 self.publish_cache_entry(&mut cache, &subject_key, Some(acl));
469 }
470 CacheCommit::Remove { subject_key } => {
471 if cache.remove(&subject_key).is_some() {
472 debug!("Invalidated cache for subject: {}", subject_key);
473 }
474 }
475 CacheCommit::Clear => cache.clear(),
476 }
477 Ok(())
478 }
479
480 fn publish_cache_entry(&self, cache: &mut HashMap<String, CachedAcl>, subject_key: &str, acl: Option<Acl>) {
481 if self.cache_config.max_size == 0 {
482 cache.remove(subject_key);
483 return;
484 }
485 if !cache.contains_key(subject_key) && cache.len() >= self.cache_config.max_size {
486 self.evict_oldest(cache);
487 }
488 let now = self.clock.now();
489 cache.insert(subject_key.to_string(), CachedAcl::new(acl, now));
490 }
491
492 fn evict_oldest(&self, cache: &mut HashMap<String, CachedAcl>) {
494 if let Some((oldest_key, _)) = cache
495 .iter()
496 .min_by_key(|(_, cached)| cached.last_accessed)
497 .map(|(k, v)| (k.clone(), v.clone()))
498 {
499 cache.remove(&oldest_key);
500 debug!("Evicted cache entry for subject: {}", oldest_key);
501 }
502 }
503
504 fn ensure_initialized(&self) -> AuthServiceResult<()> {
505 if *self.initialized_read()? {
506 Ok(())
507 } else {
508 Err(AuthServiceError::not_initialized(
509 "Provider not initialized".to_string(),
510 ))
511 }
512 }
513
514 fn initialized_read(&self) -> AuthServiceResult<RwLockReadGuard<'_, bool>> {
515 self.initialized
516 .read()
517 .map_err(|_| AuthServiceError::storage_lock_failed("auth.authorization.initialized"))
518 }
519
520 fn initialized_write(&self) -> AuthServiceResult<RwLockWriteGuard<'_, bool>> {
521 self.initialized
522 .write()
523 .map_err(|_| AuthServiceError::storage_lock_failed("auth.authorization.initialized"))
524 }
525
526 fn storage_read(&self) -> AuthServiceResult<RwLockReadGuard<'_, HashMap<String, Acl>>> {
527 self.storage
528 .read()
529 .map_err(|_| AuthServiceError::storage_lock_failed("auth.authorization.storage"))
530 }
531
532 fn storage_write(&self) -> AuthServiceResult<RwLockWriteGuard<'_, HashMap<String, Acl>>> {
533 self.storage
534 .write()
535 .map_err(|_| AuthServiceError::storage_lock_failed("auth.authorization.storage"))
536 }
537
538 fn cache_write(&self) -> AuthServiceResult<RwLockWriteGuard<'_, HashMap<String, CachedAcl>>> {
539 self.cache
540 .write()
541 .map_err(|_| AuthServiceError::storage_lock_failed("auth.authorization.cache"))
542 }
543
544 fn filter_acls(&self, acls: Vec<Acl>, subject_filter: Option<&str>, resource_filter: Option<&str>) -> Vec<Acl> {
546 acls.into_iter()
547 .filter(|acl| {
548 if let Some(filter) = subject_filter {
550 if !acl.subject_key().contains(filter) {
551 return false;
552 }
553 }
554
555 if let Some(filter) = resource_filter {
557 let has_matching_resource = acl.policies().iter().any(|policy| {
558 policy.entries().iter().any(|entry| {
559 entry
560 .resource()
561 .resource_key()
562 .is_some_and(|resource| resource.contains(filter))
563 })
564 });
565
566 if !has_matching_resource {
567 return false;
568 }
569 }
570
571 true
572 })
573 .filter(|acl| {
574 acl.policies().iter().any(|policy| !policy.entries().is_empty())
576 })
577 .collect()
578 }
579}
580
581#[derive(Debug, Default, Serialize, Deserialize)]
582#[serde(rename_all = "camelCase")]
583struct StoredAclSnapshot {
584 acls: Vec<StoredAclRecord>,
585}
586
587#[derive(Debug, Serialize, Deserialize)]
588#[serde(rename_all = "camelCase")]
589struct StoredAclRecord {
590 subject: String,
591 policies: Vec<StoredPolicyRecord>,
592}
593
594#[derive(Debug, Serialize, Deserialize)]
595#[serde(rename_all = "camelCase")]
596struct StoredPolicyRecord {
597 policy_type: String,
598 entries: Vec<StoredPolicyEntryRecord>,
599}
600
601#[derive(Debug, Serialize, Deserialize)]
602#[serde(rename_all = "camelCase")]
603struct StoredPolicyEntryRecord {
604 resource: String,
605 actions: Vec<String>,
606 #[serde(default, skip_serializing_if = "Vec::is_empty")]
607 source_ips: Vec<String>,
608 decision: String,
609}
610
611fn auth_metadata_snapshot_path(config: &AuthConfig, file_name: &str) -> Option<PathBuf> {
612 let raw_path = config.auth_config_path.as_str().trim();
613 if raw_path.is_empty() {
614 return None;
615 }
616 let path = PathBuf::from(raw_path);
617 let root = if path.extension().is_some() {
618 path.with_extension("")
619 } else {
620 path
621 };
622 Some(root.join(file_name))
623}
624
625fn read_acl_snapshot(path: &Path) -> AuthServiceResult<HashMap<String, Acl>> {
626 let bytes = match std::fs::read(path) {
627 Ok(bytes) => bytes,
628 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(HashMap::new()),
629 Err(error) => return Err(storage_read_error(path, error)),
630 };
631 if bytes.iter().all(u8::is_ascii_whitespace) {
632 return Ok(HashMap::new());
633 }
634 let snapshot: StoredAclSnapshot = serde_json::from_slice(&bytes).map_err(|error| {
635 AuthServiceError::with_source(AuthOperation::DecodeMetadata, AuthFailureKind::InvalidData, error)
636 })?;
637 let mut acls = HashMap::new();
638 for record in snapshot.acls {
639 let acl = acl_from_record(record)?;
640 acls.insert(acl.subject_key().to_string(), acl);
641 }
642 Ok(acls)
643}
644
645fn encode_acl_snapshot(acls: &HashMap<String, Acl>) -> AuthServiceResult<Vec<u8>> {
646 let mut records = acls.values().map(acl_to_record).collect::<Vec<_>>();
647 records.sort_by(|left, right| left.subject.cmp(&right.subject));
648 let snapshot = StoredAclSnapshot { acls: records };
649 serde_json::to_vec_pretty(&snapshot).map_err(|error| {
650 AuthServiceError::with_source(AuthOperation::EncodeMetadata, AuthFailureKind::InvalidData, error)
651 })
652}
653
654fn write_acl_snapshot(path: &Path, content: &[u8]) -> AuthServiceResult<()> {
655 if let Some(parent) = path.parent() {
656 std::fs::create_dir_all(parent).map_err(|error| storage_write_error(parent, error))?;
657 }
658
659 let temp_file = temp_snapshot_path(path);
660 std::fs::write(&temp_file, content).map_err(|error| storage_write_error(&temp_file, error))?;
661 match std::fs::rename(&temp_file, path) {
662 Ok(()) => Ok(()),
663 Err(rename_error) => {
664 std::fs::copy(&temp_file, path).map_err(AuthServiceError::storage_write_failed)?;
665 let _ = rename_error;
666 let _ = std::fs::remove_file(&temp_file);
667 Ok(())
668 }
669 }
670}
671
672fn acl_to_record(acl: &Acl) -> StoredAclRecord {
673 StoredAclRecord {
674 subject: acl.subject_key().to_string(),
675 policies: acl
676 .policies()
677 .iter()
678 .map(|policy| StoredPolicyRecord {
679 policy_type: policy.policy_type().name().to_string(),
680 entries: policy.entries().iter().filter_map(policy_entry_to_record).collect(),
681 })
682 .collect(),
683 }
684}
685
686fn policy_entry_to_record(entry: &PolicyEntry) -> Option<StoredPolicyEntryRecord> {
687 Some(StoredPolicyEntryRecord {
688 resource: entry.resource().resource_key()?,
689 actions: entry.actions().iter().map(|action| action.name().to_string()).collect(),
690 source_ips: entry
691 .environment()
692 .map(|environment| environment.source_ips().clone())
693 .unwrap_or_default(),
694 decision: entry.decision().name().to_string(),
695 })
696}
697
698fn acl_from_record(record: StoredAclRecord) -> AuthServiceResult<Acl> {
699 let subject_type = subject_type_from_key(&record.subject)?;
700 let policies = record
701 .policies
702 .into_iter()
703 .map(policy_from_record)
704 .collect::<AuthServiceResult<Vec<_>>>()?;
705 Ok(Acl::of_with_policies(record.subject, subject_type, policies))
706}
707
708fn policy_from_record(record: StoredPolicyRecord) -> AuthServiceResult<Policy> {
709 let policy_type = PolicyType::get_by_name(&record.policy_type)
710 .ok_or_else(|| snapshot_decode_error(format!("Invalid policy type '{}'", record.policy_type)))?;
711 let entries = record
712 .entries
713 .into_iter()
714 .map(policy_entry_from_record)
715 .collect::<AuthServiceResult<Vec<_>>>()?;
716 Ok(Policy::of_entries(policy_type, entries))
717}
718
719fn policy_entry_from_record(record: StoredPolicyEntryRecord) -> AuthServiceResult<PolicyEntry> {
720 let resource = Resource::of_str(&record.resource)
721 .ok_or_else(|| snapshot_decode_error(format!("Invalid resource '{}'", record.resource)))?;
722 let actions = record
723 .actions
724 .iter()
725 .map(|action| {
726 Action::get_by_name(action).ok_or_else(|| snapshot_decode_error(format!("Invalid action '{action}'")))
727 })
728 .collect::<AuthServiceResult<Vec<_>>>()?;
729 let decision = Decision::get_by_name(&record.decision)
730 .ok_or_else(|| snapshot_decode_error(format!("Invalid decision '{}'", record.decision)))?;
731 let environment = Environment::of_list(record.source_ips);
732 Ok(PolicyEntry::of(resource, actions, environment, decision))
733}
734
735fn subject_type_from_key(subject_key: &str) -> AuthServiceResult<SubjectType> {
736 let Some((subject_type, _)) = subject_key.split_once(':') else {
737 return Ok(SubjectType::User);
738 };
739 SubjectType::get_by_name(subject_type)
740 .ok_or_else(|| snapshot_decode_error(format!("Invalid subject type '{subject_type}'")))
741}
742
743fn storage_read_error(path: &Path, error: std::io::Error) -> AuthServiceError {
744 let _ = path;
745 AuthServiceError::storage_read_failed(error)
746}
747
748fn storage_write_error(path: &Path, error: std::io::Error) -> AuthServiceError {
749 let _ = path;
750 AuthServiceError::storage_write_failed(error)
751}
752
753fn snapshot_decode_error(reason: impl Into<String>) -> AuthServiceError {
754 let _ = reason.into();
755 AuthServiceError::serialization_failed(AuthOperation::DecodeMetadata)
756}
757
758fn temp_snapshot_path(path: &Path) -> PathBuf {
759 let file_name = path.file_name().and_then(|value| value.to_str()).unwrap_or("acls.json");
760 let nanos = std::time::SystemTime::now()
761 .duration_since(std::time::UNIX_EPOCH)
762 .map_or(0, |duration| duration.as_nanos());
763 path.with_file_name(format!(".{file_name}.{nanos}.tmp"))
764}
765
766impl Default for LocalAuthorizationMetadataProvider {
767 fn default() -> Self {
768 Self::new()
769 }
770}
771
772#[allow(async_fn_in_trait)]
773impl AuthorizationMetadataProvider for LocalAuthorizationMetadataProvider {
774 fn initialize(
775 &mut self,
776 config: AuthConfig,
777 _metadata_service: Option<Box<dyn Any + Send + Sync>>,
778 ) -> AuthServiceResult<()> {
779 {
780 let initialized = self.initialized_read()?;
781 if *initialized {
782 warn!("LocalAuthorizationMetadataProvider already initialized");
783 return Ok(());
784 }
785 }
786
787 self.cache_config = CacheConfig::from_auth_config(&config);
788 let storage_path = auth_metadata_snapshot_path(&config, "acls.json");
789 self.storage_path = storage_path.clone();
790
791 debug!("Initializing LocalAuthorizationMetadataProvider at: {:?}", storage_path);
792
793 let snapshot = match storage_path {
794 Some(path) => read_acl_snapshot(&path)?,
795 None => HashMap::new(),
796 };
797 self.replace_storage(snapshot)?;
798
799 let mut initialized = self.initialized_write()?;
800 *initialized = true;
801 debug!("LocalAuthorizationMetadataProvider initialized successfully");
802 Ok(())
803 }
804
805 fn shutdown(&mut self) {
806 let Ok(mut initialized) = self.initialized.write() else {
807 warn!("LocalAuthorizationMetadataProvider initialized lock is poisoned during shutdown");
808 return;
809 };
810 if !*initialized {
811 return;
812 }
813
814 debug!("Shutting down LocalAuthorizationMetadataProvider");
815
816 if let Err(error) = self.commit_storage_snapshot(HashMap::new(), CacheCommit::Clear) {
817 warn!("LocalAuthorizationMetadataProvider storage cleanup failed during shutdown: {error}");
818 }
819
820 *initialized = false;
821 debug!("LocalAuthorizationMetadataProvider shut down");
822 }
823
824 async fn create_acl(&self, acl: Acl) -> AuthServiceResult<()> {
825 self.ensure_initialized()?;
826
827 let subject_key = acl.subject_key().to_string();
828 debug!("Creating ACL for subject: {}", subject_key);
829
830 let _write_guard = self.write_lock.lock().await;
831 let mut snapshot = {
832 let storage = self.storage_read()?;
833 storage.clone()
834 };
835 if snapshot.contains_key(&subject_key) {
836 return Err(AuthServiceError::invalid_context(format!(
837 "ACL already exists for subject: {}",
838 subject_key
839 )));
840 }
841 snapshot.insert(subject_key.clone(), acl.clone());
842
843 let persistence = self.persist_storage_snapshot(&snapshot).await;
844 if !persistence.may_have_written() {
845 return Err(persistence
846 .into_error()
847 .expect("not-written persistence carries its error"));
848 }
849 self.commit_storage_snapshot(
854 snapshot,
855 CacheCommit::Store {
856 subject_key: subject_key.clone(),
857 acl,
858 },
859 )?;
860 match persistence.into_error() {
861 Some(error) => Err(error),
862 None => {
863 debug!("ACL created successfully for subject: {}", subject_key);
864 Ok(())
865 }
866 }
867 }
868
869 async fn delete_acl<S: Subject + Send + Sync>(&self, subject: &S) -> AuthServiceResult<()> {
870 self.ensure_initialized()?;
871
872 let subject_key = subject.subject_key();
873 debug!("Deleting ACL for subject: {}", subject_key);
874
875 let _write_guard = self.write_lock.lock().await;
876 let mut snapshot = {
877 let storage = self.storage_read()?;
878 storage.clone()
879 };
880 snapshot.remove(subject_key);
881 let persistence = self.persist_storage_snapshot(&snapshot).await;
882 if !persistence.may_have_written() {
883 return Err(persistence
884 .into_error()
885 .expect("not-written persistence carries its error"));
886 }
887 self.commit_storage_snapshot(
891 snapshot,
892 CacheCommit::Remove {
893 subject_key: subject_key.to_string(),
894 },
895 )?;
896 match persistence.into_error() {
897 Some(error) => Err(error),
898 None => {
899 debug!("ACL deleted successfully for subject: {}", subject_key);
900 Ok(())
901 }
902 }
903 }
904
905 async fn update_acl(&self, acl: Acl) -> AuthServiceResult<()> {
906 self.ensure_initialized()?;
907
908 let subject_key = acl.subject_key().to_string();
909 debug!("Updating ACL for subject: {}", subject_key);
910
911 let _write_guard = self.write_lock.lock().await;
912 let mut snapshot = {
913 let storage = self.storage_read()?;
914 storage.clone()
915 };
916 snapshot.insert(subject_key.clone(), acl.clone());
917 let persistence = self.persist_storage_snapshot(&snapshot).await;
918 if !persistence.may_have_written() {
919 return Err(persistence
920 .into_error()
921 .expect("not-written persistence carries its error"));
922 }
923 self.commit_storage_snapshot(
924 snapshot,
925 CacheCommit::Store {
926 subject_key: subject_key.clone(),
927 acl,
928 },
929 )?;
930 match persistence.into_error() {
931 Some(error) => Err(error),
932 None => {
933 debug!("ACL updated successfully for subject: {}", subject_key);
934 Ok(())
935 }
936 }
937 }
938
939 fn get_acl<S: Subject + Send + Sync>(
940 &self,
941 subject: &S,
942 ) -> impl std::future::Future<Output = AuthServiceResult<Option<Acl>>> + Send {
943 let initialized = self.ensure_initialized();
944 let subject_key = subject.subject_key().to_string();
945 let provider = self;
946
947 async move {
948 initialized?;
949
950 debug!("Getting ACL for subject: {}", subject_key);
951 provider.get_cached(&subject_key).await
952 }
953 }
954
955 async fn list_acl(
956 &self,
957 subject_filter: Option<&str>,
958 resource_filter: Option<&str>,
959 ) -> AuthServiceResult<Vec<Acl>> {
960 self.ensure_initialized()?;
961
962 debug!(
963 "Listing ACLs with subject_filter={:?}, resource_filter={:?}",
964 subject_filter, resource_filter
965 );
966
967 let filtered_acls = self.filter_acls(self.list_from_storage()?, subject_filter, resource_filter);
970
971 debug!("Found {} matching ACLs", filtered_acls.len());
972 Ok(filtered_acls)
973 }
974}
975
976#[cfg(test)]
977mod tests {
978 use std::fs;
979 use std::sync::atomic::AtomicU64;
980 use std::sync::atomic::Ordering;
981 use std::sync::Barrier;
982
983 use cheetah_string::CheetahString;
984 use rocketmq_security_api::Action;
985 use tempfile::TempDir;
986
987 use super::*;
988 use crate::authentication::enums::subject_type::SubjectType;
989 use crate::authentication::model::user::User;
990 use crate::authorization::enums::decision::Decision;
991 use crate::authorization::enums::policy_type::PolicyType;
992 use crate::authorization::model::environment::Environment;
993 use crate::authorization::model::policy::Policy;
994 use crate::authorization::model::policy_entry::PolicyEntry;
995 use crate::authorization::model::resource::Resource;
996
997 #[derive(Default)]
998 struct ManualClock {
999 now_millis: AtomicU64,
1000 }
1001
1002 impl ManualClock {
1003 fn advance(&self, duration: Duration) {
1004 let millis = u64::try_from(duration.as_millis()).unwrap();
1005 self.now_millis.fetch_add(millis, Ordering::Relaxed);
1006 }
1007 }
1008
1009 impl MonotonicClock for ManualClock {
1010 fn now(&self) -> Duration {
1011 Duration::from_millis(self.now_millis.load(Ordering::Relaxed))
1012 }
1013 }
1014
1015 fn acl_for_topic(subject: &str, topic: &str) -> Acl {
1016 Acl::of(
1017 subject,
1018 SubjectType::User,
1019 Policy::of(
1020 vec![Resource::of_topic(topic)],
1021 vec![Action::Pub],
1022 None,
1023 Decision::Allow,
1024 ),
1025 )
1026 }
1027
1028 fn metadata_io_actor(name: &str) -> MetadataIoActor {
1029 let context = rocketmq_runtime::RuntimeContext::try_from_current(name).unwrap();
1030 rocketmq_runtime::MetadataIoConfig::default()
1031 .into_plan()
1032 .expect("default metadata I/O config is valid")
1033 .start(&context.service_context("auth.authorization-metadata"))
1034 .unwrap()
1035 }
1036
1037 #[tokio::test]
1038 async fn test_local_provider_initialization() {
1039 let mut provider = LocalAuthorizationMetadataProvider::new();
1040 let config = AuthConfig::default();
1041
1042 let result = provider.initialize(config, None);
1043 assert!(result.is_ok());
1044
1045 let config2 = AuthConfig::default();
1047 let result2 = provider.initialize(config2, None);
1048 assert!(result2.is_ok());
1049 }
1050
1051 #[tokio::test]
1052 async fn test_local_provider_create_acl() {
1053 let mut provider = LocalAuthorizationMetadataProvider::new();
1054 provider.initialize(AuthConfig::default(), None).unwrap();
1055
1056 let resource = Resource::of_topic("test-topic");
1057 let entry = PolicyEntry::of(
1058 resource,
1059 vec![Action::Pub],
1060 Environment::of("192.168.1.1"),
1061 Decision::Allow,
1062 );
1063 let policy = Policy::of_entries(PolicyType::Custom, vec![entry]);
1064 let acl = Acl::of("user:test", SubjectType::User, policy);
1065
1066 let result = provider.create_acl(acl).await;
1067 assert!(result.is_ok());
1068 }
1069
1070 #[tokio::test]
1071 async fn test_local_provider_get_acl() {
1072 let mut provider = LocalAuthorizationMetadataProvider::new();
1073 provider.initialize(AuthConfig::default(), None).unwrap();
1074
1075 let user = User::of("test");
1076 let result = provider.get_acl(&user).await;
1077
1078 assert!(result.is_ok());
1079 assert!(result.unwrap().is_none());
1080 }
1081
1082 #[tokio::test]
1083 async fn test_local_provider_update_acl() {
1084 let mut provider = LocalAuthorizationMetadataProvider::new();
1085 provider.initialize(AuthConfig::default(), None).unwrap();
1086
1087 let resource = Resource::of_topic("test-topic");
1088 let entry = PolicyEntry::of(
1089 resource,
1090 vec![Action::Pub],
1091 Environment::of("192.168.1.1"),
1092 Decision::Allow,
1093 );
1094 let policy = Policy::of_entries(PolicyType::Custom, vec![entry]);
1095 let acl = Acl::of("user:test", SubjectType::User, policy);
1096
1097 let result = provider.update_acl(acl).await;
1098 assert!(result.is_ok());
1099 }
1100
1101 #[tokio::test]
1102 async fn test_local_provider_delete_acl() {
1103 let mut provider = LocalAuthorizationMetadataProvider::new();
1104 provider.initialize(AuthConfig::default(), None).unwrap();
1105
1106 let user = User::of("test");
1107 let result = provider.delete_acl(&user).await;
1108 assert!(result.is_ok());
1109 }
1110
1111 #[tokio::test]
1112 async fn test_local_provider_list_acl() {
1113 let mut provider = LocalAuthorizationMetadataProvider::new();
1114 provider.initialize(AuthConfig::default(), None).unwrap();
1115
1116 let result = provider.list_acl(None, None).await;
1117 assert!(result.is_ok());
1118 assert!(result.unwrap().is_empty());
1119 }
1120
1121 #[tokio::test]
1122 async fn test_local_provider_not_initialized() {
1123 let provider = LocalAuthorizationMetadataProvider::new();
1124 let user = User::of("test");
1125
1126 let result = provider.get_acl(&user).await;
1127 let error = result.unwrap_err();
1128 assert_eq!(error.kind(), AuthFailureKind::Unavailable);
1129 assert_eq!(error.operation(), AuthOperation::Initialize);
1130 }
1131
1132 #[test]
1133 fn test_cache_config_default() {
1134 let config = CacheConfig::default();
1135 assert_eq!(config.max_size, 1000);
1136 assert_eq!(config.ttl, Duration::from_secs(300));
1137 assert_eq!(config.refresh_interval, Duration::from_secs(60));
1138 }
1139
1140 #[test]
1141 fn initialize_applies_acl_cache_config() {
1142 let mut provider = LocalAuthorizationMetadataProvider::new();
1143 provider
1144 .initialize(
1145 AuthConfig {
1146 acl_cache_max_num: 2,
1147 acl_cache_expired_second: 3,
1148 acl_cache_refresh_second: 4,
1149 ..AuthConfig::default()
1150 },
1151 None,
1152 )
1153 .unwrap();
1154
1155 assert_eq!(provider.cache_config.max_size, 2);
1156 assert_eq!(provider.cache_config.ttl, Duration::from_secs(3));
1157 assert_eq!(provider.cache_config.refresh_interval, Duration::from_secs(4));
1158 }
1159
1160 #[tokio::test]
1161 async fn zero_acl_cache_max_num_disables_cache_entries() {
1162 let mut provider = LocalAuthorizationMetadataProvider::new();
1163 provider
1164 .initialize(
1165 AuthConfig {
1166 acl_cache_max_num: 0,
1167 ..AuthConfig::default()
1168 },
1169 None,
1170 )
1171 .unwrap();
1172
1173 let acl = Acl::of(
1174 "cache-disabled",
1175 SubjectType::User,
1176 Policy::of(
1177 vec![Resource::of_topic("topic-a")],
1178 vec![Action::Pub],
1179 None,
1180 Decision::Allow,
1181 ),
1182 );
1183 provider.create_acl(acl).await.unwrap();
1184
1185 let user = User::of("cache-disabled");
1186 assert!(provider.get_acl(&user).await.unwrap().is_some());
1187 assert!(provider.cache.read().unwrap().is_empty());
1188 }
1189
1190 #[tokio::test]
1191 async fn acl_cache_refreshes_from_storage_at_the_configured_boundary() {
1192 let clock = Arc::new(ManualClock::default());
1193 let mut provider = LocalAuthorizationMetadataProvider::with_clock(clock.clone());
1194 provider
1195 .initialize(
1196 AuthConfig {
1197 acl_cache_expired_second: 10,
1198 acl_cache_refresh_second: 2,
1199 ..AuthConfig::default()
1200 },
1201 None,
1202 )
1203 .unwrap();
1204
1205 let first = acl_for_topic("refresh-bound", "topic-a");
1206 provider.create_acl(first.clone()).await.unwrap();
1207 let user = User::of("refresh-bound");
1208 let reads_before = provider.canonical_read_count.load(Ordering::Relaxed);
1209
1210 clock.advance(Duration::from_millis(1_999));
1211 assert_eq!(provider.get_acl(&user).await.unwrap(), Some(first.clone()));
1212 assert_eq!(provider.canonical_read_count.load(Ordering::Relaxed), reads_before);
1213 assert_eq!(
1214 provider.cache.read().unwrap()[first.subject_key()].created_at,
1215 Duration::ZERO
1216 );
1217
1218 clock.advance(Duration::from_millis(1));
1219 assert_eq!(provider.get_acl(&user).await.unwrap(), Some(first.clone()));
1220 assert_eq!(provider.canonical_read_count.load(Ordering::Relaxed), reads_before + 1);
1221 assert_eq!(
1222 provider.cache.read().unwrap()[first.subject_key()].created_at,
1223 Duration::from_secs(2)
1224 );
1225 assert_eq!(provider.list_acl(None, None).await.unwrap(), vec![first]);
1226 }
1227
1228 #[tokio::test]
1229 async fn zero_acl_cache_refresh_interval_reads_through_on_every_lookup() {
1230 let clock = Arc::new(ManualClock::default());
1231 let mut provider = LocalAuthorizationMetadataProvider::with_clock(clock.clone());
1232 provider
1233 .initialize(
1234 AuthConfig {
1235 acl_cache_expired_second: 10,
1236 acl_cache_refresh_second: 0,
1237 ..AuthConfig::default()
1238 },
1239 None,
1240 )
1241 .unwrap();
1242
1243 let first = acl_for_topic("refresh-zero", "topic-a");
1244 let second = acl_for_topic("refresh-zero", "topic-b");
1245 let third = acl_for_topic("refresh-zero", "topic-c");
1246 let user = User::of("refresh-zero");
1247 provider.create_acl(first).await.unwrap();
1248
1249 provider.update_acl(second.clone()).await.unwrap();
1250 let reads_before = provider.canonical_read_count.load(Ordering::Relaxed);
1251 assert_eq!(provider.get_acl(&user).await.unwrap(), Some(second));
1252 assert_eq!(provider.canonical_read_count.load(Ordering::Relaxed), reads_before + 1);
1253
1254 provider.update_acl(third.clone()).await.unwrap();
1255 assert_eq!(provider.get_acl(&user).await.unwrap(), Some(third.clone()));
1256 assert_eq!(provider.canonical_read_count.load(Ordering::Relaxed), reads_before + 2);
1257 assert_eq!(provider.list_acl(None, None).await.unwrap(), vec![third]);
1258 assert_eq!(clock.now(), Duration::ZERO);
1259 }
1260
1261 #[tokio::test]
1262 async fn acl_cache_expires_at_the_exact_ttl_boundary() {
1263 let clock = Arc::new(ManualClock::default());
1264 let mut provider = LocalAuthorizationMetadataProvider::with_clock(clock.clone());
1265 provider
1266 .initialize(
1267 AuthConfig {
1268 acl_cache_expired_second: 1,
1269 acl_cache_refresh_second: 10,
1270 ..AuthConfig::default()
1271 },
1272 None,
1273 )
1274 .unwrap();
1275
1276 let first = acl_for_topic("expiry-bound", "topic-a");
1277 provider.create_acl(first.clone()).await.unwrap();
1278 let user = User::of("expiry-bound");
1279 let reads_before = provider.canonical_read_count.load(Ordering::Relaxed);
1280
1281 clock.advance(Duration::from_millis(999));
1282 assert_eq!(provider.get_acl(&user).await.unwrap(), Some(first.clone()));
1283 assert_eq!(provider.canonical_read_count.load(Ordering::Relaxed), reads_before);
1284 assert_eq!(
1285 provider.cache.read().unwrap()[first.subject_key()].created_at,
1286 Duration::ZERO
1287 );
1288
1289 clock.advance(Duration::from_millis(1));
1290 assert_eq!(provider.get_acl(&user).await.unwrap(), Some(first.clone()));
1291 assert_eq!(provider.canonical_read_count.load(Ordering::Relaxed), reads_before + 1);
1292 assert_eq!(
1293 provider.cache.read().unwrap()[first.subject_key()].created_at,
1294 Duration::from_secs(1)
1295 );
1296 }
1297
1298 async fn assert_cache_wait_samples_boundary(ttl: Duration, refresh_interval: Duration, boundary: Duration) {
1299 let clock = Arc::new(ManualClock::default());
1300 let mut provider = LocalAuthorizationMetadataProvider::with_clock(clock.clone());
1301 provider
1302 .initialize(
1303 AuthConfig {
1304 acl_cache_expired_second: u32::try_from(ttl.as_secs()).unwrap(),
1305 acl_cache_refresh_second: u32::try_from(refresh_interval.as_secs()).unwrap(),
1306 ..AuthConfig::default()
1307 },
1308 None,
1309 )
1310 .unwrap();
1311
1312 let acl = acl_for_topic("lock-boundary", "topic-a");
1313 provider.create_acl(acl.clone()).await.unwrap();
1314 let provider = Arc::new(provider);
1315 let cache = provider.cache.clone();
1316 let cache_locked = Arc::new(Barrier::new(2));
1317 let release_cache = Arc::new(Barrier::new(2));
1318 let holder = {
1319 let cache_locked = cache_locked.clone();
1320 let release_cache = release_cache.clone();
1321 std::thread::spawn(move || {
1322 let _cache_guard = cache.write().unwrap();
1323 cache_locked.wait();
1324 release_cache.wait();
1325 })
1326 };
1327 cache_locked.wait();
1328
1329 let attempted = provider.install_cache_lock_attempt_hook();
1330 let reader = {
1331 let provider = provider.clone();
1332 tokio::spawn(async move { provider.get_acl(&User::of("lock-boundary")).await })
1333 };
1334 attempted.notified().await;
1335 clock.advance(boundary);
1336 release_cache.wait();
1337 holder.join().unwrap();
1338
1339 assert_eq!(reader.await.unwrap().unwrap(), Some(acl.clone()));
1340 assert_eq!(provider.cache.read().unwrap()[acl.subject_key()].created_at, boundary);
1341 }
1342
1343 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1344 async fn cache_freshness_samples_clock_after_lock_wait_at_refresh_and_ttl_boundaries() {
1345 assert_cache_wait_samples_boundary(Duration::from_secs(10), Duration::from_secs(2), Duration::from_secs(2))
1346 .await;
1347 assert_cache_wait_samples_boundary(Duration::from_secs(1), Duration::from_secs(10), Duration::from_secs(1))
1348 .await;
1349 }
1350
1351 #[tokio::test]
1352 async fn stale_refill_cannot_overwrite_or_return_after_concurrent_update() {
1353 let clock = Arc::new(ManualClock::default());
1354 let mut provider = LocalAuthorizationMetadataProvider::with_clock(clock.clone());
1355 provider
1356 .initialize(
1357 AuthConfig {
1358 acl_cache_expired_second: 10,
1359 acl_cache_refresh_second: 1,
1360 ..AuthConfig::default()
1361 },
1362 None,
1363 )
1364 .unwrap();
1365 let old_acl = acl_for_topic("update-race", "topic-old");
1366 let new_acl = acl_for_topic("update-race", "topic-new");
1367 provider.create_acl(old_acl).await.unwrap();
1368 clock.advance(Duration::from_secs(1));
1369 let (storage_read, resume) = provider.install_refill_hook();
1370 let provider = Arc::new(provider);
1371 let reader = {
1372 let provider = provider.clone();
1373 tokio::spawn(async move { provider.get_acl(&User::of("update-race")).await })
1374 };
1375
1376 storage_read.notified().await;
1377 provider.update_acl(new_acl.clone()).await.unwrap();
1378 resume.notify_one();
1379
1380 assert_eq!(reader.await.unwrap().unwrap(), Some(new_acl.clone()));
1381 assert_eq!(
1382 provider.cache.read().unwrap()[new_acl.subject_key()].acl,
1383 Some(new_acl.clone())
1384 );
1385 let reads_after_race = provider.canonical_read_count.load(Ordering::Relaxed);
1386 assert_eq!(
1387 provider.get_acl(&User::of("update-race")).await.unwrap(),
1388 Some(new_acl.clone())
1389 );
1390 assert_eq!(
1391 provider.canonical_read_count.load(Ordering::Relaxed),
1392 reads_after_race,
1393 "the immediate post-race lookup must hit the non-expired cache"
1394 );
1395 assert_eq!(provider.list_acl(None, None).await.unwrap(), vec![new_acl]);
1396 }
1397
1398 #[tokio::test]
1399 async fn stale_refill_cannot_resurrect_or_return_after_concurrent_delete() {
1400 let clock = Arc::new(ManualClock::default());
1401 let mut provider = LocalAuthorizationMetadataProvider::with_clock(clock.clone());
1402 provider
1403 .initialize(
1404 AuthConfig {
1405 acl_cache_expired_second: 10,
1406 acl_cache_refresh_second: 1,
1407 ..AuthConfig::default()
1408 },
1409 None,
1410 )
1411 .unwrap();
1412 provider
1413 .create_acl(acl_for_topic("delete-race", "topic-old"))
1414 .await
1415 .unwrap();
1416 clock.advance(Duration::from_secs(1));
1417 let (storage_read, resume) = provider.install_refill_hook();
1418 let provider = Arc::new(provider);
1419 let reader = {
1420 let provider = provider.clone();
1421 tokio::spawn(async move { provider.get_acl(&User::of("delete-race")).await })
1422 };
1423
1424 storage_read.notified().await;
1425 provider.delete_acl(&User::of("delete-race")).await.unwrap();
1426 resume.notify_one();
1427
1428 assert_eq!(reader.await.unwrap().unwrap(), None);
1429 assert_eq!(provider.cache.read().unwrap()["User:delete-race"].acl, None);
1430 let reads_after_race = provider.canonical_read_count.load(Ordering::Relaxed);
1431 assert_eq!(provider.get_acl(&User::of("delete-race")).await.unwrap(), None);
1432 assert_eq!(
1433 provider.canonical_read_count.load(Ordering::Relaxed),
1434 reads_after_race,
1435 "the immediate post-revocation lookup must hit the negative cache"
1436 );
1437 assert!(provider.list_acl(None, None).await.unwrap().is_empty());
1438 }
1439
1440 #[tokio::test]
1441 async fn list_uses_canonical_storage_and_never_overlays_a_stale_cache_snapshot() {
1442 let clock = Arc::new(ManualClock::default());
1443 let mut provider = LocalAuthorizationMetadataProvider::with_clock(clock);
1444 provider.initialize(AuthConfig::default(), None).unwrap();
1445 let old_acl = acl_for_topic("list-consistency", "topic-old");
1446 let new_acl = acl_for_topic("list-consistency", "topic-new");
1447 let subject_key = old_acl.subject_key().to_string();
1448 provider.create_acl(old_acl.clone()).await.unwrap();
1449 provider.update_acl(new_acl.clone()).await.unwrap();
1450 provider
1451 .cache
1452 .write()
1453 .unwrap()
1454 .insert(subject_key.clone(), CachedAcl::new(Some(old_acl), Duration::ZERO));
1455
1456 assert_eq!(provider.list_acl(None, None).await.unwrap(), vec![new_acl.clone()]);
1457
1458 provider.delete_acl(&User::of("list-consistency")).await.unwrap();
1459 provider
1460 .cache
1461 .write()
1462 .unwrap()
1463 .insert(subject_key, CachedAcl::new(Some(new_acl), Duration::ZERO));
1464 assert!(provider.list_acl(None, None).await.unwrap().is_empty());
1465 }
1466
1467 #[tokio::test]
1468 async fn local_provider_persists_acls_across_reinitialization() {
1469 let temp = TempDir::new().unwrap();
1470 let config = AuthConfig {
1471 auth_config_path: CheetahString::from_string(temp.path().join("auth.json").to_string_lossy().into_owned()),
1472 ..AuthConfig::default()
1473 };
1474
1475 let metadata_io = metadata_io_actor("auth-authorization-persistence-test");
1476 let mut provider = LocalAuthorizationMetadataProvider::with_metadata_io(metadata_io.clone());
1477 provider.initialize(config.clone(), None).unwrap();
1478 let acl = Acl::of(
1479 "alice",
1480 SubjectType::User,
1481 Policy::of(
1482 vec![Resource::of_topic("topic-a")],
1483 vec![Action::Pub],
1484 Environment::of("192.168.0.1"),
1485 Decision::Allow,
1486 ),
1487 );
1488 provider.create_acl(acl).await.unwrap();
1489
1490 let mut restarted = LocalAuthorizationMetadataProvider::with_metadata_io(metadata_io);
1491 restarted.initialize(config, None).unwrap();
1492 let user = User::of("alice");
1493 let restored = restarted.get_acl(&user).await.unwrap().unwrap();
1494
1495 assert_eq!(restored.subject_key(), "User:alice");
1496 assert_eq!(restored.policies().len(), 1);
1497 assert_eq!(restored.policies()[0].entries().len(), 1);
1498 assert_eq!(
1499 restored.policies()[0].entries()[0].resource().resource_key().as_deref(),
1500 Some("Topic:topic-a")
1501 );
1502 }
1503
1504 #[tokio::test]
1505 async fn local_provider_persists_acl_update_and_delete_across_reinitialization() {
1506 let temp = TempDir::new().unwrap();
1507 let config = AuthConfig {
1508 auth_config_path: CheetahString::from_string(temp.path().join("auth.json").to_string_lossy().into_owned()),
1509 ..AuthConfig::default()
1510 };
1511
1512 let metadata_io = metadata_io_actor("auth-authorization-update-delete-test");
1513 let mut provider = LocalAuthorizationMetadataProvider::with_metadata_io(metadata_io.clone());
1514 provider.initialize(config.clone(), None).unwrap();
1515 provider
1516 .create_acl(Acl::of(
1517 "alice",
1518 SubjectType::User,
1519 Policy::of(
1520 vec![Resource::of_topic("topic-a")],
1521 vec![Action::Pub],
1522 None,
1523 Decision::Allow,
1524 ),
1525 ))
1526 .await
1527 .unwrap();
1528 provider
1529 .update_acl(Acl::of(
1530 "alice",
1531 SubjectType::User,
1532 Policy::of(
1533 vec![Resource::of_topic("topic-b")],
1534 vec![Action::Sub],
1535 None,
1536 Decision::Deny,
1537 ),
1538 ))
1539 .await
1540 .unwrap();
1541
1542 let mut restarted = LocalAuthorizationMetadataProvider::with_metadata_io(metadata_io.clone());
1543 restarted.initialize(config.clone(), None).unwrap();
1544 let user = User::of("alice");
1545 let restored = restarted.get_acl(&user).await.unwrap().unwrap();
1546 let entry = &restored.policies()[0].entries()[0];
1547 assert_eq!(entry.resource().resource_key().as_deref(), Some("Topic:topic-b"));
1548 assert_eq!(entry.actions(), &vec![Action::Sub]);
1549 assert_eq!(entry.decision(), Decision::Deny);
1550
1551 restarted.delete_acl(&user).await.unwrap();
1552
1553 let mut deleted_restart = LocalAuthorizationMetadataProvider::with_metadata_io(metadata_io);
1554 deleted_restart.initialize(config, None).unwrap();
1555 assert!(deleted_restart.get_acl(&user).await.unwrap().is_none());
1556 }
1557
1558 #[test]
1559 fn local_provider_rejects_corrupted_acl_snapshot() {
1560 let temp = TempDir::new().unwrap();
1561 let config = AuthConfig {
1562 auth_config_path: CheetahString::from_string(temp.path().join("auth.json").to_string_lossy().into_owned()),
1563 ..AuthConfig::default()
1564 };
1565 let snapshot = temp.path().join("auth").join("acls.json");
1566 fs::create_dir_all(snapshot.parent().unwrap()).unwrap();
1567 fs::write(&snapshot, b"{not valid json").unwrap();
1568
1569 let mut provider = LocalAuthorizationMetadataProvider::new();
1570 let error = provider.initialize(config, None).unwrap_err();
1571
1572 assert_eq!(error.kind(), AuthFailureKind::InvalidData);
1573 assert_eq!(error.operation(), AuthOperation::DecodeMetadata);
1574 assert!(!error.to_string().contains("acls.json"));
1575 }
1576
1577 #[derive(Debug)]
1580 struct UnconfirmedReplacementFileSystem;
1581
1582 impl rocketmq_runtime::MetadataFileSystem for UnconfirmedReplacementFileSystem {
1583 fn persist_atomic(&self, _target: &Path, _bytes: &[u8]) -> rocketmq_runtime::RuntimeResult<()> {
1584 Err(rocketmq_runtime::RuntimeError::io(
1585 rocketmq_runtime::MetadataIoOperation::SyncParent.runtime_operation(),
1586 std::io::Error::other("injected parent directory sync failure"),
1587 ))
1588 }
1589 }
1590
1591 #[derive(Debug)]
1593 struct FailedBeforeCommitFileSystem;
1594
1595 impl rocketmq_runtime::MetadataFileSystem for FailedBeforeCommitFileSystem {
1596 fn persist_atomic(&self, _target: &Path, _bytes: &[u8]) -> rocketmq_runtime::RuntimeResult<()> {
1597 Err(rocketmq_runtime::RuntimeError::io(
1598 rocketmq_runtime::MetadataIoOperation::WriteTemporary.runtime_operation(),
1599 std::io::Error::other("injected temporary write failure"),
1600 ))
1601 }
1602 }
1603
1604 fn file_system_metadata_io_actor(
1605 name: &str,
1606 file_system: Arc<dyn rocketmq_runtime::MetadataFileSystem>,
1607 ) -> MetadataIoActor {
1608 let context = rocketmq_runtime::RuntimeContext::try_from_current(name).unwrap();
1609 rocketmq_runtime::MetadataIoConfig::default()
1610 .into_plan()
1611 .expect("default metadata I/O config is valid")
1612 .start_with_file_system(&context.service_context("auth.authorization-metadata"), file_system)
1613 .unwrap()
1614 }
1615
1616 fn provider_over(
1617 name: &str,
1618 temp: &TempDir,
1619 file_system: Arc<dyn rocketmq_runtime::MetadataFileSystem>,
1620 ) -> LocalAuthorizationMetadataProvider {
1621 let config = AuthConfig {
1622 auth_config_path: CheetahString::from_string(temp.path().join("auth.json").to_string_lossy().into_owned()),
1623 ..AuthConfig::default()
1624 };
1625 let mut provider =
1626 LocalAuthorizationMetadataProvider::with_metadata_io(file_system_metadata_io_actor(name, file_system));
1627 provider.initialize(config, None).unwrap();
1628 provider
1629 }
1630
1631 #[tokio::test]
1632 async fn unconfirmed_acl_replacement_publishes_memory_and_records_the_generation() {
1633 let temp = TempDir::new().unwrap();
1634 let provider = provider_over(
1635 "auth-unconfirmed-acl",
1636 &temp,
1637 Arc::new(UnconfirmedReplacementFileSystem),
1638 );
1639 assert!(!provider.reconciliation_required());
1640
1641 let error = provider
1642 .create_acl(acl_for_topic("alice", "topic-a"))
1643 .await
1644 .expect_err("an unconfirmed replacement reports an error");
1645 assert!(!error.to_string().is_empty());
1646
1647 assert_eq!(
1651 provider.storage_read().unwrap().len(),
1652 1,
1653 "an unconfirmed replacement must not be rolled back in memory"
1654 );
1655 assert!(provider.reconciliation_required());
1656 assert!(provider.reconciliation_generation().is_some());
1657 }
1658
1659 #[tokio::test]
1660 async fn failed_before_commit_acl_write_keeps_memory_and_reconciliation_clear() {
1661 let temp = TempDir::new().unwrap();
1662 let provider = provider_over("auth-pre-commit-acl", &temp, Arc::new(FailedBeforeCommitFileSystem));
1663
1664 provider
1665 .create_acl(acl_for_topic("alice", "topic-a"))
1666 .await
1667 .expect_err("a definite pre-commit failure reports an error");
1668
1669 assert!(
1670 provider.storage_read().unwrap().is_empty(),
1671 "a change that provably never reached the file must not be published"
1672 );
1673 assert!(!provider.reconciliation_required());
1674 assert!(provider.reconciliation_generation().is_none());
1675 }
1676}