dmsc/auth/mod.rs
1//! Copyright © 2025-2026 Wenze Wei. All Rights Reserved.
2//!
3//! This file is part of DMSC.
4//! The DMSC project belongs to the Dunimd Team.
5//!
6//! Licensed under the Apache License, Version 2.0 (the "License");
7//! You may not use this file except in compliance with the License.
8//! You may obtain a copy of the License at
9//!
10//! http://www.apache.org/licenses/LICENSE-2.0
11//!
12//! Unless required by applicable law or agreed to in writing, software
13//! distributed under the License is distributed on an "AS IS" BASIS,
14//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//! See the License for the specific language governing permissions and
16//! limitations under the License.
17
18//! # Authentication Module
19//!
20//! This module provides comprehensive authentication and authorization functionality for DMSC,
21//! offering multiple authentication methods and a robust permission system.
22//!
23//! ## Key Components
24//!
25//! - **DMSCAuthModule**: Main auth module implementing service module traits
26//! - **DMSCAuthConfig**: Configuration for authentication behavior
27//! - **DMSCJWTManager**: JWT token management for stateless authentication
28//! - **DMSCSessionManager**: Session management for stateful authentication
29//! - **DMSCPermissionManager**: Permission and role management
30//! - **DMSCOAuthManager**: OAuth provider integration
31//! - **DMSCJWTClaims**: JWT token claims structure
32//! - **DMSCJWTValidationOptions**: JWT validation options
33//! - **DMSCOAuthProvider**: OAuth provider interface
34//! - **DMSCOAuthToken**: OAuth token structure
35//! - **DMSCOAuthUserInfo**: OAuth user information
36//! - **DMSCPermission**: Permission structure
37//! - **DMSCRole**: Role structure with permissions
38//! - **DMSCSession**: Session structure
39//!
40//! ## Design Principles
41//!
42//! 1. **Multiple Authentication Methods**: Supports JWT, sessions, OAuth, and API keys
43//! 2. **Configurable**: Highly configurable authentication behavior
44//! 3. **Async Support**: Full async/await compatibility for session and OAuth operations
45//! 4. **Role-Based Access Control**: Comprehensive permission system with roles
46//! 5. **Stateless and Stateful Options**: Supports both stateless (JWT) and stateful (session) authentication
47//! 6. **Service Module Integration**: Implements service module traits for seamless integration
48//! 7. **Thread-safe**: Uses Arc and RwLock for safe concurrent access
49//! 8. **Non-critical**: Auth failures should not break the entire application
50//! 9. **Extensible**: Easy to add new authentication methods and OAuth providers
51//! 10. **Secure by Default**: Sensible default configurations for security
52//!
53//! ## Usage
54//!
55//! ```rust,ignore
56//! use dmsc::prelude::*;
57//! use dmsc::auth::{DMSCAuthConfig, DMSCJWTManager, DMSCJWTClaims};
58//! use serde_json::json;
59//!
60//! async fn example() -> DMSCResult<()> {
61//! // Create auth configuration
62//! let auth_config = DMSCAuthConfig {
63//! enabled: true,
64//! jwt_secret: "secure-secret-key".to_string(),
65//! jwt_expiry_secs: 3600,
66//! session_timeout_secs: 86400,
67//! oauth_providers: vec![],
68//! enable_api_keys: true,
69//! enable_session_auth: true,
70//! };
71//!
72//! // Create auth module
73//! let auth_module = DMSCAuthModule::new(auth_config);
74//!
75//! // Get JWT manager
76//! let jwt_manager = auth_module.jwt_manager();
77//!
78//! // Create JWT claims
79//! let claims = DMSCJWTClaims {
80//! sub: "user-123".to_string(),
81//! email: "user@example.com".to_string(),
82//! roles: vec!["user".to_string()],
83//! permissions: vec!["read:data".to_string()],
84//! extra: json!({ "custom": "value" }),
85//! };
86//!
87//! // Generate JWT token
88//! let token = jwt_manager.generate_token(claims)?;
89//! println!("Generated JWT token: {}", token);
90//!
91//! // Validate JWT token
92//! let validated_claims = jwt_manager.validate_token(&token)?;
93//! println!("Validated claims: {:?}", validated_claims);
94//!
95//! // Get session manager
96//! let session_manager = auth_module.session_manager();
97//!
98//! // Create a session
99//! let session = session_manager.write().await.create_session("user-123").await?;
100//! println!("Created session: {}", session.id);
101//!
102//! Ok(())
103//! }
104//! ```
105
106mod jwt;
107mod oauth;
108mod permissions;
109mod session;
110mod security;
111mod revocation;
112
113pub use jwt::{DMSCJWTManager, DMSCJWTClaims, DMSCJWTValidationOptions};
114pub use oauth::{DMSCOAuthManager, DMSCOAuthToken, DMSCOAuthUserInfo, DMSCOAuthProvider};
115pub use permissions::{DMSCPermissionManager, DMSCPermission, DMSCRole};
116pub use session::{DMSCSessionManager, DMSCSession};
117pub use security::DMSCSecurityManager;
118pub use revocation::{DMSCJWTRevocationList, DMSCRevokedTokenInfo};
119
120use crate::core::{DMSCResult, DMSCError, DMSCServiceContext};
121use rand::RngCore;
122use serde::Deserialize;
123use std::env;
124use std::sync::Arc;
125use tokio::sync::RwLock;
126#[cfg(feature = "pyo3")]
127use tokio::runtime::Handle;
128
129const DEFAULT_JWT_SECRET_ENV: &str = "DMSC_JWT_SECRET";
130const FALLBACK_SECRET_LENGTH: usize = 64;
131
132fn load_jwt_secret_from_env() -> String {
133 env::var(DEFAULT_JWT_SECRET_ENV).unwrap_or_else(|_| {
134 let mut secret = vec![0u8; FALLBACK_SECRET_LENGTH];
135 rand::thread_rng().fill_bytes(&mut secret);
136 hex::encode(secret)
137 })
138}
139
140fn load_oauth_env_var(provider_name: &str, suffix: &str) -> Result<String, DMSCError> {
141 let env_var = format!("DMSC_OAUTH_{}_{}", provider_name.to_uppercase(), suffix);
142 env::var(&env_var).map_err(|_| {
143 DMSCError::Config(format!(
144 "OAuth {} is not set for provider '{}'. Please set the environment variable {}",
145 suffix.to_lowercase(),
146 provider_name,
147 env_var
148 ))
149 })
150}
151
152fn get_oauth_url(provider_name: &str, endpoint: &str) -> String {
153 match load_oauth_env_var(provider_name, endpoint) {
154 Ok(url) if !url.is_empty() => url,
155 _ => format!("https://{}.com/oauth/{}", provider_name, endpoint)
156 }
157}
158
159#[cfg(feature = "pyo3")]
160use pyo3::PyResult;
161
162/// Configuration for the authentication module.
163///
164/// This struct defines the configuration options for authentication behavior, including
165/// JWT settings, session settings, OAuth providers, and enabled authentication methods.
166///
167/// ## Security
168///
169/// The JWT secret is loaded from the `DMSC_JWT_SECRET` environment variable. If not set,
170/// a cryptographically secure random secret is generated automatically.
171///
172/// **Important**: For production environments, always set the `DMSC_JWT_SECRET` environment
173/// variable to a strong, unique value. Do not rely on auto-generated secrets in production.
174#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
175#[derive(Debug, Clone)]
176#[derive(Deserialize)]
177pub struct DMSCAuthConfig {
178 /// Whether authentication is enabled
179 pub enabled: bool,
180 /// Secret key for JWT token generation and validation
181 pub jwt_secret: String,
182 /// JWT token expiry time in seconds
183 pub jwt_expiry_secs: u64,
184 /// Session timeout in seconds
185 pub session_timeout_secs: u64,
186 /// List of OAuth providers to enable
187 pub oauth_providers: Vec<String>,
188 /// Whether API key authentication is enabled
189 pub enable_api_keys: bool,
190 /// Whether session authentication is enabled
191 pub enable_session_auth: bool,
192 /// OAuth token cache backend type (Memory or Redis)
193 #[cfg(feature = "cache")]
194 pub oauth_cache_backend_type: crate::cache::DMSCCacheBackendType,
195 /// Redis URL for OAuth token cache (used when backend is Redis)
196 #[cfg(feature = "cache")]
197 pub oauth_cache_redis_url: String,
198}
199
200impl Default for DMSCAuthConfig {
201 fn default() -> Self {
202 Self {
203 enabled: true,
204 jwt_secret: load_jwt_secret_from_env(),
205 jwt_expiry_secs: 3600,
206 session_timeout_secs: 86400,
207 oauth_providers: vec![],
208 enable_api_keys: true,
209 enable_session_auth: true,
210 #[cfg(feature = "cache")]
211 oauth_cache_backend_type: crate::cache::DMSCCacheBackendType::Memory,
212 #[cfg(feature = "cache")]
213 oauth_cache_redis_url: "redis://127.0.0.1:6379".to_string(),
214 }
215 }
216}
217
218/// Main authentication module for DMSC.
219///
220/// This module provides comprehensive authentication and authorization functionality,
221/// including JWT management, session management, permission management, and OAuth integration.
222#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
223pub struct DMSCAuthModule {
224 /// Authentication configuration
225 config: DMSCAuthConfig,
226 /// JWT manager for stateless authentication
227 jwt_manager: Arc<DMSCJWTManager>,
228 /// Session manager for stateful authentication, protected by a RwLock for thread-safe access
229 session_manager: Arc<RwLock<DMSCSessionManager>>,
230 /// Permission manager for role-based access control, protected by a RwLock for thread-safe access
231 permission_manager: Arc<RwLock<DMSCPermissionManager>>,
232 /// OAuth manager for OAuth provider integration, protected by a RwLock for thread-safe access
233 oauth_manager: Arc<RwLock<DMSCOAuthManager>>,
234 /// JWT token revocation list for token invalidation
235 revocation_list: Arc<DMSCJWTRevocationList>,
236}
237
238impl DMSCAuthModule {
239 /// Creates a new authentication module with the given configuration.
240 ///
241 /// **Performance Note**: This method creates a permission manager using the synchronous
242 /// `new()` method which uses `blocking_write` during initialization. For async contexts,
243 /// consider using `new_async()` to avoid blocking the runtime.
244 ///
245 /// # Parameters
246 ///
247 /// - `config`: The authentication configuration to use
248 ///
249 /// # Returns
250 ///
251 /// A `DMSCResult` containing the new `DMSCAuthModule` instance
252 ///
253 /// # Errors
254 ///
255 /// Returns an error if Redis cache creation fails when Redis backend is configured
256 pub async fn new(config: DMSCAuthConfig) -> crate::core::error::DMSCResult<Self> {
257 let jwt_manager = Arc::new(DMSCJWTManager::create(config.jwt_secret.clone(), config.jwt_expiry_secs));
258 let session_manager = Arc::new(RwLock::new(DMSCSessionManager::new(config.session_timeout_secs)));
259 let permission_manager = Arc::new(RwLock::new(DMSCPermissionManager::new()));
260
261 #[cfg(feature = "cache")]
262 let cache: Arc<dyn crate::cache::DMSCCache> = match config.oauth_cache_backend_type {
263 crate::cache::DMSCCacheBackendType::Memory => {
264 Arc::new(crate::cache::DMSCMemoryCache::new())
265 }
266 crate::cache::DMSCCacheBackendType::Redis => {
267 let cache = crate::cache::DMSCRedisCache::new(&config.oauth_cache_redis_url).await
268 .map_err(|e| crate::core::error::DMSCError::RedisError(format!("Failed to create Redis cache for OAuth: {}", e)))?;
269 Arc::new(cache)
270 }
271 _ => Arc::new(crate::cache::DMSCMemoryCache::new()),
272 };
273
274 #[cfg(not(feature = "cache"))]
275 let cache = Arc::new(crate::cache::DMSCMemoryCache::new());
276
277 let oauth_manager = Arc::new(RwLock::new(DMSCOAuthManager::new(cache)));
278 let revocation_list = Arc::new(DMSCJWTRevocationList::new());
279
280 Ok(Self {
281 config,
282 jwt_manager,
283 session_manager,
284 permission_manager,
285 oauth_manager,
286 revocation_list,
287 })
288 }
289
290 /// Creates a new authentication module with the given configuration (synchronous version).
291 ///
292 /// This is a synchronous wrapper for use in the builder pattern.
293 ///
294 /// # Parameters
295 ///
296 /// - `config`: The authentication configuration to use
297 ///
298 /// # Returns
299 ///
300 /// A new `DMSCAuthModule` instance
301 pub fn with_config(config: DMSCAuthConfig) -> Self {
302 let jwt_manager = Arc::new(DMSCJWTManager::create(config.jwt_secret.clone(), config.jwt_expiry_secs));
303 let session_manager = Arc::new(RwLock::new(DMSCSessionManager::new(config.session_timeout_secs)));
304 let permission_manager = Arc::new(RwLock::new(DMSCPermissionManager::new()));
305 let cache = Arc::new(crate::cache::DMSCMemoryCache::new());
306 let oauth_manager = Arc::new(RwLock::new(DMSCOAuthManager::new(cache)));
307 let revocation_list = Arc::new(DMSCJWTRevocationList::new());
308
309 Self {
310 config,
311 jwt_manager,
312 session_manager,
313 permission_manager,
314 oauth_manager,
315 revocation_list,
316 }
317 }
318
319 /// Creates a new authentication module with the given configuration asynchronously.
320 ///
321 /// This method is preferred for async contexts as it avoids blocking the runtime
322 /// during permission manager initialization by using the async `new_async()` method.
323 ///
324 /// # Parameters
325 ///
326 /// - `config`: The authentication configuration to use
327 ///
328 /// # Returns
329 ///
330 /// A `DMSCResult` containing the new `DMSCAuthModule` instance
331 ///
332 /// # Errors
333 ///
334 /// Returns an error if Redis cache creation fails when Redis backend is configured
335 pub async fn new_async(config: DMSCAuthConfig) -> crate::core::error::DMSCResult<Self> {
336 let jwt_manager = Arc::new(DMSCJWTManager::create(config.jwt_secret.clone(), config.jwt_expiry_secs));
337 let session_manager = Arc::new(RwLock::new(DMSCSessionManager::new(config.session_timeout_secs)));
338 let permission_manager = Arc::new(RwLock::new(DMSCPermissionManager::new_async().await));
339
340 #[cfg(feature = "cache")]
341 let cache: Arc<dyn crate::cache::DMSCCache> = match config.oauth_cache_backend_type {
342 crate::cache::DMSCCacheBackendType::Memory => {
343 Arc::new(crate::cache::DMSCMemoryCache::new())
344 }
345 crate::cache::DMSCCacheBackendType::Redis => {
346 let cache = crate::cache::DMSCRedisCache::new(&config.oauth_cache_redis_url).await
347 .map_err(|e| crate::core::error::DMSCError::RedisError(format!("Failed to create Redis cache: {}", e)))?;
348 Arc::new(cache)
349 }
350 _ => Arc::new(crate::cache::DMSCMemoryCache::new()),
351 };
352
353 #[cfg(not(feature = "cache"))]
354 let cache = Arc::new(crate::cache::DMSCMemoryCache::new());
355
356 let oauth_manager = Arc::new(RwLock::new(DMSCOAuthManager::new(cache)));
357 let revocation_list = Arc::new(DMSCJWTRevocationList::new());
358
359 Ok(Self {
360 config,
361 jwt_manager,
362 session_manager,
363 permission_manager,
364 oauth_manager,
365 revocation_list,
366 })
367 }
368
369 /// Returns a reference to the JWT revocation list.
370 ///
371 /// # Returns
372 ///
373 /// An Arc<DMSCJWTRevocationList> providing thread-safe access to the token revocation list
374 pub fn revocation_list(&self) -> Arc<DMSCJWTRevocationList> {
375 self.revocation_list.clone()
376 }
377
378 /// Returns a reference to the JWT manager.
379 ///
380 /// # Returns
381 ///
382 /// An Arc<DMSCJWTManager> providing thread-safe access to the JWT manager
383 pub fn jwt_manager(&self) -> Arc<DMSCJWTManager> {
384 self.jwt_manager.clone()
385 }
386
387 /// Returns a reference to the session manager.
388 ///
389 /// # Returns
390 ///
391 /// An Arc<RwLock<DMSCSessionManager>> providing thread-safe access to the session manager
392 pub fn session_manager(&self) -> Arc<RwLock<DMSCSessionManager>> {
393 self.session_manager.clone()
394 }
395
396 /// Returns a reference to the permission manager.
397 ///
398 /// # Returns
399 ///
400 /// An Arc<RwLock<DMSCPermissionManager>> providing thread-safe access to the permission manager
401 pub fn permission_manager(&self) -> Arc<RwLock<DMSCPermissionManager>> {
402 self.permission_manager.clone()
403 }
404
405 /// Returns a reference to the OAuth manager.
406 ///
407 /// # Returns
408 ///
409 /// An Arc<RwLock<DMSCOAuthManager>> providing thread-safe access to the OAuth manager
410 pub fn oauth_manager(&self) -> Arc<RwLock<DMSCOAuthManager>> {
411 self.oauth_manager.clone()
412 }
413}
414
415#[cfg(feature = "pyo3")]
416/// Python bindings for the DMSC Authentication Module.
417///
418/// This module provides Python interface to DMSC authentication functionality,
419/// enabling Python applications to leverage DMSC's authentication capabilities.
420///
421/// ## Supported Operations
422///
423/// - JWT token generation and validation
424/// - Session management for stateful authentication
425/// - Permission and role management for RBAC
426/// - OAuth provider integration
427///
428/// ## Python Usage Example
429///
430/// ```python
431/// from dmsc import DMSCAuthConfig, DMSCJWTManager
432///
433/// # Create auth configuration
434/// config = DMSCAuthConfig(
435/// enabled=True,
436/// jwt_secret="secure-secret-key",
437/// jwt_expiry_secs=3600,
438/// session_timeout_secs=86400,
439/// oauth_providers=["google", "github"],
440/// enable_api_keys=True,
441/// enable_session_auth=True,
442/// )
443///
444/// # Create auth module
445/// auth_module = DMSCAuthModule(config)
446///
447/// # Get JWT manager and generate token
448/// jwt_manager = auth_module.jwt_manager()
449/// token = jwt_manager.generate_token("user123", ["user"], ["read:data"])
450/// ```
451#[pyo3::prelude::pymethods]
452impl DMSCAuthModule {
453 #[new]
454 fn py_new(config: DMSCAuthConfig) -> PyResult<Self> {
455 let rt = Handle::current();
456 rt.block_on(async {
457 Self::new(config).await
458 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
459 })
460 }
461
462 #[getter]
463 fn get_config(&self) -> DMSCAuthConfig {
464 self.config.clone()
465 }
466
467 #[getter]
468 fn get_jwt_expiry_secs(&self) -> u64 {
469 self.jwt_manager.get_token_expiry()
470 }
471
472 #[getter]
473 fn get_session_timeout_secs(&self) -> u64 {
474 self.config.session_timeout_secs
475 }
476
477 #[getter]
478 fn is_enabled(&self) -> bool {
479 self.config.enabled
480 }
481
482 #[getter]
483 fn is_api_keys_enabled(&self) -> bool {
484 self.config.enable_api_keys
485 }
486
487 #[getter]
488 fn is_session_auth_enabled(&self) -> bool {
489 self.config.enable_session_auth
490 }
491
492 #[getter]
493 fn get_oauth_providers(&self) -> Vec<String> {
494 self.config.oauth_providers.clone()
495 }
496
497 fn validate_jwt_token(&self, token: &str) -> bool {
498 self.jwt_manager.validate_token(token).is_ok()
499 }
500
501 fn generate_test_token(&self, subject: &str, roles: Vec<String>, permissions: Vec<String>) -> PyResult<String> {
502 self.jwt_manager.generate_token(subject, roles, permissions)
503 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
504 }
505}
506
507impl crate::core::ServiceModule for DMSCAuthModule {
508 fn name(&self) -> &str {
509 "DMSC.Auth"
510 }
511
512 fn is_critical(&self) -> bool {
513 false
514 }
515
516 fn priority(&self) -> i32 {
517 20
518 }
519
520 fn dependencies(&self) -> Vec<&str> {
521 vec![]
522 }
523
524 fn init(&mut self, _ctx: &mut crate::core::DMSCServiceContext) -> crate::core::DMSCResult<()> {
525 Ok(())
526 }
527
528 fn start(&mut self, _ctx: &mut crate::core::DMSCServiceContext) -> crate::core::DMSCResult<()> {
529 Ok(())
530 }
531
532 fn shutdown(&mut self, _ctx: &mut crate::core::DMSCServiceContext) -> crate::core::DMSCResult<()> {
533 Ok(())
534 }
535}
536
537#[async_trait::async_trait]
538impl crate::core::DMSCModule for DMSCAuthModule {
539 /// Returns the name of the authentication module.
540 ///
541 /// # Returns
542 ///
543 /// The module name as a string
544 fn name(&self) -> &str {
545 "DMSC.Auth"
546 }
547
548 /// Indicates whether the authentication module is critical.
549 ///
550 /// The authentication module is non-critical, meaning that if it fails to initialize or operate,
551 /// it should not break the entire application. This allows the core functionality to continue
552 /// even if authentication features are unavailable.
553 ///
554 /// # Returns
555 ///
556 /// `false` since authentication is non-critical
557 fn is_critical(&self) -> bool {
558 false // Auth failures should not break the application
559 }
560
561 /// Initializes the authentication module asynchronously.
562 ///
563 /// This method performs the following steps:
564 /// 1. Loads configuration from the service context
565 /// 2. Updates the module configuration if provided
566 /// 3. Reinitializes the JWT manager with the new configuration
567 /// 4. Initializes OAuth providers if configured
568 ///
569 /// # Parameters
570 ///
571 /// - `ctx`: The service context containing configuration
572 ///
573 /// # Returns
574 ///
575 /// A `DMSCResult<()>` indicating success or failure
576 async fn init(&mut self, ctx: &mut DMSCServiceContext) -> DMSCResult<()> {
577 log::info!("Initializing DMSC Auth Module");
578
579 // Load configuration
580 let binding = ctx.config();
581 let cfg = binding.config();
582
583 // Update configuration if provided
584 if let Some(auth_config) = cfg.get("auth") {
585 self.config = serde_yaml::from_str(auth_config)
586 .unwrap_or_else(|_| DMSCAuthConfig::default());
587 }
588
589 // Initialize JWT manager with new config
590 self.jwt_manager = Arc::new(DMSCJWTManager::create(self.config.jwt_secret.clone(), self.config.jwt_expiry_secs));
591
592 // Initialize OAuth providers if configured
593 if !self.config.oauth_providers.is_empty() {
594 for provider_name in &self.config.oauth_providers {
595 let client_id = load_oauth_env_var(provider_name, "CLIENT_ID")?;
596 let client_secret = load_oauth_env_var(provider_name, "CLIENT_SECRET")?;
597
598 let provider_config = crate::auth::oauth::DMSCOAuthProvider {
599 id: provider_name.clone(),
600 name: provider_name.clone(),
601 client_id,
602 client_secret,
603 auth_url: get_oauth_url(provider_name, "authorize"),
604 token_url: get_oauth_url(provider_name, "token"),
605 user_info_url: get_oauth_url(provider_name, "userinfo"),
606 scopes: vec!["openid".to_string(), "profile".to_string(), "email".to_string()],
607 enabled: true,
608 redirect_uri: None,
609 };
610
611 let oauth_mgr = self.oauth_manager.write().await;
612 oauth_mgr.register_provider(provider_config).await?;
613 log::info!("OAuth provider registered: {provider_name}");
614 }
615 }
616
617 log::info!("DMSC Auth Module initialized successfully");
618 Ok(())
619 }
620
621 /// Performs asynchronous cleanup after the application has shut down.
622 ///
623 /// This method cleans up all sessions managed by the session manager.
624 ///
625 /// # Parameters
626 ///
627 /// - `_ctx`: The service context (not used in this implementation)
628 ///
629 /// # Returns
630 ///
631 /// A `DMSCResult<()>` indicating success or failure
632 async fn after_shutdown(&mut self, _ctx: &mut DMSCServiceContext) -> DMSCResult<()> {
633 log::info!("Cleaning up DMSC Auth Module");
634
635 // Cleanup sessions
636 let session_mgr = self.session_manager.write().await;
637 session_mgr.cleanup_all().await?;
638
639 log::info!("DMSC Auth Module cleanup completed");
640 Ok(())
641 }
642}