dbnexus 0.5.1

An enterprise-grade database abstraction layer for Rust with built-in permission control and connection pooling
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
// Copyright (c) 2026 Kirky.X
// SPDX-License-Identifier: MIT
//! Authentication module implementation details.
//!
//! Contains impl blocks extracted from [`super`].

use super::*;

use std::collections::HashMap;
use std::sync::Arc;

use tokio::sync::RwLock;

impl AuthenticationManager {
    /// 创建新的认证管理器
    ///
    /// # 参数
    ///
    /// * `jwt_secret` - JWT 签名密钥(建议从环境变量读取)
    pub fn new(jwt_secret: &[u8]) -> Self {
        Self {
            password_hasher: PasswordHasher::new(),
            jwt_manager: JwtManager::new(jwt_secret),
            users: Arc::new(RwLock::new(HashMap::new())),
            max_users: super::DEFAULT_MAX_USERS,
        }
    }

    /// 使用自定义配置创建认证管理器
    pub fn with_config(jwt_secret: &[u8], access_expiration_secs: u64, refresh_expiration_secs: u64) -> Self {
        Self {
            password_hasher: PasswordHasher::new(),
            jwt_manager: JwtManager::with_expiration(jwt_secret, access_expiration_secs, refresh_expiration_secs),
            users: Arc::new(RwLock::new(HashMap::new())),
            max_users: super::DEFAULT_MAX_USERS,
        }
    }

    /// 使用自定义用户上限创建认证管理器
    pub fn with_max_users(jwt_secret: &[u8], max_users: usize) -> Self {
        Self {
            password_hasher: PasswordHasher::new(),
            jwt_manager: JwtManager::new(jwt_secret),
            users: Arc::new(RwLock::new(HashMap::new())),
            max_users,
        }
    }

    /// 添加或更新用户(直接插入已哈希的 User)
    ///
    /// vuln-0002 修复:验证 `password_hash` 为非空且符合 bcrypt 格式。
    ///
    /// 适用于测试/迁移场景。生产用户注册应使用 [`register_user`](Self::register_user),
    /// 后者会验证密码强度并自动哈希。
    ///
    /// # 错误
    ///
    /// - `password_hash` 为空时返回 `AuthError::PasswordHash`
    /// - `password_hash` 不符合 bcrypt 格式时返回 `AuthError::PasswordHash`
    pub async fn add_user(&self, user: User) -> AuthResult<()> {
        validate_bcrypt_hash(&user.password_hash)?;
        let mut users = self.users.write().await;
        // H-7: 检查用户存储上限(已存在的用户更新不计入)
        if !users.contains_key(&user.username) && users.len() >= self.max_users {
            return Err(AuthError::UserLimitReached(format!(
                "Cannot add user '{}': user storage limit of {} reached",
                user.username, self.max_users
            )));
        }
        users.insert(user.username.clone(), user);
        Ok(())
    }

    /// 添加或更新用户(跳过密码哈希验证)
    ///
    /// **HD-3 文档明确**:此方法可见性为 `pub(crate)`,**不对外暴露**。
    /// 仅限 crate 内部迁移/测试场景使用(如批量导入历史用户、测试 fixture 构造)。
    /// 外部代码必须使用 [`add_user`](Self::add_user)(验证 bcrypt 格式)或
    /// [`register_user`](Self::register_user)(验证密码强度 + 哈希 + 存储)。
    ///
    /// `pub(crate)` 可见性是 HD-3 修复的硬性约束:确保外部调用方无法绕过
    /// `add_user` 的 bcrypt 格式验证(vuln-0002),从源头杜绝无效哈希进入存储。
    /// 若未来需要对外暴露,必须先增加等价的安全验证,不得直接放宽可见性。
    ///
    /// **安全警告**:此方法不验证 `password_hash` 格式,仅限内部迁移/测试使用。
    ///
    /// # Safety(语义层面)
    ///
    /// 调用方必须确保 `user.password_hash` 是有效的 bcrypt 哈希,
    /// 否则后续 `authenticate()` 会因 `bcrypt::verify` 失败而拒绝该用户登录。
    #[doc(hidden)]
    pub(crate) async fn add_user_unchecked(&self, user: User) -> AuthResult<()> {
        let mut users = self.users.write().await;
        // H-7: 检查用户存储上限(已存在的用户更新不计入)
        if !users.contains_key(&user.username) && users.len() >= self.max_users {
            return Err(AuthError::UserLimitReached(format!(
                "Cannot add user '{}': user storage limit of {} reached",
                user.username, self.max_users
            )));
        }
        users.insert(user.username.clone(), user);
        Ok(())
    }

    /// 注册新用户(验证密码强度 + 哈希 + 存储)
    ///
    /// 与 [`add_user`](Self::add_user) 的区别:接收明文密码,内部执行
    /// `validate_strength → hash → insert` 完整流程。适用于用户注册场景。
    ///
    /// # 参数
    ///
    /// * `username` - 用户名(同时作为内部用户 ID)
    /// * `password` - 明文密码(需通过强度检查:≥8 字符 + 含字母 + 含数字)
    /// * `role` - 用户角色
    ///
    /// # 错误
    ///
    /// 密码强度不足时返回 `AuthError::PasswordHash`
    pub async fn register_user(&self, username: &str, password: &str, role: &str) -> AuthResult<()> {
        // 1. 验证密码强度
        self.password_hasher.validate_strength(password)?;

        // 2. 哈希密码
        let password_hash = self.password_hasher.hash(password)?;

        // 3. 构造 User 并存储(使用 add_user_unchecked 因为密码已由 validate_strength 验证,
        //    且 password_hash 来自 self.password_hasher.hash() 一定是有效 bcrypt 格式)
        let user = User {
            id: username.to_string(),
            username: username.to_string(),
            password_hash,
            role: role.to_string(),
            email: None,
            created_at: None,
        };

        self.add_user_unchecked(user).await
    }

    /// 用户认证
    ///
    /// 验证用户凭据并生成 JWT Token
    pub async fn authenticate(&self, credentials: AuthCredentials) -> AuthResult<String> {
        // 1. 验证用户名
        let users = self.users.read().await;
        let user = users.get(&credentials.username).ok_or(AuthError::InvalidCredentials)?;

        // 2. 验证密码
        self.password_hasher
            .verify(&credentials.password, &user.password_hash)?;

        // 3. 生成 JWT token
        let token = self
            .jwt_manager
            .generate_token(&user.id, &user.username, &user.role, TokenType::Access)?;

        Ok(token)
    }

    /// 验证 JWT Token
    pub fn verify_token(&self, token: &str) -> AuthResult<JwtClaims> {
        self.jwt_manager.verify_token(token)
    }

    /// 刷新访问令牌
    pub fn refresh_token(&self, refresh_token: &str) -> AuthResult<String> {
        self.jwt_manager.refresh_access_token(refresh_token)
    }

    /// 获取用户信息
    pub async fn get_user(&self, username: &str) -> AuthResult<User> {
        let users = self.users.read().await;
        users
            .get(username)
            .cloned()
            .ok_or_else(|| AuthError::UserNotFound(username.to_string()))
    }

    /// 删除用户
    pub async fn remove_user(&self, username: &str) -> AuthResult<()> {
        let mut users = self.users.write().await;
        users
            .remove(username)
            .map(|_| ())
            .ok_or_else(|| AuthError::UserNotFound(username.to_string()))
    }
}

/// 验证 bcrypt 哈希格式(vuln-0002 修复)
///
/// bcrypt 哈希格式:`$2<a|b|y>$<cost>$<22-char-salt><31-char-hash>`
/// 总长度固定为 60 字符。
///
/// # 参数
///
/// * `hash` - 待验证的密码哈希字符串
///
/// # 错误
///
/// - 哈希为空时返回 `AuthError::PasswordHash`
/// - 哈希不以 `$2a$`/`$2b$`/`$2y$` 开头时返回 `AuthError::PasswordHash`
/// - 哈希长度不为 60 时返回 `AuthError::PasswordHash`
/// - cost 字段非数字时返回 `AuthError::PasswordHash`
fn validate_bcrypt_hash(hash: &str) -> AuthResult<()> {
    if hash.is_empty() {
        return Err(AuthError::PasswordHash("password_hash must not be empty".to_string()));
    }

    // bcrypt 哈希固定长度为 60 字节
    if hash.len() != 60 {
        return Err(AuthError::PasswordHash(format!(
            "password_hash must be a valid bcrypt hash (60 bytes, got {})",
            hash.len()
        )));
    }

    // 必须是纯 ASCII(bcrypt 哈希始终为 ASCII,防止多字节 UTF-8 输入导致切片 panic)
    if !hash.is_ascii() {
        return Err(AuthError::PasswordHash("password_hash must be ASCII".to_string()));
    }

    // 必须以 $2a$、$2b$ 或 $2y$ 开头
    let prefix = &hash[..4];
    if !matches!(prefix, "$2a$" | "$2b$" | "$2y$") {
        return Err(AuthError::PasswordHash(
            "password_hash must start with $2a$, $2b$, or $2y$".to_string(),
        ));
    }

    // cost 字段(第 4-5 位)必须为两位数字
    let cost_str = &hash[4..6];
    if !cost_str.chars().all(|c| c.is_ascii_digit()) {
        return Err(AuthError::PasswordHash(
            "password_hash cost factor must be numeric".to_string(),
        ));
    }

    // 第 6 位必须是 $
    if !hash[6..7].starts_with('$') {
        return Err(AuthError::PasswordHash(
            "password_hash format invalid: missing $ after cost".to_string(),
        ));
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_validate_bcrypt_hash_valid() {
        // 真实 bcrypt 哈希
        let hasher = PasswordHasher::new();
        let hash = hasher.hash("TestPass@123").unwrap();
        assert!(validate_bcrypt_hash(&hash).is_ok());
    }

    #[test]
    fn test_validate_bcrypt_hash_empty() {
        assert!(validate_bcrypt_hash("").is_err());
    }

    #[test]
    fn test_validate_bcrypt_hash_plaintext() {
        assert!(validate_bcrypt_hash("plaintext").is_err());
        assert!(validate_bcrypt_hash("password123").is_err());
    }

    #[test]
    fn test_validate_bcrypt_hash_wrong_prefix() {
        // 错误的前缀
        let fake_hash = "$3a$12$01234567890123456789012345678901234567890123456789";
        assert!(validate_bcrypt_hash(fake_hash).is_err());
    }

    #[test]
    fn test_validate_bcrypt_hash_wrong_length() {
        // 正确前缀但长度不对
        let short_hash = "$2b$12$short";
        assert!(validate_bcrypt_hash(short_hash).is_err());
    }

    #[test]
    fn test_validate_bcrypt_hash_non_numeric_cost() {
        // cost 字段非数字
        let fake_hash = "$2b$ab$0123456789012345678901234567890123456789012345678";
        assert!(validate_bcrypt_hash(fake_hash).is_err());
    }

    /// vuln-0002 回归测试:add_user 必须拒绝空 password_hash
    #[tokio::test]
    async fn test_vuln_0002_add_user_rejects_empty_hash() {
        let mgr = AuthenticationManager::new(b"test-secret-key-for-testing-32bx");
        let user = User {
            id: "u1".to_string(),
            username: "test".to_string(),
            password_hash: "".to_string(),
            role: "user".to_string(),
            email: None,
            created_at: None,
        };
        let result = mgr.add_user(user).await;
        assert!(
            matches!(result, Err(AuthError::PasswordHash(_))),
            "add_user must reject empty password_hash"
        );
    }

    /// vuln-0002 回归测试:add_user 必须拒绝非 bcrypt 格式的 password_hash
    #[tokio::test]
    async fn test_vuln_0002_add_user_rejects_plaintext_hash() {
        let mgr = AuthenticationManager::new(b"test-secret-key-for-testing-32bx");
        let user = User {
            id: "u1".to_string(),
            username: "test".to_string(),
            password_hash: "plaintext_password".to_string(),
            role: "user".to_string(),
            email: None,
            created_at: None,
        };
        let result = mgr.add_user(user).await;
        assert!(
            matches!(result, Err(AuthError::PasswordHash(_))),
            "add_user must reject non-bcrypt password_hash"
        );
    }

    /// vuln-0002 回归测试:add_user 接受有效 bcrypt 哈希
    #[tokio::test]
    async fn test_vuln_0002_add_user_accepts_valid_bcrypt() {
        let mgr = AuthenticationManager::new(b"test-secret-key-for-testing-32bx");
        let hasher = PasswordHasher::new();
        let hash = hasher.hash("ValidPass@123").unwrap();
        let user = User {
            id: "u1".to_string(),
            username: "test".to_string(),
            password_hash: hash,
            role: "user".to_string(),
            email: None,
            created_at: None,
        };
        let result = mgr.add_user(user).await;
        assert!(result.is_ok(), "add_user should accept valid bcrypt hash");
    }

    /// vuln-0002 回归测试:add_user_unchecked 不验证 password_hash
    #[tokio::test]
    async fn test_vuln_0002_add_user_unchecked_skips_validation() {
        let mgr = AuthenticationManager::new(b"test-secret-key-for-testing-32bx");
        let user = User {
            id: "u1".to_string(),
            username: "test".to_string(),
            password_hash: "anything".to_string(),
            role: "user".to_string(),
            email: None,
            created_at: None,
        };
        let result = mgr.add_user_unchecked(user).await;
        assert!(
            result.is_ok(),
            "add_user_unchecked should skip validation for internal use"
        );
    }

    // ===== HD-3 测试:add_user vs add_user_unchecked 职责清晰分离 =====

    /// HD-3 测试:add_user 必须验证 bcrypt 格式(公共 API 职责)
    ///
    /// 同一无效 password_hash,add_user 必须拒绝,add_user_unchecked 必须接受。
    /// 验证两者职责清晰分离:公共 API 强制安全,内部 API 跳过验证。
    #[tokio::test]
    async fn test_hd3_add_user_vs_unchecked_role_separation() {
        let mgr = AuthenticationManager::new(b"test-secret-key-for-testing-32bx");

        // 构造无效 password_hash 的 User
        let make_user = || User {
            id: "u1".to_string(),
            username: "test_hd3".to_string(),
            password_hash: "invalid-not-bcrypt".to_string(),
            role: "user".to_string(),
            email: None,
            created_at: None,
        };

        // add_user(公共 API)必须拒绝无效 bcrypt 哈希
        let result = mgr.add_user(make_user()).await;
        assert!(
            matches!(result, Err(AuthError::PasswordHash(_))),
            "HD-3: add_user (public API) must reject invalid bcrypt hash"
        );

        // add_user_unchecked(pub(crate) 内部 API)跳过验证,接受任意 hash
        let result = mgr.add_user_unchecked(make_user()).await;
        assert!(
            result.is_ok(),
            "HD-3: add_user_unchecked (pub(crate) internal) should skip validation"
        );
    }

    /// HD-3 测试:add_user_unchecked 接受的 user 可被 get_user 读回
    ///
    /// 验证 pub(crate) 内部 API 写入的数据可被公共读取 API 读回,
    /// 确保迁移场景(如批量导入)的数据完整性。
    #[tokio::test]
    async fn test_hd3_add_user_unchecked_data_round_trip() {
        let mgr = AuthenticationManager::new(b"test-secret-key-for-testing-32bx");

        // 内部 API 写入(模拟迁移场景)
        let user = User {
            id: "migrated-1".to_string(),
            username: "migrated_user".to_string(),
            password_hash: "$2b$12$somefakebutnonemptyhashplaceholder012345678901234567890123456".to_string(),
            role: "user".to_string(),
            email: Some("migrated@example.com".to_string()),
            created_at: None,
        };
        mgr.add_user_unchecked(user)
            .await
            .expect("internal write should succeed");

        // 公共 API 读回
        let read = mgr
            .get_user("migrated_user")
            .await
            .expect("get_user should find migrated user");
        assert_eq!(read.id, "migrated-1");
        assert_eq!(read.role, "user");
        assert_eq!(read.email.as_deref(), Some("migrated@example.com"));
    }
}