metabase_api_rs/core/models/
user.rs

1//! User and permission models for Metabase user management
2//!
3//! This module provides data structures for working with
4//! Metabase users, groups, and permissions.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9use super::common::UserId;
10
11/// Unique identifier for a group
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub struct GroupId(pub i64);
14
15/// Unique identifier for a permission
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17pub struct PermissionId(pub i64);
18
19/// Represents a Metabase user
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub struct User {
22    /// Unique identifier for the user
23    pub id: UserId,
24
25    /// User's email address
26    pub email: String,
27
28    /// User's first name
29    pub first_name: String,
30
31    /// User's last name
32    pub last_name: String,
33
34    /// Common name (usually first_name + last_name)
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub common_name: Option<String>,
37
38    /// Whether the user is a superuser
39    #[serde(default)]
40    pub is_superuser: bool,
41
42    /// Whether the user is active
43    #[serde(default = "default_true")]
44    pub is_active: bool,
45
46    /// Whether the user is a Metabase internal user
47    #[serde(default)]
48    pub is_qbnewb: bool,
49
50    /// When the user joined
51    pub date_joined: DateTime<Utc>,
52
53    /// Last login time
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub last_login: Option<DateTime<Utc>>,
56
57    /// Groups the user belongs to
58    #[serde(default, skip_serializing_if = "Vec::is_empty")]
59    pub group_ids: Vec<GroupId>,
60
61    /// User's locale
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub locale: Option<String>,
64
65    /// Google auth enabled
66    #[serde(default)]
67    pub google_auth: bool,
68
69    /// LDAP auth enabled
70    #[serde(default)]
71    pub ldap_auth: bool,
72
73    // Additional fields from API specification
74    /// Login attributes (e.g., SAML, LDAP attributes)
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub login_attributes: Option<serde_json::Value>,
77
78    /// User group membership details
79    #[serde(default, skip_serializing_if = "Vec::is_empty")]
80    pub user_group_memberships: Vec<UserGroupMembership>,
81}
82
83/// Represents user group membership details
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85pub struct UserGroupMembership {
86    /// Group ID
87    pub id: GroupId,
88
89    /// Whether this is the default group
90    #[serde(default)]
91    pub is_default: bool,
92}
93
94/// Health check status
95#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
96pub struct HealthStatus {
97    /// Overall health status
98    pub status: String,
99
100    /// Metabase version
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub version: Option<String>,
103
104    /// Database connection status
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub database: Option<bool>,
107}
108
109/// Represents a user group
110#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
111pub struct Group {
112    /// Unique identifier for the group
113    pub id: GroupId,
114
115    /// Group name
116    pub name: String,
117
118    /// Group members (user IDs)
119    #[serde(default, skip_serializing_if = "Vec::is_empty")]
120    pub members: Vec<UserId>,
121}
122
123/// Represents a permission for a group
124#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
125pub struct Permission {
126    /// Unique identifier for the permission
127    pub id: PermissionId,
128
129    /// Permission object path (e.g., "/db/1/schema/PUBLIC/")
130    pub object: String,
131
132    /// Group this permission applies to
133    pub group_id: GroupId,
134}
135
136/// Permission graph for a group
137#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
138pub struct PermissionGraph {
139    /// Group ID
140    pub group_id: GroupId,
141
142    /// Native query permissions
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub native: Option<serde_json::Value>,
145
146    /// Schema permissions
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub schemas: Option<serde_json::Value>,
149}
150
151/// Request to create a new user
152#[derive(Debug, Clone, Serialize)]
153pub struct CreateUserRequest {
154    /// Email address
155    pub email: String,
156
157    /// First name
158    pub first_name: String,
159
160    /// Last name
161    pub last_name: String,
162
163    /// Password (required for email/password auth)
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub password: Option<String>,
166
167    /// Group IDs to add the user to
168    #[serde(default, skip_serializing_if = "Vec::is_empty")]
169    pub group_ids: Vec<GroupId>,
170
171    /// User's locale
172    #[serde(skip_serializing_if = "Option::is_none")]
173    pub locale: Option<String>,
174}
175
176/// Request to update a user
177#[derive(Debug, Clone, Default, Serialize)]
178pub struct UpdateUserRequest {
179    /// New email
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub email: Option<String>,
182
183    /// New first name
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub first_name: Option<String>,
186
187    /// New last name
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub last_name: Option<String>,
190
191    /// New password
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub password: Option<String>,
194
195    /// Whether user is active
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub is_active: Option<bool>,
198
199    /// Whether user is superuser
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub is_superuser: Option<bool>,
202
203    /// New group IDs
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub group_ids: Option<Vec<GroupId>>,
206
207    /// User's locale
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub locale: Option<String>,
210}
211
212/// Request to create a new group
213#[derive(Debug, Clone, Serialize)]
214pub struct CreateGroupRequest {
215    /// Group name
216    pub name: String,
217
218    /// Initial member IDs
219    #[serde(default, skip_serializing_if = "Vec::is_empty")]
220    pub members: Vec<UserId>,
221}
222
223/// Request to update a group
224#[derive(Debug, Clone, Default, Serialize)]
225pub struct UpdateGroupRequest {
226    /// New name
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub name: Option<String>,
229
230    /// New member list
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub members: Option<Vec<UserId>>,
233}
234
235/// Membership change request
236#[derive(Debug, Clone, Serialize)]
237pub struct MembershipRequest {
238    /// Group ID
239    pub group_id: GroupId,
240
241    /// User ID
242    pub user_id: UserId,
243}
244
245fn default_true() -> bool {
246    true
247}
248
249impl User {
250    /// Creates a new user builder
251    pub fn builder(
252        email: impl Into<String>,
253        first_name: impl Into<String>,
254        last_name: impl Into<String>,
255    ) -> UserBuilder {
256        UserBuilder::new(email, first_name, last_name)
257    }
258
259    /// Gets the user's full name
260    pub fn full_name(&self) -> String {
261        format!("{} {}", self.first_name, self.last_name)
262    }
263}
264
265/// Builder for creating User instances
266pub struct UserBuilder {
267    email: String,
268    first_name: String,
269    last_name: String,
270    password: Option<String>,
271    is_superuser: bool,
272    group_ids: Vec<GroupId>,
273    locale: Option<String>,
274}
275
276impl UserBuilder {
277    /// Creates a new user builder
278    pub fn new(
279        email: impl Into<String>,
280        first_name: impl Into<String>,
281        last_name: impl Into<String>,
282    ) -> Self {
283        Self {
284            email: email.into(),
285            first_name: first_name.into(),
286            last_name: last_name.into(),
287            password: None,
288            is_superuser: false,
289            group_ids: Vec::new(),
290            locale: None,
291        }
292    }
293
294    /// Sets the password
295    pub fn password(mut self, password: impl Into<String>) -> Self {
296        self.password = Some(password.into());
297        self
298    }
299
300    /// Sets whether the user is a superuser
301    pub fn superuser(mut self, is_superuser: bool) -> Self {
302        self.is_superuser = is_superuser;
303        self
304    }
305
306    /// Adds a group ID
307    pub fn add_group(mut self, group_id: GroupId) -> Self {
308        self.group_ids.push(group_id);
309        self
310    }
311
312    /// Sets the locale
313    pub fn locale(mut self, locale: impl Into<String>) -> Self {
314        self.locale = Some(locale.into());
315        self
316    }
317
318    /// Builds the User instance
319    pub fn build(self) -> User {
320        User {
321            id: UserId(0), // Will be set by the server
322            email: self.email,
323            first_name: self.first_name.clone(),
324            last_name: self.last_name.clone(),
325            common_name: Some(format!("{} {}", self.first_name, self.last_name)),
326            is_superuser: self.is_superuser,
327            is_active: true,
328            is_qbnewb: false,
329            date_joined: Utc::now(),
330            last_login: None,
331            group_ids: self.group_ids,
332            locale: self.locale,
333            google_auth: false,
334            ldap_auth: false,
335            login_attributes: None,
336            user_group_memberships: Vec::new(),
337        }
338    }
339
340    /// Builds a CreateUserRequest
341    pub fn build_request(self) -> CreateUserRequest {
342        CreateUserRequest {
343            email: self.email,
344            first_name: self.first_name,
345            last_name: self.last_name,
346            password: self.password,
347            group_ids: self.group_ids,
348            locale: self.locale,
349        }
350    }
351}
352
353impl Group {
354    /// Creates a new group builder
355    pub fn builder(name: impl Into<String>) -> GroupBuilder {
356        GroupBuilder::new(name)
357    }
358}
359
360/// Builder for creating Group instances
361pub struct GroupBuilder {
362    name: String,
363    members: Vec<UserId>,
364}
365
366impl GroupBuilder {
367    /// Creates a new group builder
368    pub fn new(name: impl Into<String>) -> Self {
369        Self {
370            name: name.into(),
371            members: Vec::new(),
372        }
373    }
374
375    /// Adds a member to the group
376    pub fn add_member(mut self, user_id: UserId) -> Self {
377        self.members.push(user_id);
378        self
379    }
380
381    /// Sets all members
382    pub fn members(mut self, members: Vec<UserId>) -> Self {
383        self.members = members;
384        self
385    }
386
387    /// Builds the Group instance
388    pub fn build(self) -> Group {
389        Group {
390            id: GroupId(0), // Will be set by the server
391            name: self.name,
392            members: self.members,
393        }
394    }
395
396    /// Builds a CreateGroupRequest
397    pub fn build_request(self) -> CreateGroupRequest {
398        CreateGroupRequest {
399            name: self.name,
400            members: self.members,
401        }
402    }
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408
409    #[test]
410    fn test_user_creation() {
411        let user = User::builder("john@example.com", "John", "Doe")
412            .password("secure123")
413            .superuser(false)
414            .add_group(GroupId(1))
415            .locale("en_US")
416            .build();
417
418        assert_eq!(user.email, "john@example.com");
419        assert_eq!(user.first_name, "John");
420        assert_eq!(user.last_name, "Doe");
421        assert_eq!(user.full_name(), "John Doe");
422        assert!(!user.is_superuser);
423        assert_eq!(user.group_ids.len(), 1);
424    }
425
426    #[test]
427    fn test_create_user_request() {
428        let request = User::builder("jane@example.com", "Jane", "Smith")
429            .password("password123")
430            .add_group(GroupId(2))
431            .build_request();
432
433        assert_eq!(request.email, "jane@example.com");
434        assert_eq!(request.first_name, "Jane");
435        assert_eq!(request.last_name, "Smith");
436        assert_eq!(request.password, Some("password123".to_string()));
437        assert_eq!(request.group_ids.len(), 1);
438    }
439
440    #[test]
441    fn test_group_creation() {
442        let group = Group::builder("Administrators")
443            .add_member(UserId(1))
444            .add_member(UserId(2))
445            .build();
446
447        assert_eq!(group.name, "Administrators");
448        assert_eq!(group.members.len(), 2);
449    }
450
451    #[test]
452    fn test_update_user_request() {
453        let request = UpdateUserRequest {
454            email: Some("newemail@example.com".to_string()),
455            is_active: Some(false),
456            ..Default::default()
457        };
458
459        assert_eq!(request.email, Some("newemail@example.com".to_string()));
460        assert_eq!(request.is_active, Some(false));
461        assert!(request.first_name.is_none());
462    }
463
464    #[test]
465    fn test_permission() {
466        let permission = Permission {
467            id: PermissionId(1),
468            object: "/db/1/schema/PUBLIC/".to_string(),
469            group_id: GroupId(1),
470        };
471
472        assert_eq!(permission.object, "/db/1/schema/PUBLIC/");
473        assert_eq!(permission.group_id, GroupId(1));
474    }
475}