corevpn_core/
user.rs

1//! User management types
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7/// Unique user identifier
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct UserId(String);
10
11impl UserId {
12    /// Create a new user ID from string
13    pub fn new(id: impl Into<String>) -> Self {
14        Self(id.into())
15    }
16
17    /// Create from email (common for OAuth2)
18    pub fn from_email(email: &str) -> Self {
19        Self(email.to_lowercase())
20    }
21
22    /// Create a random user ID
23    pub fn random() -> Self {
24        Self(Uuid::new_v4().to_string())
25    }
26
27    /// Get the ID as a string
28    pub fn as_str(&self) -> &str {
29        &self.0
30    }
31}
32
33impl std::fmt::Display for UserId {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        write!(f, "{}", self.0)
36    }
37}
38
39impl From<String> for UserId {
40    fn from(s: String) -> Self {
41        Self(s)
42    }
43}
44
45impl From<&str> for UserId {
46    fn from(s: &str) -> Self {
47        Self(s.to_string())
48    }
49}
50
51/// User role for access control
52#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
53pub enum UserRole {
54    /// Full access to all networks
55    Admin,
56    /// Standard VPN access
57    #[default]
58    User,
59    /// Limited access (specific routes only)
60    Limited,
61    /// Read-only (monitoring only, no VPN access)
62    ReadOnly,
63    /// Custom role with specific permissions
64    Custom(String),
65}
66
67/// VPN User
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct User {
70    /// Unique user ID
71    pub id: UserId,
72    /// Email address
73    pub email: Option<String>,
74    /// Display name
75    pub name: Option<String>,
76    /// User role
77    pub role: UserRole,
78    /// Whether user is enabled
79    pub enabled: bool,
80    /// OAuth2 provider (if using OAuth2)
81    pub oauth_provider: Option<String>,
82    /// OAuth2 subject claim
83    pub oauth_subject: Option<String>,
84    /// Groups the user belongs to
85    pub groups: Vec<String>,
86    /// Static VPN IP (if configured)
87    pub static_ip: Option<crate::VpnAddress>,
88    /// Custom routes for this user
89    pub custom_routes: Vec<crate::Route>,
90    /// Maximum concurrent sessions
91    pub max_sessions: u32,
92    /// User creation time
93    pub created_at: DateTime<Utc>,
94    /// Last login time
95    pub last_login: Option<DateTime<Utc>>,
96    /// Account expiration (if set)
97    pub expires_at: Option<DateTime<Utc>>,
98    /// Additional metadata
99    pub metadata: std::collections::HashMap<String, String>,
100}
101
102impl User {
103    /// Create a new user
104    pub fn new(id: UserId) -> Self {
105        Self {
106            id,
107            email: None,
108            name: None,
109            role: UserRole::default(),
110            enabled: true,
111            oauth_provider: None,
112            oauth_subject: None,
113            groups: vec![],
114            static_ip: None,
115            custom_routes: vec![],
116            max_sessions: 3,
117            created_at: Utc::now(),
118            last_login: None,
119            expires_at: None,
120            metadata: std::collections::HashMap::new(),
121        }
122    }
123
124    /// Create from OAuth2 claims
125    pub fn from_oauth(
126        provider: &str,
127        subject: &str,
128        email: Option<&str>,
129        name: Option<&str>,
130        groups: Vec<String>,
131    ) -> Self {
132        let id = email
133            .map(UserId::from_email)
134            .unwrap_or_else(|| UserId::new(format!("{}:{}", provider, subject)));
135
136        Self {
137            id,
138            email: email.map(String::from),
139            name: name.map(String::from),
140            role: UserRole::User,
141            enabled: true,
142            oauth_provider: Some(provider.to_string()),
143            oauth_subject: Some(subject.to_string()),
144            groups,
145            static_ip: None,
146            custom_routes: vec![],
147            max_sessions: 3,
148            created_at: Utc::now(),
149            last_login: Some(Utc::now()),
150            expires_at: None,
151            metadata: std::collections::HashMap::new(),
152        }
153    }
154
155    /// Check if user is expired
156    pub fn is_expired(&self) -> bool {
157        self.expires_at
158            .map(|exp| Utc::now() > exp)
159            .unwrap_or(false)
160    }
161
162    /// Check if user can connect
163    pub fn can_connect(&self) -> bool {
164        self.enabled && !self.is_expired()
165    }
166
167    /// Check if user is in a specific group
168    pub fn in_group(&self, group: &str) -> bool {
169        self.groups.iter().any(|g| g == group)
170    }
171
172    /// Check if user has admin role
173    pub fn is_admin(&self) -> bool {
174        matches!(self.role, UserRole::Admin)
175    }
176
177    /// Record login
178    pub fn record_login(&mut self) {
179        self.last_login = Some(Utc::now());
180    }
181
182    /// Set email
183    pub fn with_email(mut self, email: &str) -> Self {
184        self.email = Some(email.to_string());
185        self
186    }
187
188    /// Set name
189    pub fn with_name(mut self, name: &str) -> Self {
190        self.name = Some(name.to_string());
191        self
192    }
193
194    /// Set role
195    pub fn with_role(mut self, role: UserRole) -> Self {
196        self.role = role;
197        self
198    }
199
200    /// Add to group
201    pub fn add_group(&mut self, group: &str) {
202        if !self.in_group(group) {
203            self.groups.push(group.to_string());
204        }
205    }
206
207    /// Remove from group
208    pub fn remove_group(&mut self, group: &str) {
209        self.groups.retain(|g| g != group);
210    }
211}
212
213/// User store trait for different backends
214#[async_trait::async_trait]
215pub trait UserStore: Send + Sync {
216    /// Get user by ID
217    async fn get_user(&self, id: &UserId) -> Option<User>;
218
219    /// Get user by email
220    async fn get_user_by_email(&self, email: &str) -> Option<User>;
221
222    /// Get user by OAuth2 subject
223    async fn get_user_by_oauth(&self, provider: &str, subject: &str) -> Option<User>;
224
225    /// Create or update user
226    async fn upsert_user(&self, user: &User) -> crate::Result<()>;
227
228    /// Delete user
229    async fn delete_user(&self, id: &UserId) -> crate::Result<()>;
230
231    /// List all users
232    async fn list_users(&self) -> Vec<User>;
233
234    /// Get users in a group
235    async fn get_users_in_group(&self, group: &str) -> Vec<User>;
236}
237
238/// In-memory user store (for testing/simple deployments)
239pub struct MemoryUserStore {
240    users: parking_lot::RwLock<std::collections::HashMap<UserId, User>>,
241}
242
243impl MemoryUserStore {
244    /// Create a new in-memory user store
245    pub fn new() -> Self {
246        Self {
247            users: parking_lot::RwLock::new(std::collections::HashMap::new()),
248        }
249    }
250}
251
252impl Default for MemoryUserStore {
253    fn default() -> Self {
254        Self::new()
255    }
256}
257
258#[async_trait::async_trait]
259impl UserStore for MemoryUserStore {
260    async fn get_user(&self, id: &UserId) -> Option<User> {
261        self.users.read().get(id).cloned()
262    }
263
264    async fn get_user_by_email(&self, email: &str) -> Option<User> {
265        let email_lower = email.to_lowercase();
266        self.users
267            .read()
268            .values()
269            .find(|u| u.email.as_ref().map(|e| e.to_lowercase()) == Some(email_lower.clone()))
270            .cloned()
271    }
272
273    async fn get_user_by_oauth(&self, provider: &str, subject: &str) -> Option<User> {
274        self.users
275            .read()
276            .values()
277            .find(|u| {
278                u.oauth_provider.as_deref() == Some(provider)
279                    && u.oauth_subject.as_deref() == Some(subject)
280            })
281            .cloned()
282    }
283
284    async fn upsert_user(&self, user: &User) -> crate::Result<()> {
285        self.users.write().insert(user.id.clone(), user.clone());
286        Ok(())
287    }
288
289    async fn delete_user(&self, id: &UserId) -> crate::Result<()> {
290        self.users.write().remove(id);
291        Ok(())
292    }
293
294    async fn list_users(&self) -> Vec<User> {
295        self.users.read().values().cloned().collect()
296    }
297
298    async fn get_users_in_group(&self, group: &str) -> Vec<User> {
299        self.users
300            .read()
301            .values()
302            .filter(|u| u.in_group(group))
303            .cloned()
304            .collect()
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    #[test]
313    fn test_user_creation() {
314        let user = User::new(UserId::from_email("test@example.com"))
315            .with_email("test@example.com")
316            .with_name("Test User")
317            .with_role(UserRole::Admin);
318
319        assert!(user.is_admin());
320        assert!(user.can_connect());
321        assert_eq!(user.email, Some("test@example.com".to_string()));
322    }
323
324    #[test]
325    fn test_user_groups() {
326        let mut user = User::new(UserId::new("test"));
327
328        user.add_group("developers");
329        user.add_group("vpn-users");
330
331        assert!(user.in_group("developers"));
332        assert!(user.in_group("vpn-users"));
333        assert!(!user.in_group("admins"));
334
335        user.remove_group("developers");
336        assert!(!user.in_group("developers"));
337    }
338
339    #[tokio::test]
340    async fn test_memory_user_store() {
341        let store = MemoryUserStore::new();
342
343        let user = User::new(UserId::from_email("test@example.com"))
344            .with_email("test@example.com");
345
346        store.upsert_user(&user).await.unwrap();
347
348        let found = store.get_user_by_email("test@example.com").await;
349        assert!(found.is_some());
350        assert_eq!(found.unwrap().id, user.id);
351    }
352}