Skip to main content

webgates_core/
accounts.rs

1//! Account types for representing the current user in authorization flows.
2//!
3//! This module provides [`Account`], the central domain type used throughout
4//! `webgates-core`.
5//!
6//! If you are onboarding to the crate, this is usually the first type to learn.
7//! An account brings together the data that authorization decisions care about:
8//!
9//! - your application-level user identifier
10//! - assigned roles
11//! - group membership
12//! - directly granted permissions
13//!
14//! # Quick start
15//!
16//! ```rust
17//! use webgates_core::accounts::Account;
18//! use webgates_core::groups::Group;
19//! use webgates_core::permissions::Permissions;
20//! use webgates_core::roles::Role;
21//!
22//! let account = Account::<Role, Group>::new("user@example.com")
23//!     .with_groups(vec![Group::new("engineering")])
24//!     .with_permissions(Permissions::from_iter(["read:api", "write:docs"]));
25//! ```
26
27use crate::authz::access_hierarchy::AccessHierarchy;
28use crate::permissions::Permissions;
29use crate::permissions::permission_id::PermissionId;
30use serde::{Deserialize, Serialize};
31use uuid::Uuid;
32
33/// Authorization-relevant information about a user or principal.
34///
35/// `Account` is the main input to authorization checks in `webgates-core`.
36/// It stores the user identity plus the roles, groups, and direct permissions
37/// that a policy may evaluate.
38///
39/// In most applications, you create or load an account during authentication,
40/// then pass it into an authorization service when a protected operation is
41/// requested.
42///
43/// # Creating Accounts
44///
45/// ```rust
46/// use webgates_core::accounts::Account;
47/// use webgates_core::groups::Group;
48/// use webgates_core::permissions::Permissions;
49/// use webgates_core::roles::Role;
50///
51/// let account = Account::<Role, Group>::new("user123");
52///
53/// let permissions: Permissions = ["read:profile", "write:profile"].into_iter().collect();
54/// let account = Account::<Role, Group>::new("admin@example.com")
55///     .with_roles(vec![Role::Admin])
56///     .with_groups(vec![Group::new("staff")])
57///     .with_permissions(permissions);
58/// ```
59///
60/// # Working with Permissions
61///
62/// ```rust
63/// # use webgates_core::accounts::Account;
64/// # use webgates_core::groups::Group;
65/// # use webgates_core::permissions::permission_id::PermissionId;
66/// # use webgates_core::roles::Role;
67/// # let mut account = Account::<Role, Group>::new("user");
68/// account.grant_permission("read:api");
69/// account.grant_permission(PermissionId::from("write:api"));
70///
71/// if account.permissions.has("read:api") {
72///     println!("User can read API");
73/// }
74///
75/// account.revoke_permission("write:api");
76/// ```
77#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
78pub struct Account<R, G>
79where
80    R: AccessHierarchy + Eq,
81    G: Eq + Clone,
82{
83    /// Stable unique identifier for the account.
84    ///
85    /// This UUID is generated when the account is created. Applications can use
86    /// it as the durable internal identifier that links account state to other
87    /// records such as credentials, profiles, or repository entries.
88    pub account_id: Uuid,
89    /// Application-level user identifier, such as an email address or username.
90    ///
91    /// This is typically the identifier a person uses to log in and the one your
92    /// application uses to look up the account.
93    pub user_id: String,
94    /// Roles assigned to this account.
95    ///
96    /// Roles usually represent broad privilege levels such as user, moderator,
97    /// or admin. When the role type implements [`AccessHierarchy`], higher roles
98    /// can satisfy lower-role requirements when a policy allows supervisor access.
99    pub roles: Vec<R>,
100    /// Groups this account belongs to.
101    ///
102    /// Groups model exact membership such as departments, tenants, project teams,
103    /// or support rotations.
104    pub groups: Vec<G>,
105    /// Directly granted permissions for this account.
106    ///
107    /// Use direct permissions when roles or groups are too broad and you need
108    /// feature-level or action-level access control.
109    pub permissions: Permissions,
110}
111
112impl<R, G> Account<R, G>
113where
114    R: AccessHierarchy + Eq + Clone + Default,
115    G: Eq + Clone,
116{
117    /// Creates a new account for the given user identifier.
118    ///
119    /// A fresh UUID is generated automatically. The new account starts with:
120    ///
121    /// - the default role for `R`
122    /// - no groups
123    /// - no direct permissions
124    ///
125    /// With the built-in [`crate::roles::Role`] type, the default role is
126    /// `Role::User`.
127    ///
128    /// # Parameters
129    /// - `user_id`: Unique identifier for the user, such as an email or username.
130    ///
131    /// # Examples
132    /// ```rust
133    /// use webgates_core::accounts::Account;
134    /// use webgates_core::groups::Group;
135    /// use webgates_core::roles::Role;
136    ///
137    /// let account = Account::<Role, Group>::new("user@example.com");
138    ///
139    /// assert_eq!(account.user_id, "user@example.com");
140    /// assert_eq!(account.roles, vec![Role::User]);
141    /// assert!(account.groups.is_empty());
142    /// ```
143    pub fn new(user_id: &str) -> Self {
144        Self {
145            account_id: Uuid::now_v7(),
146            user_id: user_id.to_string(),
147            groups: Vec::new(),
148            roles: vec![R::default()],
149            permissions: Permissions::new(),
150        }
151    }
152
153    /// Returns this account with the provided roles.
154    ///
155    /// This is useful when building an account that should not keep the single
156    /// default role assigned by [`Self::new`].
157    ///
158    /// # Example
159    /// ```rust
160    /// use webgates_core::accounts::Account;
161    /// use webgates_core::groups::Group;
162    /// use webgates_core::roles::Role;
163    ///
164    /// let account = Account::<Role, Group>::new("user@example.com")
165    ///     .with_roles(vec![Role::Admin]);
166    ///
167    /// assert!(account.has_role(&Role::Admin));
168    /// assert!(!account.has_role(&Role::User));
169    /// ```
170    pub fn with_roles(self, roles: Vec<R>) -> Self {
171        Self { roles, ..self }
172    }
173
174    /// Returns this account with the provided groups.
175    ///
176    /// This is useful when building an account with initial group membership.
177    ///
178    /// # Example
179    /// ```rust
180    /// use webgates_core::accounts::Account;
181    /// use webgates_core::groups::Group;
182    /// use webgates_core::roles::Role;
183    ///
184    /// let account = Account::<Role, Group>::new("user@example.com")
185    ///     .with_groups(vec![Group::new("engineering")]);
186    ///
187    /// assert!(account.is_member_of(&Group::new("engineering")));
188    /// assert!(!account.is_member_of(&Group::new("marketing")));
189    /// ```
190    pub fn with_groups(self, groups: Vec<G>) -> Self {
191        Self { groups, ..self }
192    }
193
194    /// Returns this account with the provided direct permissions.
195    ///
196    /// This is useful when building an account that starts with a known
197    /// permission set.
198    ///
199    /// # Example
200    /// ```rust
201    /// use webgates_core::accounts::Account;
202    /// use webgates_core::groups::Group;
203    /// use webgates_core::permissions::Permissions;
204    /// use webgates_core::roles::Role;
205    ///
206    /// let permissions: Permissions = ["read:profile", "write:profile"].into_iter().collect();
207    /// let account = Account::<Role, Group>::new("user@example.com")
208    ///     .with_permissions(permissions);
209    /// ```
210    pub fn with_permissions(self, permissions: Permissions) -> Self {
211        Self {
212            permissions,
213            ..self
214        }
215    }
216
217    /// Grants a direct permission to this account.
218    ///
219    /// # Example
220    /// ```rust
221    /// use webgates_core::accounts::Account;
222    /// use webgates_core::groups::Group;
223    /// use webgates_core::permissions::permission_id::PermissionId;
224    /// use webgates_core::roles::Role;
225    ///
226    /// let mut account = Account::<Role, Group>::new("user");
227    /// account.grant_permission("read:profile");
228    /// account.grant_permission(PermissionId::from("write:profile"));
229    /// ```
230    pub fn grant_permission<P>(&mut self, permission: P)
231    where
232        P: Into<PermissionId>,
233    {
234        self.permissions.grant(permission);
235    }
236
237    /// Revokes a direct permission from this account.
238    ///
239    /// # Example
240    /// ```rust
241    /// use webgates_core::accounts::Account;
242    /// use webgates_core::groups::Group;
243    /// use webgates_core::permissions::permission_id::PermissionId;
244    /// use webgates_core::roles::Role;
245    ///
246    /// let mut account = Account::<Role, Group>::new("user");
247    /// account.grant_permission("write:profile");
248    /// account.revoke_permission(PermissionId::from("write:profile"));
249    /// ```
250    pub fn revoke_permission<P>(&mut self, permission: P)
251    where
252        P: Into<PermissionId>,
253    {
254        self.permissions.revoke(permission);
255    }
256
257    /// Returns `true` when this account has the given role.
258    ///
259    /// # Example
260    ///
261    /// ```rust
262    /// use webgates_core::accounts::Account;
263    /// use webgates_core::groups::Group;
264    /// use webgates_core::roles::Role;
265    ///
266    /// let account = Account::<Role, Group>::new("user@example.com");
267    ///
268    /// assert!(account.has_role(&Role::User));
269    /// assert!(!account.has_role(&Role::Admin));
270    /// ```
271    pub fn has_role(&self, role: &R) -> bool {
272        self.roles.contains(role)
273    }
274
275    /// Returns `true` when this account belongs to the given group.
276    ///
277    /// # Example
278    ///
279    /// ```rust
280    /// use webgates_core::accounts::Account;
281    /// use webgates_core::groups::Group;
282    /// use webgates_core::roles::Role;
283    ///
284    /// let mut account = Account::<Role, Group>::new("user@example.com");
285    /// account.groups.push(Group::new("engineering"));
286    ///
287    /// assert!(account.is_member_of(&Group::new("engineering")));
288    /// assert!(!account.is_member_of(&Group::new("marketing")));
289    /// ```
290    pub fn is_member_of(&self, group: &G) -> bool {
291        self.groups.contains(group)
292    }
293
294    /// Returns `true` when this account has the specified direct permission.
295    ///
296    /// Accepts any type that converts into [`PermissionId`], such as `&str` or
297    /// `PermissionId` itself.
298    ///
299    /// # Example
300    ///
301    /// ```rust
302    /// use webgates_core::accounts::Account;
303    /// use webgates_core::groups::Group;
304    /// use webgates_core::permissions::permission_id::PermissionId;
305    /// use webgates_core::roles::Role;
306    ///
307    /// let mut account = Account::<Role, Group>::new("user@example.com");
308    /// account.grant_permission("read:api");
309    /// account.grant_permission(PermissionId::from("write:docs"));
310    ///
311    /// assert!(account.has_permission("read:api"));
312    /// assert!(account.has_permission(PermissionId::from("write:docs")));
313    /// assert!(!account.has_permission("admin:system"));
314    /// ```
315    pub fn has_permission<P>(&self, permission: P) -> bool
316    where
317        P: Into<PermissionId>,
318    {
319        self.permissions.has(permission)
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::Account;
326    use crate::groups::Group;
327    use crate::permissions::Permissions;
328    use crate::roles::Role;
329
330    #[test]
331    fn new_uses_default_role_and_empty_groups() {
332        let account = Account::<Role, Group>::new("user@example.com");
333
334        assert_eq!(account.user_id, "user@example.com");
335        assert_eq!(account.roles, vec![Role::User]);
336        assert!(account.groups.is_empty());
337        assert!(account.permissions.is_empty());
338    }
339
340    #[test]
341    fn with_roles_replaces_default_role_set() {
342        let account = Account::<Role, Group>::new("user@example.com").with_roles(vec![Role::Admin]);
343
344        assert_eq!(account.roles, vec![Role::Admin]);
345    }
346
347    #[test]
348    fn with_groups_replaces_group_set() {
349        let groups = vec![Group::new("engineering"), Group::new("backend-team")];
350        let account = Account::<Role, Group>::new("user@example.com").with_groups(groups.clone());
351
352        assert_eq!(account.groups, groups);
353    }
354
355    #[test]
356    fn with_permissions_replaces_permission_set() {
357        let permissions = Permissions::from_iter(["read:api", "write:api"]);
358
359        let account = Account::<Role, Group>::new("user@example.com").with_permissions(permissions);
360
361        assert!(account.has_permission("read:api"));
362        assert!(account.has_permission("write:api"));
363    }
364
365    #[test]
366    fn grant_and_revoke_permission_update_account_permissions() {
367        let mut account = Account::<Role, Group>::new("user@example.com");
368
369        account.grant_permission("read:api");
370        assert!(account.has_permission("read:api"));
371
372        account.revoke_permission("read:api");
373        assert!(!account.has_permission("read:api"));
374    }
375
376    #[test]
377    fn role_and_group_queries_reflect_membership() {
378        let mut account = Account::<Role, Group>::new("user@example.com");
379        account.roles = vec![Role::Admin];
380        account.groups = vec![Group::new("engineering")];
381
382        assert!(account.has_role(&Role::Admin));
383        assert!(!account.has_role(&Role::User));
384        assert!(account.is_member_of(&Group::new("engineering")));
385        assert!(!account.is_member_of(&Group::new("marketing")));
386    }
387}