Skip to main content

rocketmq_auth/
runtime.rs

1// Copyright 2026 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
15use std::collections::HashSet;
16use std::sync::atomic::AtomicU64;
17use std::sync::atomic::Ordering as AtomicOrdering;
18use std::sync::Arc;
19use std::sync::RwLock;
20use std::time::Duration;
21
22use cheetah_string::CheetahString;
23use rocketmq_protocol::protocol::remoting_command::RemotingCommand;
24use rocketmq_runtime::BlockingExecutor;
25use rocketmq_runtime::ChildServiceContext;
26use rocketmq_runtime::MetadataIoActor;
27use rocketmq_runtime::MetadataIoConfig;
28use rocketmq_runtime::ScheduledExecutionPolicy;
29use rocketmq_runtime::ScheduledTaskConfig;
30use rocketmq_runtime::ScheduledTaskGroup;
31use rocketmq_runtime::ScheduledTaskSnapshot;
32use rocketmq_runtime::ShutdownReport;
33use rocketmq_security_api::AuthorizationDecision;
34use rocketmq_security_api::DetailedDecision;
35use rocketmq_security_api::LayerEvaluation;
36use rocketmq_security_api::LayerFailureKind;
37use rocketmq_security_api::LayerRequirement;
38use rocketmq_security_api::ResourcePattern;
39use rocketmq_security_api::ResourceType;
40use tracing::debug;
41use tracing::info;
42use tracing::warn;
43
44use crate::acl::AclConfigFingerprint;
45use crate::acl::FileAclConfigLoader;
46use crate::acl::WhiteList;
47use crate::authentication::builder::default_authentication_context_builder::DefaultAuthenticationContextBuilder;
48use crate::authentication::builder::AuthenticationContextBuilder;
49use crate::authentication::enums::subject_type::SubjectType;
50use crate::authentication::enums::user_status::UserStatus;
51use crate::authentication::enums::user_type::UserType;
52use crate::authentication::model::user::User;
53#[cfg(test)]
54use crate::authentication::provider::authentication_metadata_provider::AuthenticationMetadataProvider;
55use crate::authentication::provider::AuthenticationProvider;
56use crate::authentication::provider::DefaultAuthenticationProvider;
57use crate::authentication::provider::LocalAuthenticationMetadataProvider;
58use crate::authorization::enums::policy_type::PolicyType;
59use crate::authorization::metadata_provider::AuthorizationMetadataProvider;
60use crate::authorization::metadata_provider::LocalAuthorizationMetadataProvider;
61use crate::authorization::model::acl::Acl;
62use crate::authorization::model::policy::Policy;
63use crate::authorization::model::policy_entry::PolicyEntry;
64use crate::authorization::model::resource::Resource;
65use crate::authorization::provider::AuthorizationProvider;
66use crate::authorization::provider::DefaultAuthorizationProvider;
67use crate::config::AuthConfig;
68use crate::migration::alc::acl_config::AclConfig;
69use crate::migration::alc::plain_access_config::PlainAccessConfig;
70use crate::migration::alc::plain_permission_manager::PlainPermissionManager;
71use crate::permission::Permission;
72use crate::project_authorization_error;
73use crate::AuthFailureKind;
74use crate::AuthMetrics;
75use crate::AuthMetricsSnapshot;
76use crate::AuthOperation;
77use crate::AuthServiceError;
78use crate::AuthServiceResult;
79use crate::RemotingAuthContext;
80
81const ACCESS_KEY: &str = "AccessKey";
82
83mod startup;
84pub use startup::AuthStartupFailure;
85#[cfg(test)]
86mod provider_tests;
87
88#[derive(Clone)]
89pub struct ProviderRegistry {
90    owner: Arc<crate::provider_owner::ProviderOwner>,
91    acl_white_list_snapshot: Arc<RwLock<WhiteList>>,
92    acl_managed_access_keys: Arc<RwLock<HashSet<String>>>,
93    acl_generation: Arc<AtomicU64>,
94    acl_fingerprint: Arc<RwLock<Option<AclConfigFingerprint>>>,
95    metrics: AuthMetrics,
96}
97
98impl ProviderRegistry {
99    pub fn local(config: &AuthConfig) -> AuthServiceResult<Self> {
100        Self::local_with_metadata_io(config, None)
101    }
102
103    pub fn local_with_metadata_io(
104        config: &AuthConfig,
105        metadata_io: Option<MetadataIoActor>,
106    ) -> AuthServiceResult<Self> {
107        validate_metadata_provider_name(
108            "authenticationMetadataProvider",
109            config.authentication_metadata_provider.as_str(),
110            &[
111                "LocalAuthenticationMetadataProvider",
112                "FileSnapshotAuthenticationMetadataProvider",
113                "local",
114                "file",
115            ],
116        )?;
117        validate_metadata_provider_name(
118            "authorizationMetadataProvider",
119            config.authorization_metadata_provider.as_str(),
120            &[
121                "LocalAuthorizationMetadataProvider",
122                "FileSnapshotAuthorizationMetadataProvider",
123                "local",
124                "file",
125            ],
126        )?;
127
128        let authentication_metadata_provider = Arc::new(
129            LocalAuthenticationMetadataProvider::with_config_and_metadata_io(config, metadata_io.clone())?,
130        );
131        let mut authorization_metadata_provider = metadata_io.map_or_else(
132            LocalAuthorizationMetadataProvider::new,
133            LocalAuthorizationMetadataProvider::with_metadata_io,
134        );
135        authorization_metadata_provider.initialize(config.clone(), None)?;
136
137        Ok(Self::from_bundle(
138            crate::provider_owner::local_bundle(
139                authentication_metadata_provider,
140                Arc::new(authorization_metadata_provider),
141            ),
142            true,
143        ))
144    }
145
146    fn from_bundle(bundle: crate::ProviderBundle, initialized: bool) -> Self {
147        Self {
148            owner: crate::provider_owner::ProviderOwner::new(bundle, initialized),
149            acl_white_list_snapshot: Arc::new(RwLock::new(WhiteList::default())),
150            acl_managed_access_keys: Arc::new(RwLock::new(HashSet::new())),
151            acl_generation: Arc::new(AtomicU64::new(0)),
152            acl_fingerprint: Arc::new(RwLock::new(None)),
153            metrics: AuthMetrics::default(),
154        }
155    }
156
157    /// Loads persistent authentication and authorization snapshots on the
158    /// caller-provided bounded metadata I/O lane.
159    pub async fn load_with_metadata_io(
160        config: &AuthConfig,
161        metadata_io: MetadataIoActor,
162        blocking: BlockingExecutor,
163    ) -> AuthServiceResult<Self> {
164        let config = config.clone();
165        blocking
166            .spawn_io("auth.provider-registry.load", move || {
167                Self::local_with_metadata_io(&config, Some(metadata_io))
168            })
169            .await
170            .map_err(AuthServiceError::metadata_io)?
171    }
172
173    pub fn authentication_metadata_provider(&self) -> Arc<crate::UserMetadataHandle> {
174        self.owner.users.clone()
175    }
176
177    pub(crate) fn admission(&self) -> Arc<crate::provider_owner::ProviderAdmission> {
178        self.owner.admission.clone()
179    }
180
181    pub fn authorization_metadata_provider(&self) -> Arc<crate::AclMetadataHandle> {
182        self.owner.acls.clone()
183    }
184
185    pub async fn shutdown_until(&self, deadline: rocketmq_runtime::ShutdownDeadline) -> AuthServiceResult<()> {
186        self.owner.shutdown_until(deadline, true).await
187    }
188
189    fn set_acl_white_list_snapshot(&self, snapshot: WhiteList) -> AuthServiceResult<()> {
190        let mut guard = self
191            .acl_white_list_snapshot
192            .write()
193            .map_err(|_| AuthServiceError::storage_lock_failed("auth.acl.white-list"))?;
194        *guard = snapshot;
195        Ok(())
196    }
197
198    fn acl_managed_access_keys(&self) -> AuthServiceResult<HashSet<String>> {
199        let guard = self
200            .acl_managed_access_keys
201            .read()
202            .map_err(|_| AuthServiceError::storage_lock_failed("auth.acl.managed-keys"))?;
203        Ok(guard.clone())
204    }
205
206    fn set_acl_managed_access_keys(&self, access_keys: HashSet<String>) -> AuthServiceResult<()> {
207        let mut guard = self
208            .acl_managed_access_keys
209            .write()
210            .map_err(|_| AuthServiceError::storage_lock_failed("auth.acl.managed-keys"))?;
211        *guard = access_keys;
212        Ok(())
213    }
214
215    fn advance_acl_generation(&self) -> u64 {
216        self.metrics.record_cache_invalidation();
217        self.acl_generation.fetch_add(1, AtomicOrdering::AcqRel) + 1
218    }
219
220    pub fn acl_generation(&self) -> u64 {
221        self.acl_generation.load(AtomicOrdering::Acquire)
222    }
223
224    pub fn acl_generation_counter(&self) -> Arc<AtomicU64> {
225        self.acl_generation.clone()
226    }
227
228    pub fn metrics(&self) -> AuthMetrics {
229        self.metrics.clone()
230    }
231
232    pub fn metrics_snapshot(&self) -> AuthMetricsSnapshot {
233        self.metrics.snapshot()
234    }
235
236    fn acl_fingerprint(&self) -> AuthServiceResult<Option<AclConfigFingerprint>> {
237        let guard = self
238            .acl_fingerprint
239            .read()
240            .map_err(|_| AuthServiceError::storage_lock_failed("auth.acl.fingerprint"))?;
241        Ok(*guard)
242    }
243
244    fn set_acl_fingerprint(&self, fingerprint: Option<AclConfigFingerprint>) -> AuthServiceResult<()> {
245        let mut guard = self
246            .acl_fingerprint
247            .write()
248            .map_err(|_| AuthServiceError::storage_lock_failed("auth.acl.fingerprint"))?;
249        *guard = fingerprint;
250        Ok(())
251    }
252
253    pub fn is_acl_white_remote_address(
254        &self,
255        access_key: Option<&str>,
256        source_ip: Option<&str>,
257    ) -> AuthServiceResult<bool> {
258        let _operation = self.owner.admission.enter()?;
259        let guard = self
260            .acl_white_list_snapshot
261            .read()
262            .map_err(|_| AuthServiceError::storage_lock_failed("auth.acl.white-list"))?;
263        let matched = guard.matches(access_key, source_ip);
264        self.metrics.record_whitelist_check(matched);
265        Ok(matched)
266    }
267
268    pub fn update_global_white_remote_addresses<I, S>(&self, addresses: I) -> AuthServiceResult<u64>
269    where
270        I: IntoIterator<Item = S>,
271        S: AsRef<str>,
272    {
273        let _operation = self.owner.admission.enter()?;
274        let updated_snapshot = {
275            let guard = self
276                .acl_white_list_snapshot
277                .read()
278                .map_err(|_| AuthServiceError::storage_lock_failed("auth.acl.white-list"))?;
279            guard.with_global_patterns(addresses)
280        };
281        self.set_acl_white_list_snapshot(updated_snapshot)?;
282        Ok(self.advance_acl_generation())
283    }
284}
285
286fn validate_metadata_provider_name(key: &'static str, configured: &str, supported: &[&str]) -> AuthServiceResult<()> {
287    let configured = configured.trim();
288    if configured.is_empty()
289        || supported
290            .iter()
291            .any(|candidate| configured.eq_ignore_ascii_case(candidate))
292    {
293        return Ok(());
294    }
295
296    let _ = (key, configured, supported);
297    Err(AuthServiceError::configuration_error("unsupported metadata provider"))
298}
299
300pub struct AuthRuntimeBuilder {
301    config: AuthConfig,
302    service_context: ChildServiceContext,
303    provider_registry: Option<ProviderRegistry>,
304    metadata_io: Option<MetadataIoActor>,
305}
306
307impl AuthRuntimeBuilder {
308    pub fn new(config: AuthConfig, service_context: ChildServiceContext) -> Self {
309        Self {
310            config,
311            service_context,
312            provider_registry: None,
313            metadata_io: None,
314        }
315    }
316
317    pub fn with_provider_registry(mut self, provider_registry: ProviderRegistry) -> Self {
318        self.provider_registry = Some(provider_registry);
319        self
320    }
321
322    pub fn with_provider_bundle(mut self, providers: crate::ProviderBundle) -> Self {
323        self.provider_registry = Some(ProviderRegistry::from_bundle(providers, false));
324        self
325    }
326
327    pub fn with_metadata_io_actor(mut self, metadata_io: MetadataIoActor) -> Self {
328        self.metadata_io = Some(metadata_io);
329        self
330    }
331
332    pub async fn build(self) -> AuthServiceResult<AuthRuntime> {
333        let owns_metadata_io = self.metadata_io.is_none();
334        let metadata_io = match self.metadata_io.clone() {
335            Some(metadata_io) => metadata_io,
336            None => match MetadataIoConfig::default()
337                .into_plan()
338                .expect("default metadata I/O config is valid")
339                .start(&self.service_context.component("auth.metadata-io"))
340            {
341                Ok(metadata_io) => metadata_io,
342                Err(error) => {
343                    return Err(startup::rollback(
344                        AuthServiceError::provider_failed(AuthOperation::Initialize, error),
345                        self.provider_registry.as_ref().map(|registry| registry.owner.clone()),
346                        None,
347                        self.service_context,
348                    )
349                    .await);
350                }
351            },
352        };
353        let registry_result = match self.provider_registry.clone() {
354            Some(provider_registry) => Ok(provider_registry),
355            None => {
356                ProviderRegistry::load_with_metadata_io(
357                    &self.config,
358                    metadata_io.clone(),
359                    self.service_context.metadata_io().clone(),
360                )
361                .await
362            }
363        };
364
365        let provider_registry = match registry_result {
366            Ok(registry) => registry,
367            Err(error) => {
368                return Err(startup::rollback(
369                    error,
370                    None,
371                    owns_metadata_io.then_some(metadata_io),
372                    self.service_context,
373                )
374                .await);
375            }
376        };
377        let prepared = self.prepare_services(&provider_registry).await;
378        let (authentication_service, authorization_service, acl_file_watch_handle) = match prepared {
379            Ok(services) => services,
380            Err(error) => {
381                return Err(startup::rollback(
382                    error,
383                    Some(provider_registry.owner.clone()),
384                    owns_metadata_io.then_some(metadata_io),
385                    self.service_context,
386                )
387                .await);
388            }
389        };
390
391        Ok(AuthRuntime {
392            config: self.config,
393            service_context: self.service_context,
394            provider_registry,
395            authentication_service,
396            authorization_service,
397            acl_file_watch_handle,
398            owned_metadata_io: owns_metadata_io.then_some(metadata_io),
399            shutdown_report: Arc::new(tokio::sync::Mutex::new(None)),
400        })
401    }
402
403    async fn prepare_services(
404        &self,
405        provider_registry: &ProviderRegistry,
406    ) -> AuthServiceResult<(AuthenticationService, AuthorizationService, Option<AclFileWatchHandle>)> {
407        validate_metadata_provider_name(
408            "authenticationProvider",
409            self.config
410                .authentication_provider
411                .as_str()
412                .rsplit('.')
413                .next()
414                .unwrap_or_default(),
415            &["DefaultAuthenticationProvider", "default"],
416        )?;
417        validate_metadata_provider_name(
418            "authorizationProvider",
419            self.config
420                .authorization_provider
421                .as_str()
422                .rsplit('.')
423                .next()
424                .unwrap_or_default(),
425            &["DefaultAuthorizationProvider", "default"],
426        )?;
427        // Reject unsupported imports before initializing or seeding a candidate.
428        if (!self.config.acl_file.trim().is_empty() || self.config.migrate_auth_from_v1_enabled)
429            && provider_registry.owner.importer.is_none()
430        {
431            return Err(AuthServiceError::new(
432                AuthOperation::InitializeProvider,
433                AuthFailureKind::Unsupported,
434            ));
435        }
436
437        provider_registry
438            .owner
439            .initialize(&self.config, &self.service_context)
440            .await?;
441        seed_initial_users(provider_registry, &self.config).await?;
442        let migrated_acl_entries = migrate_auth_from_v1(provider_registry, &self.config).await?;
443        if migrated_acl_entries > 0 {
444            info!(
445                "Migrated {} legacy ACL account(s) into auth metadata",
446                migrated_acl_entries
447            );
448        }
449        let loaded_acl_entries = load_configured_acl_file(
450            provider_registry,
451            &self.config,
452            self.service_context.metadata_io().clone(),
453            true,
454        )
455        .await?
456        .account_count;
457        if loaded_acl_entries > 0 {
458            info!("Loaded {} ACL account(s) from configured ACL file", loaded_acl_entries);
459        }
460
461        let mut authentication_provider = DefaultAuthenticationProvider::new();
462        authentication_provider.initialize_with_registry(self.config.clone(), provider_registry.clone())?;
463
464        let mut authorization_provider = DefaultAuthorizationProvider::new();
465        authorization_provider.initialize_with_registry(self.config.clone(), provider_registry.clone())?;
466
467        let authentication_service = AuthenticationService::new(
468            self.config.clone(),
469            Arc::new(authentication_provider),
470            provider_registry.metrics(),
471            provider_registry.owner.admission.clone(),
472        );
473        let authorization_service = AuthorizationService::new(
474            self.config.clone(),
475            Arc::new(authorization_provider),
476            provider_registry.metrics(),
477            provider_registry.owner.admission.clone(),
478        );
479        let acl_file_watch_handle =
480            start_acl_file_watcher(&self.config, provider_registry.clone(), &self.service_context)?;
481
482        Ok((authentication_service, authorization_service, acl_file_watch_handle))
483    }
484}
485
486#[derive(Clone)]
487pub struct AuthRuntime {
488    config: AuthConfig,
489    service_context: ChildServiceContext,
490    provider_registry: ProviderRegistry,
491    authentication_service: AuthenticationService,
492    authorization_service: AuthorizationService,
493    acl_file_watch_handle: Option<AclFileWatchHandle>,
494    owned_metadata_io: Option<MetadataIoActor>,
495    shutdown_report: Arc<tokio::sync::Mutex<Option<ShutdownReport>>>,
496}
497
498impl AuthRuntime {
499    /// Admits an entrypoint request and keeps its provider work in the drain set.
500    ///
501    /// # Errors
502    /// Returns `Unavailable` once shutdown starts, including for whitelist paths.
503    pub fn enter_request(&self) -> AuthServiceResult<crate::AuthRequestGuard> {
504        self.provider_registry.owner.admission.enter()
505    }
506
507    pub fn config(&self) -> &AuthConfig {
508        &self.config
509    }
510
511    pub fn provider_registry(&self) -> &ProviderRegistry {
512        &self.provider_registry
513    }
514
515    pub fn authentication_service(&self) -> &AuthenticationService {
516        &self.authentication_service
517    }
518
519    pub fn authorization_service(&self) -> &AuthorizationService {
520        &self.authorization_service
521    }
522
523    pub fn enabled(&self) -> bool {
524        self.config.authentication_enabled || self.config.authorization_enabled
525    }
526
527    /// Returns whether this runtime requires a detailed authorization result.
528    ///
529    /// Disabled authorization is an explicit compatibility boundary and is the
530    /// only configuration state represented by `Optional` here. Authentication
531    /// failures and authorization failures remain fail-closed layer failures.
532    #[must_use]
533    pub const fn detailed_authorization_requirement(&self) -> LayerRequirement {
534        if self.config.authorization_enabled {
535            LayerRequirement::Required
536        } else {
537            LayerRequirement::Optional
538        }
539    }
540
541    pub async fn reload_acl_file(&self) -> AuthServiceResult<usize> {
542        let _operation = self.provider_registry.owner.admission.enter()?;
543        Ok(load_configured_acl_file(
544            &self.provider_registry,
545            &self.config,
546            self.service_context.metadata_io().clone(),
547            true,
548        )
549        .await?
550        .account_count)
551    }
552
553    pub fn is_acl_white_remote_address(
554        &self,
555        access_key: Option<&str>,
556        source_ip: Option<&str>,
557    ) -> AuthServiceResult<bool> {
558        self.provider_registry
559            .is_acl_white_remote_address(access_key, source_ip)
560    }
561
562    pub fn acl_generation(&self) -> u64 {
563        self.provider_registry.acl_generation()
564    }
565
566    pub fn invalidate_acl_cache(&self) -> u64 {
567        self.provider_registry.advance_acl_generation()
568    }
569
570    pub fn acl_generation_counter(&self) -> Arc<AtomicU64> {
571        self.provider_registry.acl_generation_counter()
572    }
573
574    pub fn metrics(&self) -> AuthMetrics {
575        self.provider_registry.metrics()
576    }
577
578    pub fn metrics_snapshot(&self) -> AuthMetricsSnapshot {
579        self.provider_registry.metrics_snapshot()
580    }
581
582    pub fn update_global_white_remote_addresses<I, S>(&self, addresses: I) -> AuthServiceResult<u64>
583    where
584        I: IntoIterator<Item = S>,
585        S: AsRef<str>,
586    {
587        self.provider_registry.update_global_white_remote_addresses(addresses)
588    }
589
590    pub async fn shutdown(&self) -> AuthServiceResult<()> {
591        let _ = self.shutdown_with_report().await?;
592        Ok(())
593    }
594
595    pub async fn shutdown_with_report(&self) -> AuthServiceResult<Option<ShutdownReport>> {
596        self.shutdown_until(rocketmq_runtime::ShutdownDeadline::after(Duration::from_secs(5)))
597            .await
598    }
599
600    pub async fn shutdown_until(
601        &self,
602        deadline: rocketmq_runtime::ShutdownDeadline,
603    ) -> AuthServiceResult<Option<ShutdownReport>> {
604        self.provider_registry.owner.admission.close();
605        let at = tokio::time::Instant::from_std(deadline.instant());
606        let timeout = || AuthServiceError::new(AuthOperation::MaintainService, AuthFailureKind::Timeout);
607        let mut completed = tokio::time::timeout_at(at, self.shutdown_report.lock())
608            .await
609            .map_err(|_| timeout())?;
610        if completed.is_some() {
611            return Ok(completed.clone());
612        }
613        if let Some(watcher) = &self.acl_file_watch_handle {
614            let report = watcher.scheduled_tasks.shutdown(deadline.remaining()).await;
615            if !report.is_healthy() {
616                return Err(timeout());
617            }
618        }
619        self.provider_registry.shutdown_until(deadline).await?;
620        if let Some(metadata_io) = &self.owned_metadata_io {
621            let report = metadata_io
622                .shutdown_until(rocketmq_runtime::MetadataDeadline::at(at))
623                .await;
624            if report.timed_out || report.pending_operations != 0 {
625                return Err(timeout());
626            }
627        }
628        let report = self.service_context.task_group().shutdown(deadline.remaining()).await;
629        if !report.is_healthy() {
630            return Err(timeout());
631        }
632        *completed = Some(report.clone());
633        Ok(Some(report))
634    }
635
636    pub fn acl_file_watcher_snapshot(&self) -> Vec<ScheduledTaskSnapshot> {
637        self.acl_file_watch_handle
638            .as_ref()
639            .map(AclFileWatchHandle::snapshot)
640            .unwrap_or_default()
641    }
642
643    pub async fn check_remoting(
644        &self,
645        auth_context: &RemotingAuthContext,
646        command: &RemotingCommand,
647    ) -> AuthServiceResult<()> {
648        self.check_remoting_for_code(auth_context, command, command.code())
649            .await
650    }
651
652    /// Checks a remoting command using the immutable operation captured at
653    /// ingress rather than the processor-mutable command code.
654    pub async fn check_remoting_for_code(
655        &self,
656        auth_context: &RemotingAuthContext,
657        command: &RemotingCommand,
658        original_code: i32,
659    ) -> AuthServiceResult<()> {
660        let mut authoritative_command = command.clone();
661        authoritative_command.set_code_mut(original_code);
662        self.check_remoting_with_source_ip(
663            auth_context,
664            &authoritative_command,
665            auth_context.source_ip(),
666            auth_context.channel_id(),
667        )
668        .await
669    }
670
671    pub async fn check_remoting_with_source_ip(
672        &self,
673        auth_context: &RemotingAuthContext,
674        command: &RemotingCommand,
675        source_ip: Option<&str>,
676        channel_id: Option<&str>,
677    ) -> AuthServiceResult<()> {
678        let _operation = self.provider_registry.owner.admission.enter()?;
679        auth_context.validate()?;
680        if source_ip != auth_context.source_ip() || channel_id != auth_context.channel_id() {
681            return Err(AuthServiceError::new(
682                AuthOperation::Authenticate,
683                AuthFailureKind::Unauthenticated,
684            ));
685        }
686        if self.is_acl_white_remote_address(access_key_from_command(command), source_ip)? {
687            return Ok(());
688        }
689
690        self.authentication_service
691            .authenticate_remoting(command, channel_id)
692            .await?;
693        self.authorization_service
694            .authorize_remoting(auth_context, command)
695            .await
696    }
697
698    /// Evaluates authentication and detailed authorization for a remoting request.
699    ///
700    /// The global ACL whitelist remains an allow decision. When authorization is
701    /// disabled, the authorization service returns `Abstain`; callers must resolve
702    /// that value through the explicitly selected layer requirement. Errors never
703    /// become an abstention and deliberately carry no request or credential data.
704    pub async fn evaluate_remoting_detailed(
705        &self,
706        auth_context: &RemotingAuthContext,
707        command: &RemotingCommand,
708    ) -> LayerEvaluation<DetailedDecision> {
709        self.evaluate_remoting_detailed_for_code(auth_context, command, command.code())
710            .await
711    }
712
713    /// Evaluates remoting authentication and authorization against the
714    /// immutable operation code captured by trusted ingress.
715    pub async fn evaluate_remoting_detailed_for_code(
716        &self,
717        auth_context: &RemotingAuthContext,
718        command: &RemotingCommand,
719        original_code: i32,
720    ) -> LayerEvaluation<DetailedDecision> {
721        let mut authoritative_command = command.clone();
722        authoritative_command.set_code_mut(original_code);
723        let command = &authoritative_command;
724        auth_context.validate().map_err(|_| LayerFailureKind::Error)?;
725        let is_whitelisted = self
726            .is_acl_white_remote_address(access_key_from_command(command), auth_context.source_ip())
727            .map_err(|_| LayerFailureKind::Error)?;
728        if is_whitelisted {
729            return Ok(DetailedDecision::Allow);
730        }
731
732        self.authentication_service
733            .authenticate_remoting(command, auth_context.channel_id())
734            .await
735            .map_err(|_| LayerFailureKind::Error)?;
736        self.authorization_service
737            .authorize_remoting_detailed(auth_context, command)
738            .await
739    }
740
741    /// Authenticates a privileged maintenance request and returns the verified
742    /// identity consumed by the independent maintenance policy.
743    ///
744    /// Unlike the ordinary remoting path, this method never treats disabled
745    /// authentication or an authentication whitelist as success.
746    pub async fn authenticate_maintenance_principal(
747        &self,
748        command: &RemotingCommand,
749        channel_id: Option<&str>,
750    ) -> AuthServiceResult<CheetahString> {
751        self.authentication_service
752            .authenticate_maintenance_principal(command, channel_id)
753            .await
754    }
755}
756
757#[derive(Clone)]
758struct AclFileWatchHandle {
759    scheduled_tasks: ScheduledTaskGroup,
760}
761
762impl AclFileWatchHandle {
763    fn snapshot(&self) -> Vec<ScheduledTaskSnapshot> {
764        self.scheduled_tasks.snapshot()
765    }
766}
767
768#[derive(Clone)]
769pub struct AuthenticationService {
770    config: AuthConfig,
771    whitelist: HashSet<String>,
772    provider: Arc<DefaultAuthenticationProvider>,
773    builder: DefaultAuthenticationContextBuilder,
774    metrics: AuthMetrics,
775    admission: Arc<crate::provider_owner::ProviderAdmission>,
776}
777
778impl AuthenticationService {
779    fn new(
780        config: AuthConfig,
781        provider: Arc<DefaultAuthenticationProvider>,
782        metrics: AuthMetrics,
783        admission: Arc<crate::provider_owner::ProviderAdmission>,
784    ) -> Self {
785        Self {
786            whitelist: parse_whitelist(config.authentication_whitelist.as_str()),
787            config,
788            provider,
789            builder: DefaultAuthenticationContextBuilder::new(),
790            metrics,
791            admission,
792        }
793    }
794
795    pub async fn authenticate_remoting(
796        &self,
797        command: &RemotingCommand,
798        channel_id: Option<&str>,
799    ) -> AuthServiceResult<()> {
800        let _operation = self.admission.enter()?;
801        if !self.config.authentication_enabled {
802            return Ok(());
803        }
804        if self.whitelist.contains(&command.code().to_string()) {
805            self.metrics.record_whitelist_check(true);
806            return Ok(());
807        }
808
809        let context = self.builder.build_from_remoting(command, channel_id).inspect_err(|_| {
810            self.metrics.record_authentication_result(false);
811        })?;
812        self.provider.authenticate(&context).await
813    }
814
815    /// Authenticates a privileged maintenance request without a bypass path.
816    ///
817    /// # Errors
818    ///
819    /// Returns an authentication error when authentication is disabled, the
820    /// request has no canonical identity, the signed context is malformed, or
821    /// credential verification fails.
822    pub async fn authenticate_maintenance_principal(
823        &self,
824        command: &RemotingCommand,
825        channel_id: Option<&str>,
826    ) -> AuthServiceResult<CheetahString> {
827        let _operation = self.admission.enter()?;
828        if !self.config.authentication_enabled {
829            return Err(AuthServiceError::new(
830                AuthOperation::Authenticate,
831                AuthFailureKind::Unauthenticated,
832            ));
833        }
834        let context = self.builder.build_from_remoting(command, channel_id).inspect_err(|_| {
835            self.metrics.record_authentication_result(false);
836        })?;
837        let principal = context
838            .username()
839            .filter(|username| !username.trim().is_empty())
840            .cloned()
841            .ok_or_else(|| AuthServiceError::new(AuthOperation::Authenticate, AuthFailureKind::Unauthenticated))?;
842        self.provider.authenticate(&context).await?;
843        Ok(principal)
844    }
845}
846
847#[derive(Clone)]
848pub struct AuthorizationService {
849    config: AuthConfig,
850    whitelist: HashSet<String>,
851    provider: Arc<DefaultAuthorizationProvider>,
852    metrics: AuthMetrics,
853    admission: Arc<crate::provider_owner::ProviderAdmission>,
854}
855
856impl AuthorizationService {
857    fn new(
858        config: AuthConfig,
859        provider: Arc<DefaultAuthorizationProvider>,
860        metrics: AuthMetrics,
861        admission: Arc<crate::provider_owner::ProviderAdmission>,
862    ) -> Self {
863        Self {
864            whitelist: parse_whitelist(config.authorization_whitelist.as_str()),
865            config,
866            provider,
867            metrics,
868            admission,
869        }
870    }
871
872    pub async fn authorize_remoting(
873        &self,
874        auth_context: &RemotingAuthContext,
875        command: &RemotingCommand,
876    ) -> AuthServiceResult<()> {
877        let _operation = self.admission.enter()?;
878        auth_context.validate()?;
879        if !self.config.authorization_enabled {
880            return Ok(());
881        }
882        if self.whitelist.contains(&command.code().to_string()) {
883            self.metrics.record_whitelist_check(true);
884            return Ok(());
885        }
886
887        let contexts = self
888            .provider
889            .new_contexts_from_remoting_command(auth_context, command)
890            .inspect_err(|_| {
891                self.metrics.record_authorization_result(false);
892            })?;
893
894        for context in contexts {
895            let decision = self.provider.authorize(&context).await?;
896            require_authorization(decision)?;
897        }
898
899        Ok(())
900    }
901
902    /// Evaluates remoting authorization using the layered detailed contract.
903    ///
904    /// A disabled authorization service explicitly abstains; typed policy denials
905    /// remain denials, while provider and context errors retain a fail-closed
906    /// failure kind.
907    pub async fn authorize_remoting_detailed(
908        &self,
909        auth_context: &RemotingAuthContext,
910        command: &RemotingCommand,
911    ) -> LayerEvaluation<DetailedDecision> {
912        let _operation = self.admission.enter().map_err(|_| LayerFailureKind::Error)?;
913        auth_context.validate().map_err(|_| LayerFailureKind::Error)?;
914        if !self.config.authorization_enabled {
915            return Ok(DetailedDecision::Abstain);
916        }
917        if self.whitelist.contains(&command.code().to_string()) {
918            self.metrics.record_whitelist_check(true);
919            return Ok(DetailedDecision::Allow);
920        }
921
922        let contexts = self
923            .provider
924            .new_contexts_from_remoting_command(auth_context, command)
925            .map_err(|error| {
926                self.metrics.record_authorization_result(false);
927                project_authorization_error(&error)
928            })?;
929
930        for context in contexts {
931            match self.provider.authorize(&context).await {
932                Ok(AuthorizationDecision::Allow) => {}
933                Ok(AuthorizationDecision::Deny(_)) => {
934                    return Ok(DetailedDecision::Deny);
935                }
936                Err(error) => {
937                    return Err(project_authorization_error(&error));
938                }
939            }
940        }
941
942        Ok(DetailedDecision::Allow)
943    }
944}
945
946fn parse_whitelist(value: &str) -> HashSet<String> {
947    value
948        .split(',')
949        .map(str::trim)
950        .filter(|entry| !entry.is_empty())
951        .map(ToOwned::to_owned)
952        .collect()
953}
954
955#[derive(Clone, Copy, Debug, PartialEq, Eq)]
956struct AclReloadResult {
957    account_count: usize,
958    changed: bool,
959}
960
961async fn load_configured_acl_file(
962    provider_registry: &ProviderRegistry,
963    config: &AuthConfig,
964    blocking: rocketmq_runtime::BlockingExecutor,
965    force: bool,
966) -> AuthServiceResult<AclReloadResult> {
967    let acl_file = config.acl_file.as_str().trim();
968    if acl_file.is_empty() {
969        return Ok(AclReloadResult {
970            account_count: 0,
971            changed: false,
972        });
973    }
974
975    let metrics = provider_registry.metrics();
976    metrics.record_acl_reload_attempt();
977    let result = async {
978        let loader = FileAclConfigLoader::new(acl_file.to_owned(), blocking);
979        let (acl_config, fingerprint) = loader.load_with_fingerprint().await?;
980        if !force && provider_registry.acl_fingerprint()? == Some(fingerprint) {
981            return Ok(AclReloadResult {
982                account_count: 0,
983                changed: false,
984            });
985        }
986
987        let count = apply_acl_config(provider_registry, &acl_config).await?;
988        provider_registry.set_acl_fingerprint(Some(fingerprint))?;
989        Ok(AclReloadResult {
990            account_count: count,
991            changed: true,
992        })
993    }
994    .await;
995
996    match &result {
997        Ok(reload_result) if reload_result.changed => metrics.record_acl_reload_success(),
998        Ok(_) => metrics.record_acl_reload_skipped(),
999        Err(_) => metrics.record_acl_reload_failure(),
1000    }
1001
1002    result
1003}
1004
1005async fn migrate_auth_from_v1(provider_registry: &ProviderRegistry, config: &AuthConfig) -> AuthServiceResult<usize> {
1006    if !config.migrate_auth_from_v1_enabled {
1007        return Ok(0);
1008    }
1009    let plain_permission_manager = PlainPermissionManager::new();
1010    migrate_auth_from_v1_manager(provider_registry, &plain_permission_manager).await
1011}
1012
1013async fn migrate_auth_from_v1_manager(
1014    provider_registry: &ProviderRegistry,
1015    plain_permission_manager: &PlainPermissionManager,
1016) -> AuthServiceResult<usize> {
1017    let acl_config = plain_permission_manager.get_all_acl_config()?;
1018    apply_acl_config(provider_registry, &acl_config).await
1019}
1020
1021fn start_acl_file_watcher(
1022    config: &AuthConfig,
1023    provider_registry: ProviderRegistry,
1024    service_context: &ChildServiceContext,
1025) -> AuthServiceResult<Option<AclFileWatchHandle>> {
1026    let acl_file = config.acl_file.as_str().trim();
1027    if acl_file.is_empty() || !config.acl_file_watch_enabled {
1028        return Ok(None);
1029    }
1030
1031    let watch_config = config.clone();
1032    let interval = Duration::from_millis(config.acl_file_watch_interval_millis.max(1));
1033    let watcher_context = service_context.component("auth.acl-file-watcher");
1034    let task_group = watcher_context.task_group().clone();
1035    let scheduled_tasks = ScheduledTaskGroup::new(task_group.clone());
1036    let blocking = service_context.metadata_io().clone();
1037    scheduled_tasks
1038        .schedule(
1039            ScheduledTaskConfig::fixed_rate_no_overlap("auth.acl-file-watcher.reload", interval),
1040            ScheduledExecutionPolicy::default(),
1041            move || {
1042                let provider_registry = provider_registry.clone();
1043                let watch_config = watch_config.clone();
1044                let blocking = blocking.clone();
1045                async move {
1046                    match load_configured_acl_file(&provider_registry, &watch_config, blocking, false).await {
1047                        Ok(result) if result.changed => {
1048                            debug!(
1049                                "Reloaded {} ACL account(s) from configured ACL file",
1050                                result.account_count
1051                            )
1052                        }
1053                        Ok(_) => debug!("ACL file unchanged; skipped reload"),
1054                        Err(error) => warn!("Failed to reload ACL file: {error}"),
1055                    }
1056                }
1057            },
1058        )
1059        .map_err(|error| AuthServiceError::provider_failed(AuthOperation::Initialize, error))?;
1060
1061    Ok(Some(AclFileWatchHandle { scheduled_tasks }))
1062}
1063
1064async fn apply_acl_config(provider_registry: &ProviderRegistry, acl_config: &AclConfig) -> AuthServiceResult<usize> {
1065    let prepared_config = prepare_acl_config(acl_config)?;
1066    let previous_access_keys = provider_registry.acl_managed_access_keys()?;
1067    let importer = provider_registry
1068        .owner
1069        .importer
1070        .as_ref()
1071        .ok_or_else(|| AuthServiceError::new(AuthOperation::InitializeProvider, AuthFailureKind::Unsupported))?;
1072    let removed: Vec<_> = previous_access_keys
1073        .difference(&prepared_config.access_keys)
1074        .map(|key| crate::SubjectKey::from_subject(&User::of(key.as_str())))
1075        .collect();
1076    let _operation = provider_registry.owner.admission.enter()?;
1077    importer.import(&prepared_config.accounts, &removed).await?;
1078
1079    provider_registry.set_acl_white_list_snapshot(prepared_config.white_list_snapshot)?;
1080    provider_registry.set_acl_managed_access_keys(prepared_config.access_keys)?;
1081    provider_registry.advance_acl_generation();
1082
1083    Ok(prepared_config.accounts_len)
1084}
1085
1086struct PreparedAclConfig {
1087    white_list_snapshot: WhiteList,
1088    access_keys: HashSet<String>,
1089    accounts: Vec<crate::AclImportAccount>,
1090    accounts_len: usize,
1091}
1092
1093fn prepare_acl_config(acl_config: &AclConfig) -> AuthServiceResult<PreparedAclConfig> {
1094    let mut access_keys = HashSet::new();
1095    let mut accounts = Vec::new();
1096
1097    if let Some(plain_accounts) = acl_config.plain_access_configs() {
1098        for account in plain_accounts {
1099            let access_key = required_plain_field(account.access_key(), "accessKey", "<missing>")?.to_owned();
1100            let user = user_from_plain_account(account)?;
1101            let acl = acl_from_plain_account(account)?;
1102            access_keys.insert(access_key);
1103            accounts.push(crate::AclImportAccount { user, acl });
1104        }
1105    }
1106
1107    let accounts_len = accounts.len();
1108    Ok(PreparedAclConfig {
1109        white_list_snapshot: WhiteList::from_acl_config(acl_config),
1110        access_keys,
1111        accounts,
1112        accounts_len,
1113    })
1114}
1115
1116fn user_from_plain_account(account: &PlainAccessConfig) -> AuthServiceResult<User> {
1117    let access_key = required_plain_field(account.access_key(), "accessKey", "<missing>")?;
1118    let secret_key = required_plain_field(account.secret_key(), "secretKey", access_key)?;
1119    let user_type = if account.is_admin() {
1120        UserType::Super
1121    } else {
1122        UserType::Normal
1123    };
1124    let mut user = User::of_with_type(access_key, secret_key, user_type);
1125    user.set_user_status(UserStatus::Enable);
1126    Ok(user)
1127}
1128
1129fn acl_from_plain_account(account: &PlainAccessConfig) -> AuthServiceResult<Option<Acl>> {
1130    let access_key = required_plain_field(account.access_key(), "accessKey", "<missing>")?;
1131    let mut policies = Vec::new();
1132    let mut default_entries = Vec::new();
1133    let mut custom_entries = Vec::new();
1134
1135    if let Some(permission) = account.default_topic_perm() {
1136        push_default_entry(&mut default_entries, ResourceType::Topic, permission.as_str());
1137    }
1138    if let Some(permission) = account.default_group_perm() {
1139        push_default_entry(&mut default_entries, ResourceType::Group, permission.as_str());
1140    }
1141    if let Some(topic_perms) = account.topic_perms() {
1142        push_named_entries(&mut custom_entries, ResourceType::Topic, topic_perms);
1143    }
1144    if let Some(group_perms) = account.group_perms() {
1145        push_named_entries(&mut custom_entries, ResourceType::Group, group_perms);
1146    }
1147    if let Some(permission) = account.cluster_perm() {
1148        push_cluster_entry(&mut default_entries, permission.as_str())?;
1149    }
1150
1151    if !custom_entries.is_empty() {
1152        policies.push(Policy::of_entries(PolicyType::Custom, custom_entries));
1153    }
1154    if !default_entries.is_empty() {
1155        policies.push(Policy::of_entries(PolicyType::Default, default_entries));
1156    }
1157
1158    if policies.is_empty() {
1159        return Ok(None);
1160    }
1161
1162    Ok(Some(Acl::of_with_policies(
1163        access_key.to_owned(),
1164        SubjectType::User,
1165        policies,
1166    )))
1167}
1168
1169fn push_cluster_entry(entries: &mut Vec<PolicyEntry>, permission: &str) -> AuthServiceResult<()> {
1170    let permission = permission.trim();
1171    let (actions, decision) = match permission {
1172        "GET" => (
1173            vec![rocketmq_security_api::Action::Get],
1174            crate::authorization::enums::decision::Decision::Allow,
1175        ),
1176        "DENY" => (
1177            vec![rocketmq_security_api::Action::All],
1178            crate::authorization::enums::decision::Decision::Deny,
1179        ),
1180        _ => {
1181            return Err(AuthServiceError::configuration_error("invalid cluster permission"));
1182        }
1183    };
1184    entries.push(PolicyEntry::of(
1185        Resource::of(ResourceType::Cluster, None, ResourcePattern::Any),
1186        actions,
1187        None,
1188        decision,
1189    ));
1190    Ok(())
1191}
1192
1193fn push_default_entry(entries: &mut Vec<PolicyEntry>, resource_type: ResourceType, permission: &str) {
1194    let (actions, decision) = Permission::migration_actions_and_decision(Some(permission));
1195    entries.push(PolicyEntry::of(
1196        Resource::of(resource_type, None, ResourcePattern::Any),
1197        actions,
1198        None,
1199        decision,
1200    ));
1201}
1202
1203fn push_named_entries(
1204    entries: &mut Vec<PolicyEntry>,
1205    resource_type: ResourceType,
1206    permissions: &[cheetah_string::CheetahString],
1207) {
1208    for permission_entry in permissions {
1209        let raw = permission_entry.as_str().trim();
1210        if raw.is_empty() {
1211            continue;
1212        }
1213        let Some((resource_name, permission)) = raw.split_once('=') else {
1214            warn!("Skipping ACL policy entry without permission assignment: {raw}");
1215            continue;
1216        };
1217        let resource_name = resource_name.trim();
1218        if resource_name.is_empty() {
1219            warn!("Skipping ACL policy entry with blank resource name");
1220            continue;
1221        }
1222
1223        let resource = match resource_type {
1224            ResourceType::Topic => Resource::of_topic(resource_name),
1225            ResourceType::Group => Resource::of_group(resource_name.to_owned()),
1226            _ => Resource::of(resource_type, Some(resource_name.to_owned()), ResourcePattern::Literal),
1227        };
1228        let (actions, decision) = Permission::migration_actions_and_decision(Some(permission));
1229        entries.push(PolicyEntry::of(resource, actions, None, decision));
1230    }
1231}
1232
1233fn required_plain_field<'a>(
1234    value: Option<&'a cheetah_string::CheetahString>,
1235    field_name: &'static str,
1236    access_key: &str,
1237) -> AuthServiceResult<&'a str> {
1238    value
1239        .map(|value| value.as_str().trim())
1240        .filter(|value| !value.is_empty())
1241        .ok_or_else(|| {
1242            let _ = (field_name, access_key);
1243            AuthServiceError::configuration_error("required ACL field is blank")
1244        })
1245}
1246
1247async fn seed_initial_users(provider_registry: &ProviderRegistry, config: &AuthConfig) -> AuthServiceResult<()> {
1248    seed_init_authentication_user(provider_registry.authentication_metadata_provider(), config).await?;
1249    seed_inner_client_user(provider_registry.authentication_metadata_provider(), config).await
1250}
1251
1252async fn seed_init_authentication_user(
1253    provider: Arc<crate::UserMetadataHandle>,
1254    config: &AuthConfig,
1255) -> AuthServiceResult<()> {
1256    let init_user = config.init_authentication_user.as_str().trim();
1257    if init_user.is_empty() {
1258        return Ok(());
1259    }
1260
1261    if init_user.starts_with('{') {
1262        if let Ok(mut user) = serde_json::from_str::<User>(init_user) {
1263            user.set_user_type(UserType::Super);
1264            if user.user_status().is_none() {
1265                user.set_user_status(UserStatus::Enable);
1266            }
1267            return create_user_if_absent(provider, user).await;
1268        }
1269    }
1270
1271    let parts: Vec<&str> = init_user.splitn(2, ':').collect();
1272    if parts.len() == 2 {
1273        let username = parts[0].trim();
1274        let password = parts[1].trim();
1275        if !username.is_empty() && !password.is_empty() {
1276            let mut user = User::of_with_type(username, password, UserType::Super);
1277            user.set_user_status(UserStatus::Enable);
1278            return create_user_if_absent(provider, user).await;
1279        }
1280    }
1281
1282    Ok(())
1283}
1284
1285async fn seed_inner_client_user(
1286    provider: Arc<crate::UserMetadataHandle>,
1287    config: &AuthConfig,
1288) -> AuthServiceResult<()> {
1289    #[derive(serde::Deserialize)]
1290    struct SessionCredentials {
1291        #[serde(rename = "accessKey")]
1292        access_key: String,
1293        #[serde(rename = "secretKey")]
1294        secret_key: String,
1295    }
1296
1297    let credentials = config.inner_client_authentication_credentials.as_str().trim();
1298    if credentials.is_empty() {
1299        return Ok(());
1300    }
1301
1302    let Ok(credentials) = serde_json::from_str::<SessionCredentials>(credentials) else {
1303        return Ok(());
1304    };
1305
1306    let mut user = User::of_with_type(credentials.access_key, credentials.secret_key, UserType::Super);
1307    user.set_user_status(UserStatus::Enable);
1308    create_user_if_absent(provider, user).await
1309}
1310
1311async fn create_user_if_absent(provider: Arc<crate::UserMetadataHandle>, user: User) -> AuthServiceResult<()> {
1312    match provider.get_user(user.username().as_str()).await {
1313        Ok(_) => Ok(()),
1314        Err(error) if error.kind() == AuthFailureKind::NotFound => provider.create_user(user).await,
1315        Err(error) => Err(error),
1316    }
1317}
1318
1319fn require_authorization(decision: AuthorizationDecision) -> AuthServiceResult<()> {
1320    match decision {
1321        AuthorizationDecision::Allow => Ok(()),
1322        AuthorizationDecision::Deny(_) => Err(AuthServiceError::new(
1323            AuthOperation::Authorize,
1324            AuthFailureKind::PermissionDenied,
1325        )),
1326    }
1327}
1328
1329fn access_key_from_command(command: &RemotingCommand) -> Option<&str> {
1330    command.ext_fields().and_then(|fields| {
1331        fields
1332            .get(&CheetahString::from_static_str(ACCESS_KEY))
1333            .map(|value| value.as_str().trim())
1334            .filter(|value| !value.is_empty())
1335    })
1336}
1337
1338#[cfg(test)]
1339mod tests {
1340    use std::collections::HashMap;
1341    use std::fs;
1342    use std::time::Duration;
1343    use std::time::Instant;
1344
1345    use cheetah_string::CheetahString;
1346    use rocketmq_protocol::code::request_code::RequestCode;
1347    use rocketmq_security_api::Action;
1348    use rocketmq_security_api::AuthorizationDenial;
1349    use tempfile::TempDir;
1350    use tokio::time::sleep;
1351
1352    use super::*;
1353    use crate::authentication::acl_signer;
1354    use crate::authentication::enums::subject_type::SubjectType;
1355    use crate::authorization::enums::decision::Decision;
1356    use crate::authorization::enums::policy_type::PolicyType;
1357    use crate::authorization::model::acl::Acl;
1358    use crate::authorization::model::policy::Policy;
1359    use crate::authorization::model::resource::Resource;
1360
1361    #[test]
1362    fn ordinary_denials_use_one_fixed_public_error_and_r16_projection() {
1363        for denial in [
1364            AuthorizationDenial::SubjectUnknown,
1365            AuthorizationDenial::ResourceUnknown,
1366            AuthorizationDenial::PermissionDenied,
1367            AuthorizationDenial::PolicyNotApplicable,
1368            AuthorizationDenial::MaintenanceRestricted,
1369        ] {
1370            let error = require_authorization(AuthorizationDecision::Deny(denial)).unwrap_err();
1371            assert_eq!(error.operation(), AuthOperation::Authorize);
1372            assert_eq!(error.kind(), AuthFailureKind::PermissionDenied);
1373            let canonical = rocketmq_error::Error::from(error);
1374            assert_eq!(canonical.descriptor(), &rocketmq_error::AUTH_PERMISSION_DENIED);
1375            assert_eq!(canonical.to_string(), "auth.permission.denied: Permission was denied");
1376            assert_eq!(canonical.descriptor().projection().remoting().code.as_i32(), 16);
1377        }
1378    }
1379
1380    struct AuthRuntimeBuilder;
1381
1382    impl AuthRuntimeBuilder {
1383        #[allow(
1384            clippy::new_ret_no_self,
1385            reason = "test facade preserves concise existing builder call sites"
1386        )]
1387        fn new(config: AuthConfig) -> super::AuthRuntimeBuilder {
1388            let runtime = rocketmq_runtime::RuntimeContext::from_current("auth-runtime-test");
1389            super::AuthRuntimeBuilder::new(config, runtime.service_context("auth-runtime"))
1390        }
1391    }
1392
1393    fn send_message_command(topic: &str, access_key: &str, signature: &str) -> RemotingCommand {
1394        let mut ext_fields = HashMap::new();
1395        ext_fields.insert(CheetahString::from_static_str("topic"), CheetahString::from(topic));
1396        ext_fields.insert(
1397            CheetahString::from_static_str("AccessKey"),
1398            CheetahString::from(access_key),
1399        );
1400        ext_fields.insert(
1401            CheetahString::from_static_str("Signature"),
1402            CheetahString::from(signature),
1403        );
1404        RemotingCommand::create_remoting_command(RequestCode::SendMessage.to_i32()).set_ext_fields(ext_fields)
1405    }
1406
1407    fn send_message_command_without_credentials(topic: &str) -> RemotingCommand {
1408        let mut ext_fields = HashMap::new();
1409        ext_fields.insert(CheetahString::from_static_str("topic"), CheetahString::from(topic));
1410        RemotingCommand::create_remoting_command(RequestCode::SendMessage.to_i32()).set_ext_fields(ext_fields)
1411    }
1412
1413    fn signed_command(
1414        request_code: RequestCode,
1415        access_key: &str,
1416        secret_key: &str,
1417        fields: &[(&str, &str)],
1418    ) -> RemotingCommand {
1419        let mut ext_fields = HashMap::new();
1420        ext_fields.insert(
1421            CheetahString::from_static_str("AccessKey"),
1422            CheetahString::from(access_key),
1423        );
1424        let mut signed_values = std::collections::BTreeMap::new();
1425        signed_values.insert("AccessKey", access_key);
1426        for (name, value) in fields {
1427            ext_fields.insert(CheetahString::from(*name), CheetahString::from(*value));
1428            signed_values.insert(*name, *value);
1429        }
1430        let content = signed_values.values().copied().collect::<String>();
1431        let signature = acl_signer::cal_signature(content.as_bytes(), secret_key).unwrap();
1432        ext_fields.insert(
1433            CheetahString::from_static_str("Signature"),
1434            CheetahString::from(signature),
1435        );
1436        RemotingCommand::create_remoting_command(request_code.to_i32()).set_ext_fields(ext_fields)
1437    }
1438
1439    #[test]
1440    fn provider_registry_rejects_unsupported_metadata_provider() {
1441        let config = AuthConfig {
1442            authentication_metadata_provider: CheetahString::from_static_str("RocksDBAuthenticationMetadataProvider"),
1443            ..AuthConfig::default()
1444        };
1445
1446        let error = match ProviderRegistry::local(&config) {
1447            Ok(_) => panic!("unsupported provider should fail fast"),
1448            Err(error) => error,
1449        };
1450
1451        assert_eq!(error.operation(), AuthOperation::Initialize);
1452        assert_eq!(error.kind(), AuthFailureKind::InvalidConfiguration);
1453    }
1454
1455    #[test]
1456    fn provider_registry_accepts_file_snapshot_metadata_provider_aliases() {
1457        let config = AuthConfig {
1458            authentication_metadata_provider: CheetahString::from_static_str(
1459                "FileSnapshotAuthenticationMetadataProvider",
1460            ),
1461            authorization_metadata_provider: CheetahString::from_static_str(
1462                "FileSnapshotAuthorizationMetadataProvider",
1463            ),
1464            ..AuthConfig::default()
1465        };
1466
1467        let registry = ProviderRegistry::local(&config).expect("file snapshot aliases should use local providers");
1468
1469        assert_eq!(registry.acl_generation(), 0);
1470    }
1471
1472    #[test]
1473    fn migrated_sre_reader_can_get_but_cannot_mutate_cluster_resources() {
1474        let mut account = PlainAccessConfig::new();
1475        account.set_access_key(CheetahString::from_static_str("sre-reader"));
1476        account.set_cluster_perm(CheetahString::from_static_str("GET"));
1477        account.set_default_topic_perm(CheetahString::from_static_str("GET"));
1478        account.set_default_group_perm(CheetahString::from_static_str("GET"));
1479
1480        let acl = acl_from_plain_account(&account).unwrap().unwrap();
1481        let entries = acl.get_policy(PolicyType::Default).unwrap().entries();
1482
1483        for resource_type in [ResourceType::Cluster, ResourceType::Topic, ResourceType::Group] {
1484            let entry = entries
1485                .iter()
1486                .find(|entry| entry.resource().resource_type() == resource_type)
1487                .unwrap();
1488            assert_eq!(entry.decision(), Decision::Allow);
1489            assert!(entry.is_match_action(&[Action::Get]));
1490            assert!(!entry.is_match_action(&[Action::Update]));
1491            assert!(!entry.is_match_action(&[Action::Delete]));
1492        }
1493    }
1494
1495    #[test]
1496    fn migrated_cluster_permission_rejects_non_read_permissions() {
1497        let mut account = PlainAccessConfig::new();
1498        account.set_access_key(CheetahString::from_static_str("sre-reader"));
1499        account.set_cluster_perm(CheetahString::from_static_str("UPDATE"));
1500
1501        let error = acl_from_plain_account(&account).unwrap_err();
1502
1503        assert_eq!(error.operation(), AuthOperation::Initialize);
1504        assert_eq!(error.kind(), AuthFailureKind::InvalidConfiguration);
1505    }
1506
1507    #[tokio::test]
1508    async fn migrated_sre_reader_accepts_broker_get_and_rejects_topic_mutation_rpc() {
1509        let temp = TempDir::new().unwrap();
1510        let acl_file = temp.path().join("plain_acl.yml");
1511        fs::write(
1512            &acl_file,
1513            r#"
1514accounts:
1515  - accessKey: sre-reader
1516    secretKey: reader-secret
1517    admin: false
1518    defaultTopicPerm: GET
1519    defaultGroupPerm: GET
1520    clusterPerm: GET
1521"#,
1522        )
1523        .unwrap();
1524        let runtime = AuthRuntimeBuilder::new(AuthConfig {
1525            cluster_name: CheetahString::from_static_str("SreDev"),
1526            acl_file: CheetahString::from(acl_file.to_string_lossy().as_ref()),
1527            authentication_enabled: true,
1528            authorization_enabled: true,
1529            ..AuthConfig::default()
1530        })
1531        .build()
1532        .await
1533        .unwrap();
1534
1535        let broker_get = signed_command(RequestCode::GetBrokerRuntimeInfo, "sre-reader", "reader-secret", &[]);
1536        runtime
1537            .check_remoting(&RemotingAuthContext::embedded("test-session"), &broker_get)
1538            .await
1539            .unwrap();
1540
1541        let topic_mutation = signed_command(
1542            RequestCode::UpdateAndCreateTopic,
1543            "sre-reader",
1544            "reader-secret",
1545            &[("topic", "Orders")],
1546        );
1547        let error = runtime
1548            .check_remoting(&RemotingAuthContext::embedded("test-session"), &topic_mutation)
1549            .await
1550            .unwrap_err();
1551        assert_eq!(error.operation(), AuthOperation::Authorize);
1552        assert_eq!(error.kind(), AuthFailureKind::PermissionDenied);
1553
1554        runtime.shutdown().await.unwrap();
1555    }
1556
1557    #[tokio::test]
1558    async fn build_runtime_seeds_initial_super_user() {
1559        let runtime = AuthRuntimeBuilder::new(AuthConfig {
1560            init_authentication_user: CheetahString::from_static_str("admin:secret"),
1561            ..AuthConfig::default()
1562        })
1563        .build()
1564        .await
1565        .unwrap();
1566
1567        let user = runtime
1568            .provider_registry()
1569            .authentication_metadata_provider()
1570            .get_user("admin")
1571            .await
1572            .unwrap();
1573        assert_eq!(user.user_type(), Some(UserType::Super));
1574        assert_eq!(user.user_status(), Some(UserStatus::Enable));
1575    }
1576
1577    #[tokio::test]
1578    async fn runtime_authenticates_and_authorizes_remoting_command() {
1579        let runtime = AuthRuntimeBuilder::new(AuthConfig {
1580            authentication_enabled: true,
1581            authorization_enabled: true,
1582            ..AuthConfig::default()
1583        })
1584        .build()
1585        .await
1586        .unwrap();
1587
1588        let authn_provider = runtime.provider_registry().authentication_metadata_provider();
1589        let mut user = User::of_with_type("alice", "secret", UserType::Normal);
1590        user.set_user_status(UserStatus::Enable);
1591        authn_provider.create_user(user).await.unwrap();
1592
1593        let authz_provider = runtime.provider_registry().authorization_metadata_provider();
1594        let acl = Acl::of(
1595            "alice",
1596            SubjectType::User,
1597            Policy::of(
1598                vec![Resource::of_topic("topic-a")],
1599                vec![Action::Pub],
1600                None,
1601                Decision::Allow,
1602            ),
1603        );
1604        authz_provider.create_acl(acl).await.unwrap();
1605
1606        let signature = acl_signer::cal_signature("alicetopic-a".as_bytes(), "secret").unwrap();
1607        let command = send_message_command("topic-a", "alice", &signature);
1608
1609        runtime
1610            .check_remoting(&RemotingAuthContext::embedded("test-session"), &command)
1611            .await
1612            .unwrap();
1613    }
1614
1615    #[tokio::test]
1616    async fn remoting_runtime_rejects_missing_or_mismatched_ingress_metadata() {
1617        let runtime = AuthRuntimeBuilder::new(AuthConfig::default()).build().await.unwrap();
1618        let command = RemotingCommand::create_remoting_command(RequestCode::SendMessage.to_i32());
1619
1620        runtime
1621            .check_remoting(&RemotingAuthContext::default(), &command)
1622            .await
1623            .expect_err("missing session and source metadata must fail closed");
1624
1625        let context = RemotingAuthContext::network("192.0.2.10", "session-a");
1626        runtime
1627            .check_remoting_with_source_ip(&context, &command, Some("192.0.2.11"), Some("session-a"))
1628            .await
1629            .expect_err("metadata overrides must match the typed ingress context");
1630    }
1631
1632    #[tokio::test]
1633    async fn authoritative_operation_code_cannot_be_replaced_by_a_mutated_command_code() {
1634        let runtime = AuthRuntimeBuilder::new(AuthConfig {
1635            authentication_enabled: true,
1636            authentication_whitelist: CheetahString::from(RequestCode::HeartBeat.to_i32().to_string()),
1637            ..AuthConfig::default()
1638        })
1639        .build()
1640        .await
1641        .unwrap();
1642        let context = RemotingAuthContext::embedded("test-session");
1643        let mutated = RemotingCommand::create_remoting_command(RequestCode::HeartBeat.to_i32());
1644
1645        runtime
1646            .check_remoting(&context, &mutated)
1647            .await
1648            .expect("the test command code is explicitly whitelisted");
1649        runtime
1650            .check_remoting_for_code(&context, &mutated, RequestCode::SendMessage.to_i32())
1651            .await
1652            .expect_err("the immutable SendMessage operation must still require authentication");
1653    }
1654
1655    #[tokio::test]
1656    async fn detailed_authorization_records_each_provider_decision_once() {
1657        let runtime = AuthRuntimeBuilder::new(AuthConfig {
1658            authentication_enabled: true,
1659            authorization_enabled: true,
1660            ..AuthConfig::default()
1661        })
1662        .build()
1663        .await
1664        .expect("test auth runtime should initialize");
1665
1666        let authn_provider = runtime.provider_registry().authentication_metadata_provider();
1667        let mut user = User::of_with_type("alice", "secret", UserType::Normal);
1668        user.set_user_status(UserStatus::Enable);
1669        authn_provider
1670            .create_user(user)
1671            .await
1672            .expect("test user should be stored");
1673
1674        let authz_provider = runtime.provider_registry().authorization_metadata_provider();
1675        authz_provider
1676            .create_acl(Acl::of(
1677                "alice",
1678                SubjectType::User,
1679                Policy::of(
1680                    vec![Resource::of_topic("topic-a")],
1681                    vec![Action::Pub],
1682                    None,
1683                    Decision::Allow,
1684                ),
1685            ))
1686            .await
1687            .expect("test ACL should be stored");
1688
1689        let command = send_message_command(
1690            "topic-a",
1691            "alice",
1692            &acl_signer::cal_signature("alicetopic-a".as_bytes(), "secret")
1693                .expect("test signature should be generated"),
1694        );
1695        assert_eq!(
1696            runtime
1697                .evaluate_remoting_detailed(&RemotingAuthContext::embedded("test-session"), &command)
1698                .await,
1699            Ok(DetailedDecision::Allow)
1700        );
1701
1702        let snapshot = runtime.metrics_snapshot();
1703        assert_eq!(snapshot.authorization_successes, 1);
1704        assert_eq!(snapshot.authorization_failures, 0);
1705        runtime.shutdown().await.expect("test auth runtime should shut down");
1706    }
1707
1708    #[tokio::test]
1709    async fn build_runtime_loads_configured_acl_file() {
1710        let temp = TempDir::new().unwrap();
1711        let acl_file = temp.path().join("plain_acl.yml");
1712        fs::write(
1713            &acl_file,
1714            r#"
1715accounts:
1716  - accessKey: alice
1717    secretKey: secret
1718    admin: false
1719    defaultTopicPerm: DENY
1720    defaultGroupPerm: SUB
1721    topicPerms:
1722      - TopicA=PUB
1723    groupPerms:
1724      - GroupA=SUB
1725"#,
1726        )
1727        .unwrap();
1728
1729        let runtime = AuthRuntimeBuilder::new(AuthConfig {
1730            acl_file: CheetahString::from(acl_file.to_string_lossy().as_ref()),
1731            ..AuthConfig::default()
1732        })
1733        .build()
1734        .await
1735        .unwrap();
1736
1737        let authn_provider = runtime.provider_registry().authentication_metadata_provider();
1738        let user = authn_provider.get_user("alice").await.unwrap();
1739        assert_eq!(user.password().map(|value| value.as_str()), Some("secret"));
1740        assert_eq!(user.user_type(), Some(UserType::Normal));
1741        assert_eq!(user.user_status(), Some(UserStatus::Enable));
1742
1743        let authz_provider = runtime.provider_registry().authorization_metadata_provider();
1744        let acl = authz_provider.get_acl(&User::of("alice")).await.unwrap().unwrap();
1745        assert!(acl.get_policy(PolicyType::Custom).is_some());
1746        assert!(acl.get_policy(PolicyType::Default).is_some());
1747    }
1748
1749    #[tokio::test]
1750    async fn migrate_auth_from_v1_manager_loads_legacy_plain_acl_files() {
1751        let temp = TempDir::new().unwrap();
1752        let auth_config = AuthConfig {
1753            auth_config_path: CheetahString::from(temp.path().join("auth-store").to_string_lossy().as_ref()),
1754            ..AuthConfig::default()
1755        };
1756        let context = rocketmq_runtime::RuntimeContext::try_from_current("auth-v1-migration-test").unwrap();
1757        let metadata_io = MetadataIoConfig::default()
1758            .into_plan()
1759            .expect("default metadata I/O config is valid")
1760            .start(&context.service_context("auth.v1-migration"))
1761            .unwrap();
1762        let registry = ProviderRegistry::local_with_metadata_io(&auth_config, Some(metadata_io)).unwrap();
1763        let acl_file = temp.path().join("conf").join("acl").join("legacy.yml");
1764        fs::create_dir_all(acl_file.parent().unwrap()).unwrap();
1765        fs::write(
1766            &acl_file,
1767            r#"
1768globalWhiteRemoteAddresses:
1769  - 10.1.*.*
1770accounts:
1771  - accessKey: legacy
1772    secretKey: secret
1773    admin: false
1774    topicPerms:
1775      - TopicA=PUB
1776"#,
1777        )
1778        .unwrap();
1779        let manager = PlainPermissionManager {
1780            file_home: temp.path().to_string_lossy().into_owned(),
1781            default_acl_dir: temp.path().join("conf").join("acl").to_string_lossy().into_owned(),
1782            default_acl_file: temp
1783                .path()
1784                .join("conf")
1785                .join("plain_acl.yml")
1786                .to_string_lossy()
1787                .into_owned(),
1788            file_list: vec![acl_file.to_string_lossy().into_owned()],
1789        };
1790
1791        let migrated = migrate_auth_from_v1_manager(&registry, &manager).await.unwrap();
1792
1793        assert_eq!(migrated, 1);
1794        let user = registry
1795            .authentication_metadata_provider()
1796            .get_user("legacy")
1797            .await
1798            .unwrap();
1799        assert_eq!(user.password().map(|value| value.as_str()), Some("secret"));
1800        let acl = registry
1801            .authorization_metadata_provider()
1802            .get_acl(&User::of("legacy"))
1803            .await
1804            .unwrap()
1805            .unwrap();
1806        assert!(acl.get_policy(PolicyType::Custom).is_some());
1807        assert!(registry.is_acl_white_remote_address(None, Some("10.1.2.3")).unwrap());
1808    }
1809
1810    #[tokio::test]
1811    async fn migrate_auth_from_v1_is_disabled_by_default() {
1812        let temp = TempDir::new().unwrap();
1813        let config = AuthConfig {
1814            auth_config_path: CheetahString::from(temp.path().join("auth-store").to_string_lossy().as_ref()),
1815            ..AuthConfig::default()
1816        };
1817        let registry = ProviderRegistry::local(&config).unwrap();
1818
1819        let migrated = migrate_auth_from_v1(&registry, &config).await.unwrap();
1820
1821        assert_eq!(migrated, 0);
1822    }
1823
1824    #[tokio::test]
1825    async fn acl_white_remote_address_short_circuits_remoting_auth_and_authorization() {
1826        let temp = TempDir::new().unwrap();
1827        let acl_file = temp.path().join("plain_acl.yml");
1828        fs::write(
1829            &acl_file,
1830            r#"
1831globalWhiteRemoteAddresses:
1832  - 10.10.*.*
1833accounts:
1834  - accessKey: alice
1835    secretKey: secret
1836    whiteRemoteAddress: 192.168.0.*
1837    defaultTopicPerm: DENY
1838"#,
1839        )
1840        .unwrap();
1841
1842        let runtime = AuthRuntimeBuilder::new(AuthConfig {
1843            acl_file: CheetahString::from(acl_file.to_string_lossy().as_ref()),
1844            authentication_enabled: true,
1845            authorization_enabled: true,
1846            ..AuthConfig::default()
1847        })
1848        .build()
1849        .await
1850        .unwrap();
1851
1852        assert!(runtime.is_acl_white_remote_address(None, Some("10.10.1.2")).unwrap());
1853        assert!(runtime
1854            .is_acl_white_remote_address(Some("alice"), Some("192.168.0.7"))
1855            .unwrap());
1856        assert!(!runtime
1857            .is_acl_white_remote_address(Some("alice"), Some("192.168.1.7"))
1858            .unwrap());
1859
1860        let global_command = send_message_command_without_credentials("TopicA");
1861        runtime
1862            .check_remoting(
1863                &RemotingAuthContext::network("10.10.1.2", "test-session"),
1864                &global_command,
1865            )
1866            .await
1867            .unwrap();
1868
1869        let account_command = send_message_command("TopicA", "alice", "");
1870        runtime
1871            .check_remoting(
1872                &RemotingAuthContext::network("192.168.0.7", "test-session"),
1873                &account_command,
1874            )
1875            .await
1876            .unwrap();
1877
1878        runtime
1879            .check_remoting(
1880                &RemotingAuthContext::network("192.168.1.7", "test-session"),
1881                &account_command,
1882            )
1883            .await
1884            .expect_err("non-whitelisted source should still require a valid signature");
1885    }
1886
1887    #[tokio::test]
1888    async fn runtime_global_white_remote_address_update_preserves_account_whitelist_and_advances_generation() {
1889        let temp = TempDir::new().unwrap();
1890        let acl_file = temp.path().join("plain_acl.yml");
1891        fs::write(
1892            &acl_file,
1893            r#"
1894globalWhiteRemoteAddresses:
1895  - 10.10.*.*
1896accounts:
1897  - accessKey: alice
1898    secretKey: secret
1899    whiteRemoteAddress: 192.168.0.*
1900"#,
1901        )
1902        .unwrap();
1903
1904        let runtime = AuthRuntimeBuilder::new(AuthConfig {
1905            acl_file: CheetahString::from(acl_file.to_string_lossy().as_ref()),
1906            ..AuthConfig::default()
1907        })
1908        .build()
1909        .await
1910        .unwrap();
1911        let generation = runtime.acl_generation();
1912
1913        runtime
1914            .update_global_white_remote_addresses(vec!["172.16.*.*"])
1915            .unwrap();
1916
1917        assert!(runtime.acl_generation() > generation);
1918        assert!(!runtime.is_acl_white_remote_address(None, Some("10.10.1.2")).unwrap());
1919        assert!(runtime.is_acl_white_remote_address(None, Some("172.16.1.2")).unwrap());
1920        assert!(runtime
1921            .is_acl_white_remote_address(Some("alice"), Some("192.168.0.7"))
1922            .unwrap());
1923    }
1924
1925    #[tokio::test]
1926    async fn provider_registry_local_loads_persisted_authentication_users() {
1927        let temp = TempDir::new().unwrap();
1928        let config = AuthConfig {
1929            auth_config_path: CheetahString::from(temp.path().join("auth.json").to_string_lossy().as_ref()),
1930            ..AuthConfig::default()
1931        };
1932
1933        let mut provider = LocalAuthenticationMetadataProvider::new();
1934        provider.initialize(config.clone(), None).await.unwrap();
1935        provider
1936            .create_user(User::of_with_password("persisted", "secret"))
1937            .await
1938            .unwrap();
1939
1940        let registry = ProviderRegistry::local(&config).unwrap();
1941        let restored = registry
1942            .authentication_metadata_provider()
1943            .get_user("persisted")
1944            .await
1945            .unwrap();
1946
1947        assert_eq!(restored.password().map(|value| value.as_str()), Some("secret"));
1948    }
1949
1950    #[tokio::test]
1951    async fn auth_runtime_restores_persisted_metadata_after_restart() {
1952        let temp = TempDir::new().unwrap();
1953        let config = AuthConfig {
1954            auth_config_path: CheetahString::from(temp.path().join("auth.json").to_string_lossy().as_ref()),
1955            authentication_enabled: true,
1956            authorization_enabled: true,
1957            ..AuthConfig::default()
1958        };
1959
1960        let runtime = AuthRuntimeBuilder::new(config.clone()).build().await.unwrap();
1961        let authn_provider = runtime.provider_registry().authentication_metadata_provider();
1962        let mut user = User::of_with_type("alice", "secret", UserType::Normal);
1963        user.set_user_status(UserStatus::Enable);
1964        authn_provider.create_user(user).await.unwrap();
1965
1966        let authz_provider = runtime.provider_registry().authorization_metadata_provider();
1967        authz_provider
1968            .create_acl(Acl::of(
1969                "alice",
1970                SubjectType::User,
1971                Policy::of(
1972                    vec![Resource::of_topic("topic-a")],
1973                    vec![Action::Pub],
1974                    None,
1975                    Decision::Allow,
1976                ),
1977            ))
1978            .await
1979            .unwrap();
1980        runtime.shutdown().await.unwrap();
1981
1982        let restarted = AuthRuntimeBuilder::new(config).build().await.unwrap();
1983        let signature = acl_signer::cal_signature("alicetopic-a".as_bytes(), "secret").unwrap();
1984        let command = send_message_command("topic-a", "alice", &signature);
1985
1986        restarted
1987            .check_remoting(&RemotingAuthContext::embedded("test-session"), &command)
1988            .await
1989            .unwrap();
1990        restarted.shutdown().await.unwrap();
1991    }
1992
1993    #[tokio::test]
1994    async fn auth_runtime_rejects_corrupted_persisted_users_snapshot() {
1995        let temp = TempDir::new().unwrap();
1996        let config = AuthConfig {
1997            auth_config_path: CheetahString::from(temp.path().join("auth.json").to_string_lossy().as_ref()),
1998            authentication_enabled: true,
1999            ..AuthConfig::default()
2000        };
2001        let snapshot = temp.path().join("auth").join("users.json");
2002        fs::create_dir_all(snapshot.parent().unwrap()).unwrap();
2003        fs::write(&snapshot, b"{not valid json").unwrap();
2004
2005        let error = match AuthRuntimeBuilder::new(config).build().await {
2006            Ok(_) => panic!("runtime must reject corrupted users snapshot"),
2007            Err(error) => error,
2008        };
2009
2010        assert_eq!(error.operation(), AuthOperation::DecodeMetadata);
2011        assert_eq!(error.kind(), AuthFailureKind::InvalidData);
2012        assert!(!error.to_string().contains("users.json"));
2013    }
2014
2015    #[tokio::test]
2016    async fn auth_runtime_rejects_corrupted_persisted_acls_snapshot() {
2017        let temp = TempDir::new().unwrap();
2018        let config = AuthConfig {
2019            auth_config_path: CheetahString::from(temp.path().join("auth.json").to_string_lossy().as_ref()),
2020            authorization_enabled: true,
2021            ..AuthConfig::default()
2022        };
2023        let snapshot = temp.path().join("auth").join("acls.json");
2024        fs::create_dir_all(snapshot.parent().unwrap()).unwrap();
2025        fs::write(&snapshot, b"{not valid json").unwrap();
2026
2027        let error = match AuthRuntimeBuilder::new(config).build().await {
2028            Ok(_) => panic!("runtime must reject corrupted ACL snapshot"),
2029            Err(error) => error,
2030        };
2031
2032        assert_eq!(error.operation(), AuthOperation::DecodeMetadata);
2033        assert_eq!(error.kind(), AuthFailureKind::InvalidData);
2034        assert!(!error.to_string().contains("acls.json"));
2035        let canonical = rocketmq_error::Error::from(error);
2036        assert_eq!(canonical.descriptor(), &rocketmq_error::CORE_SERIALIZATION_FAILED);
2037        let auth_error = std::error::Error::source(&canonical)
2038            .and_then(|source| source.downcast_ref::<crate::AuthServiceError>())
2039            .expect("canonical error must retain the auth facade source");
2040        assert_eq!(auth_error.operation(), crate::AuthOperation::DecodeMetadata);
2041        assert_eq!(auth_error.kind(), crate::AuthFailureKind::InvalidData);
2042        let startup = std::error::Error::source(auth_error)
2043            .and_then(|source| source.downcast_ref::<AuthStartupFailure>())
2044            .expect("startup error must retain its rollback owner and primary failure");
2045        assert!(startup.cleanup_error().is_none());
2046        assert!(std::error::Error::source(startup.primary())
2047            .and_then(|source| source.downcast_ref::<serde_json::Error>())
2048            .is_some());
2049    }
2050
2051    #[tokio::test]
2052    async fn acl_file_watcher_reloads_changed_user_secret() {
2053        let temp = TempDir::new().unwrap();
2054        let acl_file = temp.path().join("plain_acl.yml");
2055        fs::write(
2056            &acl_file,
2057            r#"
2058accounts:
2059  - accessKey: alice
2060    secretKey: first
2061"#,
2062        )
2063        .unwrap();
2064
2065        let runtime = AuthRuntimeBuilder::new(AuthConfig {
2066            acl_file: CheetahString::from(acl_file.to_string_lossy().as_ref()),
2067            acl_file_watch_enabled: true,
2068            acl_file_watch_interval_millis: 25,
2069            ..AuthConfig::default()
2070        })
2071        .build()
2072        .await
2073        .unwrap();
2074
2075        let authn_provider = runtime.provider_registry().authentication_metadata_provider();
2076        let user = authn_provider.get_user("alice").await.unwrap();
2077        assert_eq!(user.password().map(|value| value.as_str()), Some("first"));
2078
2079        fs::write(
2080            &acl_file,
2081            r#"
2082accounts:
2083  - accessKey: alice
2084    secretKey: second
2085"#,
2086        )
2087        .unwrap();
2088
2089        let deadline = Instant::now() + Duration::from_secs(2);
2090        loop {
2091            let user = authn_provider.get_user("alice").await.unwrap();
2092            if user.password().map(|value| value.as_str()) == Some("second") {
2093                break;
2094            }
2095            assert!(
2096                Instant::now() < deadline,
2097                "ACL file watcher did not reload the updated user secret"
2098            );
2099            sleep(Duration::from_millis(25)).await;
2100        }
2101
2102        runtime.shutdown().await.unwrap();
2103    }
2104
2105    #[tokio::test]
2106    async fn acl_file_watcher_skips_unchanged_file_without_advancing_generation() {
2107        let temp = TempDir::new().unwrap();
2108        let acl_file = temp.path().join("plain_acl.yml");
2109        fs::write(
2110            &acl_file,
2111            r#"
2112accounts:
2113  - accessKey: alice
2114    secretKey: first
2115"#,
2116        )
2117        .unwrap();
2118
2119        let runtime = AuthRuntimeBuilder::new(AuthConfig {
2120            acl_file: CheetahString::from(acl_file.to_string_lossy().as_ref()),
2121            acl_file_watch_enabled: true,
2122            acl_file_watch_interval_millis: 20,
2123            ..AuthConfig::default()
2124        })
2125        .build()
2126        .await
2127        .unwrap();
2128        let generation = runtime.acl_generation();
2129
2130        sleep(Duration::from_millis(120)).await;
2131
2132        assert_eq!(
2133            runtime.acl_generation(),
2134            generation,
2135            "watcher must not advance generation when ACL file content is unchanged"
2136        );
2137
2138        runtime.shutdown().await.unwrap();
2139    }
2140
2141    #[tokio::test]
2142    async fn manual_acl_reload_failure_preserves_previous_snapshot_and_generation() {
2143        let temp = TempDir::new().unwrap();
2144        let acl_file = temp.path().join("plain_acl.yml");
2145        fs::write(
2146            &acl_file,
2147            r#"
2148globalWhiteRemoteAddresses:
2149  - 10.10.*.*
2150accounts:
2151  - accessKey: alice
2152    secretKey: first
2153"#,
2154        )
2155        .unwrap();
2156
2157        let runtime = AuthRuntimeBuilder::new(AuthConfig {
2158            acl_file: CheetahString::from(acl_file.to_string_lossy().as_ref()),
2159            ..AuthConfig::default()
2160        })
2161        .build()
2162        .await
2163        .unwrap();
2164        let authn_provider = runtime.provider_registry().authentication_metadata_provider();
2165        let generation = runtime.acl_generation();
2166        assert!(runtime.is_acl_white_remote_address(None, Some("10.10.1.2")).unwrap());
2167        let user = authn_provider.get_user("alice").await.unwrap();
2168        assert_eq!(user.password().map(|value| value.as_str()), Some("first"));
2169
2170        fs::write(
2171            &acl_file,
2172            r#"
2173globalWhiteRemoteAddresses:
2174  - 172.16.*.*
2175accounts:
2176  - accessKey: alice
2177"#,
2178        )
2179        .unwrap();
2180
2181        let error = runtime.reload_acl_file().await.unwrap_err();
2182        assert_eq!(error.operation(), AuthOperation::Initialize);
2183        assert_eq!(error.kind(), AuthFailureKind::InvalidConfiguration);
2184        assert_eq!(runtime.acl_generation(), generation);
2185
2186        let user = authn_provider.get_user("alice").await.unwrap();
2187        assert_eq!(user.password().map(|value| value.as_str()), Some("first"));
2188        assert!(runtime.is_acl_white_remote_address(None, Some("10.10.1.2")).unwrap());
2189        assert!(!runtime.is_acl_white_remote_address(None, Some("172.16.1.2")).unwrap());
2190    }
2191
2192    #[tokio::test]
2193    async fn acl_file_watcher_failure_preserves_snapshot_and_recovers_after_fix() {
2194        let temp = TempDir::new().unwrap();
2195        let acl_file = temp.path().join("plain_acl.yml");
2196        fs::write(
2197            &acl_file,
2198            r#"
2199globalWhiteRemoteAddresses:
2200  - 10.10.*.*
2201accounts:
2202  - accessKey: alice
2203    secretKey: first
2204"#,
2205        )
2206        .unwrap();
2207
2208        let runtime = AuthRuntimeBuilder::new(AuthConfig {
2209            acl_file: CheetahString::from(acl_file.to_string_lossy().as_ref()),
2210            acl_file_watch_enabled: true,
2211            acl_file_watch_interval_millis: 25,
2212            ..AuthConfig::default()
2213        })
2214        .build()
2215        .await
2216        .unwrap();
2217        let authn_provider = runtime.provider_registry().authentication_metadata_provider();
2218        let generation = runtime.acl_generation();
2219
2220        fs::write(
2221            &acl_file,
2222            r#"
2223globalWhiteRemoteAddresses:
2224  - 172.16.*.*
2225accounts:
2226  - accessKey: alice
2227"#,
2228        )
2229        .unwrap();
2230
2231        sleep(Duration::from_millis(150)).await;
2232
2233        assert_eq!(runtime.acl_generation(), generation);
2234        let user = authn_provider.get_user("alice").await.unwrap();
2235        assert_eq!(user.password().map(|value| value.as_str()), Some("first"));
2236        assert!(runtime.is_acl_white_remote_address(None, Some("10.10.1.2")).unwrap());
2237        assert!(!runtime.is_acl_white_remote_address(None, Some("172.16.1.2")).unwrap());
2238
2239        fs::write(
2240            &acl_file,
2241            r#"
2242globalWhiteRemoteAddresses:
2243  - 172.16.*.*
2244accounts:
2245  - accessKey: alice
2246    secretKey: second
2247"#,
2248        )
2249        .unwrap();
2250
2251        let deadline = Instant::now() + Duration::from_secs(2);
2252        loop {
2253            let user = authn_provider.get_user("alice").await.unwrap();
2254            if user.password().map(|value| value.as_str()) == Some("second")
2255                && runtime.is_acl_white_remote_address(None, Some("172.16.1.2")).unwrap()
2256            {
2257                break;
2258            }
2259            assert!(
2260                Instant::now() < deadline,
2261                "ACL file watcher did not recover after a failed reload"
2262            );
2263            sleep(Duration::from_millis(25)).await;
2264        }
2265
2266        assert!(runtime.acl_generation() > generation);
2267        runtime.shutdown().await.unwrap();
2268    }
2269
2270    #[tokio::test]
2271    async fn manual_acl_reload_updates_watcher_fingerprint() {
2272        let temp = TempDir::new().unwrap();
2273        let acl_file = temp.path().join("plain_acl.yml");
2274        fs::write(
2275            &acl_file,
2276            r#"
2277accounts:
2278  - accessKey: alice
2279    secretKey: first
2280"#,
2281        )
2282        .unwrap();
2283
2284        let runtime = AuthRuntimeBuilder::new(AuthConfig {
2285            acl_file: CheetahString::from(acl_file.to_string_lossy().as_ref()),
2286            acl_file_watch_enabled: true,
2287            acl_file_watch_interval_millis: 20,
2288            ..AuthConfig::default()
2289        })
2290        .build()
2291        .await
2292        .unwrap();
2293
2294        fs::write(
2295            &acl_file,
2296            r#"
2297accounts:
2298  - accessKey: alice
2299    secretKey: second
2300"#,
2301        )
2302        .unwrap();
2303
2304        runtime.reload_acl_file().await.unwrap();
2305        let generation = runtime.acl_generation();
2306
2307        sleep(Duration::from_millis(120)).await;
2308
2309        assert_eq!(
2310            runtime.acl_generation(),
2311            generation,
2312            "watcher must not re-apply a file that was already manually reloaded"
2313        );
2314
2315        runtime.shutdown().await.unwrap();
2316    }
2317
2318    #[tokio::test]
2319    async fn acl_file_watcher_reloads_white_remote_address_snapshot() {
2320        let temp = TempDir::new().unwrap();
2321        let acl_file = temp.path().join("plain_acl.yml");
2322        fs::write(
2323            &acl_file,
2324            r#"
2325accounts:
2326  - accessKey: alice
2327    secretKey: first
2328"#,
2329        )
2330        .unwrap();
2331
2332        let runtime = AuthRuntimeBuilder::new(AuthConfig {
2333            acl_file: CheetahString::from(acl_file.to_string_lossy().as_ref()),
2334            acl_file_watch_enabled: true,
2335            acl_file_watch_interval_millis: 25,
2336            ..AuthConfig::default()
2337        })
2338        .build()
2339        .await
2340        .unwrap();
2341
2342        assert!(!runtime.is_acl_white_remote_address(None, Some("10.10.1.2")).unwrap());
2343
2344        fs::write(
2345            &acl_file,
2346            r#"
2347globalWhiteRemoteAddresses:
2348  - 10.10.*.*
2349accounts:
2350  - accessKey: alice
2351    secretKey: first
2352"#,
2353        )
2354        .unwrap();
2355
2356        let deadline = Instant::now() + Duration::from_secs(2);
2357        loop {
2358            if runtime.is_acl_white_remote_address(None, Some("10.10.1.2")).unwrap() {
2359                break;
2360            }
2361            assert!(
2362                Instant::now() < deadline,
2363                "ACL file watcher did not reload the updated white remote address"
2364            );
2365            sleep(Duration::from_millis(25)).await;
2366        }
2367
2368        runtime.shutdown().await.unwrap();
2369    }
2370
2371    #[tokio::test]
2372    async fn acl_file_watcher_shutdown_is_idempotent_and_stops_future_reloads() {
2373        let temp = TempDir::new().unwrap();
2374        let acl_file = temp.path().join("plain_acl.yml");
2375        fs::write(
2376            &acl_file,
2377            r#"
2378accounts:
2379  - accessKey: alice
2380    secretKey: first
2381"#,
2382        )
2383        .unwrap();
2384
2385        let runtime = AuthRuntimeBuilder::new(AuthConfig {
2386            acl_file: CheetahString::from(acl_file.to_string_lossy().as_ref()),
2387            acl_file_watch_enabled: true,
2388            acl_file_watch_interval_millis: 20,
2389            ..AuthConfig::default()
2390        })
2391        .build()
2392        .await
2393        .unwrap();
2394        runtime.shutdown().await.unwrap();
2395        runtime.shutdown().await.unwrap();
2396
2397        let generation = runtime.acl_generation();
2398        let reload_attempts = runtime.metrics_snapshot().acl_reload_attempts;
2399        fs::write(
2400            &acl_file,
2401            r#"
2402accounts:
2403  - accessKey: alice
2404    secretKey: second
2405"#,
2406        )
2407        .unwrap();
2408        sleep(Duration::from_millis(120)).await;
2409
2410        assert_eq!(runtime.acl_generation(), generation);
2411        assert_eq!(
2412            runtime.metrics_snapshot().acl_reload_attempts,
2413            reload_attempts,
2414            "shutdown watcher must not poll or reload after shutdown",
2415        );
2416    }
2417
2418    #[tokio::test]
2419    async fn auth_metrics_track_reload_whitelist_signature_and_auth_outcomes() {
2420        let temp = TempDir::new().unwrap();
2421        let acl_file = temp.path().join("plain_acl.yml");
2422        fs::write(
2423            &acl_file,
2424            r#"
2425globalWhiteRemoteAddresses:
2426  - 10.10.*.*
2427accounts:
2428  - accessKey: alice
2429    secretKey: secret
2430    topicPerms:
2431      - TopicA=PUB
2432"#,
2433        )
2434        .unwrap();
2435
2436        let runtime = AuthRuntimeBuilder::new(AuthConfig {
2437            acl_file: CheetahString::from(acl_file.to_string_lossy().as_ref()),
2438            authentication_enabled: true,
2439            authorization_enabled: true,
2440            ..AuthConfig::default()
2441        })
2442        .build()
2443        .await
2444        .unwrap();
2445
2446        assert!(runtime.is_acl_white_remote_address(None, Some("10.10.1.2")).unwrap());
2447
2448        let signature = acl_signer::cal_signature("aliceTopicA".as_bytes(), "secret").unwrap();
2449        let valid_command = send_message_command("TopicA", "alice", &signature);
2450        runtime
2451            .check_remoting(&RemotingAuthContext::network("127.0.0.1", "channel-a"), &valid_command)
2452            .await
2453            .unwrap();
2454
2455        let invalid_command = send_message_command("TopicA", "alice", "bad-signature");
2456        runtime
2457            .check_remoting(
2458                &RemotingAuthContext::network("127.0.0.1", "channel-b"),
2459                &invalid_command,
2460            )
2461            .await
2462            .expect_err("invalid signature should fail authentication");
2463
2464        let snapshot = runtime.metrics_snapshot();
2465        assert_eq!(snapshot.acl_reload_attempts, 1);
2466        assert_eq!(snapshot.acl_reload_successes, 1);
2467        assert!(snapshot.cache_invalidations >= 1);
2468        assert!(snapshot.whitelist_hits >= 1);
2469        assert!(snapshot.whitelist_misses >= 2);
2470        assert_eq!(snapshot.signature_successes, 1);
2471        assert_eq!(snapshot.signature_failures, 1);
2472        assert_eq!(snapshot.authentication_successes, 1);
2473        assert_eq!(snapshot.authentication_failures, 1);
2474        assert_eq!(snapshot.authorization_successes, 1);
2475
2476        runtime.shutdown().await.unwrap();
2477    }
2478}