Skip to main content

rocketmq_auth/
config.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fmt;
16
17use cheetah_string::CheetahString;
18use serde::Deserialize;
19use serde::Serialize;
20
21use crate::authentication::acl_signer::SignatureAlgorithm;
22use crate::maintenance::MaintenancePolicyReference;
23use crate::AuthFailureKind;
24use crate::AuthOperation;
25use crate::AuthServiceError;
26use crate::AuthServiceResult;
27
28#[derive(Clone, Serialize, Deserialize)]
29#[serde(default, rename_all = "camelCase")]
30pub struct AuthConfig {
31    pub config_name: CheetahString,
32    pub cluster_name: CheetahString,
33    pub auth_config_path: CheetahString,
34    pub acl_file: CheetahString,
35    pub acl_file_watch_enabled: bool,
36    pub acl_file_watch_interval_millis: u64,
37
38    pub authentication_enabled: bool,
39    pub authentication_provider: CheetahString,
40    pub authentication_metadata_provider: CheetahString,
41    pub authentication_strategy: CheetahString,
42    pub authentication_whitelist: CheetahString,
43    pub init_authentication_user: CheetahString,
44    pub inner_client_authentication_credentials: CheetahString,
45    pub signature_algorithm: SignatureAlgorithm,
46    /// Request replay-protection window in milliseconds.
47    ///
48    /// `0` keeps Java-compatible behavior and disables timestamp expiry checks.
49    pub request_timestamp_expired_millis: u64,
50
51    pub authorization_enabled: bool,
52    pub authorization_provider: CheetahString,
53    pub authorization_metadata_provider: CheetahString,
54    pub authorization_strategy: CheetahString,
55    pub authorization_whitelist: CheetahString,
56
57    /// Whether privileged production maintenance APIs are exposed.
58    pub maintenance_enabled: bool,
59    /// Pinned maintenance policy path, resolved by the service composition root.
60    pub maintenance_policy_path: CheetahString,
61    /// Expected maintenance policy version.
62    pub maintenance_policy_version: u64,
63    /// Expected SHA-256 of the exact maintenance policy bytes.
64    pub maintenance_policy_sha256: CheetahString,
65
66    pub migrate_auth_from_v1_enabled: bool,
67
68    pub user_cache_max_num: u32,
69    pub user_cache_expired_second: u32,
70    pub user_cache_refresh_second: u32,
71
72    pub acl_cache_max_num: u32,
73    pub acl_cache_expired_second: u32,
74    /// Maximum age of an ACL cache value returned without re-reading the provider's canonical
75    /// storage snapshot. `0` makes every ACL lookup read through storage.
76    pub acl_cache_refresh_second: u32,
77
78    pub stateful_authentication_cache_max_num: u32,
79    pub stateful_authentication_cache_expired_second: u32,
80    pub stateful_authorization_cache_max_num: u32,
81    pub stateful_authorization_cache_expired_second: u32,
82    pub stateful_authorization_cache_negative_enable: bool,
83}
84
85impl fmt::Debug for AuthConfig {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        f.debug_struct("AuthConfig")
88            .field("config_name", &self.config_name)
89            .field("cluster_name", &self.cluster_name)
90            .field("auth_config_path", &self.auth_config_path)
91            .field("acl_file", &self.acl_file)
92            .field("acl_file_watch_enabled", &self.acl_file_watch_enabled)
93            .field("acl_file_watch_interval_millis", &self.acl_file_watch_interval_millis)
94            .field("authentication_enabled", &self.authentication_enabled)
95            .field("authentication_provider", &self.authentication_provider)
96            .field(
97                "authentication_metadata_provider",
98                &self.authentication_metadata_provider,
99            )
100            .field("authentication_strategy", &self.authentication_strategy)
101            .field("authentication_whitelist", &self.authentication_whitelist)
102            .field(
103                "init_authentication_user",
104                &redacted_if_present(&self.init_authentication_user),
105            )
106            .field(
107                "inner_client_authentication_credentials",
108                &redacted_if_present(&self.inner_client_authentication_credentials),
109            )
110            .field("signature_algorithm", &self.signature_algorithm)
111            .field(
112                "request_timestamp_expired_millis",
113                &self.request_timestamp_expired_millis,
114            )
115            .field("authorization_enabled", &self.authorization_enabled)
116            .field("authorization_provider", &self.authorization_provider)
117            .field("authorization_metadata_provider", &self.authorization_metadata_provider)
118            .field("authorization_strategy", &self.authorization_strategy)
119            .field("authorization_whitelist", &self.authorization_whitelist)
120            .field("maintenance_enabled", &self.maintenance_enabled)
121            .field("maintenance_policy_path", &self.maintenance_policy_path)
122            .field("maintenance_policy_version", &self.maintenance_policy_version)
123            .field("maintenance_policy_sha256", &self.maintenance_policy_sha256)
124            .field("migrate_auth_from_v1_enabled", &self.migrate_auth_from_v1_enabled)
125            .field("user_cache_max_num", &self.user_cache_max_num)
126            .field("user_cache_expired_second", &self.user_cache_expired_second)
127            .field("user_cache_refresh_second", &self.user_cache_refresh_second)
128            .field("acl_cache_max_num", &self.acl_cache_max_num)
129            .field("acl_cache_expired_second", &self.acl_cache_expired_second)
130            .field("acl_cache_refresh_second", &self.acl_cache_refresh_second)
131            .field(
132                "stateful_authentication_cache_max_num",
133                &self.stateful_authentication_cache_max_num,
134            )
135            .field(
136                "stateful_authentication_cache_expired_second",
137                &self.stateful_authentication_cache_expired_second,
138            )
139            .field(
140                "stateful_authorization_cache_max_num",
141                &self.stateful_authorization_cache_max_num,
142            )
143            .field(
144                "stateful_authorization_cache_expired_second",
145                &self.stateful_authorization_cache_expired_second,
146            )
147            .field(
148                "stateful_authorization_cache_negative_enable",
149                &self.stateful_authorization_cache_negative_enable,
150            )
151            .finish()
152    }
153}
154
155fn redacted_if_present(value: &CheetahString) -> Option<&'static str> {
156    if value.is_empty() {
157        None
158    } else {
159        Some("<redacted>")
160    }
161}
162
163impl Default for AuthConfig {
164    fn default() -> Self {
165        Self {
166            config_name: CheetahString::new(),
167            cluster_name: CheetahString::new(),
168            auth_config_path: CheetahString::new(),
169            acl_file: CheetahString::new(),
170            acl_file_watch_enabled: false,
171            acl_file_watch_interval_millis: 5_000,
172
173            authentication_enabled: false,
174            authentication_provider: CheetahString::new(),
175            authentication_metadata_provider: CheetahString::new(),
176            authentication_strategy: CheetahString::new(),
177            authentication_whitelist: CheetahString::new(),
178            init_authentication_user: CheetahString::new(),
179            inner_client_authentication_credentials: CheetahString::new(),
180            signature_algorithm: SignatureAlgorithm::default(),
181            request_timestamp_expired_millis: 0,
182
183            authorization_enabled: false,
184            authorization_provider: CheetahString::new(),
185            authorization_metadata_provider: CheetahString::new(),
186            authorization_strategy: CheetahString::new(),
187            authorization_whitelist: CheetahString::new(),
188
189            maintenance_enabled: false,
190            maintenance_policy_path: CheetahString::new(),
191            maintenance_policy_version: 0,
192            maintenance_policy_sha256: CheetahString::new(),
193
194            migrate_auth_from_v1_enabled: false,
195
196            user_cache_max_num: 1000,
197            user_cache_expired_second: 600,
198            user_cache_refresh_second: 60,
199
200            acl_cache_max_num: 1000,
201            acl_cache_expired_second: 600,
202            acl_cache_refresh_second: 60,
203
204            stateful_authentication_cache_max_num: 10000,
205            stateful_authentication_cache_expired_second: 60,
206            stateful_authorization_cache_max_num: 10000,
207            stateful_authorization_cache_expired_second: 60,
208            stateful_authorization_cache_negative_enable: false,
209        }
210    }
211}
212
213impl AuthConfig {
214    /// Builds the immutable maintenance policy reference after validating the
215    /// service-level fail-closed prerequisites.
216    ///
217    /// # Errors
218    ///
219    /// Returns a typed reference error when maintenance is enabled without
220    /// authentication, authorization, or a complete path/version/SHA pin.
221    pub fn maintenance_policy_reference(&self) -> AuthServiceResult<Option<MaintenancePolicyReference>> {
222        if !self.maintenance_enabled {
223            return Ok(None);
224        }
225        if !self.authentication_enabled || !self.authorization_enabled {
226            return Err(AuthServiceError::new(
227                AuthOperation::MaintainService,
228                AuthFailureKind::InvalidConfiguration,
229            ));
230        }
231        if self.maintenance_policy_path.trim().is_empty()
232            || self.maintenance_policy_version == 0
233            || self.maintenance_policy_sha256.trim().is_empty()
234        {
235            return Err(AuthServiceError::new(
236                AuthOperation::MaintainService,
237                AuthFailureKind::InvalidConfiguration,
238            ));
239        }
240        Ok(Some(MaintenancePolicyReference {
241            path: self.maintenance_policy_path.as_str().into(),
242            version: self.maintenance_policy_version,
243            sha256: self.maintenance_policy_sha256.to_string(),
244        }))
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_auth_config_default_values() {
254        let config = AuthConfig::default();
255
256        // String fields
257        assert!(config.config_name.is_empty());
258        assert!(config.cluster_name.is_empty());
259        assert!(config.auth_config_path.is_empty());
260        assert!(config.acl_file.is_empty());
261
262        assert!(config.authentication_provider.is_empty());
263        assert!(config.authentication_metadata_provider.is_empty());
264        assert!(config.authentication_strategy.is_empty());
265        assert!(config.authentication_whitelist.is_empty());
266        assert!(config.init_authentication_user.is_empty());
267        assert!(config.inner_client_authentication_credentials.is_empty());
268        assert_eq!(config.signature_algorithm, SignatureAlgorithm::HmacSha1);
269        assert_eq!(config.request_timestamp_expired_millis, 0);
270
271        assert!(config.authorization_provider.is_empty());
272        assert!(config.authorization_metadata_provider.is_empty());
273        assert!(config.authorization_strategy.is_empty());
274        assert!(config.authorization_whitelist.is_empty());
275
276        // Boolean fields
277        assert!(!config.authentication_enabled);
278        assert!(!config.authorization_enabled);
279        assert!(!config.maintenance_enabled);
280        assert!(config.maintenance_policy_path.is_empty());
281        assert_eq!(config.maintenance_policy_version, 0);
282        assert!(config.maintenance_policy_sha256.is_empty());
283        assert!(!config.acl_file_watch_enabled);
284        assert!(!config.migrate_auth_from_v1_enabled);
285        assert_eq!(config.acl_file_watch_interval_millis, 5_000);
286
287        // Cache defaults
288        assert_eq!(config.user_cache_max_num, 1000);
289        assert_eq!(config.user_cache_expired_second, 600);
290        assert_eq!(config.user_cache_refresh_second, 60);
291
292        assert_eq!(config.acl_cache_max_num, 1000);
293        assert_eq!(config.acl_cache_expired_second, 600);
294        assert_eq!(config.acl_cache_refresh_second, 60);
295
296        assert_eq!(config.stateful_authentication_cache_max_num, 10000);
297        assert_eq!(config.stateful_authentication_cache_expired_second, 60);
298        assert_eq!(config.stateful_authorization_cache_max_num, 10000);
299        assert_eq!(config.stateful_authorization_cache_expired_second, 60);
300        assert!(!config.stateful_authorization_cache_negative_enable);
301    }
302
303    #[test]
304    fn auth_config_deserializes_acl_file_fields_from_camel_case() {
305        let config: AuthConfig = serde_yaml::from_str(
306            r#"
307aclFile: conf/plain_acl.yml
308aclFileWatchEnabled: true
309aclFileWatchIntervalMillis: 250
310signatureAlgorithm: HmacSHA256
311requestTimestampExpiredMillis: 300000
312statefulAuthorizationCacheNegativeEnable: true
313"#,
314        )
315        .unwrap();
316
317        assert_eq!(config.acl_file.as_str(), "conf/plain_acl.yml");
318        assert!(config.acl_file_watch_enabled);
319        assert_eq!(config.acl_file_watch_interval_millis, 250);
320        assert_eq!(config.signature_algorithm, SignatureAlgorithm::HmacSha256);
321        assert_eq!(config.request_timestamp_expired_millis, 300_000);
322        assert!(config.stateful_authorization_cache_negative_enable);
323    }
324
325    #[test]
326    fn auth_config_debug_redacts_embedded_credentials() {
327        let config = AuthConfig {
328            init_authentication_user: CheetahString::from("admin:init-secret"),
329            inner_client_authentication_credentials: CheetahString::from(
330                r#"{"accessKey":"inner","secretKey":"inner-secret"}"#,
331            ),
332            ..AuthConfig::default()
333        };
334
335        let debug = format!("{config:?}");
336
337        assert!(debug.contains("<redacted>"));
338        assert!(!debug.contains("init-secret"));
339        assert!(!debug.contains("inner-secret"));
340    }
341
342    #[test]
343    fn maintenance_reference_requires_auth_and_complete_pin() {
344        let disabled = AuthConfig::default();
345        assert!(disabled
346            .maintenance_policy_reference()
347            .expect("disabled maintenance has no reference")
348            .is_none());
349
350        let incomplete = AuthConfig {
351            maintenance_enabled: true,
352            ..AuthConfig::default()
353        };
354        assert!(incomplete.maintenance_policy_reference().is_err());
355
356        let complete = AuthConfig {
357            authentication_enabled: true,
358            authorization_enabled: true,
359            maintenance_enabled: true,
360            maintenance_policy_path: "maintenance-policy.json".into(),
361            maintenance_policy_version: 7,
362            maintenance_policy_sha256: "a".repeat(64).into(),
363            ..AuthConfig::default()
364        };
365        assert_eq!(
366            complete
367                .maintenance_policy_reference()
368                .expect("complete reference")
369                .expect("enabled maintenance reference")
370                .version,
371            7
372        );
373    }
374}