Skip to main content

wecomx_auth/
credentials.rs

1//! 凭据存储抽象:[`CredentialStore`] trait 与内存实现。
2//!
3//! [`Credentials`] 为凭据总账:bot 信息与 Bearer token 共存,保证原子更新。
4//! 服务端 / 容器部署可经 [`CredentialStore`] 接入 KMS、Vault 或共享 Secret Store。
5
6use std::sync::{Arc, RwLock};
7
8use serde::{Deserialize, Serialize};
9
10use crate::bot::BotCredential;
11use crate::error::AuthError;
12
13/// 本地凭据总账:bot 信息与 Bearer token 共存,保证原子更新。
14#[derive(Debug, Clone, Default, Serialize, Deserialize)]
15pub struct Credentials {
16    pub bot: Option<BotCredential>,
17    pub token: Option<String>,
18}
19
20/// 凭据存储抽象。
21///
22/// 实现方按自身介质决定持久化方式(加密文件 / 内存 / KMS / Redis 等);
23/// `save` 必须整体覆盖写入(bot 与 token 原子更新)。
24pub trait CredentialStore: Send + Sync {
25    /// 读取凭据;无凭据时返回 `None`。
26    ///
27    /// # Errors
28    /// 存储介质故障(IO / 远端不可达)时返回 [`AuthError::Storage`];
29    /// 凭据缺失或解密失败不算错误(返回 `None`,与「未授权」表现一致)。
30    fn load(&self) -> Result<Option<Credentials>, AuthError>;
31
32    /// 整体覆盖写入凭据。
33    ///
34    /// # Errors
35    /// 写入失败时返回 [`AuthError::Storage`]。
36    fn save(&self, credentials: &Credentials) -> Result<(), AuthError>;
37
38    /// 清除凭据(无凭据时应为 no-op)。
39    ///
40    /// # Errors
41    /// 删除失败时返回 [`AuthError::Storage`]。
42    fn clear(&self) -> Result<(), AuthError>;
43}
44
45/// 内存凭据存储(进程内共享)。
46///
47/// 适用于:单实例部署从环境变量 / 挂载 Secret 注入凭据,或测试。
48#[derive(Debug, Clone, Default)]
49pub struct MemoryCredentialStore {
50    inner: Arc<RwLock<Option<Credentials>>>,
51}
52
53impl MemoryCredentialStore {
54    /// 创建空存储;`initial` 非空时预置凭据。
55    pub fn new(initial: Credentials) -> Self {
56        Self {
57            inner: Arc::new(RwLock::new(
58                Some(initial).filter(|c| c.bot.is_some() || c.token.is_some()),
59            )),
60        }
61    }
62
63    /// 进程内共享实例。
64    pub fn shared(self) -> Arc<Self> {
65        Arc::new(self)
66    }
67}
68
69impl CredentialStore for MemoryCredentialStore {
70    fn load(&self) -> Result<Option<Credentials>, AuthError> {
71        Ok(self.inner.read().unwrap_or_else(|e| e.into_inner()).clone())
72    }
73
74    fn save(&self, credentials: &Credentials) -> Result<(), AuthError> {
75        let value = Some(credentials.clone()).filter(|c| c.bot.is_some() || c.token.is_some());
76        *self.inner.write().unwrap_or_else(|e| e.into_inner()) = value;
77        Ok(())
78    }
79
80    fn clear(&self) -> Result<(), AuthError> {
81        *self.inner.write().unwrap_or_else(|e| e.into_inner()) = None;
82        Ok(())
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    //! ## 模块摘要:credentials(凭据总账与存储抽象)
89    //!
90    //! ### 关键接口
91    //! - [Credentials] — bot 信息与 Bearer token 共存的联合结构
92    //! - [CredentialStore] — 凭据存储抽象(load / save / clear)
93    //! - [MemoryCredentialStore] — 内存实现(进程内共享)
94    //!
95    //! ### 关键分支与异常路径
96    //! - bot 与 token 均空 → save 等价于 clear
97    //! - bot 与 token 可独立更新互不影响
98
99    use super::*;
100
101    fn bot(id: &str) -> BotCredential {
102        BotCredential::new(id.to_string(), "secret".into())
103    }
104
105    /// P0:bot + token 保存后可完整读回
106    #[test]
107    fn memory_store_roundtrip() {
108        let store = MemoryCredentialStore::new(Credentials::default());
109        assert!(store.load().unwrap().is_none());
110
111        let creds = Credentials {
112            bot: Some(bot("bot1")),
113            token: Some("tok-1".into()),
114        };
115        store.save(&creds).unwrap();
116        let loaded = store.load().unwrap().unwrap();
117        assert_eq!(loaded.bot.as_ref().map(|b| b.id.as_str()), Some("bot1"));
118        assert_eq!(loaded.token.as_deref(), Some("tok-1"));
119    }
120
121    /// P0:bot 与 token 均空时 save 等价 clear
122    #[test]
123    fn memory_store_save_empty_clears() {
124        let store = MemoryCredentialStore::new(Credentials {
125            bot: Some(bot("bot1")),
126            token: None,
127        });
128        store.save(&Credentials::default()).unwrap();
129        assert!(store.load().unwrap().is_none());
130    }
131
132    /// P0:clear 清空凭据;空存储 clear 为 no-op
133    #[test]
134    fn memory_store_clear() {
135        let store = MemoryCredentialStore::new(Credentials {
136            bot: Some(bot("bot1")),
137            token: Some("tok-1".into()),
138        });
139        store.clear().unwrap();
140        assert!(store.load().unwrap().is_none());
141        store.clear().unwrap();
142    }
143}