Skip to main content

rocketmq_auth/authorization/metadata_provider/
local.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Local authorization metadata provider implementation.
16//!
17//! This module provides a local implementation of the `AuthorizationMetadataProvider`
18//! trait for storing and retrieving ACL (Access Control List) metadata.
19
20use 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
63/// Local authorization metadata provider backed by an in-memory snapshot and an optional JSON
64/// snapshot file.
65///
66/// This provider implements ACL metadata storage with the following features:
67/// - Persistent local snapshot storage when `auth_config_path` is configured
68/// - In-memory caching for performance
69/// - Thread-safe operations
70/// - Automatic cache invalidation on updates
71///
72/// # Architecture
73///
74/// ```text
75/// LocalAuthorizationMetadataProvider
76/// |- JSON snapshot file (persistent, optional)
77/// |  `- acls.json
78/// `- Cache (in-memory)
79///    `- subject_key -> ACL
80/// ```
81///
82/// # Storage Format
83///
84/// ACLs are stored in a JSON snapshot with the following layout:
85/// - **Key**: Subject key (e.g., "User:alice", "Role:admin")
86/// - **Value**: JSON-serialized ACL object
87///
88/// # Thread Safety
89///
90/// This implementation uses `Arc` and `RwLock` to ensure thread-safe access
91/// to both the storage and cache layers.
92///
93/// # Examples
94///
95/// ```rust,ignore
96/// use rocketmq_auth::LocalAuthorizationMetadataProvider;
97/// use rocketmq_auth::AuthConfig;
98///
99/// let config = AuthConfig {
100///     auth_config_path: "/path/to/config".to_string(),
101///     ..Default::default()
102/// };
103///
104/// let mut provider = LocalAuthorizationMetadataProvider::new();
105/// provider.initialize(config, None)?;
106/// ```
107pub struct LocalAuthorizationMetadataProvider {
108    /// Path to the JSON snapshot file.
109    storage_path: Option<PathBuf>,
110    storage: Arc<RwLock<HashMap<String, Acl>>>,
111
112    /// In-memory ACL cache (subject_key -> ACL)
113    /// Using RwLock for thread-safe access
114    cache: Arc<RwLock<HashMap<String, CachedAcl>>>,
115
116    /// Cache configuration
117    cache_config: CacheConfig,
118    clock: Arc<dyn MonotonicClock>,
119    /// Generation of the canonical storage snapshot. Writers publish storage before advancing the
120    /// generation with `Release`; refill readers use `Acquire` before reading and while holding the
121    /// cache publication lock.
122    storage_generation: AtomicU64,
123
124    /// The one ACL snapshot generation whose durability was not confirmed.
125    reconciliation: ReconciliationGeneration,
126
127    /// Initialization state
128    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/// Cached ACL entry with expiration
153#[derive(Clone, Debug)]
154struct CachedAcl {
155    acl: Option<Acl>,
156    /// Timestamp when this entry was last accessed (for LRU)
157    last_accessed: Duration,
158    /// Timestamp when this entry was created (for TTL)
159    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    /// A cached value is never returned beyond either configured bound. A zero refresh interval
172    /// therefore makes every lookup read through to the canonical in-memory storage snapshot.
173    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/// Cache configuration
184#[derive(Clone, Debug)]
185struct CacheConfig {
186    /// Maximum number of entries in cache
187    max_size: usize,
188    /// Time-to-live for cache entries
189    ttl: Duration,
190    /// Time after which entries are refreshed
191    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),             // 5 minutes
199            refresh_interval: Duration::from_secs(60), // 1 minute
200        }
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        // A flush re-encodes memory that is already published, so only the
219        // reported error matters here.
220        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    /// Create a new local authorization metadata provider.
238    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    /// Writes one ACL snapshot and reports what the provider confirmed.
314    ///
315    /// The result is not an error type on purpose: a caller has to publish the
316    /// accepted change even when the replacement is unconfirmed, and only a
317    /// definite pre-commit failure may hold it back.
318    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            // The record is released by evidence: a later confirmed generation
342            // at least as new as the recorded one resolves it.
343            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    /// Returns whether a metadata write is still unconfirmed.
375    pub fn reconciliation_required(&self) -> bool {
376        self.reconciliation.required()
377    }
378
379    /// Returns the generation whose durability was not confirmed.
380    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    /// Get ACL from cache, loading from storage if necessary.
394    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        // Check cache first. Refresh is access-triggered, so no background task or second
400        // lifecycle owner is required.
401        {
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        // Cache miss or expired, load from storage
422        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        // A mutation commits while holding the same cache lock, so equality here proves that the
430        // canonical value read above still belongs to the current storage generation.
431        {
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        // A refill that overlaps a mutation waits once for the existing mutation lock, then reads
440        // and publishes while holding the cache commit boundary. This is bounded and cannot spin
441        // under repeated writer contention.
442        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    /// Commit invariant: every canonical replacement holds the cache publication lock, replaces
451    /// storage, and advances `storage_generation` with `Release` before applying its final cache
452    /// state. A refill either publishes first and is overwritten by this commit, or observes the
453    /// new generation with `Acquire` and performs a settled reread.
454    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    /// Evict the oldest (least recently used) entry from cache.
493    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    /// Filter ACLs by subject and resource patterns.
545    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                // Filter by subject
549                if let Some(filter) = subject_filter {
550                    if !acl.subject_key().contains(filter) {
551                        return false;
552                    }
553                }
554
555                // Filter by resource
556                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                // Keep ACLs that have policy entries
575                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        // The change is published even when the replacement is unconfirmed.
850        // Dropping it would leave memory older than a file that may already
851        // contain it, and the next mutation would write that older snapshot
852        // over the newer durable state.
853        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        // A revocation is applied to memory even when the replacement is
888        // unconfirmed, so a deleted ACL is never silently reinstated by the
889        // next snapshot.
890        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        // Canonical storage is the source of truth. Mixing it with a separately sampled cache can
968        // resurrect a value that a concurrent update or revocation already replaced.
969        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        // Double initialization should succeed with warning
1046        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    /// Fails after the target was replaced, which the actor reports as an
1578    /// unconfirmed replacement.
1579    #[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    /// Fails before the target was replaced.
1592    #[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        // The target may already hold the change, so memory must not be older
1648        // than it. Dropping the snapshot here is what let the next mutation
1649        // write the older state over the newer file.
1650        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}